Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bolt & Neo4j Browser

The server speaks the native Bolt protocol, so the official Neo4j drivers and the Neo4j Browser connect to patinaDB directly. This has been validated end-to-end against Neo4j driver 6.2.0 (over both raw TCP and WebSocket) and the Neo4j Browser (node creation plus path/relationship graph visualisation).

Connecting

The Bolt listener (default 127.0.0.1:7687) auto-detects the transport: a raw Bolt TCP handshake or a WebSocket upgrade (the Browser’s JavaScript driver speaks Bolt inside WebSocket binary frames). Both work on the same port.

Neo4j Browser: open browser.neo4j.io, connect to bolt://localhost:7687, authenticate with --auth-user / --auth-password.

Official driver (Python):

from neo4j import GraphDatabase
drv = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "secret"))
with drv.session() as s:
    s.run("CREATE (a:Person {name:$n})", n="Ada")
    for rec in s.run("MATCH (n:Person) RETURN n"):
        print(rec["n"])
drv.close()

Nodes, relationships, and paths come back as proper Bolt graph types (Node / Relationship / Path), so drivers hydrate them as graph objects and the Browser visualises them.

Spatial values over Bolt

A patinaDB Point is sent as a native Neo4j Point struct, so official drivers decode it as a first-class point (neo4j.spatial.Point / a driver’s Point type):

  • a 2-D point → PackStream struct tag 0x58 with fields {srid: Integer, x: Float, y: Float};
  • a 3-D point → tag 0x59 with an added z: Float.

The srid is the CRS discriminant patinaDB already stores — cartesian 7203, cartesian-3d 9157, wgs-84 4326, wgs-84-3d 4979 (the same SRIDs Neo4j uses). For wgs-84 the point’s x is the longitude and y the latitude.

A point can also be passed as a parameter: send $center as a 0x58/0x59 struct and it decodes back into a point you can use in Cypher, e.g. RETURN distance($center, n.loc).

Polygons have no native Bolt type. Bolt only defines Point2D/Point3D, so a patinaDB Polygon is returned as a Map{type: "Polygon", srid: Integer, rings: [[[x, y], …], …]} (rings[0] is the exterior ring, rings[1..] the holes). A driver therefore sees a plain map, not a spatial object; decode it yourself if you need the geometry.

Authentication

Auth is checked at LOGON (scheme: "basic"). An unauthenticated connection cannot run queries. See Authentication & TLS.

Streaming reads

Read results stream lazily: the query runs on a worker thread that pushes hydrated records over a bounded channel, with PULL n batching and backpressure. Memory stays bounded regardless of result size, so you can stream large result sets without materialising them all server-side.

Transactions

Both autocommit and explicit transactions are real.

  • Autocommit (a bare RUN): each statement commits on its own as one engram — atomic, even for a multi-clause or bulk statement.
  • Explicit (BEGIN … COMMIT / ROLLBACK): statements between BEGIN and COMMIT are buffered, not committed one by one. BEGIN pins a consistent snapshot of the database; every RUN sees that snapshot plus the transaction’s own uncommitted writes (repeatable reads + read-your-own-writes); COMMIT applies the whole buffer as one atomic engram (a single Raft entry on a cluster) after a conflict check; ROLLBACK discards it — nothing is written.

Explicit transactions run at snapshot isolation (SI). A transaction is bound to one database — a mid-transaction USE <other> is rejected — and schema/admin statements (e.g. CREATE INDEX, CREATE DATABASE) run eagerly, not buffered. The official Neo4j drivers’ managed transaction functions work as expected, including automatic retry on the conflict error below.

Isolation: explicit transactions are snapshot-isolated, not serializable

  • BEGIN pins the database HEAD (the serialization point). Every read in the transaction observes the graph as of that moment — a commit by another connection made after your BEGIN is invisible to your transaction (repeatable reads, no phantoms). Your own buffered writes are visible to your own reads (read-your-own-writes).
  • COMMIT is first-committer-wins. Before proposing the buffer, the server checks whether any write committed since your pinned snapshot touched an entity/property your transaction also wrote. If so, the COMMIT is rejected with a transient error (Neo.TransientError.Transaction.LockClientStopped), which the Neo4j drivers’ managed-transaction functions retry automatically. This prevents lost updates (e.g. two clients that both “read a counter, increment, write it back” — one commits, the other retries against the new value instead of silently clobbering it).
  • Autocommit (a bare single-statement RUN) holds the per-database write lock for the statement, so autocommit writes to one database are serialized.

What SI does not give you: write skew

Snapshot isolation detects write-write conflicts only. A read-write conflict with disjoint write-sets (write skew) is still possible: two transactions can each read a value the other overwrites, as long as they write different things, and both commit. This is the standard, well-understood SI limitation — it is not serializable and not linearizable. If you need a constraint that spans rows one transaction reads and another writes (e.g. “at most one on-call engineer”), enforce it in a single autocommit statement or with an application-level guard. See Limitations.

Conflict granularity

Property writes conflict at (entity, property) granularity (two transactions updating different properties of the same node do not falsely conflict); create/delete and label changes conflict at whole-entity granularity. Freshly-created nodes carry new UUIDs, so concurrent inserts never conflict.

Database selection

The target database is taken per-RUN / per-BEGIN (from the Bolt db field or a USE <db> prefix) and is never sticky across autocommit statements — connections are pooled, so each statement resolves its own database. See Multi-Database.

System / introspection shim

On connect, the Neo4j Browser fires admin/introspection statements that are not ordinary Cypher (CALL dbms.components(), SHOW DATABASES, CALL db.labels(), routing-table lookups, …). The server recognises these and returns canned/registry-derived results so the Browser connects cleanly, shows a server version, and populates its database dropdown. Real procedures (notably CALL db.index.fulltext.*) are not swallowed by the shim — they reach the engine and return real data.

Cluster-aware routing

neo4j:// (routing) drivers ask the server for a routing table and then connect to the addresses it returns. In a cluster, patinaDB answers with the real topology — writes to the leader, reads spread across the followers — so a routing driver offloads reads to replicas automatically. See High Availability → Cluster-aware Bolt routing for the full WRITE/READ/ROUTE breakdown and the lag-immune clean-tag read pattern. A single-node server returns itself for all three roles.

Behind a reverse proxy

Behind a TLS-terminating proxy or load balancer, set --advertised-addr to the public host:port so the routing table sends drivers to a reachable endpoint (each node advertises its own). For a direct bolt:// connection this isn’t needed — the default advertises the listen address. See Authentication & TLS.