Skip to main content

Chisel

Struct Chisel 

Source
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn commit(&mut self) -> Result<()>

Commit the active transaction. Performs three fsyncs before returning — this is the point at which changes become durable:

  1. I28 pre-drain flush. TransactionManager::commit_inner pre-drains the cache before persist_freemap to keep CacheFull off the commit path (see ISSUES.md I28).
  2. Main data-pages flush. PageCache::flush phase 2 issues one fsync that covers every in-cache write plus every drained-batch write.
  3. 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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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).

Source

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.

Source

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).

Source

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.

Source

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).

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Auto Trait Implementations§

§

impl !Freeze for Chisel

§

impl !RefUnwindSafe for Chisel

§

impl !Sync for Chisel

§

impl Send for Chisel

§

impl Unpin for Chisel

§

impl UnsafeUnpin for Chisel

§

impl UnwindSafe for Chisel

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.