Skip to main content

Rust SDK Guide

🔧 Skill Level: Intermediate ⏱️ Time Required: 25 minutes 📦 Requirements: Rust 1.85+ (edition 2024) Version: 2.0.3

Complete guide to SochDB's Rust SDK: a synchronous, embedded database client with WAL durability, MVCC transactions, SQL, vector search, and a set of agent-oriented overlays (graph, temporal graph, policy, routing, queue, semantic cache, atomic memory).

License

The SochDB core engine — the Rust workspace, the sochdb crate, the server, and the MCP server — is licensed AGPL-3.0-or-later (commercial licensing is available). The language SDKs (Python, Node.js, Go) are Apache-2.0. Because the Rust SDK is part of the core workspace, it inherits AGPL-3.0-or-later.

Embedded, synchronous, in-process

The current sochdb crate is a synchronous, in-process embedded database. There is no async API, no Tokio dependency, and no network/IPC client in this crate. Remote access (gRPC, MCP) is provided by separate components and the generated language SDKs. Older docs referencing an async IpcClient describe an API that no longer exists.


📦 Installation

The user-facing Rust SDK is the crate named sochdb (its source lives in the sochdb-client/ directory, but the published crate name is sochdb).

cargo add sochdb

Or add it to Cargo.toml directly:

[dependencies]
sochdb = "2.0.3"

Cargo features

FeatureDefaultWhat it enables
simdoffSIMD acceleration
embeddedoffThe full Database kernel via EmbeddedConnection / DurableSochClient (in-process SQL execution, etc.)
[dependencies]
sochdb = { version = "2.0.3", features = ["embedded"] }

Quick Start

use sochdb::Connection;

fn main() -> sochdb::Result<()> {
// Durable, WAL-backed connection (default).
let conn = Connection::open("./example_db")?;

conn.put(b"greeting", b"Hello, SochDB!")?;
if let Some(v) = conn.get(b"greeting")? {
println!("{}", String::from_utf8_lossy(&v));
}

// Hierarchical path keys.
conn.put_path("users/alice/name", b"Alice Smith")?;
let name = conn.get_path("users/alice/name")?;
println!("{:?}", name.map(|b| String::from_utf8_lossy(&b).into_owned()));

Ok(())
}

Output:

Hello, SochDB!
Some("Alice Smith")

For tests or throwaway data, use the in-memory connection:

use sochdb::SochConnection;

let conn = SochConnection::open("./vector_db")?; // path is ignored; data is in-memory

Connection Types

The crate exposes several connection kinds plus type aliases. All are synchronous.

TypeAlias(es)StorageUse for
DurableConnectionConnection, DatabaseWAL + MVCC + crash recoveryProduction / persistence
SochConnectionInMemoryConnectionIn-memoryTests / ephemeral work
EmbeddedConnection— (requires embedded feature)Full Database kernelIn-process SQL, execute_sql
ReadOnlyConnectionRead-only view of a durable DBRead replicas / safety
pub use connection::DurableConnection;
pub type Connection = DurableConnection; // default, WAL-durable
pub type Database = DurableConnection; // alias

pub use connection::SochConnection;
pub type InMemoryConnection = SochConnection; // in-memory, testing

Opening connections

use sochdb::Connection;

// Default durable open.
let conn = Connection::open("./data")?;

// With explicit durability/group-commit config.
use sochdb::connection::{ConnectionConfig, Durability};
let cfg = ConnectionConfig::default(); // see defaults below
let conn = Connection::open_with_config("./data", cfg)?;

// Ephemeral durable connection (temporary directory).
let tmp = Connection::open_ephemeral()?;

ConnectionConfig and durability

pub struct ConnectionConfig {
pub group_commit: bool,
pub sync_mode: Durability,
pub enable_ordered_index: bool,
pub group_commit_batch_size: usize,
pub group_commit_max_wait_us: u64,
}

pub enum Durability {
Full, // crash-durable
NormalMayLoseRecentCommits, // may lose the most recent commits
Unsafe, // no fsync
}

Default ConnectionConfig: group_commit = true, sync_mode = Durability::Full, enable_ordered_index = true, group_commit_batch_size = 100, group_commit_max_wait_us = 10_000.

Presets are available: ConnectionConfig::throughput_optimized(), latency_optimized(), and max_durability(). Only Durability::Full is crash-durable (Durability::is_crash_durable()).


Key-Value Operations

