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 +4 -4
- data/README.md +10 -3
- data/bin/neo4j_bolt +4 -1
- data/lib/neo4j_bolt/version.rb +1 -1
- data/lib/neo4j_bolt.rb +172 -33
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38ed2e6727f1bc4baccc838b195d72a2392bbb82788598d704a1e81bd0f20de0
|
|
4
|
+
data.tar.gz: d781a67c3eee09dd9e32a9af60fe1fb65a61bb8afddb96307dd897cbcd99ddf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
data/lib/neo4j_bolt/version.rb
CHANGED
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
|
-
|
|
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
|
-
@
|
|
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 @
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 && @
|
|
123
|
+
return unless @io && @pretty && @line_open
|
|
74
124
|
|
|
75
|
-
|
|
76
|
-
@line_width = 0
|
|
77
|
-
@line_open = false
|
|
125
|
+
clear_line
|
|
78
126
|
end
|
|
79
127
|
|
|
80
128
|
private
|
|
81
129
|
|
|
82
|
-
def
|
|
83
|
-
|
|
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
|
-
|
|
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
|