1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! Moraine brings a [SlateDB](https://slatedb.io) backend to
//! [DuckLake](https://ducklake.select): 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:
//!
//! ```
//! # use std::sync::Arc;
//! # use moraine::{Catalog, CatalogOptions, ColumnDef};
//! # use object_store::memory::InMemory;
//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
//! 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,
//! }],
//! )?;
//! 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);
//! # Ok::<(), moraine::Error>(()) }).unwrap();
//! ```
//!
//! 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. [`Catalog::recent_rows`] reads them back — or
//! [`Catalog::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. It 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:
//!
//! ```
//! # use std::sync::Arc;
//! # use moraine::{Catalog, CatalogOptions};
//! # use object_store::memory::InMemory;
//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
//! # let object_store = Arc::new(InMemory::new());
//! # let catalog = Catalog::open(object_store.clone(), CatalogOptions::default()).await?;
//! 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());
//! # Ok::<(), moraine::Error>(()) }).unwrap();
//! ```
//!
//! 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:
//!
//! ```
//! # use std::sync::Arc;
//! # use moraine::{Catalog, CatalogOptions, MaintenanceRequest};
//! # use object_store::memory::InMemory;
//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
//! 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);
//! # Ok::<(), moraine::Error>(()) }).unwrap();
//! ```
//!
//! 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. [`Catalog::store_census`] says where the weight is, from
//! the store's manifest alone:
//!
//! ```
//! # use std::sync::Arc;
//! # use moraine::{Catalog, CatalogOptions, CensusRequest, CompactStoreRequest, SubspaceName};
//! # use object_store::memory::InMemory;
//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
//! 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());
//! # Ok::<(), moraine::Error>(()) }).unwrap();
//! ```
//!
//! 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 deliberately a
//! separate verb rather than part of opening a catalog — the cost and timing
//! are the operator's to choose. While it runs, every reader refuses the
//! store rather than serving a view of a keyspace in motion.
//!
//! 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 exposes `CrashPoint` and
//! `inject_crash` so a test can arm one and stop a migration at a named
//! durable boundary. Off by default; a build without it carries an empty
//! function at each seam and no fault surface.
//!
//! # Diagnostics
//!
//! The crate emits [`tracing`](https://docs.rs/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.
pub use ;
pub use ;
/// Crash-injection seams, for tests that drive a migration to a named
/// durable boundary and stop it there. Unstable and not part of the semver
/// contract.
pub use ;
pub use ;
pub use ;