Skip to main content

bison_db/
store.rs

1//! The single-file document store: [`Db`].
2//!
3//! `bison-db` persists documents to one append-only file. Every write — insert,
4//! overwrite, or delete — appends a self-describing record to the tail; the file
5//! is never edited in place. An in-memory index maps each live document id to
6//! the byte offset of its most recent record, so a read is one hash lookup and
7//! one positional read. This log-structured design makes writes sequential
8//! (the pattern disks and SSDs serve fastest) and keeps a crash from corrupting
9//! data already on disk: a half-written record at the tail is detected by its
10//! length and checksum and dropped on the next open.
11//!
12//! ## Record framing
13//!
14//! The file opens with a fixed header (magic plus a format version), then a run
15//! of records. Each record is an 8-byte frame (`u32` payload length, `u32`
16//! CRC-32C of the payload) followed by the payload itself: a one-byte operation
17//! tag, the 8-byte document id, and — for an insert or overwrite — the encoded
18//! document body. A delete writes a tombstone with no body.
19//!
20//! ## Durability
21//!
22//! A record reaches the OS page cache as soon as it is written, so it is visible
23//! to later reads in the same process immediately. When it becomes durable
24//! against a power loss is governed by the store's [`SyncPolicy`]:
25//!
26//! - [`SyncPolicy::Always`] forces an `fsync` after every write, so each
27//!   operation is durable the moment it returns.
28//! - [`SyncPolicy::Manual`] (the default) syncs only on [`Db::flush`] and once,
29//!   best-effort, on drop. It is faster, and writes remain crash-*safe* — a torn
30//!   write is never misread — but the most recent unsynced writes can be lost on
31//!   power loss.
32//!
33//! Either way the on-disk invariant holds: a crash never tears a record that was
34//! already durable. On a newly created file, the parent directory is `fsync`ed
35//! so the file's existence is itself durable.
36
37use std::collections::HashMap;
38use std::fmt;
39use std::fs::{File, OpenOptions};
40use std::ops::RangeBounds;
41use std::path::{Path, PathBuf};
42
43use crate::codec::{crc32c, decode_document, encode_document_into};
44use crate::error::{Error, Result};
45use crate::index::{SecondaryIndex, in_bounds, total_cmp_value};
46use crate::sys::{read_exact_at, write_all_at};
47use crate::value::{Document, Value};
48
49/// The largest record payload the store will write or accept while reading.
50///
51/// A document encodes to at most this many bytes; a larger one is rejected with
52/// [`Error::ValueTooLarge`] on write. On read, any framed length above this cap
53/// is treated as corruption, which bounds the allocation the recovery path can
54/// be asked to make from a damaged file.
55pub const MAX_RECORD_BYTES: usize = 64 * 1024 * 1024;
56
57/// Magic bytes at the start of every store file. The trailing digit tracks the
58/// header layout, distinct from the format version that follows it.
59const HEADER_MAGIC: [u8; 8] = *b"BISONDB1";
60
61/// On-disk format version. Frozen at `1` as of v0.4.0: the layout described in
62/// `docs/FORMAT.md` is stable, and files written by 0.2.0 onward are readable by
63/// every later release. Bumped only on an incompatible record-layout change,
64/// which would be a major-version event.
65const FORMAT_VERSION: u16 = 1;
66
67/// Length of the file header: 8 magic bytes, a `u16` version, 6 reserved bytes.
68const HEADER_LEN: u64 = 16;
69
70/// Size of a record frame: a `u32` length followed by a `u32` checksum.
71const FRAME_LEN: usize = 8;
72
73/// Smallest legal payload: a one-byte op tag plus an 8-byte id, with no body
74/// (the shape of a delete tombstone).
75const MIN_PAYLOAD: usize = 1 + 8;
76
77/// Operation tag for an insert or overwrite: the payload carries a document body.
78const OP_PUT: u8 = 1;
79
80/// Operation tag for a delete: the payload is the op tag and id only.
81const OP_DELETE: u8 = 2;
82
83/// A document's primary key within a [`Db`].
84///
85/// Ids are assigned by [`Db::insert`] as a dense, monotonically increasing
86/// sequence starting at 1; `0` is never assigned and can be used as a sentinel.
87/// The id is stable for the life of the document and survives reopening the
88/// file. Reconstruct one with [`DocId::from`] when you have stored it elsewhere.
89///
90/// # Examples
91///
92/// ```
93/// use bison_db::DocId;
94/// let id = DocId::from(7);
95/// assert_eq!(id.get(), 7);
96/// assert_eq!(id.to_string(), "7");
97/// ```
98#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub struct DocId(u64);
100
101impl DocId {
102    /// Returns the underlying `u64`.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use bison_db::DocId;
108    /// assert_eq!(DocId::from(42).get(), 42);
109    /// ```
110    #[inline]
111    #[must_use]
112    pub const fn get(self) -> u64 {
113        self.0
114    }
115}
116
117impl From<u64> for DocId {
118    #[inline]
119    fn from(raw: u64) -> Self {
120        DocId(raw)
121    }
122}
123
124impl From<DocId> for u64 {
125    #[inline]
126    fn from(id: DocId) -> Self {
127        id.0
128    }
129}
130
131impl fmt::Display for DocId {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}", self.0)
134    }
135}
136
137/// Where a live document's body sits in the file.
138#[derive(Clone, Copy)]
139struct BodyLoc {
140    /// Byte offset of the encoded document body.
141    offset: u64,
142    /// Length of the encoded document body in bytes.
143    len: u32,
144}
145
146/// A point-in-time summary of a store's size and contents.
147///
148/// Returned by [`Db::stats`]. The gap between `file_bytes` and `live_bytes`
149/// (plus framing) is space held by superseded and deleted records — the slack a
150/// future compaction step will reclaim.
151///
152/// # Examples
153///
154/// ```no_run
155/// # fn main() -> bison_db::Result<()> {
156/// let db = bison_db::Db::open("data.bison")?;
157/// let stats = db.stats();
158/// println!("{} live documents in {} bytes", stats.live_documents, stats.file_bytes);
159/// # Ok(())
160/// # }
161/// ```
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct Stats {
164    /// Number of documents currently readable.
165    pub live_documents: usize,
166    /// Total size of the file on disk, in bytes.
167    pub file_bytes: u64,
168    /// Bytes occupied by the bodies of live documents, excluding framing.
169    pub live_bytes: u64,
170}
171
172/// When a write is made durable on disk.
173///
174/// bison-db never holds writes in a userspace buffer — every write reaches the
175/// operating system immediately and is visible to later reads. This policy
176/// controls only when the store forces those bytes through the OS cache to the
177/// physical device with `fsync`, which is what protects them from a power loss.
178///
179/// # Examples
180///
181/// ```
182/// # fn main() -> bison_db::Result<()> {
183/// use bison_db::{DbOptions, SyncPolicy};
184/// # let path = std::env::temp_dir().join("bison_db_syncpolicy_doc.bison");
185/// # let _ = std::fs::remove_file(&path);
186/// // Durable per write, at the cost of an fsync on every insert/update/delete.
187/// let db = DbOptions::new().sync(SyncPolicy::Always).open(&path)?;
188/// # drop(db);
189/// # let _ = std::fs::remove_file(&path);
190/// # Ok(())
191/// # }
192/// ```
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
194pub enum SyncPolicy {
195    /// `fsync` after every write before it returns. Each insert, update, and
196    /// delete is durable the moment the call completes, at the cost of one
197    /// device sync per operation.
198    Always,
199    /// `fsync` only when [`Db::flush`] is called (and once, best-effort, when the
200    /// store is dropped). Writes are still crash-*safe* — a torn write is never
201    /// misread — but the most recent unsynced writes can be lost on power loss.
202    /// This is the default, and the fastest policy.
203    #[default]
204    Manual,
205}
206
207/// Options for opening a [`Db`], built fluently and finished with
208/// [`open`](DbOptions::open).
209///
210/// Use this when the default [`Db::open`] is not enough — currently, to choose a
211/// [`SyncPolicy`]. The set of options is intentionally small and will only grow
212/// additively.
213///
214/// # Examples
215///
216/// ```
217/// # fn main() -> bison_db::Result<()> {
218/// use bison_db::{DbOptions, SyncPolicy};
219/// # let path = std::env::temp_dir().join("bison_db_dboptions_doc.bison");
220/// # let _ = std::fs::remove_file(&path);
221/// let db = DbOptions::new().sync(SyncPolicy::Always).open(&path)?;
222/// assert_eq!(db.sync_policy(), SyncPolicy::Always);
223/// # drop(db);
224/// # let _ = std::fs::remove_file(&path);
225/// # Ok(())
226/// # }
227/// ```
228#[derive(Clone, Copy, Debug, Default)]
229pub struct DbOptions {
230    sync: SyncPolicy,
231}
232
233impl DbOptions {
234    /// Creates options with the defaults ([`SyncPolicy::Manual`]).
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use bison_db::{DbOptions, SyncPolicy};
240    /// assert_eq!(DbOptions::new().build_sync_policy(), SyncPolicy::Manual);
241    /// ```
242    #[must_use]
243    pub fn new() -> Self {
244        DbOptions::default()
245    }
246
247    /// Sets the [`SyncPolicy`] for the store.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use bison_db::{DbOptions, SyncPolicy};
253    /// let opts = DbOptions::new().sync(SyncPolicy::Always);
254    /// assert_eq!(opts.build_sync_policy(), SyncPolicy::Always);
255    /// ```
256    #[must_use]
257    pub fn sync(mut self, policy: SyncPolicy) -> Self {
258        self.sync = policy;
259        self
260    }
261
262    /// Returns the [`SyncPolicy`] these options currently carry.
263    ///
264    /// # Examples
265    ///
266    /// ```
267    /// use bison_db::{DbOptions, SyncPolicy};
268    /// assert_eq!(DbOptions::new().build_sync_policy(), SyncPolicy::Manual);
269    /// ```
270    #[must_use]
271    pub fn build_sync_policy(&self) -> SyncPolicy {
272        self.sync
273    }
274
275    /// Opens (or creates) the store at `path` with these options.
276    ///
277    /// Equivalent to [`Db::open`] when the options are the defaults.
278    ///
279    /// # Errors
280    ///
281    /// Same as [`Db::open`].
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// # fn main() -> bison_db::Result<()> {
287    /// use bison_db::{DbOptions, SyncPolicy};
288    /// # let path = std::env::temp_dir().join("bison_db_dboptions_open_doc.bison");
289    /// # let _ = std::fs::remove_file(&path);
290    /// let db = DbOptions::new().sync(SyncPolicy::Always).open(&path)?;
291    /// # drop(db);
292    /// # let _ = std::fs::remove_file(&path);
293    /// # Ok(())
294    /// # }
295    /// ```
296    pub fn open<P: AsRef<Path>>(self, path: P) -> Result<Db> {
297        Db::open_inner(path.as_ref().to_path_buf(), self.sync)
298    }
299}
300
301/// An embedded document store backed by a single append-only file.
302///
303/// Open one with [`Db::open`], then [`insert`](Db::insert),
304/// [`get`](Db::get), [`update`](Db::update), and [`delete`](Db::delete)
305/// documents by id. Call [`flush`](Db::flush) to make recent writes durable, and
306/// [`compact`](Db::compact) to reclaim space left by overwrites and deletes.
307///
308/// # Concurrency
309///
310/// `Db` follows a single-writer, multi-reader model, like an embedded SQL
311/// engine: reads ([`get`](Db::get), [`find`](Db::find), [`range`](Db::range))
312/// take `&self`, while writes take `&mut self`. The compiler therefore enforces
313/// that writes are exclusive. `Db` is [`Send`] and [`Sync`], so the idiomatic
314/// way to share one across threads is an [`Arc`](std::sync::Arc)`<`[`RwLock`](std::sync::RwLock)`<Db>>`:
315/// many threads can read concurrently, and a writer takes the lock exclusively.
316/// The single-writer design is inherent to a single append-only file — there is
317/// one tail — and is the right fit for an embedded store.
318///
319/// # Examples
320///
321/// ```
322/// # fn main() -> bison_db::Result<()> {
323/// use bison_db::{Db, Document};
324///
325/// let dir = std::env::temp_dir().join("bison_db_doc_example");
326/// let _ = std::fs::remove_file(&dir);
327/// let mut db = Db::open(&dir)?;
328///
329/// let mut user = Document::new();
330/// user.set("name", "grace").set("born", 1906_i64);
331/// let id = db.insert(user)?;
332///
333/// let fetched = db.get(id)?.expect("just inserted");
334/// assert_eq!(fetched.get("name").and_then(|v| v.as_str()), Some("grace"));
335///
336/// db.flush()?;
337/// # let _ = std::fs::remove_file(&dir);
338/// # Ok(())
339/// # }
340/// ```
341pub struct Db {
342    /// The open store file, used for both positional reads and tail appends.
343    file: File,
344    /// Path the store was opened from, returned by [`Db::path`].
345    path: PathBuf,
346    /// Live document id to the location of its most recent body.
347    index: HashMap<u64, BodyLoc>,
348    /// Offset at which the next record will be appended.
349    tail: u64,
350    /// Id that the next [`Db::insert`] will assign.
351    next_id: u64,
352    /// Reusable buffer for framing a record, so writes do not allocate.
353    scratch: Vec<u8>,
354    /// Secondary indexes by field name, built on demand and maintained on every
355    /// write. Not persisted: rebuilt via [`Db::create_index`] each session.
356    indexes: HashMap<String, SecondaryIndex>,
357    /// When to force writes to disk with `fsync`.
358    sync: SyncPolicy,
359}
360
361impl Db {
362    /// Opens the store at `path`, creating an empty one if the file does not
363    /// exist, and replaying any existing records to rebuild the index.
364    ///
365    /// On open the whole log is scanned: each record's checksum is verified and
366    /// the in-memory index is reconstructed from the surviving inserts and
367    /// deletes. A record left half-written by a crash — detectable because it
368    /// runs past the end of the file or fails its checksum at the tail — is
369    /// truncated away, restoring the file to its last consistent state. A
370    /// checksum failure on a record that is *not* at the tail is reported as
371    /// [`Error::Corrupt`], because that indicates in-place damage rather than a
372    /// torn write.
373    ///
374    /// Uses [`SyncPolicy::Manual`]; for a different policy, open through
375    /// [`DbOptions`].
376    ///
377    /// # Errors
378    ///
379    /// Returns [`Error::Io`] if the file cannot be opened or read,
380    /// [`Error::BadMagic`] if an existing file is not a bison-db store,
381    /// [`Error::UnsupportedVersion`] if it was written by a newer format, and
382    /// [`Error::Corrupt`] if a non-tail record fails verification.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// # fn main() -> bison_db::Result<()> {
388    /// let path = std::env::temp_dir().join("bison_db_open_example.bison");
389    /// let _ = std::fs::remove_file(&path);
390    /// let db = bison_db::Db::open(&path)?;
391    /// assert!(db.is_empty());
392    /// # let _ = std::fs::remove_file(&path);
393    /// # Ok(())
394    /// # }
395    /// ```
396    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
397        DbOptions::new().open(path)
398    }
399
400    /// Opens (or creates) the store at `path` with the given [`DbOptions`].
401    ///
402    /// A shorthand for [`DbOptions::open`]; see [`Db::open`] for the open and
403    /// recovery contract.
404    ///
405    /// # Errors
406    ///
407    /// Same as [`Db::open`].
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// # fn main() -> bison_db::Result<()> {
413    /// use bison_db::{Db, DbOptions, SyncPolicy};
414    /// # let path = std::env::temp_dir().join("bison_db_open_with_example.bison");
415    /// # let _ = std::fs::remove_file(&path);
416    /// let db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always))?;
417    /// assert_eq!(db.sync_policy(), SyncPolicy::Always);
418    /// # drop(db);
419    /// # let _ = std::fs::remove_file(&path);
420    /// # Ok(())
421    /// # }
422    /// ```
423    pub fn open_with<P: AsRef<Path>>(path: P, options: DbOptions) -> Result<Self> {
424        options.open(path)
425    }
426
427    /// The shared open path used by [`Db::open`] and [`DbOptions::open`].
428    fn open_inner(path: PathBuf, sync: SyncPolicy) -> Result<Self> {
429        // Remove any temporary file left behind by an interrupted compaction;
430        // the original store file is the authoritative copy.
431        let _ = std::fs::remove_file(compacting_path(&path));
432
433        let file = OpenOptions::new()
434            .read(true)
435            .write(true)
436            .create(true)
437            .truncate(false)
438            .open(&path)?;
439        let file_len = file.metadata()?.len();
440
441        let mut db = Db {
442            file,
443            path,
444            index: HashMap::new(),
445            tail: HEADER_LEN,
446            next_id: 1,
447            scratch: Vec::with_capacity(256),
448            indexes: HashMap::new(),
449            sync,
450        };
451
452        if file_len == 0 {
453            db.write_header()?;
454            // Make the newly created file's directory entry durable, so the file
455            // is guaranteed to exist after a crash that follows creation.
456            sync_parent_dir(&db.path)?;
457        } else {
458            db.verify_header(file_len)?;
459            db.replay(file_len)?;
460        }
461        Ok(db)
462    }
463
464    /// Returns the store's [`SyncPolicy`].
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// # fn main() -> bison_db::Result<()> {
470    /// use bison_db::{Db, SyncPolicy};
471    /// # let path = std::env::temp_dir().join("bison_db_syncpolicy_getter.bison");
472    /// # let _ = std::fs::remove_file(&path);
473    /// let db = Db::open(&path)?;
474    /// assert_eq!(db.sync_policy(), SyncPolicy::Manual);
475    /// # drop(db);
476    /// # let _ = std::fs::remove_file(&path);
477    /// # Ok(())
478    /// # }
479    /// ```
480    #[must_use]
481    pub fn sync_policy(&self) -> SyncPolicy {
482        self.sync
483    }
484
485    /// Inserts `doc`, assigning and returning a fresh [`DocId`].
486    ///
487    /// The document is appended to the log and indexed; it is readable
488    /// immediately and durable after the next [`flush`](Db::flush).
489    ///
490    /// # Errors
491    ///
492    /// Returns [`Error::ValueTooLarge`] if the encoded document exceeds
493    /// [`MAX_RECORD_BYTES`], or [`Error::Io`] if the append fails.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// # fn main() -> bison_db::Result<()> {
499    /// # let path = std::env::temp_dir().join("bison_db_insert_example.bison");
500    /// # let _ = std::fs::remove_file(&path);
501    /// use bison_db::{Db, Document};
502    /// let mut db = Db::open(&path)?;
503    /// let mut doc = Document::new();
504    /// doc.set("k", "v");
505    /// let id = db.insert(doc)?;
506    /// assert!(db.contains(id));
507    /// # let _ = std::fs::remove_file(&path);
508    /// # Ok(())
509    /// # }
510    /// ```
511    pub fn insert(&mut self, doc: Document) -> Result<DocId> {
512        let id = self.next_id;
513        self.append(OP_PUT, id, Some(&doc))?;
514        self.next_id = id + 1;
515        self.index_add(id, &doc);
516        Ok(DocId(id))
517    }
518
519    /// Reads the document stored under `id`, or `None` if no live document has
520    /// that id.
521    ///
522    /// # Errors
523    ///
524    /// Returns [`Error::Io`] if the body cannot be read, or [`Error::Corrupt`]
525    /// if the stored bytes fail to decode (which a passing checksum makes
526    /// unexpected in practice).
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// # fn main() -> bison_db::Result<()> {
532    /// # let path = std::env::temp_dir().join("bison_db_get_example.bison");
533    /// # let _ = std::fs::remove_file(&path);
534    /// use bison_db::{Db, Document, DocId};
535    /// let mut db = Db::open(&path)?;
536    /// let id = db.insert({ let mut d = Document::new(); d.set("n", 1_i64); d })?;
537    /// assert!(db.get(id)?.is_some());
538    /// assert!(db.get(DocId::from(9999))?.is_none());
539    /// # let _ = std::fs::remove_file(&path);
540    /// # Ok(())
541    /// # }
542    /// ```
543    pub fn get(&self, id: DocId) -> Result<Option<Document>> {
544        match self.index.get(&id.0).copied() {
545            Some(loc) => self.read_body(loc).map(Some),
546            None => Ok(None),
547        }
548    }
549
550    /// Overwrites the document stored under `id` with `doc`, returning `true` if
551    /// a document was present to overwrite and `false` otherwise.
552    ///
553    /// A successful update appends a new record and repoints the index; the
554    /// previous body remains in the file as dead space until compaction.
555    ///
556    /// # Errors
557    ///
558    /// Returns [`Error::ValueTooLarge`] or [`Error::Io`] under the same
559    /// conditions as [`insert`](Db::insert).
560    ///
561    /// # Examples
562    ///
563    /// ```
564    /// # fn main() -> bison_db::Result<()> {
565    /// # let path = std::env::temp_dir().join("bison_db_update_example.bison");
566    /// # let _ = std::fs::remove_file(&path);
567    /// use bison_db::{Db, Document, DocId};
568    /// let mut db = Db::open(&path)?;
569    /// let id = db.insert({ let mut d = Document::new(); d.set("v", 1_i64); d })?;
570    ///
571    /// let mut next = Document::new();
572    /// next.set("v", 2_i64);
573    /// assert!(db.update(id, next)?);
574    /// assert!(!db.update(DocId::from(404), Document::new())?);
575    /// # let _ = std::fs::remove_file(&path);
576    /// # Ok(())
577    /// # }
578    /// ```
579    pub fn update(&mut self, id: DocId, doc: Document) -> Result<bool> {
580        let Some(loc) = self.index.get(&id.0).copied() else {
581            return Ok(false);
582        };
583        if !self.indexes.is_empty() {
584            let old = self.read_body(loc)?;
585            self.index_remove(id.0, &old);
586        }
587        self.append(OP_PUT, id.0, Some(&doc))?;
588        self.index_add(id.0, &doc);
589        Ok(true)
590    }
591
592    /// Deletes the document stored under `id`, returning `true` if one was
593    /// present and `false` otherwise.
594    ///
595    /// A tombstone is appended so the deletion survives reopening; the document
596    /// is unreadable as soon as this returns.
597    ///
598    /// # Errors
599    ///
600    /// Returns [`Error::Io`] if the tombstone cannot be appended.
601    ///
602    /// # Examples
603    ///
604    /// ```
605    /// # fn main() -> bison_db::Result<()> {
606    /// # let path = std::env::temp_dir().join("bison_db_delete_example.bison");
607    /// # let _ = std::fs::remove_file(&path);
608    /// use bison_db::{Db, Document};
609    /// let mut db = Db::open(&path)?;
610    /// let id = db.insert({ let mut d = Document::new(); d.set("x", 1_i64); d })?;
611    /// assert!(db.delete(id)?);
612    /// assert!(db.get(id)?.is_none());
613    /// assert!(!db.delete(id)?);
614    /// # let _ = std::fs::remove_file(&path);
615    /// # Ok(())
616    /// # }
617    /// ```
618    pub fn delete(&mut self, id: DocId) -> Result<bool> {
619        let Some(loc) = self.index.get(&id.0).copied() else {
620            return Ok(false);
621        };
622        if !self.indexes.is_empty() {
623            let old = self.read_body(loc)?;
624            self.index_remove(id.0, &old);
625        }
626        self.append(OP_DELETE, id.0, None)?;
627        Ok(true)
628    }
629
630    /// Returns `true` if a live document has this `id`.
631    ///
632    /// This is an in-memory index lookup with no file access.
633    ///
634    /// # Examples
635    ///
636    /// ```
637    /// # fn main() -> bison_db::Result<()> {
638    /// # let path = std::env::temp_dir().join("bison_db_contains_example.bison");
639    /// # let _ = std::fs::remove_file(&path);
640    /// use bison_db::{Db, Document};
641    /// let mut db = Db::open(&path)?;
642    /// let id = db.insert(Document::new())?;
643    /// assert!(db.contains(id));
644    /// # let _ = std::fs::remove_file(&path);
645    /// # Ok(())
646    /// # }
647    /// ```
648    #[must_use]
649    pub fn contains(&self, id: DocId) -> bool {
650        self.index.contains_key(&id.0)
651    }
652
653    /// Returns the number of live documents.
654    ///
655    /// # Examples
656    ///
657    /// ```
658    /// # fn main() -> bison_db::Result<()> {
659    /// # let path = std::env::temp_dir().join("bison_db_len_example.bison");
660    /// # let _ = std::fs::remove_file(&path);
661    /// use bison_db::{Db, Document};
662    /// let mut db = Db::open(&path)?;
663    /// db.insert(Document::new())?;
664    /// assert_eq!(db.len(), 1);
665    /// # let _ = std::fs::remove_file(&path);
666    /// # Ok(())
667    /// # }
668    /// ```
669    #[must_use]
670    pub fn len(&self) -> usize {
671        self.index.len()
672    }
673
674    /// Returns `true` if the store holds no live documents.
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// # fn main() -> bison_db::Result<()> {
680    /// # let path = std::env::temp_dir().join("bison_db_isempty_example.bison");
681    /// # let _ = std::fs::remove_file(&path);
682    /// let db = bison_db::Db::open(&path)?;
683    /// assert!(db.is_empty());
684    /// # let _ = std::fs::remove_file(&path);
685    /// # Ok(())
686    /// # }
687    /// ```
688    #[must_use]
689    pub fn is_empty(&self) -> bool {
690        self.index.is_empty()
691    }
692
693    /// Returns an iterator over the ids of all live documents.
694    ///
695    /// The order is unspecified and may change between runs; collect and sort if
696    /// you need a stable order.
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// # fn main() -> bison_db::Result<()> {
702    /// # let path = std::env::temp_dir().join("bison_db_ids_example.bison");
703    /// # let _ = std::fs::remove_file(&path);
704    /// use bison_db::{Db, Document};
705    /// let mut db = Db::open(&path)?;
706    /// db.insert(Document::new())?;
707    /// db.insert(Document::new())?;
708    /// assert_eq!(db.ids().count(), 2);
709    /// # let _ = std::fs::remove_file(&path);
710    /// # Ok(())
711    /// # }
712    /// ```
713    pub fn ids(&self) -> impl Iterator<Item = DocId> + '_ {
714        self.index.keys().copied().map(DocId)
715    }
716
717    /// Flushes buffered writes and `fsync`s the file, making every preceding
718    /// write durable against power loss.
719    ///
720    /// # Errors
721    ///
722    /// Returns [`Error::Io`] if the sync fails.
723    ///
724    /// # Examples
725    ///
726    /// ```
727    /// # fn main() -> bison_db::Result<()> {
728    /// # let path = std::env::temp_dir().join("bison_db_flush_example.bison");
729    /// # let _ = std::fs::remove_file(&path);
730    /// use bison_db::{Db, Document};
731    /// let mut db = Db::open(&path)?;
732    /// db.insert(Document::new())?;
733    /// db.flush()?;
734    /// # let _ = std::fs::remove_file(&path);
735    /// # Ok(())
736    /// # }
737    /// ```
738    pub fn flush(&mut self) -> Result<()> {
739        self.file.sync_all()?;
740        Ok(())
741    }
742
743    /// Returns the path the store was opened from.
744    ///
745    /// # Examples
746    ///
747    /// ```
748    /// # fn main() -> bison_db::Result<()> {
749    /// # let path = std::env::temp_dir().join("bison_db_path_example.bison");
750    /// # let _ = std::fs::remove_file(&path);
751    /// let db = bison_db::Db::open(&path)?;
752    /// assert_eq!(db.path(), path.as_path());
753    /// # let _ = std::fs::remove_file(&path);
754    /// # Ok(())
755    /// # }
756    /// ```
757    #[must_use]
758    pub fn path(&self) -> &Path {
759        &self.path
760    }
761
762    /// Returns a [`Stats`] snapshot of the store's size and live contents.
763    ///
764    /// # Examples
765    ///
766    /// ```
767    /// # fn main() -> bison_db::Result<()> {
768    /// # let path = std::env::temp_dir().join("bison_db_stats_example.bison");
769    /// # let _ = std::fs::remove_file(&path);
770    /// use bison_db::{Db, Document};
771    /// let mut db = Db::open(&path)?;
772    /// db.insert(Document::new())?;
773    /// assert_eq!(db.stats().live_documents, 1);
774    /// # let _ = std::fs::remove_file(&path);
775    /// # Ok(())
776    /// # }
777    /// ```
778    #[must_use]
779    pub fn stats(&self) -> Stats {
780        let live_bytes = self.index.values().map(|loc| u64::from(loc.len)).sum();
781        Stats {
782            live_documents: self.index.len(),
783            file_bytes: self.tail,
784            live_bytes,
785        }
786    }
787
788    /// Rewrites the file to contain only live documents, reclaiming the space
789    /// held by superseded and deleted records.
790    ///
791    /// Every overwrite and delete leaves dead bytes behind in the append-only
792    /// log; over time the file grows past the size of its live data. Compaction
793    /// writes a fresh copy containing one current record per live document, then
794    /// atomically swaps it in. Document ids are preserved, so existing
795    /// [`DocId`]s and secondary indexes remain valid; only the on-disk layout
796    /// changes.
797    ///
798    /// The compacted copy is built in a sibling temporary file and put in place
799    /// with an atomic rename, so a crash at any point leaves either the original
800    /// file or the fully compacted one — never a partial result. A leftover
801    /// temporary from an interrupted compaction is cleaned up on the next
802    /// [`open`](Db::open).
803    ///
804    /// Compaction is durable on return regardless of [`SyncPolicy`]: the new file
805    /// is `fsync`ed before the swap.
806    ///
807    /// # Errors
808    ///
809    /// Returns [`Error::Io`] if the temporary file cannot be written or swapped
810    /// in, or [`Error::Corrupt`] if a live record cannot be read back.
811    ///
812    /// # Examples
813    ///
814    /// ```
815    /// # fn main() -> bison_db::Result<()> {
816    /// # let path = std::env::temp_dir().join("bison_db_compact_example.bison");
817    /// # let _ = std::fs::remove_file(&path);
818    /// use bison_db::{Db, Document};
819    /// let mut db = Db::open(&path)?;
820    /// let id = db.insert({ let mut d = Document::new(); d.set("v", 1_i64); d })?;
821    /// for n in 2..1000 {
822    ///     db.update(id, { let mut d = Document::new(); d.set("v", n); d })?;
823    /// }
824    /// let before = db.stats().file_bytes;
825    ///
826    /// db.compact()?; // collapse 999 versions down to one live record
827    ///
828    /// assert!(db.stats().file_bytes < before);
829    /// assert_eq!(db.get(id)?.unwrap().get("v").and_then(|v| v.as_int()), Some(999));
830    /// # let _ = std::fs::remove_file(&path);
831    /// # Ok(())
832    /// # }
833    /// ```
834    pub fn compact(&mut self) -> Result<()> {
835        let temp_path = compacting_path(&self.path);
836        let _ = std::fs::remove_file(&temp_path);
837
838        let temp = OpenOptions::new()
839            .read(true)
840            .write(true)
841            .create_new(true)
842            .open(&temp_path)?;
843        write_header_to(&temp)?;
844
845        let mut new_index = HashMap::with_capacity(self.index.len());
846        let mut tail = HEADER_LEN;
847        let mut body = Vec::new();
848        let mut frame = Vec::new();
849
850        for (&id, &loc) in &self.index {
851            body.resize(loc.len as usize, 0);
852            read_exact_at(&self.file, &mut body, loc.offset)?;
853
854            frame.clear();
855            frame.extend_from_slice(&[0u8; FRAME_LEN]);
856            frame.push(OP_PUT);
857            frame.extend_from_slice(&id.to_le_bytes());
858            frame.extend_from_slice(&body);
859            let payload_len = frame.len() - FRAME_LEN;
860            let crc = crc32c(&frame[FRAME_LEN..]);
861            frame[0..4].copy_from_slice(&(payload_len as u32).to_le_bytes());
862            frame[4..8].copy_from_slice(&crc.to_le_bytes());
863
864            write_all_at(&temp, &frame, tail)?;
865            let offset = tail + FRAME_LEN as u64 + MIN_PAYLOAD as u64;
866            let _ = new_index.insert(
867                id,
868                BodyLoc {
869                    offset,
870                    len: loc.len,
871                },
872            );
873            tail += (FRAME_LEN + payload_len) as u64;
874        }
875
876        temp.sync_all()?;
877        drop(temp);
878
879        // Re-point our handle at the temporary file, which closes the original
880        // file so the rename can replace it on every platform. The temporary is
881        // open with share-delete, so renaming it while open is permitted.
882        self.file = OpenOptions::new().read(true).write(true).open(&temp_path)?;
883        std::fs::rename(&temp_path, &self.path)?;
884        self.file = OpenOptions::new().read(true).write(true).open(&self.path)?;
885        sync_parent_dir(&self.path)?;
886
887        self.index = new_index;
888        self.tail = tail;
889        Ok(())
890    }
891
892    /// Builds a secondary index over `field`, making [`find`](Db::find) and
893    /// [`range`](Db::range) on that field fast point and range lookups instead of
894    /// full scans.
895    ///
896    /// The index is built by reading every live document once and recording its
897    /// value for `field`; documents without the field are skipped. From then on,
898    /// it is maintained automatically on every insert, update, and delete. Any
899    /// number of fields may be indexed — call this once per field.
900    ///
901    /// Indexes live in memory only and are **not** persisted: after reopening a
902    /// store, call this again for each field you want indexed. Calling it for a
903    /// field that is already indexed is a no-op.
904    ///
905    /// # Errors
906    ///
907    /// Returns [`Error::Io`] or [`Error::Corrupt`] if a document cannot be read
908    /// while building the index.
909    ///
910    /// # Examples
911    ///
912    /// ```
913    /// # fn main() -> bison_db::Result<()> {
914    /// # let path = std::env::temp_dir().join("bison_db_createindex_example.bison");
915    /// # let _ = std::fs::remove_file(&path);
916    /// use bison_db::{Db, Document, Value};
917    /// let mut db = Db::open(&path)?;
918    /// db.insert({ let mut d = Document::new(); d.set("city", "Oslo"); d })?;
919    ///
920    /// db.create_index("city")?;
921    /// let hits = db.find("city", &Value::from("Oslo"))?;
922    /// assert_eq!(hits.len(), 1);
923    /// # let _ = std::fs::remove_file(&path);
924    /// # Ok(())
925    /// # }
926    /// ```
927    pub fn create_index(&mut self, field: &str) -> Result<()> {
928        if self.indexes.contains_key(field) {
929            return Ok(());
930        }
931        let mut index = SecondaryIndex::new();
932        let entries: Vec<(u64, BodyLoc)> = self.index.iter().map(|(id, loc)| (*id, *loc)).collect();
933        for (id, loc) in entries {
934            let doc = self.read_body(loc)?;
935            if let Some(value) = doc.get(field) {
936                index.add(value, id);
937            }
938        }
939        let _ = self.indexes.insert(field.to_string(), index);
940        Ok(())
941    }
942
943    /// Drops the secondary index over `field`, returning `true` if one existed.
944    ///
945    /// # Examples
946    ///
947    /// ```
948    /// # fn main() -> bison_db::Result<()> {
949    /// # let path = std::env::temp_dir().join("bison_db_dropindex_example.bison");
950    /// # let _ = std::fs::remove_file(&path);
951    /// let mut db = bison_db::Db::open(&path)?;
952    /// db.create_index("name")?;
953    /// assert!(db.drop_index("name"));
954    /// assert!(!db.drop_index("name"));
955    /// # let _ = std::fs::remove_file(&path);
956    /// # Ok(())
957    /// # }
958    /// ```
959    pub fn drop_index(&mut self, field: &str) -> bool {
960        self.indexes.remove(field).is_some()
961    }
962
963    /// Returns an iterator over the names of the currently indexed fields.
964    ///
965    /// The order is unspecified.
966    ///
967    /// # Examples
968    ///
969    /// ```
970    /// # fn main() -> bison_db::Result<()> {
971    /// # let path = std::env::temp_dir().join("bison_db_indexes_example.bison");
972    /// # let _ = std::fs::remove_file(&path);
973    /// let mut db = bison_db::Db::open(&path)?;
974    /// db.create_index("a")?;
975    /// db.create_index("b")?;
976    /// assert_eq!(db.indexes().count(), 2);
977    /// # let _ = std::fs::remove_file(&path);
978    /// # Ok(())
979    /// # }
980    /// ```
981    pub fn indexes(&self) -> impl Iterator<Item = &str> {
982        self.indexes.keys().map(String::as_str)
983    }
984
985    /// Returns the ids of all live documents whose `field` equals `value`.
986    ///
987    /// If `field` is indexed (see [`create_index`](Db::create_index)) this is a
988    /// point lookup; otherwise it falls back to scanning every live document, so
989    /// the result is correct either way — the index only changes the speed.
990    /// Equality follows the same total order the indexes use, so a `Float` field
991    /// distinguishes `0.0` from `-0.0`.
992    ///
993    /// # Errors
994    ///
995    /// Returns [`Error::Io`] or [`Error::Corrupt`] if a document must be read
996    /// (the unindexed path) and cannot be.
997    ///
998    /// # Examples
999    ///
1000    /// ```
1001    /// # fn main() -> bison_db::Result<()> {
1002    /// # let path = std::env::temp_dir().join("bison_db_find_example.bison");
1003    /// # let _ = std::fs::remove_file(&path);
1004    /// use bison_db::{Db, Document, Value};
1005    /// let mut db = Db::open(&path)?;
1006    /// db.insert({ let mut d = Document::new(); d.set("role", "admin"); d })?;
1007    /// db.insert({ let mut d = Document::new(); d.set("role", "user"); d })?;
1008    /// db.create_index("role")?;
1009    ///
1010    /// assert_eq!(db.find("role", &Value::from("admin"))?.len(), 1);
1011    /// assert!(db.find("role", &Value::from("ghost"))?.is_empty());
1012    /// # let _ = std::fs::remove_file(&path);
1013    /// # Ok(())
1014    /// # }
1015    /// ```
1016    pub fn find(&self, field: &str, value: &Value) -> Result<Vec<DocId>> {
1017        if let Some(index) = self.indexes.get(field) {
1018            return Ok(index.equal(value).into_iter().map(DocId).collect());
1019        }
1020        let mut out = Vec::new();
1021        for (id, loc) in &self.index {
1022            let doc = self.read_body(*loc)?;
1023            if doc
1024                .get(field)
1025                .is_some_and(|v| total_cmp_value(v, value) == core::cmp::Ordering::Equal)
1026            {
1027                out.push(DocId(*id));
1028            }
1029        }
1030        Ok(out)
1031    }
1032
1033    /// Returns the ids of all live documents whose `field` falls within `range`.
1034    ///
1035    /// Bounds are [`Value`]s compared with the same total order the indexes use;
1036    /// any [`RangeBounds`] form works (`a..b`, `a..=b`, `..b`, `a..`, `..`).
1037    /// If `field` is indexed the matches come back ordered by field value (then
1038    /// id); otherwise the store scans every live document. As with
1039    /// [`find`](Db::find), the index changes only the speed, not the result.
1040    ///
1041    /// # Errors
1042    ///
1043    /// Returns [`Error::Io`] or [`Error::Corrupt`] if a document must be read
1044    /// (the unindexed path) and cannot be.
1045    ///
1046    /// # Examples
1047    ///
1048    /// ```
1049    /// # fn main() -> bison_db::Result<()> {
1050    /// # let path = std::env::temp_dir().join("bison_db_range_example.bison");
1051    /// # let _ = std::fs::remove_file(&path);
1052    /// use bison_db::{Db, Document, Value};
1053    /// let mut db = Db::open(&path)?;
1054    /// for age in [17_i64, 25, 40, 70] {
1055    ///     db.insert({ let mut d = Document::new(); d.set("age", age); d })?;
1056    /// }
1057    /// db.create_index("age")?;
1058    ///
1059    /// // Working-age adults: 18..=65.
1060    /// let hits = db.range("age", Value::from(18_i64)..=Value::from(65_i64))?;
1061    /// assert_eq!(hits.len(), 2); // 25 and 40
1062    /// # let _ = std::fs::remove_file(&path);
1063    /// # Ok(())
1064    /// # }
1065    /// ```
1066    pub fn range<R: RangeBounds<Value>>(&self, field: &str, range: R) -> Result<Vec<DocId>> {
1067        let lo = range.start_bound();
1068        let hi = range.end_bound();
1069        if let Some(index) = self.indexes.get(field) {
1070            return Ok(index.range(lo, hi).into_iter().map(DocId).collect());
1071        }
1072        let mut out = Vec::new();
1073        for (id, loc) in &self.index {
1074            let doc = self.read_body(*loc)?;
1075            if doc.get(field).is_some_and(|v| in_bounds(v, lo, hi)) {
1076                out.push(DocId(*id));
1077            }
1078        }
1079        Ok(out)
1080    }
1081
1082    /// Reads and decodes the document body at `loc`.
1083    fn read_body(&self, loc: BodyLoc) -> Result<Document> {
1084        let mut buf = vec![0u8; loc.len as usize];
1085        read_exact_at(&self.file, &mut buf, loc.offset)?;
1086        decode_document(&buf)
1087    }
1088
1089    /// Adds document `id`'s indexed field values to every secondary index.
1090    fn index_add(&mut self, id: u64, doc: &Document) {
1091        for (field, index) in &mut self.indexes {
1092            if let Some(value) = doc.get(field) {
1093                index.add(value, id);
1094            }
1095        }
1096    }
1097
1098    /// Removes document `id`'s indexed field values from every secondary index.
1099    fn index_remove(&mut self, id: u64, doc: &Document) {
1100        for (field, index) in &mut self.indexes {
1101            if let Some(value) = doc.get(field) {
1102                index.remove(value, id);
1103            }
1104        }
1105    }
1106
1107    /// Appends one framed record and updates the index accordingly.
1108    ///
1109    /// For [`OP_PUT`] the body is encoded and the index repointed at it; for
1110    /// [`OP_DELETE`] the index entry is removed. The frame is built in `scratch`
1111    /// so the steady-state write path performs no per-record allocation.
1112    fn append(&mut self, op: u8, id: u64, doc: Option<&Document>) -> Result<()> {
1113        self.scratch.clear();
1114        // Reserve the frame header; the length and checksum are backfilled once
1115        // the payload is known.
1116        self.scratch.extend_from_slice(&[0u8; FRAME_LEN]);
1117        self.scratch.push(op);
1118        self.scratch.extend_from_slice(&id.to_le_bytes());
1119        if let Some(doc) = doc {
1120            encode_document_into(&mut self.scratch, doc)?;
1121        }
1122
1123        let payload_len = self.scratch.len() - FRAME_LEN;
1124        if payload_len > MAX_RECORD_BYTES {
1125            return Err(Error::ValueTooLarge);
1126        }
1127        let crc = crc32c(&self.scratch[FRAME_LEN..]);
1128        self.scratch[0..4].copy_from_slice(&(payload_len as u32).to_le_bytes());
1129        self.scratch[4..8].copy_from_slice(&crc.to_le_bytes());
1130
1131        write_all_at(&self.file, &self.scratch, self.tail)?;
1132
1133        let record_start = self.tail;
1134        self.tail += (FRAME_LEN + payload_len) as u64;
1135
1136        match op {
1137            OP_PUT => {
1138                let offset = record_start + FRAME_LEN as u64 + MIN_PAYLOAD as u64;
1139                let len = (payload_len - MIN_PAYLOAD) as u32;
1140                let _ = self.index.insert(id, BodyLoc { offset, len });
1141            }
1142            OP_DELETE => {
1143                let _ = self.index.remove(&id);
1144            }
1145            _ => {}
1146        }
1147
1148        if self.sync == SyncPolicy::Always {
1149            self.file.sync_all()?;
1150        }
1151        Ok(())
1152    }
1153
1154    /// Writes the 16-byte file header at offset 0 and syncs it, establishing a
1155    /// valid empty store.
1156    fn write_header(&mut self) -> Result<()> {
1157        write_header_to(&self.file)?;
1158        self.file.sync_all()?;
1159        Ok(())
1160    }
1161
1162    /// Validates the header of an existing file: length, magic, and version.
1163    fn verify_header(&self, file_len: u64) -> Result<()> {
1164        if file_len < HEADER_LEN {
1165            return Err(Error::BadMagic);
1166        }
1167        let mut header = [0u8; HEADER_LEN as usize];
1168        read_exact_at(&self.file, &mut header, 0)?;
1169        if header[0..8] != HEADER_MAGIC {
1170            return Err(Error::BadMagic);
1171        }
1172        let version = u16::from_le_bytes([header[8], header[9]]);
1173        if version > FORMAT_VERSION {
1174            return Err(Error::UnsupportedVersion(version));
1175        }
1176        Ok(())
1177    }
1178
1179    /// Scans every record after the header, rebuilding the index and truncating
1180    /// a torn record at the tail if one is found.
1181    fn replay(&mut self, file_len: u64) -> Result<()> {
1182        let mut offset = HEADER_LEN;
1183        let mut frame = [0u8; FRAME_LEN];
1184
1185        loop {
1186            if offset + FRAME_LEN as u64 > file_len {
1187                break;
1188            }
1189            read_exact_at(&self.file, &mut frame, offset)?;
1190            let payload_len = u32::from_le_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
1191            let expected_crc = u32::from_le_bytes([frame[4], frame[5], frame[6], frame[7]]);
1192
1193            if !(MIN_PAYLOAD..=MAX_RECORD_BYTES).contains(&payload_len) {
1194                // A length this size at the tail is an incomplete write; mid-file
1195                // it is corruption. Either way the run of valid records ends here.
1196                break;
1197            }
1198            let record_end = offset + FRAME_LEN as u64 + payload_len as u64;
1199            if record_end > file_len {
1200                break;
1201            }
1202
1203            let mut payload = vec![0u8; payload_len];
1204            read_exact_at(&self.file, &mut payload, offset + FRAME_LEN as u64)?;
1205            if crc32c(&payload) != expected_crc {
1206                if record_end == file_len {
1207                    // Torn final record: drop it and stop.
1208                    break;
1209                }
1210                return Err(Error::Corrupt("crc mismatch"));
1211            }
1212
1213            let op = payload[0];
1214            let id = u64::from_le_bytes([
1215                payload[1], payload[2], payload[3], payload[4], payload[5], payload[6], payload[7],
1216                payload[8],
1217            ]);
1218
1219            match op {
1220                OP_PUT => {
1221                    let offset = offset + FRAME_LEN as u64 + MIN_PAYLOAD as u64;
1222                    let len = (payload_len - MIN_PAYLOAD) as u32;
1223                    let _ = self.index.insert(id, BodyLoc { offset, len });
1224                }
1225                OP_DELETE => {
1226                    let _ = self.index.remove(&id);
1227                }
1228                _ => return Err(Error::Corrupt("unknown record op")),
1229            }
1230            if id >= self.next_id {
1231                self.next_id = id + 1;
1232            }
1233            offset = record_end;
1234        }
1235
1236        if offset < file_len {
1237            // Trailing torn bytes: cut the file back to the last good record.
1238            self.file.set_len(offset)?;
1239        }
1240        self.tail = offset;
1241        Ok(())
1242    }
1243}
1244
1245impl fmt::Debug for Db {
1246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247        f.debug_struct("Db")
1248            .field("path", &self.path)
1249            .field("live_documents", &self.index.len())
1250            .field("file_bytes", &self.tail)
1251            .field("sync", &self.sync)
1252            .finish()
1253    }
1254}
1255
1256/// Compile-time proof that [`Db`] is `Send + Sync`, so it can be shared across
1257/// threads behind a lock as the concurrency docs describe. If a future field
1258/// broke this, the crate would fail to compile here rather than silently.
1259const _: fn() = || {
1260    fn assert_send_sync<T: Send + Sync>() {}
1261    assert_send_sync::<Db>();
1262};
1263
1264impl Drop for Db {
1265    /// Makes a best-effort `fsync` on a clean shutdown under
1266    /// [`SyncPolicy::Manual`], so a normal program exit does not lose writes that
1267    /// were never explicitly flushed. Under [`SyncPolicy::Always`] every write is
1268    /// already durable, so nothing is done. Any error here is ignored because a
1269    /// destructor cannot return one; call [`Db::flush`] before dropping when you
1270    /// need to observe a sync failure.
1271    fn drop(&mut self) {
1272        if self.sync == SyncPolicy::Manual {
1273            let _ = self.file.sync_all();
1274        }
1275    }
1276}
1277
1278/// Writes the 16-byte file header at offset 0 of `file` (without syncing).
1279fn write_header_to(file: &File) -> Result<()> {
1280    let mut header = [0u8; HEADER_LEN as usize];
1281    header[0..8].copy_from_slice(&HEADER_MAGIC);
1282    header[8..10].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
1283    write_all_at(file, &header, 0)?;
1284    Ok(())
1285}
1286
1287/// The sibling path a compaction writes its temporary file to: the store's path
1288/// with a `.compacting` suffix. A leftover one is removed on [`Db::open`].
1289fn compacting_path(path: &Path) -> PathBuf {
1290    let mut name = path.as_os_str().to_os_string();
1291    name.push(".compacting");
1292    PathBuf::from(name)
1293}
1294
1295/// Forces the directory containing `path` to disk, so the file's creation is
1296/// durable. On Unix this is a real `fsync` of the parent directory; on Windows,
1297/// directory handles do not support this and file-level `fsync` already persists
1298/// the entry, so this is a documented no-op.
1299#[cfg(unix)]
1300fn sync_parent_dir(path: &Path) -> Result<()> {
1301    let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
1302    let dir = parent.unwrap_or_else(|| Path::new("."));
1303    let handle = File::open(dir)?;
1304    handle.sync_all()?;
1305    Ok(())
1306}
1307
1308/// Windows counterpart to [`sync_parent_dir`]: a no-op, because the file-level
1309/// `fsync` already makes the directory entry durable on this platform.
1310#[cfg(windows)]
1311fn sync_parent_dir(_path: &Path) -> Result<()> {
1312    Ok(())
1313}
1314
1315#[cfg(test)]
1316#[allow(clippy::unwrap_used, clippy::expect_used)]
1317mod tests {
1318    use super::*;
1319    use crate::value::Value;
1320    use std::sync::atomic::{AtomicU64, Ordering};
1321
1322    /// Returns a unique temp path and removes any stale file at it.
1323    fn temp_path() -> PathBuf {
1324        static COUNTER: AtomicU64 = AtomicU64::new(0);
1325        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1326        let pid = std::process::id();
1327        let path = std::env::temp_dir().join(format!("bison_db_test_{pid}_{n}.bison"));
1328        let _ = std::fs::remove_file(&path);
1329        path
1330    }
1331
1332    fn doc(pairs: &[(&str, i64)]) -> Document {
1333        let mut d = Document::new();
1334        for (k, v) in pairs {
1335            d.set(*k, *v);
1336        }
1337        d
1338    }
1339
1340    #[test]
1341    fn test_insert_get_roundtrip() {
1342        let path = temp_path();
1343        let mut db = Db::open(&path).unwrap();
1344        let id = db.insert(doc(&[("a", 1), ("b", 2)])).unwrap();
1345        let got = db.get(id).unwrap().unwrap();
1346        assert_eq!(got.get("a").and_then(Value::as_int), Some(1));
1347        let _ = std::fs::remove_file(&path);
1348    }
1349
1350    #[test]
1351    fn test_get_missing_returns_none() {
1352        let path = temp_path();
1353        let db = Db::open(&path).unwrap();
1354        assert!(db.get(DocId::from(1)).unwrap().is_none());
1355        let _ = std::fs::remove_file(&path);
1356    }
1357
1358    #[test]
1359    fn test_delete_removes_document() {
1360        let path = temp_path();
1361        let mut db = Db::open(&path).unwrap();
1362        let id = db.insert(doc(&[("x", 9)])).unwrap();
1363        assert!(db.delete(id).unwrap());
1364        assert!(db.get(id).unwrap().is_none());
1365        assert!(!db.delete(id).unwrap());
1366        let _ = std::fs::remove_file(&path);
1367    }
1368
1369    #[test]
1370    fn test_update_changes_value() {
1371        let path = temp_path();
1372        let mut db = Db::open(&path).unwrap();
1373        let id = db.insert(doc(&[("v", 1)])).unwrap();
1374        assert!(db.update(id, doc(&[("v", 2)])).unwrap());
1375        assert_eq!(
1376            db.get(id)
1377                .unwrap()
1378                .unwrap()
1379                .get("v")
1380                .and_then(Value::as_int),
1381            Some(2)
1382        );
1383        let _ = std::fs::remove_file(&path);
1384    }
1385
1386    #[test]
1387    fn test_update_absent_id_is_false() {
1388        let path = temp_path();
1389        let mut db = Db::open(&path).unwrap();
1390        assert!(!db.update(DocId::from(7), Document::new()).unwrap());
1391        let _ = std::fs::remove_file(&path);
1392    }
1393
1394    #[test]
1395    fn test_reopen_recovers_state() {
1396        let path = temp_path();
1397        let (a, b);
1398        {
1399            let mut db = Db::open(&path).unwrap();
1400            a = db.insert(doc(&[("n", 10)])).unwrap();
1401            b = db.insert(doc(&[("n", 20)])).unwrap();
1402            db.delete(a).unwrap();
1403            db.flush().unwrap();
1404        }
1405        let db = Db::open(&path).unwrap();
1406        assert!(db.get(a).unwrap().is_none());
1407        assert_eq!(
1408            db.get(b).unwrap().unwrap().get("n").and_then(Value::as_int),
1409            Some(20)
1410        );
1411        assert_eq!(db.len(), 1);
1412        let _ = std::fs::remove_file(&path);
1413    }
1414
1415    #[test]
1416    fn test_reopen_continues_id_sequence() {
1417        let path = temp_path();
1418        let first;
1419        {
1420            let mut db = Db::open(&path).unwrap();
1421            first = db.insert(Document::new()).unwrap();
1422        }
1423        let mut db = Db::open(&path).unwrap();
1424        let second = db.insert(Document::new()).unwrap();
1425        assert!(second.get() > first.get());
1426        let _ = std::fs::remove_file(&path);
1427    }
1428
1429    #[test]
1430    fn test_open_rejects_foreign_file() {
1431        let path = temp_path();
1432        std::fs::write(&path, b"this is definitely not a bison-db file at all").unwrap();
1433        assert!(matches!(Db::open(&path), Err(Error::BadMagic)));
1434        let _ = std::fs::remove_file(&path);
1435    }
1436
1437    #[test]
1438    fn test_torn_tail_is_truncated_on_open() {
1439        let path = temp_path();
1440        let keep;
1441        {
1442            let mut db = Db::open(&path).unwrap();
1443            keep = db.insert(doc(&[("ok", 1)])).unwrap();
1444            db.flush().unwrap();
1445        }
1446        // Append a bogus frame claiming a payload longer than what follows.
1447        {
1448            use std::io::Write;
1449            let mut f = OpenOptions::new().append(true).open(&path).unwrap();
1450            let mut frame = Vec::new();
1451            frame.extend_from_slice(&999u32.to_le_bytes());
1452            frame.extend_from_slice(&0u32.to_le_bytes());
1453            frame.extend_from_slice(b"short");
1454            f.write_all(&frame).unwrap();
1455            f.flush().unwrap();
1456        }
1457        let db = Db::open(&path).unwrap();
1458        assert!(db.get(keep).unwrap().is_some());
1459        assert_eq!(db.len(), 1);
1460        let _ = std::fs::remove_file(&path);
1461    }
1462
1463    #[test]
1464    fn test_stats_reflect_live_documents() {
1465        let path = temp_path();
1466        let mut db = Db::open(&path).unwrap();
1467        db.insert(doc(&[("a", 1)])).unwrap();
1468        let id = db.insert(doc(&[("b", 2)])).unwrap();
1469        db.delete(id).unwrap();
1470        let stats = db.stats();
1471        assert_eq!(stats.live_documents, 1);
1472        assert!(stats.file_bytes > HEADER_LEN);
1473        let _ = std::fs::remove_file(&path);
1474    }
1475
1476    fn sorted(mut ids: Vec<DocId>) -> Vec<u64> {
1477        ids.sort();
1478        ids.into_iter().map(DocId::get).collect()
1479    }
1480
1481    #[test]
1482    fn test_create_index_then_find() {
1483        let path = temp_path();
1484        let mut db = Db::open(&path).unwrap();
1485        let a = db.insert(doc(&[("g", 1)])).unwrap();
1486        let b = db.insert(doc(&[("g", 2)])).unwrap();
1487        let c = db.insert(doc(&[("g", 1)])).unwrap();
1488
1489        db.create_index("g").unwrap();
1490        assert_eq!(
1491            sorted(db.find("g", &Value::from(1_i64)).unwrap()),
1492            sorted(vec![a, c])
1493        );
1494        assert_eq!(
1495            sorted(db.find("g", &Value::from(2_i64)).unwrap()),
1496            vec![b.get()]
1497        );
1498        assert!(db.find("g", &Value::from(9_i64)).unwrap().is_empty());
1499        let _ = std::fs::remove_file(&path);
1500    }
1501
1502    #[test]
1503    fn test_find_indexed_matches_scan() {
1504        let path = temp_path();
1505        let mut db = Db::open(&path).unwrap();
1506        for n in [1, 2, 2, 3, 2] {
1507            db.insert(doc(&[("k", n)])).unwrap();
1508        }
1509        let scan = sorted(db.find("k", &Value::from(2_i64)).unwrap()); // no index yet
1510        db.create_index("k").unwrap();
1511        let indexed = sorted(db.find("k", &Value::from(2_i64)).unwrap());
1512        assert_eq!(scan, indexed);
1513        assert_eq!(scan.len(), 3);
1514        let _ = std::fs::remove_file(&path);
1515    }
1516
1517    #[test]
1518    fn test_range_query_inclusive_and_exclusive() {
1519        let path = temp_path();
1520        let mut db = Db::open(&path).unwrap();
1521        for n in [10, 20, 30, 40] {
1522            db.insert(doc(&[("age", n)])).unwrap();
1523        }
1524        db.create_index("age").unwrap();
1525        assert_eq!(
1526            db.range("age", Value::from(20_i64)..=Value::from(30_i64))
1527                .unwrap()
1528                .len(),
1529            2
1530        );
1531        assert_eq!(
1532            db.range("age", Value::from(20_i64)..Value::from(40_i64))
1533                .unwrap()
1534                .len(),
1535            2
1536        );
1537        assert_eq!(db.range("age", Value::from(25_i64)..).unwrap().len(), 2);
1538        let _ = std::fs::remove_file(&path);
1539    }
1540
1541    #[test]
1542    fn test_index_maintained_on_update_and_delete() {
1543        let path = temp_path();
1544        let mut db = Db::open(&path).unwrap();
1545        let id = db.insert(doc(&[("status", 1)])).unwrap();
1546        db.create_index("status").unwrap();
1547        assert_eq!(db.find("status", &Value::from(1_i64)).unwrap(), vec![id]);
1548
1549        db.update(id, doc(&[("status", 2)])).unwrap();
1550        assert!(db.find("status", &Value::from(1_i64)).unwrap().is_empty());
1551        assert_eq!(db.find("status", &Value::from(2_i64)).unwrap(), vec![id]);
1552
1553        db.delete(id).unwrap();
1554        assert!(db.find("status", &Value::from(2_i64)).unwrap().is_empty());
1555        let _ = std::fs::remove_file(&path);
1556    }
1557
1558    #[test]
1559    fn test_indexes_listed_and_dropped() {
1560        let path = temp_path();
1561        let mut db = Db::open(&path).unwrap();
1562        db.create_index("a").unwrap();
1563        db.create_index("b").unwrap();
1564        db.create_index("a").unwrap(); // idempotent
1565        let mut names: Vec<&str> = db.indexes().collect();
1566        names.sort_unstable();
1567        assert_eq!(names, ["a", "b"]);
1568        assert!(db.drop_index("a"));
1569        assert!(!db.drop_index("a"));
1570        assert_eq!(db.indexes().count(), 1);
1571        let _ = std::fs::remove_file(&path);
1572    }
1573
1574    #[test]
1575    fn test_index_not_persisted_but_rebuildable_after_reopen() {
1576        let path = temp_path();
1577        let id;
1578        {
1579            let mut db = Db::open(&path).unwrap();
1580            id = db.insert(doc(&[("city", 7)])).unwrap();
1581            db.create_index("city").unwrap();
1582            db.flush().unwrap();
1583        }
1584        let mut db = Db::open(&path).unwrap();
1585        assert_eq!(db.indexes().count(), 0); // indexes are not on disk
1586        db.create_index("city").unwrap(); // rebuild from the log
1587        assert_eq!(db.find("city", &Value::from(7_i64)).unwrap(), vec![id]);
1588        let _ = std::fs::remove_file(&path);
1589    }
1590
1591    #[test]
1592    fn test_default_sync_policy_is_manual() {
1593        let path = temp_path();
1594        let db = Db::open(&path).unwrap();
1595        assert_eq!(db.sync_policy(), SyncPolicy::Manual);
1596        let _ = std::fs::remove_file(&path);
1597    }
1598
1599    #[test]
1600    fn test_options_set_always_sync_policy() {
1601        let path = temp_path();
1602        let mut db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always)).unwrap();
1603        assert_eq!(db.sync_policy(), SyncPolicy::Always);
1604        // Every write fsyncs; data is present and reopen recovers it.
1605        let id = db.insert(doc(&[("v", 1)])).unwrap();
1606        assert!(db.get(id).unwrap().is_some());
1607        drop(db);
1608        let db = Db::open(&path).unwrap();
1609        assert_eq!(db.len(), 1);
1610        let _ = std::fs::remove_file(&path);
1611    }
1612
1613    #[test]
1614    fn test_always_sync_persists_without_explicit_flush() {
1615        let path = temp_path();
1616        let id;
1617        {
1618            let mut db = Db::open_with(&path, DbOptions::new().sync(SyncPolicy::Always)).unwrap();
1619            id = db.insert(doc(&[("durable", 1)])).unwrap();
1620            // No flush() call: Always already synced each write.
1621        }
1622        let db = Db::open(&path).unwrap();
1623        assert!(db.get(id).unwrap().is_some());
1624        let _ = std::fs::remove_file(&path);
1625    }
1626
1627    #[test]
1628    fn test_dboptions_open_matches_db_open() {
1629        let path = temp_path();
1630        let db = DbOptions::new().open(&path).unwrap();
1631        assert_eq!(db.sync_policy(), SyncPolicy::Manual);
1632        let _ = std::fs::remove_file(&path);
1633    }
1634
1635    #[test]
1636    fn test_compact_reclaims_space_and_preserves_data() {
1637        let path = temp_path();
1638        let mut db = Db::open(&path).unwrap();
1639        let id = db.insert(doc(&[("v", 1)])).unwrap();
1640        for n in 2..500 {
1641            db.update(id, doc(&[("v", n)])).unwrap();
1642        }
1643        let before = db.stats().file_bytes;
1644
1645        db.compact().unwrap();
1646
1647        let after = db.stats().file_bytes;
1648        assert!(
1649            after < before,
1650            "compaction should shrink the file: {after} !< {before}"
1651        );
1652        assert_eq!(db.len(), 1);
1653        assert_eq!(
1654            db.get(id)
1655                .unwrap()
1656                .unwrap()
1657                .get("v")
1658                .and_then(Value::as_int),
1659            Some(499)
1660        );
1661        let _ = std::fs::remove_file(&path);
1662    }
1663
1664    #[test]
1665    fn test_compact_drops_deleted_records() {
1666        let path = temp_path();
1667        let mut db = Db::open(&path).unwrap();
1668        let keep = db.insert(doc(&[("k", 1)])).unwrap();
1669        let gone = db.insert(doc(&[("k", 2)])).unwrap();
1670        db.delete(gone).unwrap();
1671
1672        db.compact().unwrap();
1673
1674        assert_eq!(db.len(), 1);
1675        assert!(db.get(keep).unwrap().is_some());
1676        assert!(db.get(gone).unwrap().is_none());
1677        let _ = std::fs::remove_file(&path);
1678    }
1679
1680    #[test]
1681    fn test_compact_preserves_ids_and_indexes() {
1682        let path = temp_path();
1683        let mut db = Db::open(&path).unwrap();
1684        let a = db.insert(doc(&[("city", 1)])).unwrap();
1685        let b = db.insert(doc(&[("city", 2)])).unwrap();
1686        db.create_index("city").unwrap();
1687
1688        db.compact().unwrap();
1689
1690        // Ids are unchanged and the secondary index still resolves them.
1691        assert_eq!(db.find("city", &Value::from(1_i64)).unwrap(), vec![a]);
1692        assert_eq!(db.find("city", &Value::from(2_i64)).unwrap(), vec![b]);
1693        let _ = std::fs::remove_file(&path);
1694    }
1695
1696    #[test]
1697    fn test_compact_then_reopen_recovers() {
1698        let path = temp_path();
1699        let id;
1700        {
1701            let mut db = Db::open(&path).unwrap();
1702            id = db.insert(doc(&[("v", 7)])).unwrap();
1703            db.update(id, doc(&[("v", 8)])).unwrap();
1704            db.compact().unwrap();
1705            db.flush().unwrap();
1706        }
1707        let db = Db::open(&path).unwrap();
1708        assert_eq!(db.len(), 1);
1709        assert_eq!(
1710            db.get(id)
1711                .unwrap()
1712                .unwrap()
1713                .get("v")
1714                .and_then(Value::as_int),
1715            Some(8)
1716        );
1717        let _ = std::fs::remove_file(&path);
1718    }
1719
1720    #[test]
1721    fn test_compact_empty_db_is_ok() {
1722        let path = temp_path();
1723        let mut db = Db::open(&path).unwrap();
1724        db.compact().unwrap();
1725        assert!(db.is_empty());
1726        // Still writable afterwards.
1727        let id = db.insert(doc(&[("x", 1)])).unwrap();
1728        assert!(db.get(id).unwrap().is_some());
1729        let _ = std::fs::remove_file(&path);
1730    }
1731
1732    #[test]
1733    fn test_open_removes_stale_compacting_temp() {
1734        let path = temp_path();
1735        {
1736            let mut db = Db::open(&path).unwrap();
1737            db.insert(doc(&[("v", 1)])).unwrap();
1738            db.flush().unwrap();
1739        }
1740        // Simulate a compaction that died before swapping in its temp file.
1741        let stale = compacting_path(&path);
1742        std::fs::write(&stale, b"garbage from an interrupted compaction").unwrap();
1743        assert!(stale.exists());
1744
1745        let db = Db::open(&path).unwrap();
1746        assert!(!stale.exists(), "open should remove the stale temp");
1747        assert_eq!(db.len(), 1);
1748        let _ = std::fs::remove_file(&path);
1749    }
1750}