nexus-graph-sdk 2.1.0

Official Rust SDK for Nexus graph database
Documentation

Nexus Rust SDK

Crates.io docs.rs License Rust CI

Official Rust SDK for Nexus graph database.

Compatibility: SDK 2.1.0 ↔ nexus-server 2.1.0. SDK and server move in lockstep on the same X.Y.Z train; mismatches within the same MAJOR.MINOR are usually fine but only the exact-match build is in the test matrix. See docs/COMPATIBILITY_MATRIX.md.

The crate is published as nexus-graph-sdk (the generic nexus-sdk name on crates.io is owned by the unrelated Nexus Workflow project). The module name stays nexus_sdk, so every use nexus_sdk::...; statement keeps working unchanged.

Installation

Add to your Cargo.toml:

[dependencies]

nexus-graph-sdk = "2.1.0"

Transports

The SDK is RPC-first starting in 1.0.0. The default endpoint is nexus://127.0.0.1:15475 (native binary RPC, length-prefixed MessagePack over TCP). URL scheme picks the transport:

URL form Transport Default port
nexus://host[:port] Binary RPC 15475
http://host[:port] HTTP/JSON 15474
https://host[:port] HTTPS/JSON 443
host[:port] (no scheme) Binary RPC 15475

Override per-client with ClientConfig.transport, per-process with the NEXUS_SDK_TRANSPORT env var (values: nexus, http, https, resp3, auto). URL scheme always wins over env/config.

See docs/specs/sdk-transport.md for the canonical contract every Nexus SDK follows.

Usage

Basic Example

use nexus_sdk::NexusClient;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client — `nexus://` picks the native binary RPC
    // transport automatically. Legacy `http://localhost:15474` still
    // works for REST/JSON.
    let client = NexusClient::new("nexus://127.0.0.1:15475")?;

    // Execute a Cypher query
    let result = client.execute_cypher("MATCH (n) RETURN n LIMIT 10", None).await?;
    println!("Found {} rows", result.rows.len());

    // Create a node
    let mut properties = HashMap::new();
    properties.insert("name".to_string(), nexus_sdk::Value::String("Alice".to_string()));
    let create_response = client.create_node(vec!["Person".to_string()], properties).await?;
    println!("Created node with ID: {}", create_response.node_id);

    // Get a node
    let get_response = client.get_node(create_response.node_id).await?;
    if let Some(node) = get_response.node {
        println!("Node: {:?}", node);
    }

    Ok(())
}

External IDs

Nodes can carry a caller-supplied stable identifier in prefixed string form. Supported variants: sha256:<64-hex>, blake3:<64-hex>, sha512:<128-hex>, uuid:<canonical>, str:<utf8 ≤256 B>, bytes:<hex ≤128 chars>.

use std::collections::HashMap;
use nexus_sdk::{NexusClient, Value};

let client = NexusClient::new("http://127.0.0.1:15474")?;

// Create with an external id (default conflict policy: "error").
let mut props = HashMap::new();
props.insert("title".to_string(), Value::String("Phase 10 spec".to_string()));
let resp = client
    .create_node_with_external_id(
        vec!["Document".to_string()],
        props.clone(),
        "str:doc-phase10",
        None,
    )
    .await?;
println!("created node id={}", resp.node_id);

// Idempotent re-creation: ON CONFLICT MATCH returns the existing node.
client
    .create_node_with_external_id(
        vec!["Document".to_string()],
        props,
        "str:doc-phase10",
        Some("match"),
    )
    .await?;

// Resolve by external id (returns `node: None` when absent).
let lookup = client.get_node_by_external_id("str:doc-phase10").await?;
if let Some(node) = lookup.node {
    println!("resolved internal id={}", node.id);
}

// Cypher round-trip works too: `RETURN n._id` projects the prefixed string.
let result = client
    .execute_cypher(
        "CREATE (n:Document {_id: 'str:doc-cypher'}) RETURN n._id",
        None,
    )
    .await?;

Live integration suite (requires a running server):

NEXUS_LIVE_HOST=http://localhost:15474 cargo test --test external_id_live -- --nocapture

With Authentication

use nexus_sdk::NexusClient;

// Using API key
let client = NexusClient::with_api_key("nexus://127.0.0.1:15475", "your-api-key")?;

// Or using username/password
let client = NexusClient::with_credentials("nexus://127.0.0.1:15475", "user", "pass")?;

// Force HTTP/JSON when you need a REST-only path
let legacy = NexusClient::new("http://127.0.0.1:15474")?;

Schema Management

// Create a label
let response = client.create_label("Person".to_string()).await?;

// List all labels. Each entry is a `LabelInfo { name, id }` —
// the second field is the catalog id allocated by the engine,
// not a count. (Renamed from `Vec<(String, u32)>` in 1.15.0.)
let labels = client.list_labels().await?;
for label in &labels.labels {
    println!("  {} (id={})", label.name, label.id);
}

// Create a relationship type
let response = client.create_rel_type("KNOWS".to_string()).await?;

