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