pub struct Db { /* private fields */ }std only.Expand description
An embedded document store backed by a single append-only file.
Open one with Db::open, then insert,
get, update, and delete
documents by id. Call flush to make recent writes durable, and
compact to reclaim space left by overwrites and deletes.
§Concurrency
Db follows a single-writer, multi-reader model, like an embedded SQL
engine: reads (get, find, range)
take &self, while writes take &mut self. The compiler therefore enforces
that writes are exclusive. Db is Send and Sync, so the idiomatic
way to share one across threads is an Arc<RwLock<Db>>:
many threads can read concurrently, and a writer takes the lock exclusively.
The single-writer design is inherent to a single append-only file — there is
one tail — and is the right fit for an embedded store.
§Examples
use bison_db::{Db, Document};
let dir = std::env::temp_dir().join("bison_db_doc_example");
let _ = std::fs::remove_file(&dir);
let mut db = Db::open(&dir)?;
let mut user = Document::new();
user.set("name", "grace").set("born", 1906_i64);
let id = db.insert(user)?;
let fetched = db.get(id)?.expect("just inserted");
assert_eq!(fetched.get("name").and_then(|v| v.as_str()), Some("grace"));
db.flush()?;Implementations§
Source§impl Db
impl Db
Sourcepub fn open<P: AsRef<Path>>(path: P) -> Result<Self>
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self>
Opens the store at path, creating an empty one if the file does not
exist, and replaying any existing records to rebuild the index.
On open the whole log is scanned: each record’s checksum is verified and
the in-memory index is reconstructed 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, restoring the file to its last consistent state. A
checksum failure on a record that is not at the tail is reported as
Error::Corrupt, because that indicates in-place damage rather than a
torn write.
Uses SyncPolicy::Manual; for a different policy, open through
DbOptions.
§Errors
Returns Error::Io if the file cannot be opened or read,
Error::BadMagic if an existing file is not a bison-db store,
Error::UnsupportedVersion if it was written by a newer format, and
Error::Corrupt if a non-tail record fails verification.
§Examples
let path = std::env::temp_dir().join("bison_db_open_example.bison");
let _ = std::fs::remove_file(&path);
let db = bison_db::Db::open(&path)?;
assert!(db.is_empty());Sourcepub fn open_with<P: AsRef<Path>>(path: P, options: DbOptions) -> Result<Self>
pub fn open_with<P: AsRef<Path>>(path: P, options: DbOptions) -> Result<Self>
Opens (or creates) the store at path with the given DbOptions.
A shorthand for DbOptions::open; see Db::open for the open and
recovery contract.
§Errors
Same as Db::open.
§Examples
use bison_db::{Db, DbOptions, SyncPolicy};
let db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always))?;
assert_eq!(db.sync_policy(), SyncPolicy::Always);Sourcepub fn sync_policy(&self) -> SyncPolicy
pub fn sync_policy(&self) -> SyncPolicy
Returns the store’s SyncPolicy.
§Examples
use bison_db::{Db, SyncPolicy};
let db = Db::open(&path)?;
assert_eq!(db.sync_policy(), SyncPolicy::Manual);Sourcepub fn insert(&mut self, doc: Document) -> Result<DocId>
pub fn insert(&mut self, doc: Document) -> Result<DocId>
Inserts doc, assigning and returning a fresh DocId.
The document is appended to the log and indexed; it is readable
immediately and durable after the next flush.
§Errors
Returns Error::ValueTooLarge if the encoded document exceeds
MAX_RECORD_BYTES, or Error::Io if the append fails.
§Examples
use bison_db::{Db, Document};
let mut db = Db::open(&path)?;
let mut doc = Document::new();
doc.set("k", "v");
let id = db.insert(doc)?;
assert!(db.contains(id));Sourcepub fn get(&self, id: DocId) -> Result<Option<Document>>
pub fn get(&self, id: DocId) -> Result<Option<Document>>
Reads the document stored under id, or None if no live document has
that id.
§Errors
Returns Error::Io if the body cannot be read, or Error::Corrupt
if the stored bytes fail to decode (which a passing checksum makes
unexpected in practice).
§Examples
use bison_db::{Db, Document, DocId};
let mut db = Db::open(&path)?;
let id = db.insert({ let mut d = Document::new(); d.set("n", 1_i64); d })?;
assert!(db.get(id)?.is_some());
assert!(db.get(DocId::from(9999))?.is_none());Sourcepub fn update(&mut self, id: DocId, doc: Document) -> Result<bool>
pub fn update(&mut self, id: DocId, doc: Document) -> Result<bool>
Overwrites the document stored under id with doc, returning true if
a document was present to overwrite and false otherwise.
A successful update appends a new record and repoints the index; the previous body remains in the file as dead space until compaction.
§Errors
Returns Error::ValueTooLarge or Error::Io under the same
conditions as insert.
§Examples
use bison_db::{Db, Document, DocId};
let mut db = Db::open(&path)?;
let id = db.insert({ let mut d = Document::new(); d.set("v", 1_i64); d })?;
let mut next = Document::new();
next.set("v", 2_i64);
assert!(db.update(id, next)?);
assert!(!db.update(DocId::from(404), Document::new())?);Sourcepub fn delete(&mut self, id: DocId) -> Result<bool>
pub fn delete(&mut self, id: DocId) -> Result<bool>
Deletes the document stored under id, returning true if one was
present and false otherwise.
A tombstone is appended so the deletion survives reopening; the document is unreadable as soon as this returns.
§Errors
Returns Error::Io if the tombstone cannot be appended.
§Examples
use bison_db::{Db, Document};
let mut db = Db::open(&path)?;
let id = db.insert({ let mut d = Document::new(); d.set("x", 1_i64); d })?;
assert!(db.delete(id)?);
assert!(db.get(id)?.is_none());
assert!(!db.delete(id)?);Sourcepub fn contains(&self, id: DocId) -> bool
pub fn contains(&self, id: DocId) -> bool
Returns true if a live document has this id.
This is an in-memory index lookup with no file access.
§Examples
use bison_db::{Db, Document};
let mut db = Db::open(&path)?;
let id = db.insert(Document::new())?;
assert!(db.contains(id));Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
Returns the number of live documents.
§Examples
use bison_db::{Db, Document};
let mut db = Db::open(&path)?;
db.insert(Document::new())?;
assert_eq!(db.len(), 1);Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Returns true if the store holds no live documents.
§Examples
let db = bison_db::Db::open(&path)?;
assert!(db.is_empty());Sourcepub fn ids(&self) -> impl Iterator<Item = DocId> + '_
pub fn ids(&self) -> impl Iterator<Item = DocId> + '_
Returns an iterator over the ids of all live documents.
The order is unspecified and may change between runs; collect and sort if you need a stable order.
§Examples
use bison_db::{Db, Document};
let mut db = Db::open(&path)?;
db.insert(Document::new())?;
db.insert(Document::new())?;
assert_eq!(db.ids().count(), 2);Sourcepub fn path(&self) -> &Path
pub fn path(&self) -> &Path
Returns the path the store was opened from.
§Examples
let db = bison_db::Db::open(&path)?;
assert_eq!(db.path(), path.as_path());Sourcepub fn compact(&mut self) -> Result<()>
pub fn compact(&mut self) -> Result<()>
Rewrites the file to contain only live documents, reclaiming the space held by superseded and deleted records.
Every overwrite and delete leaves dead bytes behind in the append-only
log; over time the file grows past the size of its live data. Compaction
writes a fresh copy containing one current record per live document, then
atomically swaps it in. Document ids are preserved, so existing
DocIds and secondary indexes remain valid; only the on-disk layout
changes.
The compacted copy is built in a sibling temporary file and put in place
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 cleaned up on the next
open.
Compaction is durable on return regardless of SyncPolicy: the new file
is fsynced before the swap.
§Errors
Returns Error::Io if the temporary file cannot be written or swapped
in, or Error::Corrupt if a live record cannot be read back.
§Examples
use bison_db::{Db, Document};
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..1000 {
db.update(id, { let mut d = Document::new(); d.set("v", n); d })?;
}
let before = db.stats().file_bytes;
db.compact()?; // collapse 999 versions down to one live record
assert!(db.stats().file_bytes < before);
assert_eq!(db.get(id)?.unwrap().get("v").and_then(|v| v.as_int()), Some(999));Sourcepub fn create_index(&mut self, field: &str) -> Result<()>
pub fn create_index(&mut self, field: &str) -> Result<()>
Builds a secondary index over field, making find and
range on that field fast point and range lookups instead of
full scans.
The index is built by reading every live document once and recording its
value for field; documents without the field are skipped. From then on,
it is maintained automatically on every insert, update, and delete. Any
number of fields may be indexed — call this once per field.
Indexes live in memory only and are not persisted: after reopening a store, call this again for each field you want indexed. Calling it for a field that is already indexed is a no-op.
§Errors
Returns Error::Io or Error::Corrupt if a document cannot be read
while building the index.
§Examples
use bison_db::{Db, Document, Value};
let mut db = Db::open(&path)?;
db.insert({ let mut d = Document::new(); d.set("city", "Oslo"); d })?;
db.create_index("city")?;
let hits = db.find("city", &Value::from("Oslo"))?;
assert_eq!(hits.len(), 1);Sourcepub fn drop_index(&mut self, field: &str) -> bool
pub fn drop_index(&mut self, field: &str) -> bool
Drops the secondary index over field, returning true if one existed.
§Examples
let mut db = bison_db::Db::open(&path)?;
db.create_index("name")?;
assert!(db.drop_index("name"));
assert!(!db.drop_index("name"));Sourcepub fn indexes(&self) -> impl Iterator<Item = &str>
pub fn indexes(&self) -> impl Iterator<Item = &str>
Returns an iterator over the names of the currently indexed fields.
The order is unspecified.
§Examples
let mut db = bison_db::Db::open(&path)?;
db.create_index("a")?;
db.create_index("b")?;
assert_eq!(db.indexes().count(), 2);Sourcepub fn find(&self, field: &str, value: &Value) -> Result<Vec<DocId>>
pub fn find(&self, field: &str, value: &Value) -> Result<Vec<DocId>>
Returns the ids of all live documents whose field equals value.
If field is indexed (see create_index) this is a
point lookup; otherwise it falls back to scanning every live document, so
the result is correct either way — the index only changes the speed.
Equality follows the same total order the indexes use, so a Float field
distinguishes 0.0 from -0.0.
§Errors
Returns Error::Io or Error::Corrupt if a document must be read
(the unindexed path) and cannot be.
§Examples
use bison_db::{Db, Document, Value};
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 })?;
db.create_index("role")?;
assert_eq!(db.find("role", &Value::from("admin"))?.len(), 1);
assert!(db.find("role", &Value::from("ghost"))?.is_empty());Sourcepub fn range<R: RangeBounds<Value>>(
&self,
field: &str,
range: R,
) -> Result<Vec<DocId>>
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.
Bounds are Values compared with the same total order the indexes use;
any RangeBounds form works (a..b, a..=b, ..b, a.., ..).
If field is indexed the matches come back ordered by field value (then
id); otherwise the store scans every live document. As with
find, the index changes only the speed, not the result.
§Errors
Returns Error::Io or Error::Corrupt if a document must be read
(the unindexed path) and cannot be.
§Examples
use bison_db::{Db, Document, Value};
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")?;
// Working-age adults: 18..=65.
let hits = db.range("age", Value::from(18_i64)..=Value::from(65_i64))?;
assert_eq!(hits.len(), 2); // 25 and 40Trait Implementations§
Source§impl Drop for Db
impl Drop for Db
Source§fn drop(&mut self)
fn drop(&mut self)
Makes a best-effort fsync on a clean shutdown under
SyncPolicy::Manual, so a normal program exit does not lose writes that
were never explicitly flushed. Under SyncPolicy::Always every write is
already durable, so nothing is done. Any error here is ignored because a
destructor cannot return one; call Db::flush before dropping when you
need to observe a sync failure.