# 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")]));

assert_eq!(user.get("name").and_then(|v| v.as_str()), Some("ada"));
```

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:

| Operation | Time |
|-----------|------|
| `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).
