11 releases
Uses new Rust 2024
| new 0.3.4 | Jan 8, 2026 |
|---|---|
| 0.3.3 | Jan 6, 2026 |
| 0.2.9 | Jan 3, 2026 |
#35 in Database implementations
Used in toondb-mcp
6MB
123K
SLoC
ToonDB Rust Client
The official Rust client SDK for ToonDB — a high-performance embedded document database with HNSW vector search, built-in multi-tenancy, and SQL support.
Features
- ✅ Zero-Copy Reads — Direct access to memory-mapped data
- ✅ Native Vector Search — Built-in HNSW index for embeddings
- ✅ SQL Support — Full SQL via toondb-query integration
- ✅ IPC Client — Connect to ToonDB server (async)
- ✅ Multi-Tenancy — Efficient prefix scanning for data isolation
- ✅ ACID Transactions — Snapshot isolation with automatic commit/abort
- ✅ Thread-Safe — Safe concurrent access with MVCC
- ✅ Columnar Storage — Efficient for analytical queries
Installation
Add to your Cargo.toml:
[dependencies]
toondb = "0.3" # Or specific version like "0.3.0"
tokio = { version = "1", features = ["full"] } # For async IPC
What's New in Latest Release
🎯 Namespace Isolation
Logical database namespaces for true multi-tenancy without key prefixing:
use toondb::{Database, NamespaceHandle};
let db = Database::open("./my_database")?;
// Create isolated namespaces
let user_db = db.namespace("users")?;
let orders_db = db.namespace("orders")?;
// Keys don't collide across namespaces
user_db.put(b"123", b r#"{"name":"Alice"}"#)?;
orders_db.put(b"123", b r#"{"total":500}"#)?; // Different "123"!
// Each namespace has isolated collections
user_db.create_collection("profiles", CollectionConfig {
vector_dim: 384,
index_type: IndexType::HNSW,
metric: DistanceMetric::Cosine,
..Default::default()
})?;
🔍 Hybrid Search
Combine dense vectors (HNSW) with sparse BM25 text search:
use toondb_vector::{HybridSearchEngine, HybridQuery, RRFFusion};
// Create collection with hybrid search
let config = CollectionConfig {
vector_dim: 384,
index_type: IndexType::HNSW,
enable_bm25: true, // Enable text search
..Default::default()
};
let collection = db.create_collection("documents", config)?;
// Insert documents with text and vectors
let doc = Document {
id: "doc1".to_string(),
text: Some("Machine learning models for NLP tasks".to_string()),
vector: vec![0.1, 0.2, 0.3, /* ... 384 dims */],
};
collection.insert(&doc)?;
// Hybrid search (vector + text)
let query = HybridQuery {
vector: query_embedding,
text: Some("NLP transformer".to_string()),
k: 10,
alpha: 0.7, // 70% vector, 30% BM25
rrf_fusion: true, // Reciprocal Rank Fusion
};
let results = collection.hybrid_search(&query)?;
📄 Multi-Vector Documents
Store multiple embeddings per document (e.g., title + content):
use toondb_vector::MultiVectorDocument;
use std::collections::HashMap;
// Insert document with multiple vectors
let mut vectors = HashMap::new();
vectors.insert("title".to_string(), title_embedding);
vectors.insert("abstract".to_string(), abstract_embedding);
vectors.insert("content".to_string(), content_embedding);
let multi_doc = MultiVectorDocument {
id: "article1".to_string(),
text: Some("Deep Learning: A Survey".to_string()),
vectors,
};
collection.insert_multi_vector(&multi_doc)?;
// Search with aggregation strategy
let mut query_vectors = HashMap::new();
query_vectors.insert("title".to_string(), query_title_embedding);
query_vectors.insert("content".to_string(), query_content_embedding);
let results = collection.multi_vector_search(
&query_vectors,
10, // k
AggregationStrategy::MaxPooling // or MeanPooling, WeightedSum
)?;
🧩 Context-Aware Queries
Optimize retrieval for LLM context windows:
use toondb::ContextQuery;
// Query with token budget
let config = ContextQueryConfig {
vector: query_embedding,
max_tokens: 4000,
target_provider: Some("gpt-4".to_string()),
dedup_strategy: DeduplicationStrategy::Semantic,
};
let results = collection.context_query(&config)?;
// Results fit within 4000 tokens, deduplicated for relevance
Quick Start
IPC Client (Async)
use toondb::IpcClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to ToonDB server
let mut client = IpcClient::connect("./my_database/toondb.sock").await?;
// Put and Get
client.put(b"user:123", b r#"{"name":"Alice","age":30}"#).await?;
let value = client.get(b"user:123").await?;
if let Some(data) = value {
println!("{}", String::from_utf8_lossy(&data));
// Output: {"name":"Alice","age":30}
}
Ok(())
}
Start server first:
toondb-server --db ./my_database
# Output: [IpcServer] Listening on "./my_database/toondb.sock"
Embedded Mode (Direct FFI)
For single-process applications with maximum performance:
use toondb_core::Database;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open database
let db = Database::open("./my_database")?;
// Key-value operations
db.put(b"key", b"value")?;
let value = db.get(b"key")?;
if let Some(data) = value {
println!("{}", String::from_utf8_lossy(&data));
// Output: value
}
Ok(())
}
Core Operations
Basic Key-Value
// Put
client.put(b"key", b"value").await?;
// Get
match client.get(b"key").await? {
Some(value) => println!("{}", String::from_utf8_lossy(&value)),
None => println!("Key not found"),
}
// Delete
client.delete(b"key").await?;
Output:
value
Key not found (after delete)
Path Operations
// Hierarchical data storage
client.put_path("users/alice/email", b"[email protected]").await?;
client.put_path("users/alice/age", b"30").await?;
client.put_path("users/bob/email", b"[email protected]").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
Prefix Scanning ⭐
The most efficient way to iterate keys with a common prefix:
use toondb::IpcClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = IpcClient::connect("./db/toondb.sock").await?;
// Insert multi-tenant data
client.put(b"tenants/acme/users/1", b r#"{"name":"Alice"}"#).await?;
client.put(b"tenants/acme/users/2", b r#"{"name":"Bob"}"#).await?;
client.put(b"tenants/acme/orders/1", b r#"{"total":100}"#).await?;
client.put(b"tenants/globex/users/1", b r#"{"name":"Charlie"}"#).await?;
// Scan only ACME Corp data (tenant isolation)
let results = client.scan("tenants/acme/").await?;
println!("ACME Corp has {} items:", results.len());
for kv in results {
println!(" {}: {}",
String::from_utf8_lossy(&kv.key),
String::from_utf8_lossy(&kv.value)
);
}
Ok(())
}
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|) performance
- Isolated: Perfect for multi-tenant apps
- Efficient: Zero-copy reads from storage
Transactions
Automatic Transactions
// Transaction with automatic commit/abort
client.with_transaction(|txn| async move {
txn.put(b"account:1:balance", b"1000").await?;
txn.put(b"account:2:balance", b"500").await?;
Ok(())
}).await?;
Output:
✅ Transaction committed
Manual Transaction Control
let txn = client.begin_transaction().await?;
txn.put(b"key1", b"value1").await?;
txn.put(b"key2", b"value2").await?;
// Commit or abort
if success {
client.commit_transaction(txn).await?;
} else {
client.abort_transaction(txn).await?;
}
SQL Operations
ToonDB supports full SQL via the toondb-query crate:
use toondb_query::QueryEngine;
use toondb_core::Database;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = Database::open("./sql_db")?;
let query_engine = QueryEngine::new(db);
// Create table
query_engine.execute(r#"
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER
)
"#)?;
// Insert data
query_engine.execute(r#"
INSERT INTO users (id, name, email, age)
VALUES (1, 'Alice', '[email protected]', 30)
"#)?;
query_engine.execute(r#"
INSERT INTO users (id, name, email, age)
VALUES (2, 'Bob', '[email protected]', 25)
"#)?;
// Query
let results = query_engine.execute("SELECT * FROM users WHERE age > 26")?;
for row in results {
println!("{:?}", row);
}
Ok(())
}
Output:
Row { id: 1, name: "Alice", email: "[email protected]", age: 30 }
Complex SQL Queries
// JOIN query
let results = query_engine.execute(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
"#)?;
for row in results {
println!("{} bought {} for ${}",
row.get_str("name")?,
row.get_str("product")?,
row.get_f64("amount")?
);
}
Output:
Alice bought Laptop for $999.99
Bob bought Keyboard for $75
Aggregations
// GROUP BY with aggregations
let results = query_engine.execute(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
"#)?;
for row in results {
println!("{}: {} orders, ${} total",
row.get_str("name")?,
row.get_i64("order_count")?,
row.get_f64("total")?
);
}
Output:
Alice: 2 orders, $1024.99 total
Bob: 1 orders, $75 total
Vector Search
HNSW Index
use toondb_index::{HnswIndex, DistanceMetric};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create HNSW index
let mut index = HnswIndex::new(
384, // dimension
DistanceMetric::Cosine, // metric
16, // m
100 // ef_construction
)?;
// Build from embeddings
let embeddings = vec![
vec![0.1, 0.2, 0.3, /* ... 384 dims */],
vec![0.4, 0.5, 0.6, /* ... 384 dims */],
];
let labels = vec!["doc1", "doc2"];
index.bulk_build(&embeddings, &labels)?;
// Search
let query = vec![0.15, 0.25, 0.35, /* ... 384 dims */];
let results = index.query(&query, 10, 50)?; // k=10, ef_search=50
for (i, result) in results.iter().enumerate() {
println!("{}. {} (distance: {:.4})",
i + 1,
result.label,
result.distance
);
}
Ok(())
}
Output:
1. doc1 (distance: 0.0234)
2. doc2 (distance: 0.1567)
Complete Example: Multi-Tenant SaaS App
use toondb::IpcClient;
use serde_json::Value;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = IpcClient::connect("./saas_db/toondb.sock").await?;
// Insert tenant data
client.put(
b"tenants/acme/users/alice",
br#"{"role":"admin","email":"[email protected]"}"#
).await?;
client.put(
b"tenants/acme/users/bob",
br#"{"role":"user","email":"[email protected]"}"#
).await?;
client.put(
b"tenants/globex/users/charlie",
br#"{"role":"admin","email":"[email protected]"}"#
).await?;
// Scan ACME Corp data only (tenant isolation)
let acme_data = client.scan("tenants/acme/").await?;
println!("ACME Corp: {} users", acme_data.len());
for kv in acme_data {
let user: Value = serde_json::from_slice(&kv.value)?;
println!(" {}: {} ({})",
String::from_utf8_lossy(&kv.key),
user["email"].as_str().unwrap(),
user["role"].as_str().unwrap()
);
}
// Scan Globex Corp data
let globex_data = client.scan("tenants/globex/").await?;
println!("\nGlobex Inc: {} users", globex_data.len());
for kv in globex_data {
let user: Value = serde_json::from_slice(&kv.value)?;
println!(" {}: {} ({})",
String::from_utf8_lossy(&kv.key),
user["email"].as_str().unwrap(),
user["role"].as_str().unwrap()
);
}
Ok(())
}
Output:
ACME Corp: 2 users
tenants/acme/users/alice: alice@acme.com (admin)
tenants/acme/users/bob: bob@acme.com (user)
Globex Inc: 1 users
tenants/globex/users/charlie: charlie@globex.com (admin)
API Reference
IpcClient (Async)
| Method | Description |
|---|---|
IpcClient::connect(path) |
Connect to IPC server |
put(key, value) |
Store key-value pair |
get(key) |
Retrieve value (Option) |
delete(key) |
Delete a key |
put_path(path, value) |
Store at hierarchical path |
get_path(path) |
Retrieve by path |
scan(prefix) |
Scan keys with prefix |
begin_transaction() |
Start transaction |
commit_transaction(txn) |
Commit transaction |
abort_transaction(txn) |
Abort transaction |
Database (Embedded)
| Method | Description |
|---|---|
Database::open(path) |
Open/create database |
put(key, value) |
Store key-value pair |
get(key) |
Retrieve value (Option) |
delete(key) |
Delete a key |
scan(start, end) |
Iterate key range |
checkpoint() |
Force durability checkpoint |
stats() |
Get storage statistics |
Configuration
use toondb::Config;
let config = Config {
create_if_missing: true,
wal_enabled: true,
sync_mode: SyncMode::Normal, // Full, Normal, Off
memtable_size_bytes: 64 * 1024 * 1024, // 64MB
..Default::default()
};
let db = Database::open_with_config("./my_db", config)?;
Error Handling
use toondb::{IpcClient, Error};
match client.get(b"key").await {
Ok(Some(value)) => {
println!("Found: {}", String::from_utf8_lossy(&value));
}
Ok(None) => {
println!("Key not found");
}
Err(Error::ConnectionFailed) => {
eprintln!("Server not running!");
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
Best Practices
✅ Use IPC for multi-process — Better for microservices ✅ Use embedded for single-process — Maximum performance ✅ Use scan() for multi-tenancy — Efficient prefix-based isolation ✅ Use transactions — Atomic multi-key operations ✅ Use async/await — Non-blocking I/O for IPC ✅ Handle errors properly — Match on Error variants
Crate Organization
| Crate | Purpose |
|---|---|
toondb |
High-level client SDK (this crate) |
toondb-core |
Core database engine |
toondb-storage |
Storage layer with IPC server |
toondb-index |
Vector search (HNSW) |
toondb-query |
SQL query engine |
toondb-client |
Low-level client bindings |
Building from Source
# Clone repository
git clone https://github.com/toondb/toondb
cd toondb
# Build all crates
cargo build --release
# Run tests
cargo test --all
# Build specific crate
cargo build --release -p toondb-client
Examples
See the examples directory for more:
basic_operations.rs- Simple key-value operationsmulti_tenant.rs- Multi-tenant data isolationtransactions.rs- ACID transactionsvector_search.rs- HNSW vector searchsql_queries.rs- SQL operations
Platform Support
- Linux (x86_64, aarch64)
- macOS (Intel, Apple Silicon)
- Windows (x64)
Requires Rust 1.70 or later.
License
Apache License 2.0
Links
Support
- GitHub Issues: https://github.com/toondb/toondb/issues
- Email: [email protected]
Author
Sushanth - GitHub
Dependencies
~39–57MB
~838K SLoC