1use std::collections::BTreeSet;
4
5use sha2::{Digest, Sha256};
6
7use crate::{
8 ConstraintSourceKey, EntitySourceKey, FieldSourceKey, MAX_SCHEMA_MIGRATION_ENTITIES,
9 MAX_SCHEMA_MIGRATION_RENAMES, MAX_SCHEMA_MIGRATION_TRANSFORMS, RelationSourceKey,
10 RuleSourceKey, ScalarLiteral, ScalarType, SchemaContractError, SchemaMigrationPlanDigest,
11 TypeSourceKey, check_len,
12};
13
14pub(crate) const MIGRATION_PROGRAM_VERSION_CURRENT: u16 = 1;
15const MIGRATION_PLAN_DIGEST_PROFILE: &[u8] = b"icydb.schema-migration-plan.v1";
16
17#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct DeclaredEntityVersion(u32);
20
21impl DeclaredEntityVersion {
22 pub const fn try_new(value: u32) -> Result<Self, SchemaContractError> {
28 if value == 0 {
29 return Err(SchemaContractError::InvalidEntityVersion);
30 }
31 Ok(Self(value))
32 }
33
34 #[must_use]
36 pub const fn get(self) -> u32 {
37 self.0
38 }
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
43pub enum SchemaMigrationRename {
44 Field {
46 from: FieldSourceKey,
48 to: FieldSourceKey,
50 },
51 NamedType {
53 from: TypeSourceKey,
55 to: TypeSourceKey,
57 },
58 EnumVariant {
60 named_type: TypeSourceKey,
62 from: TypeSourceKey,
64 to: TypeSourceKey,
66 },
67 RecordField {
69 named_type: TypeSourceKey,
71 from: FieldSourceKey,
73 to: FieldSourceKey,
75 },
76 Relation {
78 from: RelationSourceKey,
80 to: RelationSourceKey,
82 },
83 Constraint {
85 from: ConstraintSourceKey,
87 to: ConstraintSourceKey,
89 },
90 Rule {
92 named_type: TypeSourceKey,
94 from: RuleSourceKey,
96 to: RuleSourceKey,
98 },
99}
100
101impl SchemaMigrationRename {
102 const fn sort_key(&self) -> (u8, &str, &str, &str) {
103 match self {
104 Self::Field { from, to } => (0, "", from.as_str(), to.as_str()),
105 Self::NamedType { from, to } => (1, "", from.as_str(), to.as_str()),
106 Self::EnumVariant {
107 named_type,
108 from,
109 to,
110 } => (2, named_type.as_str(), from.as_str(), to.as_str()),
111 Self::RecordField {
112 named_type,
113 from,
114 to,
115 } => (3, named_type.as_str(), from.as_str(), to.as_str()),
116 Self::Relation { from, to } => (4, "", from.as_str(), to.as_str()),
117 Self::Constraint { from, to } => (5, "", from.as_str(), to.as_str()),
118 Self::Rule {
119 named_type,
120 from,
121 to,
122 } => (6, named_type.as_str(), from.as_str(), to.as_str()),
123 }
124 }
125
126 fn validate(&self) -> Result<(), SchemaContractError> {
127 let (_, _, from, to) = self.sort_key();
128 if from == to {
129 return Err(SchemaContractError::InvalidMigrationPlan);
130 }
131 Ok(())
132 }
133}
134
135#[derive(Clone, Debug, Eq, PartialEq)]
137pub enum SchemaMigrationTransform {
138 Fill {
140 to: FieldSourceKey,
142 literal: ScalarLiteral,
144 },
145 Copy {
147 from: FieldSourceKey,
149 to: FieldSourceKey,
151 },
152 CheckedCast {
154 from: FieldSourceKey,
156 to: FieldSourceKey,
158 target: ScalarType,
160 },
161 Coalesce {
163 from: FieldSourceKey,
165 to: FieldSourceKey,
167 literal: ScalarLiteral,
169 },
170}
171
172impl SchemaMigrationTransform {
173 #[must_use]
175 pub const fn target(&self) -> &FieldSourceKey {
176 match self {
177 Self::Fill { to, .. }
178 | Self::Copy { to, .. }
179 | Self::CheckedCast { to, .. }
180 | Self::Coalesce { to, .. } => to,
181 }
182 }
183
184 const fn sort_key(&self) -> (&str, u8, &str) {
185 match self {
186 Self::Fill { to, .. } => (to.as_str(), 0, ""),
187 Self::Copy { from, to } => (to.as_str(), 1, from.as_str()),
188 Self::CheckedCast { from, to, .. } => (to.as_str(), 2, from.as_str()),
189 Self::Coalesce { from, to, .. } => (to.as_str(), 3, from.as_str()),
190 }
191 }
192
193 fn validate(&self) -> Result<(), SchemaContractError> {
194 match self {
195 Self::Fill { literal, .. } | Self::Coalesce { literal, .. } => literal.validate(),
196 Self::Copy { from, to } if from == to => {
197 Err(SchemaContractError::InvalidMigrationTransform)
198 }
199 Self::CheckedCast { target, .. } if !is_v1_cast_target(*target) => {
200 Err(SchemaContractError::InvalidMigrationTransform)
201 }
202 Self::Copy { .. } | Self::CheckedCast { .. } => Ok(()),
203 }
204 }
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct EntityMigration {
210 entity: EntitySourceKey,
211 from: DeclaredEntityVersion,
212 from_name: Option<EntitySourceKey>,
213 renames: Vec<SchemaMigrationRename>,
214 transforms: Vec<SchemaMigrationTransform>,
215}
216
217impl EntityMigration {
218 pub fn try_new(
226 entity: EntitySourceKey,
227 from: DeclaredEntityVersion,
228 from_name: Option<EntitySourceKey>,
229 mut renames: Vec<SchemaMigrationRename>,
230 mut transforms: Vec<SchemaMigrationTransform>,
231 ) -> Result<Self, SchemaContractError> {
232 check_len(
233 "migration renames",
234 renames.len(),
235 MAX_SCHEMA_MIGRATION_RENAMES,
236 )?;
237 check_len(
238 "migration transforms",
239 transforms.len(),
240 MAX_SCHEMA_MIGRATION_TRANSFORMS,
241 )?;
242 if from_name.as_ref() == Some(&entity) {
243 return Err(SchemaContractError::InvalidMigrationPlan);
244 }
245 if from_name.is_none() && renames.is_empty() && transforms.is_empty() {
246 return Err(SchemaContractError::InvalidMigrationPlan);
247 }
248 for rename in &renames {
249 rename.validate()?;
250 }
251 for transform in &transforms {
252 transform.validate()?;
253 }
254 ensure_distinct_rename_ownership(&renames)?;
255 ensure_distinct_transform_targets(&transforms)?;
256 renames.sort_unstable_by(|left, right| left.sort_key().cmp(&right.sort_key()));
257 transforms.sort_unstable_by(|left, right| left.sort_key().cmp(&right.sort_key()));
258 Ok(Self {
259 entity,
260 from,
261 from_name,
262 renames,
263 transforms,
264 })
265 }
266
267 #[must_use]
269 pub const fn entity(&self) -> &EntitySourceKey {
270 &self.entity
271 }
272
273 #[must_use]
275 pub const fn from(&self) -> DeclaredEntityVersion {
276 self.from
277 }
278
279 #[must_use]
281 pub const fn from_name(&self) -> Option<&EntitySourceKey> {
282 self.from_name.as_ref()
283 }
284
285 #[must_use]
287 pub fn renames(&self) -> &[SchemaMigrationRename] {
288 &self.renames
289 }
290
291 #[must_use]
293 pub fn transforms(&self) -> &[SchemaMigrationTransform] {
294 &self.transforms
295 }
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct SchemaMigrationPlan {
301 program_version: u16,
302 transitions: Vec<EntityMigration>,
303 digest: SchemaMigrationPlanDigest,
304}
305
306impl SchemaMigrationPlan {
307 pub fn try_new(mut transitions: Vec<EntityMigration>) -> Result<Self, SchemaContractError> {
314 check_len(
315 "migration entity transitions",
316 transitions.len(),
317 MAX_SCHEMA_MIGRATION_ENTITIES,
318 )?;
319 if transitions.is_empty() {
320 return Err(SchemaContractError::InvalidMigrationPlan);
321 }
322 transitions.sort_unstable_by(|left, right| left.entity.cmp(&right.entity));
323 if transitions
324 .windows(2)
325 .any(|pair| pair[0].entity == pair[1].entity)
326 {
327 return Err(SchemaContractError::DuplicateMigrationTarget);
328 }
329 let mut predecessor_entities = BTreeSet::new();
330 for transition in &transitions {
331 let predecessor = transition.from_name.as_ref().unwrap_or(&transition.entity);
332 if !predecessor_entities.insert(predecessor.clone()) {
333 return Err(SchemaContractError::DuplicateMigrationSource);
334 }
335 }
336 let transition_bytes = crate::encode_migration_transitions_for_digest(&transitions)?;
337 let digest = digest_plan_transitions(&transition_bytes);
338 let plan = Self {
339 program_version: MIGRATION_PROGRAM_VERSION_CURRENT,
340 transitions,
341 digest,
342 };
343 Ok(plan)
344 }
345
346 #[must_use]
348 pub const fn program_version(&self) -> u16 {
349 self.program_version
350 }
351
352 #[must_use]
354 pub fn transitions(&self) -> &[EntityMigration] {
355 &self.transitions
356 }
357
358 #[must_use]
360 pub const fn digest(&self) -> SchemaMigrationPlanDigest {
361 self.digest
362 }
363
364 pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
365 if self.program_version != MIGRATION_PROGRAM_VERSION_CURRENT {
366 return Err(SchemaContractError::UnsupportedMigrationProgramVersion {
367 found: self.program_version,
368 supported: MIGRATION_PROGRAM_VERSION_CURRENT,
369 });
370 }
371 let rebuilt = Self::try_new(self.transitions.clone())?;
372 if rebuilt != *self {
373 return Err(SchemaContractError::NonCanonical);
374 }
375 Ok(())
376 }
377}
378
379fn digest_plan_transitions(bytes: &[u8]) -> SchemaMigrationPlanDigest {
380 let mut hasher = Sha256::new();
381 hasher.update(MIGRATION_PLAN_DIGEST_PROFILE);
382 hasher.update(MIGRATION_PROGRAM_VERSION_CURRENT.to_be_bytes());
383 hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
384 hasher.update(bytes);
385 SchemaMigrationPlanDigest::from_bytes(hasher.finalize().into())
386}
387
388fn ensure_distinct_rename_ownership(
389 renames: &[SchemaMigrationRename],
390) -> Result<(), SchemaContractError> {
391 let mut sources = BTreeSet::new();
392 let mut targets = BTreeSet::new();
393 for rename in renames {
394 let (kind, owner, from, to) = rename.sort_key();
395 if !sources.insert((kind, owner, from)) {
396 return Err(SchemaContractError::DuplicateMigrationSource);
397 }
398 if !targets.insert((kind, owner, to)) {
399 return Err(SchemaContractError::DuplicateMigrationTarget);
400 }
401 }
402 Ok(())
403}
404
405fn ensure_distinct_transform_targets(
406 transforms: &[SchemaMigrationTransform],
407) -> Result<(), SchemaContractError> {
408 let mut targets = BTreeSet::new();
409 for transform in transforms {
410 if !targets.insert(transform.target()) {
411 return Err(SchemaContractError::DuplicateMigrationTarget);
412 }
413 }
414 Ok(())
415}
416
417const fn is_v1_cast_target(target: ScalarType) -> bool {
418 matches!(
419 target,
420 ScalarType::Int8
421 | ScalarType::Int16
422 | ScalarType::Int32
423 | ScalarType::Int64
424 | ScalarType::Int128
425 | ScalarType::Nat8
426 | ScalarType::Nat16
427 | ScalarType::Nat32
428 | ScalarType::Nat64
429 | ScalarType::Nat128
430 | ScalarType::Decimal { .. }
431 )
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::{
438 MAX_SCHEMA_MIGRATION_PLAN_BYTES, decode_schema_migration_plan, encode_schema_migration_plan,
439 };
440
441 fn entity(value: &str) -> EntitySourceKey {
442 EntitySourceKey::try_new(value).expect("entity source should admit")
443 }
444
445 fn field(value: &str) -> FieldSourceKey {
446 FieldSourceKey::try_new(value).expect("field source should admit")
447 }
448
449 fn transition(entity_name: &str, from_name: &str) -> EntityMigration {
450 EntityMigration::try_new(
451 entity(entity_name),
452 DeclaredEntityVersion::try_new(1).expect("version should admit"),
453 Some(entity(from_name)),
454 vec![SchemaMigrationRename::Field {
455 from: field("old_value"),
456 to: field("value"),
457 }],
458 Vec::new(),
459 )
460 .expect("transition should admit")
461 }
462
463 #[test]
464 fn declared_entity_version_is_strictly_positive() {
465 assert_eq!(
466 DeclaredEntityVersion::try_new(0),
467 Err(SchemaContractError::InvalidEntityVersion),
468 );
469 assert_eq!(
470 DeclaredEntityVersion::try_new(1)
471 .expect("version one should admit")
472 .get(),
473 1,
474 );
475 }
476
477 #[test]
478 fn plan_canonicalization_is_declaration_order_independent() {
479 let account = transition("Account", "User");
480 let article = transition("Article", "Post");
481 let first = SchemaMigrationPlan::try_new(vec![account.clone(), article.clone()])
482 .expect("plan should admit");
483 let reverse = SchemaMigrationPlan::try_new(vec![article, account])
484 .expect("reverse plan should admit");
485
486 assert_eq!(first, reverse);
487 assert_eq!(first.digest(), reverse.digest());
488 let encoded = encode_schema_migration_plan(&first).expect("plan should encode");
489 assert_eq!(
490 decode_schema_migration_plan(&encoded).expect("plan should decode"),
491 first,
492 );
493 }
494
495 #[test]
496 fn every_current_migration_operation_roundtrips_exactly() {
497 let transition = EntityMigration::try_new(
498 entity("Account"),
499 DeclaredEntityVersion::try_new(3).expect("version should admit"),
500 Some(entity("User")),
501 vec![
502 SchemaMigrationRename::Field {
503 from: field("old_field"),
504 to: field("new_field"),
505 },
506 SchemaMigrationRename::NamedType {
507 from: TypeSourceKey::try_new("OldType").expect("type should admit"),
508 to: TypeSourceKey::try_new("NewType").expect("type should admit"),
509 },
510 SchemaMigrationRename::EnumVariant {
511 named_type: TypeSourceKey::try_new("OldEnum").expect("type should admit"),
512 from: TypeSourceKey::try_new("OldVariant").expect("variant should admit"),
513 to: TypeSourceKey::try_new("NewVariant").expect("variant should admit"),
514 },
515 SchemaMigrationRename::RecordField {
516 named_type: TypeSourceKey::try_new("OldRecord").expect("type should admit"),
517 from: field("old_member"),
518 to: field("new_member"),
519 },
520 SchemaMigrationRename::Relation {
521 from: RelationSourceKey::try_new("old_relation")
522 .expect("relation should admit"),
523 to: RelationSourceKey::try_new("new_relation").expect("relation should admit"),
524 },
525 SchemaMigrationRename::Constraint {
526 from: ConstraintSourceKey::try_new("old_constraint")
527 .expect("constraint should admit"),
528 to: ConstraintSourceKey::try_new("new_constraint")
529 .expect("constraint should admit"),
530 },
531 SchemaMigrationRename::Rule {
532 named_type: TypeSourceKey::try_new("OldRuleOwner").expect("type should admit"),
533 from: RuleSourceKey::try_new("old_rule").expect("rule should admit"),
534 to: RuleSourceKey::try_new("new_rule").expect("rule should admit"),
535 },
536 ],
537 vec![
538 SchemaMigrationTransform::Fill {
539 to: field("filled"),
540 literal: ScalarLiteral::Nat(1),
541 },
542 SchemaMigrationTransform::Copy {
543 from: field("copy_source"),
544 to: field("copied"),
545 },
546 SchemaMigrationTransform::CheckedCast {
547 from: field("cast_source"),
548 to: field("casted"),
549 target: ScalarType::Nat64,
550 },
551 SchemaMigrationTransform::Coalesce {
552 from: field("nullable_source"),
553 to: field("coalesced"),
554 literal: ScalarLiteral::Nat(0),
555 },
556 ],
557 )
558 .expect("transition should admit");
559 let plan = SchemaMigrationPlan::try_new(vec![transition]).expect("plan should admit");
560 let encoded = encode_schema_migration_plan(&plan).expect("plan should encode");
561
562 assert_eq!(
563 decode_schema_migration_plan(&encoded).expect("plan should decode"),
564 plan,
565 );
566 }
567
568 #[test]
569 fn plan_digest_has_one_fixed_current_vector() {
570 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
571 .expect("plan should admit");
572 assert_eq!(
573 plan.digest().to_bytes(),
574 [
575 198, 22, 117, 206, 250, 126, 65, 102, 162, 77, 246, 242, 60, 127, 68, 242, 56, 105,
576 141, 56, 136, 233, 54, 180, 100, 187, 24, 63, 167, 158, 239, 93,
577 ],
578 );
579 }
580
581 #[test]
582 fn duplicate_targets_predecessors_and_transform_targets_reject() {
583 let account = transition("Account", "User");
584 assert_eq!(
585 SchemaMigrationPlan::try_new(vec![account.clone(), account]),
586 Err(SchemaContractError::DuplicateMigrationTarget),
587 );
588 assert_eq!(
589 SchemaMigrationPlan::try_new(vec![
590 transition("Account", "User"),
591 transition("Profile", "User"),
592 ]),
593 Err(SchemaContractError::DuplicateMigrationSource),
594 );
595 assert_eq!(
596 EntityMigration::try_new(
597 entity("Account"),
598 DeclaredEntityVersion::try_new(1).expect("version should admit"),
599 None,
600 Vec::new(),
601 vec![
602 SchemaMigrationTransform::Fill {
603 to: field("value"),
604 literal: ScalarLiteral::Nat(1),
605 },
606 SchemaMigrationTransform::Copy {
607 from: field("old_value"),
608 to: field("value"),
609 },
610 ],
611 ),
612 Err(SchemaContractError::DuplicateMigrationTarget),
613 );
614 }
615
616 #[test]
617 fn obsolete_programs_and_oversized_transport_fail_closed() {
618 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
619 .expect("plan should admit");
620 let mut bytes = encode_schema_migration_plan(&plan).expect("plan should encode");
621 bytes[5..7].copy_from_slice(&2_u16.to_be_bytes());
622 assert_eq!(
623 decode_schema_migration_plan(&bytes),
624 Err(SchemaContractError::UnsupportedMigrationProgramVersion {
625 found: 2,
626 supported: 1,
627 }),
628 );
629 assert!(matches!(
630 decode_schema_migration_plan(&vec![0; MAX_SCHEMA_MIGRATION_PLAN_BYTES + 1]),
631 Err(SchemaContractError::EncodedTooLarge { .. }),
632 ));
633 }
634}