neo4j_bolt 0.4.1 → 0.4.2

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: 66da85ac04ffd0ef43fea4fba9a676ea2168770f85825573652a58dbc0a48f2b
4
+ data.tar.gz: 132911b813e88d5a3efb590fe5cd29da0c8adb6663b06b9931bb0a03f4fcae9c
5
5
  SHA512:
6
- metadata.gz: 9640636068e101464e9a92e0ca38e465bc3c847863b457d90ae3e5361c37960359e7fd9c24ec8915f1ad27f90311b0867b95973801e28a17939f2bf70b3bad75
7
- data.tar.gz: ed3142cd503626d3ba4d04ea86a708426178a070513c14325e46bac405bfa384250f8978d12cf6eb72920bb35ec7170cbb3ecd90500de121f6c650279387313e
6
+ metadata.gz: 37e9205800f8661683bc246c6a962a15254c2d1e4a33d5b948f85d5e59d9ac38911e67c4a2e18bb318bc9f3ceeb3eedb7199004e98b0ce1694ae2e0f16f9ce8e
7
+ data.tar.gz: 3f7066485f251b5939b9a448b441290f16714311ebd7baa2b2baa27d785ae2463902c63b6c14b532df5adfe574242c8e415305967677110d92943a431e4360d3
data/README.md CHANGED
@@ -157,7 +157,7 @@ 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
161
 
162
162
  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
163
 
@@ -1,3 +1,3 @@
1
1
  module Neo4jBolt
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
3
3
  end
data/lib/neo4j_bolt.rb CHANGED
@@ -23,70 +23,185 @@ module Neo4jBolt
23
23
 
24
24
  class ProgressReporter
25
25
  UPDATE_INTERVAL = 0.5
26
+ BAR_WIDTH = 24
27
+ SPINNER = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
28
+ ANSI = {
29
+ reset: "\e[0m",
30
+ bold: "\e[1m",
31
+ dim: "\e[2m",
32
+ cyan: "\e[36m",
33
+ green: "\e[32m",
34
+ yellow: "\e[33m"
35
+ }.freeze
26
36
 
27
37
  def initialize(io)
28
38
  @io = io
29
- @tty = io && io.respond_to?(:tty?) && io.tty? && Neo4jBolt.bolt_verbosity.to_i.zero?
39
+ mode = ENV.fetch("NEO4J_BOLT_PROGRESS", "auto")
40
+ unless %w[auto pretty plain].include?(mode)
41
+ raise ArgumentError, "NEO4J_BOLT_PROGRESS must be auto, pretty, or plain"
42
+ end
43
+
44
+ detected_tty = io && io.respond_to?(:tty?) && io.tty? && Neo4jBolt.bolt_verbosity.to_i.zero?
45
+ @pretty = io && (mode == "pretty" || (mode == "auto" && detected_tty))
46
+ @color = @pretty && !ENV.key?("NO_COLOR") && (mode == "pretty" || ENV["TERM"] != "dumb")
30
47
  @last_update_at = nil
31
- @line_width = 0
32
48
  @line_open = false
49
+ @spinner_index = 0
33
50
  end
34
51
 
35
- def update(message, force: false)
52
+ def update(message, force: false, percent: nil)
36
53
  return unless @io
37
54
 
38
55
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
56
  return if !force && @last_update_at && now - @last_update_at < UPDATE_INTERVAL
40
57
 
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
58
+ if @pretty
59
+ percent ||= extract_percent(message)
60
+ render_line(activity_line(message, percent))
47
61
  else
48
62
  @io.puts message
49
63
  end
50
64
  @last_update_at = now
51
65
  end
52
66
 
53
- def note(message)
67
+ def note(message, level: :info)
54
68
  return unless @io
55
69
 
56
- clear_tty_line
57
- @io.puts message
58
- @last_update_at = nil
70
+ if @pretty && level == :info
71
+ update(message, force: true)
72
+ else
73
+ clear_line
74
+ if @pretty
75
+ icon = level == :warning ? color(:yellow, "!") : color(:cyan, "•")
76
+ @io.puts "#{icon} #{terminal_text(message)}"
77
+ else
78
+ @io.puts message
79
+ end
80
+ @last_update_at = nil
81
+ end
59
82
  end
60
83
 
61
84
  def finish(message)
62
85
  return unless @io
63
86
 
64
- update(message, force: true)
65
- if @tty
66
- @io.puts
67
- @line_width = 0
87
+ if @pretty
88
+ clear_line
89
+ label, detail = terminal_parts(message)
90
+ line = "#{color(:green, '✓')} #{color(:bold, label)}"
91
+ line += " #{detail}" unless detail.empty?
92
+ @io.puts line
68
93
  @line_open = false
94
+ else
95
+ @io.puts message
69
96
  end
97
+ @last_update_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
98
  end
71
99
 
72
100
  def close
73
- return unless @io && @tty && @line_open
101
+ return unless @io && @pretty && @line_open
74
102
 
