neo4j_bolt 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c587989e6936680692675035dada5d220122cc81655f2607b97e72b4725b23b
4
- data.tar.gz: 6bedef9539b66644474e2d657b1e4454918a7bfe4a7db73cdfc626fb9b150426
3
+ metadata.gz: 38ed2e6727f1bc4baccc838b195d72a2392bbb82788598d704a1e81bd0f20de0
4
+ data.tar.gz: d781a67c3eee09dd9e32a9af60fe1fb65a61bb8afddb96307dd897cbcd99ddf1
5
5
  SHA512:
6
- metadata.gz: 9640636068e101464e9a92e0ca38e465bc3c847863b457d90ae3e5361c37960359e7fd9c24ec8915f1ad27f90311b0867b95973801e28a17939f2bf70b3bad75
7
- data.tar.gz: ed3142cd503626d3ba4d04ea86a708426178a070513c14325e46bac405bfa384250f8978d12cf6eb72920bb35ec7170cbb3ecd90500de121f6c650279387313e
6
+ metadata.gz: e1d4c318c5adb68878da6368dc8d694a63c79cc953ab0e87ffe5aceaf77ce9d47d06b35a80cef8ea835ac90e6753974a59c2bb2bd9088f144fdf8761976b9b8a
7
+ data.tar.gz: d28fd8341cbbfe89391c7685331ed8fc666864edb7222f8ccb3530ddd3e93b2bad68d9fa6ebdf75049cad99c8d68926b356e3b55fadcb795e017dfc0be9d0f22
data/README.md CHANGED
@@ -157,7 +157,14 @@ File.open("database.dump", "r") do |io|
157
157
  end
158
158
  ```
159
159
 
160
- Passing `progress_io:` is optional for the Ruby API. The CLI always reports dump/load progress on stderr, so dump data written to stdout remains safe to redirect or pipe. Terminal progress is updated in place; redirected stderr receives periodic progress lines.
160
+ Passing `progress_io:` is optional for the Ruby API. The CLI always reports dump/load progress on stderr, so dump data written to stdout remains safe to redirect or pipe. In `auto` mode a terminal gets a colored, in-place progress bar while redirected stderr receives periodic plain-text progress lines. Set `NEO4J_BOLT_PROGRESS=pretty` to force the terminal display through a container or other wrapper that hides the TTY, or `NEO4J_BOLT_PROGRESS=plain` to force log-friendly output. `NO_COLOR` disables colors without disabling the in-place display.
161
+
162
+ Both `dump` and `load` accept `--color COLOR` to choose the spinner and filled progress-bar accent. The default is `cyan`. The Ruby API exposes the same setting as `progress_color:`. Available colors are `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `bright-red`, `bright-green`, `bright-yellow`, `bright-blue`, `bright-magenta`, `bright-cyan`, and `bright-white`.
163
+
164
+ ```bash
165
+ neo4j_bolt dump --color magenta -o database.dump
166
+ neo4j_bolt load --color bright-blue database.dump
167
+ ```
161
168
 
162
169
  The `0, 1, 2, ...` IDs are synthetic dump-local IDs. Database-internal IDs and element IDs are never written to the persistent format. Dumping keeps the count, node, and relationship reads in one transaction, so the driver-provided entity identity used to connect the two streamed result sets stays within Neo4j's transaction-scoped identity guarantee. To keep dump numbering deterministic, ordering uses `elementId()` on modern Neo4j and an internal `id()` fallback only on Neo4j 4.4, where `elementId()` does not exist. Old Neo4jBolt dumps remain loadable.
163
170
 
@@ -175,8 +182,8 @@ The `neo4j_bolt` executable retains these commands:
175
182
  | --- | --- |
176
183
  | `neo4j_bolt console` | Open an IRB console with Neo4jBolt loaded |
177
184
  | `neo4j_bolt clear --srsly` | Delete all nodes and relationships |
178
- | `neo4j_bolt dump` | Write the textual database dump |
179
- | `neo4j_bolt load [--force] [--batch-size N] PATH` | Load a textual dump |
185
+ | `neo4j_bolt dump [--color COLOR]` | Write the textual database dump |
186
+ | `neo4j_bolt load [--force] [--batch-size N] [--color COLOR] PATH` | Load a textual dump |
180
187
  | `neo4j_bolt index ls` | List constraints and indexes |
181
188
  | `neo4j_bolt index rm --force` | Remove all constraints and indexes |
