Usage

This guide walks through the driver's main API. If you have not connected yet, start with the quickstart. Runnable programs live in the examples directory.

Packages and what to open

open Neodriver exposes the whole driver: Driver, Conn, Session, Tx, Neo4jResult, Summary, and the core types Errors, Config, Addressing, Values, Temporal, Hydration and Packstream. Programs also need the eio_main library for the Eio_main.run entry point.

Connecting

let session =
  Driver.connect ~uri:"bolt://localhost:7687"
    ~auth:(Conn.basic_auth ~credentials:"password" ())
    net clock sw

Schemes and TLS

neo4j:// (routing) is supported in minimal form: routing tables are fetched over the ROUTE message (Bolt 4.3+) or, on older servers, by calling the dbms.routing.getRoutingTable procedure, and addresses are selected per access mode. Failed servers are deactivated (dropped from the routing tables and their pools closed) until a refresh re-lists them; a NotALeader/read-only failure removes the address from the writers only. Server-side routing is enabled for routed drivers (the routing context in HELLO, the ssr.enabled hint and the rt tables from RUN responses). A default-database session resolves its effective database to the server's home database (from the ROUTE response, cached per impersonated user with pool_config.home_db_cache_ttl) and sends it in RUN/BEGIN.

Sessions

Session.run sends an auto-commit query and returns a lazily streamed Neo4jResult.t:

match Session.run session ~query:"RETURN 1 AS n" ~parameters:[] with
| Ok result -> (
    match Neo4jResult.values result with
    | Ok [ [ Values.Int n ] ] -> Printf.printf "n = %Ld\n" n
    | _ -> ())
| Error error -> failwith (Errors.to_string error)

Transactions

Explicit

let conn =
  match Session.conn session with Ok conn -> conn | Error error -> failwith (Errors.to_string error)
in
let hydration = Conn.hydration conn in
match Session.begin_transaction session with
| Ok tx -> (
    match
      Tx.run tx ~hydration ~query:"CREATE (:Person {name: $name})"
        ~parameters:[ ("name", Values.String "Alice") ]
    with
    | Ok result -> (
        match Neo4jResult.consume result with
        | Ok _ -> (
            match Tx.commit tx with
            | Ok _ -> ()
            | Error error ->
                ignore (Tx.rollback tx);
                failwith (Errors.to_string error))
        | Error error ->
            ignore (Tx.rollback tx);
            failwith (Errors.to_string error))
    | Error error ->
        ignore (Tx.rollback tx);
        failwith (Errors.to_string error))
| Error error -> failwith (Errors.to_string error)

Managed (with retry)

let conn =
  match Session.conn session with Ok conn -> conn | Error error -> failwith (Errors.to_string error)
in
let hydration = Conn.hydration conn in
let created = ref 0 in
let work tx =
  match
    Tx.run tx ~hydration ~query:"CREATE (:Person {name: $name})"
      ~parameters:[ ("name", Values.String "Bob") ]
  with
  | Ok result -> (
      match Neo4jResult.consume result with
      | Ok summary ->
          created := summary.counters.nodes_created;
          Ok ()
      | Error error -> Error (Session.Driver error))
  | Error error -> Error (Session.Driver error)
in
match Session.execute session ~mode:Config.Write work with
| Ok () -> Printf.printf "created %d node(s)\n" !created
| Error (Session.Driver error) -> failwith (Errors.to_string error)
| Error Session.Client -> failwith "the application aborted the transaction"

Bookmarks and causal consistency

Neo4j clusters are eventually consistent: a write lands on one server while a later transaction may be routed to another that has not seen it yet. A bookmark is an opaque token the server returns with a successful commit — it represents the state of the database after that transaction. When a transaction carries the bookmarks of earlier writes, routing uses them to deliver it to a server that has already applied those writes. This is causal chaining (or read-your-writes): a transaction observes the writes whose bookmarks it carries.

Bookmark values

The driver represents bookmarks as a Bookmarks.t: an immutable, duplicate-free set of bookmark strings kept in the order they were given. It is the type of Session.last_bookmarks and of the session config's bookmarks. Build values with Bookmarks.of_list, combine them with Bookmarks.union, read them back with Bookmarks.to_list. Sessions and managers never invent bookmarks — they only pass on what the server returned.

A session chains itself

