# bison-db v0.4.0 — Durability policy + on-disk format freeze

**The format is stable.** v0.4.0 makes durability configurable, hardens crash
recovery, and **freezes the on-disk format at version 1** — files written by
0.2.0 onward stay readable by every later release. Zero breaking changes against
v0.3.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.

## A note on "WAL"

bison-db is **log-structured**: the data file *is* an append-only log, so writes
are write-ahead by construction. Bolting a second, separate WAL onto a
log-structured store would be redundant. So the durability work in this release
is about the knobs that actually matter — *when* the log is forced to disk, and
guaranteeing the format that log is written in will not change underneath you.

## What's new in 0.4.0

### Configurable durability — `SyncPolicy`

bison-db never buffers writes in userspace; every write reaches the OS
immediately and is visible to later reads. `SyncPolicy` controls only when those
bytes are forced to the device with `fsync`.

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

fn main() -> bison_db::Result<()> {
    # let path = std::env::temp_dir().join("bison_db_rel_040.bison");
    # let _ = std::fs::remove_file(&path);
    // Durable per write: every insert/update/delete fsyncs before returning.
    let mut db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always))?;
    db.insert({ let mut d = Document::new(); d.set("entry", "debit 100"); d })?;
    assert_eq!(db.sync_policy(), SyncPolicy::Always);
    # drop(db);
    # let _ = std::fs::remove_file(&path);
    Ok(())
}
```

- **`SyncPolicy::Always`** — `fsync` after every write. Each operation is durable
  the instant it returns.
- **`SyncPolicy::Manual`** *(default)* — `fsync` on `flush()`, and best-effort on
  drop. Fastest; recent unsynced writes can be lost on power loss, but the file
  is never corrupt.

`Db::open` is unchanged and uses `Manual`; opt into a policy through the new
`DbOptions` builder or `Db::open_with`.

### Recovery hardening

- **Best-effort sync on drop** under `Manual`, so a clean program exit does not
  silently lose unflushed writes.
- **Parent-directory `fsync` on creation** (Unix), so a new store's *existence* —
  not just its contents — survives a crash right after creation. On Windows this
  is a documented no-op, since file-level `fsync` already persists the directory
  entry there.

### On-disk format frozen — `docs/FORMAT.md`

The file layout is now **stable at format version 1** and fully specified in
[`docs/FORMAT.md`](https://github.com/jamesgober/bison-db/blob/main/docs/FORMAT.md):
the 16-byte header, the length+CRC-32C record frame, the operation/id/body
payload, and the recursive value encoding. Files written by 0.2.0 onward remain
readable; an incompatible change would bump both the format version and the
crate's major version. Multi-byte integers are little-endian regardless of host,
so files are portable across platforms.

### Example and benchmark

- `examples/durability.rs` writes a batch under each policy and reopens both to
  show they recover identically.
- A new Criterion group, `insert_by_sync_policy`, measures the `Manual`-vs-`Always`
  insert cost so the durability trade-off is quantified, not asserted.

## 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 |

The cost of `SyncPolicy::Always` is one device `fsync` per write and is dominated
by the storage hardware, not by bison-db — on the development machine, a batch of
2,000 inserts took a few milliseconds under `Manual` versus a couple of seconds
under `Always`. Choose per workload: batch with `Manual` + `flush` for
throughput, `Always` for per-record durability.

## Breaking changes

**None.** All additions are new types and methods; `Db::open` behaves exactly as
before. The on-disk format is unchanged from v0.2.0 — it is now simply declared
frozen.

## 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`): 44 unit + 9 integration +
3 property + 57 doctests.

## What's next

- **v0.5.0 — Concurrency + compaction.** Safe shared-access patterns for
  multi-threaded workloads, and a compaction step that reclaims the space held by
  superseded and deleted records.

## Installation

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

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