182
189
  | `neo4j_bolt visualize` | Generate a GraphViz document |
data/bin/neo4j_bolt CHANGED
@@ -47,9 +47,10 @@ class App
47
47
  long_desc 'Dump all nodes and relationships.'
48
48
  command :dump do |c|
49
49
  c.flag [:o, 'out-file'.to_sym], :default_value => '/dev/stdout'
50
+ c.flag [:color], :default_value => 'cyan', :desc => 'progress accent color'
50
51
  c.action do |global_options, options|
51
52
  File.open(options['out-file'.to_sym], 'w') do |f|
52
- dump_database(f, progress_io: $stderr)
53
+ dump_database(f, progress_io: $stderr, progress_color: options[:color])
53
54
  end
54
55
  end
55
56
  end
@@ -63,6 +64,7 @@ class App
63
64
  c.switch [:f, :force], :default_value => false, :desc => 'force appending nodes even if the database is not empty'
64
65
  c.flag [:b, 'batch-size'.to_sym], :default_value => Neo4jBolt::LOAD_INITIAL_BATCH_SIZE,
65
66
  :desc => 'initial batch size; automatically reduced on transaction-memory errors'
67
+ c.flag [:color], :default_value => 'cyan', :desc => 'progress accent color'
66
68
  c.action do |global_options, options, args|
67
69
  help_now!('input path is required') if args.empty?
68
70
  path = args.shift
@@ -71,6 +73,7 @@ class App
71
73
  f,
72
74
  force_append: options[:force],
73
75
  progress_io: $stderr,
76
+ progress_color: options[:color],
74
77
  initial_batch_size: options['batch-size'.to_sym]
75
78
  )
76
79
  end
@@ -1,3 +1,3 @@
1
1
  module Neo4jBolt
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.3"
3
3
  end
data/lib/neo4j_bolt.rb CHANGED
@@ -23,70 +23,207 @@ module Neo4jBolt
23
23
 
24
24
  class ProgressReporter
25
25
  UPDATE_INTERVAL = 0.5
26
-
27
- def initialize(io)
26
+ BAR_WIDTH = 24
27
+ SPINNER = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
28
+ ANSI = {
29
+ reset: "\e[0m",
30
+ bold: "\e[1m",
31
+ dim: "\e[2m",
32
+ red: "\e[31m",
33
+ green: "\e[32m",
34
+ yellow: "\e[33m",
35
+ blue: "\e[34m",
36
+ magenta: "\e[35m",
37
+ cyan: "\e[36m",
38
+ white: "\e[37m",
39
+ bright_red: "\e[91m",
40
+ bright_green: "\e[92m",
41
+ bright_yellow: "\e[93m",
42
+ bright_blue: "\e[94m",
43
+ bright_magenta: "\e[95m",
44
+ bright_cyan: "\e[96m",
45
+ bright_white: "\e[97m"
46
+ }.freeze
47
+ ACCENT_STYLES = {
48
+ "red" => :red, "green" => :green, "yellow" => :yellow, "blue" => :blue,
49
+ "magenta" => :magenta, "cyan" => :cyan, "white" => :white,
50
+ "bright-red" => :bright_red, "bright-green" => :bright_green,
51
+ "bright-yellow" => :bright_yellow, "bright-blue" => :bright_blue,
52
+ "bright-magenta" => :bright_magenta, "bright-cyan" => :bright_cyan,
53
+ "bright-white" => :bright_white
54
+ }.freeze
55
+
56
+ def initialize(io, color: "cyan")
28
57
  @io = io
29
- @tty = io && io.respond_to?(:tty?) && io.tty? && Neo4jBolt.bolt_verbosity.to_i.zero?
58
+ @accent_style = ACCENT_STYLES.fetch(color.to_s) do
59
+ raise ArgumentError, "progress color must be one of: #{ACCENT_STYLES.keys.join(', ')}"
60
+ end
61
+ mode = ENV.fetch("NEO4J_BOLT_PROGRESS", "auto")
62
+ unless %w[auto pretty plain].include?(mode)
63
+ raise ArgumentError, "NEO4J_BOLT_PROGRESS must be auto, pretty, or plain"
64
+ end
65
+
66
+ detected_tty = io && io.respond_to?(:tty?) && io.tty? && Neo4jBolt.bolt_verbosity.to_i.zero?
67
+ @pretty = io && (mode == "pretty" || (mode == "auto" && detected_tty))
68
+ @color = @pretty && !ENV.key?("NO_COLOR") && (mode == "pretty" || ENV["TERM"] != "dumb")
30
69
  @last_update_at = nil
