obj/lib.rs
1//! `obj` — embedded document database (public crate).
2//!
3//! This crate is the user-facing surface of the `obj` storage
4//! engine. It wraps the `obj-core` building blocks (pager, WAL,
5//! B+tree, codec, catalog, transaction layer) into the typed
6//! [`Db`] / [`Collection<T>`] API described in
7//! [`design.md`](https://github.com/uname-n/obj/blob/master/design.md).
8//!
9//! Worked examples for every topic live next to the relevant item
10//! in this crate's rustdoc:
11//!
12//! - Opening / CRUD: see [`Db::open`], [`Db::insert`], [`Db::get`],
13//! [`Db::update`], [`Db::delete`], [`Db::upsert`].
14//! - Transactions: see [`Db::transaction`] and
15//! [`Db::read_transaction`].
16//! - Iteration: see [`Db::iter_all`] and [`Db::all`].
17//! - Queries: see [`Db::query`], [`Query::sort_by`],
18//! [`Query::index_range`], [`Query::count`].
19//! - Attach / backup / integrity: see [`Db::attach`],
20//! [`Db::backup_to`], [`Db::integrity_check`].
21//! - Configuration: see [`Config`].
22//!
23//! # Quick start
24//!
25//! ```no_run
26//! use obj::Db;
27//! use serde::{Deserialize, Serialize};
28//!
29//! #[derive(Debug, Serialize, Deserialize, obj::Document)]
30//! struct Order { customer_id: u64, total_cents: u64 }
31//!
32//! fn run() -> obj::Result<()> {
33//! let db = Db::open("app.obj")?;
34//! let id = db.insert(Order { customer_id: 1, total_cents: 100 })?;
35//! let back: Option<Order> = db.get(id)?;
36//! assert!(back.is_some());
37//! Ok(())
38//! }
39//! ```
40//!
41//! # Core CRUD and the `Document` derive
42//!
43//! Open a database with one of three constructors:
44//!
45//! - [`Db::open`] / [`Db::open_with`] — file-backed; creates if
46//! absent, reopens otherwise.
47//! - [`Db::memory`] / [`Db::memory_with`] — in-memory, ephemeral.
48//! No persistence, no file locks. Useful for unit tests.
49//! - [`Db::open_readonly`] — read-only against an existing file.
50//! Every mutating call returns
51//! [`Err(Error::ReadOnly { .. })`](Error::ReadOnly).
52//!
53//! Each `Db` is `Send + Sync`. Share across threads via `Arc<Db>`
54//! for the concurrent-reader / single-writer workload documented
55//! in [`docs/concurrency.md`](https://github.com/uname-n/obj/blob/master/docs/concurrency.md).
56//!
57//! Implement the [`Document`] trait on every type you want to
58//! persist. The [`obj::Document`](crate::Document) re-export is a
59//! `proc-macro` that fills in the trait's associated constants
60//! from optional `#[obj(...)]` attributes:
61//!
62//! - `#[obj(collection = "...")]` — sets [`Document::COLLECTION`].
63//! Default: the type name.
64//! - `#[obj(version = N)]` — sets [`Document::VERSION`]. Default: 1.
65//! - `#[obj(index)]`, `#[obj(index = unique)]`,
66//! `#[obj(index = each)]` on a field — declare secondary indexes
67//! (see § "Queries and indexes" below).
68//! - `#[obj(index_composite(fields = ("a", "b")))]` at struct
69//! level — declare a composite index.
70//!
71//! The one-shot API runs each call inside a private transaction
72//! and is the typical entry point for ad-hoc work:
73//!
74//! - [`Db::insert`] — allocate an `Id`, write the doc.
75//! - [`Db::get`] — fetch by `Id`. Returns `Option<T>`.
76//! - [`Db::update`] — apply a closure in place. Errors with
77//! [`Error::DocumentNotFound`] if the id is absent.
78//! - [`Db::delete`] — remove by `Id`. Returns `true` if it existed.
79//! - [`Db::upsert`] — insert-or-replace at a caller-supplied `Id`.
80//! - [`Db::find_unique`] — point lookup on a `Unique` index.
81//! `O(log n)`, no collection scan.
82//!
83//! # Transactions and iteration
84//!
85//! For multi-document atomicity, [`Db::transaction`] runs a closure
86//! with a `&mut WriteTxn`. The closure returns `Result<R>`; commit
87//! on `Ok`, rollback on `Err`, rollback-via-`Drop` on panic. Inside
88//! the closure, [`WriteTxn::collection`] yields a typed
89//! [`Collection<T>`] handle whose methods compose with the parent
90//! txn — every write rides one WAL transaction.
91//!
92//! For read-only consistency across multiple reads,
93//! [`Db::read_transaction`] runs a closure with a `&ReadTxn`. The
94//! closure observes one consistent snapshot of the database;
95//! concurrent writers do not affect what it sees.
96//!
97//! For full-collection iteration there are two shapes:
98//!
99//! - [`Db::iter_all`] — streaming iterator over `Result<(Id, T)>`.
100//! Peak memory is bounded at a small constant (256 entries per
101//! refill, power-of-ten Rule 3) regardless of collection size.
102//! - [`Db::all`] — one-line shim that drives `iter_all` to
103//! exhaustion and collects into `Vec<T>`. Pays memory
104//! proportional to the collection.
105//!
106//! See [`docs/concurrency.md`](https://github.com/uname-n/obj/blob/master/docs/concurrency.md)
107//! for the lock-acquisition contract and
108//! [`Db::transaction`] / [`Db::read_transaction`] for worked
109//! examples of the closure shape.
110//!
111//! # Queries and indexes
112//!
113//! [`Db::query::<T>()`](Db::query) constructs a [`Query`] builder.
114//! Compose with [`Query::filter`], [`Query::limit`],
115//! [`Query::sort_by`], [`Query::index_range`]; terminate with
116//! [`Query::fetch`] (materialised `Vec<T>`) or [`Query::count`]
117//! (count alone, without decoding documents on the fast path).
118//!
119//! The query layer has two sources: a full primary-tree scan
120//! (default) or an index-range slice ([`Query::index_range`]). No
121//! cost-based planner — the caller picks. Source order is by
122//! primary `Id` for the full scan and by encoded index-key bytes
123//! for the index range.
124//!
125//! [`Query::sort_by`] materialises every surviving candidate into
126//! a sort buffer before applying [`Query::limit`]. The buffer is
127//! capped at [`MAX_SORT_BUFFER`] (100 000 documents); overflowing
128//! the cap surfaces [`Error::SortBufferExceeded`]. Override the
129//! cap with [`Query::sort_buffer_limit`] when the workload
130//! genuinely needs more.
131//!
132//! Indexes are declared on the document type via
133//! [`Document::indexes`] (or the derive's `#[obj(index ...)]`
134//! attributes). The catalog reconciler runs on the first
135//! [`WriteTxn::collection::<T>()`](WriteTxn::collection) call per
136//! process per collection: it declares missing specs, marks
137//! stale active descriptors `DroppedPending`, and is idempotent.
138//! Reconciliation rides the caller's WAL transaction — a rolled-
139//! back insert leaves no half-created index behind.
140//!
141//! Four [`IndexKind`]s are exposed: `Standard`, `Unique`, `Each`,
142//! `Composite`. Construct typed [`IndexSpec`]s via
143//! `IndexSpec::standard` / `::unique` / `::each` / `::composite`
144//! when hand-implementing [`Document::indexes`].
145//!
146//! # Schema evolution
147//!
148//! Bump [`Document::VERSION`] on every breaking change. Register a
149//! [`DynamicSchema`] for each prior version in
150//! [`Document::historical_schemas`], and provide a
151//! [`Document::migrate`] body that lifts the structured
152//! [`obj_core::codec::Dynamic`] view into the current `Self`.
153//!
154//! Migration is lazy: a stored record whose `type_version` is
155//! older than `Self::VERSION` is migrated on read but the on-disk
156//! bytes are NOT rewritten until the next
157//! [`Collection::update`] / [`Collection::upsert`] for that id.
158//! The collection therefore scales to billions of documents
159//! without a stop-the-world rebuild on schema bumps.
160//!
161//! - [`Error::SchemaNotRegistered`] surfaces when a stored
162//! `type_version` has no entry in `historical_schemas()`.
163//! - [`Error::SchemaVersionFromFuture`] surfaces when the stored
164//! `type_version` is newer than `Self::VERSION` (downgrade
165//! attempt).
166//!
167//! Worked recipes for the four common patterns — single-version
168//! migration, multi-version chains, tombstoned fields, enum-variant
169//! migration — live on [`Document::migrate`] and in the
170//! [integration tests](https://github.com/uname-n/obj/tree/master/crates/obj/tests):
171//! `historical_schemas.rs`, `tombstone_migration.rs`,
172//! `enum_migration.rs`, and `lazy_migration.rs`. The lazy-rewrite
173//! cycle itself is documented on [`Collection::get`].
174//!
175//! # Attach, backup, integrity
176//!
177//! [`Db::attach`] registers a read-only second `.obj` file under a
178//! caller-chosen namespace. Any [`Document`] whose `COLLECTION` is
179//! of the form `<namespace>.<name>` dispatches reads against the
180//! attached file; writes against a namespaced collection return
181//! [`Error::AttachedDatabaseIsReadOnly`]. Each attached database
182//! gets its own snapshot pinned at read-transaction begin;
183//! [`Db::detach`] removes the registry entry but in-flight reads
184//! complete against their pinned snapshot.
185//!
186//! [`Db::backup_to`] writes a self-contained `.obj` file at the
187//! LSN of an internally-taken reader snapshot. Writers continue
188//! against the source; post-snapshot writes are NOT in the
189//! destination. The algorithm is documented in
190//! [`docs/format.md`](https://github.com/uname-n/obj/blob/master/docs/format.md)
191//! § "Hot backup". Two failure modes:
192//! [`Error::BackupDestinationExists`] (refuses to overwrite) and
193//! [`Error::BackupNotSupportedForMemoryPager`] (in-memory dbs have
194//! no file backend to copy from).
195//!
196//! [`Db::integrity_check`] runs a full bidirectional walk: every
197//! active collection's primary + index B-trees, freelist sweep,
198//! orphan-page detection, primary↔index cross-reference. Returns
199//! [`IntegrityReport`] with a `failures` list and a
200//! `pages_checked` count. The lightweight subset that
201//! [`Db::open`] runs at open time is
202//! `obj_core::integrity::quick_check`; opt out of the open-time
203//! walk via [`Config::skip_open_check`].
204//!
205//! # Configuration
206//!
207//! [`Config`] is a `Clone` builder. Defaults match the
208//! "production-safe" posture documented in
209//! [`design.md`](https://github.com/uname-n/obj/blob/master/design.md):
210//!
211//! - [`Config::cache_size`] — bytes for the pager's LRU. Default
212//! 256 KiB (64 frames). Larger for read-heavy workloads on
213//! large databases; smaller on memory-constrained targets.
214//! - [`Config::sync_mode`] — durability mode for every WAL
215//! commit. Default [`SyncMode::Full`] (system-wide power loss
216//! survivable). [`SyncMode::Normal`] for `fsync`-only
217//! durability; [`SyncMode::Off`] only for tests and benchmarks.
218//! - [`Config::busy_timeout`] — max wait when acquiring the
219//! reader / writer lock. Default 5 seconds. Beyond the budget,
220//! the txn returns [`Err(Error::Busy)`](Error::Busy) rather
221//! than blocking indefinitely.
222//! - [`Config::skip_open_check`] — opt out of the open-time
223//! catalog walk. Default `false` (run the walk). Production
224//! callers should leave it on.
225//! - [`Config::cross_process_lock`] — toggle OS-level byte-range
226//! locking. Default `true` (on). Off only when every accessor
227//! shares one `Db` inside one process (in-process stress tests).
228//!
229//! # Cargo features
230//!
231//! - `serde` (off by default) — derive `serde::Serialize` and
232//! `serde::Deserialize` on the public types in this crate
233//! (`Config`, `DbStat`, `CollectionStat`, `DumpRecord`,
234//! `IntegrityReport`, `IntegrityFailure`, plus the obj-core
235//! re-exports `Id`, `SyncMode`, `LockKind`, `IndexKind`,
236//! `IndexSpec`). When the feature is on, `Serialize` and
237//! `Deserialize` are also re-exported from the crate root, so
238//! downstream callers do not need a separate `serde` dependency.
239//! Pure additive surface — no on-disk format byte changes.
240//! - `tracing` (off by default) — emit structured spans around the
241//! observability surface: `db.open`, `db.transaction`,
242//! `db.read_transaction`, `db.integrity_check`, `query.execute`,
243//! and the obj-core `pager.checkpoint` span (propagated via the
244//! `obj-core/tracing` sub-feature). The feature gates the
245//! optional `tracing` dependency on both crates so the default
246//! build has zero new transitive deps and zero span overhead.
247//! `tracing` is intentionally NOT re-exported from this crate —
248//! downstream subscribers add `tracing-subscriber` (or another
249//! subscriber crate) directly, mirroring the idiom used by
250//! `tokio` and `axum`.
251//! - `compression` (off by default) — LZ4 per-page compression at
252//! the pager layer (Phase 3, issue #8). Propagates to obj-core.
253//! Every v1.0 writer stamps `format_minor = 2` regardless of which
254//! codecs are enabled; whether a file *uses* compression is
255//! recorded by `feature_flags` bit 0, not by the minor. A build
256//! WITHOUT this feature opens any file whose bit 0 is clear, and
257//! refuses (with `Error::FormatFeatureUnsupported`) only a file
258//! that actually has the compression flag set.
259//! - `encryption` (off by default) — XChaCha20-Poly1305 per-page
260//! at-rest encryption (Phase 4, issue #9). Propagates to
261//! obj-core. As with compression, the file's minor is always 2;
262//! `feature_flags` bit 1 records whether the file is encrypted. A
263//! build WITHOUT this feature opens any file whose bit 1 is clear,
264//! and refuses (with `Error::FormatFeatureUnsupported`) a file
265//! whose bit 1 is set — the refusal keys off the feature flag, not
266//! the minor version.
267//! - `async` (off by default) — runtime-agnostic async surface
268//! mirroring the blocking [`Db`] / [`Collection`] / [`Query`]
269//! API behind a new `obj::asynchronous` module (Phase 5, issue
270//! #10). Work is routed through the
271//! [`blocking`](https://docs.rs/blocking) crate's process-wide
272//! thread pool, so the wrapper composes with Tokio, async-std,
273//! smol, and any other async runtime — no per-runtime
274//! sub-features. With the feature off the baseline build adds
275//! no new transitive dependencies and no async overhead.
276//!
277//! # Observability
278//!
279//! Enable the `tracing` feature to emit spans around database
280//! operations; spans are gated and free when the feature is off.
281//! The span set is small and stable: one `info`-level span at every
282//! transaction boundary, one `debug`-level span at every query
283//! execution and pager checkpoint. No span field captures user
284//! payload bytes — the only string-ish field is `path` on
285//! `db.open`, which is a filesystem path rather than user content.
286//!
287//! # `unsafe` policy
288//!
289//! This crate is `#![forbid(unsafe_code)]`. All `unsafe` lives in
290//! `obj-core::platform` and carries a documented safety contract
291//! per [`docs/unsafe-audit.md`](https://github.com/uname-n/obj/blob/master/docs/unsafe-audit.md).
292
293#![forbid(unsafe_code)]
294#![deny(missing_docs)]
295#![deny(rustdoc::broken_intra_doc_links)]
296// Render "Available on crate feature `…`" badges on docs.rs for the
297// feature-gated surface (the `async` module, the `serde` re-exports,
298// the `Serialize`/`Deserialize` impls). Both attributes are gated
299// behind the `docsrs` cfg that docs.rs sets — see
300// `[package.metadata.docs.rs]` in `Cargo.toml`, which passes
301// `--cfg docsrs` and builds on nightly. A stable
302// `RUSTFLAGS="-D warnings"` build never sets `docsrs`, so it never
303// sees the unstable `doc_cfg` feature — power-of-ten Rule 10.
304#![cfg_attr(docsrs, feature(doc_cfg))]
305#![cfg_attr(docsrs, doc(auto_cfg))]
306
307#[cfg(feature = "async")]
308pub mod asynchronous;
309
310mod cli;
311mod collection;
312mod config;
313mod db;
314mod index_bound;
315mod index_maint;
316mod integrity;
317mod query;
318mod txn;
319
320pub use crate::cli::{CollectionStat, DbStat, DumpIter, DumpRecord};
321pub use crate::collection::{Collection, IterIndexRange, MAX_DISTINCT_IDS};
322pub use crate::config::Config;
323pub use crate::db::{Db, IterAll};
324pub use crate::query::{Query, MAX_SORT_BUFFER};
325pub use crate::txn::{ReadTxn, WriteTxn};
326
327pub use obj_core::codec::{DynamicSchema, EnumVariantSchema, Schema};
328pub use obj_core::integrity::{IntegrityFailure, IntegrityReport};
329pub use obj_core::{
330 CompressionMode, Document, Error, Id, IndexKind, IndexSpec, LockKind, Result, SyncMode,
331};
332
333/// Re-export of `serde::Serialize` + `serde::Deserialize` under the
334/// opt-in `serde` feature (issue #6). Lets downstream code write
335/// `use obj::{Serialize, Deserialize}` without a separate `serde`
336/// dependency — the same convention `tokio` and `axum` use.
337#[cfg(feature = "serde")]
338pub use serde::{Deserialize, Serialize};
339
340/// `#[derive(obj::Document)]` proc-macro re-export.
341///
342/// Lives in the sibling `obj-derive` crate; re-exported here so
343/// users only have to depend on `obj` to use the derive. The trait
344/// itself is still `obj_core::Document` re-exported above —
345/// proc-macros and traits share a single name namespace and Rust
346/// resolves the two by use-site (`#[derive(Document)]` vs `impl
347/// Document for ...`).
348///
349/// The derive fills in [`Document::COLLECTION`] (default: the type
350/// name) and [`Document::VERSION`] (default: `1`). The struct still
351/// needs serde derives — the macro intentionally does not emit them
352/// so you stay in control of serde-level attributes
353/// (`#[serde(rename = ...)]`, etc.).
354///
355/// # Examples
356///
357/// Derive with defaults:
358///
359/// ```
360/// # fn main() -> obj::Result<()> {
361/// use obj::Db;
362/// use serde::{Deserialize, Serialize};
363///
364/// #[derive(Debug, Serialize, Deserialize, obj::Document)]
365/// struct Order {
366/// customer_id: u64,
367/// total_cents: u64,
368/// }
369///
370/// let dir = tempfile::tempdir()?;
371/// let db = Db::open(dir.path().join("orders.obj"))?;
372///
373/// // `Document::COLLECTION` defaulted to "Order".
374/// assert_eq!(<Order as obj::Document>::COLLECTION, "Order");
375/// assert_eq!(<Order as obj::Document>::VERSION, 1);
376///
377/// let id = db.insert(Order { customer_id: 1, total_cents: 4_200 })?;
378/// let back: Option<Order> = db.get::<Order>(id)?;
379/// assert_eq!(back.map(|o| o.total_cents), Some(4_200));
380/// # Ok(())
381/// # }
382/// ```
383///
384/// Override the defaults with `#[obj(...)]`:
385///
386/// ```
387/// # fn main() -> obj::Result<()> {
388/// use obj::Db;
389/// use serde::{Deserialize, Serialize};
390///
391/// #[derive(Debug, Serialize, Deserialize, obj::Document)]
392/// #[obj(collection = "people", version = 2)]
393/// struct Customer {
394/// name: String,
395/// }
396///
397/// assert_eq!(<Customer as obj::Document>::COLLECTION, "people");
398/// assert_eq!(<Customer as obj::Document>::VERSION, 2);
399///
400/// let dir = tempfile::tempdir()?;
401/// let db = Db::open(dir.path().join("people.obj"))?;
402/// let id = db.insert(Customer { name: "Ada".to_owned() })?;
403/// let back: Customer = db
404/// .get::<Customer>(id)?
405/// .ok_or(obj::Error::InvalidArgument("just inserted"))?;
406/// assert_eq!(back.name, "Ada");
407/// # Ok(())
408/// # }
409/// ```
410///
411/// Multiple `#[obj(...)]` attributes compose, and key=value pairs
412/// may share a single attribute. Both shapes produce the same impl.
413///
414/// # Declaring indexes
415///
416/// Four kinds map to the same `IndexSpec` shape:
417///
418/// | Kind | Attribute | Behaviour |
419/// |-----------|----------------------------------------------------------|--------------------------------------------|
420/// | Standard | `#[obj(index)]` | B-tree index; duplicates allowed. |
421/// | Unique | `#[obj(index = unique)]` | Uniqueness enforced at write time. |
422/// | Each | `#[obj(index = each)]` | Indexes every element of a `Vec<T>` field. |
423/// | Composite | `#[obj(index_composite(fields = ("a", "b")))]` | One index over a tuple of fields. |
424///
425/// ```
426/// # fn main() -> obj::Result<()> {
427/// use obj::Db;
428/// use serde::{Deserialize, Serialize};
429///
430/// #[derive(Debug, Clone, Serialize, Deserialize, obj::Document)]
431/// #[obj(collection = "customers_idx_doc")]
432/// #[obj(index_composite(fields = ("region", "tier"), name = "by_region_tier"))]
433/// struct Customer {
434/// #[obj(index)]
435/// customer_id: u64,
436/// #[obj(index = unique)]
437/// email: String,
438/// #[obj(index = each)]
439/// tags: Vec<String>,
440/// region: String,
441/// tier: String,
442/// }
443///
444/// let dir = tempfile::tempdir()?;
445/// let db = Db::open(dir.path().join("indexes.obj"))?;
446/// let _id = db.insert(Customer {
447/// customer_id: 1,
448/// email: "ada@example.com".to_owned(),
449/// tags: vec!["red".to_owned(), "blue".to_owned()],
450/// region: "us-east".to_owned(),
451/// tier: "gold".to_owned(),
452/// })?;
453///
454/// // Unique-index point lookup. O(log n), no collection scan.
455/// let by_email: Option<Customer> = db
456/// .find_unique::<Customer>("email", "ada@example.com")?;
457/// assert!(by_email.is_some());
458/// # Ok(())
459/// # }
460/// ```
461///
462/// # Hand-implementing `Document`
463///
464/// The derive is sugar over a trait. Implement the trait directly
465/// when you need full control — for example to share a
466/// `historical_schemas()` body across many types, or to compute the
467/// `indexes()` list at runtime:
468///
469/// ```
470/// # fn main() -> obj::Result<()> {
471/// use obj::{Db, Document, IndexSpec};
472/// use serde::{Deserialize, Serialize};
473///
474/// #[derive(Debug, Serialize, Deserialize)]
475/// struct Customer { email: String }
476///
477/// impl Document for Customer {
478/// const COLLECTION: &'static str = "customers_hand_doc";
479/// const VERSION: u32 = 1;
480///
481/// fn indexes() -> Vec<IndexSpec> {
482/// vec![IndexSpec::unique("email", "email").expect("static spec")]
483/// }
484/// }
485///
486/// let dir = tempfile::tempdir()?;
487/// let _db = Db::open(dir.path().join("hand-idx.obj"))?;
488/// # Ok(())
489/// # }
490/// ```
491///
492/// The reconciler runs on the first
493/// [`WriteTxn::collection::<T>()`](WriteTxn::collection) call per
494/// process per collection: it declares specs absent from the
495/// catalog, flips active descriptors absent from `indexes()` to
496/// `DroppedPending`, and leaves matches alone. Reconciliation
497/// rides the user's WAL transaction — a rolled-back insert leaves
498/// no half-created index behind.
499///
500/// # Schema evolution
501///
502/// Schema evolution is `(version bump) + (historical_schemas) +
503/// (migrate)`. Old records read through the new type are migrated
504/// in memory; their on-disk bytes are not rewritten until the next
505/// `update` / `upsert`. The collection therefore scales to billions
506/// of docs without a stop-the-world rebuild on every schema change.
507///
508/// ```
509/// # fn main() -> obj::Result<()> {
510/// use obj::{Db, Document};
511/// use obj_core::codec::{Dynamic, DynamicSchema};
512/// use serde::{Deserialize, Serialize};
513///
514/// // v1 wrote `Customer { name, email }`.
515/// // v2 adds `tier` with a default of "standard".
516/// #[derive(Debug, Serialize, Deserialize)]
517/// struct Customer {
518/// name: String,
519/// email: String,
520/// tier: String,
521/// }
522///
523/// impl Document for Customer {
524/// const COLLECTION: &'static str = "customers_evo_doc";
525/// const VERSION: u32 = 2;
526///
527/// fn historical_schemas() -> Vec<(u32, DynamicSchema)> {
528/// vec![(
529/// 1,
530/// DynamicSchema::map([
531/// ("name", DynamicSchema::String),
532/// ("email", DynamicSchema::String),
533/// ]),
534/// )]
535/// }
536///
537/// fn migrate(dynamic: Dynamic, from_version: u32) -> obj::Result<Self> {
538/// if from_version != 1 {
539/// return Err(obj::Error::SchemaMigrationNotImplemented {
540/// collection: Self::COLLECTION,
541/// from_version,
542/// to_version: Self::VERSION,
543/// });
544/// }
545/// Ok(Customer {
546/// name: dynamic.get_str("name")?.to_owned(),
547/// email: dynamic.get_str("email")?.to_owned(),
548/// tier: "standard".to_owned(),
549/// })
550/// }
551/// }
552///
553/// let dir = tempfile::tempdir()?;
554/// let db = Db::open(dir.path().join("evo.obj"))?;
555/// let id = db.insert(Customer {
556/// name: "Ada".to_owned(),
557/// email: "ada@example.com".to_owned(),
558/// tier: "gold".to_owned(),
559/// })?;
560/// let back: Customer = db
561/// .get::<Customer>(id)?
562/// .ok_or(obj::Error::InvalidArgument("just inserted"))?;
563/// assert_eq!(back.tier, "gold");
564/// # Ok(())
565/// # }
566/// ```
567///
568/// The rules are mechanical:
569///
570/// 1. Bump `VERSION` on every breaking change.
571/// 2. Register a schema for every prior version in
572/// `historical_schemas()`. The codec walks the on-disk postcard
573/// payload through that schema to produce the structured
574/// `Dynamic` view your `migrate` body reads.
575/// 3. `migrate` returns `Self`. Default values for new fields are
576/// the migration's responsibility — there is no implicit
577/// default.
578///
579/// A stored record whose `type_version` is newer than
580/// `Self::VERSION` surfaces [`Error::SchemaVersionFromFuture`]; an
581/// older `type_version` with no registered schema surfaces
582/// [`Error::SchemaNotRegistered`]. For multi-version chains,
583/// tombstoned fields, and enum-variant migration recipes, see the
584/// [integration tests](https://github.com/uname-n/obj/tree/master/crates/obj/tests):
585/// `historical_schemas.rs`, `tombstone_migration.rs`,
586/// `enum_migration.rs`, and `lazy_migration.rs`.
587pub use obj_derive::Document;