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::schema_delta::{SchemaDelta, SchemaOperation};
18use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
19
20pub const MIGRATION_FORMAT_V1: &str = "typebridge.migration/v1";
22pub const MIGRATION_ID_FINGERPRINT_DOMAIN: &str = "typebridge.migration.id";
26pub const MIGRATION_ID_CANONICALIZATION: &str = "typebridge.migration-id/v1";
28pub const SCHEMA_DELTA_FINGERPRINT_DOMAIN: &str = "typebridge.migration.schema-delta";
30pub const SCHEMA_DELTA_FINGERPRINT_CANONICALIZATION: &str = "typebridge.schema-delta/v1";
32pub const MIGRATION_PLAN_FINGERPRINT_DOMAIN: &str = "typebridge.migration.plan";
34pub const MIGRATION_PLAN_FINGERPRINT_CANONICALIZATION: &str = "typebridge.migration-plan/v1";
36pub const CONDITIONAL_RESOLUTION_CAPABILITY: &str = "migration.conditional-resolution";
38pub const MAX_MIGRATION_COMPONENT_BYTES: usize = 255;
40
41fn validate_component(
42 value: String,
43 kind: &'static str,
44 allow_leading_digit: bool,
45) -> Result<String, Diagnostic> {
46 let mut bytes = value.bytes();
47 let valid_first = bytes.next().is_some_and(|byte| {
48 byte.is_ascii_lowercase() || (allow_leading_digit && byte.is_ascii_digit())
49 });
50 let valid_rest = bytes.all(|byte| {
51 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
52 });
53 if value.len() <= MAX_MIGRATION_COMPONENT_BYTES && valid_first && valid_rest {
54 Ok(value)
55 } else {
56 Err(Diagnostic::stable(
57 DiagnosticCategory::InvalidContract,
58 "invalid_migration_identity_component",
59 "migration identity component must be bounded portable lowercase ASCII",
60 )
61 .with_detail("component_kind", kind))
62 }
63}
64
65macro_rules! migration_component {
66 ($name:ident, $doc:literal, $kind:literal, $leading_digit:expr) => {
67 #[doc = $doc]
68 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
69 #[serde(transparent)]
70 pub struct $name(String);
71
72 impl $name {
73 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
75 Ok(Self(validate_component(
76 value.into(),
77 $kind,
78 $leading_digit,
79 )?))
80 }
81
82 pub fn as_str(&self) -> &str {
84 &self.0
85 }
86 }
87
88 impl fmt::Display for $name {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 formatter.write_str(self.as_str())
91 }
92 }
93 };
94}
95
96migration_component!(
97 MigrationAppLabel,
98 "A portable application label forming the first component of migration identity.",
99 "app_label",
100 false
101);
102migration_component!(
103 MigrationName,
104 "A portable filename-stem migration name forming the second identity component.",
105 "name",
106 true
107);
108migration_component!(
109 MigrationStepId,
110 "A stable identity unique within one ordered migration plan.",
111 "step_id",
112 false
113);
114
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
117pub struct MigrationId {
118 app_label: MigrationAppLabel,
119 name: MigrationName,
120}
121
122impl MigrationId {
123 pub fn new(app_label: impl Into<String>, name: impl Into<String>) -> Result<Self, Diagnostic> {
125 Ok(Self {
126 app_label: MigrationAppLabel::new(app_label)?,
127 name: MigrationName::new(name)?,
128 })
129 }
130
131 #[must_use]
133 pub const fn from_components(app_label: MigrationAppLabel, name: MigrationName) -> Self {
134 Self { app_label, name }
135 }
136
137 pub const fn app_label(&self) -> &MigrationAppLabel {
139 &self.app_label
140 }
141
142 pub const fn name(&self) -> &MigrationName {
144 &self.name
145 }
146
147 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
149 to_canonical_json(self)
150 }
151
152 pub fn ledger_key(&self) -> Result<MigrationLedgerKey, Diagnostic> {
154 MigrationLedgerKey::compute(self)
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
160pub struct MigrationFormat;
161
162impl MigrationFormat {
163 pub const V1: Self = Self;
165
166 pub fn new(value: &str) -> Result<Self, Diagnostic> {
168 if value == MIGRATION_FORMAT_V1 {
169 Ok(Self::V1)
170 } else {
171 Err(Diagnostic::stable(
172 DiagnosticCategory::InvalidContract,
173 "unsupported_migration_format",
174 "canonical migration format is not supported",
175 )
176 .with_detail("actual", value.to_owned())
177 .with_detail("supported", MIGRATION_FORMAT_V1))
178 }
179 }
180
181 pub const fn as_str(self) -> &'static str {
183 MIGRATION_FORMAT_V1
184 }
185}
186
187impl Serialize for MigrationFormat {
188 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189 where
190 S: Serializer,
191 {
192 serializer.serialize_str(self.as_str())
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
198#[serde(rename_all = "snake_case")]
199pub enum RetryPolicy {
200 Never,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
206#[serde(rename_all = "snake_case")]
207pub enum RecoveryPolicy {
208 OperatorRequired,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
214#[serde(transparent)]
215pub struct MigrationManifestDigest(FingerprintDigest);
216
217impl MigrationManifestDigest {
218 #[must_use]
220 pub fn compute(canonical_manifest_bytes: &[u8]) -> Self {
221 let digest = Sha256::digest(canonical_manifest_bytes);
222 let hex = digest
223 .iter()
224 .map(|byte| format!("{byte:02x}"))
225 .collect::<String>();
226 Self(
227 FingerprintDigest::from_hex(&hex)
228 .expect("SHA-256 always produces a valid lowercase 32-byte digest"),
229 )
230 }
231
232 pub fn from_hex(value: &str) -> Result<Self, Diagnostic> {
234 FingerprintDigest::from_hex(value).map(Self)
235 }
236
237 #[must_use]
239 pub fn to_hex(self) -> String {
240 self.0.to_hex()
241 }
242
243 #[must_use]
245 pub const fn bytes(self) -> [u8; 32] {
246 self.0.bytes()
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
252#[serde(transparent)]
253pub struct MigrationLedgerKey(Fingerprint);
254
255impl MigrationLedgerKey {
256 pub fn compute(id: &MigrationId) -> Result<Self, Diagnostic> {
258 Ok(Self(Fingerprint::compute(
259 FingerprintDomain::new(MIGRATION_ID_FINGERPRINT_DOMAIN)?,
260 CanonicalizationVersion::new(MIGRATION_ID_CANONICALIZATION)?,
261 None,
262 &id.canonical_bytes()?,
263 )))
264 }
265
266 pub const fn as_fingerprint(&self) -> &Fingerprint {
268 &self.0
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
274#[serde(transparent)]
275pub struct SchemaDeltaFingerprint(Fingerprint);
276
277impl SchemaDeltaFingerprint {
278 pub fn compute(delta: &SchemaDelta) -> Result<Self, Diagnostic> {
280 Ok(Self(Fingerprint::compute(
281 FingerprintDomain::new(SCHEMA_DELTA_FINGERPRINT_DOMAIN)?,
282 CanonicalizationVersion::new(SCHEMA_DELTA_FINGERPRINT_CANONICALIZATION)?,
283 None,
284 &delta.canonical_bytes()?,
285 )))
286 }
287
288 pub const fn as_fingerprint(&self) -> &Fingerprint {
290 &self.0
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
296#[serde(rename_all = "snake_case")]
297pub enum MigrationStepKind {
298 SchemaDelta,
300 Assertion,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
306pub struct SchemaDeltaStepContract {
307 delta_fingerprint: SchemaDeltaFingerprint,
308 id: MigrationStepId,
309 recovery: RecoveryPolicy,
310 required_capabilities: CapabilitySet,
311 retry: RetryPolicy,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 reverse: Option<SchemaDelta>,
314 source_semantics: ManagedSemanticSchemaFingerprint,
315 target_semantics: ManagedSemanticSchemaFingerprint,
316}
317
318impl SchemaDeltaStepContract {
319 pub fn new(
321 id: MigrationStepId,
322 delta: &SchemaDelta,
323 reverse: Option<SchemaDelta>,
324 ) -> Result<Self, Diagnostic> {
325 if let Some(candidate) = &reverse {
326 let inverse_operations = delta
327 .operations()
328 .iter()
329 .rev()
330 .flat_map(SchemaOperation::inverse)
331 .collect();
332 let expected = SchemaDelta::new(
333 delta.format(),
334 delta.target().clone(),
335 delta.source().clone(),
336 inverse_operations,
337 )?;
338 if candidate != &expected {
339 return Err(Diagnostic::stable(
340 DiagnosticCategory::InvalidContract,
341 "schema_delta_step_inverse_mismatch",
342 "schema step reverse delta is not the exact operation-wise inverse",
343 ));
344 }
345 }
346 Ok(Self {
347 delta_fingerprint: SchemaDeltaFingerprint::compute(delta)?,
348 id,
349 recovery: RecoveryPolicy::OperatorRequired,
350 required_capabilities: delta.required_capabilities().clone(),
351 retry: RetryPolicy::Never,
352 reverse,
353 source_semantics: delta.source().managed_semantic_schema().clone(),
354 target_semantics: delta.target().managed_semantic_schema().clone(),
355 })
356 }
357
358 pub const fn id(&self) -> &MigrationStepId {
360 &self.id
361 }
362
363 pub const fn source_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
365 &self.source_semantics
366 }
367
368 pub const fn target_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
370 &self.target_semantics
371 }
372
373 pub const fn required_capabilities(&self) -> &CapabilitySet {
375 &self.required_capabilities
376 }
377
378 pub const fn delta_fingerprint(&self) -> &SchemaDeltaFingerprint {
380 &self.delta_fingerprint
381 }
382
383 pub const fn retry(&self) -> RetryPolicy {
385 self.retry
386 }
387
388 pub const fn recovery(&self) -> RecoveryPolicy {
390 self.recovery
391 }
392
393 pub const fn reverse(&self) -> Option<&SchemaDelta> {
395 self.reverse.as_ref()
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
401pub struct SchemaDeltaStep {
402 contract: SchemaDeltaStepContract,
403 delta: SchemaDelta,
404 kind: MigrationStepKind,
405}
406
407impl SchemaDeltaStep {
408 pub fn new(
410 id: MigrationStepId,
411 delta: SchemaDelta,
412 reverse: Option<SchemaDelta>,
413 ) -> Result<Self, Diagnostic> {
414 let contract = SchemaDeltaStepContract::new(id, &delta, reverse)?;
415 Ok(Self {
416 contract,
417 delta,
418 kind: MigrationStepKind::SchemaDelta,
419 })
420 }
421
422 pub const fn kind(&self) -> MigrationStepKind {
424 self.kind
425 }
426
427 pub const fn contract(&self) -> &SchemaDeltaStepContract {
429 &self.contract
430 }
431
432 pub const fn delta(&self) -> &SchemaDelta {
434 &self.delta
435 }
436
437 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
439 to_canonical_json(self)
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
445pub struct AssertionStepContract {
446 id: MigrationStepId,
447 plan_fingerprint: MigrationAssertionPlanFingerprint,
448 recovery: RecoveryPolicy,
449 required_capabilities: CapabilitySet,
450 retry: RetryPolicy,
451 source_semantics: ManagedSemanticSchemaFingerprint,
452 target_semantics: ManagedSemanticSchemaFingerprint,
453}
454
455impl AssertionStepContract {
456 fn derive(
457 id: MigrationStepId,
458 plan: &MigrationAssertionPlan,
459 expected: AssertionExpectation,
460 ) -> Result<Self, Diagnostic> {
461 if expected != AssertionExpectation::NoRows
462 || plan.expectation() != AssertionExpectation::NoRows
463 {
464 return Err(Diagnostic::stable(
465 DiagnosticCategory::InvalidContract,
466 "migration_assertion_step_expectation_mismatch",
467 "persisted migration assertions support only the no-rows expectation",
468 ));
469 }
470 let mut required_capabilities = plan.required_capabilities().clone();
471 required_capabilities.insert(
472 CapabilityId::new(CONDITIONAL_RESOLUTION_CAPABILITY)
473 .expect("the fixed conditional-resolution capability is canonical"),
474 );
475 Ok(Self {
476 id,
477 plan_fingerprint: plan.fingerprint()?,
478 recovery: RecoveryPolicy::OperatorRequired,
479 required_capabilities,
480 retry: RetryPolicy::Never,
481 source_semantics: plan.managed_semantics().clone(),
482 target_semantics: plan.managed_semantics().clone(),
483 })
484 }
485
486 pub const fn id(&self) -> &MigrationStepId {
488 &self.id
489 }
490
491 pub const fn plan_fingerprint(&self) -> &MigrationAssertionPlanFingerprint {
493 &self.plan_fingerprint
494 }
495
496 pub const fn required_capabilities(&self) -> &CapabilitySet {
498 &self.required_capabilities
499 }
500
501 pub const fn retry(&self) -> RetryPolicy {
503 self.retry
504 }
505
506 pub const fn recovery(&self) -> RecoveryPolicy {
508 self.recovery
509 }
510
511 pub const fn source_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
513 &self.source_semantics
514 }
515
516 pub const fn target_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
518 &self.target_semantics
519 }
520
521 pub const fn reverse(&self) -> Option<()> {
523 None
524 }
525}
526
527#[derive(Debug, Clone, PartialEq, Eq)]
529pub enum MigrationStep {
530 SchemaDelta(Box<SchemaDeltaStep>),
532 Assertion {
534 contract: Box<AssertionStepContract>,
536 plan: Box<MigrationAssertionPlan>,
538 expected: AssertionExpectation,
540 },
541}
542
543impl MigrationStep {
544 pub fn assertion(
546 id: MigrationStepId,
547 plan: MigrationAssertionPlan,
548 expected: AssertionExpectation,
549 ) -> Result<Self, Diagnostic> {
550 let contract = AssertionStepContract::derive(id, &plan, expected)?;
551 Ok(Self::Assertion {
552 contract: Box::new(contract),
553 plan: Box::new(plan),
554 expected,
555 })
556 }
557
558 pub const fn kind(&self) -> MigrationStepKind {
560 match self {
561 Self::SchemaDelta(_) => MigrationStepKind::SchemaDelta,
562 Self::Assertion { .. } => MigrationStepKind::Assertion,
563 }
564 }
565
566 pub const fn id(&self) -> &MigrationStepId {
568 match self {
569 Self::SchemaDelta(step) => step.contract().id(),
570 Self::Assertion { contract, .. } => contract.id(),
571 }
572 }
573
574 pub const fn required_capabilities(&self) -> &CapabilitySet {
576 match self {
577 Self::SchemaDelta(step) => step.contract().required_capabilities(),
578 Self::Assertion { contract, .. } => contract.required_capabilities(),
579 }
580 }
581
582 pub fn as_schema_delta(&self) -> Option<&SchemaDeltaStep> {
584 match self {
585 Self::SchemaDelta(step) => Some(step),
586 Self::Assertion { .. } => None,
587 }
588 }
589
590 pub fn as_assertion(
592 &self,
593 ) -> Option<(
594 &AssertionStepContract,
595 &MigrationAssertionPlan,
596 AssertionExpectation,
597 )> {
598 match self {
599 Self::SchemaDelta(_) => None,
600 Self::Assertion {
601 contract,
602 plan,
603 expected,
604 } => Some((contract, plan, *expected)),
605 }
606 }
607
608 pub fn validate(&self) -> Result<(), Diagnostic> {
610 let rebuilt = match self {
611 Self::SchemaDelta(step) => Self::SchemaDelta(Box::new(SchemaDeltaStep::new(
612 step.contract().id().clone(),
613 step.delta().clone(),
614 step.contract().reverse().cloned(),
615 )?)),
616 Self::Assertion {
617 contract,
618 plan,
619 expected,
620 } => Self::assertion(contract.id().clone(), plan.as_ref().clone(), *expected)?,
621 };
622 if &rebuilt != self {
623 return Err(Diagnostic::stable(
624 DiagnosticCategory::Integrity,
625 "migration_step_contract_mismatch",
626 "migration step claims differ from constructor-derived claims",
627 ));
628 }
629 Ok(())
630 }
631
632 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
634 to_canonical_json(self)
635 }
636}
637
638impl From<SchemaDeltaStep> for MigrationStep {
639 fn from(step: SchemaDeltaStep) -> Self {
640 Self::SchemaDelta(Box::new(step))
641 }
642}
643
644#[derive(Serialize)]
645struct AssertionStepView<'a> {
646 contract: &'a AssertionStepContract,
647 expected: AssertionExpectation,
648 kind: MigrationStepKind,
649 plan: &'a MigrationAssertionPlan,
650}
651
652impl Serialize for MigrationStep {
653 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
654 where
655 S: Serializer,
656 {
657 match self {
658 Self::SchemaDelta(step) => step.serialize(serializer),
659 Self::Assertion {
660 contract,
661 plan,
662 expected,
663 } => AssertionStepView {
664 contract,
665 expected: *expected,
666 kind: MigrationStepKind::Assertion,
667 plan,
668 }
669 .serialize(serializer),
670 }
671 }
672}
673
674#[derive(Serialize)]
675struct MigrationPlanView<'a> {
676 steps: &'a [MigrationStep],
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
681#[serde(transparent)]
682pub struct MigrationPlanFingerprint(Fingerprint);
683
684impl MigrationPlanFingerprint {
685 pub fn canonical_plan_bytes(steps: &[MigrationStep]) -> Result<Vec<u8>, Diagnostic> {
687 to_canonical_json(&MigrationPlanView { steps })
688 }
689
690 pub fn compute(steps: &[MigrationStep]) -> Result<Self, Diagnostic> {
692 Ok(Self(Fingerprint::compute(
693 FingerprintDomain::new(MIGRATION_PLAN_FINGERPRINT_DOMAIN)?,
694 CanonicalizationVersion::new(MIGRATION_PLAN_FINGERPRINT_CANONICALIZATION)?,
695 None,
696 &Self::canonical_plan_bytes(steps)?,
697 )))
698 }
699
700 pub const fn as_fingerprint(&self) -> &Fingerprint {
702 &self.0
703 }
704}