Skip to main content

type_bridge_schema_migration/
profile.rs

1//! Closed TypeDB 3.12.1 schema-transition registry.
2
3use std::collections::BTreeSet;
4use std::sync::OnceLock;
5
6use serde::Serialize;
7use type_bridge_contract::capability::{CapabilityId, CapabilitySet};
8use type_bridge_contract::codec::to_canonical_json;
9use type_bridge_contract::diagnostic::Diagnostic;
10use type_bridge_contract::fingerprint::{
11    CanonicalizationVersion, FingerprintDomain, SemanticProfileId,
12};
13use type_bridge_contract::id::FunctionId;
14use type_bridge_contract::migration::CONDITIONAL_RESOLUTION_CAPABILITY;
15use type_bridge_contract::migration_assertion_capability_vocabulary;
16use type_bridge_contract::migration_backfill::COPY_ATTRIBUTE_BACKFILL_CAPABILITY;
17use type_bridge_contract::schema::{
18    AnnotationFact, AnnotationKindId, AnnotationSubjectId, SchemaAnnotationValue, SchemaFact,
19    SchemaOperation, SchemaOperationKind,
20};
21use type_bridge_contract::schema_delta::{
22    SCHEMA_REDEFINE_CAPABILITY, SCHEMA_TRANSITION_CAPABILITY_IDS,
23    schema_transition_capability_vocabulary,
24};
25use type_bridge_contract::schema_lowering::{
26    SCHEMA_LOWERING_PROFILE_CANONICALIZATION, SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN,
27    SchemaLoweringProfileBinding, SchemaLoweringProfileFingerprint, SchemaLoweringProfileId,
28};
29use type_bridge_schema::{BUILTIN_SCHEMA_CAPABILITY_IDS, SafetyClass, SafetyClassificationError};
30
31const PROVIDER: &str = "typedb";
32const PROVIDER_VERSION: &str = "3.12.1";
33const SEMANTIC_PROFILE: &str = "typedb-3.12.1/v1";
34
35const CAP_TRANSACTION_ATOMIC: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[0];
36const CAP_DEFINE: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[1];
37const CAP_UNDEFINE: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[2];
38const CAP_REDEFINE_SUB: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[3];
39const CAP_REDEFINE_VALUE: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[4];
40const CAP_REDEFINE_RELATES_SPECIALIZATION: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[5];
41const CAP_REDEFINE_ANNOTATION: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[6];
42const CAP_REDEFINE_FUNCTION: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[7];
43const CAP_REPLACE_SUB_ANNOTATION: &str = SCHEMA_TRANSITION_CAPABILITY_IDS[8];
44
45/// Closed binding-neutral capability vocabulary for generated migration catalogs.
46pub fn migration_runtime_capability_vocabulary() -> Result<CapabilitySet, Diagnostic> {
47    let mut capabilities = schema_transition_capability_vocabulary();
48    for capability in BUILTIN_SCHEMA_CAPABILITY_IDS {
49        capabilities.insert(CapabilityId::new(*capability)?);
50    }
51    for capability in migration_assertion_capability_vocabulary().iter().cloned() {
52        capabilities.insert(capability);
53    }
54    for capability in [
55        SCHEMA_REDEFINE_CAPABILITY,
56        CONDITIONAL_RESOLUTION_CAPABILITY,
57        COPY_ATTRIBUTE_BACKFILL_CAPABILITY,
58    ] {
59        capabilities.insert(CapabilityId::new(capability)?);
60    }
61    Ok(capabilities)
62}
63
64#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
65#[serde(rename_all = "snake_case")]
66/// Schema fact category addressed by a lowering rule.
67pub enum FactKind {
68    /// Type declaration fact.
69    Type,
70    /// Direct subtype fact.
71    Sub,
72    /// Attribute value-type fact.
73    Value,
74    /// Attribute ownership fact.
75    Owns,
76    /// Relation role declaration fact.
77    Relates,
78    /// Relation role specialization fact.
79    RelatesSpecialization,
80    /// Role-playing fact.
81    Plays,
82    /// Function declaration fact.
83    Function,
84    /// Struct declaration fact.
85    Struct,
86}
87
88impl FactKind {
89    /// Closed set of fact kinds in stable registry order.
90    pub const ALL: [Self; 9] = [
91        Self::Type,
92        Self::Sub,
93        Self::Value,
94        Self::Owns,
95        Self::Relates,
96        Self::RelatesSpecialization,
97        Self::Plays,
98        Self::Function,
99        Self::Struct,
100    ];
101}
102
103#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
104#[serde(rename_all = "snake_case")]
105/// Operation applied to one schema fact.
106pub enum FactTransition {
107    /// Introduce a fact.
108    Define,
109    /// Remove a fact.
110    Undefine,
111    /// Replace a fact in place.
112    Redefine,
113}
114
115impl FactTransition {
116    /// Closed set of fact transitions in stable registry order.
117    pub const ALL: [Self; 3] = [Self::Define, Self::Undefine, Self::Redefine];
118}
119
120#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
121#[serde(rename_all = "snake_case")]
122/// Schema subject category to which an annotation is attached.
123pub enum AnnotationSubjectKind {
124    /// Type declaration subject.
125    Type,
126    /// Direct subtype subject.
127    Sub,
128    /// Attribute value-type subject.
129    Value,
130    /// Attribute ownership subject.
131    Owns,
132    /// Relation role subject.
133    Relates,
134    /// Role-playing subject.
135    Plays,
136    /// Function declaration subject.
137    Function,
138    /// Struct declaration subject.
139    Struct,
140}
141
142impl AnnotationSubjectKind {
143    /// Closed set of annotation subjects in stable registry order.
144    pub const ALL: [Self; 8] = [
145        Self::Type,
146        Self::Sub,
147        Self::Value,
148        Self::Owns,
149        Self::Relates,
150        Self::Plays,
151        Self::Function,
152        Self::Struct,
153    ];
154}
155
156#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
157#[serde(rename_all = "snake_case")]
158/// Annotation category covered by the lowering profile.
159pub enum AnnotationKind {
160    /// Abstract-type annotation.
161    Abstract,
162    /// Independent-attribute annotation.
163    Independent,
164    /// Key annotation.
165    Key,
166    /// Unique annotation.
167    Unique,
168    /// Cardinality annotation.
169    Card,
170    /// Regular-expression constraint.
171    Regex,
172    /// Ordered range constraint.
173    Range,
174    /// Enumerated-values constraint.
175    Values,
176    /// Documentation annotation.
177    Doc,
178    /// Keyed metadata annotation.
179    Meta,
180}
181
182impl AnnotationKind {
183    /// Closed set of annotation kinds in stable registry order.
184    pub const ALL: [Self; 10] = [
185        Self::Abstract,
186        Self::Independent,
187        Self::Key,
188        Self::Unique,
189        Self::Card,
190        Self::Regex,
191        Self::Range,
192        Self::Values,
193        Self::Doc,
194        Self::Meta,
195    ];
196}
197
198#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
199#[serde(rename_all = "snake_case")]
200/// Change applied to an annotation.
201pub enum AnnotationTransition {
202    /// Attach an annotation that was absent.
203    Add,
204    /// Replace an annotation value.
205    Change,
206    /// Remove an annotation.
207    Remove,
208}
209
210impl AnnotationTransition {
211    /// Closed set of annotation transitions in stable registry order.
212    pub const ALL: [Self; 3] = [Self::Add, Self::Change, Self::Remove];
213}
214
215#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
216#[serde(rename_all = "snake_case")]
217/// Provider operation used to lower a semantic transition.
218pub enum LoweringMechanism {
219    /// Lower to a TypeQL `define` statement.
220    Define,
221    /// Lower to a TypeQL `undefine` statement.
222    Undefine,
223    /// Lower to a TypeQL `redefine` statement.
224    Redefine,
225    /// Lower to an atomic `undefine` followed by `define`.
226    AtomicUndefineDefine,
227    /// The provider cannot faithfully lower the transition.
228    Unsupported,
229}
230
231#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
232/// Complete lowering and safety decision for one transition.
233pub struct TransitionRule {
234    /// Provider operation used for lowering.
235    pub mechanism: LoweringMechanism,
236    /// Required operator-safety classification.
237    pub safety: SafetyClass,
238    /// Capabilities required from the execution provider.
239    pub required_capabilities: CapabilitySet,
240    /// Whether metadata removal must address one exact metadata key.
241    pub keyed_meta: bool,
242}
243
244#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
245/// Registry row for one fact transition.
246pub struct FactTransitionRule {
247    /// Fact category matched by the row.
248    pub fact: FactKind,
249    /// Transition matched by the row.
250    pub transition: FactTransition,
251    /// Lowering decision for the matched pair.
252    pub rule: TransitionRule,
253}
254
255#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
256/// Registry row for one annotation transition.
257pub struct AnnotationTransitionRule {
258    /// Subject category matched by the row.
259    pub subject: AnnotationSubjectKind,
260    /// Annotation category matched by the row.
261    pub annotation: AnnotationKind,
262    /// Transition matched by the row.
263    pub transition: AnnotationTransition,
264    /// Lowering decision for the matched tuple.
265    pub rule: TransitionRule,
266}
267
268#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
269#[serde(rename_all = "snake_case")]
270/// Semantic safety scenario classified independently of provider syntax.
271pub enum SafetyScenario {
272    /// An explicit annotation equals the provider's implicit default.
273    ExplicitDefaultEquivalent,
274    /// Documentation or metadata changes without stored-data impact.
275    DocMetaTransition,
276    /// Add an optional ownership, role, or playing interface.
277    AddOptionalInterface,
278    /// Add an interface with a required minimum cardinality.
279    AddRequiredCardinality,
280    /// Add a key or uniqueness constraint.
281    AddKeyOrUnique,
282    /// Expand an allowed cardinality interval.
283    WidenCardinality,
284    /// Contract an allowed cardinality interval.
285    NarrowCardinality,
286    /// Remove a cardinality annotation equal to the provider default.
287    RemoveCardinalityToEqualDefault,
288    /// Remove a cardinality annotation whose provider default is narrower.
289    RemoveCardinalityToNarrowerDefault,
290    /// Remove a cardinality annotation whose provider default is wider.
291    RemoveCardinalityToWiderDefault,
292    /// Add or tighten a stored-value constraint.
293    AddOrTightenValueConstraint,
294    /// Remove a stored-value constraint.
295    RemoveValueConstraint,
296    /// Mark a type abstract.
297    AddAbstract,
298    /// Make an abstract type concrete.
299    RemoveAbstract,
300    /// Mark an attribute type independent.
301    AddIndependent,
302    /// Remove independent attribute status.
303    RemoveIndependent,
304    /// Change a direct supertype.
305    ChangeSub,
306    /// Change a relation role specialization.
307    ChangeRelatesSpecialization,
308    /// Change an attribute value type.
309    ChangeValueType,
310    /// Remove a schema fact.
311    RemoveFact,
312    /// Replace a function definition.
313    RedefineFunction,
314    /// Request a transition unsupported by the provider profile.
315    UnsupportedProviderTransition,
316}
317
318impl SafetyScenario {
319    /// Closed set of safety scenarios in stable registry order.
320    pub const ALL: [Self; 22] = [
321        Self::ExplicitDefaultEquivalent,
322        Self::DocMetaTransition,
323        Self::AddOptionalInterface,
324        Self::AddRequiredCardinality,
325        Self::AddKeyOrUnique,
326        Self::WidenCardinality,
327        Self::NarrowCardinality,
328        Self::RemoveCardinalityToEqualDefault,
329        Self::RemoveCardinalityToNarrowerDefault,
330        Self::RemoveCardinalityToWiderDefault,
331        Self::AddOrTightenValueConstraint,
332        Self::RemoveValueConstraint,
333        Self::AddAbstract,
334        Self::RemoveAbstract,
335        Self::AddIndependent,
336        Self::RemoveIndependent,
337        Self::ChangeSub,
338        Self::ChangeRelatesSpecialization,
339        Self::ChangeValueType,
340        Self::RemoveFact,
341        Self::RedefineFunction,
342        Self::UnsupportedProviderTransition,
343    ];
344}
345
346#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
347#[serde(rename_all = "snake_case")]
348/// Additional evidence required before a migration can proceed safely.
349pub enum EvidenceRequirement {
350    /// No evidence beyond the verified schema delta is required.
351    None,
352    /// Existing stored data must be proven valid under the target schema.
353    ExistingDataSatisfiesTarget,
354    /// Existing instances require a data backfill.
355    Backfill,
356    /// Values require an explicit conversion supplied by the operator.
357    ExplicitConversion,
358    /// The operator must approve the classified risk.
359    OperatorApproval,
360    /// The execution provider must first gain support for the transition.
361    ProviderSupport,
362}
363
364#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
365/// Safety classification and evidence policy for one semantic scenario.
366pub struct SafetyScenarioRule {
367    /// Scenario classified by the row.
368    pub scenario: SafetyScenario,
369    /// Safety class assigned to the scenario.
370    pub safety: SafetyClass,
371    /// Evidence required before execution.
372    pub evidence: EvidenceRequirement,
373}
374
375#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
376#[serde(rename_all = "snake_case")]
377/// Interface category with a provider-defined default cardinality.
378pub enum InterfaceKind {
379    /// Attribute ownership interface.
380    Owns,
381    /// Relation role interface.
382    Relates,
383    /// Role-playing interface.
384    Plays,
385}
386
387#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
388/// Provider default cardinality for one interface category.
389pub struct InterfaceDefault {
390    /// Interface category governed by this default.
391    pub interface: InterfaceKind,
392    /// Inclusive minimum cardinality.
393    pub min: u64,
394    /// Inclusive maximum cardinality, or unbounded when absent.
395    pub max: Option<u64>,
396}
397
398const TYPEDB_3_12_1_INTERFACE_DEFAULTS: [InterfaceDefault; 3] = [
399    InterfaceDefault {
400        interface: InterfaceKind::Owns,
401        min: 0,
402        max: Some(1),
403    },
404    InterfaceDefault {
405        interface: InterfaceKind::Relates,
406        min: 0,
407        max: Some(1),
408    },
409    InterfaceDefault {
410        interface: InterfaceKind::Plays,
411        min: 0,
412        max: None,
413    },
414];
415
416#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
417#[serde(rename_all = "snake_case")]
418/// Measured provider behavior that supports the frozen lowering profile.
419pub enum EvidenceFlag {
420    /// `redefine` statements address exactly one schema subject.
421    RedefineQueriesAreSingleton,
422    /// A rejected `redefine` leaves the schema unchanged.
423    RejectedRedefinePreservesSchema,
424    /// Schema transactions commit atomically.
425    SchemaTransactionsAreAtomic,
426    /// Role specialization preserves data that remains valid.
427    RelatesSpecializationPreservesValidData,
428    /// Direct redefinition of a subtype annotation is rejected.
429    SubAnnotationDirectRedefineRejected,
430    /// Atomic replacement of a subtype annotation is supported.
431    SubAnnotationAtomicReplacementSupported,
432    /// Metadata removal addresses one exact metadata key.
433    MetaRemovalIsKeyed,
434    /// Function redefinition alone can leave stored metadata stale.
435    FunctionRedefineLeavesStoredMetadataStale,
436    /// Atomic function replacement updates its bound metadata.
437    FunctionAtomicReplacementUpdatesMetadata,
438    /// Struct transitions are not supported by this profile.
439    StructTransitionsUnsupported,
440    /// Removing independence deletes attributes without an owner.
441    IndependentRemovalDeletesOwnerlessAttributes,
442    /// Guarded schema changes reject incompatible stored data.
443    GuardedChangesRejectInvalidData,
444    /// Removing an owns cardinality restores the `0..1` default.
445    OwnsCardRemovalRestoresZeroToOneDefault,
446    /// Removing a relates cardinality restores the `0..1` default.
447    RelatesCardRemovalRestoresZeroToOneDefault,
448    /// Removing a plays cardinality restores the `0..*` default.
449    PlaysCardRemovalRestoresZeroToUnboundedDefault,
450}
451
452impl EvidenceFlag {
453    /// Closed set of provider evidence flags in stable registry order.
454    pub const ALL: [Self; 15] = [
455        Self::RedefineQueriesAreSingleton,
456        Self::RejectedRedefinePreservesSchema,
457        Self::SchemaTransactionsAreAtomic,
458        Self::RelatesSpecializationPreservesValidData,
459        Self::SubAnnotationDirectRedefineRejected,
460        Self::SubAnnotationAtomicReplacementSupported,
461        Self::MetaRemovalIsKeyed,
462        Self::FunctionRedefineLeavesStoredMetadataStale,
463        Self::FunctionAtomicReplacementUpdatesMetadata,
464        Self::StructTransitionsUnsupported,
465        Self::IndependentRemovalDeletesOwnerlessAttributes,
466        Self::GuardedChangesRejectInvalidData,
467        Self::OwnsCardRemovalRestoresZeroToOneDefault,
468        Self::RelatesCardRemovalRestoresZeroToOneDefault,
469        Self::PlaysCardRemovalRestoresZeroToUnboundedDefault,
470    ];
471}
472
473#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
474/// Frozen, fingerprinted registry for lowering schema deltas to one provider.
475pub struct SchemaLoweringProfile {
476    /// Stable profile identifier.
477    pub id: SchemaLoweringProfileId,
478    /// Domain used to fingerprint canonical profile bytes.
479    pub fingerprint_domain: FingerprintDomain,
480    /// Canonicalization identity used for profile bytes.
481    pub canonicalization: CanonicalizationVersion,
482    /// Schema semantic profile accepted by this lowering profile.
483    pub semantic_profile: SemanticProfileId,
484    /// Provider family name.
485    pub provider: String,
486    /// Exact measured provider version.
487    pub provider_version: String,
488    /// Whether schema statements execute transactionally.
489    pub transactional_schema_queries: bool,
490    /// Capabilities required to execute every supported rule.
491    pub required_capabilities: CapabilitySet,
492    /// Provider defaults for schema interfaces.
493    pub interface_defaults: Vec<InterfaceDefault>,
494    /// Closed fact-transition rule table.
495    pub fact_rules: Vec<FactTransitionRule>,
496    /// Closed annotation-transition rule table.
497    pub annotation_rules: Vec<AnnotationTransitionRule>,
498    /// Closed semantic safety rule table.
499    pub safety_rules: Vec<SafetyScenarioRule>,
500    /// Measured provider evidence bound by the profile.
501    pub evidence: Vec<EvidenceFlag>,
502}
503
504impl SchemaLoweringProfile {
505    /// Return the rule for an exact fact transition, when registered.
506    pub fn fact_rule(&self, fact: FactKind, transition: FactTransition) -> Option<&TransitionRule> {
507        self.fact_rules
508            .iter()
509            .find(|row| row.fact == fact && row.transition == transition)
510            .map(|row| &row.rule)
511    }
512
513    /// Return the rule for an exact annotation transition, when registered.
514    pub fn annotation_rule(
515        &self,
516        subject: AnnotationSubjectKind,
517        annotation: AnnotationKind,
518        transition: AnnotationTransition,
519    ) -> Option<&TransitionRule> {
520        self.annotation_rules
521            .iter()
522            .find(|row| {
523                row.subject == subject
524                    && row.annotation == annotation
525                    && row.transition == transition
526            })
527            .map(|row| &row.rule)
528    }
529
530    /// Return the safety rule for an exact semantic scenario.
531    pub fn safety_rule(&self, scenario: SafetyScenario) -> Option<&SafetyScenarioRule> {
532        self.safety_rules
533            .iter()
534            .find(|row| row.scenario == scenario)
535    }
536}
537
538#[derive(Debug)]
539pub(crate) struct OperationTransitionClassification {
540    pub(crate) safety: SafetyClass,
541    pub(crate) atomic: bool,
542    pub(crate) required_capabilities: CapabilitySet,
543}
544
545pub(crate) fn classify_operation_transition(
546    operation: &SchemaOperation,
547) -> Result<OperationTransitionClassification, SafetyClassificationError> {
548    let mut rules = Vec::new();
549    match operation.kind() {
550        SchemaOperationKind::Define => {
551            let facts = operation.defined_facts().expect("define exposes facts");
552            let functions = facts
553                .iter()
554                .filter_map(|fact| match fact {
555                    SchemaFact::Function(function) => Some(function.id().clone()),
556                    _ => None,
557                })
558                .collect::<BTreeSet<_>>();
559            for fact in facts {
560                rules.push(classify_defined_fact(fact, &functions));
561            }
562        }
563        SchemaOperationKind::Redefine => rules.push(classify_redefinition(
564            operation
565                .expected_fact()
566                .expect("redefine exposes expected fact"),
567            operation
568                .replacement_fact()
569                .expect("redefine exposes replacement fact"),
570        )?),
571        SchemaOperationKind::Undefine => rules.push(classify_undefined_fact(
572            operation.undefined_fact().expect("undefine exposes fact"),
573        )),
574    }
575
576    let mut safety = SafetyClass::FormalOnly;
577    let mut atomic = false;
578    let mut required_capabilities = CapabilitySet::new();
579    for rule in rules {
580        if safety_rank(rule.safety) > safety_rank(safety) {
581            safety = rule.safety;
582        }
583        atomic |= rule.mechanism == LoweringMechanism::AtomicUndefineDefine;
584        for capability in rule.required_capabilities.iter().cloned() {
585            required_capabilities.insert(capability);
586        }
587    }
588    Ok(OperationTransitionClassification {
589        safety,
590        atomic,
591        required_capabilities,
592    })
593}
594
595fn classify_defined_fact(fact: &SchemaFact, functions: &BTreeSet<FunctionId>) -> TransitionRule {
596    match fact {
597        SchemaFact::Relates(relates) if relates.specializes().is_some() => {
598            fact_transition_rule(FactKind::RelatesSpecialization, FactTransition::Define)
599        }
600        SchemaFact::Annotation(annotation) => {
601            if let AnnotationSubjectId::Function(function) = annotation.id().subject()
602                && functions.contains(function)
603                && matches!(
604                    annotation.id().kind(),
605                    AnnotationKindId::Doc | AnnotationKindId::Meta(_)
606                )
607            {
608                return define(SafetyClass::SchemaMetadata, false);
609            }
610            classify_annotation(annotation, AnnotationTransition::Add, None)
611        }
612        _ => fact_transition_rule(fact_kind(fact), FactTransition::Define),
613    }
614}
615
616fn classify_undefined_fact(fact: &SchemaFact) -> TransitionRule {
617    match fact {
618        SchemaFact::Annotation(annotation) => {
619            classify_annotation(annotation, AnnotationTransition::Remove, None)
620        }
621        _ => fact_transition_rule(fact_kind(fact), FactTransition::Undefine),
622    }
623}
624
625fn classify_redefinition(
626    expected: &SchemaFact,
627    replacement: &SchemaFact,
628) -> Result<TransitionRule, SafetyClassificationError> {
629    match (expected, replacement) {
630        (SchemaFact::Relates(old), SchemaFact::Relates(new)) => {
631            let transition = match (old.specializes(), new.specializes()) {
632                (None, Some(_)) => FactTransition::Define,
633                (Some(_), None) => FactTransition::Undefine,
634                (Some(_), Some(_)) => FactTransition::Redefine,
635                (None, None) => {
636                    return Err(SafetyClassificationError::UnchangedRelatesSpecialization);
637                }
638            };
639            Ok(fact_transition_rule(
640                FactKind::RelatesSpecialization,
641                transition,
642            ))
643        }
644        (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) => Ok(classify_annotation(
645            new,
646            AnnotationTransition::Change,
647            Some(old),
648        )),
649        (left, right) if std::mem::discriminant(left) == std::mem::discriminant(right) => Ok(
650            fact_transition_rule(fact_kind(right), FactTransition::Redefine),
651        ),
652        _ => Err(SafetyClassificationError::RedefinitionCategoryChanged),
653    }
654}
655
656fn fact_kind(fact: &SchemaFact) -> FactKind {
657    match fact {
658        SchemaFact::Type(_) => FactKind::Type,
659        SchemaFact::Sub(_) => FactKind::Sub,
660        SchemaFact::Value(_) => FactKind::Value,
661        SchemaFact::Owns(_) => FactKind::Owns,
662        SchemaFact::Relates(_) => FactKind::Relates,
663        SchemaFact::Plays(_) => FactKind::Plays,
664        SchemaFact::Annotation(_) => unreachable!("annotations use the annotation registry"),
665        SchemaFact::Function(_) => FactKind::Function,
666        SchemaFact::Struct(_) => FactKind::Struct,
667    }
668}
669
670fn classify_annotation(
671    annotation: &AnnotationFact,
672    transition: AnnotationTransition,
673    expected: Option<&AnnotationFact>,
674) -> TransitionRule {
675    // Ordered-collection migration execution belongs to Plan06. Keep the
676    // historical v1 profile bytes stable while classifying the additive
677    // contract kind explicitly and fail closed before TypeQL rendering.
678    if annotation.id().kind() == &AnnotationKindId::Distinct {
679        return unsupported(false);
680    }
681    let subject = annotation_subject_kind(annotation.id().subject());
682    let kind = annotation_kind(annotation.id().kind());
683    let mut rule = annotation_transition_rule(subject, kind, transition);
684    if kind == AnnotationKind::Card
685        && let Some(target) = annotation_cardinality(annotation)
686        && let Some(default) = default_cardinality(annotation.id().subject())
687    {
688        let (from, to) = match transition {
689            AnnotationTransition::Add => (default, target),
690            AnnotationTransition::Change => {
691                let Some(source) = expected.and_then(annotation_cardinality) else {
692                    return rule;
693                };
694                (source, target)
695            }
696            AnnotationTransition::Remove => (target, default),
697        };
698        rule.safety = cardinality_transition_safety(from, to);
699    }
700    rule
701}
702
703fn annotation_subject_kind(subject: &AnnotationSubjectId) -> AnnotationSubjectKind {
704    match subject {
705        AnnotationSubjectId::Type(_) => AnnotationSubjectKind::Type,
706        AnnotationSubjectId::Sub(_) => AnnotationSubjectKind::Sub,
707        AnnotationSubjectId::Value(_) => AnnotationSubjectKind::Value,
708        AnnotationSubjectId::Owns(_) => AnnotationSubjectKind::Owns,
709        AnnotationSubjectId::Relates(_) => AnnotationSubjectKind::Relates,
710        AnnotationSubjectId::Plays(_) => AnnotationSubjectKind::Plays,
711        AnnotationSubjectId::Function(_) => AnnotationSubjectKind::Function,
712    }
713}
714
715fn annotation_kind(kind: &AnnotationKindId) -> AnnotationKind {
716    match kind {
717        AnnotationKindId::Abstract => AnnotationKind::Abstract,
718        AnnotationKindId::Independent => AnnotationKind::Independent,
719        AnnotationKindId::Key => AnnotationKind::Key,
720        AnnotationKindId::Unique => AnnotationKind::Unique,
721        AnnotationKindId::Distinct => {
722            unreachable!("distinct is classified as unsupported before the v1 annotation registry")
723        }
724        AnnotationKindId::Card => AnnotationKind::Card,
725        AnnotationKindId::Regex => AnnotationKind::Regex,
726        AnnotationKindId::Range => AnnotationKind::Range,
727        AnnotationKindId::Values => AnnotationKind::Values,
728        AnnotationKindId::Doc => AnnotationKind::Doc,
729        AnnotationKindId::Meta(_) => AnnotationKind::Meta,
730    }
731}
732
733fn annotation_cardinality(annotation: &AnnotationFact) -> Option<(u64, Option<u64>)> {
734    match annotation.value() {
735        SchemaAnnotationValue::Cardinality(cardinality) => {
736            Some(((*cardinality).min(), (*cardinality).max()))
737        }
738        _ => None,
739    }
740}
741
742fn default_cardinality(subject: &AnnotationSubjectId) -> Option<(u64, Option<u64>)> {
743    let interface = match subject {
744        AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
745        AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
746        AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
747        _ => return None,
748    };
749    TYPEDB_3_12_1_INTERFACE_DEFAULTS
750        .iter()
751        .find(|default| default.interface == interface)
752        .map(|default| (default.min, default.max))
753}
754
755fn cardinality_transition_safety(from: (u64, Option<u64>), to: (u64, Option<u64>)) -> SafetyClass {
756    if from == to {
757        SafetyClass::FormalOnly
758    } else if interval_contains(to, from) {
759        SafetyClass::Additive
760    } else if interval_contains(from, to) {
761        SafetyClass::BackfillRequired
762    } else {
763        SafetyClass::Conditional
764    }
765}
766
767fn interval_contains(outer: (u64, Option<u64>), inner: (u64, Option<u64>)) -> bool {
768    outer.0 <= inner.0
769        && match (outer.1, inner.1) {
770            (None, _) => true,
771            (Some(_), None) => false,
772            (Some(outer), Some(inner)) => outer >= inner,
773        }
774}
775
776fn safety_rank(safety: SafetyClass) -> u8 {
777    match safety {
778        SafetyClass::FormalOnly => 0,
779        SafetyClass::SchemaMetadata => 1,
780        SafetyClass::Additive => 2,
781        SafetyClass::Conditional => 3,
782        SafetyClass::BackfillRequired => 4,
783        SafetyClass::Destructive => 5,
784        SafetyClass::Opaque => 6,
785        SafetyClass::Unsupported => 7,
786    }
787}
788
789fn capabilities(ids: &[&str]) -> CapabilitySet {
790    let mut capabilities = CapabilitySet::new();
791    for id in ids {
792        capabilities
793            .insert(CapabilityId::new(*id).expect("fixed schema-lowering capability id is valid"));
794    }
795    capabilities
796}
797
798fn transition_rule(
799    mechanism: LoweringMechanism,
800    safety: SafetyClass,
801    required: &[&str],
802    keyed_meta: bool,
803) -> TransitionRule {
804    TransitionRule {
805        mechanism,
806        safety,
807        required_capabilities: capabilities(required),
808        keyed_meta,
809    }
810}
811
812fn unsupported(keyed_meta: bool) -> TransitionRule {
813    transition_rule(
814        LoweringMechanism::Unsupported,
815        SafetyClass::Unsupported,
816        &[],
817        keyed_meta,
818    )
819}
820
821fn define(safety: SafetyClass, keyed_meta: bool) -> TransitionRule {
822    transition_rule(LoweringMechanism::Define, safety, &[CAP_DEFINE], keyed_meta)
823}
824
825fn undefine(safety: SafetyClass, keyed_meta: bool) -> TransitionRule {
826    transition_rule(
827        LoweringMechanism::Undefine,
828        safety,
829        &[CAP_UNDEFINE],
830        keyed_meta,
831    )
832}
833
834fn redefine(safety: SafetyClass, capability: &str, keyed_meta: bool) -> TransitionRule {
835    transition_rule(
836        LoweringMechanism::Redefine,
837        safety,
838        &[capability],
839        keyed_meta,
840    )
841}
842
843/// Resolve the compiled lowering rule for a schema fact transition.
844pub fn fact_transition_rule(fact: FactKind, transition: FactTransition) -> TransitionRule {
845    use FactKind as F;
846    use FactTransition as T;
847    use SafetyClass as S;
848
849    match (fact, transition) {
850        (F::Type, T::Define) => define(S::Additive, false),
851        (F::Type, T::Undefine) => undefine(S::Destructive, false),
852        (F::Type, T::Redefine) => unsupported(false),
853        (F::Sub, T::Define) => define(S::Conditional, false),
854        (F::Sub, T::Undefine) => undefine(S::Destructive, false),
855        (F::Sub, T::Redefine) => redefine(S::Conditional, CAP_REDEFINE_SUB, false),
856        (F::Value, T::Define) => define(S::Additive, false),
857        (F::Value, T::Undefine) => undefine(S::Destructive, false),
858        (F::Value, T::Redefine) => redefine(S::Destructive, CAP_REDEFINE_VALUE, false),
859        (F::Owns | F::Relates | F::Plays, T::Define) => define(S::Additive, false),
860        (F::Owns | F::Relates | F::Plays, T::Undefine) => undefine(S::Destructive, false),
861        (F::Owns | F::Relates | F::Plays, T::Redefine) => unsupported(false),
862        (F::RelatesSpecialization, T::Define) => define(S::Conditional, false),
863        (F::RelatesSpecialization, T::Undefine) => undefine(S::Conditional, false),
864        (F::RelatesSpecialization, T::Redefine) => {
865            redefine(S::Conditional, CAP_REDEFINE_RELATES_SPECIALIZATION, false)
866        }
867        (F::Function, T::Define) => define(S::Additive, false),
868        (F::Function, T::Undefine) => undefine(S::Destructive, false),
869        (F::Function, T::Redefine) => redefine(S::Opaque, CAP_REDEFINE_FUNCTION, false),
870        (F::Struct, _) => unsupported(false),
871    }
872}
873
874fn annotation_is_supported(subject: AnnotationSubjectKind, annotation: AnnotationKind) -> bool {
875    use AnnotationKind as A;
876    use AnnotationSubjectKind as S;
877
878    match subject {
879        S::Type => matches!(annotation, A::Abstract | A::Independent | A::Doc | A::Meta),
880        S::Sub => matches!(annotation, A::Doc | A::Meta),
881        S::Value => matches!(
882            annotation,
883            A::Regex | A::Range | A::Values | A::Doc | A::Meta
884        ),
885        S::Owns => matches!(
886            annotation,
887            A::Key | A::Unique | A::Card | A::Regex | A::Range | A::Values | A::Doc | A::Meta
888        ),
889        S::Relates => matches!(annotation, A::Abstract | A::Card | A::Doc | A::Meta),
890        S::Plays => matches!(annotation, A::Card | A::Doc | A::Meta),
891        S::Function | S::Struct => false,
892    }
893}
894
895/// Resolve the compiled lowering rule for an annotation transition.
896pub fn annotation_transition_rule(
897    subject: AnnotationSubjectKind,
898    annotation: AnnotationKind,
899    transition: AnnotationTransition,
900) -> TransitionRule {
901    use AnnotationKind as A;
902    use AnnotationSubjectKind as S;
903    use AnnotationTransition as T;
904    use SafetyClass as C;
905
906    let keyed_meta = annotation == A::Meta;
907    if !annotation_is_supported(subject, annotation) {
908        return unsupported(keyed_meta);
909    }
910    if subject == S::Sub {
911        return match transition {
912            T::Add => define(C::SchemaMetadata, keyed_meta),
913            T::Change => transition_rule(
914                LoweringMechanism::AtomicUndefineDefine,
915                C::SchemaMetadata,
916                &[
917                    CAP_TRANSACTION_ATOMIC,
918                    CAP_UNDEFINE,
919                    CAP_DEFINE,
920                    CAP_REPLACE_SUB_ANNOTATION,
921                ],
922                keyed_meta,
923            ),
924            T::Remove => undefine(C::SchemaMetadata, keyed_meta),
925        };
926    }
927
928    match annotation {
929        A::Doc | A::Meta => match transition {
930            T::Add => define(C::SchemaMetadata, keyed_meta),
931            T::Change => redefine(C::SchemaMetadata, CAP_REDEFINE_ANNOTATION, keyed_meta),
932            T::Remove => undefine(C::SchemaMetadata, keyed_meta),
933        },
934        A::Abstract | A::Independent | A::Key | A::Unique => match transition {
935            T::Change => unsupported(keyed_meta),
936            T::Add => {
937                let safety = match annotation {
938                    A::Abstract => C::Conditional,
939                    A::Independent => C::Additive,
940                    A::Key | A::Unique => C::BackfillRequired,
941                    _ => unreachable!(),
942                };
943                define(safety, keyed_meta)
944            }
945            T::Remove => {
946                let safety = match annotation {
947                    A::Independent => C::Destructive,
948                    A::Abstract | A::Key | A::Unique => C::Additive,
949                    _ => unreachable!(),
950                };
951                undefine(safety, keyed_meta)
952            }
953        },
954        A::Card => match transition {
955            T::Add => define(C::Conditional, keyed_meta),
956            T::Change => redefine(C::Conditional, CAP_REDEFINE_ANNOTATION, keyed_meta),
957            T::Remove => undefine(C::Conditional, keyed_meta),
958        },
959        A::Regex | A::Range | A::Values => match transition {
960            T::Add => define(C::Conditional, keyed_meta),
961            T::Change => redefine(C::Conditional, CAP_REDEFINE_ANNOTATION, keyed_meta),
962            T::Remove => undefine(C::Additive, keyed_meta),
963        },
964    }
965}
966
967fn safety_rule(scenario: SafetyScenario) -> SafetyScenarioRule {
968    use EvidenceRequirement as E;
969    use SafetyClass as C;
970    use SafetyScenario as S;
971
972    let (safety, evidence) = match scenario {
973        S::ExplicitDefaultEquivalent | S::RemoveCardinalityToEqualDefault => {
974            (C::FormalOnly, E::None)
975        }
976        S::DocMetaTransition => (C::SchemaMetadata, E::None),
977        S::AddOptionalInterface
978        | S::WidenCardinality
979        | S::RemoveCardinalityToWiderDefault
980        | S::RemoveValueConstraint
981        | S::RemoveAbstract
982        | S::AddIndependent => (C::Additive, E::None),
983        S::AddRequiredCardinality
984        | S::AddKeyOrUnique
985        | S::NarrowCardinality
986        | S::RemoveCardinalityToNarrowerDefault => (C::BackfillRequired, E::Backfill),
987        S::AddOrTightenValueConstraint
988        | S::AddAbstract
989        | S::ChangeSub
990        | S::ChangeRelatesSpecialization => (C::Conditional, E::ExistingDataSatisfiesTarget),
991        S::RemoveIndependent | S::RemoveFact => (C::Destructive, E::OperatorApproval),
992        S::ChangeValueType => (C::Destructive, E::ExplicitConversion),
993        S::RedefineFunction => (C::Opaque, E::OperatorApproval),
994        S::UnsupportedProviderTransition => (C::Unsupported, E::ProviderSupport),
995    };
996    SafetyScenarioRule {
997        scenario,
998        safety,
999        evidence,
1000    }
1001}
1002
1003fn build_profile() -> SchemaLoweringProfile {
1004    let mut fact_rules = Vec::with_capacity(FactKind::ALL.len() * FactTransition::ALL.len());
1005    for fact in FactKind::ALL {
1006        for transition in FactTransition::ALL {
1007            fact_rules.push(FactTransitionRule {
1008                fact,
1009                transition,
1010                rule: fact_transition_rule(fact, transition),
1011            });
1012        }
1013    }
1014    let mut annotation_rules = Vec::with_capacity(
1015        AnnotationSubjectKind::ALL.len()
1016            * AnnotationKind::ALL.len()
1017            * AnnotationTransition::ALL.len(),
1018    );
1019    for subject in AnnotationSubjectKind::ALL {
1020        for annotation in AnnotationKind::ALL {
1021            for transition in AnnotationTransition::ALL {
1022                annotation_rules.push(AnnotationTransitionRule {
1023                    subject,
1024                    annotation,
1025                    transition,
1026                    rule: annotation_transition_rule(subject, annotation, transition),
1027                });
1028            }
1029        }
1030    }
1031
1032    SchemaLoweringProfile {
1033        id: SchemaLoweringProfileId::typedb_3_12_1(),
1034        fingerprint_domain: FingerprintDomain::new(SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN)
1035            .expect("fixed fingerprint domain is valid"),
1036        canonicalization: CanonicalizationVersion::new(SCHEMA_LOWERING_PROFILE_CANONICALIZATION)
1037            .expect("fixed canonicalization is valid"),
1038        semantic_profile: SemanticProfileId::new(SEMANTIC_PROFILE)
1039            .expect("fixed semantic profile is valid"),
1040        provider: PROVIDER.to_owned(),
1041        provider_version: PROVIDER_VERSION.to_owned(),
1042        transactional_schema_queries: true,
1043        required_capabilities: schema_transition_capability_vocabulary(),
1044        interface_defaults: TYPEDB_3_12_1_INTERFACE_DEFAULTS.to_vec(),
1045        fact_rules,
1046        annotation_rules,
1047        safety_rules: SafetyScenario::ALL.into_iter().map(safety_rule).collect(),
1048        evidence: EvidenceFlag::ALL.to_vec(),
1049    }
1050}
1051
1052/// Return the immutable TypeDB 3.12.1 lowering profile.
1053pub fn typedb_3_12_1_profile() -> &'static SchemaLoweringProfile {
1054    static PROFILE: OnceLock<SchemaLoweringProfile> = OnceLock::new();
1055    PROFILE.get_or_init(build_profile)
1056}
1057
1058/// Encode the compiled lowering profile as canonical JSON bytes.
1059pub fn canonical_profile_bytes() -> Vec<u8> {
1060    to_canonical_json(typedb_3_12_1_profile())
1061        .expect("the trusted schema-lowering profile has canonical bytes")
1062}
1063
1064/// Compute the canonical fingerprint of the compiled lowering profile.
1065pub fn profile_fingerprint() -> SchemaLoweringProfileFingerprint {
1066    SchemaLoweringProfileFingerprint::compute(&canonical_profile_bytes())
1067}
1068
1069/// Resolve the executable registry's exact canonical profile binding.
1070pub fn schema_lowering_profile_binding() -> Result<SchemaLoweringProfileBinding, Diagnostic> {
1071    SchemaLoweringProfileBinding::from_canonical_profile_bytes(&canonical_profile_bytes())
1072}