Byte-key API (DurableConnection)

use sochdb::Connection;

let conn = Connection::open("./kv_db")?;

conn.put(b"key", b"value")?;

if let Some(value) = conn.get(b"key")? { // -> Option<Vec<u8>>
println!("{}", String::from_utf8_lossy(&value));
}

conn.delete(b"key")?;

// Prefix scan -> Result<Vec<(Vec<u8>, Vec<u8>)>>
let items = conn.scan(b"users/")?;
println!("found {} items", items.len());

Output:

value
found 0 items

Path API

Hierarchical, slash-delimited keys with their own helpers:

conn.put_path("users/alice/email", b"alice@example.com")?;
conn.put_path("users/alice/settings/theme", b"dark")?;

if let Some(email) = conn.get_path("users/alice/email")? {
println!("Alice's email: {}", String::from_utf8_lossy(&email));
}

let subtree = conn.scan_path("users/alice")?; // descendants
let range = conn.scan_range("users/a", "users/b")?; // ordered range
conn.delete_path("users/alice/settings/theme")?;

Output:

Alice's email: alice@example.com

For read-heavy paths, fast read-transaction variants avoid per-call setup:

let txn = conn.begin_read_txn_fast(); // -> u64 (txn id)
let v = conn.get_path_fast("users/alice/email")?;
let scan = conn.scan_path_fast("users/alice")?;
let range = conn.scan_range_fast("users/a", "users/b")?;

Durability / maintenance

conn.fsync()?;                 // flush to disk
let lsn = conn.checkpoint()?; // -> u64
let reclaimed = conn.gc()?; // -> usize bytes/records reclaimed
let recovery = conn.recover()?;
let stats = conn.stats(); // DurableStats

Transactions

Connection-level transactions

let conn = Connection::open("./txn_db")?;

let txn_id = conn.begin_txn()?; // -> u64
conn.put_path("account/1/balance", b"1000")?;
conn.put_path("account/2/balance", b"500")?;
let commit_lsn = conn.commit_txn()?; // -> u64
// or: conn.abort_txn()?;

ClientTransaction (snapshot isolation)

ClientTransaction runs over a SochConnection and supports table-scoped reads/writes with isolation levels:

use sochdb::SochConnection;
use sochdb::transaction::{ClientTransaction, IsolationLevel};

let conn = SochConnection::open("./mem")?;

// IsolationLevel: ReadCommitted | SnapshotIsolation (default) | Serializable
let mut txn = ClientTransaction::begin(&conn, IsolationLevel::SnapshotIsolation)?;
txn.put("accounts", b"alice".to_vec(), b"1000".to_vec())?;
let bal = txn.get("accounts", b"alice")?; // -> Option<Vec<u8>>
txn.delete("accounts", b"temp".to_vec())?;

let result = txn.commit()?; // -> CommitResult
// or: txn.rollback()?;

On a write/write conflict the SDK returns ClientError::SerializationFailure { our_txn, conflicting_txn, conflicting_key } — retry the transaction.

SnapshotReader provides point-in-time reads (SnapshotReader::now(conn) or at_timestamp(conn, ts)), and BatchWriter batches writes (write / delete / flush).


Typed CRUD Builders

src/crud.rs adds fluent, typed builders to the in-memory SochConnection. Field values accept anything that converts into a SochValue.

use sochdb::prelude::*;
use sochdb::SochConnection;

let conn = SochConnection::open("./crud_db")?;

// INSERT
let res = conn.insert_into("users")
.set("id", 1)
.set("name", "Alice")
.set("email", "alice@example.com")
.execute()?; // -> InsertResult

// FIND (returns Vec<HashMap<String, SochValue>>)
let rows = conn.find("users")
.select(&["name", "email"])
.where_eq("id", 1)
.order_by("name", true)
.limit(10)
.execute()?;

// UPDATE
conn.update("users")
.set("email", "alice@new.example.com")
.where_eq("id", 1)
.execute()?; // -> UpdateResult

// DELETE
conn.delete_from("users")
.where_eq("id", 1)
.execute()?; // -> DeleteResult

// UPSERT (insert-or-update on a conflict key)
conn.upsert("users", "id")
.set("id", 1)
.set("name", "Alice")
.execute()?; // -> UpsertResult

One-shot helpers also exist: insert_one, find_by_id, delete_by_id, count(table), exists(table, field, val), update_where, and delete_where.

