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