Once a session commits a write, the driver remembers the bookmark and sends it with the session's next query — no code needed. That is why, after a CREATE in a session, a later MATCH in the same session sees the write even on a cluster. Sessions only need help across sessions: each new session starts fresh.

Manual chaining across sessions

Session.last_bookmarks exposes what the session last committed; seed a new session with it through the config's bookmarks:

let s1 = Driver.session driver in
(* run "CREATE (:Person {name: 'Alice'})" and consume the result *)

let bookmarks = Session.last_bookmarks s1 in    (* Bookmarks.t *)
Session.close s1;

let s2 =
  Driver.session ~config:{ Session.default_config with bookmarks } driver
in
(* run "MATCH (p:Person {name: 'Alice'}) RETURN p"
   — guaranteed to see Alice, even on a cluster *)

Passing bookmarks by hand works, but a Bookmark_manager does it for you.

Bookmark managers

For many short-lived sessions (for example one per web request), pass the same Bookmark_manager to all of them: each transaction is seeded with the manager's bookmarks and every successful commit updates the manager, so the chain is maintained driver-wide without bookmarks travelling through the application:

let manager = Bookmark_manager.neo4j_bookmark_manager () in
let run query =
  let session =
    Driver.session
      ~config:{ Session.default_config with bookmark_manager = Some manager }
      driver
  in
  (* run query, consume the result *)
  Session.close session
