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).
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.
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
| Feature | Default | What it enables |
|---|---|---|
simd | off | SIMD acceleration |
embedded | off | The 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.
| Type | Alias(es) | Storage | Use for |
|---|---|---|---|
DurableConnection | Connection, Database | WAL + MVCC + crash recovery | Production / persistence |
SochConnection | InMemoryConnection | In-memory | Tests / ephemeral work |
EmbeddedConnection | — (requires embedded feature) | Full Database kernel | In-process SQL, execute_sql |
ReadOnlyConnection | — | Read-only view of a durable DB | Read 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
- Durable (default)
- In-memory
- Embedded kernel
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()?;
use sochdb::SochConnection;
let conn = SochConnection::open("./ignored_path")?; // in-memory
// Or back it with a file:
let conn = SochConnection::open_persistent("./data")?;
// Requires: features = ["embedded"]
use sochdb::EmbeddedConnection;
let conn = EmbeddedConnection::open("./data")?;
// Full kernel: in-process SQL, scans, transactions.
let result = conn.execute_sql("SELECT 1")?;
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 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.
Vector Search
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>>,
}
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)
| Method | Description |
|---|---|
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_range | Path 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
| Method | Returns |
|---|---|
insert_into(table) | RowBuilder → InsertResult |
update(table) | UpdateBuilder → UpdateResult |
delete_from(table) | DeleteBuilder → DeleteResult |
find(table) | FindBuilder → Vec<HashMap<String, SochValue>> |
upsert(table, key) | UpsertBuilder → UpsertResult |
SQL
| Entry point | Description |
|---|---|
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)