<h1 align="center">
<img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
<br><b>bison-db</b><br>
<sub><sup>API REFERENCE</sup></sub>
</h1>
<div align="center">
<sup>
<a href="../README.md" title="Project Home"><b>HOME</b></a>
<span> │ </span>
<span>API</span>
<span> │ </span>
<a href="./FORMAT.md" title="On-disk Format"><b>FORMAT</b></a>
<span> │ </span>
<a href="./PERFORMANCE.md" title="Performance"><b>PERFORMANCE</b></a>
<span> │ </span>
<a href="../CHANGELOG.md" title="Changelog"><b>CHANGELOG</b></a>
</sup>
</div>
<br>
> Complete reference for every public item in `bison-db` as of `v0.7.0`, with runnable examples.
> The crate is pre-1.0: the surface grows across the 0.x series (see [`dev/ROADMAP.md`](../dev/ROADMAP.md)) and is frozen at `1.0.0`. Items marked _(planned)_ are not yet implemented.
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [The document model](#the-document-model)
- [Error handling](#error-handling)
- [Public APIs](#public-apis)
- [`Db`](#db)
- [Secondary indexes and queries](#secondary-indexes-and-queries)
- [`DbOptions` and `SyncPolicy`](#dboptions)
- [`DocId`](#docid)
- [`Stats`](#stats)
- [`Document`](#document)
- [`Value`](#value)
- [`Error` and `Result`](#error)
- [`MAX_RECORD_BYTES`](#max-record-bytes)
- [Concurrency](#concurrency)
- [Durability and recovery](#durability-and-recovery)
- [Feature flags](#feature-flags)
- [Roadmap surface](#roadmap-surface)
---
## Installation
```toml
[dependencies]
bison-db = "0.7"
# Enable serde for the document model:
bison-db = { version = "0.7", features = ["serde"] }
```
The default `std` feature provides the file-backed [`Db`]. Disabling it
(`default-features = false`) drops the store and leaves a `no_std` build of the
in-memory document model ([`Document`], [`Value`]).
MSRV: Rust 1.85 (2024 edition).
---
## Quick Start
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_quickstart.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let mut doc = Document::new();
doc.set("title", "Take Five").set("year", 1959_i64);
let id = db.insert(doc)?;
let stored = db.get(id)?.expect("just inserted");
assert_eq!(stored.get("year").and_then(|v| v.as_int()), Some(1959));
db.flush()?;
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
---
## The document model
A **document** is an ordered set of named fields — the unit the store holds.
Each field's content is a **value**: one of null, boolean, signed 64-bit
integer, 64-bit float, UTF-8 string, raw bytes, array, or a nested document.
Documents preserve field insertion order, so an encode/decode round trip
compares equal to the original.
```rust
use bison_db::{Document, Value};
let mut doc = Document::new();
doc.set("name", "ada") // &str -> Value::Str
.set("age", 36_i64) // i64 -> Value::Int
.set("admin", true) // bool -> Value::Bool
.set("scores", Value::Array(vec![Value::from(9_i64)])); // explicit array
---
## Error handling
Every fallible operation returns [`Result<T>`](#error), an alias for
`Result<T, bison_db::Error>`. [`Error`](#error) is a small, closed enum; match on
it to distinguish a missing file from a corrupt one from an oversized value.
```rust
use bison_db::{Db, Error};
fn main() {
# let path = std::env::temp_dir().join("bison_db_api_err.bison");
# std::fs::write(&path, b"not a bison-db file").unwrap();
match Db::open(&path) {
Ok(_db) => { /* opened */ }
Err(Error::BadMagic) => eprintln!("that file is not a bison-db store"),
Err(Error::UnsupportedVersion(v)) => eprintln!("written by a newer format: {v}"),
Err(Error::Corrupt(what)) => eprintln!("damaged record: {what}"),
Err(e) => eprintln!("other failure: {e}"),
}
# let _ = std::fs::remove_file(&path);
}
```
No public method panics on invalid input or hostile data; failures are values.
---
## Public APIs
### `Db`
The single-file document store. Open one with [`Db::open`], then operate on
documents by [`DocId`]. Reads take `&self` and writes take `&mut self`, so the
compiler enforces single-writer access; to share a store across threads, place
it behind your own lock.
Requires the `std` feature (on by default).
#### `Db::open`
```rust,ignore
pub fn open<P: AsRef<Path>>(path: P) -> Result<Db>
```
Opens the store at `path`, creating an empty one if the file does not exist, and
replaying any existing records to rebuild the in-memory index.
- **`path`** — the file to open or create. Parent directories must already exist.
On open the whole log is scanned: each record's CRC-32C is verified and the
index is rebuilt from the surviving inserts and deletes. A record left
half-written by a crash — detectable because it runs past the end of the file or
fails its checksum at the tail — is truncated away. A checksum failure on a
record that is **not** at the tail is reported as [`Error::Corrupt`].
**Errors:** [`Error::Io`] (cannot open/read), [`Error::BadMagic`] (not a bison-db
file), [`Error::UnsupportedVersion`] (newer format), [`Error::Corrupt`] (a
non-tail record failed verification).
```rust
use bison_db::Db;
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_open1.bison");
# let _ = std::fs::remove_file(&path);
let db = Db::open(&path)?; // creates an empty store
assert!(db.is_empty());
drop(db);
let db = Db::open(&path)?; // reopens and recovers it
assert_eq!(db.len(), 0);
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::insert`
```rust,ignore
pub fn insert(&mut self, doc: Document) -> Result<DocId>
```
Appends `doc` to the log and returns a freshly assigned [`DocId`]. The document
is readable immediately and durable after the next [`flush`](#dbflush).
- **`doc`** — the document to store; consumed by the call.
**Errors:** [`Error::ValueTooLarge`] if the encoded document exceeds
[`MAX_RECORD_BYTES`](#max-record-bytes); [`Error::Io`] if the append fails.
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_insert.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let mut a = Document::new(); a.set("n", 1_i64);
let mut b = Document::new(); b.set("n", 2_i64);
let id_a = db.insert(a)?;
let id_b = db.insert(b)?;
assert_ne!(id_a, id_b); // ids are unique and increasing
assert_eq!(db.len(), 2);
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::get`
```rust,ignore
pub fn get(&self, id: DocId) -> Result<Option<Document>>
```
Reads the document stored under `id`, or `Ok(None)` if no live document has that
id (because it was never inserted, or it was deleted).
- **`id`** — the key returned by [`insert`](#dbinsert).
**Errors:** [`Error::Io`] (read failed); [`Error::Corrupt`] (stored bytes failed
to decode — unexpected given the passing checksum).
```rust
use bison_db::{Db, Document, DocId};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_get.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let mut doc = Document::new(); doc.set("city", "Oslo");
let id = db.insert(doc)?;
let found = db.get(id)?.expect("present");
assert_eq!(found.get("city").and_then(|v| v.as_str()), Some("Oslo"));
assert!(db.get(DocId::from(99_999))?.is_none()); // absent id
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::update`
```rust,ignore
pub fn update(&mut self, id: DocId, doc: Document) -> Result<bool>
```
Overwrites the document under `id` with `doc`. Returns `Ok(true)` if a document
was present to overwrite, `Ok(false)` if `id` is unknown (in which case nothing
is written).
- **`id`** — the document to replace.
- **`doc`** — the new contents.
A successful update appends a new record and repoints the index; the previous
body stays in the file as dead space until compaction.
**Errors:** as [`insert`](#dbinsert).
```rust
use bison_db::{Db, Document, DocId};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_update.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let mut v1 = Document::new(); v1.set("v", 1_i64);
let id = db.insert(v1)?;
let mut v2 = Document::new(); v2.set("v", 2_i64);
assert!(db.update(id, v2)?); // overwrote
assert_eq!(db.get(id)?.unwrap().get("v").and_then(|v| v.as_int()), Some(2));
assert!(!db.update(DocId::from(404), Document::new())?); // nothing to update
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::delete`
```rust,ignore
pub fn delete(&mut self, id: DocId) -> Result<bool>
```
Deletes the document under `id`, returning `Ok(true)` if one was present and
`Ok(false)` otherwise. A tombstone is appended so the deletion survives
reopening; the document is unreadable as soon as this returns.
- **`id`** — the document to remove.
**Errors:** [`Error::Io`] if the tombstone cannot be appended.
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_delete.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let id = db.insert(Document::new())?;
assert!(db.delete(id)?); // removed
assert!(db.get(id)?.is_none());
assert!(!db.delete(id)?); // already gone
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### Other `Db` methods
| `contains` | `fn contains(&self, id: DocId) -> bool` | `true` if a live document has this id (in-memory lookup, no file access). |
| `len` | `fn len(&self) -> usize` | Number of live documents. |
| `is_empty` | `fn is_empty(&self) -> bool` | `true` if the store holds no live documents. |
| `ids` | `fn ids(&self) -> impl Iterator<Item = DocId>` | Iterator over all live document ids. Order is unspecified. |
| `flush` | `fn flush(&mut self) -> Result<()>` | `fsync`s the file, making preceding writes durable against power loss. |
| `path` | `fn path(&self) -> &Path` | The path the store was opened from. |
| `stats` | `fn stats(&self) -> Stats` | A [`Stats`](#stats) snapshot of size and live contents. |
| `open_with` | `fn open_with<P>(path: P, options: DbOptions) -> Result<Db>` | Open with a [`DbOptions`](#dboptions) configuration (e.g. a durability policy). |
| `sync_policy` | `fn sync_policy(&self) -> SyncPolicy` | The store's current [`SyncPolicy`](#dboptions). |
| `compact` | `fn compact(&mut self) -> Result<()>` | Rewrite the file with one record per live document, reclaiming space; see below. |
#### `Db::compact`
```rust,ignore
pub fn compact(&mut self) -> Result<()>
```
Rewrites the file to hold only live documents, reclaiming the space left by
overwrites and deletes. The store is append-only, so every update and delete
leaves a dead record behind; over time the file grows past the size of its live
data.
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`](#dbopen). Document
ids are preserved, so existing [`DocId`]s and secondary indexes stay valid; only
the on-disk layout changes.
**Errors:** [`Error::Io`] if the temporary cannot be written or swapped in;
[`Error::Corrupt`] if a live record cannot be read back.
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_compact.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", 1_i64); d })?;
for n in 2..1_000 {
db.update(id, { let mut d = Document::new(); d.set("v", n); d })?;
}
let before = db.stats().file_bytes;
db.compact()?; // 999 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(999));
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_misc.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let id = db.insert(Document::new())?;
db.insert(Document::new())?;
assert!(db.contains(id));
assert_eq!(db.len(), 2);
assert!(!db.is_empty());
let mut all: Vec<u64> = db.ids().map(|i| i.get()).collect();
all.sort();
assert_eq!(all, vec![1, 2]);
db.flush()?;
assert_eq!(db.path(), path.as_path());
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
<br>
<a id="secondary-indexes-and-queries"></a>
### Secondary indexes and queries
`find` and `range` answer queries over document fields. They work whether or not
a field is indexed: with an index they are a B-tree lookup, without one they fall
back to scanning every live document. So an index never changes a result — only
its speed. You may index **any number of fields**; call `create_index` once per
field. Indexes live in memory and are rebuilt per session (they are not in the
file), so re-declare them after reopening a store.
Both query methods compare values with a single total order over [`Value`]
(`null < bool < int < float < string < bytes < array < object`, then natural
order within a variant, floats via `f64::total_cmp`). One consequence: integers
and floats sort in separate bands, so index a numeric field with a consistent
type.
#### `Db::create_index`
```rust,ignore
pub fn create_index(&mut self, field: &str) -> Result<()>
```
Builds an ordered index over `field` by reading every live document once;
documents without the field are skipped. The index is then maintained
automatically on every insert, update, and delete. Idempotent — indexing an
already-indexed field is a no-op.
- **`field`** — the field name to index.
**Errors:** [`Error::Io`] / [`Error::Corrupt`] if a document cannot be read while
building the index.
#### `Db::find`
```rust,ignore
pub fn find(&self, field: &str, value: &Value) -> Result<Vec<DocId>>
```
Returns the ids of all live documents whose `field` equals `value`.
- **`field`** — the field to match on.
- **`value`** — the exact value to match.
**Errors:** [`Error::Io`] / [`Error::Corrupt`] on the unindexed (scan) path if a
document cannot be read.
```rust
use bison_db::{Db, Document, Value};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_find2.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
db.insert({ let mut d = Document::new(); d.set("role", "admin"); d })?;
db.insert({ let mut d = Document::new(); d.set("role", "user"); d })?;
// Works before indexing (full scan)…
assert_eq!(db.find("role", &Value::from("admin"))?.len(), 1);
// …and faster after (point lookup), with the same result.
db.create_index("role")?;
assert_eq!(db.find("role", &Value::from("admin"))?.len(), 1);
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::range`
```rust,ignore
pub fn range<R: RangeBounds<Value>>(&self, field: &str, range: R) -> Result<Vec<DocId>>
```
Returns the ids of all live documents whose `field` falls within `range`. Any
[`RangeBounds`](https://doc.rust-lang.org/std/ops/trait.RangeBounds.html) form
works: `a..b`, `a..=b`, `..b`, `a..`, `..`. When the field is indexed, matches
come back ordered by field value (then id).
- **`field`** — the field to range over.
- **`range`** — inclusive/exclusive bounds as [`Value`]s.
**Errors:** as `find`.
```rust
use bison_db::{Db, Document, Value};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_range2.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
for age in [17_i64, 25, 40, 70] {
db.insert({ let mut d = Document::new(); d.set("age", age); d })?;
}
db.create_index("age")?;
let working_age = db.range("age", Value::from(18_i64)..=Value::from(65_i64))?;
assert_eq!(working_age.len(), 2); // 25 and 40
let over_60 = db.range("age", Value::from(60_i64)..)?;
assert_eq!(over_60.len(), 1); // 70
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
#### `Db::drop_index` and `Db::indexes`
| `drop_index` | `fn drop_index(&mut self, field: &str) -> bool` | Removes a field's index; `true` if one existed. |
| `indexes` | `fn indexes(&self) -> impl Iterator<Item = &str>` | Names of the currently indexed fields (order unspecified). |
```rust
use bison_db::Db;
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_dropidx.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
db.create_index("a")?;
db.create_index("b")?;
assert_eq!(db.indexes().count(), 2);
assert!(db.drop_index("a"));
assert!(!db.drop_index("a"));
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
<br>
<a id="dboptions"></a>
### `DbOptions` and `SyncPolicy`
Open a store with a non-default configuration — currently, a durability policy.
```rust,ignore
pub enum SyncPolicy { Always, Manual } // Manual is the default
pub struct DbOptions { /* private */ }
impl DbOptions {
pub fn new() -> Self;
pub fn sync(self, policy: SyncPolicy) -> Self;
pub fn build_sync_policy(&self) -> SyncPolicy;
pub fn open<P: AsRef<Path>>(self, path: P) -> Result<Db>;
}
```
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 physical device with `fsync`:
| `SyncPolicy::Always` | after every write, before it returns | each write durable immediately; one device sync per op |
| `SyncPolicy::Manual` *(default)* | on [`Db::flush`], and best-effort on drop | fastest; recent unsynced writes can be lost on power loss |
Under **both** policies the file is never left corrupt: a torn write is detected
and truncated on the next open (see [Durability and recovery](#durability-and-recovery)).
Open with options through [`DbOptions::open`] or [`Db::open_with`]; read the
active policy with [`Db::sync_policy`].
```rust
use bison_db::{Db, DbOptions, Document, SyncPolicy};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_durability.bison");
# let _ = std::fs::remove_file(&path);
// Durable per write.
let mut db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always))?;
assert_eq!(db.sync_policy(), SyncPolicy::Always);
db.insert({ let mut d = Document::new(); d.set("entry", "debit 100"); d })?;
// No flush needed under Always — each write already fsynced.
# drop(db);
// The default is Manual, where you control when the sync happens.
let mut fast = Db::open(&path)?;
assert_eq!(fast.sync_policy(), SyncPolicy::Manual);
fast.insert({ let mut d = Document::new(); d.set("entry", "credit 50"); d })?;
fast.flush()?; // durable now
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
<br>
### `DocId`
A document's primary key within a [`Db`].
```rust,ignore
pub struct DocId(/* private */);
```
Ids are assigned by [`Db::insert`] as a dense, monotonically increasing sequence
starting at `1`; `0` is never assigned and can serve as a sentinel. An id is
stable for the life of the document and survives reopening the file. `DocId` is
`Copy`, `Ord`, and `Hash`, so it works as a map key or in a sorted collection.
| `get` | `const fn get(self) -> u64` | The underlying integer. |
| `From<u64>` | `DocId::from(raw)` | Reconstruct an id you stored elsewhere. |
| `From<DocId> for u64` | `u64::from(id)` / `id.into()` | Extract the integer. |
| `Display` | `id.to_string()` | Renders the integer. |
```rust
use bison_db::DocId;
let id = DocId::from(7);
assert_eq!(id.get(), 7);
assert_eq!(u64::from(id), 7);
assert_eq!(id.to_string(), "7");
```
<br>
### `Stats`
A point-in-time summary returned by [`Db::stats`].
```rust,ignore
pub struct Stats {
pub live_documents: usize, // documents currently readable
pub file_bytes: u64, // total size of the file on disk
pub live_bytes: u64, // bytes held by live document bodies (no framing)
}
```
The gap between `file_bytes` and `live_bytes` (plus per-record framing) is space
held by superseded and deleted records — the slack a future compaction step will
reclaim.
```rust
use bison_db::{Db, Document};
fn main() -> bison_db::Result<()> {
# let path = std::env::temp_dir().join("bison_db_api_stats.bison");
# let _ = std::fs::remove_file(&path);
let mut db = Db::open(&path)?;
let id = db.insert(Document::new())?;
db.insert(Document::new())?;
db.delete(id)?;
let s = db.stats();
assert_eq!(s.live_documents, 1); // two inserted, one deleted
assert!(s.file_bytes > 0);
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
<br>
### `Document`
An ordered collection of named fields — the record a store holds. Lookups are a
linear scan, which is the fastest strategy for the small field counts typical of
documents.
| `new` | `const fn new() -> Document` | An empty document. |
| `with_capacity` | `fn with_capacity(n: usize) -> Document` | Empty, with room for `n` fields before reallocating. |
| `set` | `fn set<K: Into<String>, V: Into<Value>>(&mut self, k, v) -> &mut Self` | Sets a field, replacing in place if the key exists; returns `&mut self` for chaining. |
| `get` | `fn get(&self, key: &str) -> Option<&Value>` | The value for `key`, if present. |
| `get_mut` | `fn get_mut(&mut self, key: &str) -> Option<&mut Value>` | Mutable access to a field's value. |
| `contains_key` | `fn contains_key(&self, key: &str) -> bool` | Whether `key` is present. |
| `remove` | `fn remove(&mut self, key: &str) -> Option<Value>` | Removes and returns a field; order of the rest is kept. |
| `len` / `is_empty` | `fn len(&self) -> usize` / `fn is_empty(&self) -> bool` | Field count / emptiness. |
| `clear` | `fn clear(&mut self)` | Removes all fields, keeping capacity. |
| `iter` | `fn iter(&self) -> impl Iterator<Item = (&str, &Value)>` | Fields in order. |
| `keys` / `values` | iterators | Field keys / values in order. |
`Document` also implements `FromIterator<(K, V)>` and `IntoIterator` for
`&Document`.
**Building and reading:**
```rust
use bison_db::{Document, Value};
let mut doc = Document::new();
doc.set("a", 1_i64).set("b", "two").set("a", 3_i64); // "a" replaced in place
assert_eq!(doc.keys().collect::<Vec<_>>(), ["a", "b"]);
assert_eq!(doc.get("a").and_then(Value::as_int), Some(3));
assert!(doc.contains_key("b"));
assert_eq!(doc.len(), 2);
```
**Mutating a field in place:**
```rust
use bison_db::{Document, Value};
let mut doc = Document::new();
doc.set("count", 41_i64);
if let Some(Value::Int(n)) = doc.get_mut("count") {
*n += 1;
}
assert_eq!(doc.get("count").and_then(Value::as_int), Some(42));
```
**From an iterator of pairs:**
```rust
use bison_db::Document;
let doc: Document = [("x", 1_i64), ("y", 2_i64)].into_iter().collect();
assert_eq!(doc.len(), 2);
```
<br>
### `Value`
The content of a document field.
```rust,ignore
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Bytes(Vec<u8>),
Array(Vec<Value>),
Object(Document),
}
```
**Construction.** `Value` implements `From` for the common Rust types, so
`Document::set` accepts them directly:
| `bool` | `Value::Bool` |
| `i32`, `i64`, `u32` | `Value::Int` |
| `f64` | `Value::Float` |
| `&str`, `String` | `Value::Str` |
| `Vec<u8>` | `Value::Bytes` |
| `Vec<Value>` | `Value::Array` |
| `Document` | `Value::Object` |
| `Option<T>` where `T: Into<Value>` | the inner value, or `Value::Null` for `None` |
**Inspection.** Each accessor returns `Some` only for the matching variant; no
coercion is performed (an `Int` is not read as a `Float`).
| `is_null` | `bool` |
| `as_bool` | `Option<bool>` |
| `as_int` | `Option<i64>` |
| `as_float` | `Option<f64>` |
| `as_str` | `Option<&str>` |
| `as_bytes` | `Option<&[u8]>` |
| `as_array` | `Option<&[Value]>` |
| `as_object` | `Option<&Document>` |
| `type_name` | `&'static str` (for diagnostics) |
```rust
use bison_db::Value;
assert_eq!(Value::from("bison").as_str(), Some("bison"));
assert_eq!(Value::from(42_i64).as_int(), Some(42));
assert_eq!(Value::from(42_i64).as_float(), None); // no coercion
assert!(Value::Null.is_null());
assert_eq!(Value::from(1_i64).type_name(), "int");
let from_none = Value::from(None::<i64>);
assert!(from_none.is_null());
```
**Nested values:**
```rust
use bison_db::{Document, Value};
let mut address = Document::new();
address.set("city", "Kyoto");
let mut user = Document::new();
user.set("name", "tomoko")
.set("tags", Value::Array(vec![Value::from("a"), Value::from("b")]))
.set("address", Value::Object(address));
let city = user.get("address")
.and_then(Value::as_object)
.and_then(|a| a.get("city"))
.and_then(Value::as_str);
assert_eq!(city, Some("Kyoto"));
```
**With `serde`** (feature `serde`), both `Value` and `Document` implement
`Serialize`/`Deserialize`, mapping onto the serde data model like a dynamic JSON
value:
```rust,ignore
let doc: bison_db::Document = serde_json::from_str(r#"{ "n": 1, "ok": true }"#)?;
let json = serde_json::to_string(&doc)?;
```
<br>
<a id="error"></a>
### `Error` and `Result`
```rust,ignore
pub type Result<T> = core::result::Result<T, Error>;
#[non_exhaustive]
pub enum Error {
Io(std::io::Error), // (std feature) an underlying filesystem op failed
BadMagic, // the file is not a bison-db store
UnsupportedVersion(u16), // the file's format is newer than this build
Corrupt(&'static str), // a record failed its checksum or structure check
ValueTooLarge, // a value exceeded MAX_RECORD_BYTES
}
```
`Error` is `#[non_exhaustive]`; always include a catch-all arm when matching.
`Error::Io` carries the originating `std::io::Error`, reachable through the
standard `source()` chain. A `Corrupt` error always means the bytes on disk did
not match what the writer produced — never that an in-memory argument was wrong.
A clean torn write at the very end of the log is recovered silently, not
reported as `Corrupt` (see [Durability and recovery](#durability-and-recovery)).
```rust
use bison_db::Error;
let err = Error::UnsupportedVersion(9);
assert!(err.to_string().contains('9'));
```
<br>
<a id="max-record-bytes"></a>
### `MAX_RECORD_BYTES`
```rust,ignore
pub const MAX_RECORD_BYTES: usize = 64 * 1024 * 1024; // 64 MiB
```
The largest record payload the store will write or accept while reading. A
document encoding to more than this is rejected on write with
[`Error::ValueTooLarge`]; on read, any framed length above the cap is treated as
corruption, which bounds the allocation the recovery path can be asked to make
from a damaged file. Requires the `std` feature.
```rust
assert_eq!(bison_db::MAX_RECORD_BYTES, 64 * 1024 * 1024);
```
---
## Concurrency
`Db` follows a **single-writer, multi-reader** model, like an embedded SQL
engine. Reads ([`get`](#dbget), [`find`](#dbfind), [`range`](#dbrange),
`contains`, `len`, …) take `&self`; writes ([`insert`](#dbinsert),
[`update`](#dbupdate), [`delete`](#dbdelete), `compact`, …) take `&mut self`. The
compiler therefore guarantees writes are exclusive.
`Db` is [`Send`] and [`Sync`], so share one across threads with an
[`Arc`]`<`[`RwLock`]`<Db>>`: many threads read concurrently, a writer takes the
lock exclusively. The single-writer design is inherent to one append-only file —
there is a single tail — and is the right fit for an embedded store.
```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_api_concurrency.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 handle = std::thread::spawn(move || reader.read().unwrap().len());
assert_eq!(handle.join().unwrap(), 1);
# let _ = std::fs::remove_file(&path);
Ok(())
}
```
[`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html
[`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html
[`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html
[`RwLock`]: https://doc.rust-lang.org/std/sync/struct.RwLock.html
---
## Durability and recovery
- **Visibility.** A write is visible to later reads in the same process as soon
as the call returns.
- **Durability.** When a write reaches the physical device depends on the
store's [`SyncPolicy`](#dboptions): `Always` `fsync`s after every write;
`Manual` (default) `fsync`s on [`Db::flush`] and, best-effort, on drop. A
freshly created file also has its parent directory `fsync`ed so the file's
existence is durable.
- **No corruption.** A crash never tears a record that was already durable.
Every record is length-framed and CRC-32C checked. On open, the log is
replayed: a partial record at the tail (short read or failing checksum at the
end of the file) is truncated; a checksum failure earlier in the file is
surfaced as [`Error::Corrupt`] rather than silently misread.
- **Frozen, versioned format.** The on-disk layout is **stable as of v0.4.0**
(format version 1) and specified in [`docs/FORMAT.md`](./FORMAT.md). Files
written by 0.2.0 onward stay readable; a file written by a newer format is
refused with [`Error::UnsupportedVersion`] rather than misinterpreted.
---
## Feature flags
| `std` | yes | Enables the file-backed [`Db`], [`DocId`], [`Stats`], and [`MAX_RECORD_BYTES`]. Without it, the crate is `no_std` (with `alloc`) and exposes only the document model. |
| `serde` | no | Derives `serde::Serialize`/`Deserialize` for [`Value`] and [`Document`]. |
---
## Roadmap surface
The public API is **frozen** as of v0.5.0: additive changes only until 1.0, no
breaking change before then. The items below are **not yet implemented** and are
listed so integrators can see the intended direction; tracked in
[`dev/ROADMAP.md`](../dev/ROADMAP.md).
- **RC hardening toward 1.0** _(v0.8.0 → 1.0)_ — a controlled head-to-head
benchmark against peer engines and real-consumer integration. (v0.6.0 added
in-tree fuzzing of the parse/recovery paths and captured baseline benchmarks;
v0.7.0 added a randomized soak test — see [`docs/PERFORMANCE.md`](./PERFORMANCE.md).)
- **Persistent / lazily-rebuilt indexes** _(post-1.0)_ — avoid re-declaring
indexes after reopening a store, via a sidecar file that does not change the
frozen main format.
---
<sub>Copyright © 2026 <strong>James Gober</strong>.</sub>