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 pub(crate) 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(
229 entity: EntitySourceKey,
230 from: DeclaredEntityVersion,
231 from_name: Option<EntitySourceKey>,
232 mut renames: Vec<SchemaMigrationRename>,
233 mut transforms: Vec<SchemaMigrationTransform>,
234 ) -> Result<Self, SchemaContractError> {
235 check_len(
236 "migration renames",
237 renames.len(),
238 MAX_SCHEMA_MIGRATION_RENAMES,
239 )?;
240 check_len(
241 "migration transforms",
242 transforms.len(),
243 MAX_SCHEMA_MIGRATION_TRANSFORMS,
244 )?;
245 if from_name.as_ref() == Some(&entity) {
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 crate::compact_sort_unstable_by(&mut renames, |left, right| {
257 left.sort_key().cmp(&right.sort_key())
258 });
259 crate::compact_sort_unstable_by(&mut transforms, |left, right| {
260 left.sort_key().cmp(&right.sort_key())
261 });
262 Ok(Self {
263 entity,
264 from,
265 from_name,
266 renames,
267 transforms,
268 })
269 }
270
271 #[must_use]
273 pub const fn entity(&self) -> &EntitySourceKey {
274 &self.entity
275 }
276
277 #[must_use]
279 pub const fn from(&self) -> DeclaredEntityVersion {
280 self.from
281 }
282
283 #[must_use]
285 pub const fn from_name(&self) -> Option<&EntitySourceKey> {
286 self.from_name.as_ref()
287 }
288
289 #[must_use]
291 pub fn renames(&self) -> &[SchemaMigrationRename] {
292 &self.renames
293 }
294
295 #[must_use]
297 pub fn transforms(&self) -> &[SchemaMigrationTransform] {
298 &self.transforms
299 }
300}
301
302#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct SchemaMigrationPlan {
305 program_version: u16,
306 transitions: Vec<EntityMigration>,
307 digest: SchemaMigrationPlanDigest,
308}
309
310impl SchemaMigrationPlan {
311 pub fn try_new(mut transitions: Vec<EntityMigration>) -> Result<Self, SchemaContractError> {
318 check_len(
319 "migration entity transitions",
320 transitions.len(),
321 MAX_SCHEMA_MIGRATION_ENTITIES,
322 )?;
323 if transitions.is_empty() {
324 return Err(SchemaContractError::InvalidMigrationPlan);
325 }
326 crate::compact_sort_unstable_by(&mut transitions, |left, right| {
327 left.entity.cmp(&right.entity)
328 });
329 if transitions
330 .windows(2)
331 .any(|pair| pair[0].entity == pair[1].entity)
332 {
333 return Err(SchemaContractError::DuplicateMigrationTarget);
334 }
335 let mut predecessor_entities = BTreeSet::new();
336 for transition in &transitions {
337 let predecessor = transition.from_name.as_ref().unwrap_or(&transition.entity);
338 if !predecessor_entities.insert(predecessor.clone()) {
339 return Err(SchemaContractError::DuplicateMigrationSource);
340 }
341 }
342 let transition_bytes = crate::encode_migration_transitions_for_digest(&transitions)?;
343 let digest = digest_plan_transitions(&transition_bytes);
344 let plan = Self {
345 program_version: MIGRATION_PROGRAM_VERSION_CURRENT,
346 transitions,
347 digest,
348 };
349 Ok(plan)
350 }
351
352 pub fn validate_target_fragment(
363 &self,
364 fragment: &crate::SchemaFragment,
365 ) -> Result<(), SchemaContractError> {
366 self.validate()?;
367 fragment.validate()?;
368 let entities = fragment
369 .entities()
370 .iter()
371 .map(|entity| (entity.source_key().clone(), entity))
372 .collect();
373 let types = fragment
374 .types()
375 .iter()
376 .map(|ty| (ty.source_key().clone(), ty))
377 .collect();
378 crate::proposal::validate_proposal_closure(
379 &crate::ExpectedAcceptedHead::Empty,
380 &entities,
381 &types,
382 &[],
383 )?;
384 crate::proposal::validate_migration_plan(self, &entities, &types)
385 }
386
387 #[must_use]
389 pub const fn program_version(&self) -> u16 {
390 self.program_version
391 }
392
393 #[must_use]
395 pub fn transitions(&self) -> &[EntityMigration] {
396 &self.transitions
397 }
398
399 #[must_use]
401 pub const fn digest(&self) -> SchemaMigrationPlanDigest {
402 self.digest
403 }
404
405 pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
406 if self.program_version != MIGRATION_PROGRAM_VERSION_CURRENT {
407 return Err(SchemaContractError::UnsupportedMigrationProgramVersion {
408 found: self.program_version,
409 supported: MIGRATION_PROGRAM_VERSION_CURRENT,
410 });
411 }
412 let rebuilt = Self::try_new(self.transitions.clone())?;
413 if rebuilt != *self {
414 return Err(SchemaContractError::NonCanonical);
415 }
416 Ok(())
417 }
418}
419
420fn digest_plan_transitions(bytes: &[u8]) -> SchemaMigrationPlanDigest {
421 let mut hasher = Sha256::new();
422 hasher.update(MIGRATION_PLAN_DIGEST_PROFILE);
423 hasher.update(MIGRATION_PROGRAM_VERSION_CURRENT.to_be_bytes());
424 hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
425 hasher.update(bytes);
426 SchemaMigrationPlanDigest::from_bytes(hasher.finalize().into())
427}
428
429fn ensure_distinct_rename_ownership(
430 renames: &[SchemaMigrationRename],
431) -> Result<(), SchemaContractError> {
432 let mut sources = BTreeSet::new();
433 let mut targets = BTreeSet::new();
434 for rename in renames {
435 let (kind, owner, from, to) = rename.sort_key();
436 if !sources.insert((kind, owner, from)) {
437 return Err(SchemaContractError::DuplicateMigrationSource);
438 }
439 if !targets.insert((kind, owner, to)) {
440 return Err(SchemaContractError::DuplicateMigrationTarget);
441 }
442 }
443 Ok(())
444}
445
446fn ensure_distinct_transform_targets(
447 transforms: &[SchemaMigrationTransform],
448) -> Result<(), SchemaContractError> {
449 let mut targets = BTreeSet::new();
450 for transform in transforms {
451 if !targets.insert(transform.target()) {
452 return Err(SchemaContractError::DuplicateMigrationTarget);
453 }
454 }
455 Ok(())
456}
457
458const fn is_v1_cast_target(target: ScalarType) -> bool {
459 matches!(
460 target,
461 ScalarType::Int8
462 | ScalarType::Int16
463 | ScalarType::Int32
464 | ScalarType::Int64
465 | ScalarType::Int128
466 | ScalarType::Nat8
467 | ScalarType::Nat16
468 | ScalarType::Nat32
469 | ScalarType::Nat64
470 | ScalarType::Nat128
471 | ScalarType::Decimal { .. }
472 )
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::{
479 MAX_SCHEMA_MIGRATION_PLAN_BYTES, decode_schema_migration_plan, encode_schema_migration_plan,
480 };
481
482 fn entity(value: &str) -> EntitySourceKey {
483 EntitySourceKey::try_new(value).expect("entity source should admit")
484 }
485
486 fn field(value: &str) -> FieldSourceKey {
487 FieldSourceKey::try_new(value).expect("field source should admit")
488 }
489
490 fn transition(entity_name: &str, from_name: &str) -> EntityMigration {
491 EntityMigration::try_new(
492 entity(entity_name),
493 DeclaredEntityVersion::try_new(1).expect("version should admit"),
494 Some(entity(from_name)),
495 vec![SchemaMigrationRename::Field {
496 from: field("old_value"),
497 to: field("value"),
498 }],
499 Vec::new(),
500 )
501 .expect("transition should admit")
502 }
503
504 #[test]
505 fn declared_entity_version_is_strictly_positive() {
506 assert_eq!(
507 DeclaredEntityVersion::try_new(0),
508 Err(SchemaContractError::InvalidEntityVersion),
509 );
510 assert_eq!(
511 DeclaredEntityVersion::try_new(1)
512 .expect("version one should admit")
513 .get(),
514 1,
515 );
516 }
517
518 #[test]
519 fn plan_canonicalization_is_declaration_order_independent() {
520 let account = transition("Account", "User");
521 let article = transition("Article", "Post");
522 let first = SchemaMigrationPlan::try_new(vec![account.clone(), article.clone()])
523 .expect("plan should admit");
524 let reverse = SchemaMigrationPlan::try_new(vec![article, account])
525 .expect("reverse plan should admit");
526
527 assert_eq!(first, reverse);
528 assert_eq!(first.digest(), reverse.digest());
529 let encoded = encode_schema_migration_plan(&first).expect("plan should encode");
530 assert_eq!(
531 decode_schema_migration_plan(&encoded).expect("plan should decode"),
532 first,
533 );
534 }
535
536 #[test]
537 fn every_current_migration_operation_roundtrips_exactly() {
538 let transition = EntityMigration::try_new(
539 entity("Account"),
540 DeclaredEntityVersion::try_new(3).expect("version should admit"),
541 Some(entity("User")),
542 vec![
543 SchemaMigrationRename::Field {
544 from: field("old_field"),
545 to: field("new_field"),
546 },
547 SchemaMigrationRename::NamedType {
548 from: TypeSourceKey::try_new("OldType").expect("type should admit"),
549 to: TypeSourceKey::try_new("NewType").expect("type should admit"),
550 },
551 SchemaMigrationRename::EnumVariant {
552 named_type: TypeSourceKey::try_new("OldEnum").expect("type should admit"),
553 from: TypeSourceKey::try_new("OldVariant").expect("variant should admit"),
554 to: TypeSourceKey::try_new("NewVariant").expect("variant should admit"),
555 },
556 SchemaMigrationRename::RecordField {
557 named_type: TypeSourceKey::try_new("OldRecord").expect("type should admit"),
558 from: field("old_member"),
559 to: field("new_member"),
560 },
561 SchemaMigrationRename::Relation {
562 from: RelationSourceKey::try_new("old_relation")
563 .expect("relation should admit"),
564 to: RelationSourceKey::try_new("new_relation").expect("relation should admit"),
565 },
566 SchemaMigrationRename::Constraint {
567 from: ConstraintSourceKey::try_new("old_constraint")
568 .expect("constraint should admit"),
569 to: ConstraintSourceKey::try_new("new_constraint")
570 .expect("constraint should admit"),
571 },
572 SchemaMigrationRename::Rule {
573 named_type: TypeSourceKey::try_new("OldRuleOwner").expect("type should admit"),
574 from: RuleSourceKey::try_new("old_rule").expect("rule should admit"),
575 to: RuleSourceKey::try_new("new_rule").expect("rule should admit"),
576 },
577 ],
578 vec![
579 SchemaMigrationTransform::Fill {
580 to: field("filled"),
581 literal: ScalarLiteral::Nat(1),
582 },
583 SchemaMigrationTransform::Copy {
584 from: field("copy_source"),
585 to: field("copied"),
586 },
587 SchemaMigrationTransform::CheckedCast {
588 from: field("cast_source"),
589 to: field("casted"),
590 target: ScalarType::Nat64,
591 },
592 SchemaMigrationTransform::Coalesce {
593 from: field("nullable_source"),
594 to: field("coalesced"),
595 literal: ScalarLiteral::Nat(0),
596 },
597 ],
598 )
599 .expect("transition should admit");
600 let plan = SchemaMigrationPlan::try_new(vec![transition]).expect("plan should admit");
601 let encoded = encode_schema_migration_plan(&plan).expect("plan should encode");
602
603 assert_eq!(
604 decode_schema_migration_plan(&encoded).expect("plan should decode"),
605 plan,
606 );
607 }
608
609 #[test]
610 fn plan_digest_has_one_fixed_current_vector() {
611 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
612 .expect("plan should admit");
613 assert_eq!(
614 plan.digest().to_bytes(),
615 [
616 198, 22, 117, 206, 250, 126, 65, 102, 162, 77, 246, 242, 60, 127, 68, 242, 56, 105,
617 141, 56, 136, 233, 54, 180, 100, 187, 24, 63, 167, 158, 239, 93,
618 ],
619 );
620 }
621
622 #[test]
623 fn dependency_only_companion_roundtrips_exactly() {
624 let companion = EntityMigration::try_new(
625 entity("Holder"),
626 DeclaredEntityVersion::try_new(1).unwrap(),
627 None,
628 Vec::new(),
629 Vec::new(),
630 )
631 .unwrap();
632 let plan = SchemaMigrationPlan::try_new(vec![transition("CatalogItem", "Item"), companion])
633 .unwrap();
634 let encoded = crate::encode_schema_migration_plan(&plan).unwrap();
635 assert_eq!(crate::decode_schema_migration_plan(&encoded).unwrap(), plan);
636 }
637
638 #[test]
639 fn duplicate_targets_predecessors_and_transform_targets_reject() {
640 let account = transition("Account", "User");
641 assert_eq!(
642 SchemaMigrationPlan::try_new(vec![account.clone(), account]),
643 Err(SchemaContractError::DuplicateMigrationTarget),
644 );
645 assert_eq!(
646 SchemaMigrationPlan::try_new(vec![
647 transition("Account", "User"),
648 transition("Profile", "User"),
649 ]),
650 Err(SchemaContractError::DuplicateMigrationSource),
651 );
652 assert_eq!(
653 EntityMigration::try_new(
654 entity("Account"),
655 DeclaredEntityVersion::try_new(1).expect("version should admit"),
656 None,
657 Vec::new(),
658 vec![
659 SchemaMigrationTransform::Fill {
660 to: field("value"),
661 literal: ScalarLiteral::Nat(1),
662 },
663 SchemaMigrationTransform::Copy {
664 from: field("old_value"),
665 to: field("value"),
666 },
667 ],
668 ),
669 Err(SchemaContractError::DuplicateMigrationTarget),
670 );
671 }
672
673 #[test]
674 fn unsupported_program_versions_and_oversized_transport_fail_closed() {
675 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
676 .expect("plan should admit");
677 let mut bytes = encode_schema_migration_plan(&plan).expect("plan should encode");
678 bytes[5..7].copy_from_slice(&2_u16.to_be_bytes());
679 assert_eq!(
680 decode_schema_migration_plan(&bytes),
681 Err(SchemaContractError::UnsupportedMigrationProgramVersion {
682 found: 2,
683 supported: 1,
684 }),
685 );
686 assert!(matches!(
687 decode_schema_migration_plan(&vec![0; MAX_SCHEMA_MIGRATION_PLAN_BYTES + 1]),
688 Err(SchemaContractError::EncodedTooLarge { .. }),
689 ));
690 }
691}