pub struct Chisel { /* private fields */ }Expand description
A live handle to an open Chisel database.
Owns (transitively) the page cache and the current in-memory view of
the superblock roots. For file-backed databases it also owns the
exclusive flock; memory-backed databases (opened via
open_in_memory[_with_options]) have no lock because the Vec-backed
PageIo is itself the database and cannot be opened twice by
construction. Dropping a Chisel releases the page cache and closes
the underlying file, which in turn releases the flock on the file
path (the lock is tied to the file descriptor, so drop order is what
matters — not an explicit unlock call).
IMPORTANT: dropping without calling commit() on an in-flight transaction
discards that transaction. Shadow paging guarantees the on-disk state is
still the last committed state, not a partial write.
Poison model (see ISSUES.md I1): if any method returns a fatal error
(I/O failure, checksum mismatch, corrupt superblock, commit protocol
failure), the Chisel handle becomes poisoned. Every subsequent call
— including reads — returns ChiselError::Poisoned. The only legal
recovery is to drop this Chisel and call Chisel::open again; the
shadow-paging crash-recovery path on reopen returns the database to the
last durable state. This mirrors std::sync::Mutex poisoning and is
necessary because Linux fsync semantics (fsyncgate, 2018) do not
permit safely retrying a failed fsync — the kernel may have discarded
the dirty pages before reporting the error.
§Errors and poisoning
One rule is shared by every fallible method below: once the handle is
poisoned (above), the method returns ChiselError::Poisoned — reads
included. Each method’s own # Errors section therefore lists only the
operational (recoverable, non-poisoning) errors specific to that call;
it does not re-list Poisoned or the fatal I/O and corruption errors,
which are universal and all funnel into the poison model. Operational
errors leave the handle usable: fix the condition (or rollback) and
continue. The constructors (open, open_in_memory*) have no handle to
poison, so their errors are fully enumerated in place.
Implementations§
Source§impl Chisel
impl Chisel
Sourcepub fn open(path: &Path, options: Options) -> Result<Chisel>
pub fn open(path: &Path, options: Options) -> Result<Chisel>
Open or create a Chisel database at path.
The “exists” check deliberately treats a zero-length file as
nonexistent: a freshly-created-but-unwritten file (e.g. from a crash
between creat(2) and the first superblock write, or from a user
touch) has no valid superblock and must go through the
create_new path. Without this, open_existing would try to parse
an empty file and fail with a corruption error.
Acquires an exclusive flock on the file before any parsing, so a
second concurrent open() on the same path fails fast with
LockFailed rather than racing on the superblock.
§Errors
InvalidSuperblockCount (the superblock_count option is out of
range), FileNotFound (no file at path and create_if_missing is
false), or LockFailed (another handle holds the exclusive flock).
For an encrypted database: NoEncryptionKey (file is encrypted but
no encryption_key given), InvalidEncryptionKey (key unwraps no
key slot), or EncryptionNotSupported (key given for a plaintext
file). When reopening an existing file, parsing the superblock can
also yield UnsupportedFormatVersion, CorruptSuperblock,
ChecksumMismatch, FileSizeMismatch, or IoError.
Sourcepub fn open_in_memory() -> Result<Chisel>
pub fn open_in_memory() -> Result<Chisel>
Open a non-durable, memory-backed Chisel database. Intended for
benchmark comparisons against SQLite :memory: and for tests that
do not need filesystem persistence. All data is lost when the
returned Chisel is dropped.
Uses default Options. For a tuned cache size or superblock count,
use open_in_memory_with_options.
§Errors
Only a bootstrap IoError from the initial superblock write — the
memory backing makes this practically infallible. See
open_in_memory_with_options.
Sourcepub fn open_in_memory_with_options(options: Options) -> Result<Chisel>
pub fn open_in_memory_with_options(options: Options) -> Result<Chisel>
Open a memory-backed Chisel database with explicit options.
options.read_only must be false: a fresh memory database must
be writable for the initial superblock bootstrap, and there is no
prior file to reopen read-only. options.create_if_missing is
ignored — memory mode always creates a fresh database. All other
options (cache_max_bytes, spillway_max_bytes, drain_insertion,
superblock_count) flow through normally.
§Errors
ReadOnlyMode if options.read_only is set (a fresh memory database
must be writable to bootstrap), InvalidSuperblockCount if
options.superblock_count is out of range, or a bootstrap IoError.
Sourcepub fn close(self) -> Result<()>
pub fn close(self) -> Result<()>
Explicit close. Exists for API symmetry and so callers can observe a
Result at teardown; functionally identical to letting the value
drop, since release of the flock and file descriptor happens in
Drop. The Result return is currently always Ok, but is kept so
future implementations can surface fsync/close errors without a
breaking change.
I38 (ISSUES.md, 2026-05-22): #[must_use] with a custom message
so callers who drop the result without explicit let _ = … get
a lint warning. Result is already #[must_use] by default;
the custom message adds the human-readable rationale.
§Errors
Currently never — close always returns Ok. The Result is reserved
so a future release can surface fsync/close errors without an API break.
Sourcepub fn begin(&mut self) -> Result<()>
pub fn begin(&mut self) -> Result<()>
Begin a new transaction. All mutating operations below require an
active transaction; allocate/update/delete will return
NoActiveTransaction otherwise. Only one transaction is active at a
time — there is no nesting beyond savepoints.
§Errors
TransactionAlreadyActive if a transaction is already open.
Sourcepub fn commit(&mut self) -> Result<()>
pub fn commit(&mut self) -> Result<()>
Commit the active transaction. Performs three fsyncs before returning — this is the point at which changes become durable:
- I28 pre-drain flush.
TransactionManager::commit_innerpre-drains the cache beforepersist_freemapto keepCacheFulloff the commit path (see ISSUES.md I28). - Main data-pages flush.
PageCache::flushphase 2 issues one fsync that covers every in-cache write plus every drained-batch write. - Superblock fsync. The alternate-slot superblock is written and fsynced; this is the linearization point.
A crash before the superblock fsync leaves the previous
committed state intact — recovery picks the older superblock
via Superblock::select and the partially-written shadow
pages become unreachable garbage.
The spillway, when engaged, adds zero additional fsyncs to
this protocol (its content does not need to survive a crash).
tests/spillway_integration.rs::no_spill_workload_preserves_two_fsync_commit
pins the count to == 3; the test name retains the older
“two_fsync” label from the original spec.
§Errors
NoActiveTransaction if none is open. Operationally, CacheFull or
SpillwayFull if the transaction’s working set exceeds the cache /
spillway caps. A failure inside the fsync/superblock protocol is fatal
and poisons the handle — the previous committed state stays intact.
Sourcepub fn rollback(&mut self) -> Result<()>
pub fn rollback(&mut self) -> Result<()>
Abort the active transaction. Pages written during the transaction become unreachable garbage (they are never linked from a superblock), so rollback is effectively free — no undo log to replay.
§Errors
NoActiveTransaction if no transaction is open.
Sourcepub fn savepoint(&mut self, name: &str) -> Result<()>
pub fn savepoint(&mut self, name: &str) -> Result<()>
Create a named savepoint within the active transaction.
§Errors
NoActiveTransaction if no transaction is open; DuplicateSavepoint if
name is already a live savepoint in this transaction.
Sourcepub fn rollback_to(&mut self, name: &str) -> Result<()>
pub fn rollback_to(&mut self, name: &str) -> Result<()>
Roll the active transaction back to a named savepoint, discarding work done since (pages written meanwhile are abandoned, as in a full rollback).
§Errors
NoActiveTransaction if no transaction is open; SavepointNotFound if
name is not a live savepoint.
Sourcepub fn release(&mut self, name: &str) -> Result<()>
pub fn release(&mut self, name: &str) -> Result<()>
Discard a named savepoint without rolling back, folding its scope into the surrounding transaction.
§Errors
NoActiveTransaction if no transaction is open; SavepointNotFound if
name is not a live savepoint.
Sourcepub fn allocate(&mut self, value: &[u8]) -> Result<Handle>
pub fn allocate(&mut self, value: &[u8]) -> Result<Handle>
Store value and return a freshly minted stable handle. Handles are
u64 identifiers assigned from a monotonic counter in the superblock;
they are never reused within a database’s lifetime and are stable
across updates, defrag, and reopens. Physical location may change;
the handle will not.
Values up to transaction::MAX_INLINE_VALUE are packed into a slot
on a data page (R1 packing — multiple values share a page); larger
values are written to an overflow chain in overflow.rs. The
caller cannot tell which path was taken except by consulting
stats; all reads go through the same read() entry point.
§Errors
NoActiveTransaction if no transaction is open; CacheFull or
SpillwayFull if the value’s pages do not fit within the cache /
spillway caps.
Sourcepub fn allocate_tagged(&mut self, value: &[u8], tag: Tag) -> Result<Handle>
pub fn allocate_tagged(&mut self, value: &[u8], tag: Tag) -> Result<Handle>
Store value tagged with tag and return a freshly minted stable handle.
Like allocate, but additionally registers the handle in the reverse
membership index (tag→handles) so handles_with_tag(tag) can enumerate it.
Tag 0 is the “untagged” sentinel — prefer plain allocate for untagged
values; the membership index is not updated for tag 0.
§Errors
As allocate (NoActiveTransaction, CacheFull,
SpillwayFull); the reverse membership-index insert is subject to the
same cap errors.
Sourcepub fn tag(&self, handle: Handle) -> Result<Option<Tag>>
pub fn tag(&self, handle: Handle) -> Result<Option<Tag>>
Return the tag stored in the handle-table entry for handle.
Returns 0 for untagged handles. Takes &self (F3).
§Errors
InvalidHandle if handle is unknown or deleted.
Sourcepub fn client_byte(&self, handle: Handle) -> Result<u8>
pub fn client_byte(&self, handle: Handle) -> Result<u8>
Return the opaque client byte stored in the handle-table entry for
handle. Returns 0 for chunks whose byte was never set (including all
chunks created before this feature). Chisel never interprets it. Takes
&self (F3).
§Errors
InvalidHandle if handle is unknown or deleted.
Sourcepub fn set_client_byte(&mut self, handle: Handle, byte: u8) -> Result<()>
pub fn set_client_byte(&mut self, handle: Handle, byte: u8) -> Result<()>
Set the opaque client byte for handle. Requires an active
transaction; durable on commit, reverted on rollback.
§Errors
NoActiveTransaction if no transaction is open; InvalidHandle if
handle is unknown or deleted.
Sourcepub fn handles_with_tag(&self, tag: Tag) -> Result<Vec<Handle>>
pub fn handles_with_tag(&self, tag: Tag) -> Result<Vec<Handle>>
Enumerate all live handles that carry tag. Returns an empty Vec if
no handles with that tag exist. Tag 0 always returns an empty Vec
(the membership index is not updated for untagged values). Takes &self (F3).
Stability: the same within-session repeatability contract as handles —
repeated calls return an identical Vec while the set of live handles
carrying tag is unchanged and no defrag has run. The order is
unspecified and may differ after a reopen or defrag.
§Errors
Only on poisoning — an empty or absent index is not an error and returns
an empty Vec.
Sourcepub fn read(&self, handle: Handle) -> Result<Vec<u8>>
pub fn read(&self, handle: Handle) -> Result<Vec<u8>>
Read the current value for handle. Takes &self — the page cache
is mutated on miss (LRU bookkeeping, page loading) via interior
mutability (see F3 in ISSUES.md). The returned Vec<u8> is a copy;
the cache retains its own page. Not Sync — a Chisel is single-
threaded by design, so this &self only enables &self-taking
read APIs in downstream wrappers (e.g. the client’s StorageEngine
trait), not cross-thread sharing.
§Errors
InvalidHandle if handle is unknown or deleted. A structural
disagreement between the handle table and the data page surfaces as the
fatal CorruptPage, which poisons the handle.
Sourcepub fn update(&mut self, handle: Handle, value: &[u8]) -> Result<()>
pub fn update(&mut self, handle: Handle, value: &[u8]) -> Result<()>
Replace the value for handle. The handle is preserved; the value
is written to a new slot (and, if it crosses the inline threshold,
to a new overflow chain). The handle-table entry is rewritten via
COW, so the update is invisible until commit.
§Errors
NoActiveTransaction if no transaction is open; InvalidHandle if
handle is unknown or deleted; CacheFull or SpillwayFull if the new
value’s pages do not fit the caps.
Sourcepub fn delete(&mut self, handle: Handle) -> Result<()>
pub fn delete(&mut self, handle: Handle) -> Result<()>
Remove a handle. The handle itself is retired (not reused); any overflow pages it owned are queued for release on commit.
§Errors
NoActiveTransaction if no transaction is open; InvalidHandle if
handle is unknown or already deleted.
Sourcepub fn delete_tagged(&mut self, handle: Handle, tag: Tag) -> Result<()>
pub fn delete_tagged(&mut self, handle: Handle, tag: Tag) -> Result<()>
Remove a handle only if its tag equals tag. Returns
ChiselError::TagMismatch (leaving the chunk and membership index
untouched) if the stored tag differs. On success, delegates to
delete, so the membership index is self-maintained.
Use this when the caller wants to assert ownership (a stale or
mis-directed handle should not silently delete the wrong chunk).
delete remains the unchecked fast path for callers that trust
their handle provenance.
§Errors
NoActiveTransaction if no transaction is open; InvalidHandle if
handle is unknown or deleted; TagMismatch if the stored tag differs
from tag (nothing is deleted in that case).
Sourcepub fn delete_with_tag(
&mut self,
tag: Tag,
max: usize,
) -> Result<TagDropProgress>
pub fn delete_with_tag( &mut self, tag: Tag, max: usize, ) -> Result<TagDropProgress>
Delete up to max chunks carrying tag, returning the handles dropped
this pass and whether the tag is now fully drained (complete). Loop
begin -> delete_with_tag -> commit until complete for an incremental,
bounded-time relation drop. max == 0 is a no-op (complete == false).
§Errors
NoActiveTransaction if no transaction is open. A mid-pass error returns
only Err — the
TagDropProgress is NOT produced, so the set of handles already
dropped this pass is not reported and is unrecoverable from the return
value. Each individual delete is atomic (a non-fatal CacheFull/
SpillwayFull leaves that one chunk untouched in both the handle table
and the membership index — see the I-series / shadow-paging invariants),
so the in-transaction state after the error is always consistent: every
chunk dropped before the failure is fully tombstoned, the failed one is
untouched. The caller therefore has two safe recoveries — rollback()
(discard the whole pass) or commit() (keep the consistent partial
drop) — and can simply re-enumerate via handles_with_tag/re-run the
bounded loop to finish. A fatal error additionally poisons the manager
(drop and reopen). To learn exactly which handles were dropped, commit
in single-element passes (max == 1) and read each success’s progress.
Sourcepub fn delete_many(&mut self, handles: &[Handle]) -> Result<()>
pub fn delete_many(&mut self, handles: &[Handle]) -> Result<()>
Delete many handles in one transaction (ISSUES.md F1 / I12).
Motivating use case (from the primary Chisel client): bulk
operations like drop_table / drop_index_table need to remove
large handle sets without leaking pages. This is a convenience
wrapper around a loop of delete() calls inside the caller’s
active transaction — the atomicity guarantee comes from the
enclosing transaction, not from anything special in this method.
On error, partial progress remains visible in the current transaction: rollback or commit to decide whether the half-done batch should be kept.
§Errors
NoActiveTransaction if no transaction is open; InvalidHandle if any
handle in handles is unknown or already deleted (handles before the
failure remain deleted in the current transaction).
Sourcepub fn set_root_name(&mut self, name: &str, handle: Handle) -> Result<()>
pub fn set_root_name(&mut self, name: &str, handle: Handle) -> Result<()>
Bind name to handle in the named-root table (ISSUES.md F2).
Names are short mnemonic labels for long-lived handles — typically
one or two per database (e.g. a meta B-tree root). Requires an
active transaction; becomes durable on commit, reverts on
rollback/rollback_to. See TransactionManager::set_root_name for
validation rules and the fixed table-size limit.
§Errors
NoActiveTransaction if no transaction is open; InvalidRootName if
name violates the validation rules; RootNameTableFull if the
fixed-size table has no free slot.
Sourcepub fn get_root_name(&self, name: &str) -> Result<Option<Handle>>
pub fn get_root_name(&self, name: &str) -> Result<Option<Handle>>
Look up a named root. Returns Ok(None) if the name is not bound.
Reads see the transactional view (pending sets/clears are visible
inside an active transaction). Takes &self (F3).
§Errors
Only on poisoning — an unbound name returns Ok(None).
Sourcepub fn clear_root_name(&mut self, name: &str) -> Result<()>
pub fn clear_root_name(&mut self, name: &str) -> Result<()>
Remove a named root. No-op if the name is not bound. Requires an active transaction; becomes durable on commit.
§Errors
NoActiveTransaction if no transaction is open.
Sourcepub fn handles(&self) -> Result<Vec<Handle>>
pub fn handles(&self) -> Result<Vec<Handle>>
Enumerate all live handles. Walks the handle-table radix tree; cost
is proportional to the number of live handles, not to the historical
maximum. Takes &self for the same reason read does (F3).
Stability: within a single open instance, repeated calls return an
identical Vec — the same handles in the same order — as long as the
live set is unchanged between calls (changed only by allocate* /
delete*; read and update do not change it) and no defrag has run.
The order itself is unspecified: it is not sorted, not insertion order,
and may differ after a reopen or defrag, or across Chisel versions.
Rely on within-session repeatability; do not rely on the order.
§Errors
Only on poisoning (e.g. a fatal IoError while walking the handle table).
Sourcepub fn stats(&self) -> Result<Stats>
pub fn stats(&self) -> Result<Stats>
Summary statistics derived by scanning the current handle table and
querying the underlying file length. file_size_bytes is computed
from page_count * PAGE_SIZE rather than stat(2) so it reflects
the page-aligned view the engine has, not any trailing partial page
that might exist mid-extend.
§Errors
Only on poisoning — a fatal IoError while scanning the handle table or
reading the file length poisons the handle.
Sourcepub fn counters(&self) -> Result<ChiselCounters>
pub fn counters(&self) -> Result<ChiselCounters>
Snapshot the four engine-activity counters (cache hits/misses,
pages allocated, fsync calls). Cumulative from the most recent
open(); the bench harness reads-subtract-reads to compute
deltas for individual operations or workloads.
Same &self semantic-read shape as stats().
§Errors
Only on poisoning.
Sourcepub fn file_size_bytes(&self) -> Result<u64>
pub fn file_size_bytes(&self) -> Result<u64>
Page-aligned on-disk size of the database, computed as
page_count × PAGE_SIZE. Same number stats().file_size_bytes
returns, but without the handle-table scan that stats() does
to populate handle_count.
I53 (ISSUES.md, 2026-05-22): broken out for the bench harness,
which calls this per measurement cell — stats() walks all
live handles via handles() (O(live handles)), which adds
milliseconds per call at 100K-handle scale. Reading just
file_size_bytes shouldn’t pay that cost. stats() keeps its
current shape because the typical caller wants all three
fields together; this is the dedicated single-field accessor.
§Errors
Only on poisoning (a fatal IoError reading the file length).
Sourcepub fn is_poisoned(&self) -> bool
pub fn is_poisoned(&self) -> bool
Returns true if this database handle has been poisoned by a
previous fatal error. A poisoned handle returns
ChiselError::Poisoned from every operation; the caller must drop
it and reopen the database to recover. See the type-level docs for
the full recovery protocol.
Sourcepub fn defrag(&mut self, options: DefragOptions) -> Result<DefragStats>
pub fn defrag(&mut self, options: DefragOptions) -> Result<DefragStats>
Run a defragmentation pass. The caller must have an active
transaction (see defrag.rs for why). This method does NOT begin or
commit one on the caller’s behalf — defrag is composable with other
work in the same transaction and atomic with it on commit.
The freemap crash-orphan sweep (step 7) is SKIPPED while a savepoint is
active, since the sweep COWs the freemap and rollback_to does not
rewind the structural recycle streams. Run defrag outside any savepoint
scope to reclaim crash-orphaned freemap pages.
§Errors
NoActiveTransaction if no transaction is open; CacheFull if the
relocation working set exceeds the cache cap.
Sourcepub fn set_cache_max_bytes(&mut self, bytes: u64) -> Result<()>
pub fn set_cache_max_bytes(&mut self, bytes: u64) -> Result<()>
Resize the in-memory cache cap. Shrinking evicts clean LRU-tail entries to fit; growing takes effect on the next allocation. See spec §“Runtime mutability”.
§Errors
TransactionInProgress if a transaction is active.
Sourcepub fn set_spillway_max_bytes(&mut self, bytes: u64) -> Result<()>
pub fn set_spillway_max_bytes(&mut self, bytes: u64) -> Result<()>
Resize the spillway cap. Setting to 0 disables the spillway
(subsequent overflow trips CacheFull at the cache cap).
Returns ChiselError::TransactionInProgress if a transaction
is active. The spillway is empty between transactions, so
resize is state-free.
§Errors
TransactionInProgress if a transaction is active.
Sourcepub fn set_drain_insertion(&mut self, policy: DrainInsertion) -> Result<()>
pub fn set_drain_insertion(&mut self, policy: DrainInsertion) -> Result<()>
Update the drain insertion policy used at the next commit.
Returns ChiselError::TransactionInProgress if a transaction
is active.
§Errors
TransactionInProgress if a transaction is active.
Sourcepub fn add_key(&mut self, existing: &Key, new: &Key) -> Result<()>
pub fn add_key(&mut self, existing: &Key, new: &Key) -> Result<()>
Add a second credential that unlocks this database. existing must
already unlock it; new is wrapped over the same data key into a free
key slot. After this returns, either credential opens the database. O(1)
superblock commit — no page is re-encrypted.
§Errors
EncryptionNotSupported if the database has no encryption;
InvalidEncryptionKey if existing unlocks no slot; NoFreeKeySlot if
all 8 key slots are full. An fsync/superblock failure is fatal and poisons
the handle.
Sourcepub fn rotate_key(&mut self, old: &Key, new: &Key) -> Result<()>
pub fn rotate_key(&mut self, old: &Key, new: &Key) -> Result<()>
Replace old with new: new is added and old is revoked in one
atomic superblock commit. After this returns, old no longer opens the
database and new does. O(1) — the data key is unchanged, no page is
re-encrypted.
§Errors
EncryptionNotSupported if the database has no encryption;
InvalidEncryptionKey if old unlocks no slot; NoFreeKeySlot if all 8
key slots are full (no room to stage new before revoking old). An
fsync/superblock failure is fatal and poisons the handle.
Sourcepub fn remove_key(&mut self, key: &Key) -> Result<()>
pub fn remove_key(&mut self, key: &Key) -> Result<()>
Revoke the credential key. After this returns, key no longer opens
the database; any other credentials are unaffected. Refuses to remove
the only remaining credential. O(1) — the data key is unchanged, no
page is re-encrypted.
§Errors
EncryptionNotSupported if the database has no encryption;
InvalidEncryptionKey if key unlocks no slot; LastKeySlot if
key is the only active credential (removing it would make the database
permanently unopenable — nothing is changed). An fsync/superblock
failure is fatal and poisons the handle.