RoughDB
Embedded key-value storage written in Rust — a port of LevelDB to Rust.
RoughDB is an LSM-tree key-value store with a LevelDB-compatible on-disk format. It supports persistent (WAL + MANIFEST + SSTables) and in-memory operation, multi-level compaction, and bidirectional iteration.
[!WARNING] While I started this all "manually", many years ago... This is an experiment of sort. While I'm relatively serious about this, I also used it more recently to experiment with different things, including generative AI. I'm reviewing all changes manually and carefully.
Getting started
Opening a database
use ;
let mut opts = default;
opts.create_if_missing = true;
let db = open?;
Use Db::default() for a lightweight in-memory database (no WAL, no flush):
let db = default;
Basic reads and writes
use ;
// Write
db.put?;
db.delete?;
// Read
match db.get
Atomic batch writes
use ;
let mut batch = new;
batch.put;
batch.put;
batch.delete;
db.write?;
Iterating over keys
Iterators start unpositioned — call seek_to_first, seek_to_last, or
seek before reading.
use ReadOptions;
let mut it = db.new_iterator?;
// Forward
it.seek_to_first;
while it.valid
// Backward
it.seek_to_last;
while it.valid
// Range scan from "b" onward
it.seek;
while it.valid
Snapshots
A snapshot pins the database to a specific point in time; reads through it see only writes that preceded the snapshot.
use ReadOptions;
let snap = db.get_snapshot;
// Reads with this snapshot see the state at the moment it was taken.
let opts = ReadOptions ;
let value = db.get_with_options?;
let mut it = db.new_iterator?;
// snap releases automatically when it goes out of scope.
Manual compaction
// Compact everything
db.compact_range?;
// Compact a specific key range
db.compact_range?;
Building and testing
Status
RoughDB is in active development. The on-disk format is LevelDB-compatible.
Implemented:
- WAL + MANIFEST + SSTable flush and crash-safe recovery
- Multi-level compaction (all 7 levels) with level-score scheduling, seek-based compaction,
trivial-move, grandparent-overlap limiting, and flush placement (
PickLevelForMemTableOutput) - Manual
compact_range - Background compaction thread — flush and compaction run on a dedicated thread; writers are never blocked by compaction I/O. Includes L0 write slowdown (≥ 8 files, 1 ms sleep) and hard stop (≥ 12 files, blocks until the background thread drains L0)
flush— explicit memtable flush to SSTable with optional wait (FlushOptions)- Batch-grouped writes — concurrent writers share one WAL record, amortising fsync cost
- Bidirectional iteration (
seek_to_first,seek_to_last,seek,next,prev) - Snapshots (
get_snapshot/release_snapshot) - Bloom filter support (
Options::filter_policy) - Block compression: Snappy (default) and Zstd (
Options::compression) get_property—leveldb.num-files-at-level<N>,leveldb.stats,leveldb.sstables,leveldb.approximate-memory-usageget_approximate_sizes— byte-range estimation via index-block seeksdestroy— safely removes a database directoryLOCKfile — prevents concurrent opens by multiple processes- Table cache — LRU open-file-handle cache bounded by
Options::max_open_files - Block cache — LRU byte-capacity cache with per-table IDs;
ReadOptions::fill_cache
Known limitations:
Options::reuse_logsis accepted but ignored.- Iterator-based seek sampling (
RecordReadSample) is not implemented. Seek-based compaction is triggered by point-lookup misses viaDb::getbut not by iterator scans.
Not yet implemented:
- Custom comparators — keys are always compared bytewise;
Options::comparatordoes not exist. Applications that require a custom sort order (e.g. reverse, integer, prefix) cannot use RoughDB. Envabstraction — file I/O is hardcoded to the local POSIX filesystem. There is no way to inject a custom storage backend (in-memory, encrypted, cloud, etc.).RepairDB— recovery from a corrupt or partial MANIFEST by scanning surviving SSTables.- Info logging — LevelDB writes compaction progress, recovery details, and errors to an
info_log. RoughDB produces no log output.
License
Apache License 2.0 — see LICENSE.