For filtering beyond equality, use where_cond(field, CompareOp, value) on the builders, or the low-level PathQuery:

use sochdb::path_query::{PathQuery, CompareOp};

let result = PathQuery::from_path(&conn, "users")
.filter("age", CompareOp::Gt, 21)
.where_like("name", "Al%")
.project(&["name", "age"])
.order_by("age", true)
.limit(20)
.execute()?; // -> SochResult (TOON-formatted)

SQL

The SDK offers several SQL entry points. The simplest is the free-function API in sql_entry. These run against the in-memory SochConnection; for SQL over a durable on-disk database use EmbeddedConnection::execute_sql (requires the embedded feature).

use sochdb::{SochConnection, sql_entry};

let conn = SochConnection::open("./sql_db")?;

let result = sql_entry::execute(&conn,
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")?;

sql_entry::execute(&conn,
"INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)")?;

// Parameterized
sql_entry::execute_with_params(&conn,
"INSERT INTO users (id, name, age) VALUES (?, ?, ?)",
&[2i64.into(), "Bob".into(), 25i64.into()])?;

// With execution stats
let (rows, stats) = sql_entry::execute_with_stats(&conn, "SELECT * FROM users")?;

Prepared statements and a programmatic query builder are also available:

use sochdb::sql_entry::{PreparedStatement, QueryBuilder};

let stmt = PreparedStatement::prepare(&conn, "SELECT * FROM users WHERE age > ?")?;
let result = stmt.execute(&[21i64.into()])?;

let (sql, params) = QueryBuilder::select(&conn, "users")
.columns(&["name", "age"])
.where_eq("id", 1)
.order_by("name")
.limit(10)
.to_sql();

Alternatively, connection methods expose the AST-based path: conn.query_ast(sql), conn.query_rows_ast(sql), conn.execute_ast(sql), and conn.sql_execute(sql). The QueryExecutor (src/query.rs) provides execute, query_rows, and execute_sql as well. All of these return a QueryResult.

SQL feature coverage

SQL support is broad but not complete. JOINs (INNER / LEFT / RIGHT / FULL / CROSS), GROUP BY, HAVING, ORDER BY, LIMIT/OFFSET, and CREATE/DROP INDEX are supported via the production executor. DISTINCT, window functions, CTEs, and subqueries in WHERE/SELECT are not yet implemented. MEDIAN and STDDEV aggregates are available only on one of the SQL paths (the sql/aggregate.rs engine), not the core volcano operator. CAST currently passes the inner value through without real type coercion. See the SQL guide for details.


The client VectorCollection (src/vectors.rs) provides similarity search over an in-memory backend.

use std::sync::Arc;
use sochdb::SochConnection;
use sochdb::vectors::VectorCollection;

let conn = Arc::new(SochConnection::open("./vec_db")?);

// Create a collection with a fixed dimension.
let mut col = VectorCollection::create(&conn, "embeddings", 384)?;

col.add(
&["doc1", "doc2"],
&[vec![0.1; 384], vec![0.2; 384]],
)?;
col.add_one("doc3", vec![0.15; 384])?;

// Top-k search -> Vec<SearchResult>
let results = col.search(&vec![0.12; 384], 5)?;
for r in &results {
println!("{} (distance {:.4})", r.id, r.distance);
}
pub struct SearchResult {
pub id: String,
pub distance: f32,
pub metadata: Option<HashMap<String, SochValue>>,
}
Brute-force backend in this crate

The client VectorCollection does not expose HNSW or any HNSW config (m, ef_construction, ef_search). For collections under VAMANA_THRESHOLD = 100_000 vectors it uses a brute-force linear scan; above that it migrates to a Vamana + Product Quantization backend (train_pq(), is_pq_trained(), compression_ratio()). The full HNSW index — with config M = 32, ef_construction = 256, ef_search = 500, metric = Cosine, precision F32 — lives in the separate sochdb-index crate, not in this SDK. See HNSW Vector Search for the engine-side index.

PQ-related defaults on the migration backend: DEFAULT_PQ_SUBSPACES = 8, DEFAULT_PQ_CENTROIDS = 256, DEFAULT_PQ_ITERATIONS = 20, MIN_PQ_TRAINING_VECTORS = 1000, DEFAULT_MIGRATION_BATCH_SIZE = 1000.


Context Query (token-budgeted)

ContextQueryBuilder (src/context_query.rs) assembles an LLM context window from multiple data sections under a token budget.

use std::sync::Arc;
use sochdb::SochConnection;
use sochdb::context_query::{ContextQueryBuilder, ContextFormat};

let conn = Arc::new(SochConnection::open("./ctx_db")?);

let result = ContextQueryBuilder::new()
.with_connection(conn.clone())
.for_session("session-123")
.for_agent("agent-7")
.with_budget(4000) // token budget
.include_schema(true)
.format(ContextFormat::Soch) // TOON (the native format); also Json, Markdown, Text
.section("history", 10) // name, priority
.last(20)
.done()
.section("knowledge", 5)
.search("docs", "query_var", 8) // collection, query var, top_k
.min_score(0.7)
.done()
.execute()?; // -> ContextQueryResult

println!("budget utilization: {:.1}%", result.utilization() * 100.0);

ContextQueryResult reports utilization(), included_sections, dropped_sections, and truncated_sections. See the context query guide for the full feature set.


Agent Overlays

The SDK ships several overlays that are generic over C: ConnectionTrait (implemented for the owned DurableConnection and SochConnection types). Each overlay constructor takes ownership of the connection (new(conn: C, ..)), so pass an owned connection, not a reference.

Graph (src/graph.rs)

use sochdb::graph::{GraphOverlay, EdgeDirection};

let graph = GraphOverlay::new(conn, "social"); // takes ownership of conn
// add_node / get_node / update_node / delete_node(id, cascade)
// add_edge / get_edges(from, Option<type>) / get_incoming_edges / delete_edge
// bfs / dfs / shortest_path / get_neighbors / get_nodes_by_type(type, limit) / get_subgraph

GraphNode { id, node_type, properties }, GraphEdge { from_id, edge_type, to_id, properties }, and traversal enums TraversalOrder { BFS, DFS } / EdgeDirection { Outgoing, Incoming, Both }. See Graph Overlay.

Temporal Graph (src/temporal_graph.rs)

Bitemporal edges with validity windows:

use sochdb::temporal_graph::TemporalGraphOverlay;

let tg = TemporalGraphOverlay::new(conn, "kg");
// add_edge_at(.., t) / invalidate_edge_at(.., t)
// get_edges_at(.., t) / get_edges_in_window(start, end) / neighbors_at / subgraph_at / edge_history

Helper types include TimeInterval, TemporalEdge::is_valid_at(t), TemporalQuery, and TimeBucket (with Timestamp = u64).

Policy (src/policy.rs)

Write/read/delete hooks, rate limits, and an audit log:

use sochdb::policy::{PolicyEngine, PolicyAction};

let engine = PolicyEngine::new(conn);
engine.before_write("secrets/*", |ctx| {
// inspect ctx, return a PolicyAction
PolicyAction::Allow
});
engine.add_rate_limit("write", 100, "global"); // operation, max_per_minute, scope
engine.enable_audit();
let log = engine.get_audit_log(50); // Vec<AuditEntry>

Hooks return a PolicyAction; helpers like deny_all(), allow_all(), require_agent_id(), and redact_value(...) are provided. See Policy & Safety Hooks.

Tool Routing (src/routing.rs)

use sochdb::routing::ToolDispatcher;

let dispatcher = ToolDispatcher::new(conn.clone());
dispatcher.register_local_agent(/* .. */);
dispatcher.register_tool(/* Tool */);
// dispatcher.invoke(..)
let agents = dispatcher.list_agents();
let tools = dispatcher.list_tools();

Lower-level AgentRegistry, ToolRouter, and RoutingStrategy / ToolCategory enums are available for custom routing. See Tool Routing.

Priority Queue (src/queue.rs)

use sochdb::prelude::*;
use sochdb::queue::{PriorityQueue, QueueConfig};

let config = QueueConfig::default() // see defaults below
.with_visibility_timeout(30_000)
.with_max_attempts(3);
let queue = PriorityQueue::new(config);

let task = queue.enqueue(/* priority */ 5, b"payload".to_vec()); // -> Task (not a Result)

let dequeued = queue.dequeue("worker-1"); // -> DequeueResult (not a Result)
// process, then ack/nack return Result<(), String>:
// queue.ack(&task_id)?;
// queue.nack(&task_id, /* new_priority */ None, /* delay_ms */ None)?;

QueueConfig defaults: default_visibility_timeout_ms = 30_000, max_attempts = 3, ascending_priority = true, dead_letter_queue_id = None.

Semantic Cache (src/semantic_cache.rs)

Similarity-aware result cache with provenance tracking:

use sochdb::semantic_cache::{SemanticCache, SemanticCacheConfig};

let cache = SemanticCache::new(conn);
// lookup(..) / store(..) / delete(&CacheKey)
// invalidate_by_source(doc_id) / get_provenance(&CacheKey) / cleanup_expired()

SemanticCacheConfig defaults: default_ttl = 3600s (1h), similarity_threshold = 0.95, max_entries = 10_000, enable_semantic_search = true, track_hits = true.

Atomic Memory (src/atomic_memory.rs)

Atomically write a bundle of blobs, embeddings, and graph nodes/edges, with crash recovery:

use sochdb::atomic_memory::{AtomicMemoryWriter, MemoryWriteBuilder};

let writer = AtomicMemoryWriter::new(conn);

let ops = MemoryWriteBuilder::new("memory-42")
.put_blob("note", b"hello".to_vec())
.put_embedding("vec", vec![0.1; 384])
.create_node("entity", /* props */)
.create_edge("relates_to", /* .. */);

// writer.write_atomic(ops)?;
let report = writer.recover()?; // -> RecoveryReport (replay incomplete intents)

Error Handling

All fallible APIs return sochdb::Result<T> = Result<T, ClientError>.

pub enum ClientError {
PathNotFound(String), ScalarPath(String), Schema(String), Validation(String),
NotFound(String), Transaction(String),
SerializationFailure { our_txn: u64, conflicting_txn: u64, conflicting_key: Vec<u8> },
Visibility(String), Wal(String), Storage(String),
Io(std::io::Error), Serialization(String), Parse(String), Constraint(String),
Vector(String), PqNotTrained,
TypeMismatch { expected: String, actual: String },
TokenBudgetExceeded { used: usize, budget: usize },
PoolExhausted, Query(String), Internal(String),
}
match conn.get(b"key") {
Ok(Some(value)) => println!("Value: {:?}", value),
Ok(None) => println!("Key not found"),
Err(e) => eprintln!("Error: {}", e),
}

High-Level Facade: SochClient

SochClient wraps an in-memory SochConnection and bundles the common operations behind one type. For durability, use Connection / DurableConnection directly, or DurableSochClient (requires the embedded feature).

use sochdb::SochClient;

let client = SochClient::open("./data")?; // backed by SochConnection (in-memory)
let client = client.with_token_budget(8000);

let query = client.query("users"); // -> PathQuery
let vectors = client.vectors("embeddings")?; // -> VectorCollection
let result = client.execute("SELECT * FROM users")?; // -> QueryResult
let txn = client.begin()?; // snapshot-isolated ClientTransaction
let stats = client.stats(); // ClientStats

ClientConfig defaults: token_budget = None, streaming = false, output_format = OutputFormat::Soch (TOON), pool_size = 10. OutputFormat is one of Soch, Json, or Columnar.


API Reference

Connection (DurableConnection = Connection = Database)

MethodDescription
Connection::open(path)Open durable, WAL-backed DB
open_with_config(path, cfg)Open with ConnectionConfig
open_ephemeral()Temporary durable DB
put(key, value) / get(key) / delete(key)Byte-key KV
scan(prefix)Prefix scan → Vec<(Vec<u8>, Vec<u8>)>
put_path / get_path / delete_path / scan_path / scan_rangePath API
begin_txn() / commit_txn() / abort_txn()Transactions
fsync() / checkpoint() / gc() / recover()Durability / maintenance
register_table(..) / insert_row_slice(..) / bulk_insert_slice(..)Schema / bulk insert
stats()DurableStats

CRUD builders

MethodReturns
insert_into(table)RowBuilderInsertResult
update(table)UpdateBuilderUpdateResult
delete_from(table)DeleteBuilderDeleteResult
find(table)FindBuilderVec<HashMap<String, SochValue>>
upsert(table, key)UpsertBuilderUpsertResult

SQL

Entry pointDescription
sql_entry::execute(conn, sql)Execute SQL → QueryResult
sql_entry::execute_with_params(..)Parameterized execution
sql_entry::execute_with_stats(..)Returns (QueryResult, QueryStats)
PreparedStatement::prepare(conn, sql)Prepared statement
conn.query_ast(sql) / conn.sql_execute(sql)AST-based execution

Resources


Last updated: June 2026 (core engine v2.0.3)