Skip to main content

icydb_core/db/schema_evolution/
descriptor.rs

1//! Module: db::schema_evolution::descriptor
2//! Responsibility: high-level schema migration descriptors and canonical row-op inputs.
3//! Does not own: migration execution or durable migration-progress storage.
4//! Boundary: caller-declared schema intent plus validated model identity.
5
6use crate::{
7    db::{
8        identity::{EntityName, IndexName},
9        schema::commit_schema_fingerprint_for_model,
10    },
11    error::InternalError,
12    model::EntityModel,
13    traits::EntityKind,
14};
15
16///
17/// SchemaMigrationEntityTarget
18///
19/// SchemaMigrationEntityTarget binds one canonical entity identity to the
20/// runtime model/path authority needed when schema evolution eventually emits
21/// row-level migration operations.
22///
23
24#[derive(Clone, Copy, Debug)]
25pub struct SchemaMigrationEntityTarget {
26    name: EntityName,
27    model: &'static EntityModel,
28}
29
30impl SchemaMigrationEntityTarget {
31    /// Build one schema-evolution target from a generated entity type.
32    pub fn for_entity<E>() -> Result<Self, InternalError>
33    where
34        E: EntityKind + 'static,
35    {
36        Self::from_model(E::MODEL)
37    }
38
39    /// Build one schema-evolution target from a runtime entity model.
40    pub fn from_model(model: &'static EntityModel) -> Result<Self, InternalError> {
41        let name = EntityName::try_from_str(model.name()).map_err(|err| {
42            InternalError::schema_evolution_invalid_identity(format!(
43                "invalid entity name '{}': {err}",
44                model.name()
45            ))
46        })?;
47
48        Ok(Self { name, model })
49    }
50
51    /// Return the canonical entity identity.
52    #[must_use]
53    pub const fn name(self) -> EntityName {
54        self.name
55    }
56
57    /// Return the runtime model that owns this schema-evolution target.
58    #[must_use]
59    pub const fn model(self) -> &'static EntityModel {
60        self.model
61    }
62
63    /// Return the runtime entity path consumed by commit runtime hooks.
64    #[must_use]
65    pub const fn runtime_path(self) -> &'static str {
66        self.model.path()
67    }
68
69    /// Return the current commit schema fingerprint for this target model.
70    #[must_use]
71    pub fn schema_fingerprint(self) -> [u8; 16] {
72        commit_schema_fingerprint_for_model(self.model.path(), self.model)
73    }
74}
75
76///
77/// SchemaMigrationStepIntent
78///
79/// SchemaMigrationStepIntent describes the high-level schema change that must be
80/// validated before any row-op migration plan is emitted.
81/// The initial supported slice models an index addition because `IndexName`
82/// already provides canonical entity + field identity.
83///
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub enum SchemaMigrationStepIntent {
87    AddIndex { index: IndexName },
88}
89
90impl SchemaMigrationStepIntent {
91    /// Build one canonical add-index migration intent.
92    #[must_use]
93    pub const fn add_index(index: IndexName) -> Self {
94        Self::AddIndex { index }
95    }
96}
97
98///
99/// SchemaMigrationRowOp
100///
101/// SchemaMigrationRowOp is the schema-evolution-owned row rewrite description.
102/// It carries a canonical entity target and raw row bytes, then the planner
103/// converts it into the lower-level migration row-op DTO only after validation.
104///
105
106#[derive(Clone, Debug)]
107pub struct SchemaMigrationRowOp {
108    target: SchemaMigrationEntityTarget,
109    key: Vec<u8>,
110    before: Option<Vec<u8>>,
111    after: Option<Vec<u8>>,
112}
113
114impl SchemaMigrationRowOp {
115    /// Build one explicit row rewrite for a schema migration.
116    #[must_use]
117    pub const fn new(
118        target: SchemaMigrationEntityTarget,
119        key: Vec<u8>,
120        before: Option<Vec<u8>>,
121        after: Option<Vec<u8>>,
122    ) -> Self {
123        Self {
124            target,
125            key,
126            before,
127            after,
128        }
129    }
130
131    /// Build one insert-style row rewrite for a schema migration.
132    #[must_use]
133    pub const fn insert(target: SchemaMigrationEntityTarget, key: Vec<u8>, after: Vec<u8>) -> Self {
134        Self::new(target, key, None, Some(after))
135    }
136
137    /// Return the canonical entity target for this row rewrite.
138    #[must_use]
139    pub const fn target(&self) -> SchemaMigrationEntityTarget {
140        self.target
141    }
142
143    /// Borrow encoded raw data-key bytes.
144    #[must_use]
145    pub const fn key(&self) -> &[u8] {
146        self.key.as_slice()
147    }
148
149    /// Borrow the optional before-image row payload.
150    #[must_use]
151    pub fn before(&self) -> Option<&[u8]> {
152        self.before.as_deref()
153    }
154
155    /// Borrow the optional after-image row payload.
156    #[must_use]
157    pub fn after(&self) -> Option<&[u8]> {
158        self.after.as_deref()
159    }
160
161    pub(in crate::db) fn into_migration_row_op(
162        self,
163    ) -> Result<crate::db::migration::MigrationRowOp, InternalError> {
164        crate::db::migration::MigrationRowOp::new(
165            self.target.runtime_path(),
166            self.key,
167            self.before,
168            self.after,
169            self.target.schema_fingerprint(),
170        )
171    }
172}
173
174///
175/// SchemaDataTransformation
176///
177/// SchemaDataTransformation describes the data rewrite portion of one schema
178/// migration descriptor.
179/// The first slice accepts explicit row rewrites only; derivation engines can
180/// add richer variants later without changing `db::migration` execution.
181///
182
183#[derive(Clone, Debug)]
184pub enum SchemaDataTransformation {
185    ExplicitRowOps(Vec<SchemaMigrationRowOp>),
186}
187
188impl SchemaDataTransformation {
189    /// Build one explicit row-op data transformation.
190    #[must_use]
191    pub const fn explicit_row_ops(row_ops: Vec<SchemaMigrationRowOp>) -> Self {
192        Self::ExplicitRowOps(row_ops)
193    }
194
195    /// Borrow the explicit row-op payload for this transformation.
196    #[must_use]
197    pub const fn row_ops(&self) -> &[SchemaMigrationRowOp] {
198        match self {
199            Self::ExplicitRowOps(row_ops) => row_ops.as_slice(),
200        }
201    }
202
203    pub(in crate::db) fn into_row_ops(self) -> Vec<SchemaMigrationRowOp> {
204        match self {
205            Self::ExplicitRowOps(row_ops) => row_ops,
206        }
207    }
208}
209
210///
211/// SchemaMigrationDescriptor
212///
213/// SchemaMigrationDescriptor is the schema-evolution authority for one
214/// high-level migration.
215/// It names the migration, freezes the monotonic version, records human-facing
216/// description text, and carries validated schema/data intent for planning.
217///
218
219#[derive(Clone, Debug)]
220pub struct SchemaMigrationDescriptor {
221    migration_id: EntityName,
222    version: u64,
223    description: String,
224    intent: SchemaMigrationStepIntent,
225    data_transformation: Option<SchemaDataTransformation>,
226}
227
228impl SchemaMigrationDescriptor {
229    /// Build one validated schema migration descriptor.
230    pub fn new(
231        migration_id: EntityName,
232        version: u64,
233        description: impl Into<String>,
234        intent: SchemaMigrationStepIntent,
235        data_transformation: Option<SchemaDataTransformation>,
236    ) -> Result<Self, InternalError> {
237        let description = description.into();
238        if version == 0 {
239            return Err(InternalError::schema_evolution_version_required(
240                migration_id.as_str(),
241            ));
242        }
243        if description.trim().is_empty() {
244            return Err(InternalError::schema_evolution_description_required(
245                migration_id.as_str(),
246            ));
247        }
248
249        Ok(Self {
250            migration_id,
251            version,
252            description,
253            intent,
254            data_transformation,
255        })
256    }
257
258    /// Return the canonical migration identity.
259    #[must_use]
260    pub const fn migration_id(&self) -> EntityName {
261        self.migration_id
262    }
263
264    /// Return the monotonic schema migration version.
265    #[must_use]
266    pub const fn version(&self) -> u64 {
267        self.version
268    }
269
270    /// Borrow the descriptor description.
271    #[must_use]
272    pub const fn description(&self) -> &str {
273        self.description.as_str()
274    }
275
276    /// Borrow the high-level schema change intent.
277    #[must_use]
278    pub const fn intent(&self) -> &SchemaMigrationStepIntent {
279        &self.intent
280    }
281
282    /// Borrow the optional data transformation.
283    #[must_use]
284    pub const fn data_transformation(&self) -> Option<&SchemaDataTransformation> {
285        self.data_transformation.as_ref()
286    }
287
288    pub(in crate::db) fn into_data_transformation(self) -> Option<SchemaDataTransformation> {
289        self.data_transformation
290    }
291}