in
run "CREATE (:Person {name: 'Alice'})";
run "MATCH (p:Person {name: 'Alice'}) RETURN p"  (* seeded with Alice's write *)

Bookmark_manager.neo4j_bookmark_manager is the built-in implementation and is thread-safe (sessions in different fibers may share it). Its optional arguments:

Any value with the Bookmark_manager.t interface (get_bookmarks / update_bookmarks) works as a manager, not just the built-in one. When a session is configured with both bookmarks and a manager, the bookmarks merge into the manager's set for the session's first transaction only; the session's own commits then drive the chain.

The runnable bookmarks example puts all three patterns together.

Authentication

Basic, bearer and custom tokens are supported. Conn.basic_auth ?principal ?credentials ?realm () is the common case; Conn.bearer_auth token sends only scheme and credentials; Auth_manager.custom_auth ... adds arbitrary schemes, a realm and extra parameters. For Bolt >= 5.1 the token is sent via a separate LOGON message after HELLO; older versions inline it in HELLO. Conn.re_auth conn auth re-authenticates when the token changes, Conn.mark_unauthenticated clears the current token (it is re-authenticated on the next use), and Conn.logon/Conn.logoff manage the authenticated state directly.

Auth managers

Driver.connect takes a plain ~auth token (wrapped in a static manager), or an optional ?auth_manager — an Auth_manager.t that supplies the current token. Auth_manager.basic ~provider rotates the password on Neo.ClientError.Security.Unauthorized; Auth_manager.bearer ~now ~provider additionally refreshes a token past its expires_at and on TokenExpired. New connections open with the manager's current token and a reused one is re-authenticated (LOGOFF + LOGON) when it rotates; an AuthorizationExpired marks every connection of the pool for re-authentication.

Session-level auth (user switching)

A session may carry its own auth token — Session.config.auth (Some token replaces the driver's auth for that session, user switching). The connection is opened with the token and re-authenticated to a changed one; this requires re-authentication support (Bolt >= 5.1).

Value types

Values.t is a plain variant, so parameters are built explicitly — there is no implicit conversion from OCaml data. The common cases:

Integers are int64 — use 42L, not 42. Null is also how you represent None inside a List or Map.

Graph, spatial, temporal and vector values are built through their types:

let born = Temporal.DateTime.of_ymd_hms (1990, 5, 17) (12, 0, 0) 0 |> Option.get in
let home = Values.Point { srid = 4326; x = 21.0122; y = 52.2297; z = None } in
let params =
  [
    ("name", Values.String "Alice");
    ("born", Values.DateTime born);
    ("home", home);
    ("tags", Values.List [ Values.String "admin"; Values.String "staff" ]);
    ("meta", Values.Map [ ("active", Values.Bool true) ]);
  ]

A small helper makes converting a custom record convenient:

let person_to_values { name; age; tags } =
  Values.Map
    [
      ("name", Values.String name);
      ("age", Values.Int age);
      ("tags", Values.List (List.map (fun t -> Values.String t) tags));
    ]

Reading values back is pattern matching:

match value with
| Values.Int n -> Printf.printf "int %Ld\n" n
| Values.String s -> Printf.printf "string %s\n" s
| Values.List items -> List.iter print_value items
| Values.Map fields -> List.iter (fun (k, v) -> ...) fields
| Values.Node node -> Printf.printf "%s\n" (String.concat "," node.labels)
| Values.Broken b -> (* the driver could not decode it *)
| _ -> ()

Values.to_string renders any value for logging. The graph types (Node, Relationship, Path) are typically read from results, not sent as parameters.

Temporal provides Date, Time, DateTime and Duration. Named time zones resolve through the embedded IANA database (1970-2040) with an LMT fallback before 1970; DateTime.of_ymd_hms, to_ymd_hms and offset_seconds handle the wall-clock/epoch conversions. Hydration converts between PackStream and Values.t; you rarely touch it directly.

Errors

Session.run, Tx.run and friends return (_, Errors.t) result. Errors.t covers server errors (Neo4j of { code; message; classification; gql_status }), Service_unavailable, Transaction_error, Configuration_error and more. Errors.to_string renders a message; Errors.is_retryable tells you whether a failure is worth retrying.

Configuration

Driver.connect options

Session settings (Session.config)

Base it on Session.default_config and update only what you need:

let config =
  { Session.default_config with
    database = Some "mydb";
    bookmarks = Bookmarks.of_list [ "bm-1" ]
  }
in
Driver.connect ~uri ~auth ~config net clock sw

How bookmarks are used (what they are, the Bookmarks.t values, manual chaining and Bookmark_managers) is covered in the "Bookmarks and causal consistency" section above.

Query and transaction options

Session.run, Session.begin_transaction and Session.execute accept:

Validated config records (Config)

Config.make_workspace_config and Config.make_pool_config build their records with validation (a Configuration_error on out-of-range values):

The pool honors max_connection_pool_size, connection_acquisition_timeout, max_connection_lifetime and liveness_check_timeout (a RESET on reuse). telemetry_disabled disables the Bolt 5.4+ TELEMETRY notifications (which also require the server to advertise telemetry.enabled); notifications_min_severity and notifications_disabled_categories are the driver-level notification filtering settings sent in HELLO (Bolt >= 5.2; None omits the field, Some [] sends an empty category list). connection_timeout, connection_write_timeout and keep_alive are not wired yet. disable_auto_commit_retries turns off the automatic one-shot retry of an auto-commit Session.run after a server failure marked idempotent — the Bolt >= 6.0 diagnostic_record._idempotent flag — which is on by default (like the Python driver).

Logging

Logging mirrors the Python driver's loggers and goes through the standard Logs library: the Log module exposes the sources Log.io (connection and Bolt message exchange), Log.pool (pool and routing), Log.session, Log.notifications and Log.auth (auth-manager token refreshes and provider failures). Logging is off by default.

Environment variables

Set NEO4J_LOG_LEVEL to one of off, error, warn, info, debug to turn logging on (the default is off, i.e. nothing is logged). NEO4J_LOG_SCOPES restricts the areas: a comma-separated list of io, pool, session, notifications, auth (default all). The variables are read once from the process-start environment (OCaml Sys.getenv semantics) and applied automatically at the first log call:

NEO4J_LOG_LEVEL=debug ./my_app
NEO4J_LOG_LEVEL=info NEO4J_LOG_SCOPES=io,pool ./my_app

Programmatic setup

Neodriver.Log.setup ~level:Debug ~scopes:[Io; Pool] () installs a Logs_fmt.reporter (to stderr) and sets the per-source levels; Log.disable () turns everything back off. Log.setup_from_env () applies the environment variables eagerly. When using your own reporter, set the levels with Logs.Src.set_level Log.io (Some Logs.Debug) (the Logs library).

Connection log lines use a "#XXXX" prefix, e.g. {v[#0001] C: RUN 'RETURN 1' {} {}[#0001] S: SUCCESS {"fields": [...]}v} The id is a per-connection counter, not a network identifier: Eio's portable API has no getsockname, so the Python driver's local-port id is replaced by a driver-assigned counter — this keeps logging working on systems without file descriptors (e.g. MirageOS). Credentials are always redacted ("*******") in HELLO/LOGON log lines, and RECORD lines log only the number of records, never the data.

Not yet implemented

See PLAN.md for the full roadmap.