1use std::collections::BTreeSet;
4
5use sha2::{Digest, Sha256};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 ConstraintSourceKey, EntitySourceKey, FieldSourceKey, MAX_SCHEMA_MIGRATION_ENTITIES,
11 MAX_SCHEMA_MIGRATION_RENAMES, MAX_SCHEMA_MIGRATION_TRANSFORMS, RelationSourceKey,
12 RuleSourceKey, ScalarLiteral, ScalarType, SchemaContractError, SchemaMigrationPlanDigest,
13 TypeSourceKey, check_len,
14};
15
16pub(crate) const MIGRATION_PROGRAM_VERSION_CURRENT: u16 = 1;
17const MIGRATION_PLAN_DIGEST_PROFILE: &[u8] = b"icydb.schema-migration-plan.wire.v1";
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
21pub struct DeclaredEntityVersion(u32);
22
23impl DeclaredEntityVersion {
24 pub const fn try_new(value: u32) -> Result<Self, SchemaContractError> {
30 if value == 0 {
31 return Err(SchemaContractError::InvalidEntityVersion);
32 }
33 Ok(Self(value))
34 }
35
36 #[must_use]
38 pub const fn get(self) -> u32 {
39 self.0
40 }
41}
42
43impl<'de> Deserialize<'de> for DeclaredEntityVersion {
44 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45 Self::try_new(u32::deserialize(deserializer)?).map_err(serde::de::Error::custom)
46 }
47}
48
49#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub enum SchemaMigrationRename {
52 Field {
54 #[serde(
56 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
57 )]
58 from: FieldSourceKey,
59 #[serde(
61 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
62 )]
63 to: FieldSourceKey,
64 },
65 NamedType {
67 #[serde(
69 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
70 )]
71 from: TypeSourceKey,
72 #[serde(
74 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
75 )]
76 to: TypeSourceKey,
77 },
78 EnumVariant {
80 #[serde(
82 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
83 )]
84 named_type: TypeSourceKey,
85 #[serde(
87 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
88 )]
89 from: TypeSourceKey,
90 #[serde(
92 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
93 )]
94 to: TypeSourceKey,
95 },
96 RecordField {
98 #[serde(
100 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
101 )]
102 named_type: TypeSourceKey,
103 #[serde(
105 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
106 )]
107 from: FieldSourceKey,
108 #[serde(
110 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
111 )]
112 to: FieldSourceKey,
113 },
114 Relation {
116 #[serde(
118 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
119 )]
120 from: RelationSourceKey,
121 #[serde(
123 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
124 )]
125 to: RelationSourceKey,
126 },
127 Constraint {
129 #[serde(
131 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
132 )]
133 from: ConstraintSourceKey,
134 #[serde(
136 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
137 )]
138 to: ConstraintSourceKey,
139 },
140 Rule {
142 #[serde(
144 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
145 )]
146 named_type: TypeSourceKey,
147 #[serde(
149 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
150 )]
151 from: RuleSourceKey,
152 #[serde(
154 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
155 )]
156 to: RuleSourceKey,
157 },
158}
159
160impl SchemaMigrationRename {
161 pub(crate) const fn sort_key(&self) -> (u8, &str, &str, &str) {
162 match self {
163 Self::Field { from, to } => (0, "", from.as_str(), to.as_str()),
164 Self::NamedType { from, to } => (1, "", from.as_str(), to.as_str()),
165 Self::EnumVariant {
166 named_type,
167 from,
168 to,
169 } => (2, named_type.as_str(), from.as_str(), to.as_str()),
170 Self::RecordField {
171 named_type,
172 from,
173 to,
174 } => (3, named_type.as_str(), from.as_str(), to.as_str()),
175 Self::Relation { from, to } => (4, "", from.as_str(), to.as_str()),
176 Self::Constraint { from, to } => (5, "", from.as_str(), to.as_str()),
177 Self::Rule {
178 named_type,
179 from,
180 to,
181 } => (6, named_type.as_str(), from.as_str(), to.as_str()),
182 }
183 }
184
185 fn validate(&self) -> Result<(), SchemaContractError> {
186 let (_, _, from, to) = self.sort_key();
187 if from == to {
188 return Err(SchemaContractError::InvalidMigrationPlan);
189 }
190 Ok(())
191 }
192}
193
194#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196pub enum SchemaMigrationTransform {
197 Fill {
199 #[serde(
201 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
202 )]
203 to: FieldSourceKey,
204 literal: ScalarLiteral,
206 },
207 Copy {
209 #[serde(
211 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
212 )]
213 from: FieldSourceKey,
214 #[serde(
216 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
217 )]
218 to: FieldSourceKey,
219 },
220 CheckedCast {
222 #[serde(
224 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
225 )]
226 from: FieldSourceKey,
227 #[serde(
229 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
230 )]
231 to: FieldSourceKey,
232 target: ScalarType,
234 },
235 Coalesce {
237 #[serde(
239 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
240 )]
241 from: FieldSourceKey,
242 #[serde(
244 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
245 )]
246 to: FieldSourceKey,
247 literal: ScalarLiteral,
249 },
250}
251
252impl SchemaMigrationTransform {
253 #[must_use]
255 pub const fn target(&self) -> &FieldSourceKey {
256 match self {
257 Self::Fill { to, .. }
258 | Self::Copy { to, .. }
259 | Self::CheckedCast { to, .. }
260 | Self::Coalesce { to, .. } => to,
261 }
262 }
263
264 const fn sort_key(&self) -> (&str, u8, &str) {
265 match self {
266 Self::Fill { to, .. } => (to.as_str(), 0, ""),
267 Self::Copy { from, to } => (to.as_str(), 1, from.as_str()),
268 Self::CheckedCast { from, to, .. } => (to.as_str(), 2, from.as_str()),
269 Self::Coalesce { from, to, .. } => (to.as_str(), 3, from.as_str()),
270 }
271 }
272
273 fn validate(&self) -> Result<(), SchemaContractError> {
274 match self {
275 Self::Fill { literal, .. } | Self::Coalesce { literal, .. } => literal.validate(),
276 Self::Copy { from, to } if from == to => {
277 Err(SchemaContractError::InvalidMigrationTransform)
278 }
279 Self::CheckedCast { target, .. } if !is_v1_cast_target(*target) => {
280 Err(SchemaContractError::InvalidMigrationTransform)
281 }
282 Self::Copy { .. } | Self::CheckedCast { .. } => Ok(()),
283 }
284 }
285}
286
287#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
289pub struct EntityMigration {
290 #[serde(
291 deserialize_with = "crate::codec::deserialize_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
292 )]
293 entity: EntitySourceKey,
294 from: DeclaredEntityVersion,
295 #[serde(
296 deserialize_with = "crate::codec::deserialize_optional_key::<_, _, { crate::MAX_SOURCE_KEY_BYTES }>"
297 )]
298 from_name: Option<EntitySourceKey>,
299 #[serde(
300 deserialize_with = "crate::codec::deserialize_list::<_, _, { crate::MAX_SCHEMA_MIGRATION_RENAMES }>"
301 )]
302 renames: Vec<SchemaMigrationRename>,
303 #[serde(
304 deserialize_with = "crate::codec::deserialize_list::<_, _, { crate::MAX_SCHEMA_MIGRATION_TRANSFORMS }>"
305 )]
306 transforms: Vec<SchemaMigrationTransform>,
307}
308
309impl EntityMigration {
310 pub fn try_new(
321 entity: EntitySourceKey,
322 from: DeclaredEntityVersion,
323 from_name: Option<EntitySourceKey>,
324 mut renames: Vec<SchemaMigrationRename>,
325 mut transforms: Vec<SchemaMigrationTransform>,
326 ) -> Result<Self, SchemaContractError> {
327 check_len(
328 "migration renames",
329 renames.len(),
330 MAX_SCHEMA_MIGRATION_RENAMES,
331 )?;
332 check_len(
333 "migration transforms",
334 transforms.len(),
335 MAX_SCHEMA_MIGRATION_TRANSFORMS,
336 )?;
337 if from_name.as_ref() == Some(&entity) {
338 return Err(SchemaContractError::InvalidMigrationPlan);
339 }
340 for rename in &renames {
341 rename.validate()?;
342 }
343 for transform in &transforms {
344 transform.validate()?;
345 }
346 ensure_distinct_rename_ownership(&renames)?;
347 ensure_distinct_transform_targets(&transforms)?;
348 crate::compact_sort_unstable_by(&mut renames, |left, right| {
349 left.sort_key().cmp(&right.sort_key())
350 });
351 crate::compact_sort_unstable_by(&mut transforms, |left, right| {
352 left.sort_key().cmp(&right.sort_key())
353 });
354 Ok(Self {
355 entity,
356 from,
357 from_name,
358 renames,
359 transforms,
360 })
361 }
362
363 fn validate(&self) -> Result<(), SchemaContractError> {
366 let rebuilt = Self::try_new(
367 self.entity.clone(),
368 self.from,
369 self.from_name.clone(),
370 self.renames.clone(),
371 self.transforms.clone(),
372 )?;
373 if rebuilt != *self {
374 return Err(SchemaContractError::NonCanonical);
375 }
376 Ok(())
377 }
378
379 #[must_use]
381 pub const fn entity(&self) -> &EntitySourceKey {
382 &self.entity
383 }
384
385 #[must_use]
387 pub const fn from(&self) -> DeclaredEntityVersion {
388 self.from
389 }
390
391 #[must_use]
393 pub const fn from_name(&self) -> Option<&EntitySourceKey> {
394 self.from_name.as_ref()
395 }
396
397 #[must_use]
399 pub fn renames(&self) -> &[SchemaMigrationRename] {
400 &self.renames
401 }
402
403 #[must_use]
405 pub fn transforms(&self) -> &[SchemaMigrationTransform] {
406 &self.transforms
407 }
408}
409
410#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
412pub struct SchemaMigrationPlan {
413 program_version: u16,
414 #[serde(
415 deserialize_with = "crate::codec::deserialize_list::<_, _, { crate::MAX_SCHEMA_MIGRATION_ENTITIES }>"
416 )]
417 transitions: Vec<EntityMigration>,
418 #[serde(skip)]
419 digest: SchemaMigrationPlanDigest,
420}
421
422impl SchemaMigrationPlan {
423 pub fn try_new(mut transitions: Vec<EntityMigration>) -> Result<Self, SchemaContractError> {
430 check_len(
431 "migration entity transitions",
432 transitions.len(),
433 MAX_SCHEMA_MIGRATION_ENTITIES,
434 )?;
435 if transitions.is_empty() {
436 return Err(SchemaContractError::InvalidMigrationPlan);
437 }
438 crate::compact_sort_unstable_by(&mut transitions, |left, right| {
439 left.entity.cmp(&right.entity)
440 });
441 if transitions
442 .windows(2)
443 .any(|pair| pair[0].entity == pair[1].entity)
444 {
445 return Err(SchemaContractError::DuplicateMigrationTarget);
446 }
447 let mut predecessor_entities = BTreeSet::new();
448 for transition in &transitions {
449 transition.validate()?;
450 let predecessor = transition.from_name.as_ref().unwrap_or(&transition.entity);
451 if !predecessor_entities.insert(predecessor.clone()) {
452 return Err(SchemaContractError::DuplicateMigrationSource);
453 }
454 }
455 let transition_bytes = crate::encode_migration_transitions_for_digest(&transitions)?;
456 let digest = digest_plan_transitions(&transition_bytes);
457 let plan = Self {
458 program_version: MIGRATION_PROGRAM_VERSION_CURRENT,
459 transitions,
460 digest,
461 };
462 Ok(plan)
463 }
464
465 pub fn validate_target_fragment(
476 &self,
477 fragment: &crate::SchemaFragment,
478 ) -> Result<(), SchemaContractError> {
479 self.validate()?;
480 fragment.validate()?;
481 let entities = fragment
482 .entities()
483 .iter()
484 .map(|entity| (entity.source_key().clone(), entity))
485 .collect();
486 let types = fragment
487 .types()
488 .iter()
489 .map(|ty| (ty.source_key().clone(), ty))
490 .collect();
491 crate::proposal::validate_proposal_closure(
492 &crate::ExpectedAcceptedHead::Empty,
493 &entities,
494 &types,
495 &[],
496 )?;
497 crate::proposal::validate_migration_plan(self, &entities, &types)
498 }
499
500 #[must_use]
502 pub const fn program_version(&self) -> u16 {
503 self.program_version
504 }
505
506 #[must_use]
508 pub fn transitions(&self) -> &[EntityMigration] {
509 &self.transitions
510 }
511
512 #[must_use]
514 pub const fn digest(&self) -> SchemaMigrationPlanDigest {
515 self.digest
516 }
517
518 pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
519 if self.program_version != MIGRATION_PROGRAM_VERSION_CURRENT {
520 return Err(SchemaContractError::UnsupportedMigrationProgramVersion {
521 found: self.program_version,
522 supported: MIGRATION_PROGRAM_VERSION_CURRENT,
523 });
524 }
525 let rebuilt = Self::try_new(self.transitions.clone())?;
526 if rebuilt != *self {
527 return Err(SchemaContractError::NonCanonical);
528 }
529 Ok(())
530 }
531}
532
533impl<'de> Deserialize<'de> for SchemaMigrationPlan {
536 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
537 #[derive(Deserialize)]
538 struct Payload {
539 program_version: u16,
540 #[serde(
541 deserialize_with = "crate::codec::deserialize_list::<_, _, { crate::MAX_SCHEMA_MIGRATION_ENTITIES }>"
542 )]
543 transitions: Vec<EntityMigration>,
544 }
545 let payload = Payload::deserialize(deserializer)?;
546 let mut plan = Self::try_new(payload.transitions).map_err(serde::de::Error::custom)?;
547 plan.program_version = payload.program_version;
548 Ok(plan)
549 }
550}
551
552fn digest_plan_transitions(bytes: &[u8]) -> SchemaMigrationPlanDigest {
553 let mut hasher = Sha256::new();
554 hasher.update(MIGRATION_PLAN_DIGEST_PROFILE);
555 hasher.update(MIGRATION_PROGRAM_VERSION_CURRENT.to_be_bytes());
556 hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
557 hasher.update(bytes);
558 SchemaMigrationPlanDigest::from_bytes(hasher.finalize().into())
559}
560
561fn ensure_distinct_rename_ownership(
562 renames: &[SchemaMigrationRename],
563) -> Result<(), SchemaContractError> {
564 let mut sources = BTreeSet::new();
565 let mut targets = BTreeSet::new();
566 for rename in renames {
567 let (kind, owner, from, to) = rename.sort_key();
568 if !sources.insert((kind, owner, from)) {
569 return Err(SchemaContractError::DuplicateMigrationSource);
570 }
571 if !targets.insert((kind, owner, to)) {
572 return Err(SchemaContractError::DuplicateMigrationTarget);
573 }
574 }
575 Ok(())
576}
577
578fn ensure_distinct_transform_targets(
579 transforms: &[SchemaMigrationTransform],
580) -> Result<(), SchemaContractError> {
581 let mut targets = BTreeSet::new();
582 for transform in transforms {
583 if !targets.insert(transform.target()) {
584 return Err(SchemaContractError::DuplicateMigrationTarget);
585 }
586 }
587 Ok(())
588}
589
590const fn is_v1_cast_target(target: ScalarType) -> bool {
591 matches!(
592 target,
593 ScalarType::Int8
594 | ScalarType::Int16
595 | ScalarType::Int32
596 | ScalarType::Int64
597 | ScalarType::Int128
598 | ScalarType::Nat8
599 | ScalarType::Nat16
600 | ScalarType::Nat32
601 | ScalarType::Nat64
602 | ScalarType::Nat128
603 | ScalarType::Decimal { .. }
604 )
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::{
611 MAX_SCHEMA_MIGRATION_PLAN_BYTES, decode_schema_migration_plan, encode_schema_migration_plan,
612 };
613
614 fn entity(value: &str) -> EntitySourceKey {
615 EntitySourceKey::try_new(value).expect("entity source should admit")
616 }
617
618 fn field(value: &str) -> FieldSourceKey {
619 FieldSourceKey::try_new(value).expect("field source should admit")
620 }
621
622 fn transition(entity_name: &str, from_name: &str) -> EntityMigration {
623 EntityMigration::try_new(
624 entity(entity_name),
625 DeclaredEntityVersion::try_new(1).expect("version should admit"),
626 Some(entity(from_name)),
627 vec![SchemaMigrationRename::Field {
628 from: field("old_value"),
629 to: field("value"),
630 }],
631 Vec::new(),
632 )
633 .expect("transition should admit")
634 }
635
636 #[test]
637 fn declared_entity_version_is_strictly_positive() {
638 assert_eq!(
639 DeclaredEntityVersion::try_new(0),
640 Err(SchemaContractError::InvalidEntityVersion),
641 );
642 assert_eq!(
643 DeclaredEntityVersion::try_new(1)
644 .expect("version one should admit")
645 .get(),
646 1,
647 );
648 }
649
650 #[test]
651 fn plan_canonicalization_is_declaration_order_independent() {
652 let account = transition("Account", "User");
653 let article = transition("Article", "Post");
654 let first = SchemaMigrationPlan::try_new(vec![account.clone(), article.clone()])
655 .expect("plan should admit");
656 let reverse = SchemaMigrationPlan::try_new(vec![article, account])
657 .expect("reverse plan should admit");
658
659 assert_eq!(first, reverse);
660 assert_eq!(first.digest(), reverse.digest());
661 let encoded = encode_schema_migration_plan(&first).expect("plan should encode");
662 assert_eq!(
663 decode_schema_migration_plan(&encoded).expect("plan should decode"),
664 first,
665 );
666 }
667
668 #[test]
669 fn every_current_migration_operation_roundtrips_exactly() {
670 let transition = EntityMigration::try_new(
671 entity("Account"),
672 DeclaredEntityVersion::try_new(3).expect("version should admit"),
673 Some(entity("User")),
674 vec![
675 SchemaMigrationRename::Field {
676 from: field("old_field"),
677 to: field("new_field"),
678 },
679 SchemaMigrationRename::NamedType {
680 from: TypeSourceKey::try_new("OldType").expect("type should admit"),
681 to: TypeSourceKey::try_new("NewType").expect("type should admit"),
682 },
683 SchemaMigrationRename::EnumVariant {
684 named_type: TypeSourceKey::try_new("OldEnum").expect("type should admit"),
685 from: TypeSourceKey::try_new("OldVariant").expect("variant should admit"),
686 to: TypeSourceKey::try_new("NewVariant").expect("variant should admit"),
687 },
688 SchemaMigrationRename::RecordField {
689 named_type: TypeSourceKey::try_new("OldRecord").expect("type should admit"),
690 from: field("old_member"),
691 to: field("new_member"),
692 },
693 SchemaMigrationRename::Relation {
694 from: RelationSourceKey::try_new("old_relation")
695 .expect("relation should admit"),
696 to: RelationSourceKey::try_new("new_relation").expect("relation should admit"),
697 },
698 SchemaMigrationRename::Constraint {
699 from: ConstraintSourceKey::try_new("old_constraint")
700 .expect("constraint should admit"),
701 to: ConstraintSourceKey::try_new("new_constraint")
702 .expect("constraint should admit"),
703 },
704 SchemaMigrationRename::Rule {
705 named_type: TypeSourceKey::try_new("OldRuleOwner").expect("type should admit"),
706 from: RuleSourceKey::try_new("old_rule").expect("rule should admit"),
707 to: RuleSourceKey::try_new("new_rule").expect("rule should admit"),
708 },
709 ],
710 vec![
711 SchemaMigrationTransform::Fill {
712 to: field("filled"),
713 literal: ScalarLiteral::Nat(1),
714 },
715 SchemaMigrationTransform::Copy {
716 from: field("copy_source"),
717 to: field("copied"),
718 },
719 SchemaMigrationTransform::CheckedCast {
720 from: field("cast_source"),
721 to: field("casted"),
722 target: ScalarType::Nat64,
723 },
724 SchemaMigrationTransform::Coalesce {
725 from: field("nullable_source"),
726 to: field("coalesced"),
727 literal: ScalarLiteral::Nat(0),
728 },
729 ],
730 )
731 .expect("transition should admit");
732 let plan = SchemaMigrationPlan::try_new(vec![transition]).expect("plan should admit");
733 let encoded = encode_schema_migration_plan(&plan).expect("plan should encode");
734
735 assert_eq!(
736 decode_schema_migration_plan(&encoded).expect("plan should decode"),
737 plan,
738 );
739 }
740
741 #[test]
742 fn plan_digest_has_one_fixed_current_vector() {
743 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
744 .expect("plan should admit");
745 assert_eq!(
746 plan.digest().to_bytes(),
747 [
748 185, 94, 228, 94, 125, 249, 118, 171, 39, 218, 171, 230, 105, 177, 195, 14, 41, 14,
749 39, 78, 248, 107, 177, 13, 16, 115, 37, 15, 145, 40, 134, 207,
750 ],
751 );
752 }
753
754 #[test]
755 fn dependency_only_companion_roundtrips_exactly() {
756 let companion = EntityMigration::try_new(
757 entity("Holder"),
758 DeclaredEntityVersion::try_new(1).unwrap(),
759 None,
760 Vec::new(),
761 Vec::new(),
762 )
763 .unwrap();
764 let plan = SchemaMigrationPlan::try_new(vec![transition("CatalogItem", "Item"), companion])
765 .unwrap();
766 let encoded = crate::encode_schema_migration_plan(&plan).unwrap();
767 assert_eq!(crate::decode_schema_migration_plan(&encoded).unwrap(), plan);
768 }
769
770 #[test]
771 fn duplicate_targets_predecessors_and_transform_targets_reject() {
772 let account = transition("Account", "User");
773 assert_eq!(
774 SchemaMigrationPlan::try_new(vec![account.clone(), account]),
775 Err(SchemaContractError::DuplicateMigrationTarget),
776 );
777 assert_eq!(
778 SchemaMigrationPlan::try_new(vec![
779 transition("Account", "User"),
780 transition("Profile", "User"),
781 ]),
782 Err(SchemaContractError::DuplicateMigrationSource),
783 );
784 assert_eq!(
785 EntityMigration::try_new(
786 entity("Account"),
787 DeclaredEntityVersion::try_new(1).expect("version should admit"),
788 None,
789 Vec::new(),
790 vec![
791 SchemaMigrationTransform::Fill {
792 to: field("value"),
793 literal: ScalarLiteral::Nat(1),
794 },
795 SchemaMigrationTransform::Copy {
796 from: field("old_value"),
797 to: field("value"),
798 },
799 ],
800 ),
801 Err(SchemaContractError::DuplicateMigrationTarget),
802 );
803 }
804
805 #[test]
806 fn wire_plan_restoration_keeps_nested_constructor_admission() {
807 let valid = transition("Account", "User");
808 let mut duplicate = valid.clone();
809 duplicate.renames.push(duplicate.renames[0].clone());
810 let mut same_name = valid.clone();
811 same_name.from_name = Some(same_name.entity.clone());
812 let mut invalid_cast = valid;
813 invalid_cast
814 .transforms
815 .push(SchemaMigrationTransform::CheckedCast {
816 from: field("old_value"),
817 to: field("value"),
818 target: ScalarType::Float64,
819 });
820 for transition in [duplicate, same_name, invalid_cast] {
821 let bytes = crate::wire::encode_one((*b"ICWM\x01", (1_u16, vec![transition]))).unwrap();
822 assert!(decode_schema_migration_plan(&bytes).is_err());
823 }
824 }
825
826 #[test]
827 fn unsupported_program_versions_and_oversized_transport_fail_closed() {
828 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
829 .expect("plan should admit");
830 let mut bytes = encode_schema_migration_plan(&plan).expect("plan should encode");
831 bytes[5] = 2;
832 assert_eq!(
833 decode_schema_migration_plan(&bytes),
834 Err(SchemaContractError::UnsupportedMigrationProgramVersion {
835 found: 2,
836 supported: 1,
837 }),
838 );
839 assert!(matches!(
840 decode_schema_migration_plan(&vec![0; MAX_SCHEMA_MIGRATION_PLAN_BYTES + 1]),
841 Err(SchemaContractError::EncodedTooLarge { .. }),
842 ));
843 }
844}