1use std::fmt;
4
5use serde::{Serialize, Serializer};
6use sha2::{Digest as _, Sha256};
7
8use crate::capability::{CapabilityId, CapabilitySet};
9use crate::codec::to_canonical_json;
10use crate::diagnostic::{Diagnostic, DiagnosticCategory};
11use crate::fingerprint::{
12 CanonicalizationVersion, Fingerprint, FingerprintDigest, FingerprintDomain,
13};
14use crate::migration_assertion::{
15 AssertionExpectation, MigrationAssertionPlan, MigrationAssertionPlanFingerprint,
16};
17use crate::migration_backfill::AttributeBackfillPlan;
18use crate::schema_delta::{SchemaDelta, SchemaOperation};
19use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
20
21pub const MIGRATION_FORMAT_V1: &str = "typebridge.migration/v1";
23pub const MIGRATION_ID_FINGERPRINT_DOMAIN: &str = "typebridge.migration.id";
27pub const MIGRATION_ID_CANONICALIZATION: &str = "typebridge.migration-id/v1";
29pub const SCHEMA_DELTA_FINGERPRINT_DOMAIN: &str = "typebridge.migration.schema-delta";
31pub const SCHEMA_DELTA_FINGERPRINT_CANONICALIZATION: &str = "typebridge.schema-delta/v1";
33pub const MIGRATION_PLAN_FINGERPRINT_DOMAIN: &str = "typebridge.migration.plan";
35pub const MIGRATION_PLAN_FINGERPRINT_CANONICALIZATION: &str = "typebridge.migration-plan/v1";
37pub const CONDITIONAL_RESOLUTION_CAPABILITY: &str = "migration.conditional-resolution";
39pub const MAX_MIGRATION_COMPONENT_BYTES: usize = 255;
41
42fn validate_component(
43 value: String,
44 kind: &'static str,
45 allow_leading_digit: bool,
46) -> Result<String, Diagnostic> {
47 let mut bytes = value.bytes();
48 let valid_first = bytes.next().is_some_and(|byte| {
49 byte.is_ascii_lowercase() || (allow_leading_digit && byte.is_ascii_digit())
50 });
51 let valid_rest = bytes.all(|byte| {
52 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
53 });
54 if value.len() <= MAX_MIGRATION_COMPONENT_BYTES && valid_first && valid_rest {
55 Ok(value)
56 } else {
57 Err(Diagnostic::stable(
58 DiagnosticCategory::InvalidContract,
59 "invalid_migration_identity_component",
60 "migration identity component must be bounded portable lowercase ASCII",
61 )
62 .with_detail("component_kind", kind))
63 }
64}
65
66macro_rules! migration_component {
67 ($name:ident, $doc:literal, $kind:literal, $leading_digit:expr) => {
68 #[doc = $doc]
69 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
70 #[serde(transparent)]
71 pub struct $name(String);
72
73 impl $name {
74 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
76 Ok(Self(validate_component(
77 value.into(),
78 $kind,
79 $leading_digit,
80 )?))
81 }
82
83 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87 }
88
89 impl fmt::Display for $name {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 formatter.write_str(self.as_str())
92 }
93 }
94 };
95}
96
97migration_component!(
98 MigrationAppLabel,
99 "A portable application label forming the first component of migration identity.",
100 "app_label",
101 false
102);
103migration_component!(
104 MigrationName,
105 "A portable filename-stem migration name forming the second identity component.",
106 "name",
107 true
108);
109migration_component!(
110 MigrationStepId,
111 "A stable identity unique within one ordered migration plan.",
112 "step_id",
113 false
114);
115
116#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
118pub struct MigrationId {
119 app_label: MigrationAppLabel,
120 name: MigrationName,
121}
122
123impl MigrationId {
124 pub fn new(app_label: impl Into<String>, name: impl Into<String>) -> Result<Self, Diagnostic> {
126 Ok(Self {
127 app_label: MigrationAppLabel::new(app_label)?,
128 name: MigrationName::new(name)?,
129 })
130 }
131
132 #[must_use]
134 pub const fn from_components(app_label: MigrationAppLabel, name: MigrationName) -> Self {
135 Self { app_label, name }
136 }
137
138 pub const fn app_label(&self) -> &MigrationAppLabel {
140 &self.app_label
141 }
142
143 pub const fn name(&self) -> &MigrationName {
145 &self.name
146 }
147
148 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
150 to_canonical_json(self)
151 }
152
153 pub fn ledger_key(&self) -> Result<MigrationLedgerKey, Diagnostic> {
155 MigrationLedgerKey::compute(self)
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
161pub struct MigrationFormat;
162
163impl MigrationFormat {
164 pub const V1: Self = Self;
166
167 pub fn new(value: &str) -> Result<Self, Diagnostic> {
169 if value == MIGRATION_FORMAT_V1 {
170 Ok(Self::V1)
171 } else {
172 Err(Diagnostic::stable(
173 DiagnosticCategory::InvalidContract,
174 "unsupported_migration_format",
175 "canonical migration format is not supported",
176 )
177 .with_detail("actual", value.to_owned())
178 .with_detail("supported", MIGRATION_FORMAT_V1))
179 }
180 }
181
182 pub const fn as_str(self) -> &'static str {
184 MIGRATION_FORMAT_V1
185 }
186}
187
188impl Serialize for MigrationFormat {
189 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
190 where
191 S: Serializer,
192 {
193 serializer.serialize_str(self.as_str())
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
199#[serde(rename_all = "snake_case")]
200pub enum RetryPolicy {
201 Never,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
207#[serde(rename_all = "snake_case")]
208pub enum RecoveryPolicy {
209 OperatorRequired,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
215#[serde(transparent)]
216pub struct MigrationManifestDigest(FingerprintDigest);
217
218impl MigrationManifestDigest {
219 #[must_use]
221 pub fn compute(canonical_manifest_bytes: &[u8]) -> Self {
222 let digest = Sha256::digest(canonical_manifest_bytes);
223 let hex = digest
224 .iter()
225 .map(|byte| format!("{byte:02x}"))
226 .collect::<String>();
227 Self(
228 FingerprintDigest::from_hex(&hex)
229 .expect("SHA-256 always produces a valid lowercase 32-byte digest"),
230 )
231 }
232
233 pub fn from_hex(value: &str) -> Result<Self, Diagnostic> {
235 FingerprintDigest::from_hex(value).map(Self)
236 }
237
238 #[must_use]
240 pub fn to_hex(self) -> String {
241 self.0.to_hex()
242 }
243
244 #[must_use]
246 pub const fn bytes(self) -> [u8; 32] {
247 self.0.bytes()
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
253#[serde(transparent)]
254pub struct MigrationLedgerKey(Fingerprint);
255
256impl MigrationLedgerKey {
257 pub fn compute(id: &MigrationId) -> Result<Self, Diagnostic> {
259 Ok(Self(Fingerprint::compute(
260 FingerprintDomain::new(MIGRATION_ID_FINGERPRINT_DOMAIN)?,
261 CanonicalizationVersion::new(MIGRATION_ID_CANONICALIZATION)?,
262 None,
263 &id.canonical_bytes()?,
264 )))
265 }
266
267 pub const fn as_fingerprint(&self) -> &Fingerprint {
269 &self.0
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
275#[serde(transparent)]
276pub struct SchemaDeltaFingerprint(Fingerprint);
277
278impl SchemaDeltaFingerprint {
279 pub fn compute(delta: &SchemaDelta) -> Result<Self, Diagnostic> {
281 Ok(Self(Fingerprint::compute(
282 FingerprintDomain::new(SCHEMA_DELTA_FINGERPRINT_DOMAIN)?,
283 CanonicalizationVersion::new(SCHEMA_DELTA_FINGERPRINT_CANONICALIZATION)?,
284 None,
285 &delta.canonical_bytes()?,
286 )))
287 }
288
289 pub const fn as_fingerprint(&self) -> &Fingerprint {
291 &self.0
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
297#[serde(rename_all = "snake_case")]
298pub enum MigrationStepKind {
299 SchemaDelta,
301 Assertion,
303 Backfill,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
309pub struct BackfillStepContract {
310 id: MigrationStepId,
311 plan_fingerprint: Fingerprint,
312 recovery: RecoveryPolicy,
313 required_capabilities: CapabilitySet,
314 retry: RetryPolicy,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 reverse: Option<crate::migration_backfill::BackfillReverseProgram>,
317 source_semantics: ManagedSemanticSchemaFingerprint,
318 target_semantics: ManagedSemanticSchemaFingerprint,
319}
320
321impl BackfillStepContract {
322 fn derive(id: MigrationStepId, plan: &AttributeBackfillPlan) -> Result<Self, Diagnostic> {
323 Ok(Self {
324 id,
325 plan_fingerprint: plan.fingerprint()?,
326 recovery: RecoveryPolicy::OperatorRequired,
327 required_capabilities: plan.required_capabilities().clone(),
328 retry: RetryPolicy::Never,
329 reverse: plan.reverse(),
330 source_semantics: plan.managed_semantics().clone(),
331 target_semantics: plan.managed_semantics().clone(),
332 })
333 }
334
335 pub const fn id(&self) -> &MigrationStepId {
337 &self.id
338 }
339 pub const fn plan_fingerprint(&self) -> &Fingerprint {
341 &self.plan_fingerprint
342 }
343 pub const fn required_capabilities(&self) -> &CapabilitySet {
345 &self.required_capabilities
346 }
347 pub const fn reverse(&self) -> Option<crate::migration_backfill::BackfillReverseProgram> {
349 self.reverse
350 }
351 pub const fn source_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
353 &self.source_semantics
354 }
355 pub const fn target_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
357 &self.target_semantics
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
363pub struct SchemaDeltaStepContract {
364 delta_fingerprint: SchemaDeltaFingerprint,
365 id: MigrationStepId,
366 recovery: RecoveryPolicy,
367 required_capabilities: CapabilitySet,
368 retry: RetryPolicy,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 reverse: Option<SchemaDelta>,
371 source_semantics: ManagedSemanticSchemaFingerprint,
372 target_semantics: ManagedSemanticSchemaFingerprint,
373}
374
375impl SchemaDeltaStepContract {
376 pub fn new(
378 id: MigrationStepId,
379 delta: &SchemaDelta,
380 reverse: Option<SchemaDelta>,
381 ) -> Result<Self, Diagnostic> {
382 if let Some(candidate) = &reverse {
383 let inverse_operations = delta
384 .operations()
385 .iter()
386 .rev()
387 .flat_map(SchemaOperation::inverse)
388 .collect();
389 let expected = SchemaDelta::new(
390 delta.format(),
391 delta.target().clone(),
392 delta.source().clone(),
393 inverse_operations,
394 )?;
395 if candidate != &expected {
396 return Err(Diagnostic::stable(
397 DiagnosticCategory::InvalidContract,
398 "schema_delta_step_inverse_mismatch",
399 "schema step reverse delta is not the exact operation-wise inverse",
400 ));
401 }
402 }
403 Ok(Self {
404 delta_fingerprint: SchemaDeltaFingerprint::compute(delta)?,
405 id,
406 recovery: RecoveryPolicy::OperatorRequired,
407 required_capabilities: delta.required_capabilities().clone(),
408 retry: RetryPolicy::Never,
409 reverse,
410 source_semantics: delta.source().managed_semantic_schema().clone(),
411 target_semantics: delta.target().managed_semantic_schema().clone(),
412 })
413 }
414
415 pub const fn id(&self) -> &MigrationStepId {
417 &self.id
418 }
419
420 pub const fn source_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
422 &self.source_semantics
423 }
424
425 pub const fn target_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
427 &self.target_semantics
428 }
429
430 pub const fn required_capabilities(&self) -> &CapabilitySet {
432 &self.required_capabilities
433 }
434
435 pub const fn delta_fingerprint(&self) -> &SchemaDeltaFingerprint {
437 &self.delta_fingerprint
438 }
439
440 pub const fn retry(&self) -> RetryPolicy {
442 self.retry
443 }
444
445 pub const fn recovery(&self) -> RecoveryPolicy {
447 self.recovery
448 }
449
450 pub const fn reverse(&self) -> Option<&SchemaDelta> {
452 self.reverse.as_ref()
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
458pub struct SchemaDeltaStep {
459 contract: SchemaDeltaStepContract,
460 delta: SchemaDelta,
461 kind: MigrationStepKind,
462}
463
464impl SchemaDeltaStep {
465 pub fn new(
467 id: MigrationStepId,
468 delta: SchemaDelta,
469 reverse: Option<SchemaDelta>,
470 ) -> Result<Self, Diagnostic> {
471 let contract = SchemaDeltaStepContract::new(id, &delta, reverse)?;
472 Ok(Self {
473 contract,
474 delta,
475 kind: MigrationStepKind::SchemaDelta,
476 })
477 }
478
479 pub const fn kind(&self) -> MigrationStepKind {
481 self.kind
482 }
483
484 pub const fn contract(&self) -> &SchemaDeltaStepContract {
486 &self.contract
487 }
488
489 pub const fn delta(&self) -> &SchemaDelta {
491 &self.delta
492 }
493
494 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
496 to_canonical_json(self)
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
502pub struct AssertionStepContract {
503 id: MigrationStepId,
504 plan_fingerprint: MigrationAssertionPlanFingerprint,
505 recovery: RecoveryPolicy,
506 required_capabilities: CapabilitySet,
507 retry: RetryPolicy,
508 source_semantics: ManagedSemanticSchemaFingerprint,
509 target_semantics: ManagedSemanticSchemaFingerprint,
510}
511
512impl AssertionStepContract {
513 fn derive(
514 id: MigrationStepId,
515 plan: &MigrationAssertionPlan,
516 expected: AssertionExpectation,
517 ) -> Result<Self, Diagnostic> {
518 if expected != AssertionExpectation::NoRows
519 || plan.expectation() != AssertionExpectation::NoRows
520 {
521 return Err(Diagnostic::stable(
522 DiagnosticCategory::InvalidContract,
523 "migration_assertion_step_expectation_mismatch",
524 "persisted migration assertions support only the no-rows expectation",
525 ));
526 }
527 let mut required_capabilities = plan.required_capabilities().clone();
528 required_capabilities.insert(
529 CapabilityId::new(CONDITIONAL_RESOLUTION_CAPABILITY)
530 .expect("the fixed conditional-resolution capability is canonical"),
531 );
532 Ok(Self {
533 id,
534 plan_fingerprint: plan.fingerprint()?,
535 recovery: RecoveryPolicy::OperatorRequired,
536 required_capabilities,
537 retry: RetryPolicy::Never,
538 source_semantics: plan.managed_semantics().clone(),
539 target_semantics: plan.managed_semantics().clone(),
540 })
541 }
542
543 pub const fn id(&self) -> &MigrationStepId {
545 &self.id
546 }
547
548 pub const fn plan_fingerprint(&self) -> &MigrationAssertionPlanFingerprint {
550 &self.plan_fingerprint
551 }
552
553 pub const fn required_capabilities(&self) -> &CapabilitySet {
555 &self.required_capabilities
556 }
557
558 pub const fn retry(&self) -> RetryPolicy {
560 self.retry
561 }
562
563 pub const fn recovery(&self) -> RecoveryPolicy {
565 self.recovery
566 }
567
568 pub const fn source_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
570 &self.source_semantics
571 }
572
573 pub const fn target_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
575 &self.target_semantics
576 }
577
578 pub const fn reverse(&self) -> Option<()> {
580 None
581 }
582}
583
584#[derive(Debug, Clone, PartialEq, Eq)]
586pub enum MigrationStep {
587 SchemaDelta(Box<SchemaDeltaStep>),
589 Assertion {
591 contract: Box<AssertionStepContract>,
593 plan: Box<MigrationAssertionPlan>,
595 expected: AssertionExpectation,
597 },
598 Backfill {
600 contract: Box<BackfillStepContract>,
602 plan: Box<AttributeBackfillPlan>,
604 },
605}
606
607impl MigrationStep {
608 pub fn backfill(id: MigrationStepId, plan: AttributeBackfillPlan) -> Result<Self, Diagnostic> {
610 let contract = BackfillStepContract::derive(id, &plan)?;
611 Ok(Self::Backfill {
612 contract: Box::new(contract),
613 plan: Box::new(plan),
614 })
615 }
616 pub fn assertion(
618 id: MigrationStepId,
619 plan: MigrationAssertionPlan,
620 expected: AssertionExpectation,
621 ) -> Result<Self, Diagnostic> {
622 let contract = AssertionStepContract::derive(id, &plan, expected)?;
623 Ok(Self::Assertion {
624 contract: Box::new(contract),
625 plan: Box::new(plan),
626 expected,
627 })
628 }
629
630 pub const fn kind(&self) -> MigrationStepKind {
632 match self {
633 Self::SchemaDelta(_) => MigrationStepKind::SchemaDelta,
634 Self::Assertion { .. } => MigrationStepKind::Assertion,
635 Self::Backfill { .. } => MigrationStepKind::Backfill,
636 }
637 }
638
639 pub const fn id(&self) -> &MigrationStepId {
641 match self {
642 Self::SchemaDelta(step) => step.contract().id(),
643 Self::Assertion { contract, .. } => contract.id(),
644 Self::Backfill { contract, .. } => contract.id(),
645 }
646 }
647
648 pub const fn required_capabilities(&self) -> &CapabilitySet {
650 match self {
651 Self::SchemaDelta(step) => step.contract().required_capabilities(),
652 Self::Assertion { contract, .. } => contract.required_capabilities(),
653 Self::Backfill { contract, .. } => contract.required_capabilities(),
654 }
655 }
656
657 pub fn as_schema_delta(&self) -> Option<&SchemaDeltaStep> {
659 match self {
660 Self::SchemaDelta(step) => Some(step),
661 Self::Assertion { .. } | Self::Backfill { .. } => None,
662 }
663 }
664
665 pub fn as_assertion(
667 &self,
668 ) -> Option<(
669 &AssertionStepContract,
670 &MigrationAssertionPlan,
671 AssertionExpectation,
672 )> {
673 match self {
674 Self::SchemaDelta(_) | Self::Backfill { .. } => None,
675 Self::Assertion {
676 contract,
677 plan,
678 expected,
679 } => Some((contract, plan, *expected)),
680 }
681 }
682
683 pub fn as_backfill(&self) -> Option<(&BackfillStepContract, &AttributeBackfillPlan)> {
685 match self {
686 Self::Backfill { contract, plan } => Some((contract, plan)),
687 Self::SchemaDelta(_) | Self::Assertion { .. } => None,
688 }
689 }
690
691 pub fn validate(&self) -> Result<(), Diagnostic> {
693 let rebuilt = match self {
694 Self::SchemaDelta(step) => Self::SchemaDelta(Box::new(SchemaDeltaStep::new(
695 step.contract().id().clone(),
696 step.delta().clone(),
697 step.contract().reverse().cloned(),
698 )?)),
699 Self::Assertion {
700 contract,
701 plan,
702 expected,
703 } => Self::assertion(contract.id().clone(), plan.as_ref().clone(), *expected)?,
704 Self::Backfill { contract, plan } => {
705 Self::backfill(contract.id().clone(), plan.as_ref().clone())?
706 }
707 };
708 if &rebuilt != self {
709 return Err(Diagnostic::stable(
710 DiagnosticCategory::Integrity,
711 "migration_step_contract_mismatch",
712 "migration step claims differ from constructor-derived claims",
713 ));
714 }
715 Ok(())
716 }
717
718 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
720 to_canonical_json(self)
721 }
722}
723
724impl From<SchemaDeltaStep> for MigrationStep {
725 fn from(step: SchemaDeltaStep) -> Self {
726 Self::SchemaDelta(Box::new(step))
727 }
728}
729
730#[derive(Serialize)]
731struct AssertionStepView<'a> {
732 contract: &'a AssertionStepContract,
733 expected: AssertionExpectation,
734 kind: MigrationStepKind,
735 plan: &'a MigrationAssertionPlan,
736}
737
738#[derive(Serialize)]
739struct BackfillStepView<'a> {
740 contract: &'a BackfillStepContract,
741 kind: MigrationStepKind,
742 plan: &'a AttributeBackfillPlan,
743}
744
745impl Serialize for MigrationStep {
746 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
747 where
748 S: Serializer,
749 {
750 match self {
751 Self::SchemaDelta(step) => step.serialize(serializer),
752 Self::Assertion {
753 contract,
754 plan,
755 expected,
756 } => AssertionStepView {
757 contract,
758 expected: *expected,
759 kind: MigrationStepKind::Assertion,
760 plan,
761 }
762 .serialize(serializer),
763 Self::Backfill { contract, plan } => BackfillStepView {
764 contract,
765 kind: MigrationStepKind::Backfill,
766 plan,
767 }
768 .serialize(serializer),
769 }
770 }
771}
772
773#[derive(Serialize)]
774struct MigrationPlanView<'a> {
775 steps: &'a [MigrationStep],
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
780#[serde(transparent)]
781pub struct MigrationPlanFingerprint(Fingerprint);
782
783impl MigrationPlanFingerprint {
784 pub fn canonical_plan_bytes(steps: &[MigrationStep]) -> Result<Vec<u8>, Diagnostic> {
786 to_canonical_json(&MigrationPlanView { steps })
787 }
788
789 pub fn compute(steps: &[MigrationStep]) -> Result<Self, Diagnostic> {
791 Ok(Self(Fingerprint::compute(
792 FingerprintDomain::new(MIGRATION_PLAN_FINGERPRINT_DOMAIN)?,
793 CanonicalizationVersion::new(MIGRATION_PLAN_FINGERPRINT_CANONICALIZATION)?,
794 None,
795 &Self::canonical_plan_bytes(steps)?,
796 )))
797 }
798
799 pub const fn as_fingerprint(&self) -> &Fingerprint {
801 &self.0
802 }
803}