|
1 | 1 | # ShorterDB |
2 | 2 |
|
3 | | -A lightweight embedded key-value store for Rust, inspired by the Log-Structured Merge-Tree (LSM-Tree) architecture. |
| 3 | +A high-performance, embedded key-value store for Rust, built with a Log-Structured Merge-Tree (LSM-Tree) architecture. |
4 | 4 |
|
5 | 5 | [](https://crates.io/crates/shorterdb) |
6 | 6 | [](https://docs.rs/shorterdb) |
7 | 7 | [](../../LICENSE) |
8 | 8 |
|
9 | | -## Overview |
| 9 | +## Features |
10 | 10 |
|
11 | | -ShorterDB is an embedded key-value database that combines simplicity with proven database design patterns. Built for learning, experimentation, and lightweight applications that need persistent storage. |
| 11 | +- **Embedded**: Runs directly in your application process (no external server required). |
| 12 | +- **Persistent**: Data is durably stored using a Write-Ahead Log (WAL) and SSTables. |
| 13 | +- **Fast**: In-memory writers using lock-free SkipLists. |
| 14 | +- **Simple API**: Minimalistic `get`, `set`, and `delete` interface. |
| 15 | +- **Thread-Safe**: Designed for concurrent access. |
12 | 16 |
|
13 | | -## Architecture |
14 | | - |
15 | | -ShorterDB implements an LSM-Tree inspired architecture with three core components: |
16 | | - |
17 | | -``` |
18 | | -┌─────────────────────────────────────────────────────────────────┐ |
19 | | -│ ShorterDB │ |
20 | | -│ │ |
21 | | -│ ┌─────────────────────────────────────────────────────────┐ │ |
22 | | -│ │ MEMTABLE │ │ |
23 | | -│ │ (In-Memory SkipList) │ │ |
24 | | -│ │ All writes go here first for fast performance │ │ |
25 | | -│ └────────────────────────┬────────────────────────────────┘ │ |
26 | | -│ │ │ |
27 | | -│ ┌─────────────────┼─────────────────┐ │ |
28 | | -│ ▼ │ ▼ │ |
29 | | -│ ┌─────────────┐ │ ┌─────────────┐ │ |
30 | | -│ │ WAL │ │ │ FLUSHER │ │ |
31 | | -│ │ (Append- │ │ │ (Background│ │ |
32 | | -│ │ Only Log) │ │ │ Thread) │ │ |
33 | | -│ └─────────────┘ │ └──────┬──────┘ │ |
34 | | -│ │ │ │ |
35 | | -│ │ ▼ │ |
36 | | -│ │ ┌───────────────────┐ │ |
37 | | -│ │ │ SST │ │ |
38 | | -│ │ │ (Sorted String │ │ |
39 | | -│ │ │ Tables on Disk) │ │ |
40 | | -│ │ └───────────────────┘ │ |
41 | | -└─────────────────────────────────────────────────────────────────┘ |
42 | | -``` |
43 | | - |
44 | | ---- |
45 | | - |
46 | | -## Core Components |
47 | | - |
48 | | -### Memtable |
49 | | - |
50 | | -The **Memtable** is an in-memory sorted data structure that serves as the write buffer for all incoming operations. |
51 | | - |
52 | | -| Property | Implementation | |
53 | | -|----------|----------------| |
54 | | -| **Data Structure** | Lock-free SkipList (`crossbeam-skiplist`) | |
55 | | -| **Ordering** | Keys are always sorted in byte order | |
56 | | -| **Concurrency** | Thread-safe reads without locks | |
57 | | -| **Size Tracking** | Approximate byte-based tracking with atomic counters | |
58 | | -| **Threshold** | Configurable (default 4MB), triggers flush when exceeded | |
59 | | - |
60 | | -**Key Features:** |
61 | | -- All writes (set/delete) are first inserted into the memtable |
62 | | -- Deletes are represented as **tombstones** (markers indicating deletion) |
63 | | -- Provides the fastest read path since data is in memory |
64 | | -- When full, becomes **immutable** and is flushed to SST in the background |
65 | | - |
66 | | ---- |
67 | | - |
68 | | -### Write-Ahead Log (WAL) |
69 | | - |
70 | | -The **WAL** is an append-only log file that ensures durability by persisting every write before it's applied to the memtable. |
71 | | - |
72 | | -| Property | Implementation | |
73 | | -|----------|----------------| |
74 | | -| **Format** | Binary: `[op(1B)][key_len(4B)][key][value_len(4B)][value]` | |
75 | | -| **Sync Mode** | `fsync` after every write for durability | |
76 | | -| **Recovery** | Replayed on startup to restore uncommitted state | |
77 | | -| **Rotation** | Truncated after successful SST flush | |
78 | | - |
79 | | -**Key Features:** |
80 | | -- Guarantees no data loss on crash (writes survive before acknowledgment) |
81 | | -- Supports both `Set` and `Delete` operations |
82 | | -- Corruption-tolerant recovery (stops at first invalid entry) |
83 | | -- Sanity checks prevent OOM from corrupted length fields (100MB max) |
84 | | - |
85 | | ---- |
86 | | - |
87 | | -### Sorted String Table (SST) |
88 | | - |
89 | | -**SST files** are immutable, sorted key-value files stored on disk. They represent the long-term persistent storage layer. |
90 | | - |
91 | | -| Property | Implementation | |
92 | | -|----------|----------------| |
93 | | -| **File Format** | `[Data Entries][Sparse Index][Footer(24B)]` | |
94 | | -| **Index** | Sparse index every 16 entries for O(log n) lookups | |
95 | | -| **Magic Number** | `SSTFILE\0` for corruption detection | |
96 | | -| **Tombstones** | Preserved until compaction | |
97 | | - |
98 | | -**File Structure:** |
99 | | -``` |
100 | | -┌────────────────────────────────┐ |
101 | | -│ Data Entries │ Sorted key-value pairs |
102 | | -│ [key_len][key][val_len] │ |
103 | | -│ [value][type_marker] │ |
104 | | -├────────────────────────────────┤ |
105 | | -│ Sparse Index │ Every 16th key → offset |
106 | | -│ [key_len][key][offset(8B)] │ |
107 | | -├────────────────────────────────┤ |
108 | | -│ Footer │ data_end(8B) + index_off(8B) |
109 | | -│ │ + magic(8B) = 24 bytes |
110 | | -└────────────────────────────────┘ |
111 | | -``` |
112 | | - |
113 | | -**Key Features:** |
114 | | -- Binary search on sparse index for efficient lookups |
115 | | -- Supports both data entries and tombstones |
116 | | -- L0 compaction when >4 files accumulate (merges and removes tombstones) |
117 | | -- Files are never modified, only created and deleted |
118 | | - |
119 | | ---- |
| 17 | +## Installation |
120 | 18 |
|
121 | | -## ACID Properties |
| 19 | +Add this to your `Cargo.toml`: |
122 | 20 |
|
123 | | -### Atomicity |
124 | | - |
125 | | -Each individual operation (`set`, `delete`) is atomic: |
126 | | - |
127 | | -- **Single-key atomicity**: A write either fully completes or doesn't happen |
128 | | -- **WAL-first**: Operations are logged before being applied |
129 | | -- **No partial writes**: If crash occurs mid-operation, recovery ignores incomplete WAL entries |
130 | | - |
131 | | -> **Limitation**: Multi-key transactions are not supported. Each operation is independent. |
132 | | -
|
133 | | ---- |
134 | | - |
135 | | -### Consistency |
136 | | - |
137 | | -The database maintains a consistent view of data: |
138 | | - |
139 | | -- **Ordered key-value store**: Keys are always sorted, enabling range scans |
140 | | -- **Tombstone semantics**: Deletes are properly propagated through all layers |
141 | | -- **Read consistency**: Queries check memtable → immutable memtable → SST in order |
142 | | -- **No phantom reads**: A key is either present with its latest value or absent |
143 | | - |
144 | | ---- |
145 | | - |
146 | | -### Isolation |
147 | | - |
148 | | -ShorterDB provides **snapshot-like** isolation for reads: |
149 | | - |
150 | | -- **Read path**: Checks layers in order (newest to oldest), returns first match |
151 | | -- **Write path**: New writes don't affect in-progress reads |
152 | | -- **Background flush**: Uses immutable memtable copy, reads continue on original |
153 | | - |
154 | | -> **Isolation Level**: Roughly equivalent to "Read Committed" — you see committed data, but concurrent writes may be visible. |
155 | | -
|
156 | | ---- |
157 | | - |
158 | | -### Durability |
159 | | - |
160 | | -Writes are durable once `set()` or `delete()` returns: |
161 | | - |
162 | | -| Guarantee | Mechanism | |
163 | | -|-----------|-----------| |
164 | | -| **Write durability** | WAL is `fsync`'d after every write | |
165 | | -| **Crash recovery** | WAL is replayed on startup to restore state | |
166 | | -| **SST durability** | Files are `sync_all`'d after creation | |
167 | | -| **WAL rotation** | Only cleared after SST is confirmed on disk | |
168 | | - |
169 | | -**Recovery Flow:** |
170 | | -``` |
171 | | -1. Open database |
172 | | -2. Load existing SST files |
173 | | -3. Replay WAL entries into fresh memtable |
174 | | -4. Resume normal operation |
| 21 | +```toml |
| 22 | +[dependencies] |
| 23 | +shorterdb = "0.2.0" |
175 | 24 | ``` |
176 | 25 |
|
177 | | ---- |
178 | | - |
179 | | -## Usage |
| 26 | +## Quick Start |
180 | 27 |
|
181 | 28 | ```rust |
182 | 29 | use shorterdb::ShorterDB; |
183 | 30 | use std::path::Path; |
184 | 31 |
|
185 | 32 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
186 | | - // Open database with default settings (4MB memtable) |
187 | | - let mut db = ShorterDB::new(Path::new("./my_db"))?; |
| 33 | + // Open the database (creates directory if missing) |
| 34 | + let mut db = ShorterDB::new(Path::new("/tmp/my_db"))?; |
188 | 35 |
|
189 | | - // Set values (accepts &str or &[u8]) |
190 | | - db.set("user:1", "alice")?; |
191 | | - db.set("user:2", "bob")?; |
| 36 | + // Store a key-value pair |
| 37 | + db.set(b"username", b"ferris")?; |
192 | 38 |
|
193 | | - // Get values |
194 | | - if let Some(value) = db.get("user:1")? { |
195 | | - println!("Found: {}", std::str::from_utf8(&value)?); |
| 39 | + // Retrieve the value |
| 40 | + if let Some(value) = db.get(b"username")? { |
| 41 | + println!("Found user: {}", String::from_utf8_lossy(&value)); |
196 | 42 | } |
197 | 43 |
|
198 | | - // Delete values |
199 | | - let existed = db.delete("user:1")?; |
200 | | - println!("Key existed: {}", existed); |
201 | | - |
202 | | - // Graceful shutdown (also called automatically on drop) |
203 | | - db.close()?; |
| 44 | + // Delete the key |
| 45 | + db.delete(b"username")?; |
204 | 46 |
|
205 | 47 | Ok(()) |
206 | 48 | } |
207 | 49 | ``` |
208 | 50 |
|
| 51 | +## Architecture |
| 52 | + |
| 53 | +ShorterDB uses a classic LSM-Tree design with a Memtable, Write-Ahead Log, and background Flusher. |
| 54 | + |
| 55 | +For deep technical details on the internal design, file formats, and ACID guarantees, see [ARCHITECTURE.md](ARCHITECTURE.md). |
| 56 | + |
209 | 57 | ## Examples |
210 | 58 |
|
| 59 | +The project workspace includes examples you can run locally: |
| 60 | + |
211 | 61 | ```bash |
212 | | -# Run the embedded example |
213 | | -cargo run --example embedded |
| 62 | +# Basic embedded usage |
| 63 | +cargo run -p examples --bin embedded |
214 | 64 |
|
215 | | -# Run the interactive REPL |
216 | | -cargo run --example repl |
| 65 | +# Interactive REPL |
| 66 | +cargo run -p examples --bin repl |
217 | 67 | ``` |
218 | 68 |
|
219 | 69 | ## License |
220 | 70 |
|
221 | | -Licensed under either of Apache License, Version 2.0 or MIT license at your option. |
| 71 | +Licensed under either of [Apache License, Version 2.0](../../LICENSE-APACHE) or [MIT license](../../LICENSE-MIT) at your option. |
0 commit comments