31
- @line_width = 0
32
70
  @line_open = false
71
+ @spinner_index = 0
33
72
  end
34
73
 
35
- def update(message, force: false)
74
+ def update(message, force: false, percent: nil)
36
75
  return unless @io
37
76
 
38
77
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
78
  return if !force && @last_update_at && now - @last_update_at < UPDATE_INTERVAL
40
79
 
41
- if @tty
42
- padding = [@line_width - message.length, 0].max
43
- @io.print "\r#{message}#{' ' * padding}"
44
- @io.flush
45
- @line_width = [@line_width, message.length].max
46
- @line_open = true
80
+ if @pretty
81
+ percent ||= extract_percent(message)
82
+ render_line(activity_line(message, percent))
47
83
  else
48
84
  @io.puts message
49
85
  end
50
86
  @last_update_at = now
51
87
  end
52
88
 
53
- def note(message)
89
+ def note(message, level: :info)
54
90
  return unless @io
55
91
 
56
- clear_tty_line
57
- @io.puts message
58
- @last_update_at = nil
92
+ if @pretty && level == :info
93
+ update(message, force: true)
94
+ else
95
+ clear_line
96
+ if @pretty
97
+ icon = level == :warning ? color(:yellow, "!") : color(:cyan, "•")
98
+ @io.puts "#{icon} #{terminal_text(message)}"
99
+ else
100
+ @io.puts message
101
+ end
102
+ @last_update_at = nil
103
+ end
59
104
  end
60
105
 
61
106
  def finish(message)
62
107
  return unless @io
63
108
 
64
- update(message, force: true)
65
- if @tty
66
- @io.puts
67
- @line_width = 0
109
+ if @pretty
110
+ clear_line
111
+ label, detail = terminal_parts(message)
112
+ line = "#{color(:green, '✓')} #{color(:bold, label)}"
113
+ line += " #{detail}" unless detail.empty?
114
+ @io.puts line
68
115
  @line_open = false
116
+ else
117
+ @io.puts message
69
118
  end
119
+ @last_update_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
120
  end
71
121
 
72
122
  def close
73
- return unless @io && @tty && @line_open
123
+ return unless @io && @pretty && @line_open
74
124
 
75
- @io.puts
76
- @line_width = 0
77
- @line_open = false
125
+ clear_line
78
126
  end
79
127
 
80
128
  private
81
129
 
82
- def clear_tty_line
83
- return unless @tty && @line_open
130
+ def activity_line(message, percent)
131
+ label, detail = terminal_parts(message, strip_percent: !percent.nil?)
132
+ spinner = SPINNER[@spinner_index % SPINNER.length]
133
+ @spinner_index += 1
134
+
135
+ parts = [color(@accent_style, spinner), color(:bold, label)]
136
+ if percent
137
+ percent = [[percent.to_i, 0].max, 100].min
138
+ parts << progress_bar(percent)
139
+ parts << color(:bold, format("%3d%%", percent))
140
+ end
141
+ parts << detail unless detail.empty?
142
+ parts.join(" ")
143
+ end
144
+
145
+ def progress_bar(percent)
146
+ filled = (percent * BAR_WIDTH / 100.0).round
147
+ empty = BAR_WIDTH - filled
148
+ "#{color(@accent_style, '━' * filled)}#{color(:dim, '─' * empty)}"
149
+ end
150
+
151
+ def terminal_parts(message, strip_percent: false)
152
+ text = message.sub(/\.\.\.\z/, "")
153
+ label, detail = text.split(": ", 2)
154
+ label = pretty_label(label)
155
+ detail ||= ""
156
+ if strip_percent
157
+ detail = detail.sub(/\s+\(\d+%\)/, "")
158
+ detail = "" if detail.match?(/\A\d+%\z/)
159
+ end
160
+ detail = detail.gsub(/ \[batch (\d+)\]/) do
161
+ " #{color(:dim, '·')} batch #{format_count(Regexp.last_match(1))}"
162
+ end
163
+ detail = format_counts(detail)
164
+ detail = detail.gsub(", ", " #{color(:dim, '·')} ")
165
+ [label, detail]
166
+ end
167
+
168
+ def terminal_text(message)
169
+ label, detail = terminal_parts(message)
170
+ detail.empty? ? format_counts(label) : "#{label}: #{detail}"
171
+ end
172
+
173
+ def pretty_label(label)
174
+ case label
175
+ when "Building temporary relationship lookup index"
176
+ "Building lookup index"
177
+ when "Temporary relationship lookup index ready."
178
+ "Lookup index ready"
179
+ when "Cleaning temporary load metadata"
180
+ "Cleaning load metadata"
181
+ when "Temporary load metadata removed."
182
+ "Load metadata removed"
183
+ else
184
+ label
185
+ end
186
+ end
187
+
188
+ def extract_percent(message)
189
+ if (match = message.match(/\((\d+)%\)/)) || (match = message.match(/:\s*(\d+)%\z/))
190
+ return match[1].to_i
191
+ end
192
+
193
+ match = message.match(/:\s*(\d+)\/(\d+) nodes\z/)
194
+ return nil unless match
195
+
196
+ total = match[2].to_i
197
+ total.zero? ? 100 : match[1].to_i * 100 / total
198
+ end
199
+
200
+ def format_counts(text)
201
+ text.gsub(/\b\d{4,}\b/) { |number| format_count(number) }
202
+ end
84
203
 
