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 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 #[must_use]
354 pub const fn program_version(&self) -> u16 {
355 self.program_version
356 }
357
358 #[must_use]
360 pub fn transitions(&self) -> &[EntityMigration] {
361 &self.transitions
362 }
363
364 #[must_use]
366 pub const fn digest(&self) -> SchemaMigrationPlanDigest {
367 self.digest
368 }
369
370 pub(crate) fn validate(&self) -> Result<(), SchemaContractError> {
371 if self.program_version != MIGRATION_PROGRAM_VERSION_CURRENT {
372 return Err(SchemaContractError::UnsupportedMigrationProgramVersion {
373 found: self.program_version,
374 supported: MIGRATION_PROGRAM_VERSION_CURRENT,
375 });
376 }
377 let rebuilt = Self::try_new(self.transitions.clone())?;
378 if rebuilt != *self {
379 return Err(SchemaContractError::NonCanonical);
380 }
381 Ok(())
382 }
383}
384
385fn digest_plan_transitions(bytes: &[u8]) -> SchemaMigrationPlanDigest {
386 let mut hasher = Sha256::new();
387 hasher.update(MIGRATION_PLAN_DIGEST_PROFILE);
388 hasher.update(MIGRATION_PROGRAM_VERSION_CURRENT.to_be_bytes());
389 hasher.update(u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
390 hasher.update(bytes);
391 SchemaMigrationPlanDigest::from_bytes(hasher.finalize().into())
392}
393
394fn ensure_distinct_rename_ownership(
395 renames: &[SchemaMigrationRename],
396) -> Result<(), SchemaContractError> {
397 let mut sources = BTreeSet::new();
398 let mut targets = BTreeSet::new();
399 for rename in renames {
400 let (kind, owner, from, to) = rename.sort_key();
401 if !sources.insert((kind, owner, from)) {
402 return Err(SchemaContractError::DuplicateMigrationSource);
403 }
404 if !targets.insert((kind, owner, to)) {
405 return Err(SchemaContractError::DuplicateMigrationTarget);
406 }
407 }
408 Ok(())
409}
410
411fn ensure_distinct_transform_targets(
412 transforms: &[SchemaMigrationTransform],
413) -> Result<(), SchemaContractError> {
414 let mut targets = BTreeSet::new();
415 for transform in transforms {
416 if !targets.insert(transform.target()) {
417 return Err(SchemaContractError::DuplicateMigrationTarget);
418 }
419 }
420 Ok(())
421}
422
423const fn is_v1_cast_target(target: ScalarType) -> bool {
424 matches!(
425 target,
426 ScalarType::Int8
427 | ScalarType::Int16
428 | ScalarType::Int32
429 | ScalarType::Int64
430 | ScalarType::Int128
431 | ScalarType::Nat8
432 | ScalarType::Nat16
433 | ScalarType::Nat32
434 | ScalarType::Nat64
435 | ScalarType::Nat128
436 | ScalarType::Decimal { .. }
437 )
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::{
444 MAX_SCHEMA_MIGRATION_PLAN_BYTES, decode_schema_migration_plan, encode_schema_migration_plan,
445 };
446
447 fn entity(value: &str) -> EntitySourceKey {
448 EntitySourceKey::try_new(value).expect("entity source should admit")
449 }
450
451 fn field(value: &str) -> FieldSourceKey {
452 FieldSourceKey::try_new(value).expect("field source should admit")
453 }
454
455 fn transition(entity_name: &str, from_name: &str) -> EntityMigration {
456 EntityMigration::try_new(
457 entity(entity_name),
458 DeclaredEntityVersion::try_new(1).expect("version should admit"),
459 Some(entity(from_name)),
460 vec![SchemaMigrationRename::Field {
461 from: field("old_value"),
462 to: field("value"),
463 }],
464 Vec::new(),
465 )
466 .expect("transition should admit")
467 }
468
469 #[test]
470 fn declared_entity_version_is_strictly_positive() {
471 assert_eq!(
472 DeclaredEntityVersion::try_new(0),
473 Err(SchemaContractError::InvalidEntityVersion),
474 );
475 assert_eq!(
476 DeclaredEntityVersion::try_new(1)
477 .expect("version one should admit")
478 .get(),
479 1,
480 );
481 }
482
483 #[test]
484 fn plan_canonicalization_is_declaration_order_independent() {
485 let account = transition("Account", "User");
486 let article = transition("Article", "Post");
487 let first = SchemaMigrationPlan::try_new(vec![account.clone(), article.clone()])
488 .expect("plan should admit");
489 let reverse = SchemaMigrationPlan::try_new(vec![article, account])
490 .expect("reverse plan should admit");
491
492 assert_eq!(first, reverse);
493 assert_eq!(first.digest(), reverse.digest());
494 let encoded = encode_schema_migration_plan(&first).expect("plan should encode");
495 assert_eq!(
496 decode_schema_migration_plan(&encoded).expect("plan should decode"),
497 first,
498 );
499 }
500
501 #[test]
502 fn every_current_migration_operation_roundtrips_exactly() {
503 let transition = EntityMigration::try_new(
504 entity("Account"),
505 DeclaredEntityVersion::try_new(3).expect("version should admit"),
506 Some(entity("User")),
507 vec![
508 SchemaMigrationRename::Field {
509 from: field("old_field"),
510 to: field("new_field"),
511 },
512 SchemaMigrationRename::NamedType {
513 from: TypeSourceKey::try_new("OldType").expect("type should admit"),
514 to: TypeSourceKey::try_new("NewType").expect("type should admit"),
515 },
516 SchemaMigrationRename::EnumVariant {
517 named_type: TypeSourceKey::try_new("OldEnum").expect("type should admit"),
518 from: TypeSourceKey::try_new("OldVariant").expect("variant should admit"),
519 to: TypeSourceKey::try_new("NewVariant").expect("variant should admit"),
520 },
521 SchemaMigrationRename::RecordField {
522 named_type: TypeSourceKey::try_new("OldRecord").expect("type should admit"),
523 from: field("old_member"),
524 to: field("new_member"),
525 },
526 SchemaMigrationRename::Relation {
527 from: RelationSourceKey::try_new("old_relation")
528 .expect("relation should admit"),
529 to: RelationSourceKey::try_new("new_relation").expect("relation should admit"),
530 },
531 SchemaMigrationRename::Constraint {
532 from: ConstraintSourceKey::try_new("old_constraint")
533 .expect("constraint should admit"),
534 to: ConstraintSourceKey::try_new("new_constraint")
535 .expect("constraint should admit"),
536 },
537 SchemaMigrationRename::Rule {
538 named_type: TypeSourceKey::try_new("OldRuleOwner").expect("type should admit"),
539 from: RuleSourceKey::try_new("old_rule").expect("rule should admit"),
540 to: RuleSourceKey::try_new("new_rule").expect("rule should admit"),
541 },
542 ],
543 vec![
544 SchemaMigrationTransform::Fill {
545 to: field("filled"),
546 literal: ScalarLiteral::Nat(1),
547 },
548 SchemaMigrationTransform::Copy {
549 from: field("copy_source"),
550 to: field("copied"),
551 },
552 SchemaMigrationTransform::CheckedCast {
553 from: field("cast_source"),
554 to: field("casted"),
555 target: ScalarType::Nat64,
556 },
557 SchemaMigrationTransform::Coalesce {
558 from: field("nullable_source"),
559 to: field("coalesced"),
560 literal: ScalarLiteral::Nat(0),
561 },
562 ],
563 )
564 .expect("transition should admit");
565 let plan = SchemaMigrationPlan::try_new(vec![transition]).expect("plan should admit");
566 let encoded = encode_schema_migration_plan(&plan).expect("plan should encode");
567
568 assert_eq!(
569 decode_schema_migration_plan(&encoded).expect("plan should decode"),
570 plan,
571 );
572 }
573
574 #[test]
575 fn plan_digest_has_one_fixed_current_vector() {
576 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
577 .expect("plan should admit");
578 assert_eq!(
579 plan.digest().to_bytes(),
580 [
581 198, 22, 117, 206, 250, 126, 65, 102, 162, 77, 246, 242, 60, 127, 68, 242, 56, 105,
582 141, 56, 136, 233, 54, 180, 100, 187, 24, 63, 167, 158, 239, 93,
583 ],
584 );
585 }
586
587 #[test]
588 fn duplicate_targets_predecessors_and_transform_targets_reject() {
589 let account = transition("Account", "User");
590 assert_eq!(
591 SchemaMigrationPlan::try_new(vec![account.clone(), account]),
592 Err(SchemaContractError::DuplicateMigrationTarget),
593 );
594 assert_eq!(
595 SchemaMigrationPlan::try_new(vec![
596 transition("Account", "User"),
597 transition("Profile", "User"),
598 ]),
599 Err(SchemaContractError::DuplicateMigrationSource),
600 );
601 assert_eq!(
602 EntityMigration::try_new(
603 entity("Account"),
604 DeclaredEntityVersion::try_new(1).expect("version should admit"),
605 None,
606 Vec::new(),
607 vec![
608 SchemaMigrationTransform::Fill {
609 to: field("value"),
610 literal: ScalarLiteral::Nat(1),
611 },
612 SchemaMigrationTransform::Copy {
613 from: field("old_value"),
614 to: field("value"),
615 },
616 ],
617 ),
618 Err(SchemaContractError::DuplicateMigrationTarget),
619 );
620 }
621
622 #[test]
623 fn obsolete_programs_and_oversized_transport_fail_closed() {
624 let plan = SchemaMigrationPlan::try_new(vec![transition("Account", "User")])
625 .expect("plan should admit");
626 let mut bytes = encode_schema_migration_plan(&plan).expect("plan should encode");
627 bytes[5..7].copy_from_slice(&2_u16.to_be_bytes());
628 assert_eq!(
629 decode_schema_migration_plan(&bytes),
630 Err(SchemaContractError::UnsupportedMigrationProgramVersion {
631 found: 2,
632 supported: 1,
633 }),
634 );
635 assert!(matches!(
636 decode_schema_migration_plan(&vec![0; MAX_SCHEMA_MIGRATION_PLAN_BYTES + 1]),
637 Err(SchemaContractError::EncodedTooLarge { .. }),
638 ));
639 }
640}