obj_core/codec/migrate.rs
1//! Schema-evolution trait — `Migrate`.
2//!
3//! `Migrate` is the API by which a [`Document`] type
4//! transforms older stored records into the current shape. The codec
5//! invokes [`migrate`](Migrate::migrate) whenever a stored record's
6//! `type_version` is less than the reader's `Document::VERSION`.
7//!
8//! Every `T: Document` is implicitly `Migrate` via the blanket impl
9//! at the bottom of this file. The default body of the inherent
10//! method [`Document::migrate`] returns
11//! [`Error::SchemaMigrationNotImplemented`].
12//! Real types override [`Document::migrate`]
13//! to handle older versions; `Migrate::migrate` forwards to it.
14//!
15//! M5 ships the trait and the decode-time dispatch only. The full
16//! "lazy migrate on read" runtime with mixed-version coexistence is
17//! M10's milestone. The on-disk contract pinned here is permanent:
18//! `type_version > T::VERSION` is always
19//! [`Error::SchemaVersionFromFuture`];
20//! `type_version < T::VERSION` always routes through `migrate`.
21//!
22//! # Power-of-ten posture
23//!
24//! - **Rule 5.** The default body errors at the boundary, never
25//! silently returns a default value — silent migration failure
26//! would be the most dangerous outcome.
27//! - **Rule 7.** Override implementations propagate every fallible
28//! step via `?`; the trait does not encourage `unwrap`.
29//! - **Rule 9.** Static dispatch only — `<T as Migrate>` is
30//! monomorphised in [`crate::codec::decode`].
31
32#![forbid(unsafe_code)]
33
34use crate::codec::{Document, Dynamic};
35#[cfg(doc)] // Error appears in doc-link references but is not directly used at runtime.
36use crate::error::Error;
37use crate::error::Result;
38
39/// Static-dispatch shim trait so [`crate::codec::decode`] can write
40/// `<T as Migrate>::migrate(...)` without taking `T: Migrate` as an
41/// explicit bound on top of `T: Document`.
42///
43/// `Migrate` is implemented for every `T: Document` via the blanket
44/// impl below, which forwards to the inherent method
45/// [`Document::migrate`]. Concrete types
46/// customise migration by overriding `Document::migrate` (which has
47/// a default erroring body); they do NOT need to touch `Migrate`
48/// directly.
49///
50/// The separation exists for two reasons:
51///
52/// 1. Rust does not have stable specialisation, so a blanket `impl
53/// Migrate for T` cannot coexist with concrete `impl Migrate for
54/// MyType` overrides. Putting the override on the `Document`
55/// trait (a single, non-blanket impl per type) sidesteps the
56/// conflict.
57/// 2. Keeping `Migrate` as a thin shim trait preserves the
58/// issue-#36 surface area (the codec calls `<T as
59/// Migrate>::migrate(...)`) without forcing a separate
60/// `impl Migrate for ...` block at every `Document` site.
61///
62/// # Errors
63///
64/// Propagates the override's errors. The default body returns
65/// [`Error::SchemaMigrationNotImplemented`].
66pub trait Migrate: Document {
67 /// Migrate an older stored record into the current `Self` type.
68 ///
69 /// Forwards to [`Document::migrate`];
70 /// see that method for the contract.
71 ///
72 /// # Errors
73 ///
74 /// Propagates [`Document::migrate`]'s
75 /// errors verbatim. The default body returns
76 /// [`Error::SchemaMigrationNotImplemented`].
77 fn migrate(dynamic: Dynamic, from_version: u32) -> Result<Self> {
78 <Self as Document>::migrate(dynamic, from_version)
79 }
80}
81
82// Blanket impl: every `T: Document` is `Migrate` via the shim above.
83// Concrete migration logic lives on `Document::migrate`; the trait
84// method here just delegates.
85impl<T: Document> Migrate for T {}