// List all relationship types. Each entry is a
// `RelTypeInfo { name, id }`.
let types = client.list_rel_types().await?;
for rel_type in &types.types {
    println!("  {} (id={})", rel_type.name, rel_type.id);
}

Multi-Database Support

// List all databases
let databases = client.list_databases().await?;
println!("Available databases: {:?}", databases.databases);
println!("Default database: {}", databases.default_database);

// Create a new database
let create_result = client.create_database("mydb").await?;
println!("Created: {}", create_result.name);

// Switch to the new database
let switch_result = client.switch_database("mydb").await?;
println!("Switched to: mydb");

// Get current database
let current_db = client.get_current_database().await?;
println!("Current database: {}", current_db);

// Create data in the current database
let result = client.execute_cypher(
    "CREATE (n:Product {name: $name}) RETURN n",
    Some(serde_json::json!({"name": "Laptop"}))
).await?;

// Get database information
let db_info = client.get_database("mydb").await?;
println!("Nodes: {}, Relationships: {}", db_info.node_count, db_info.relationship_count);

// Drop database (must not be current database)
client.switch_database("neo4j").await?;  // Switch away first
let drop_result = client.drop_database("mydb").await?;

// Or connect directly to a specific database
let db_client = NexusClient::builder()
    .url("http://localhost:15474")
    .database("mydb")
    .build()?;
// All operations will use 'mydb'
let result = db_client.execute_cypher("MATCH (n) RETURN n LIMIT 10", None).await?;

High Availability with Replication

Nexus supports master-replica replication for high availability and read scaling. Use the master for all write operations and replicas for read operations.

use nexus_sdk::NexusClient;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

/// Client for Nexus cluster with master-replica topology.
pub struct NexusCluster {
    master: NexusClient,
    replicas: Vec<NexusClient>,
    replica_index: AtomicUsize,
}

impl NexusCluster {
    /// Create a new cluster client.
    ///
    /// # Arguments
    /// * `master_url` - URL of the master node (for writes)
    /// * `replica_urls` - List of replica URLs (for reads)
    pub fn new(master_url: &str, replica_urls: &[&str]) -> Result<Self, nexus_sdk::Error> {
        let master = NexusClient::new(master_url)?;
        let replicas = replica_urls
            .iter()
            .map(|url| NexusClient::new(url))
            .collect::<Result<Vec<_>, _>>()?;

        Ok(Self {
            master,
            replicas,
            replica_index: AtomicUsize::new(0),
        })
    }

    /// Get next replica using round-robin selection.
    fn get_replica(&self) -> &NexusClient {
        if self.replicas.is_empty() {
            return &self.master;
        }
        let index = self.replica_index.fetch_add(1, Ordering::Relaxed) % self.replicas.len();
        &self.replicas[index]
    }

    /// Execute write query on master.
    pub async fn write(
        &self,
        query: &str,
        params: Option<serde_json::Value>,
    ) -> Result<nexus_sdk::QueryResult, nexus_sdk::Error> {
        self.master.execute_cypher(query, params).await
    }

    /// Execute read query on replica (round-robin).
    pub async fn read(
        &self,
        query: &str,
        params: Option<serde_json::Value>,
    ) -> Result<nexus_sdk::QueryResult, nexus_sdk::Error> {
        self.get_replica().execute_cypher(query, params).await
    }

    /// Get reference to master client.
    pub fn master(&self) -> &NexusClient {
        &self.master
    }

    /// Get reference to all replicas.
    pub fn replicas(&self) -> &[NexusClient] {
        &self.replicas
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to cluster
    // Master handles all writes, replicas handle reads
    let cluster = NexusCluster::new(
        "http://master:15474",
        &["http://replica1:15474", "http://replica2:15474"],
    )?;

    // Write operations go to master
    cluster.write(
        "CREATE (n:Person {name: $name, age: $age}) RETURN n",
        Some(serde_json::json!({"name": "Alice", "age": 30})),
    ).await?;

    // Read operations are distributed across replicas
    let result = cluster.read(
        "MATCH (n:Person) WHERE n.age > $min_age RETURN n",
        Some(serde_json::json!({"min_age": 25})),
    ).await?;
    println!("Found {} persons", result.rows.len());

    // High-volume reads are load-balanced
    for _ in 0..100 {
        cluster.read("MATCH (n) RETURN count(n) as total", None).await?;
    }

    Ok(())
}

Replication Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Application                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              NexusCluster Client                     │   │
│   │   write() ──────────┐     read() ───────────────┐   │   │
│   └─────────────────────┼───────────────────────────┼───┘   │
└─────────────────────────┼───────────────────────────┼───────┘
                          │                           │
                          ▼                           ▼
              ┌───────────────────┐     ┌─────────────────────┐
              │      MASTER       │     │      REPLICAS       │
              │   (writes only)   │────▶│   (reads only)      │
              │                   │ WAL │  ┌───────────────┐  │
              │ • CREATE          │────▶│  │   Replica 1   │  │
              │ • UPDATE          │     │  └───────────────┘  │
              │ • DELETE          │     │  ┌───────────────┐  │
              │ • MERGE           │────▶│  │   Replica 2   │  │
              └───────────────────┘     │  └───────────────┘  │
                                        └─────────────────────┘

Starting a Nexus Cluster

# Start master node

NEXUS_REPLICATION_ROLE=master \

NEXUS_REPLICATION_BIND_ADDR=0.0.0.0:15475 \

./nexus-server


# Start replica 1

NEXUS_REPLICATION_ROLE=replica \

NEXUS_REPLICATION_MASTER_ADDR=master:15475 \

./nexus-server


# Start replica 2

NEXUS_REPLICATION_ROLE=replica \

NEXUS_REPLICATION_MASTER_ADDR=master:15475 \

./nexus-server

Monitoring Replication Status

use reqwest::Client;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct ReplicationStatus {
    role: String,
    running: bool,
    mode: String,
    replica_count: Option<usize>,
}

#[derive(Debug, Deserialize)]
struct MasterStats {
    entries_replicated: u64,
    connected_replicas: u32,
}

async fn check_replication_status(master_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();

    // Check master status
    let status: ReplicationStatus = client
        .get(format!("{}/replication/status", master_url))
        .send()
        .await?
        .json()
        .await?;
    println!("Role: {}", status.role);
    println!("Running: {}", status.running);
    println!("Connected replicas: {}", status.replica_count.unwrap_or(0));

    // Get master stats
    let stats: MasterStats = client
        .get(format!("{}/replication/master/stats", master_url))
        .send()
        .await?
        .json()
        .await?;
    println!("Entries replicated: {}", stats.entries_replicated);
    println!("Connected replicas: {}", stats.connected_replicas);

    Ok(())
}

Features