85
- @io.print "\r#{' ' * @line_width}\r"
204
+ def format_count(number)
205
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
206
+ end
207
+
208
+ def render_line(line)
209
+ @io.print "\r\e[2K#{line}"
210
+ @io.flush
211
+ @line_open = true
212
+ end
213
+
214
+ def clear_line
215
+ return unless @pretty && @line_open
216
+
217
+ @io.print "\r\e[2K"
86
218
  @io.flush
87
- @line_width = 0
88
219
  @line_open = false
89
220
  end
221
+
222
+ def color(style, text)
223
+ return text unless @color
224
+
225
+ "#{ANSI.fetch(style)}#{text}#{ANSI[:reset]}"
226
+ end
90
227
  end
91
228
  private_constant :ProgressReporter
92
229
  MIN_INTEGER = -(2**63)
@@ -399,8 +536,8 @@ module Neo4jBolt
399
536
  nil
400
537
  end
401
538
 
402
- def dump_database(io, progress_io: nil)
403
- progress = ProgressReporter.new(progress_io)
539
+ def dump_database(io, progress_io: nil, progress_color: "cyan")
540
+ progress = ProgressReporter.new(progress_io, color: progress_color)
404
541
  dumped_nodes = 0
405
542
  dumped_relationships = 0
406
543
 
@@ -459,7 +596,7 @@ module Neo4jBolt
459
596
  progress&.close
460
597
  end
461
598
 
462
- def load_database_dump(io, force_append: false, progress_io: nil,
599
+ def load_database_dump(io, force_append: false, progress_io: nil, progress_color: "cyan",
463
600
  initial_batch_size: LOAD_INITIAL_BATCH_SIZE)
464
601
  raise Error, "load_database_dump cannot run inside a transaction" if transaction_context
465
602
 
@@ -471,7 +608,7 @@ module Neo4jBolt
471
608
  raise Error, "There are nodes in this database, exiting now." unless count.zero?
472
609
  end
473
610
 
474
- progress = ProgressReporter.new(progress_io)
611
+ progress = ProgressReporter.new(progress_io, color: progress_color)
475
612
  node_batches = Hash.new { |hash, key| hash[key] = [] }
476
613
  relationship_batches = Hash.new { |hash, key| hash[key] = [] }
477
614
  total_nodes, total_relationships = parse_dump(io, node_batches, relationship_batches, progress)
@@ -767,7 +904,8 @@ module Neo4jBolt
767
904
  batch_size = [batch_size, reduced_batch_size].min
768
905
  progress&.note(
769
906
  "Neo4j rejected a #{kind} batch of #{slice.size} for transaction memory; " \
770
- "retrying with batches of #{batch_size}."
907
+ "retrying with batches of #{batch_size}.",
908
+ level: :warning
771
909
  )
772
910
  end
773
911
  end
@@ -822,7 +960,8 @@ module Neo4jBolt
822
960
 
823
961
  batch_size = [batch_size / 2, 1].max
824
962
  progress&.note(
825
- "Neo4j rejected a cleanup batch for transaction memory; retrying with batches of #{batch_size}."
963
+ "Neo4j rejected a cleanup batch for transaction memory; retrying with batches of #{batch_size}.",
964
+ level: :warning
826
965
  )
827
966
  end
828
967
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neo4j_bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Specht