txn-db 0.3.0

MVCC transaction engine for Rust storage layers. Snapshot isolation and serializable transactions with multi-version concurrency control, conflict detection, and a durable transaction log on wal-db. The transaction layer for embedded databases and Hive DB.
Documentation

Available now (0.3):

  • MVCC — each write creates a new version; readers see a consistent snapshot without blocking writers
  • Snapshot isolation — a transaction reads the database as of its start timestamp; its own writes are visible to itself before commit
  • Serializable (SSI) — opt-in read-set validation under the serializable feature, rejecting write skew and the read-only anomaly
  • Write-write conflict detection — first-committer-wins at commit; the later writer is told to retry with a typed, retryable error
  • Sharded commit path — lock-free timestamp allocation and per-shard conflict checks, so commits to unrelated keys do not contend (loom-checked)
  • Pluggable backing store — the version store is the VersionStore trait; an in-memory store ships, and any backend plugs in

On the roadmap:

  • Durable txn log — commits logged to wal-db before acknowledgment (under durability)
  • Garbage collection — old versions reclaimed once no live snapshot can observe them

Installation

[dependencies]
txn-db = "0.3"

# Opt into serializable isolation:
txn-db = { version = "0.3", features = ["serializable"] }

Quick start

The whole common case is begin, read and write through the transaction, commit:

use txn_db::Db;

let db = Db::new();

// Write two keys in one atomic transaction.
let mut tx = db.begin();
tx.put(b"user:1:name".to_vec(), b"ada".to_vec());
tx.put(b"user:1:role".to_vec(), b"admin".to_vec());
tx.commit()?;

// A later transaction reads a consistent snapshot.
let tx = db.begin();
assert_eq!(tx.get(b"user:1:name")?.as_deref(), Some(&b"ada"[..]));
# Ok::<(), txn_db::TxnError>(())

When two transactions race to write the same key, the first to commit wins and the second is told to retry — that is what prevents lost updates:

use txn_db::Db;

let db = Db::new();
let mut a = db.begin();
let mut b = db.begin();
a.put(b"counter".to_vec(), b"1".to_vec());
b.put(b"counter".to_vec(), b"2".to_vec());

a.commit()?;                          // first committer wins
let err = b.commit().unwrap_err();    // second is rejected
assert!(err.is_retryable());          // retry against the fresh snapshot
# Ok::<(), txn_db::TxnError>(())

The retry loop is a few lines; see examples/concurrent_counter.rs for the contended read-modify-write pattern, examples/bank_transfer.rs for an atomic multi-key transfer, and examples/custom_store.rs for plugging in your own VersionStore.

Serializable isolation

Snapshot isolation still allows write skew: two transactions that read the same rows and write different ones can both commit, breaking an invariant that ties those rows together. With the serializable feature, begin_serializable validates a transaction's read set at commit and rejects exactly those cases.

# #[cfg(feature = "serializable")]
# {
use txn_db::Db;

let db = Db::new();
let mut seed = db.begin();
seed.put(b"on_call:alice".to_vec(), vec![1]);
seed.put(b"on_call:bob".to_vec(), vec![1]);
seed.commit()?;

// Both read the pair, then each takes one row off — classic write skew.
let mut t1 = db.begin_serializable();
let mut t2 = db.begin_serializable();
let _ = (t1.get(b"on_call:alice")?, t1.get(b"on_call:bob")?);
let _ = (t2.get(b"on_call:alice")?, t2.get(b"on_call:bob")?);
t1.put(b"on_call:alice".to_vec(), vec![0]);
t2.put(b"on_call:bob".to_vec(), vec![0]);

t1.commit()?;                          // first commits
assert!(t2.commit().is_err());         // second read a row t1 changed — rejected
# }
# Ok::<(), txn_db::TxnError>(())

See examples/serializable_doctors.rs for the full on-call-doctors demonstration, side by side under both isolation levels.

Examples

Example What it shows
quick_start Shortest end-to-end: open, write, read back.
bank_transfer Atomic multi-key update with conflict retries.
concurrent_counter Many threads increment one key; no update is lost.
snapshot_reads A snapshot stays stable as the database moves on.
custom_store Backing the engine with a custom VersionStore.
serializable_doctors Write skew under SI vs serializable (needs --features serializable).
cargo run --example quick_start
cargo run --example serializable_doctors --features serializable

Status

This is the 0.3 release: the MVCC core and snapshot isolation from the foundation, now with serializable isolation (the serializable feature) and a sharded, lock-free commit path whose concurrency is verified with loom. The docs/API.md reference documents the full Tier-1 surface, and the remaining phases — durable commits via wal-db and version garbage collection — follow per the roadmap. The shape of the Tier-1 API is settled and will not change before 1.0.

Where It Fits

txn-db is the transaction layer. It builds on:

  • wal-db — durable transaction commit log
  • lsm-db — a natural backing version store
  • Hive DB — the transaction orchestration layer (DISTRO) builds on these semantics

It stays foreign-compatible: usable standalone over any version store that implements the trait.

Contributing

Before opening a PR, cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean. Hot-path changes require a criterion benchmark; correctness-critical paths require property and/or loom tests.