pub struct Db { /* private fields */ }Expand description
The embedded document database.
Db is Send + Sync; share across threads via Arc<Db> for
concurrent reader / single-writer access. See
design.md
§ “Concurrent readers, one writer”.
At M6 the public Db is hard-typed against
obj_core::FileHandle. A future refactor may make it generic
over F: FileBackend so fault-injection harnesses can build on
the same API; today the test helpers reach for the lower-level
obj-core building blocks instead.
Implementations§
Source§impl Db
impl Db
Sourcepub fn stat(&self) -> Result<DbStat>
pub fn stat(&self) -> Result<DbStat>
One-shot snapshot of header + catalog summary.
CLI-facing — used by obj stat. May move pre-1.0; user code
should prefer the typed Db::iter_all /
Db::read_transaction(|tx| tx.collection::<T>()) APIs.
§Errors
Error::Busyif the pager mutex is poisoned or contested.- Pager / B-tree / postcard errors propagated from the catalog walk + per-collection primary-tree walks.
Sourcepub fn dump_raw(&self, collection: &str, limit: usize) -> Result<DumpIter<'_>>
pub fn dump_raw(&self, collection: &str, limit: usize) -> Result<DumpIter<'_>>
Streaming type-erased walk of a named collection’s primary
B-tree. Each step yields a DumpRecord: the primary id,
the per-doc header (decoded), and the raw payload bytes.
limit == 0 is treated as unbounded; the caller’s iteration
loop is the implicit bound. Power-of-ten Rule 2: callers
who want a hard cap must impose it on their take(...).
CLI-facing — used by obj dump. The Document trait is
NOT consulted; schema-aware decode requires a registered
type and is the caller’s responsibility above this layer.
§Namespace dispatch (M11 #132) + snapshot isolation (#135)
A bare collection resolves against the calling Db’s own
env, but BOTH the catalog lookup and the primary-tree walk go
through the read txn’s pinned obj_core::ReaderSnapshot
(#135) — the scan is snapshot-isolated against concurrent
writers exactly as the point reads (get_via_snapshot) and the
attached path are; a writer’s post-snapshot node splits / page
reuse cannot surface as a spurious Corruption. A
"<namespace>.<tail>" name resolves against the read-only
database attached under <namespace>: the iterator pins one
obj_core::ReaderSnapshot on the attached env for its whole
lifetime and walks that env’s primary tree as-of the pinned LSN
— mirroring the namespace dispatch the point-read shims
(get_with_version etc.) gained in #123, so all() /
query.fetch() over an attachment see the same documents the
namespaced point reads do.
§Errors
Error::CollectionNotFoundifcollectionis not registered AT THE SNAPSHOT’S PINNED LSN.Error::CollectionNamespaceUnknownifcollectioncarries a namespace prefix that is not attached on this handle.- As
Db::read_transaction(construction-time).
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>
Open or create a file-backed database at path with default
configuration.
Creates the file if absent; reopens otherwise. A Db is
Send + Sync — share across threads via Arc<Db> for the
concurrent-reader / single-writer workload documented in
docs/concurrency.md.
For an ephemeral database use Db::memory; for a
read-only handle that coexists with another process’s writer
use Db::open_readonly; for custom durability /
cache / lock knobs use Db::open_with + Config.
§Examples
use obj::Db;
let dir = tempfile::tempdir()?;
// File-backed. Creates the file if absent; reopens otherwise.
let _db = Db::open(dir.path().join("app.obj"))?;
// In-memory. No persistence, no file locks. Useful for tests.
let _mem = Db::memory()?;
// Read-only. Coexists safely with a writer in another process.
// Every mutating call returns `Err(Error::ReadOnly { ... })`.
let _ro = Db::open_readonly(dir.path().join("app.obj"))?;§Errors
Returns the underlying Error from
obj_core::pager::Pager::open on syscall or format
failure.
Sourcepub fn memory() -> Result<Self>
pub fn memory() -> Result<Self>
Open a fresh in-memory database. No persistence, no file locks. Useful for unit tests and ephemeral workloads.
§Errors
Returns Error::InvalidArgument only if config has
zero cache frames.
Sourcepub fn memory_with(config: Config) -> Result<Self>
pub fn memory_with(config: Config) -> Result<Self>
Sourcepub fn open_readonly<P: AsRef<Path>>(path: P) -> Result<Self>
pub fn open_readonly<P: AsRef<Path>>(path: P) -> Result<Self>
Sourcepub fn transaction<R, F>(&self, body: F) -> Result<R>
pub fn transaction<R, F>(&self, body: F) -> Result<R>
Run a closure inside a write transaction.
Begins a WriteTxn, runs the closure with &mut tx. If
the closure returns Ok(r), the transaction is committed and
Ok(r) is returned. If the closure returns Err(e), the
transaction is rolled back and Err(e) is returned. A
panic inside the closure unwinds with an implicit rollback
via the WriteTxn Drop impl. See
docs/concurrency.md
for the lock-acquisition contract.
§Examples
Atomic batch — both inserts commit together or not at all:
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, obj::Document)]
struct Order {
customer_id: u64,
total_cents: u64,
}
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("txn.obj"))?;
let (a, b) = db.transaction(|tx| {
let coll = tx.collection::<Order>()?;
let a = coll.insert(Order { customer_id: 1, total_cents: 50 })?;
let b = coll.insert(Order { customer_id: 2, total_cents: 200 })?;
Ok((a, b))
})?;
assert_ne!(a, b, "freshly-allocated ids are distinct");Returning Err(_) rolls every staged write back; the Err
the closure returns is the Err the caller sees:
use obj::{Db, Error};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, obj::Document)]
struct Order { total_cents: u64 }
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("rollback.obj"))?;
let id = db.insert(Order { total_cents: 10 })?;
let outcome: obj::Result<()> = db.transaction(|tx| {
let coll = tx.collection::<Order>()?;
coll.update(id, |o| { o.total_cents = 99_999; })?;
Err(Error::InvalidArgument("synthetic abort"))
});
assert!(matches!(outcome, Err(Error::InvalidArgument(_))));
let after: Order = db
.get::<Order>(id)?
.ok_or(Error::InvalidArgument("just inserted"))?;
assert_eq!(after.total_cents, 10, "rolled-back update is invisible");§Errors
Error::ReadOnlyif the database was opened read-only.Error::Busyif a sibling transaction holds the lock(s).- Any error the closure returns.
Sourcepub fn read_transaction<R, F>(&self, body: F) -> Result<R>
pub fn read_transaction<R, F>(&self, body: F) -> Result<R>
Run a closure inside a read transaction. See
Self::transaction for the closure shape and atomicity
contract; reads inside body observe a consistent
snapshot of the database.
Every read inside the closure observes a single consistent snapshot — the snapshot is pinned at the moment the closure begins. Concurrent writers do not affect what the closure sees.
§Examples
Two reads inside one read_transaction see the same value
even if a writer commits in between:
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, obj::Document)]
struct Order { total_cents: u64 }
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("read.obj"))?;
let id = db.insert(Order { total_cents: 10 })?;
let (a, b) = db.read_transaction(|tx| {
let coll = tx.collection::<Order>()?;
let a = coll.get(id)?;
let b = coll.get(id)?;
Ok((a, b))
})?;
assert_eq!(a, b);§Errors
Error::Busyif the reader lock could not be acquired.- Any error the closure returns.
Sourcepub fn attach<P: AsRef<Path>>(
&mut self,
path: P,
namespace: impl Into<String>,
) -> Result<()>
pub fn attach<P: AsRef<Path>>( &mut self, path: P, namespace: impl Into<String>, ) -> Result<()>
Attach the database at path under namespace. The
attached file is opened read-only; collections in the
attached file become visible to subsequent
Db::collection::<T>() calls (and the one-shot per-op API
— Db::get::<T>(), etc.) when T::COLLECTION is of the
form <namespace>.<collection_name>.
Writes against namespaced collections return
Error::AttachedDatabaseIsReadOnly.
Each attached database gets its own snapshot pinned at
read-transaction begin; Db::detach removes the registry
entry but in-flight reads complete against their pinned
snapshot.
§Examples
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
#[obj(collection = "orders_attach_doc")]
struct Order { total_cents: u64 }
// Same struct shape, namespaced collection name. Reads
// against this type route to the attached "archive" db.
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
#[obj(collection = "archive.orders_attach_doc")]
struct ArchivedOrder { total_cents: u64 }
let dir = tempfile::tempdir()?;
let live = dir.path().join("live.obj");
let archive = dir.path().join("archive.obj");
// Seed the archive (writes go through its own un-namespaced type).
{
let archive_db = Db::open(&archive)?;
let _ = archive_db.insert(Order { total_cents: 999 })?;
}
// Open the live db, attach the archive under "archive".
let mut db = Db::open(&live)?;
let _ = db.insert(Order { total_cents: 100 })?;
db.attach(&archive, "archive")?;
// One read transaction, two collections.
db.read_transaction(|tx| {
let live = tx.collection::<Order>()?;
let arch = tx.collection::<ArchivedOrder>()?;
assert_eq!(live.all()?.len(), 1);
assert_eq!(arch.all()?.len(), 1);
Ok(())
})?;
db.detach("archive")?;§Errors
Error::AttachmentAlreadyExistsifnamespaceis in use on thisDb.Error::AttachmentNotReadableifpathcannot be opened read-only.
Shared-reference (&self) form of Self::attach. Attaches
the database at path under namespace through &self, for
callers that hold a shared handle (e.g. an Arc<Db>) and
cannot obtain &mut self.
Behaviour is identical to Self::attach: the attachment
registry is interior-mutable, guarded by the same per-Db
mutex, so concurrent attach_shared / detach_shared calls
from multiple threads serialise on that mutex. The duplicate-
namespace re-check after the read-only open closes the race
between two concurrent attaches of the same namespace.
§Examples
use std::sync::Arc;
use obj::Db;
let dir = tempfile::tempdir()?;
let live = dir.path().join("live.obj");
let archive = dir.path().join("archive.obj");
let _ = Db::open(&archive)?;
// A shared handle cannot call `&mut self` `attach`, but can
// call `attach_shared`.
let db = Arc::new(Db::open(&live)?);
db.attach_shared(&archive, "archive")?;
db.detach_shared("archive")?;§Errors
Error::AttachmentAlreadyExistsifnamespaceis in use on thisDb.Error::AttachmentNotReadableifpathcannot be opened read-only.Error::Busyif the registry mutex is poisoned.
Sourcepub fn detach(&mut self, namespace: &str) -> Result<()>
pub fn detach(&mut self, namespace: &str) -> Result<()>
Remove the attachment registered under namespace. Returns
Error::CollectionNamespaceUnknown if the namespace is
not attached.
In-flight read transactions hold their own snapshot pins on the attached env; detach removes the registry entry, but the in-flight read may still complete against its pinned snapshot.
§Errors
Error::CollectionNamespaceUnknownifnamespaceis not attached.Error::Busyif the registry mutex is poisoned.
Shared-reference (&self) form of Self::detach. Removes the
attachment registered under namespace through &self, for
callers that hold a shared handle (e.g. an Arc<Db>).
Behaviour is identical to Self::detach. In-flight read
transactions hold their own snapshot pins on the attached env;
detach_shared removes the registry entry, but the in-flight
read may still complete against its pinned snapshot. Concurrent
attach_shared / detach_shared calls serialise on the same
per-Db registry mutex.
§Errors
Error::CollectionNamespaceUnknownifnamespaceis not attached.Error::Busyif the registry mutex is poisoned.
Sourcepub fn insert<T: Document>(&self, doc: T) -> Result<Id>
pub fn insert<T: Document>(&self, doc: T) -> Result<Id>
Insert doc into its collection. One-shot transaction;
returns the assigned Id.
The one-shot API opens, commits, and closes a private
transaction per call. Reach for Db::transaction when
several mutations must commit or roll back as a single
atomic unit.
§Examples
One-shot CRUD against a Document-derived type:
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
struct Order {
customer_id: u64,
total_cents: u64,
status: String,
}
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("oneshot.obj"))?;
// insert returns the freshly-allocated Id.
let id = db.insert(Order {
customer_id: 1,
total_cents: 100,
status: "pending".to_owned(),
})?;
// get returns Option<T>.
let _maybe: Option<Order> = db.get::<Order>(id)?;
// update applies a closure in place.
db.update::<Order, _>(id, |o| {
o.status = "shipped".to_owned();
})?;
// upsert at a caller-supplied id (insert or replace).
let id2 = obj::Id::try_new(42)
.ok_or(obj::Error::InvalidArgument("non-zero"))?;
db.upsert::<Order>(id2, Order {
customer_id: 2,
total_cents: 999,
status: "new".to_owned(),
})?;
// delete returns true if the row existed.
let existed = db.delete::<Order>(id)?;
assert!(existed);§Errors
As Self::transaction plus any error from
crate::Collection::insert.
Sourcepub fn get<T: Document>(&self, id: Id) -> Result<Option<T>>
pub fn get<T: Document>(&self, id: Id) -> Result<Option<T>>
Fetch the document at id. Returns Ok(None) if absent.
§Errors
As Self::read_transaction plus any error from
crate::Collection::get.
Sourcepub fn update<T, F>(&self, id: Id, f: F) -> Result<()>
pub fn update<T, F>(&self, id: Id, f: F) -> Result<()>
Update the document at id via the closure.
§Errors
Error::DocumentNotFoundifiddoes not exist.- As
Self::transaction.
Sourcepub fn find_unique<T: Document>(
&self,
index_name: &str,
key: impl Into<Dynamic>,
) -> Result<Option<T>>
pub fn find_unique<T: Document>( &self, index_name: &str, key: impl Into<Dynamic>, ) -> Result<Option<T>>
Convenience wrapper around crate::Collection::find_unique
— db.find_unique::<Customer>("by_email", "ada@example.com").
Runs inside a one-shot read transaction.
O(log n), no collection scan; the lookup walks the named
index’s B-tree directly. Defined only on Unique indexes —
for the other kinds use
Collection::lookup or
Collection::index_range.
§Examples
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
#[obj(collection = "customers_find_unique_doc")]
struct Customer {
#[obj(index = unique)]
email: String,
}
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("find-unique.obj"))?;
let _ = db.insert(Customer { email: "ada@example.com".to_owned() })?;
let by_email: Option<Customer> = db
.find_unique::<Customer>("email", "ada@example.com")?;
assert!(by_email.is_some());§Errors
Sourcepub fn query<T: Document + Send + 'static>(&self) -> Query<'_, T>
pub fn query<T: Document + Send + 'static>(&self) -> Query<'_, T>
Construct a fresh M8 crate::Query builder rooted at this
database. The builder borrows &self for the build phase;
the borrow ends when crate::Query::fetch returns.
Compose with Query::filter,
Query::limit,
Query::sort_by,
Query::index_range. Terminate
with Query::fetch (for the
documents) or Query::count (for the
count alone).
Mirrors design.md
§ Querying — see the M8 examples in
crates/obj/tests/design_md_queries.rs
for the full surface.
§Examples
Top-N matching documents by an indexed field, ascending:
use obj::Db;
use obj_core::codec::Dynamic;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
enum OrderStatus { Pending, Shipped }
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
#[obj(collection = "orders_query_doc")]
struct Order {
#[obj(index)]
customer_id: u64,
status: OrderStatus,
#[obj(index)]
placed_at: u64,
}
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("queries.obj"))?;
for i in 0..20u64 {
let _ = db.insert(Order {
customer_id: i % 3,
status: if i % 2 == 0 { OrderStatus::Pending } else { OrderStatus::Shipped },
placed_at: i * 1_000,
})?;
}
let pending: Vec<Order> = db
.query::<Order>()
.filter(|o| o.status == OrderStatus::Pending)
.sort_by(|o| Dynamic::U64(o.placed_at))
.limit(5)
.fetch()?;
assert!(pending.len() <= 5);
assert!(pending.iter().all(|o| o.status == OrderStatus::Pending));Sourcepub fn collection<T: Document + Send + 'static>(
&self,
name: impl Into<String>,
) -> Collection<'_, T>
pub fn collection<T: Document + Send + 'static>( &self, name: impl Into<String>, ) -> Collection<'_, T>
Open a read-only typed handle to the collection registered
under the runtime name, instead of the type’s compile-time
T::COLLECTION.
This unlocks the
design.md
§ Portability example for
attached databases: by passing a namespaced name like
"archive.orders", the returned crate::Collection reads
from the database attached under the "archive" namespace
— see Db::attach.
Construction is infallible: errors (missing collection,
unknown namespace, busy lock) surface at the first method
call on the handle, not at the call to collection(name).
Each read-only method on the returned handle opens a private
Db::read_transaction and dispatches against the
runtime-named collection’s catalog row.
§Read-only
The returned handle rejects every mutating call —
crate::Collection::insert, update, delete, upsert
all return Error::ReadOnly. To write into a non-default
collection, override obj_core::Document::COLLECTION on
the type itself (compile-time-bound) and use the regular
Db::transaction / crate::WriteTxn::collection path.
Phase 1B (M11 #94) intentionally limits the runtime accessor
to reads — the write-through-runtime-name path requires
engine plumbing that is deferred to a later milestone.
§Example
use obj::{Db, Document};
use serde::{Deserialize, Serialize};
use tempfile::tempdir;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Order { customer_id: u64, total_cents: u64 }
impl Document for Order {
const COLLECTION: &'static str = "orders";
const VERSION: u32 = 1;
}
fn run() -> obj::Result<()> {
let dir = tempdir()?;
let archive_path = dir.path().join("archive.obj");
// Populate the archive database first.
{
let archive_db = Db::open(&archive_path)?;
archive_db.insert(Order { customer_id: 1, total_cents: 999 })?;
}
// Attach it under a namespace and read via the runtime
// accessor — no need to re-declare `Order` with a
// namespaced `COLLECTION`.
let main_path = dir.path().join("main.obj");
let mut db = Db::open(&main_path)?;
db.attach(&archive_path, "archive")?;
let archived: Vec<Order> = db
.collection::<Order>("archive.orders")
.all()?
.into_iter()
.map(|(_id, doc)| doc)
.collect();
assert_eq!(archived.len(), 1);
Ok(())
}Sourcepub fn all<T: Document + Send + 'static>(&self) -> Result<Vec<T>>
pub fn all<T: Document + Send + 'static>(&self) -> Result<Vec<T>>
Convenience shim mirroring
design.md —
for order in db.all::<Order>()? { ... }. Returns an owned
Vec<T> (materialised). One-line shim over Db::iter_all
that drives the streaming iterator to exhaustion and
collects; if the collection is large enough that peak
memory matters, prefer Db::iter_all directly.
Sorting requires materialisation — the comparator needs
every key up front. Use Db::query +
Query::sort_by for the
top-N-sorted workload; the iterator side has no streaming
sorted shape.
§Examples
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, obj::Document)]
struct Order { total_cents: u64 }
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("all.obj"))?;
for i in 0..5u64 {
let _ = db.insert(Order { total_cents: i * 10 })?;
}
let listed: Vec<Order> = db.all::<Order>()?;
assert_eq!(listed.len(), 5);§Errors
As Db::iter_all.
Sourcepub fn backup_to<P: AsRef<Path>>(&self, dest: P) -> Result<()>
pub fn backup_to<P: AsRef<Path>>(&self, dest: P) -> Result<()>
Write a self-contained .obj file at dest carrying this
database’s state at the LSN of an internally-taken reader
snapshot.
Hot backup — writers continue uninterrupted against the source. Post-snapshot writes are NOT in the destination.
Algorithm (per
docs/format.md
§ Hot backup):
- Take a
ReaderSnapshotagainst the source pager (pinspinned_lsn). OpenOptions::create_new(true)ondest.- Copy main-file pages
0..page_counttodest. - Overlay every frame in the snapshot’s frozen WAL view
onto
destat the frame’s page-id offset. - If the snapshot carries a WAL-staged page-0 header,
overlay it (so
dest’s page-0 reflects the catalog root / freelist head / page count the snapshot would have observed). - Patch
dest’s page-0 header: zerowal_salt, recompute the header CRC32C. sync_data(SyncMode::Full)ondest.- Drop the snapshot (releases the WAL pin).
On any mid-backup error the destination file is removed best-effort so a half-written backup does not linger.
§Examples
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
#[obj(collection = "notes_backup_doc")]
struct Note { body: String }
let dir = tempfile::tempdir()?;
let src = dir.path().join("src.obj");
let dst = dir.path().join("backup.obj");
let db = Db::open(&src)?;
let _ = db.insert(Note { body: "before backup".to_owned() })?;
db.backup_to(&dst)?;
// The backup is itself a fully-formed obj file. Open it and read.
let backup = Db::open(&dst)?;
let listed: Vec<Note> = backup.all::<Note>()?;
assert_eq!(listed.len(), 1);§Errors
Error::BackupDestinationExistsifdestalready exists.Error::BackupNotSupportedForMemoryPagerwhen called on aDbconstructed viaDb::memory/Db::memory_with.Error::Ioon syscall failure during the copy.
Sourcepub fn checkpoint(&self) -> Result<()>
pub fn checkpoint(&self) -> Result<()>
Fold committed WAL pages into the main .obj file and reset
the WAL back to its 64-byte header.
Wraps obj_core::pager::Pager::checkpoint. Acquires the
pager lock the same way Self::backup_to does, then calls
the pager checkpoint. After it returns, the committed records
live in the main file rather than the -wal sidecar, and the
WAL is truncated to header-only.
§Deferred / no-op behavior
- If a live MVCC reader has pinned an LSN below the end of the WAL, the checkpoint is deferred (a safe no-op) so the reader’s frames are not reclaimed out from under it.
- If there is nothing to fold (no committed WAL frames), the call is a harmless no-op.
This is the reusable engine entry point behind the Python
Db.checkpoint() binding and a future checkpoint-on-close
path (issue #5).
§Errors
Error::ReadOnlyif the database was opened read-only.Error::Busyif the pager lock is poisoned.- Any
Errorfromobj_core::pager::Pager::checkpoint(e.g.Error::Ioon syscall failure).
Sourcepub fn iter_all<T: Document + Send + 'static>(&self) -> Result<IterAll<'_, T>>
pub fn iter_all<T: Document + Send + 'static>(&self) -> Result<IterAll<'_, T>>
Streaming iterator over every (Id, T) pair in the
collection. The returned IterAll holds a read transaction
— and therefore a pinned reader snapshot — for its entire
lifetime; the borrow on self ends when the iterator is
dropped.
Each next call yields Result<(Id, T)>. Per-doc decode
errors surface as Some(Err(_)) rather than ending the
iteration; the caller decides whether to propagate or
continue.
Peak memory does NOT scale with collection size — the
iterator’s internal buffer is fixed at
ITER_ALL_BATCH = 256 entries (~128 KiB at the
design.md
~512 byte/doc estimate). Power-of-ten Rule 3.
Query::fetch is sort-compatible (sort requires
materialisation); iter_all is NOT — there is no streaming
shape for sort because the comparator needs every key
up front. Use Query::sort_by + fetch for the sorted
workload; use iter_all for the unsorted large-scan
workload.
§Examples
Streaming a small collection and folding into a running sum. The iterator’s peak memory stays bounded regardless of how many documents the collection holds:
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, obj::Document)]
struct Order { total_cents: u64 }
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("iter.obj"))?;
for i in 0..5u64 {
let _ = db.insert(Order { total_cents: i * 10 })?;
}
let mut total: u64 = 0;
for step in db.iter_all::<Order>()? {
let (_id, doc) = step?;
total = total
.checked_add(doc.total_cents)
.ok_or(obj::Error::InvalidArgument("overflow"))?;
}
assert_eq!(total, 0 + 10 + 20 + 30 + 40);§Errors
- As
Db::read_transaction(construction-time). Error::CollectionNotFoundif the collection is not yet registered at the snapshot’s pinned LSN.- Per-step iteration may yield
Some(Err(_))for pager, B-tree, or codec failures.
Source§impl Db
impl Db
Sourcepub fn integrity_check(&self) -> Result<IntegrityReport>
pub fn integrity_check(&self) -> Result<IntegrityReport>
Run the on-demand full integrity walk and return a structured
IntegrityReport.
The walk:
- Opens a read snapshot (does NOT block writers).
- Walks the catalog B-tree and every
Activecollection’s primary + index B-trees, validating per-page CRCs, sort invariants, depth and sibling-chain invariants. - Cross-references each
Activeindex against its primary: every index entry must point at an extant primary id, and every primary id must be referenced by at least one entry in each non-EachActiveindex. - Sweeps the freelist chain.
- Compares the set of reachable pages to
0..page_count, emittingIntegrityFailure::OrphanPagefor each unreferenced page id.
I/O failures during the walk surface as Err(_); content-
level violations are accumulated into
report.failures and the walk continues.
A lighter-weight catalog-only walk runs automatically at
Db::open time; opt out via
Config::skip_open_check.
§Examples
use obj::Db;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, obj::Document)]
#[obj(collection = "users_integrity_doc")]
struct User { email: String }
let dir = tempfile::tempdir()?;
let db = Db::open(dir.path().join("check.obj"))?;
for i in 0..16u32 {
let _ = db.insert(User { email: format!("u{i}@example.com") })?;
}
let report = db.integrity_check()?;
assert!(report.is_ok(), "clean db must pass: {:?}", report.failures);
assert!(report.pages_checked > 0);§Errors
Error::Ioon cache-miss read failure during the walk.Error::Busyif the pager mutex is poisoned.- Pager / B-tree errors propagated from the catalog walk.