stratum-db 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. stratum_db-1.0.0/LICENSE +21 -0
  2. stratum_db-1.0.0/PKG-INFO +265 -0
  3. stratum_db-1.0.0/README.md +245 -0
  4. stratum_db-1.0.0/pyproject.toml +32 -0
  5. stratum_db-1.0.0/setup.cfg +4 -0
  6. stratum_db-1.0.0/stratum/__init__.py +0 -0
  7. stratum_db-1.0.0/stratum/api.py +41 -0
  8. stratum_db-1.0.0/stratum/db.py +90 -0
  9. stratum_db-1.0.0/stratum/engine.py +220 -0
  10. stratum_db-1.0.0/stratum/grpc/__init__.py +0 -0
  11. stratum_db-1.0.0/stratum/grpc/server.py +45 -0
  12. stratum_db-1.0.0/stratum/grpc/stratum_pb2.py +48 -0
  13. stratum_db-1.0.0/stratum/grpc/stratum_pb2_grpc.py +183 -0
  14. stratum_db-1.0.0/stratum/init_db.py +6 -0
  15. stratum_db-1.0.0/stratum/memtable.py +59 -0
  16. stratum_db-1.0.0/stratum/metrics.py +54 -0
  17. stratum_db-1.0.0/stratum/models.py +33 -0
  18. stratum_db-1.0.0/stratum/sstable.py +253 -0
  19. stratum_db-1.0.0/stratum/wal.py +71 -0
  20. stratum_db-1.0.0/stratum_db.egg-info/PKG-INFO +265 -0
  21. stratum_db-1.0.0/stratum_db.egg-info/SOURCES.txt +34 -0
  22. stratum_db-1.0.0/stratum_db.egg-info/dependency_links.txt +1 -0
  23. stratum_db-1.0.0/stratum_db.egg-info/entry_points.txt +2 -0
  24. stratum_db-1.0.0/stratum_db.egg-info/requires.txt +14 -0
  25. stratum_db-1.0.0/stratum_db.egg-info/top_level.txt +1 -0
  26. stratum_db-1.0.0/tests/test_compact_10000.py +42 -0
  27. stratum_db-1.0.0/tests/test_engine_scan.py +128 -0
  28. stratum_db-1.0.0/tests/test_grpc_server.py +144 -0
  29. stratum_db-1.0.0/tests/test_kill_recovery_part1.py +74 -0
  30. stratum_db-1.0.0/tests/test_kill_recovery_part2.py +55 -0
  31. stratum_db-1.0.0/tests/test_memtable.py +35 -0
  32. stratum_db-1.0.0/tests/test_metrics.py +33 -0
  33. stratum_db-1.0.0/tests/test_phase6_properties.py +127 -0
  34. stratum_db-1.0.0/tests/test_recovery.py +61 -0
  35. stratum_db-1.0.0/tests/test_sstable.py +793 -0
  36. stratum_db-1.0.0/tests/test_wal.py +182 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vishnu Sai Paineni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.4