  • Native binary RPC transport (default on nexus:// URLs; ~3–10× lower latency vs HTTP/JSON)
  • ✅ Pluggable transport layer (TransportMode::NexusRpc / Http / Https)
  • ✅ Cypher query execution
  • ✅ Database statistics
  • ✅ Health check
  • ✅ Node CRUD operations (Create, Read, Update, Delete)
  • ✅ Relationship CRUD operations (Create, Update, Delete)
  • ✅ Batch operations (sequential implementation)
  • ✅ Schema management (Labels, Relationship Types)
  • ✅ Performance monitoring (Query statistics, slow queries, plan cache)
  • ✅ Transaction support (BEGIN, COMMIT, ROLLBACK)
  • ✅ Query builder for type-safe query construction
  • ✅ Multi-database support (create, list, switch, drop databases)
  • ✅ Retry logic with exponential backoff (basic implementation)
  • ✅ API key authentication
  • ✅ Username/password authentication
  • ✅ Proper error handling
  • ✅ Type-safe models

Dependencies Status

All dependencies are up-to-date:

  • reqwest 0.12 - Latest stable
  • tokio 1.0 - Latest stable
  • serde 1.0 - Latest stable
  • serde_json 1.0 - Latest stable
  • thiserror 1.0 - Latest 1.x (2.0 available but breaking changes)
  • anyhow 1.0 - Latest stable
  • futures 0.3 - Latest stable
  • url 2.5 - Latest stable
  • base64 0.22 - Latest stable
  • tracing 0.1 - Latest stable

Examples

See the examples/ directory for complete examples:

  • basic_usage.rs - Basic operations with nodes, relationships, and schema
  • with_auth.rs - Authentication examples
  • performance_monitoring.rs - Performance monitoring and statistics
  • transactions.rs - Transaction management examples
  • query_builder.rs - Query builder usage examples
  • multi_database.rs - Multi-database support examples

Run examples with:

cargo run --example basic_usage

cargo run --example with_auth

cargo run --example performance_monitoring

cargo run --example transactions

cargo run --example query_builder

cargo run --example multi_database

Testing

Run tests with:

# Run unit tests

cargo test


# Run integration tests (requires running Nexus server)

NEXUS_TEST_SERVER=http://localhost:15474 cargo test -- --ignored


# Run specific test suites

cargo test --test integration_test

cargo test --test transaction_test

cargo test --test query_builder_test

cargo test --test relationship_test

cargo test --test multi_database_test

Test Coverage

The SDK includes comprehensive tests for:

  • ✅ Client initialization and configuration
  • ✅ Cypher query execution
  • ✅ Node CRUD operations
  • ✅ Relationship CRUD operations (Create, Update, Delete)
  • ✅ Schema management (Labels, Relationship Types)
  • ✅ Transaction support (Begin, Commit, Rollback)
  • ✅ Query builder functionality
  • ✅ Performance monitoring
  • ✅ Batch operations
  • ✅ Multi-database support (Create, list, switch, drop databases)

Roadmap

  • Native binary RPC transport (landed 1.0.0)
  • Node CRUD operations
  • Relationship CRUD operations (Create, Update, Delete)
  • Schema management
  • Integration tests
  • Examples
  • Transaction support
  • Query builder
  • Retry logic with exponential backoff (basic implementation)
  • Neo4j compatibility comparison tests
  • RESP3 transport (wire format parsed; implementation deferred)
  • Connection pooling

License

Licensed under the Apache License, Version 2.0.

See LICENSE for details.