RedDB
An async, in-memory embedded document database for Rust with optional WAL-based persistence.
Features
- In-memory first — the live store is an
Arc<RwLock<HashMap<Uuid, Vec<u8>>>>. Every read and write hits RAM; disk is never on the hot path. - Optional persistence — a WAL-style append-only log survives process restarts. Choose
MemDbfor pure in-memory operation or a typed alias (BinDb,JsonDb,RonDb,YamlDb) for durability. - Async-first — built on Tokio 1.x; every I/O method is
async. - Pluggable serializers — Binary (bincode), JSON, RON, and YAML, each behind an optional feature flag.
- Closure-based queries —
QueryBuilderwith.filter(),.order_by(),.skip(),.limit(), terminating with.all(),.first(),.count(), or.ids(). - Bulk updates and deletes —
update_whereanddelete_whereaccept arbitrary predicates. - Transactions —
begin()/commit()/rollback()buffer operations and apply them atomically. - Hash indexes —
add_indexregisters a string-keyed index maintained automatically on every write;using_indexlooks up documents in O(1). - Compaction —
compact()rewrites the log with exactly one record per live document. - Configurable write order —
MemoryFirst(default, faster) orFileFirst(stronger durability guarantee).
Quick start
use ;
use ;
async
Batch insert and equality-based operations:
// Batch insert
let docs = db.insert.await?;
// Find all documents equal to a value (exact match on serialized bytes)
let matches: = db.find.await?;
// Update all documents equal to a value; returns the count of updated docs
let updated = db.update.await?;
// Delete all documents equal to a value; returns the count of deleted docs
let deleted = db.delete.await?;
// Retrieve all
let all: = db.find_all.await?;
Persistence
Use a serializer-typed alias to enable file persistence. The database appends to a WAL on every write and reloads the full state on open or new.
use ;
use ;
async
The file is named <db_name><extension> (e.g. notes.ron) in the current directory by default. Use DbConfig to change the location — see Configuration.
Queries
QueryBuilder provides a lazy, chainable interface. Execution happens only when a terminal method is called.
use Ordering;
use ;
// All pending tasks, sorted by priority descending, second page of 10
let results: = db
.
.filter
.order_by
.skip
.limit
.all
.await?;
// First matching document
let first: = db
.
.filter
.first
.await?;
// Count only
let n: usize = db..filter.count.await?;
// Only IDs
use Uuid;
let ids: = db..filter.ids.await?;
update_where
update_where selects documents by predicate and applies a transformation closure. Use .exec() to get the count of updated documents or .returning() to get the updated documents back. An optional .limit() caps the number of documents affected.
// Boost the priority of all pending tasks by 1; returns the count
let count: usize = db
.
.exec
.await?;
// Cap at 5 updates and return the modified documents
let docs: = db
.
.limit
.returning
.await?;
delete_where
delete_where removes every document that satisfies a predicate and returns the count of deleted documents.
// Remove all completed tasks
let deleted: usize = db
.
.await?;
Transactions
begin() returns a Transaction that buffers operations. The live store is not modified until commit() is called. rollback() silently discards all staged operations.
let mut tx = db.begin;
let doc = tx.insert_one?;
tx.update_one?;
tx.delete_one;
// Apply atomically — in-memory map, indexes, and WAL are updated together
tx.commit.await?;
// Or discard everything without touching the store
// tx.rollback();
Staged operations are not visible to concurrent readers until commit returns successfully.
Hash indexes
add_index builds a string-keyed hash index over the existing collection and keeps it current on every subsequent write. using_index performs an O(1) key lookup.
// Register the index (scans all existing documents once to build the initial state)
db..await?;
// O(1) lookup — returns all documents whose role field equals "admin"
let admins: = db..await?;
// The index is maintained automatically on every insert, update, and delete
db.insert_one.await?;
// admins index now includes dave
Multiple named indexes may be registered on the same database instance. Lookups on an unregistered index name return an error.
Configuration
DbConfig is a builder that controls how a database is opened or created.
use ;
let db = .await?;
| Option | Default | Description |
|---|---|---|
dir(path) |
. (current directory) |
Directory where the WAL file is written |
compaction_ratio(f64) |
2.0 |
Compact when file size >= live data size × ratio |
write_order(WriteOrder) |
MemoryFirst |
Order of in-memory and WAL updates on each write |
WriteOrder
MemoryFirst(default) — updates the in-memory map first, then appends to the WAL. Lowest latency. A crash between the two steps leaves the WAL one record behind, which is self-correcting on next open.FileFirst— appends to the WAL first, then updates the in-memory map. Stronger durability guarantee: if the process crashes after the WAL write, the in-memory state is reconstructed correctly on restart.
Storage stats
stats() returns a point-in-time snapshot of storage metrics.
let s = db.stats.await?;
println!;
println!; // always 0 for MemDb
println!;
Trigger a manual compaction to rewrite the log with one record per live document:
db.compact.await?;
compact() is a no-op for MemDb.
Serializers
Each serializer is gated behind a Cargo feature flag. Enable only the formats you need, or use full to enable all of them.
| Feature flag | Type alias | Format | File extension |
|---|---|---|---|
bin_ser |
BinDb |
bincode (binary) | .bin |
bin_ser |
MemDb |
bincode (in-memory only, no file) | — |
json_ser |
JsonDb |
JSON | .json |
ron_ser |
RonDb |
RON | .ron |
yaml_ser |
YamlDb |
YAML | .yaml |
All type aliases expand to RedDb<Serializer, Storage>. You can compose your own combination by naming the type parameters directly if you need a custom serializer or storage backend.
Complete API reference
Construction
// Open or create with explicit config
pub async pub async
Insert
pub async
Find
// By id — error if missing
pub async pub async pub async pub async
QueryBuilder
.filter // keep only matching documents
.order_by // sort result
.skip // skip first n
.limit // take at most n
// terminals
.all .first .count .ids
Update
// Replace by id; returns true if found
pub async pub async
UpdateWhereBuilder
.limit // stop after n updates
// terminals
.exec // returns count
.returning // returns updated docs
Delete
// Delete by id; returns the removed document
pub async pub async pub async
Transactions
.insert_one .update_one .delete_one
.commit // apply all staged ops atomically
.rollback // discard all staged ops
Hash indexes
// Build index and keep it current on every write
pub async pub async
Maintenance
pub async
Cargo.toml
[]
= { = "2.0", = ["ron_ser"] }
= { = "1", = ["macros", "rt-multi-thread"] }
= { = "1", = ["derive"] }
To enable all serializers:
[]
= { = "2.0", = ["full"] }
Run the full test suite:
Migrating from v1
Cargo.toml
# Before
= { = "0.2", = ["ron_ser"] }
# After
= { = "2.0", = ["ron_ser"] }
Opening the database
// v1 (sync)
let db = .unwrap;
// v2 (async)
let db = .await?;
// or with config:
let db = .await?;
Document fields
doc._id → doc.id // renamed
doc.data → doc.data // unchanged
doc._st → removed // Status is internal; not part of Document<T>
Queries
// v1 — exact byte-match against a fully-constructed value
let results = db.find.await?;
// v2 — closure predicate (partial fields, ranges, etc.)
let results = db.
.filter
.all
.await?;
Updates
// v1 — replace all that byte-match old_value
let n = db.update.await?;
// v2 — closure-based transform
let n = db
.
.exec
.await?;
Deletes
// v1
let n = db.delete.await?;
// v2
let n = db..await?;
File format
v1 and v2 files are not compatible. v1 used newline-delimited records (which corrupted binary data); v2 uses length-prefix framing. Use the migration helper below or delete your v1 files before opening with v2.
Migrating data with from_v1
Enable the migrate feature and call from_v1 once to convert a v1 file into a new v2 database. Original document UUIDs are preserved.
= { = "2.0", = ["ron_ser", "migrate"] }
use Ron;
// Reads users.ron (v1 format), writes users_v2.ron (v2 format)
let count = .await?;
println!;
- Call this once. Running it a second time appends duplicates to the v2 file.
- Binary (
.bin) v1 files cannot be migrated — the v1 line-delimited format corrupted binary records. Use JSON, RON, or YAML sources only.
License
RedDB is dual-licensed under MIT or Apache-2.0, at your option.