# bison-db v0.2.0 — Document model + single-file store
**The first working release.** v0.2.0 turns the v0.1.0 scaffold into a usable
embedded database: a typed document model and a durable, single-file store with
`insert` / `get` / `update` / `delete`. Every record is length-framed and
CRC-32C checked, and a write torn by a crash is detected and dropped on the next
open. The crate builds and the full test suite passes on Linux and Windows.
## What is bison-db?
An embedded, document-oriented database for Rust: store schemaless documents
entirely in-process, with no server, no network, and no external services. The
whole database is one file — trivial to ship, copy, and back up — and writes are
appended to a checksummed log, so a crash never leaves a half-written record
behind. It is the first member of the Bison family of embedded databases.
## What's new in 0.2.0
### The document model — `Value` and `Document`
A `Document` is an insertion-ordered set of named fields; each field's content
is a `Value` — null, bool, `i64`, `f64`, string, bytes, array, or a nested
document. Field order is preserved so an encode/decode round trip compares equal.
`Value` implements `From` for the common Rust types, so building a record reads
naturally:
```rust
use bison_db::{Document, Value};
let mut user = Document::new();
user.set("name", "ada")
.set("age", 36_i64)
.set("admin", true)
.set("roles", Value::Array(vec![Value::from("author"), Value::from("admin")]));
Lookups are a linear scan over a flat vector — the fastest layout for the small
field counts documents actually carry, with no hashing or pointer chasing.
### The single-file store — `Db`
`Db` persists documents to one append-only file. Every write appends a
self-describing record to the tail; the file is never edited in place. An
in-memory index maps each live document id to the byte offset of its most recent
record, so a read is one hash lookup and one positional read.
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_rel_020.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let mut doc = Document::new();
doc.set("title", "Kind of Blue").set("year", 1959_i64);
let id = db.insert(doc)?; // -> DocId
let stored = db.get(id)?.expect("present");
assert_eq!(stored.get("year").and_then(|v| v.as_int()), Some(1959));
db.update(id, { let mut d = Document::new(); d.set("title", "So What"); d })?;
assert!(db.delete(id)?);
db.flush()?; // fsync: durable from here
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
Reads take `&self` and writes take `&mut self`, so the compiler enforces
single-writer access. `DocId`s are dense, monotonic, and stable across reopen;
`Stats` reports live document count and file size.
### Crash-safe log with replay recovery
The file opens with a versioned header, then a run of records: an 8-byte frame
(`u32` length, `u32` CRC-32C) followed by an op tag, the document id, and — for
an insert or overwrite — the encoded body. On `open`, the log is replayed and
every checksum verified:
- A record left half-written by a crash (it runs past the end of the file, or
fails its checksum at the tail) is truncated away, restoring the last
consistent state.
- A checksum failure on a record that is **not** at the tail is reported as
`Error::Corrupt` rather than silently misread.
- A file written by a newer format is refused with `Error::UnsupportedVersion`.
The `examples/crash_recovery.rs` program demonstrates this end to end by
appending garbage to a flushed store and reopening it intact.
### Bounded, non-panicking decoding
Every length field in the binary format is validated against the bytes actually
present before anything is allocated, and `MAX_RECORD_BYTES` (64 MiB) caps record
size. A corrupt or hostile length yields `Error::Corrupt` or
`Error::ValueTooLarge` — never an over-read, a panic, or an unbounded allocation.
A property test exercises this against arbitrary truncations.
### `serde` support
Behind the `serde` feature, `Value` and `Document` implement
`Serialize`/`Deserialize`, mapping onto the serde data model like a dynamic JSON
value. Documents move in and out of JSON, MessagePack, or any serde format:
```rust,ignore
let doc: bison_db::Document = serde_json::from_str(r#"{ "n": 1, "ok": true }"#)?;
let json = serde_json::to_string(&doc)?;
```
### Tests, examples, and benchmarks
- **Property tests** (`tests/proptests.rs`): inserting any document and reading
it back is lossless; after an arbitrary mix of inserts and deletes, a reopened
store reflects exactly the surviving set.
- **Integration tests** (`tests/store.rs`): many-document round trips, reopen
recovery, nested documents, large values, torn-tail truncation.
- **Examples** (`examples/`): `quick_start`, `user_profiles`, `crash_recovery`,
`json_interop`.
- **Benchmarks** (`benches/bison_bench.rs`): Criterion harnesses for `insert`,
`get`, and `update` against a real on-disk store.
## Performance
Indicative single-threaded figures from `cargo bench` on a developer laptop
(Linux/WSL2, x86_64, Rust 1.95), measured against a real on-disk store:
| `insert` a small document | ~0.8 µs |
| `get` a small document | ~0.3 µs |
| `update` a small document | ~0.8 µs |
These are baselines for the 0.x series, not a final claim; a populated
head-to-head comparison against other embedded and document stores is planned
for the 1.0 cycle.
## Breaking changes
None against `v0.1.0`, which had no public API. One behavioural note: with
default features disabled the crate is now `no_std` and exposes only the
in-memory document model — the file store requires `std`.
## Verification
Run on Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable 1.95.x; the same
commands run in the CI matrix across Linux, macOS, and Windows on stable and
MSRV 1.85:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
cargo build --no-default-features
cargo build --examples
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
```
All green. Test counts at this tag (`--all-features`): 28 unit + 9 integration +
2 property + 44 doctests.
## What's next
- **v0.3.0 — Secondary indexes + field and range queries.** Declare indexed
fields per document for lookups beyond the primary id, and query by field
predicate or by range over an index.
## Installation
```toml
[dependencies]
bison-db = "0.2"
# With serde support for the document model:
bison-db = { version = "0.2", features = ["serde"] }
```
MSRV: Rust 1.85 (2024 edition).
## Documentation
- [README](https://github.com/jamesgober/bison-db/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/bison-db/blob/main/docs/API.md)
- [Roadmap](https://github.com/jamesgober/bison-db/blob/main/dev/ROADMAP.md)
- [CHANGELOG](https://github.com/jamesgober/bison-db/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/bison-db/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/bison-db/blob/main/CHANGELOG.md#020---2026-06-06).