neo4j_bolt 0.4.3 → 0.4.5
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 +35 -10
- data/bin/neo4j_bolt +12 -3
- data/lib/neo4j_bolt/version.rb +1 -1
- data/lib/neo4j_bolt.rb +87 -9
- 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: cc5ccd396277de9c75f73bd338f89a2af476475940d53e0399cfbbeaa9589dc6
|
|
4
|
+
data.tar.gz: 90a393bb3f4819245613ca34206d24932487de5af4959b27e26232137766e15f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4f8c0aa87b72f4081d19476dbb684ced5fed6851b729f7db4bb7d8b06f7e41f1607581498b1195043edc63433fe91b8ca7aadf0c58c3d8c7f14bf79dcae1395
|
|
7
|
+
data.tar.gz: '028fc3fc677e0a8d83f6f5554d851ff29b1ab7bf6b0c059ea646f18062f646e4c43554f612bb6c69055658af56e4f08bda30c5756b57899475b98178b91d6e87'
|
data/README.md
CHANGED
|
@@ -4,14 +4,14 @@ Neo4jBolt 0.4 is a small compatibility and convenience layer for Ruby applicatio
|
|
|
4
4
|
|
|
5
5
|
Neo4jBolt no longer implements the Bolt wire protocol itself.
|
|
6
6
|
|
|
7
|
-
`0.4.
|
|
7
|
+
`0.4.5` requires Ruby 3.4 or newer and pins `neo4j-ruby-driver` to `6.2.1.beta.4`. Applications on older Rubies can remain on the Neo4jBolt 0.3.x line; this prerelease is intentionally not an automatic upgrade for them.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
11
|
For this prerelease, specify the version explicitly:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
gem "neo4j_bolt", "0.4.
|
|
14
|
+
gem "neo4j_bolt", "0.4.5"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Then run `bundle install`. A running Neo4j database is required.
|
|
@@ -26,6 +26,27 @@ Neo4jBolt.bolt_port = 7687
|
|
|
26
26
|
Neo4jBolt.bolt_verbosity = 0
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
The connection endpoint, authentication, and database selection can use the standard Neo4j client environment variables:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export NEO4J_URI=neo4j://localhost:7687
|
|
33
|
+
export NEO4J_USERNAME=neo4j
|
|
34
|
+
export NEO4J_PASSWORD=secret
|
|
35
|
+
export NEO4J_DATABASE=neo4j
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
They are read when `neo4j_bolt` is loaded. `NEO4J_URI` accepts `neo4j://HOST[:PORT]` and `bolt://HOST[:PORT]` endpoint notation; Neo4jBolt uses the host and port while retaining its existing direct Bolt connection behavior. The default port is 7687. Explicit Ruby configuration overrides those defaults:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Neo4jBolt.bolt_host = "localhost"
|
|
42
|
+
Neo4jBolt.bolt_port = 7687
|
|
43
|
+
Neo4jBolt.bolt_username = "neo4j"
|
|
44
|
+
Neo4jBolt.bolt_password = "secret"
|
|
45
|
+
Neo4jBolt.bolt_database = "movies"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If both username and password are unset, Neo4jBolt keeps the existing unauthenticated connection behavior. If only one is set, the first query raises `Neo4jBolt::Error`. Leaving the database unset lets Neo4j choose the user's/server's default database.
|
|
49
|
+
|
|
29
50
|
Applications can include the module as before:
|
|
30
51
|
|
|
31
52
|
```ruby
|
|
@@ -36,7 +57,7 @@ They can also extend another object or call the convenience methods directly on
|
|
|
36
57
|
|
|
37
58
|
One lazily initialized upstream driver owns a thread-safe connection pool. Each standalone query uses a short-lived session and returns its connection to that pool after its result has been consumed. Queries from several Puma threads are not globally serialized.
|
|
38
59
|
|
|
39
|
-
`cleanup_neo4j` safely retires the driver and its pool after active operations finish. A later query creates a new driver. Do not call cleanup from inside an active query or transaction on the same thread.
|
|
60
|
+
`cleanup_neo4j` safely retires the driver and its pool after active operations finish. A later query creates a new driver. Changing host, port, username, or password also retires the pool so later connections use the new settings. Database selection is a session setting and therefore applies to newly opened sessions without rebuilding the driver. Do not call cleanup from inside an active query or transaction on the same thread.
|
|
40
61
|
|
|
41
62
|
## Running queries
|
|
42
63
|
|
|
@@ -168,7 +189,7 @@ neo4j_bolt load --color bright-blue database.dump
|
|
|
168
189
|
|
|
169
190
|
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.
|
|
170
191
|
|
|
171
|
-
For relational loads, the adapter assigns a random temporary label and
|
|
192
|
+
For relational loads, the adapter assigns a random temporary label and the fixed reserved property `__neo4j_bolt_load_id` to imported nodes. After the nodes are committed it builds a temporary `neo4j_bolt_` index on that property, uses indexed lookups while creating relationships, drops the index, and removes the temporary metadata in batches. The fixed property key is reused by every load instead of registering a new random property-key token each time. Neo4j can keep the property key registered after all values have been removed, so it may remain visible in Browser's property-key list; no imported node retains the property after successful cleanup. Relational loads refuse to run if the dump itself, or existing data during `force_append`, currently uses the reserved property. No database-internal identity is carried from one load transaction to another. Loading a relational dump therefore requires permission to create and drop an index.
|
|
172
193
|
|
|
173
194
|
Loads start with batches of 5,000 records by default. This is an initial ceiling rather than a claimed optimum: if Neo4j returns a transaction-memory/resource error whose server semantics guarantee rollback, the loader halves the failed batch and retries it, then keeps the smaller size for the rest of that phase. Node and relationship phases adapt independently. Other errors are not retried, because a generic connection failure cannot safely prove that a `CREATE` transaction did not commit. Callers can change the initial ceiling with `initial_batch_size:`; the CLI exposes the same setting as `--batch-size`.
|
|
174
195
|
|
|
@@ -188,7 +209,7 @@ The `neo4j_bolt` executable retains these commands:
|
|
|
188
209
|
| `neo4j_bolt index rm --force` | Remove all constraints and indexes |
|
|
189
210
|
| `neo4j_bolt visualize` | Generate a GraphViz document |
|
|
190
211
|
|
|
191
|
-
Use `--host HOST:PORT` to
|
|
212
|
+
Use `--host HOST:PORT`, `--username USER`, `--password PASSWORD`, and `--database DATABASE` to override connection settings. Host and port otherwise inherit `NEO4J_URI` when present, falling back to `localhost:7687`; username, password, and database inherit `NEO4J_USERNAME`, `NEO4J_PASSWORD`, and `NEO4J_DATABASE`. Prefer the password environment variable for routine use because command-line arguments may be visible in shell history or process listings. `gli` remains a runtime dependency for the CLI, and `pry` remains for the separate `bin/console` executable.
|
|
192
213
|
|
|
193
214
|
## Tested Neo4j versions
|
|
194
215
|
|
|
@@ -198,7 +219,7 @@ The same complete integration suite is run against these exact Community images:
|
|
|
198
219
|
- `neo4j:5.26.28-community`
|
|
199
220
|
- `neo4j:2026.06.0-community`
|
|
200
221
|
|
|
201
|
-
No compatibility beyond this matrix is claimed for `0.4.
|
|
222
|
+
No compatibility beyond this matrix is claimed for `0.4.5`.
|
|
202
223
|
|
|
203
224
|
Run one modern LTS target:
|
|
204
225
|
|
|
@@ -212,15 +233,16 @@ Run the complete sequential matrix:
|
|
|
212
233
|
bundle exec rake spec:matrix
|
|
213
234
|
```
|
|
214
235
|
|
|
215
|
-
The harness creates uniquely named disposable containers with
|
|
236
|
+
The harness creates uniquely named disposable containers with Basic authentication enabled, dynamically publishes Bolt ports, waits by establishing a real driver/query connection, and cleans every container through shell traps. A developer does not need to start Neo4j manually.
|
|
216
237
|
|
|
217
|
-
To point RSpec itself at an already disposable database, set `NEO4J_BOLT_TEST_HOST` and `NEO4J_BOLT_TEST_PORT`. The explicit port requirement protects real databases from the destructive integration suite.
|
|
238
|
+
To point RSpec itself at an already disposable database, set `NEO4J_BOLT_TEST_HOST` and `NEO4J_BOLT_TEST_PORT`. If it requires authentication or an explicit database, also set `NEO4J_USERNAME`, `NEO4J_PASSWORD`, and `NEO4J_DATABASE`. The explicit port requirement protects real databases from the destructive integration suite.
|
|
218
239
|
|
|
219
240
|
## Compatibility inventory for 0.4
|
|
220
241
|
|
|
221
242
|
| API | Status |
|
|
222
243
|
| --- | --- |
|
|
223
244
|
| `bolt_host`, `bolt_port`, `bolt_verbosity` | Preserved |
|
|
245
|
+
| `bolt_username`, `bolt_password`, `bolt_database` | Additive authentication/database configuration |
|
|
224
246
|
| Included/extended/module-style use | Preserved |
|
|
225
247
|
| `neo4j_query`, including incremental block form | Preserved via adapter |
|
|
226
248
|
| `neo4j_query_expect_one` | Preserved via adapter |
|
|
@@ -235,12 +257,15 @@ To point RSpec itself at an already disposable database, set `NEO4J_BOLT_TEST_HO
|
|
|
235
257
|
| CLI commands | Preserved |
|
|
236
258
|
| `BoltSocket`, `BoltBuffer`, protocol markers/state/parser/packer | Intentionally removed private implementation details |
|
|
237
259
|
|
|
238
|
-
## 0.4.
|
|
260
|
+
## 0.4.5 migration notes
|
|
239
261
|
|
|
240
262
|
- Ruby 3.4 or newer is required; Ruby 2.x/3.0–3.3 applications should stay on 0.3.x until upgraded.
|
|
241
263
|
- The exact prerelease upstream dependency is pinned while no stable `neo4j-ruby-driver` 6.2.x exists.
|
|
242
264
|
- Connections are pooled and safe for concurrent use instead of one mutable socket per including object.
|
|
243
|
-
-
|
|
265
|
+
- `NEO4J_URI` now supplies the default host and port for `bolt://` and `neo4j://` endpoint notation; an explicit CLI `--host` or Ruby `bolt_host`/`bolt_port` setting overrides it.
|
|
266
|
+
- Basic authentication and database selection are exposed through `bolt_username`, `bolt_password`, and `bolt_database`, with `NEO4J_USERNAME`, `NEO4J_PASSWORD`, and `NEO4J_DATABASE` as environment defaults.
|
|
267
|
+
- TLS, routing behavior, URI query parameters, and other upstream-driver configuration are still not exposed through the compatibility API.
|
|
268
|
+
- Relational loads reuse one reserved `__neo4j_bolt_load_id` property key while retaining the existing indexed lookup algorithm, preventing a new property-key token from being registered on every load.
|
|
244
269
|
- `BoltSocket`, `BoltBuffer`, `ServerState`, `BoltMarker`, `UnexpectedServerResponse`, `State`, and `CypherError` were undocumented wire internals and are removed.
|
|
245
270
|
|
|
246
271
|
## Development
|
data/bin/neo4j_bolt
CHANGED
|
@@ -21,13 +21,22 @@ class App
|
|
|
21
21
|
version Neo4jBolt::VERSION
|
|
22
22
|
|
|
23
23
|
flag [:v, :verbosity], :default_value => 0
|
|
24
|
-
flag [:h, :host], :default_value =>
|
|
24
|
+
flag [:h, :host], :default_value => nil,
|
|
25
|
+
:desc => 'Neo4j host:port (default: NEO4J_URI or localhost:7687)'
|
|
26
|
+
flag [:u, :username], :default_value => nil, :desc => 'Neo4j username (default: NEO4J_USERNAME)'
|
|
27
|
+
flag [:p, :password], :default_value => nil, :desc => 'Neo4j password (default: NEO4J_PASSWORD)'
|
|
28
|
+
flag [:d, :database], :default_value => nil, :desc => 'Neo4j database (default: NEO4J_DATABASE)'
|
|
25
29
|
|
|
26
30
|
pre do |global_options, command, options, args|
|
|
27
31
|
host = global_options[:host]
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
unless host.nil?
|
|
33
|
+
Neo4jBolt.bolt_host = host.split(':').first
|
|
34
|
+
Neo4jBolt.bolt_port = host.split(':').last.to_i
|
|
35
|
+
end
|
|
30
36
|
Neo4jBolt.bolt_verbosity = global_options[:verbosity].to_i
|
|
37
|
+
Neo4jBolt.bolt_username = global_options[:username] unless global_options[:username].nil?
|
|
38
|
+
Neo4jBolt.bolt_password = global_options[:password] unless global_options[:password].nil?
|
|
39
|
+
Neo4jBolt.bolt_database = global_options[:database] unless global_options[:database].nil?
|
|
31
40
|
true
|
|
32
41
|
end
|
|
33
42
|
|
data/lib/neo4j_bolt/version.rb
CHANGED
data/lib/neo4j_bolt.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "set"
|
|
6
6
|
require "thread"
|
|
7
|
+
require "uri"
|
|
7
8
|
require "neo4j/driver"
|
|
8
9
|
require "neo4j_bolt/version"
|
|
9
10
|
|
|
@@ -18,8 +19,9 @@ module Neo4jBolt
|
|
|
18
19
|
LOAD_MEMORY_GQL_STATUSES = %w[51N72 51N73].freeze
|
|
19
20
|
LOAD_PROGRESS_STEP = 5_000
|
|
20
21
|
DUMP_PROGRESS_STEP = 1_000
|
|
22
|
+
LOAD_ID_PROPERTY = "__neo4j_bolt_load_id"
|
|
21
23
|
private_constant :LOAD_MEMORY_ERROR_CODES, :LOAD_MEMORY_GQL_STATUSES,
|
|
22
|
-
:LOAD_PROGRESS_STEP, :DUMP_PROGRESS_STEP
|
|
24
|
+
:LOAD_PROGRESS_STEP, :DUMP_PROGRESS_STEP, :LOAD_ID_PROPERTY
|
|
23
25
|
|
|
24
26
|
class ProgressReporter
|
|
25
27
|
UPDATE_INTERVAL = 0.5
|
|
@@ -335,10 +337,22 @@ module Neo4jBolt
|
|
|
335
337
|
host = "[#{host}]" if host.include?(":") && !host.start_with?("[")
|
|
336
338
|
Neo4j::Driver::GraphDatabase.driver(
|
|
337
339
|
"bolt://#{host}:#{Neo4jBolt.bolt_port}",
|
|
338
|
-
|
|
340
|
+
authentication_token
|
|
339
341
|
)
|
|
340
342
|
end
|
|
341
343
|
|
|
344
|
+
def authentication_token
|
|
345
|
+
username = Neo4jBolt.bolt_username
|
|
346
|
+
password = Neo4jBolt.bolt_password
|
|
347
|
+
return Neo4j::Driver::AuthTokens.none if username.nil? && password.nil?
|
|
348
|
+
|
|
349
|
+
if username.nil? || password.nil?
|
|
350
|
+
raise Error, "Neo4j username and password must either both be set or both be unset"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
Neo4j::Driver::AuthTokens.basic(username, password)
|
|
354
|
+
end
|
|
355
|
+
|
|
342
356
|
def leases
|
|
343
357
|
@leases ||= 0
|
|
344
358
|
end
|
|
@@ -354,8 +368,8 @@ module Neo4jBolt
|
|
|
354
368
|
end
|
|
355
369
|
|
|
356
370
|
class << self
|
|
357
|
-
attr_reader :bolt_host, :bolt_port
|
|
358
|
-
attr_accessor :bolt_verbosity
|
|
371
|
+
attr_reader :bolt_host, :bolt_port, :bolt_username, :bolt_password
|
|
372
|
+
attr_accessor :bolt_verbosity, :bolt_database
|
|
359
373
|
|
|
360
374
|
def bolt_host=(host)
|
|
361
375
|
reconfigure_driver if defined?(@bolt_host) && @bolt_host != host
|
|
@@ -367,6 +381,16 @@ module Neo4jBolt
|
|
|
367
381
|
@bolt_port = port
|
|
368
382
|
end
|
|
369
383
|
|
|
384
|
+
def bolt_username=(username)
|
|
385
|
+
reconfigure_driver if defined?(@bolt_username) && @bolt_username != username
|
|
386
|
+
@bolt_username = username
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def bolt_password=(password)
|
|
390
|
+
reconfigure_driver if defined?(@bolt_password) && @bolt_password != password
|
|
391
|
+
@bolt_password = password
|
|
392
|
+
end
|
|
393
|
+
|
|
370
394
|
private
|
|
371
395
|
|
|
372
396
|
def reconfigure_driver
|
|
@@ -374,13 +398,47 @@ module Neo4jBolt
|
|
|
374
398
|
end
|
|
375
399
|
end
|
|
376
400
|
|
|
377
|
-
|
|
378
|
-
|
|
401
|
+
neo4j_uri = ENV["NEO4J_URI"]
|
|
402
|
+
if neo4j_uri.nil? || neo4j_uri.empty?
|
|
403
|
+
self.bolt_host = "localhost"
|
|
404
|
+
self.bolt_port = 7687
|
|
405
|
+
else
|
|
406
|
+
begin
|
|
407
|
+
parsed_neo4j_uri = URI.parse(neo4j_uri)
|
|
408
|
+
rescue URI::InvalidURIError => error
|
|
409
|
+
raise Error, "Invalid NEO4J_URI: #{error.message}"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
unless %w[bolt neo4j].include?(parsed_neo4j_uri.scheme) && parsed_neo4j_uri.host
|
|
413
|
+
raise Error, "NEO4J_URI must be a bolt:// or neo4j:// URI with a host"
|
|
414
|
+
end
|
|
415
|
+
unless parsed_neo4j_uri.userinfo.nil? &&
|
|
416
|
+
["", "/"].include?(parsed_neo4j_uri.path.to_s) &&
|
|
417
|
+
parsed_neo4j_uri.query.nil? && parsed_neo4j_uri.fragment.nil?
|
|
418
|
+
raise Error, "NEO4J_URI may only contain scheme, host, and port"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
self.bolt_host = parsed_neo4j_uri.host
|
|
422
|
+
self.bolt_port = parsed_neo4j_uri.port || 7687
|
|
423
|
+
end
|
|
379
424
|
self.bolt_verbosity = 0
|
|
425
|
+
self.bolt_username = ENV["NEO4J_USERNAME"]
|
|
426
|
+
self.bolt_password = ENV["NEO4J_PASSWORD"]
|
|
427
|
+
self.bolt_database = ENV["NEO4J_DATABASE"]
|
|
380
428
|
|
|
381
429
|
# Keep both `include Neo4jBolt` and direct module-style calls working.
|
|
382
430
|
extend self
|
|
383
431
|
|
|
432
|
+
def with_session(driver)
|
|
433
|
+
database = Neo4jBolt.bolt_database
|
|
434
|
+
if database.nil?
|
|
435
|
+
driver.session { |session| yield session }
|
|
436
|
+
else
|
|
437
|
+
driver.session(database: database) { |session| yield session }
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
private :with_session
|
|
441
|
+
|
|
384
442
|
def neo4j_query(query, data = {}, &block)
|
|
385
443
|
validate_parameters!(data)
|
|
386
444
|
log_query(query, data)
|
|
@@ -390,7 +448,7 @@ module Neo4jBolt
|
|
|
390
448
|
run_and_convert(context.transaction, query, data, &block)
|
|
391
449
|
else
|
|
392
450
|
DriverRegistry.with_driver do |driver|
|
|
393
|
-
driver
|
|
451
|
+
with_session(driver) do |session|
|
|
394
452
|
run_and_convert(session, query, data, &block)
|
|
395
453
|
end
|
|
396
454
|
end
|
|
@@ -430,7 +488,7 @@ module Neo4jBolt
|
|
|
430
488
|
end
|
|
431
489
|
|
|
432
490
|
DriverRegistry.with_driver do |driver|
|
|
433
|
-
driver
|
|
491
|
+
with_session(driver) do |session|
|
|
434
492
|
upstream_transaction = session.begin_transaction
|
|
435
493
|
context = TransactionContext.new(
|
|
436
494
|
session: session, transaction: upstream_transaction, depth: 1, rollback_only: false
|
|
@@ -615,12 +673,32 @@ module Neo4jBolt
|
|
|
615
673
|
|
|
616
674
|
temporary_token = SecureRandom.hex(12)
|
|
617
675
|
temporary_label = "__neo4j_bolt_load_#{temporary_token}"
|
|
618
|
-
temporary_id_property =
|
|
676
|
+
temporary_id_property = LOAD_ID_PROPERTY
|
|
619
677
|
temporary_index = "neo4j_bolt_load_#{temporary_token}"
|
|
620
678
|
quoted_temporary_label = quote_identifier(temporary_label)
|
|
621
679
|
quoted_temporary_id = quote_identifier(temporary_id_property)
|
|
622
680
|
quoted_temporary_index = quote_identifier(temporary_index)
|
|
623
681
|
needs_relationship_lookup = total_relationships.positive?
|
|
682
|
+
|
|
683
|
+
if needs_relationship_lookup
|
|
684
|
+
dump_uses_reserved_property = node_batches.each_value.any? do |nodes|
|
|
685
|
+
nodes.any? { |node| node.fetch("properties", {}).key?(LOAD_ID_PROPERTY) }
|
|
686
|
+
end
|
|
687
|
+
if dump_uses_reserved_property
|
|
688
|
+
raise Error, "Dump contains reserved Neo4jBolt load property #{LOAD_ID_PROPERTY.inspect}."
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
if force_append
|
|
692
|
+
reserved_property_in_use = neo4j_query(
|
|
693
|
+
"MATCH (n) WHERE n.#{quoted_temporary_id} IS NOT NULL RETURN 1 AS present LIMIT 1"
|
|
694
|
+
).any?
|
|
695
|
+
if reserved_property_in_use
|
|
696
|
+
raise Error,
|
|
697
|
+
"Database contains reserved Neo4jBolt load property #{LOAD_ID_PROPERTY.inspect}."
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
624
702
|
loaded_nodes = 0
|
|
625
703
|
loaded_relationships = 0
|
|
626
704
|
index_created = false
|