2
+ Name: stratum-db
3
+ Version: 1.0.0
4
+ Summary: A standalone write-optimized LSM-tree key-value database
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: sortedcontainers
9
+ Provides-Extra: metadata
10
+ Requires-Dist: sqlalchemy; extra == "metadata"
11
+ Requires-Dist: psycopg[binary]; extra == "metadata"
12
+ Provides-Extra: server
13
+ Requires-Dist: grpcio; extra == "server"
14
+ Requires-Dist: grpcio-tools; extra == "server"
15
+ Requires-Dist: python-dotenv; extra == "server"
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: hypothesis; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # Stratum DB
22
+
23
+ [![Tests](https://github.com/VishnuP2412/stratum-db/actions/workflows/tests.yml/badge.svg)](https://github.com/VishnuP2412/stratum-db/actions/workflows/tests.yml)
24
+
25
+ `stratum-db` is a write-optimized, crash-safe Python LSM-tree key-value engine.
26
+ It implements a durable MemTable + WAL + SSTable storage stack with binary formats,
27
+ crash recovery, compaction, sparse indexes, Bloom filters, and a gRPC interface.
28
+
29
+ ## Performance
30
+
31
+ Stratum DB is designed to make indexed point lookups practical at larger data
32
+ volumes. A recorded pre-index benchmark took approximately **30 minutes for
33
+ 100,000 lookups**. The current implementation includes sparse on-disk indexes
34
+ and Bloom filters to avoid unnecessary full-file searches; a directly comparable
35
+ post-index lookup measurement is still pending.
36
+
37
+ | Workload | Recorded result | Notes |
38
+ |---|---:|---|
39
+ | 100,000 point lookups, pre-index implementation | ~30 minutes | Historical baseline |
40
+ | 1,000,000-entry compaction | 38:18 | Full-scale slow test |
41
+
42
+ The million-entry result was recorded with Python 3.12. Hardware, dataset
43
+ parameters, and a comparable post-index lookup result should be recorded before
44
+ using these figures as a cross-machine performance comparison.
45
+
46
+ ## What this repo contains
47
+
48
+ - `stratum/engine.py` — the core `Engine` class that coordinates WAL, MemTable, SSTable files, and compaction
49
+ - `stratum/wal.py` — write-ahead log with CRC-checked append and replay
50
+ - `stratum/memtable.py` — in-memory sorted key-value store with tombstone support
51
+ - `stratum/sstable.py` — immutable, sorted on-disk segment file format with range scan, sparse indexes, Bloom filters, restart-safe rediscovery, and shared entry-parsing
52
+ - `stratum/grpc/` — protobuf definition, generated stubs, and the gRPC server
53
+ - `stratum/db.py` and `stratum/models.py` — optional PostgreSQL metadata integration for SSTables and compaction jobs
54
+ - `tests/` — pytest coverage for WAL, MemTable, SSTable, recovery, compaction, and real SIGKILL crash-safety tests
55
+ - `STRATUM_PROJECT.md` — design rationale, tradeoffs, and phase-by-phase decisions, including known gaps
56
+ - `Stratum Dev Roadmap.md` — project scope, phased milestones, and ecosystem alignment
57
+
58
+ ## Status
59
+
60
+ This repository currently implements:
61
+
62
+ - durable WAL append with CRC validation and corruption-safe replay
63
+ - in-memory `MemTable` with sorted keys, tombstone-based deletes, and `bytes`-only key/value enforcement
64
+ - SSTable flush (atomic temp-file + fsync + rename) and range/point read via `scan()`
65
+ - **SSTable rediscovery on restart** — `Engine` rebuilds its full view of on-disk SSTables from a fresh directory scan at startup, so data flushed in a _previous_ process lifetime is visible again after a restart, not just WAL-replayed MemTable state
66
+ - **Unambiguous tombstone handling across the MemTable/SSTable boundary** — `scan()` returns tombstones untouched (filtering is the caller's job), and `Engine.get()` stops at the first SSTable (newest-first) that has _any_ record for a key, live or deleted, so a delete correctly shadows an older value in an older file instead of being silently bypassed
67
+ - **Manual compaction** (`Engine.compact()`) — merges all current SSTables into one, resolves conflicting versions of a key by highest sequence number, and physically drops tombstoned entries — the first point in the pipeline where deleted data is actually removed from disk rather than just filtered at read time. Not auto-triggered; called explicitly
68
+ - **Sparse on-disk indexes and Bloom filters** — each SSTable stores a sparse key-to-offset index and a Bloom filter, and `Engine.get()` uses both to avoid unnecessary full-file searches
69
+ - `bytes`-only API contracts enforced (fail loudly, no silent type coercion) at every component boundary, including the shared low-level entry-parsing path used by both `scan()` and restart-time loading
70
+ - real process-kill (`SIGKILL`) crash-safety tests covering WAL replay, SSTable flush/rename, and compaction's old-file cleanup step (via deterministic fault injection — see `STRATUM_PROJECT.md` for why a live SIGKILL specifically inside that window wasn't pursued further)
71
+ - **gRPC `Get`, `Put`, and streaming `Scan` operations** — a standalone server is available through `stratum/grpc/server.py`
72
+
73
+ **Known limitations:** compaction currently uses a naive full-materialize-then-sort merge rather than a tiered or streaming strategy, and it is manual rather than automatically triggered by a size or level threshold. These are deliberate next-stage tradeoffs; see `STRATUM_PROJECT.md`.
74
+
75
+ PostgreSQL metadata writes are optional and disabled by default. Set `STRATUM_METADATA_ENABLED=true` to enable them and install the `metadata` extra. The core engine does not require SQLAlchemy or gRPC dependencies.
76
+ Docker is only needed if you choose to exercise the optional `metadata` extra against a local PostgreSQL instance; it is not part of the default setup or test run.
77
+
78
+ Phase 6 property-based tests are included in the development extra and cover
79
+ WAL replay and compaction state preservation.
80
+
81
+ ## Requirements
82
+
83
+ - Python 3.12+
84
+ - Core: `sortedcontainers`
85
+ - Server extra: `grpcio`, `grpcio-tools`, and `python-dotenv`
86
+ - Optional UI: `fastapi`, `uvicorn`, and `httpx2` (for UI tests)
87
+ - Metadata extra: `sqlalchemy` and `psycopg[binary]`
88
+ - Dev/test extra: `pytest`
89
+
90
+ ## Installation
91
+
92
+ For end-user installation from PyPI or GitHub, see
93
+ [`INSTALLATION.md`](INSTALLATION.md). For deployment topologies and operational
94
+ guidance, see [`DEPLOYMENT.md`](DEPLOYMENT.md).
95
+
96
+ ```bash
97
+ python -m venv .venv
98
+ source .venv/bin/activate
99
+ pip install -U pip
100
+ pip install -e .
101
+ ```
102
+
103
+ Install the optional server, metadata, and test dependencies as needed:
104
+
105
+ ```bash
106
+ pip install -e '.[server,dev]'
107
+ pip install -e '.[metadata]'
108
+ ```
109
+
110
+ Runtime settings use the `STRATUM_*` environment-variable prefix. The gRPC entrypoint loads `.env` when `python-dotenv` is installed, while explicitly set process environment variables take precedence. `STRATUM_METADATA_ENABLED` defaults to `false`; accepted true values are `1`, `true`, `yes`, and `on` (case-insensitive), and accepted false values are `0`, `false`, `no`, and `off`.
111
+
112
+ ## Example usage
113
+
114
+ ```python
115
+ from pathlib import Path
116
+ from stratum.engine import Engine
117
+
118
+ engine = Engine(Path("./data"))
119
+ engine.put(b"user:1", b"Alice")
120
+ print(engine.get(b"user:1"))
121
+ engine.delete(b"user:1")
122
+
123
+ # Merge all current SSTables into one, dropping tombstoned keys for good.
124
+ # Manual only — not triggered automatically on any threshold.
125
+ engine.compact()
126
+ ```
127
+
128
+ Stratum also includes a standalone gRPC server with `Get`, `Put`, and streaming `Scan` operations.
129
+
130
+ ### Optional local web UI
131
+
132
+ Stratum includes an optional, offline browser UI for local administration. It
133
+ uses a small FastAPI gateway with the embedded `Engine` and a static vanilla
134
+ JavaScript frontend; there is no Node/npm or frontend build step.
135
+
136
+ Install and run it from the repository root:
137
+
138
+ ```bash
139
+ python -m pip install -r ui/requirements.txt
140
+ STRATUM_UI_DATA_DIR=./ui_data python -m uvicorn ui.app:app --host 127.0.0.1 --port 8000
141
+ ```
142
+
143
+ Then open <http://127.0.0.1:8000/>. The UI supports Put, Get, Delete, inclusive
144
+ range scans, live WAL/MemTable/SSTable state, and manual compaction. It binds
145
+ to localhost by default, has no authentication, and must be the only writer for
146
+ its configured data directory; do not run it alongside `stratum-server` against
147
+ the same directory. `STRATUM_UI_HOST`, `STRATUM_UI_PORT`, and
148
+ `STRATUM_UI_DATA_DIR` configure the bind address, port, and data directory.
149
+
150
+ The UI does not expose Flush because `Engine` has no public flush method. Values
151
+ are limited to 1 MiB and scan limits to 1,000 rows. See
152
+ [`ui/README.md`](ui/README.md) for the complete setup and operating guidance.
153
+
154
+ The gRPC server can optionally expose Prometheus metrics without adding a
155
+ runtime dependency. Set `STRATUM_METRICS_PORT` to an HTTP port to enable the
156
+ `/metrics` endpoint; leave it unset for the smallest standalone deployment.
157
+ The endpoint reports operation counters and current MemTable/SSTable gauges.
158
+
159
+ ### Important API contracts
160
+
161
+ - `Engine.put(key, value)` and `Engine.delete(key)` require `key`/`value` to be `bytes` — non-`bytes` input raises `TypeError` immediately, no silent conversion
162
+ - `Engine.get(key)` returns `bytes` or `None`
163
+ - gRPC `Put`, `Get`, and `Scan` reject empty keys/bounds with `INVALID_ARGUMENT`; proto3's unset `bytes` fields otherwise arrive as `b""` and would silently target the same empty key
164
+ - gRPC's default receive limit is approximately 4 MiB; larger requests receive `RESOURCE_EXHAUSTED` before reaching the engine
165
+ - The gRPC channel is intentionally insecure and has no TLS or authentication in v1.0
166
+ - `Engine.scan(start_key, end_key)` requires both inclusive byte bounds; open-ended scans are rejected
167
+ - `Delete` is available on the in-process `Engine` API, but there is no Delete gRPC method in v1.0; adding it is deferred because it requires proto regeneration and compatibility review
168
+ - A `put()` call that happens to trigger a MemTable flush is **not durable** until that flush completes — if the process dies mid-flush, that specific write is lost (see `STRATUM_PROJECT.md` for the reasoning)
169
+ - `Engine.compact()` is safe to interrupt at any point: a crash before the merged file is fully written leaves all original SSTables untouched; a crash after leaves the merged file and possibly some undeleted (but now redundant) originals — reads remain correct either way, since the newest file always wins on a shadowing conflict (see `STRATUM_PROJECT.md`, Phase 3)
170
+ - WAL and SSTable files are persisted under the provided `data_dir` (SSTables optionally under a separate `table_dir`, defaulting to `data_dir` if not given)
171
+
172
+ ## Phase 6 scope and deferred work
173
+
174
+ Phase 6 adds property-based tests, optional Prometheus metrics, clean package
175
+ metadata, and format documentation. The core package remains standalone and
176
+ requires only `sortedcontainers`; gRPC and metrics are optional server
177
+ capabilities.
178
+
179
+ The following are deliberately deferred rather than silently omitted:
180
+
181
+ - automatic or background compaction;
182
+ - tiered/streaming compaction with a `heapq` merge;
183
+ - write batching;
184
+ - an outbox pattern for external event delivery;
185
+ - PostgreSQL metadata and Redis Streams integration;
186
+ - TLS, authentication, and multi-instance production hardening.
187
+
188
+ See [`docs/WAL_FORMAT.md`](docs/WAL_FORMAT.md),
189
+ [`docs/SSTABLE_FORMAT.md`](docs/SSTABLE_FORMAT.md), and
190
+ [`docs/COMPACTION.md`](docs/COMPACTION.md) for the binary formats and
191
+ compaction rationale.
192
+
193
+ ## Project structure
194
+
195
+ ```text
196
+ stratum-db/
197
+ ├── STRATUM_PROJECT.md
198
+ ├── LICENSE
199
+ ├── README.md
200
+ ├── Stratum Dev Roadmap.md
201
+ ├── pyproject.toml
202
+ ├── docker-compose.dev.yml
203
+ ├── TEST_INVENTORY.md
204
+ ├── stratum/
205
+ │ ├── __init__.py
206
+ │ ├── db.py
207
+ │ ├── engine.py
208
+ │ ├── memtable.py
209
+ │ ├── models.py
210
+ │ ├── sstable.py
211
+ │ ├── wal.py
212
+ │ └── grpc/
213
+ │ ├── __init__.py
214
+ │ ├── server.py
215
+ │ ├── stratum.proto
216
+ │ ├── stratum_pb2.py
217
+ │ └── stratum_pb2_grpc.py
218
+ └── tests/
219
+ ├── test_memtable.py
220
+ ├── test_wal.py
221
+ ├── test_sstable.py
222
+ ├── test_recovery.py
223
+ └── test_compaction.py
224
+ ```
225
+
226
+ ## Tests
227
+
228
+ Run the default suite with:
229
+
230
+ ```bash
231
+ pytest
232
+ ```
233
+
234
+ The million-entry compaction check is marked `slow` and is excluded from the
235
+ default run. Run it independently when validating full-scale performance:
236
+
237
+ ```bash
238
+ pytest -m slow
239
+ ```
240
+
241
+ Validation record: the full non-slow suite completed with metadata unset at
242
+ `102 passed, 1 deselected`; a fresh-clone run of the pending tree completed at
243
+ `106 passed, 1 deselected`; the independent million-entry run completed with
244
+ `1 passed, 102 deselected` in 38:18. The latter was run at full scale.
245
+
246
+ Includes real `kill -9` (`SIGKILL`) crash-injection tests — not simulated failure — for WAL replay and SSTable flush/truncate crash windows, plus deterministic fault-injection tests for compaction's old-file cleanup step.
247
+
248
+ ## Design and durability notes
249
+
250
+ - WAL records use a packed header and CRC32 checksum; replay stops at the first corrupted entry rather than skipping past it
251
+ - SSTable flush writes to a temporary file, `fsync`s it to force bytes actually onto disk, then atomically renames into place — the rename is the single commit point. `Engine.compact()`'s merged output goes through the same path, reused rather than reimplemented
252
+ - Tombstones are represented as deleted entries; `scan()` returns them untouched and callers decide what to do with them — `Engine.get()` treats the first hit (newest file first) as authoritative, `Engine.compact()` sees every tombstone across every file and uses that visibility to drop dead keys permanently
253
+ - `Engine.get()` checks MemTable first, then falls back to SSTables newest-first, so a more recent write — or delete — always shadows an older one, across both in-memory and on-disk state
254
+ - SSTable entry parsing (header unpack, truncation checks, duplicate-key integrity check) lives in one shared generator used by both `scan()` and restart-time file loading, so a corrupted file is caught consistently regardless of which path reads it first
255
+ - `mmap` was deliberately not used for the WAL, despite being in the original project roadmap — chosen against in favor of plain `fsync()`-based writes to avoid `SIGBUS`-class risk on a correctness-critical path; see `STRATUM_PROJECT.md` for the full tradeoff
256
+ - Compaction uses a naive full-materialize-then-sort merge rather than a streaming/heap-based k-way merge — a deliberate choice given measured data volumes at project scale, not a performance oversight; see `STRATUM_PROJECT.md`, Phase 3, for the tradeoff analysis
257
+
258
+ ## Contributing
259
+
260
+ Contributions are welcome via issues or pull requests. Please keep changes aligned with the
261
+ project's current goal: a correct, durable Python LSM-tree engine with a clean byte-oriented API.
262
+
263
+ ## License
264
+
265
+ This project is licensed under the terms of the [LICENSE](LICENSE) file.
@@ -0,0 +1,245 @@
1
+ # Stratum DB
2
+
3
+ [![Tests](https://github.com/VishnuP2412/stratum-db/actions/workflows/tests.yml/badge.svg)](https://github.com/VishnuP2412/stratum-db/actions/workflows/tests.yml)
4
+
5
+ `stratum-db` is a write-optimized, crash-safe Python LSM-tree key-value engine.
6
+ It implements a durable MemTable + WAL + SSTable storage stack with binary formats,
7
+ crash recovery, compaction, sparse indexes, Bloom filters, and a gRPC interface.
8
+
9
+ ## Performance
10
+
11
+ Stratum DB is designed to make indexed point lookups practical at larger data
12
+ volumes. A recorded pre-index benchmark took approximately **30 minutes for
13
+ 100,000 lookups**. The current implementation includes sparse on-disk indexes
14
+ and Bloom filters to avoid unnecessary full-file searches; a directly comparable
15
+ post-index lookup measurement is still pending.
16
+
17
+ | Workload | Recorded result | Notes |
18
+ |---|---:|---|
19
+ | 100,000 point lookups, pre-index implementation | ~30 minutes | Historical baseline |
20
+ | 1,000,000-entry compaction | 38:18 | Full-scale slow test |
21
+
22
+ The million-entry result was recorded with Python 3.12. Hardware, dataset
23
+ parameters, and a comparable post-index lookup result should be recorded before
24
+ using these figures as a cross-machine performance comparison.
25
+
26
+ ## What this repo contains
27
+
28
+ - `stratum/engine.py` — the core `Engine` class that coordinates WAL, MemTable, SSTable files, and compaction
29
+ - `stratum/wal.py` — write-ahead log with CRC-checked append and replay
30
+ - `stratum/memtable.py` — in-memory sorted key-value store with tombstone support
31
+ - `stratum/sstable.py` — immutable, sorted on-disk segment file format with range scan, sparse indexes, Bloom filters, restart-safe rediscovery, and shared entry-parsing
32
+ - `stratum/grpc/` — protobuf definition, generated stubs, and the gRPC server
33
+ - `stratum/db.py` and `stratum/models.py` — optional PostgreSQL metadata integration for SSTables and compaction jobs
34
+ - `tests/` — pytest coverage for WAL, MemTable, SSTable, recovery, compaction, and real SIGKILL crash-safety tests
35
+ - `STRATUM_PROJECT.md` — design rationale, tradeoffs, and phase-by-phase decisions, including known gaps
36
+ - `Stratum Dev Roadmap.md` — project scope, phased milestones, and ecosystem alignment
37
+
38
+ ## Status
39
+
40
+ This repository currently implements:
41
+
42
+ - durable WAL append with CRC validation and corruption-safe replay
43
+ - in-memory `MemTable` with sorted keys, tombstone-based deletes, and `bytes`-only key/value enforcement
44
+ - SSTable flush (atomic temp-file + fsync + rename) and range/point read via `scan()`
45
+ - **SSTable rediscovery on restart** — `Engine` rebuilds its full view of on-disk SSTables from a fresh directory scan at startup, so data flushed in a _previous_ process lifetime is visible again after a restart, not just WAL-replayed MemTable state
46
+ - **Unambiguous tombstone handling across the MemTable/SSTable boundary** — `scan()` returns tombstones untouched (filtering is the caller's job), and `Engine.get()` stops at the first SSTable (newest-first) that has _any_ record for a key, live or deleted, so a delete correctly shadows an older value in an older file instead of being silently bypassed
47
+ - **Manual compaction** (`Engine.compact()`) — merges all current SSTables into one, resolves conflicting versions of a key by highest sequence number, and physically drops tombstoned entries — the first point in the pipeline where deleted data is actually removed from disk rather than just filtered at read time. Not auto-triggered; called explicitly
48
+ - **Sparse on-disk indexes and Bloom filters** — each SSTable stores a sparse key-to-offset index and a Bloom filter, and `Engine.get()` uses both to avoid unnecessary full-file searches
49
+ - `bytes`-only API contracts enforced (fail loudly, no silent type coercion) at every component boundary, including the shared low-level entry-parsing path used by both `scan()` and restart-time loading
50
+ - real process-kill (`SIGKILL`) crash-safety tests covering WAL replay, SSTable flush/rename, and compaction's old-file cleanup step (via deterministic fault injection — see `STRATUM_PROJECT.md` for why a live SIGKILL specifically inside that window wasn't pursued further)
51
+ - **gRPC `Get`, `Put`, and streaming `Scan` operations** — a standalone server is available through `stratum/grpc/server.py`
52
+
53
+ **Known limitations:** compaction currently uses a naive full-materialize-then-sort merge rather than a tiered or streaming strategy, and it is manual rather than automatically triggered by a size or level threshold. These are deliberate next-stage tradeoffs; see `STRATUM_PROJECT.md`.
54
+
55
+ PostgreSQL metadata writes are optional and disabled by default. Set `STRATUM_METADATA_ENABLED=true` to enable them and install the `metadata` extra. The core engine does not require SQLAlchemy or gRPC dependencies.
56
+ Docker is only needed if you choose to exercise the optional `metadata` extra against a local PostgreSQL instance; it is not part of the default setup or test run.
57
+
58
+ Phase 6 property-based tests are included in the development extra and cover
59
+ WAL replay and compaction state preservation.
60
+
61
+ ## Requirements
62
+
63
+ - Python 3.12+
64
+ - Core: `sortedcontainers`
65
+ - Server extra: `grpcio`, `grpcio-tools`, and `python-dotenv`
66
+ - Optional UI: `fastapi`, `uvicorn`, and `httpx2` (for UI tests)
67
+ - Metadata extra: `sqlalchemy` and `psycopg[binary]`
68
+ - Dev/test extra: `pytest`
69
+
70
+ ## Installation
71
+
72
+ For end-user installation from PyPI or GitHub, see
73
+ [`INSTALLATION.md`](INSTALLATION.md). For deployment topologies and operational
74
+ guidance, see [`DEPLOYMENT.md`](DEPLOYMENT.md).
75
+
76
+ ```bash
77
+ python -m venv .venv
78
+ source .venv/bin/activate
79
+ pip install -U pip
80
+ pip install -e .
81
+ ```
82
+
83
+ Install the optional server, metadata, and test dependencies as needed:
84
+
85
+ ```bash
86
+ pip install -e '.[server,dev]'
87
+ pip install -e '.[metadata]'
88
+ ```
89
+
90
+ Runtime settings use the `STRATUM_*` environment-variable prefix. The gRPC entrypoint loads `.env` when `python-dotenv` is installed, while explicitly set process environment variables take precedence. `STRATUM_METADATA_ENABLED` defaults to `false`; accepted true values are `1`, `true`, `yes`, and `on` (case-insensitive), and accepted false values are `0`, `false`, `no`, and `off`.
91
+
92
+ ## Example usage
93
+
94
+ ```python
95
+ from pathlib import Path
96
+ from stratum.engine import Engine
97
+
98
+ engine = Engine(Path("./data"))
99
+ engine.put(b"user:1", b"Alice")
100
+ print(engine.get(b"user:1"))
101
+ engine.delete(b"user:1")
102
+
103
+ # Merge all current SSTables into one, dropping tombstoned keys for good.
104
+ # Manual only — not triggered automatically on any threshold.
105
+ engine.compact()
106
+ ```
107
+
108
+ Stratum also includes a standalone gRPC server with `Get`, `Put`, and streaming `Scan` operations.
109
+
110
+ ### Optional local web UI
111
+
112
+ Stratum includes an optional, offline browser UI for local administration. It
113
+ uses a small FastAPI gateway with the embedded `Engine` and a static vanilla
114
+ JavaScript frontend; there is no Node/npm or frontend build step.
115
+
116
+ Install and run it from the repository root:
117
+
118
+ ```bash
119
+ python -m pip install -r ui/requirements.txt
120
+ STRATUM_UI_DATA_DIR=./ui_data python -m uvicorn ui.app:app --host 127.0.0.1 --port 8000
121
+ ```
122
+
123
+ Then open <http://127.0.0.1:8000/>. The UI supports Put, Get, Delete, inclusive
124
+ range scans, live WAL/MemTable/SSTable state, and manual compaction. It binds
125
+ to localhost by default, has no authentication, and must be the only writer for
126
+ its configured data directory; do not run it alongside `stratum-server` against
127
+ the same directory. `STRATUM_UI_HOST`, `STRATUM_UI_PORT`, and
128
+ `STRATUM_UI_DATA_DIR` configure the bind address, port, and data directory.
129
+
130
+ The UI does not expose Flush because `Engine` has no public flush method. Values
131
+ are limited to 1 MiB and scan limits to 1,000 rows. See
132
+ [`ui/README.md`](ui/README.md) for the complete setup and operating guidance.
133
+
134
+ The gRPC server can optionally expose Prometheus metrics without adding a
135
+ runtime dependency. Set `STRATUM_METRICS_PORT` to an HTTP port to enable the
136
+ `/metrics` endpoint; leave it unset for the smallest standalone deployment.
137
+ The endpoint reports operation counters and current MemTable/SSTable gauges.
138
+
139
+ ### Important API contracts
140
+
141
+ - `Engine.put(key, value)` and `Engine.delete(key)` require `key`/`value` to be `bytes` — non-`bytes` input raises `TypeError` immediately, no silent conversion
142
+ - `Engine.get(key)` returns `bytes` or `None`
143
+ - gRPC `Put`, `Get`, and `Scan` reject empty keys/bounds with `INVALID_ARGUMENT`; proto3's unset `bytes` fields otherwise arrive as `b""` and would silently target the same empty key
144
+ - gRPC's default receive limit is approximately 4 MiB; larger requests receive `RESOURCE_EXHAUSTED` before reaching the engine
145
+ - The gRPC channel is intentionally insecure and has no TLS or authentication in v1.0
146
+ - `Engine.scan(start_key, end_key)` requires both inclusive byte bounds; open-ended scans are rejected
147
+ - `Delete` is available on the in-process `Engine` API, but there is no Delete gRPC method in v1.0; adding it is deferred because it requires proto regeneration and compatibility review
148
+ - A `put()` call that happens to trigger a MemTable flush is **not durable** until that flush completes — if the process dies mid-flush, that specific write is lost (see `STRATUM_PROJECT.md` for the reasoning)
149
+ - `Engine.compact()` is safe to interrupt at any point: a crash before the merged file is fully written leaves all original SSTables untouched; a crash after leaves the merged file and possibly some undeleted (but now redundant) originals — reads remain correct either way, since the newest file always wins on a shadowing conflict (see `STRATUM_PROJECT.md`, Phase 3)
150
+ - WAL and SSTable files are persisted under the provided `data_dir` (SSTables optionally under a separate `table_dir`, defaulting to `data_dir` if not given)
151
+
152
+ ## Phase 6 scope and deferred work
153
+
154
+ Phase 6 adds property-based tests, optional Prometheus metrics, clean package
155
+ metadata, and format documentation. The core package remains standalone and
156
+ requires only `sortedcontainers`; gRPC and metrics are optional server
157
+ capabilities.
158
+
159
+ The following are deliberately deferred rather than silently omitted:
160
+
161
+ - automatic or background compaction;
162
+ - tiered/streaming compaction with a `heapq` merge;
163
+ - write batching;
164
+ - an outbox pattern for external event delivery;
165
+ - PostgreSQL metadata and Redis Streams integration;
166
+ - TLS, authentication, and multi-instance production hardening.
167
+
168
+ See [`docs/WAL_FORMAT.md`](docs/WAL_FORMAT.md),
169
+ [`docs/SSTABLE_FORMAT.md`](docs/SSTABLE_FORMAT.md), and
170
+ [`docs/COMPACTION.md`](docs/COMPACTION.md) for the binary formats and
171
+ compaction rationale.
172
+
173
+ ## Project structure
174
+
175
+ ```text
176
+ stratum-db/
177
+ ├── STRATUM_PROJECT.md
178
+ ├── LICENSE
179
+ ├── README.md
180
+ ├── Stratum Dev Roadmap.md
181
+ ├── pyproject.toml
182
+ ├── docker-compose.dev.yml
183
+ ├── TEST_INVENTORY.md
184
+ ├── stratum/
185
+ │ ├── __init__.py
186
+ │ ├── db.py
187
+ │ ├── engine.py
188
+ │ ├── memtable.py
189
+ │ ├── models.py
190
+ │ ├── sstable.py
191
+ │ ├── wal.py
192
+ │ └── grpc/
193
+ │ ├── __init__.py
194
+ │ ├── server.py
195
+ │ ├── stratum.proto
196
+ │ ├── stratum_pb2.py
197
+ │ └── stratum_pb2_grpc.py
198
+ └── tests/
199
+ ├── test_memtable.py
200
+ ├── test_wal.py
201
+ ├── test_sstable.py
202
+ ├── test_recovery.py
203
+ └── test_compaction.py
204
+ ```
205
+
206
+ ## Tests
207
+
208
+ Run the default suite with:
209
+
210
+ ```bash
211
+ pytest
212
+ ```
213
+
214
+ The million-entry compaction check is marked `slow` and is excluded from the
215
+ default run. Run it independently when validating full-scale performance:
216
+
217
+ ```bash
218
+ pytest -m slow
219
+ ```
220
+
221
+ Validation record: the full non-slow suite completed with metadata unset at
222
+ `102 passed, 1 deselected`; a fresh-clone run of the pending tree completed at
223
+ `106 passed, 1 deselected`; the independent million-entry run completed with
224
+ `1 passed, 102 deselected` in 38:18. The latter was run at full scale.
225
+
226
+ Includes real `kill -9` (`SIGKILL`) crash-injection tests — not simulated failure — for WAL replay and SSTable flush/truncate crash windows, plus deterministic fault-injection tests for compaction's old-file cleanup step.
227
+
228
+ ## Design and durability notes
229
+
230
+ - WAL records use a packed header and CRC32 checksum; replay stops at the first corrupted entry rather than skipping past it
231
+ - SSTable flush writes to a temporary file, `fsync`s it to force bytes actually onto disk, then atomically renames into place — the rename is the single commit point. `Engine.compact()`'s merged output goes through the same path, reused rather than reimplemented
232
+ - Tombstones are represented as deleted entries; `scan()` returns them untouched and callers decide what to do with them — `Engine.get()` treats the first hit (newest file first) as authoritative, `Engine.compact()` sees every tombstone across every file and uses that visibility to drop dead keys permanently
233
+ - `Engine.get()` checks MemTable first, then falls back to SSTables newest-first, so a more recent write — or delete — always shadows an older one, across both in-memory and on-disk state
234
+ - SSTable entry parsing (header unpack, truncation checks, duplicate-key integrity check) lives in one shared generator used by both `scan()` and restart-time file loading, so a corrupted file is caught consistently regardless of which path reads it first
235
+ - `mmap` was deliberately not used for the WAL, despite being in the original project roadmap — chosen against in favor of plain `fsync()`-based writes to avoid `SIGBUS`-class risk on a correctness-critical path; see `STRATUM_PROJECT.md` for the full tradeoff
236
+ - Compaction uses a naive full-materialize-then-sort merge rather than a streaming/heap-based k-way merge — a deliberate choice given measured data volumes at project scale, not a performance oversight; see `STRATUM_PROJECT.md`, Phase 3, for the tradeoff analysis
237
+
238
+ ## Contributing
239
+
240
+ Contributions are welcome via issues or pull requests. Please keep changes aligned with the
241
+ project's current goal: a correct, durable Python LSM-tree engine with a clean byte-oriented API.
242
+
243
+ ## License
244
+
245
+ This project is licensed under the terms of the [LICENSE](LICENSE) file.
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "stratum-db"
3
+ version = "1.0.0"
4
+ description = "A standalone write-optimized LSM-tree key-value database"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "sortedcontainers",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["setuptools>=68"]
13
+ build-backend = "setuptools.build_meta"
14
+
15
+ [project.optional-dependencies]
16
+ metadata = ["sqlalchemy", "psycopg[binary]"]
17
+ server = ["grpcio", "grpcio-tools", "python-dotenv"]
18
+ dev = ["pytest", "hypothesis"]
19
+
20
+ [project.scripts]
21
+ stratum-server = "stratum.api:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["stratum*"]
25
+
26
+ [tool.pytest.ini_options]
27
+ pythonpath = ["."]
28
+ addopts = "-m 'not slow'"
29
+ markers = [
30
+ "timeout(seconds): mark test to fail if it exceeds the given number of seconds",
31
+ "slow: mark tests that are excluded from the default suite"
32
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,41 @@
1
+ import os
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ def _get_config():
10
+ data_dir = Path(os.environ.get("STRATUM_DATA_DIR","./data"))
11
+
12
+ table_dir_raw = os.environ.get("STRATUM_TABLE_DIR")
13
+ table_dir = Path(table_dir_raw) if table_dir_raw else None
14
+
15
+ port_raw = os.environ.get("STRATUM_PORT","50051")
16
+
17
+ try:
18
+ port = int(port_raw)
19
+ except ValueError:
20
+ raise ValueError(
21
+ f"STRATUM_PORT must be an integer, got {port_raw!r}"
22
+ )
23
+
24
+ log_level_name = os.environ.get("STRATUM_LOG_LEVEL","INFO").upper()
25
+ log_level = getattr(logging, log_level_name)
26
+ if not isinstance(log_level, int):
27
+ raise ValueError(
28
+ f"STRATUM_LOG_LEVEL must be a valid level name, got {log_level_name!r}"
29
+ )
30
+
31
+ return data_dir, table_dir, port, log_level
32
+
33
+ def main():
34
+ data_dir, table_dir, port, log_level = _get_config()
35
+ logging.basicConfig(level=log_level)
36
+
37
+ from stratum.grpc.server import serve
38
+ serve(data_dir = data_dir, table_dir=table_dir, port=port)
39
+
40
+ if __name__ == "__main__":
41
+ main()