# bison-db v0.5.0 — Compaction, concurrency, and the API freeze

**The public API is frozen.** v0.5.0 adds space-reclaiming compaction, defines
the concurrency model, and declares the public surface stable — additive changes
only from here until 1.0. Zero breaking changes against v0.4.0.

## 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, writes are appended to a checksummed log, and a crash
never leaves a half-written record behind.

## What's new in 0.5.0

### Compaction — `Db::compact`

bison-db is append-only: every overwrite and delete leaves the previous record
behind as dead space, so an actively-updated store grows past the size of its
live data. `compact` rewrites the file with one record per live document and
reclaims the rest.

```rust
use bison_db::{Db, Document};

fn main() -> bison_db::Result<()> {
    # let path = std::env::temp_dir().join("bison_db_rel_050.bison");
    # let _ = std::fs::remove_file(&path);
    let mut db = Db::open(&path)?;
    let id = db.insert({ let mut d = Document::new(); d.set("v", 0_i64); d })?;
    for n in 1..=10_000 {
        db.update(id, { let mut d = Document::new(); d.set("v", n); d })?;
    }
    let before = db.stats().file_bytes;

    db.compact()?;                       // 10k dead versions collapse to one record

    assert!(db.stats().file_bytes < before);
    assert_eq!(db.get(id)?.unwrap().get("v").and_then(|v| v.as_int()), Some(10_000));
    # let _ = std::fs::remove_file(&path);
    Ok(())
}
```

In the `compaction` example, 10,000 updates to one document shrink the file from
~390 KB to 55 bytes with the live value intact.

**It is crash-safe.** The compacted copy is built in a sibling temporary file,
`fsync`ed, and swapped in with an **atomic rename**, so a crash at any point
leaves either the original file or the fully compacted one — never a partial
result. A leftover temporary from an interrupted compaction is removed on the
next `open`. Document ids are preserved, so existing `DocId`s and secondary
indexes stay valid; only the on-disk layout changes. The atomic-swap path is
verified on both Windows and Linux.

### Concurrency model

`Db` follows a **single-writer, multi-reader** model, like an embedded SQL
engine: reads take `&self`, writes take `&mut self`, so the compiler guarantees
writes are exclusive. `Db` is `Send + Sync` — asserted at compile time — so the
idiomatic way to share one across threads is an `Arc<RwLock<Db>>`: many readers
concurrently, or one exclusive writer.

```rust
use std::sync::{Arc, RwLock};
use bison_db::{Db, Document};

fn main() -> bison_db::Result<()> {
    # let path = std::env::temp_dir().join("bison_db_rel_050_conc.bison");
    # let _ = std::fs::remove_file(&path);
    let db = Arc::new(RwLock::new(Db::open(&path)?));
    db.write().unwrap().insert({ let mut d = Document::new(); d.set("k", 1_i64); d })?;

    let reader = Arc::clone(&db);
    let n = std::thread::spawn(move || reader.read().unwrap().len()).join().unwrap();
    assert_eq!(n, 1);
    # let _ = std::fs::remove_file(&path);
    Ok(())
}
```

A single append-only file has one tail, so single-writer is inherent to the
design and the right fit for an embedded store. A new integration test exercises
concurrent readers against a live writer through `Arc<RwLock<Db>>`.

### Public API freeze

The public surface is now **stable**: additive changes only until 1.0, and no
breaking change before then. The frozen set is recorded in
[`dev/ROADMAP.md`](https://github.com/jamesgober/bison-db/blob/main/dev/ROADMAP.md)
and documented item-by-item in
[`docs/API.md`](https://github.com/jamesgober/bison-db/blob/main/docs/API.md):
the types `Db`, `DbOptions`, `SyncPolicy`, `DocId`, `Stats`, `Document`, `Value`,
`Error`, `Result`, the constant `MAX_RECORD_BYTES`, and their methods. Combined
with the v0.4.0 on-disk format freeze, both the API and the file format are now
stable contracts.

## Performance

Indicative single-threaded figures from `cargo bench` on a developer laptop
(Linux/WSL2, x86_64, Rust 1.95), against a real on-disk store:

| Operation | Time |
|-----------|------|
| `insert` (`Manual`, small doc) | ~0.8 µs |
| `get` (small doc) | ~0.3 µs |
| `find` by indexed field (10k-doc store) | ~60 ns |
| `find` by full scan (10k-doc store) | ~1.6 ms |

Compaction is a maintenance operation, not a hot path; its cost is proportional
to the live data size (one sequential rewrite plus one `fsync`).

## Breaking changes

**None.** `Db::compact` and the concurrency guarantees are additive; every
existing method behaves exactly as before. The on-disk format is unchanged.

## 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
cargo audit
cargo deny check
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
```

All green; `cargo audit` and `cargo deny` clean. Test counts at this tag
(`--all-features`): 50 unit + 9 integration + 3 concurrency + 3 property + 58
doctests.

## What's next

- **v0.6.0 → 1.0.0 — Alpha / Beta / RC / Stable.** Integrate against real
  consumers, broaden testing, capture final benchmarks, then publish 1.0 with the
  API and format that are already frozen here.

## Installation

```toml
[dependencies]
bison-db = "0.5"

# With serde support for the document model:
bison-db = { version = "0.5", 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)
- [On-disk Format](https://github.com/jamesgober/bison-db/blob/main/docs/FORMAT.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.4.0...v0.5.0`](https://github.com/jamesgober/bison-db/compare/v0.4.0...v0.5.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/bison-db/blob/main/CHANGELOG.md#050---2026-06-08).
