Rust SDK Guide
🔧 Skill Level: Intermediate
⏱️ Time Required: 25 minutes
📦 Requirements: Rust 1.70+, Tokio runtime
Complete guide to SochDB's Rust SDK with async IPC client, SQL integration, and zero-copy operations.
📦 Installation
Add to Cargo.toml:
[dependencies]
sochdb = "0.4.0"
tokio = { version = "1", features = ["full"] }
# Optional: For SQL support
sochdb-query = "0.4.0"
What's New in 0.2.6:
- ✅ Async IPC client with Tokio
- ✅ Enhanced scan() method for multi-tenant isolation
- ✅ SQL integration via sochdb-query
- ✅ Zero-copy read optimizations
- ✅ Improved error types
CLI Tools (v0.3.1+)
Install the official CLI tools using Cargo:
# Install sochdb-server and sochdb-bulk
cargo install sochdb-tools
# Install sochdb-grpc-server
cargo install sochdb-grpc
These binaries provide the same functionality as the Python/Node/Go wrappers but are compiled directly from source.
Deep Dive: See Server Reference for full usage.
Quick Start
Async IPC Client
use sochdb_client::IpcClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to SochDB server
let client = IpcClient::connect("./my_database/sochdb.sock").await?;
// Put and Get
client.put(b"user:123", b"{\"name\":\"Alice\",\"age\":30}").await?;
let value = client.get(b"user:123").await?;
if let Some(val) = value {
println!("{}", String::from_utf8_lossy(&val));
// Output: {"name":"Alice","age":30}
}
Ok(())
}
Output:
{"name":"Alice","age":30}
Async IPC Client
⭐ SochDB uses async IPC — no blocking operations:
Start Server
# Terminal 1: Start SochDB server
sochdb-server --db ./my_database
# Server listens at: ./my_database/sochdb.sock
Connect from Rust
use sochdb_client::IpcClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./my_database/sochdb.sock").await?;
// Check latency
let latency = client.ping().await?;
println!("Ping: {:.2}ms", latency * 1000.0);
Ok(())
}
Output:
Ping: 0.12ms
Wire Protocol:
- All integers: Little Endian
- Format:
[opcode:1][length:4 LE][payload] - Binary-safe: Handles null bytes, UTF-8
SQL Integration
⭐ Full SQL via sochdb-query:
CREATE TABLE
use sochdb_client::IpcClient;
use sochdb_query::SqlExecutor;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./my_db/sochdb.sock").await?;
let executor = SqlExecutor::new(client);
// Create table
executor.execute(r#"
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER
)
"#).await?;
// Insert data
executor.execute(r#"
INSERT INTO users (id, name, email, age)
VALUES (1, 'Alice', 'alice@example.com', 30)
"#).await?;
executor.execute(r#"
INSERT INTO users (id, name, email, age)
VALUES (2, 'Bob', 'bob@example.com', 25)
"#).await?;
println!("✅ Table created and data inserted");
Ok(())
}
Output:
✅ Table created and data inserted
SELECT Queries
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct User {
id: i32,
name: String,
email: String,
age: i32,
}
// Query all users
let results: Vec<User> = executor
.query("SELECT * FROM users")
.await?;
for user in results {
println!("{}: {} ({} years old)", user.id, user.name, user.age);
}
Output:
1: Alice (30 years old)
2: Bob (25 years old)
JOIN Queries
// Create orders table
executor.execute(r#"
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER,
product TEXT,
amount REAL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
"#).await?;
// Insert orders
executor.execute("INSERT INTO orders VALUES (1, 1, 'Laptop', 999.99)").await?;
executor.execute("INSERT INTO orders VALUES (2, 1, 'Mouse', 25.00)").await?;
executor.execute("INSERT INTO orders VALUES (3, 2, 'Keyboard', 75.00)").await?;
// JOIN query
#[derive(Debug, Deserialize)]
struct OrderSummary {
name: String,
product: String,
amount: f64,
}
let results: Vec<OrderSummary> = executor.query(r#"
SELECT users.name, orders.product, orders.amount
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.amount > 50
ORDER BY orders.amount DESC
"#).await?;
for order in results {
println!("{} bought {} for ${}", order.name, order.product, order.amount);
}
Output:
Alice bought Laptop for $999.99
Bob bought Keyboard for $75
Aggregations
#[derive(Debug, Deserialize)]
struct UserStats {
name: String,
order_count: i32,
total: f64,
}
let results: Vec<UserStats> = executor.query(r#"
SELECT
users.name,
COUNT(*) as order_count,
SUM(orders.amount) as total
FROM users
JOIN orders ON users.id = orders.user_id
GROUP BY users.name
ORDER BY total DESC
"#).await?;
for stat in results {
println!("{}: {} orders, ${:.2} total", stat.name, stat.order_count, stat.total);
}
Output:
Alice: 2 orders, $1024.99 total
Bob: 1 orders, $75.00 total
Key-Value Operations
Basic Operations
use sochdb_client::IpcClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./my_db/sochdb.sock").await?;
// Put
client.put(b"key", b"value").await?;
// Get
if let Some(value) = client.get(b"key").await? {
println!("{}", String::from_utf8_lossy(&value));
}
// Delete
client.delete(b"key").await?;
// Get after delete
let deleted = client.get(b"key").await?;
println!("After delete: {:?}", deleted);
Ok(())
}
Output:
value
After delete: None
JSON Operations
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
email: String,
age: u32,
}
// Store JSON
let user = User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
};
let json = serde_json::to_vec(&user)?;
client.put(b"users/alice", &json).await?;
// Retrieve JSON
if let Some(value) = client.get(b"users/alice").await? {
let user: User = serde_json::from_slice(&value)?;
println!("Name: {}, Age: {}", user.name, user.age);
}
Output:
Name: Alice, Age: 30
Path API
// Store hierarchical data
client.put_path(&["users", "alice", "email"], b"alice@example.com").await?;
client.put_path(&["users", "alice", "age"], b"30").await?;
client.put_path(&["users", "alice", "settings", "theme"], b"dark").await?;
// Retrieve by path
if let Some(email) = client.get_path(&["users", "alice", "email"]).await? {
println!("Alice's email: {}", String::from_utf8_lossy(&email));
}
Output:
Alice's email: alice@example.com
Wire Format:
[path_count: 2 bytes LE]
[path_len_1: 2 bytes LE][path_1: UTF-8]
[path_len_2: 2 bytes LE][path_2: UTF-8]
...
Prefix Scanning
⭐ New in 0.2.6 — Multi-tenant isolation:
// Insert multi-tenant data
client.put(b"tenants/acme/users/1", b"{\"name\":\"Alice\"}").await?;
client.put(b"tenants/acme/users/2", b"{\"name\":\"Bob\"}").await?;
client.put(b"tenants/acme/orders/1", b"{\"total\":100}").await?;
client.put(b"tenants/globex/users/1", b"{\"name\":\"Charlie\"}").await?;
// Scan only ACME Corp data (tenant isolation)
let results = client.scan(b"tenants/acme/", b"tenants/acme;").await?;
println!("ACME Corp has {} items:", results.len());
for (key, value) in results {
println!(" {}: {}",
String::from_utf8_lossy(&key),
String::from_utf8_lossy(&value)
);
}
Output:
ACME Corp has 3 items:
tenants/acme/orders/1: {"total":100}
tenants/acme/users/1: {"name":"Alice"}
tenants/acme/users/2: {"name":"Bob"}
Why use scan():
- Fast: O(|prefix|) — only reads matching keys
- Isolated: Perfect for multi-tenancy
- Efficient: Binary-safe iteration
Range trick:
// Scan "users/" to "users;" (semicolon is after '/' in ASCII)
let results = client.scan(b"users/", b"users;").await?;
// Matches: users/1, users/2, users/alice, ...
// Excludes: user, users, usersabc
Transactions
Atomic Operations
let txn = client.begin_transaction().await?;
// Perform operations
txn.put(b"account:1:balance", b"1000").await?;
txn.put(b"account:2:balance", b"500").await?;
// Commit or abort
if some_condition {
txn.commit().await?;
println!("✅ Transaction committed");
} else {
txn.abort().await?;
println!("❌ Transaction aborted");
}
Output:
✅ Transaction committed
Transaction with Scan
let txn = client.begin_transaction().await?;
txn.put(b"key1", b"value1").await?;
txn.put(b"key2", b"value2").await?;
// Scan within transaction
let results = txn.scan(b"key", b"key~").await?;
for (key, value) in results {
println!("{}: {}",
String::from_utf8_lossy(&key),
String::from_utf8_lossy(&value)
);
}
txn.commit().await?;
Output:
key1: value1
key2: value2
Query Builder
Returns results in TOON format (token-optimized):
use sochdb_client::query::QueryBuilder;
// Insert structured data
client.put(
b"products/laptop",
b"{\"name\":\"Laptop\",\"price\":999,\"stock\":5}"
).await?;
client.put(
b"products/mouse",
b"{\"name\":\"Mouse\",\"price\":25,\"stock\":20}"
).await?;
// Query with column selection
let results = QueryBuilder::new(&client)
.prefix("products/")
.select(&["name", "price"])
.limit(10)
.execute()
.await?;
for (key, value) in results {
println!("{}: {}",
String::from_utf8_lossy(&key),
String::from_utf8_lossy(&value)
);
}
Output (TOON Format):
products/laptop: result[1]{name,price}:Laptop,999
products/mouse: result[1]{name,price}:Mouse,25
TOON benefits:
- Fewer tokens for LLMs
- Structured output
- Easy parsing
Zero-Copy Reads
⭐ Avoid allocations with Bytes:
use bytes::Bytes;
// Zero-copy read (returns Bytes backed by socket buffer)
let value: Option<Bytes> = client.get_zero_copy(b"large_blob").await?;
if let Some(bytes) = value {
// No heap allocation — direct view into socket buffer
println!("Read {} bytes (zero-copy)", bytes.len());
// Can slice without copying
let slice = &bytes[0..100];
}
Performance:
- Regular
get(): AllocatesVec<u8> - Zero-copy
get_zero_copy(): Returns Bytes (shared buffer) - Best for: Large values, read-heavy workloads
Best Practices
1. Use SQL for Structured Data
// ✅ Good: Use SQL for relational data
executor.execute("CREATE TABLE users (...)").await?;
let users: Vec<User> = executor.query("SELECT * FROM users WHERE age > 25").await?;
2. Use K-V for Unstructured Data
// ✅ Good: Use K-V for documents, blobs, cache
client.put(b"cache:user:123", &json_bytes).await?;
client.put(b"blob:image:456", &image_bytes).await?;
3. Use scan() for Multi-Tenancy
// ✅ Good: Efficient tenant isolation
let tenant_id = "acme";
let prefix = format!("tenants/{}/", tenant_id);
let end = format!("tenants/{};", tenant_id);
let data = client.scan(prefix.as_bytes(), end.as_bytes()).await?;
4. Use Transactions for Atomicity
// ✅ Good: Atomic operations
let txn = client.begin_transaction().await?;
txn.put(b"key1", b"value1").await?;
txn.put(b"key2", b"value2").await?;
txn.commit().await?;
5. Use Zero-Copy for Large Reads
// ✅ Good: Zero-copy for large blobs
let blob = client.get_zero_copy(b"large_blob").await?;
// ❌ Bad: Unnecessary allocation
let blob = client.get(b"large_blob").await?; // Copies to `Vec<u8>`
6. Handle Errors Properly
// ✅ Good: Proper error handling
match client.get(b"key").await {
Ok(Some(value)) => println!("Value: {:?}", value),
Ok(None) => println!("Key not found"),
Err(e) => eprintln!("Error: {}", e),
}
// ❌ Bad: Unwrap can panic
let value = client.get(b"key").await.unwrap().unwrap();
Complete Examples
Example 1: Multi-Tenant SaaS with SQL + K-V
use sochdb_client::IpcClient;
use sochdb_query::SqlExecutor;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct Tenant {
id: i32,
name: String,
created_at: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct TenantUser {
role: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./saas_db/sochdb.sock").await?;
let executor = SqlExecutor::new(client.clone());
// SQL for tenant metadata
executor.execute(r#"
CREATE TABLE IF NOT EXISTS tenants (
id INTEGER PRIMARY KEY,
name TEXT,
created_at TEXT
)
"#).await?;
executor.execute("INSERT INTO tenants VALUES (1, 'ACME Corp', '2026-01-01')").await?;
executor.execute("INSERT INTO tenants VALUES (2, 'Globex Inc', '2026-01-01')").await?;
// K-V for tenant-specific data
let alice = TenantUser {
role: "admin".to_string(),
email: "alice@acme.com".to_string(),
};
client.put(b"tenants/1/users/alice", &serde_json::to_vec(&alice)?).await?;
let bob = TenantUser {
role: "user".to_string(),
email: "bob@acme.com".to_string(),
};
client.put(b"tenants/1/users/bob", &serde_json::to_vec(&bob)?).await?;
let charlie = TenantUser {
role: "admin".to_string(),
email: "charlie@globex.com".to_string(),
};
client.put(b"tenants/2/users/charlie", &serde_json::to_vec(&charlie)?).await?;
// Query SQL
let tenants: Vec<Tenant> = executor.query("SELECT * FROM tenants ORDER BY name").await?;
for tenant in tenants {
// Scan tenant-specific K-V data
let prefix = format!("tenants/{}/", tenant.id);
let end = format!("tenants/{};", tenant.id);
let users = client.scan(prefix.as_bytes(), end.as_bytes()).await?;
println!("\n{} ({} users):", tenant.name, users.len());
for (key, value) in users {
let user: TenantUser = serde_json::from_slice(&value)?;
println!(" {}: {} ({})",
String::from_utf8_lossy(&key),
user.email,
user.role
);
}
}
Ok(())
}
Output:
ACME Corp (2 users):
tenants/1/users/alice: alice@acme.com (admin)
tenants/1/users/bob: bob@acme.com (user)
Globex Inc (1 users):
tenants/2/users/charlie: charlie@globex.com (admin)
Example 2: E-commerce with SQL
use sochdb_client::IpcClient;
use sochdb_query::SqlExecutor;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct CategoryStats {
category: String,
order_count: i32,
revenue: f64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./ecommerce/sochdb.sock").await?;
let executor = SqlExecutor::new(client);
// Create schema
executor.execute(r#"
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL,
category TEXT
)
"#).await?;
executor.execute(r#"
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
product_id INTEGER,
quantity INTEGER,
total REAL
)
"#).await?;
// Insert data
executor.execute("INSERT INTO products VALUES (1, 'Laptop', 999.99, 'Electronics')").await?;
executor.execute("INSERT INTO products VALUES (2, 'Mouse', 25.00, 'Electronics')").await?;
executor.execute("INSERT INTO products VALUES (3, 'Desk', 299.99, 'Furniture')").await?;
executor.execute("INSERT INTO orders VALUES (1, 1, 2, 1999.98)").await?;
executor.execute("INSERT INTO orders VALUES (2, 2, 5, 125.00)").await?;
// Analytics query
let results: Vec<CategoryStats> = executor.query(r#"
SELECT
products.category,
COUNT(orders.id) as order_count,
SUM(orders.total) as revenue
FROM products
JOIN orders ON products.id = orders.product_id
GROUP BY products.category
ORDER BY revenue DESC
"#).await?;
println!("Category Performance:");
for stat in results {
println!("{}: {} orders, ${:.2}", stat.category, stat.order_count, stat.revenue);
}
Ok(())
}
Output:
Category Performance:
Electronics: 2 orders, $2124.98
Example 3: Session Cache
use sochdb_client::IpcClient;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Serialize, Deserialize)]
struct Session {
user_id: String,
token: String,
expires_at: u64,
}
struct SessionStore {
client: IpcClient,
}
impl SessionStore {
fn new(client: IpcClient) -> Self {
Self { client }
}
async fn create(&self, user_id: &str, token: &str, ttl_ms: u64) -> Result<(), Box<dyn std::error::Error>> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
let session = Session {
user_id: user_id.to_string(),
token: token.to_string(),
expires_at: now + ttl_ms,
};
let key = format!("sessions/{}", token);
self.client.put(key.as_bytes(), &serde_json::to_vec(&session)?).await?;
Ok(())
}
async fn get(&self, token: &str) -> Result<Option<Session>, Box<dyn std::error::Error>> {
let key = format!("sessions/{}", token);
if let Some(value) = self.client.get(key.as_bytes()).await? {
let session: Session = serde_json::from_slice(&value)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
if now > session.expires_at {
self.delete(token).await?;
return Ok(None);
}
return Ok(Some(session));
}
Ok(None)
}
async fn delete(&self, token: &str) -> Result<(), Box<dyn std::error::Error>> {
let key = format!("sessions/{}", token);
self.client.delete(key.as_bytes()).await?;
Ok(())
}
async fn cleanup(&self) -> Result<usize, Box<dyn std::error::Error>> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
let results = self.client.scan(b"sessions/", b"sessions;").await?;
let mut removed = 0;
for (key, value) in results {
let session: Session = serde_json::from_slice(&value)?;
if now > session.expires_at {
self.client.delete(&key).await?;
removed += 1;
}
}
Ok(removed)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IpcClient::connect("./sessions/sochdb.sock").await?;
let store = SessionStore::new(client);
// Create sessions
store.create("user1", "token123", 60000).await?; // 1 minute
store.create("user2", "token456", 120000).await?; // 2 minutes
println!("Created 2 sessions");
// Retrieve session
if let Some(session) = store.get("token123").await? {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
let expires_in = (session.expires_at - now) / 1000;
println!("Session for {}: expires in {}s", session.user_id, expires_in);
}
// Cleanup expired
let removed = store.cleanup().await?;
println!("Cleaned up {} expired sessions", removed);
Ok(())
}
Output:
Created 2 sessions
Session for user1: expires in 60s
Cleaned up 0 expired sessions
API Reference
IpcClient
| Method | Description |
|---|---|
IpcClient::connect(path) | Connect to server |
ping() | Check latency |
put(key, value) | Store key-value |
get(key) | Retrieve value (None if not found) |
get_zero_copy(key) | Zero-copy read ⭐ |
delete(key) | Delete key |
put_path(path, value) | Store by path |
get_path(path) | Get by path |
scan(start, end) | Iterate range ⭐ |
begin_transaction() | Begin transaction |
Transaction
| Method | Description |
|---|---|
put(key, value) | Store in transaction |
get(key) | Retrieve from transaction |
delete(key) | Delete in transaction |
scan(start, end) | Scan in transaction |
commit() | Commit changes |
abort() | Rollback changes |
SqlExecutor
| Method | Description |
|---|---|
SqlExecutor::new(client) | Create executor |
execute(query) | Execute DDL/DML |
query<T>(query) | Execute SELECT (returns Vec<T>) |
Resources
Last updated: January 2026 (v0.3.1)