Neo4jBolt

Neo4jBolt 0.4 is a small compatibility and convenience layer for Ruby applications, backed by neo4j-ruby-driver. It preserves the straightforward Neo4jBolt application API while delegating connections, pooling, Bolt negotiation, protocol state, PackStream, reconnects, and modern Neo4j value support to the upstream driver.

Neo4jBolt no longer implements the Bolt wire protocol itself.

0.4.0 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.

Installation

For this prerelease, specify the version explicitly:

gem "neo4j_bolt", "0.4.0"

Then run bundle install. A running Neo4j database is required.

Connecting

The existing host, port, and verbosity settings remain available:

Neo4jBolt.bolt_host = "localhost"
Neo4jBolt.bolt_port = 7687
Neo4jBolt.bolt_verbosity = 0

Applications can include the module as before:

include Neo4jBolt

They can also extend another object or call the convenience methods directly on Neo4jBolt.

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.

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.

Running queries

Materialize all result rows:

rows = neo4j_query("MATCH (person:Person) RETURN person.name AS name")
puts rows.first["name"]

Or process records incrementally without first materializing the complete result:

neo4j_query("MATCH (person:Person) RETURN person") do |row|
  puts row["person"][:name]
end

The block form returns nil. Its upstream session, result, and pooled connection remain alive for the duration of iteration.

Parameters stay separate from Cypher:

row = neo4j_query_expect_one(
  "MATCH (person:Person {email: $email}) RETURN person",
  email: "ada@example.test"
)

neo4j_query_expect_one raises Neo4jBolt::ExpectedOneResultError unless the query produces exactly one row.

Result compatibility

Result-row keys are strings:

row["person"]

Nodes and relationships remain Hash subclasses whose property keys are symbols:

node = row["person"]
node[:name]
node.id
node.labels
node.element_id

Neo4jBolt::Node preserves id and labels and additively exposes element_id.

Neo4jBolt::Relationship preserves id, start_node_id, end_node_id, and type, and additively exposes element_id, start_node_element_id, and end_node_element_id.

Arrays and maps are adapted recursively. Map/property keys inside values are symbols, matching Neo4jBolt 0.3 behavior. Modern upstream values that 0.3 could not represent—such as temporal, spatial, path, byte, duration, and UUID values—pass through as upstream value objects. This is additive support.

Signed 64-bit integer limits are checked before transport. Out-of-range integers raise Neo4jBolt::IntegerOutOfRangeError.

Transactions

The convenient compatibility API is unchanged:

transaction do
  neo4j_query("CREATE (:Person {name: 'Ada'})")
  neo4j_query("CREATE (:Person {name: 'Grace'})")
end

Nested transaction blocks reuse one actual upstream session and transaction. Applications do not receive or pass an upstream transaction object.

Transaction context is isolated by calling thread and by the object using Neo4jBolt. Two threads using the same object receive independent upstream transactions and pooled connections.

Transactions are rollback-only after any nested operation raises, even if application code rescues the exception later. This includes database failures and ExpectedOneResultError:

transaction do
  neo4j_query("CREATE (:Marker)")

  begin
    neo4j_query("invalid cypher")
  rescue Neo4jBolt::SyntaxError
  end
end
# Nothing is committed.

Calling rollback inside a transaction marks that outer transaction rollback-only and returns nil. Calling it outside a transaction raises Neo4jBolt::Error; the old 0.3 implementation exposed the method but accidentally delegated to a nonexistent private implementation.

Upstream server/driver failures are translated to Neo4jBolt::Error. Syntax and uniqueness-constraint failures are translated narrowly to Neo4jBolt::SyntaxError and Neo4jBolt::ConstraintValidationFailedError. The upstream exception remains available as error.cause.

Constraints and indexes

The existing setup format remains supported:

setup_constraints_and_indexes(
  ["User/email", "Session/sid"],
  ["Session/expires"]
)

Managed entries retain the neo4j_bolt_ prefix. Setup removes obsolete entries with that prefix only; it does not remove arbitrary application-defined indexes or constraints.

Dump and load

The textual format is unchanged:

n {"id":0,"labels":["Person"],"properties":{"name":"Ada"}}
n {"id":1,"labels":["Person"],"properties":{"name":"Grace"}}
r {"from":0,"to":1,"type":"KNOWS","properties":{"since":2024}}
File.open("database.dump", "w") { |io| dump_database(io, progress_io: $stderr) }
File.open("database.dump", "r") do |io|
  load_database_dump(io, progress_io: $stderr)
end

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.

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.

For relational loads, the adapter assigns a random temporary label and dump-ID property 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. 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.

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.

Loading requires an empty database unless force_append: true is passed. load_database_dump owns its batch transactions and therefore rejects being called from inside transaction.

CLI

The neo4j_bolt executable retains these commands:

Command Purpose
neo4j_bolt console Open an IRB console with Neo4jBolt loaded
neo4j_bolt clear --srsly Delete all nodes and relationships
neo4j_bolt dump Write the textual database dump
neo4j_bolt load [--force] [--batch-size N] PATH Load a textual dump
neo4j_bolt index ls List constraints and indexes
neo4j_bolt index rm --force Remove all constraints and indexes
neo4j_bolt visualize Generate a GraphViz document

Use --host HOST:PORT to select a server. gli remains a runtime dependency for the CLI, and pry remains for the separate bin/console executable.

Tested Neo4j versions

The same complete integration suite is run against these exact Community images:

  • neo4j:4.4.48-community
  • neo4j:5.26.28-community
  • neo4j:2026.06.0-community

No compatibility beyond this matrix is claimed for 0.4.0.

Run one modern LTS target:

bundle exec rake spec

Run the complete sequential matrix:

bundle exec rake spec:matrix

The harness creates uniquely named disposable containers with NEO4J_AUTH=none, 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.

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.

Compatibility inventory for 0.4

API Status
bolt_host, bolt_port, bolt_verbosity Preserved
Included/extended/module-style use Preserved
neo4j_query, including incremental block form Preserved via adapter
neo4j_query_expect_one Preserved via adapter
transaction, nesting, rollback-only behavior Preserved via thread-local adapter
rollback Preserved and repaired as rollback-only
cleanup_neo4j, wait_for_neo4j Preserved via pooled driver lifecycle
setup_constraints_and_indexes Preserved with current Cypher
dump_database, load_database_dump Preserved; relationship reconstruction modernized
Named error classes Preserved with narrow upstream translation
Node and Relationship Hash behavior Preserved via recursive value adapter
Element identity and modern values Additive behavior
CLI commands Preserved
BoltSocket, BoltBuffer, protocol markers/state/parser/packer Intentionally removed private implementation details

0.4.0 migration notes

  • Ruby 3.4 or newer is required; Ruby 2.x/3.0–3.3 applications should stay on 0.3.x until upgraded.
  • The exact prerelease upstream dependency is pinned while no stable neo4j-ruby-driver 6.2.x exists.
  • Connections are pooled and safe for concurrent use instead of one mutable socket per including object.
  • TLS, routing, authentication, database selection, and other upstream-driver configuration are not newly exposed through the legacy three-setting API in this compatibility prerelease.
  • BoltSocket, BoltBuffer, ServerState, BoltMarker, UnexpectedServerResponse, State, and CypherError were undocumented wire internals and are removed.

Development

Run bin/setup, then bundle exec rake spec or bundle exec rake spec:matrix. Build without publishing with bundle exec rake build.

Bug reports and pull requests are welcome at https://github.com/specht/neo4j_bolt.