75
- @io.puts
76
- @line_width = 0
77
- @line_open = false
103
+ clear_line
78
104
  end
79
105
 
80
106
  private
81
107
 
82
- def clear_tty_line
83
- return unless @tty && @line_open
108
+ def activity_line(message, percent)
109
+ label, detail = terminal_parts(message, strip_percent: !percent.nil?)
110
+ spinner = SPINNER[@spinner_index % SPINNER.length]
111
+ @spinner_index += 1
112
+
113
+ parts = [color(:cyan, spinner), color(:bold, label)]
114
+ if percent
115
+ percent = [[percent.to_i, 0].max, 100].min
116
+ parts << progress_bar(percent)
117
+ parts << color(:bold, format("%3d%%", percent))
118
+ end
119
+ parts << detail unless detail.empty?
120
+ parts.join(" ")
121
+ end
122
+
123
+ def progress_bar(percent)
124
+ filled = (percent * BAR_WIDTH / 100.0).round
125
+ empty = BAR_WIDTH - filled
126
+ "#{color(:cyan, '━' * filled)}#{color(:dim, '─' * empty)}"
127
+ end
128
+
129
+ def terminal_parts(message, strip_percent: false)
130
+ text = message.sub(/\.\.\.\z/, "")
131
+ label, detail = text.split(": ", 2)
132
+ label = pretty_label(label)
133
+ detail ||= ""
134
+ if strip_percent
135
+ detail = detail.sub(/\s+\(\d+%\)/, "")
136
+ detail = "" if detail.match?(/\A\d+%\z/)
137
+ end
138
+ detail = detail.gsub(/ \[batch (\d+)\]/) do
139
+ " #{color(:dim, '·')} batch #{format_count(Regexp.last_match(1))}"
140
+ end
141
+ detail = format_counts(detail)
142
+ detail = detail.gsub(", ", " #{color(:dim, '·')} ")
143
+ [label, detail]
144
+ end
145
+
146
+ def terminal_text(message)
147
+ label, detail = terminal_parts(message)
148
+ detail.empty? ? format_counts(label) : "#{label}: #{detail}"
149
+ end
150
+
151
+ def pretty_label(label)
152
+ case label
153
+ when "Building temporary relationship lookup index"
154
+ "Building lookup index"
155
+ when "Temporary relationship lookup index ready."
156
+ "Lookup index ready"
157
+ when "Cleaning temporary load metadata"
158
+ "Cleaning load metadata"
159
+ when "Temporary load metadata removed."
160
+ "Load metadata removed"
161
+ else
162
+ label
163
+ end
164
+ end
165
+
166
+ def extract_percent(message)
167
+ if (match = message.match(/\((\d+)%\)/)) || (match = message.match(/:\s*(\d+)%\z/))
168
+ return match[1].to_i
169
+ end
170
+
171
+ match = message.match(/:\s*(\d+)\/(\d+) nodes\z/)
172
+ return nil unless match
173
+
174
+ total = match[2].to_i
175
+ total.zero? ? 100 : match[1].to_i * 100 / total
176
+ end
177
+
178
+ def format_counts(text)
179
+ text.gsub(/\b\d{4,}\b/) { |number| format_count(number) }
180
+ end
181
+
182
+ def format_count(number)
183
+ number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
184
+ end
84
185
 
85
- @io.print "\r#{' ' * @line_width}\r"
186
+ def render_line(line)
187
+ @io.print "\r\e[2K#{line}"
188
+ @io.flush
189
+ @line_open = true
190
+ end
191
+
192
+ def clear_line
193
+ return unless @pretty && @line_open
194
+
195
+ @io.print "\r\e[2K"
86
196
  @io.flush
87
- @line_width = 0
88
197
  @line_open = false
89
198
  end
199
+
200
+ def color(style, text)
201
+ return text unless @color
202
+
203
+ "#{ANSI.fetch(style)}#{text}#{ANSI[:reset]}"
204
+ end
90
205
  end
91
206
  private_constant :ProgressReporter
92
207
  MIN_INTEGER = -(2**63)
@@ -767,7 +882,8 @@ module Neo4jBolt
767
882
  batch_size = [batch_size, reduced_batch_size].min
768
883
  progress&.note(
769
884
  "Neo4j rejected a #{kind} batch of #{slice.size} for transaction memory; " \
770
- "retrying with batches of #{batch_size}."
885
+ "retrying with batches of #{batch_size}.",
886
+ level: :warning
771
887
  )
772
888
  end
773
889
  end
@@ -822,7 +938,8 @@ module Neo4jBolt
822
938
 
823
939
  batch_size = [batch_size / 2, 1].max
824
940
  progress&.note(
825
- "Neo4j rejected a cleanup batch for transaction memory; retrying with batches of #{batch_size}."
941
+ "Neo4j rejected a cleanup batch for transaction memory; retrying with batches of #{batch_size}.",
942
+ level: :warning
826
943
  )
827
944
  end
828
945
  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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Specht