Expand description
Moraine brings a SlateDB backend to DuckLake: a DuckLake catalog implemented on a transactional KV store over object storage, instead of the usual relational catalog database.
Exactly one process holds a read-write Catalog per store at a time
(opening a second fences the first); any number of processes may read
snapshots concurrently. Schemas, tables, views, data files, and
statistics all commit through the same transaction; catalog options
live outside the snapshot protocol (last-write-wins, no snapshot
minted).
§A worked example
Open a catalog on a store, evolve its schema through commits, and read both the current state and any past snapshot:
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default()).await?;
let v1 = catalog
.commit(|tx| {
let sales = tx.create_schema("sales")?;
tx.create_table(
sales,
"orders",
&[ColumnDef {
name: "id".into(),
column_type: "BIGINT".into(),
nulls_allowed: false,
default_value: None,
children: Vec::new(),
}],
)?;
Ok(())
})
.await?;
// The head sees the latest shape (plus the bootstrap-minted `main`)...
let head = catalog.snapshot().await?;
assert_eq!(head.schemas().len(), 2);
// ...while `v1` remains queryable by time travel, forever.
let past = catalog.snapshot_at(v1).await?;
assert_eq!(past.schemas().len(), 2);See examples/walkthrough.rs for a longer tour that also alters and
renames a table across a second commit. Data files register through the
same Catalog::commit path, minting a snapshot without bumping the
schema version.
§Inlined data
A small insert need not cost a Parquet file. Transaction::inline_insert
stores a chunk of rows in the catalog itself — Arrow IPC bytes the caller
encodes, carried by the same single write the commit already performs —
and the rows draw ids from the table’s row-id counter exactly as a
registered file’s do. ReadOnlyCatalog::recent_rows reads them back — or
ReadOnlyCatalog::recent_rows_at, for the rows a past snapshot saw —
Transaction::inline_delete tombstones one, and
Transaction::flush_inlined_data drains them into a data file the
caller wrote, preserving their ids and backdating the file’s record so
time travel across the flush still finds them exactly once.
§Readers
Catalog::open_read_only opens the same store without becoming its
writer, so it never fences one. What it hands back is a
ReadOnlyCatalog, which carries the reads and no mutator at all — so
committing through a read-only handle is a compile error rather than a
runtime one, and a Catalog serves the same reads by dereferencing to
it. Read-only comes in two forms, and the difference is what the
reader’s credentials must be allowed to do.
The default follows the latest state: it sees every commit as it lands, and pays for that by writing a checkpoint into the manifest on open and refreshing it while it lives. The other form pins the reader to a checkpoint minted in advance, and writes nothing whatsoever — for a deployment whose readers hold strictly read-only credentials:
catalog.commit(|tx| tx.create_schema("sales").map(|_| ())).await?;
let checkpoint = catalog.create_checkpoint(None).await?;
let mut options = CatalogOptions::default();
options.checkpoint = Some(checkpoint);
let reader = Catalog::open_read_only(object_store, options).await?;
assert!(reader.snapshot().await?.schema_by_name("sales").is_some());A pinned reader reads that cut and no other, however long it stays open
— a commit made after the checkpoint is invisible to it. The checkpoint
also pins the objects it references against SlateDB’s own collection
until it expires or Catalog::delete_checkpoint removes it, so give
one a lifetime unless something else will.
§Maintenance
Dropping an equality index (or a table that has one) makes its entries
invisible immediately but does not delete them, so the range would leak
without a sweep. Catalog::maintain reclaims those ranges:
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default()).await?;
let report = catalog.maintain(MaintenanceRequest::default()).await?;
// Nothing has been dropped, so there is nothing to reclaim.
assert_eq!(report.indexes_swept, 0);A pass mints no snapshot and leaves head unchanged, and deletes in
bounded batches (MaintenanceRequest::batch_size) so a large
reclamation never holds the writer. It is safe to interrupt: an
abandoned pass leaves a smaller one for next time, never a torn one.
Deciding what is dead rests on catalog ids never being reused, so a
sweep can run alongside a live writer without coordinating with it.
Beneath the catalog, the store itself accumulates. Every key lives in
a subspace, each subspace is its own tree, and overwriting a key leaves
the old version readable-through until a merge rewrites the SSTs
holding it — which the substrate’s own scheduler only does under write
pressure, so a store that goes quiet keeps its dead weight
indefinitely. ReadOnlyCatalog::store_census says where the weight is,
from the store’s manifest alone:
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default()).await?;
catalog.commit(|tx| tx.create_schema("sales").map(|_| ())).await?;
let census = catalog.store_census(CensusRequest::default()).await?;
// Every subspace is reported, so two censuses compare row by row.
assert!(census.subspaces.iter().any(|s| s.subspace == SubspaceName::Current));
// ...and the merge collapses a subspace's sorted runs into one.
let report = catalog.compact_store(CompactStoreRequest::default()).await?;
assert!(!report.merges.is_empty());The census costs a manifest read plus one listing and is available
read-only; its per-subspace figures count SSTs that have been written
out, so a commit still in the write-ahead log is in none of them. The
listing is what catches the rest: StoreCensus::unaccounted_bytes
reports what the object store holds outside those SSTs, and a large
figure there means a slow attach no merge will fix — an unpinned reader
replays the log before it materializes anything. Ask for
CensusRequest::count_live_entries and it also scans, which costs a
full read of the store and is the only way to learn what fraction of a
subspace is live. Catalog::compact_store then merges: moraine picks
no sources and no destination, and the plan the substrate makes for a
whole tree is what permits dropping a tombstone rather than carrying it
forward. It needs the writer, since the compactor that executes a merge
runs inside it.
Snapshot expiry, file cleanup, and data-file compaction all belong to
DuckLake and run through its own functions; the SlateDB store collects
its superseded objects itself, unprompted. A host embedding this crate
calls these verbs on whatever cadence it likes — the crate spawns no
threads and schedules nothing. Under the DuckDB extension, all of it is
sequenced for you; see that crate’s docs for moraine_maintenance,
moraine_store_census, and moraine_compact_store.
§Format migration
A store records the structural layout version it was written at. A binary opens any version it understands and refuses one it does not, rather than misreading it. Most version bumps are additive — a new subspace, no existing key moved — and need no rewrite: the store is stamped forward the first time it uses the newer feature, and older stores stay readable.
A bump that moves existing keys does need a rewrite, and
Catalog::migrate performs it: a one-way, crash-resumable pass that
takes the single writer, walks the keyspace under a durable cursor, and
flips the version stamp in one final atomic batch. It is a separate verb
from opening a catalog, so the operator chooses when to pay for it. While
it runs, every reader refuses the store.
No such rewrite exists yet, so migrate reports a no-op against every
store this release can open.
§Features
fault-injection— compiles the crash-injection seams the migration driver consults between its durable batches, and exposesCrashPoint,inject_crash,SyntheticMigration,install_migration, andCrashCaseso a test can install a migration unit and stop it at a named durable boundary. Off by default; a build without it carries no fault surface.fuzzing— exposes the codec and read-path decode entry points thefuzz/targets drive. They live in their own crate and so cannot reach the crate-private codecs; nothing else needs this. Off by default.
§Diagnostics
The crate emits tracing events and installs
no subscriber. Rare, consequential moments log at info (an open, the
once-ever bootstrap, a format-stamp upgrade) or warn (a spent retry
budget, an oversized commit refused); per-commit summaries and retry
traces log at debug. Every event is per-operation — never per row,
entry, or file. An embedding host sees them by installing any
subscriber. The DuckDB extension consumes its own — it cannot share the
host’s — and forwards them to duckdb_logs; see that crate’s logging
module.
§Layering
catalog— the DuckLake domain model. Never touches SlateDB directly.store— the SlateDB layer: key layout and value codecs. Knows nothing about DuckLake semantics.transaction— the commit protocol turning a catalog transaction into an atomic store write.
Structs§
- Build
Step - How much one step of a staged index build may commit. A step ends at
whichever bound it reaches first, and always carries at least one
entry.
entriesbounds one step’s entry buffer; derivation holds the step being filled and the one committing, plus a fixed window of decoded source batches read ahead.bytesbounds the single object-store request a batch becomes. - Cache
Identity - The object namespace shared by cache users. Equal identities assert that the same object path names the same immutable contents.
- Cache
Status - Current size and pressure of the process-shared caches. The disk figure is a configured capacity, not live occupancy.
- Cache
Tally - What the cache has served, by tier.
cache_tallyreports the process’s;ReadOnlyCatalog::cache_tallyone attach’s. - Catalog
- A read-write handle to a moraine catalog: cheap to clone, drives reads and commits.
- Catalog
Options - Options for opening a catalog.
- Catalog
Snapshot - An immutable catalog view at one snapshot.
- Census
Request - How a
ReadOnlyCatalog::store_censuscall should run. - Column
Alteration - A change to one column.
Nonefields leave the attribute untouched;default_valueuses a nestedOptionso “clear the default” (Some(None)) is distinct from “leave it” (None). - Column
Def - A column definition: the input to table creation and column addition.
- Column
Id - Identifies a column within its table (a field id: stable across renames and never reused, even after the column is dropped).
- Column
Info - A live column: its definition plus identity and position.
- Column
Order - The sort order of one indexed column: its direction and NULL placement. Defaults to ascending, NULLS LAST.
- Column
Stats - A column’s table-level statistics. Min/max are opaque strings, stored verbatim — moraine never interprets or merges them.
- Compact
Store Report - What a store merge did, subspace by subspace.
- Compact
Store Request - How a
Catalog::compact_storecall should run. - Data
File - A data file to register: the file already exists on object storage
(data before metadata).
row_id_startis allocated by the commit, never caller-provided. - Data
File Id - Identifies a data file.
- Data
File Info - A live data file, as read from a snapshot.
- Data
Store - An object store holding data files, named once for the caches that key on it. Build one per store and clone it to share cached reads.
- Delete
File - A delete file to register, targeting one data file’s rows.
- Delete
File Id - Identifies a delete file.
- Delete
File Info - A live delete file, as read from a snapshot.
- Delete
File Registration - One delete file to land through
Catalog::commit_located_deletion: the caller has already written it — the union of an existing delete file’s positions (if any) with newly located deletions — and supplies enough to register it in place of the file it replaces. - Excluded
Positions - File positions a caller treats as deleted on top of the snapshot’s own delete files, such as a transaction’s uncommitted deletions.
- Existing
Delete File - A delete file already registered against a data file a caller is about to delete more rows from.
- File
Column Stats - Per-column statistics of one data file. Min/max are opaque strings, stored verbatim — moraine never interprets or merges them.
- File
Index Entry - One writer-supplied index entry for a row of a registered data file.
The ordinal is the row’s 0-based position in the file; the commit maps
it to a row id (
row_id_start + ordinal). - File
Index Removal - One writer-supplied entry removal for a registered delete file: the killed row’s id and the values it was indexed under. Against a dense-range target the id must lie inside the target’s row-id range; against a per-row-id target it is the file’s embedded id, supplied verbatim.
- File
RowCandidate - Where one stable row id may physically live at head.
- Flushed
Data File - A data file a flush wrote, carrying rows that were inlined before it. Unlike an ordinary registration, the rows keep the ids they were inlined under and the record is backdated, so a pre-flush time-travel read finds them in the file rather than in the drained chunks.
- Index
Def - The definition of an equality index to create.
- Index
Entry - One writer-supplied index entry: a row and its indexed column values,
in the index’s column order. A
Nonein any position is SQL NULL; a row with any NULL indexed value is stored as a collision-exempt multi-shaped entry, soIS NULLfinds it and a unique index still admits unlimited NULL rows. - IndexId
- Identifies an equality index within its table (allocated from the global catalog-id counter).
- Index
Info - A live equality index, as read from a snapshot.
- Index
Read Identity - An opaque read revision, separated from the resources pinning it.
- Index
Read Scope - Index, file, and inline reads pinned to one committed store revision. Dropping the scope and its read clones releases the pinned transaction.
- Inline
Chunk - A chunk of rows to inline: one Arrow IPC record-batch body over the
table’s user columns, decoded against the schema-only stream recorded
for its
schema_version. Row ids are allocated by the commit from the table’s row-id counter, never caller-provided — the same allocation a data-file registration performs. - Live
Count - Live keys of one subspace, and the bytes they occupy.
- Located
Deletion - One requested row’s exact position within one of a table’s current data files, with the delete file currently registered against it, if any.
- Located
Positions - What
ReadOnlyCatalog::locate_row_positionsresolved: oneLocatedDeletionper data file carrying a requested row, the row ids resolved as live inlined rows, and — when any position was resolved — the directory a new delete file for this table belongs in. - Located
RowScan - A selective reader emitting one projected Arrow batch at a time. Dropping the reader cancels its remaining reads.
- MacroId
- Identifies a macro.
- Macro
Implementation Def - One implementation (arity overload) of a macro.
- Macro
Info - A macro with its implementations.
- Macro
Parameter Def - One parameter of a macro implementation. An absent default stores
default_value: Nonewithdefault_value_type"unknown", matching the row DuckLake writes. - Maintenance
Report - What a maintenance pass reclaimed.
- Maintenance
Request - What a maintenance pass should reclaim.
- Maintenance
Status Pass - One completed maintenance pass in the catalog’s durable status history.
- Maintenance
Status Step - One step reported by a completed maintenance pass.
- Mapping
Id - Identifies a column mapping (allocated by DuckLake from the same counter as data-file ids).
- Mapping
Info - A column mapping for externally written Parquet: immutable once
created, referenced by data files via their
mapping_id. - Memory
Tally - Logical memory Moraine can attribute without sampling process RSS. Cache occupancy is process-wide; the other fields belong to one catalog.
- Migration
Report - What one migrate call did.
- Migration
Request - How a
Catalog::migratecall should run. - Name
Mapping Def - One
ducklake_name_mappingrow: how one physical column of an externally written file resolves to a table field.column_idis a 0-based ordinal local to the mapping;parent_column, when present, references a smaller ordinal (parents precede children in preorder). - Object
Store Tally - Physical object-store requests SlateDB has issued for one catalog,
retries included. WAL objects land in the
main_*fields while the WAL shares the main store. Durations sum completed request latency, so concurrent requests can exceed wall-clock time. - Partition
Column Def - One key of a partition spec: a source column and the transform applied
to it. Transforms are stored verbatim as DuckLake writes them
(
identity,year,bucket(16), …); moraine never parses or applies one. A bare partition column carriesidentityrather than an empty transform, so every key is explicit. - Partition
Id - Identifies a partition spec within its table (allocated from the global catalog-id counter).
- Partition
Spec - A table’s partition spec: its partition keys in order. A table has at most one live spec; setting a new one ends the old, and files written under the old spec keep referencing it.
- Read
Only Catalog - The read surface of a moraine catalog: cheap to clone, drives every
read. This is what a read-only attach hands back, and what a
read-write
Catalogderefs to. - Recent
Row - One inlined row, with the Arrow IPC bytes a caller needs to decode it.
Rows of one chunk share one
chunk_body, and rows of one schema version share onearrow_schema. - RowSummary
Occupancy - Resident file row-id summaries by shape, and their bytes together; the
summary share of
auxiliary_metadata_occupancy_bytes. - RowSummary
Warmth - What one warming pass did.
- Scheduled
Deletion - One
ducklake_files_scheduled_for_deletionrow: a path awaiting physical deletion, decoupled from the expiry that scheduled it. - Schema
Id - Identifies a schema.
- Schema
Info - A live schema.
- Snapshot
Id - Identifies a committed snapshot.
- Snapshot
Info - Identity and metadata of a committed snapshot.
- SortId
- Identifies a sort spec within its table (allocated from the global catalog-id counter).
- Sort
KeyDef - One key of a sort spec: a SQL expression and how it orders. Every
field is stored verbatim as DuckLake writes it — the expression and its
dialect, thesort_direction(ASC/DESC), thenull_order(NULLS_FIRST/NULLS_LAST) — and moraine parses or evaluates none of them. - Sort
Spec - A table’s sort spec: its sort keys in order. A table has at most one live spec; setting a new one ends the old. Unlike a partition spec, no data file records the sort spec it was written under — a sort spec is a write-time instruction, not file provenance.
- Store
Census - What a store weighs, subspace by subspace.
- Store
Objects - What the object store holds, by the kind of object holding it. The per-subspace figures account for SST bytes only; the write-ahead log shows up only here.
- Subspace
Census - What one subspace weighs. The physical figures come from the store’s manifest and count what has been written out, superseded versions and tombstones included; a write still in the write-ahead log is in none of them.
- Subspace
Merge - What one subspace’s merge did.
- TableId
- Identifies a table.
- Table
Info - A live table.
- Table
Stats - A table’s statistics.
- TagEntry
- One tag on a catalog object: a key/value row, begin/end-versioned like any temporal row. Ended entries stay readable for time travel until garbage-collected.
- Timestamp
- An instant, as microseconds from the Unix epoch in UTC — the precision the catalog persists and the extension ABI carries. Negative counts precede the epoch.
- Transaction
- The mutation handle a commit closure receives.
- ViewId
- Identifies a view.
- View
Info - A live view.
Enums§
- Cache
Preload - How much of a store to load into the on-disk object cache as it opens.
- Compaction
Target - Which trees a
Catalog::compact_storecall merges. - Direction
- Sort direction of an indexed column.
- Error
- Errors returned by moraine operations.
- Index
KeyValue - A single non-null indexed column value, typed by the column’s category.
NULL is not a variant here — it is the
Noneof a column’s optional value, which encodes to a leading null flag. - Index
Maintenance - When SQL writes add entries to an index.
- Index
State - The build lifecycle of an equality index.
- IntWidth
- Byte width of a fixed-width integer column.
- Merge
Outcome - How one subspace’s merge ended.
- Null
Order - Where NULLs sort relative to the non-null values of an indexed column,
independent of the column’s
Direction. - Option
Scope - An option scope: global, or one schema or table.
- Subspace
Name - One subspace of the keyspace, as a census names it. A segment this
build does not recognize is reported as
Unknownrather than dropped. - TagTarget
- A catalog object a tag can be attached to. Schemas, tables, and views share one id space, so the variant selects which entity the verb validates against and how the change is classified, not how the tag is keyed. Column tags are carried on the column itself and are not reachable here.
Functions§
- cache_
status - Current process-wide cache sizing and occupancy, summed over every store’s cache. The metadata figure is the ceiling past which metadata stops being protected, not a partition; the block figure is the whole budget.
- cache_
tally - What the process’s block cache has served since it was built, metadata
and data blocks counted apart. Process-wide, outliving the catalogs
that produced it; use it to size
CatalogOptions::cache_memoryandcache_size, andReadOnlyCatalog::cache_tallyto see which attach is spending them.
Type Aliases§
- Commit
Member - One member of a
Catalog::commit_groupbatch: a closure authoring one logical commit. - Result
- Crate-wide result alias.