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::schema::{
15    AnnotationFact, AnnotationKindId, AnnotationSubjectId, SchemaAnnotationValue, SchemaFact,
16    SchemaOperation, SchemaOperationKind,
17};
18use type_bridge_contract::schema_lowering::{
19    SCHEMA_LOWERING_PROFILE_CANONICALIZATION, SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN,
20    SchemaLoweringProfileBinding, SchemaLoweringProfileFingerprint, SchemaLoweringProfileId,
21};
22use type_bridge_schema::{SafetyClass, SafetyClassificationError};
23
24const PROVIDER: &str = "typedb";
25const PROVIDER_VERSION: &str = "3.12.1";
26const SEMANTIC_PROFILE: &str = "typedb-3.12.1/v1";
27
28const CAP_TRANSACTION_ATOMIC: &str = "schema.transaction.atomic";
29const CAP_DEFINE: &str = "schema.transition.define";
30const CAP_UNDEFINE: &str = "schema.transition.undefine";
31const CAP_REDEFINE_SUB: &str = "schema.transition.redefine.sub";
32const CAP_REDEFINE_VALUE: &str = "schema.transition.redefine.value";
33const CAP_REDEFINE_RELATES_SPECIALIZATION: &str =
34    "schema.transition.redefine.relates.specialization";
35const CAP_REDEFINE_ANNOTATION: &str = "schema.transition.redefine.annotation";
36const CAP_REDEFINE_FUNCTION: &str = "schema.transition.redefine.function";
37const CAP_REPLACE_SUB_ANNOTATION: &str = "schema.transition.replace.sub.annotation";
38
39const REQUIRED_CAPABILITY_IDS: [&str; 9] = [
40    CAP_TRANSACTION_ATOMIC,
41    CAP_DEFINE,
42    CAP_UNDEFINE,
43    CAP_REDEFINE_SUB,
44    CAP_REDEFINE_VALUE,
45    CAP_REDEFINE_RELATES_SPECIALIZATION,
46    CAP_REDEFINE_ANNOTATION,
47    CAP_REDEFINE_FUNCTION,
48    CAP_REPLACE_SUB_ANNOTATION,
49];
50
51#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
52#[serde(rename_all = "snake_case")]
53pub enum FactKind {
54    Type,
55    Sub,
56    Value,
57    Owns,
58    Relates,
59    RelatesSpecialization,
60    Plays,
61    Function,
62    Struct,
63}
64
65impl FactKind {
66    pub const ALL: [Self; 9] = [
67        Self::Type,
68        Self::Sub,
69        Self::Value,
70        Self::Owns,
71        Self::Relates,
72        Self::RelatesSpecialization,
73        Self::Plays,
74        Self::Function,
75        Self::Struct,
76    ];
77}
78
79#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
80#[serde(rename_all = "snake_case")]
81pub enum FactTransition {
82    Define,
83    Undefine,
84    Redefine,
85}
86
87impl FactTransition {
88    pub const ALL: [Self; 3] = [Self::Define, Self::Undefine, Self::Redefine];
89}
90
91#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
92#[serde(rename_all = "snake_case")]
93pub enum AnnotationSubjectKind {
94    Type,
95    Sub,
96    Value,
97    Owns,
98    Relates,
99    Plays,
100    Function,
101    Struct,
102}
103
104impl AnnotationSubjectKind {
105    pub const ALL: [Self; 8] = [
106        Self::Type,
107        Self::Sub,
108        Self::Value,
109        Self::Owns,
110        Self::Relates,
111        Self::Plays,
112        Self::Function,
113        Self::Struct,
114    ];
115}
116
117#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
118#[serde(rename_all = "snake_case")]
119pub enum AnnotationKind {
120    Abstract,
121    Independent,
122    Key,
123    Unique,
124    Card,
125    Regex,
126    Range,
127    Values,
128    Doc,
129    Meta,
130}
131
132impl AnnotationKind {
133    pub const ALL: [Self; 10] = [
134        Self::Abstract,
135        Self::Independent,
136        Self::Key,
137        Self::Unique,
138        Self::Card,
139        Self::Regex,
140        Self::Range,
141        Self::Values,
142        Self::Doc,
143        Self::Meta,
144    ];
145}
146
147#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
148#[serde(rename_all = "snake_case")]
149pub enum AnnotationTransition {
150    Add,
151    Change,
152    Remove,
153}
154
155impl AnnotationTransition {
156    pub const ALL: [Self; 3] = [Self::Add, Self::Change, Self::Remove];
157}
158
159#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
160#[serde(rename_all = "snake_case")]
161pub enum LoweringMechanism {
162    Define,
163    Undefine,
164    Redefine,
165    AtomicUndefineDefine,
166    Unsupported,
167}
168
169#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
170pub struct TransitionRule {
171    pub mechanism: LoweringMechanism,
172    pub safety: SafetyClass,
173    pub required_capabilities: CapabilitySet,
174    pub keyed_meta: bool,
175}
176
177#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
178pub struct FactTransitionRule {
179    pub fact: FactKind,
180    pub transition: FactTransition,
181    pub rule: TransitionRule,
182}
183
184#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
185pub struct AnnotationTransitionRule {
186    pub subject: AnnotationSubjectKind,
187    pub annotation: AnnotationKind,
188    pub transition: AnnotationTransition,
189    pub rule: TransitionRule,
190}
191
192#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
193#[serde(rename_all = "snake_case")]
194pub enum SafetyScenario {
195    ExplicitDefaultEquivalent,
196    DocMetaTransition,
197    AddOptionalInterface,
198    AddRequiredCardinality,
199    AddKeyOrUnique,
200    WidenCardinality,
201    NarrowCardinality,
202    RemoveCardinalityToEqualDefault,
203    RemoveCardinalityToNarrowerDefault,
204    RemoveCardinalityToWiderDefault,
205    AddOrTightenValueConstraint,
206    RemoveValueConstraint,
207    AddAbstract,
208    RemoveAbstract,
209    AddIndependent,
210    RemoveIndependent,
211    ChangeSub,
212    ChangeRelatesSpecialization,
213    ChangeValueType,
214    RemoveFact,
215    RedefineFunction,
216    UnsupportedProviderTransition,
217}
218
219impl SafetyScenario {
220    pub const ALL: [Self; 22] = [
221        Self::ExplicitDefaultEquivalent,
222        Self::DocMetaTransition,
223        Self::AddOptionalInterface,
224        Self::AddRequiredCardinality,
225        Self::AddKeyOrUnique,
226        Self::WidenCardinality,
227        Self::NarrowCardinality,
228        Self::RemoveCardinalityToEqualDefault,
229        Self::RemoveCardinalityToNarrowerDefault,
230        Self::RemoveCardinalityToWiderDefault,
231        Self::AddOrTightenValueConstraint,
232        Self::RemoveValueConstraint,
233        Self::AddAbstract,
234        Self::RemoveAbstract,
235        Self::AddIndependent,
236        Self::RemoveIndependent,
237        Self::ChangeSub,
238        Self::ChangeRelatesSpecialization,
239        Self::ChangeValueType,
240        Self::RemoveFact,
241        Self::RedefineFunction,
242        Self::UnsupportedProviderTransition,
243    ];
244}
245
246#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
247#[serde(rename_all = "snake_case")]
248pub enum EvidenceRequirement {
249    None,
250    ExistingDataSatisfiesTarget,
251    Backfill,
252    ExplicitConversion,
253    OperatorApproval,
254    ProviderSupport,
255}
256
257#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
258pub struct SafetyScenarioRule {
259    pub scenario: SafetyScenario,
260    pub safety: SafetyClass,
261    pub evidence: EvidenceRequirement,
262}
263
264#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
265#[serde(rename_all = "snake_case")]
266pub enum InterfaceKind {
267    Owns,
268    Relates,
269    Plays,
270}
271
272#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
273pub struct InterfaceDefault {
274    pub interface: InterfaceKind,
275    pub min: u64,
276    pub max: Option<u64>,
277}
278
279const TYPEDB_3_12_1_INTERFACE_DEFAULTS: [InterfaceDefault; 3] = [
280    InterfaceDefault {
281        interface: InterfaceKind::Owns,
282        min: 0,
283        max: Some(1),
284    },
285    InterfaceDefault {
286        interface: InterfaceKind::Relates,
287        min: 0,
288        max: Some(1),
289    },
290    InterfaceDefault {
291        interface: InterfaceKind::Plays,
292        min: 0,
293        max: None,
294    },
295];
296
297#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
298#[serde(rename_all = "snake_case")]
299pub enum EvidenceFlag {
300    RedefineQueriesAreSingleton,
301    RejectedRedefinePreservesSchema,
302    SchemaTransactionsAreAtomic,
303    RelatesSpecializationPreservesValidData,
304    SubAnnotationDirectRedefineRejected,
305    SubAnnotationAtomicReplacementSupported,
306    MetaRemovalIsKeyed,
307    FunctionRedefineLeavesStoredMetadataStale,
308    FunctionAtomicReplacementUpdatesMetadata,
309    StructTransitionsUnsupported,
310    IndependentRemovalDeletesOwnerlessAttributes,
311    GuardedChangesRejectInvalidData,
312    OwnsCardRemovalRestoresZeroToOneDefault,
313    RelatesCardRemovalRestoresZeroToOneDefault,
314    PlaysCardRemovalRestoresZeroToUnboundedDefault,
315}
316
317impl EvidenceFlag {
318    pub const ALL: [Self; 15] = [
319        Self::RedefineQueriesAreSingleton,
320        Self::RejectedRedefinePreservesSchema,
321        Self::SchemaTransactionsAreAtomic,
322        Self::RelatesSpecializationPreservesValidData,
323        Self::SubAnnotationDirectRedefineRejected,
324        Self::SubAnnotationAtomicReplacementSupported,
325        Self::MetaRemovalIsKeyed,
326        Self::FunctionRedefineLeavesStoredMetadataStale,
327        Self::FunctionAtomicReplacementUpdatesMetadata,
328        Self::StructTransitionsUnsupported,
329        Self::IndependentRemovalDeletesOwnerlessAttributes,
330        Self::GuardedChangesRejectInvalidData,
331        Self::OwnsCardRemovalRestoresZeroToOneDefault,
332        Self::RelatesCardRemovalRestoresZeroToOneDefault,
333        Self::PlaysCardRemovalRestoresZeroToUnboundedDefault,
334    ];
335}
336
337#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
338pub struct SchemaLoweringProfile {
339    pub id: SchemaLoweringProfileId,
340    pub fingerprint_domain: FingerprintDomain,
341    pub canonicalization: CanonicalizationVersion,
342    pub semantic_profile: SemanticProfileId,
343    pub provider: String,
344    pub provider_version: String,
345    pub transactional_schema_queries: bool,
346    pub required_capabilities: CapabilitySet,
347    pub interface_defaults: Vec<InterfaceDefault>,
348    pub fact_rules: Vec<FactTransitionRule>,
349    pub annotation_rules: Vec<AnnotationTransitionRule>,
350    pub safety_rules: Vec<SafetyScenarioRule>,
351    pub evidence: Vec<EvidenceFlag>,
352}
353
354impl SchemaLoweringProfile {
355    pub fn fact_rule(&self, fact: FactKind, transition: FactTransition) -> Option<&TransitionRule> {
356        self.fact_rules
357            .iter()
358            .find(|row| row.fact == fact && row.transition == transition)
359            .map(|row| &row.rule)
360    }
361
362    pub fn annotation_rule(
363        &self,
364        subject: AnnotationSubjectKind,
365        annotation: AnnotationKind,
366        transition: AnnotationTransition,
367    ) -> Option<&TransitionRule> {
368        self.annotation_rules
369            .iter()
370            .find(|row| {
371                row.subject == subject
372                    && row.annotation == annotation
373                    && row.transition == transition
374            })
375            .map(|row| &row.rule)
376    }
377
378    pub fn safety_rule(&self, scenario: SafetyScenario) -> Option<&SafetyScenarioRule> {
379        self.safety_rules
380            .iter()
381            .find(|row| row.scenario == scenario)
382    }
383}
384
385#[derive(Debug)]
386pub(crate) struct OperationTransitionClassification {
387    pub(crate) safety: SafetyClass,
388    pub(crate) atomic: bool,
389    pub(crate) required_capabilities: CapabilitySet,
390}
391
392pub(crate) fn classify_operation_transition(
393    operation: &SchemaOperation,
394) -> Result<OperationTransitionClassification, SafetyClassificationError> {
395    let mut rules = Vec::new();
396    match operation.kind() {
397        SchemaOperationKind::Define => {
398            let facts = operation.defined_facts().expect("define exposes facts");
399            let functions = facts
400                .iter()
401                .filter_map(|fact| match fact {
402                    SchemaFact::Function(function) => Some(function.id().clone()),
403                    _ => None,
404                })
405                .collect::<BTreeSet<_>>();
406            for fact in facts {
407                rules.push(classify_defined_fact(fact, &functions));
408            }
409        }
410        SchemaOperationKind::Redefine => rules.push(classify_redefinition(
411            operation
412                .expected_fact()
413                .expect("redefine exposes expected fact"),
414            operation
415                .replacement_fact()
416                .expect("redefine exposes replacement fact"),
417        )?),
418        SchemaOperationKind::Undefine => rules.push(classify_undefined_fact(
419            operation.undefined_fact().expect("undefine exposes fact"),
420        )),
421    }
422
423    let mut safety = SafetyClass::FormalOnly;
424    let mut atomic = false;
425    let mut required_capabilities = CapabilitySet::new();
426    for rule in rules {
427        if safety_rank(rule.safety) > safety_rank(safety) {
428            safety = rule.safety;
429        }
430        atomic |= rule.mechanism == LoweringMechanism::AtomicUndefineDefine;
431        for capability in rule.required_capabilities.iter().cloned() {
432            required_capabilities.insert(capability);
433        }
434    }
435    Ok(OperationTransitionClassification {
436        safety,
437        atomic,
438        required_capabilities,
439    })
440}
441
442fn classify_defined_fact(fact: &SchemaFact, functions: &BTreeSet<FunctionId>) -> TransitionRule {
443    match fact {
444        SchemaFact::Relates(relates) if relates.specializes().is_some() => {
445            fact_transition_rule(FactKind::RelatesSpecialization, FactTransition::Define)
446        }
447        SchemaFact::Annotation(annotation) => {
448            if let AnnotationSubjectId::Function(function) = annotation.id().subject()
449                && functions.contains(function)
450                && matches!(
451                    annotation.id().kind(),
452                    AnnotationKindId::Doc | AnnotationKindId::Meta(_)
453                )
454            {
455                return define(SafetyClass::SchemaMetadata, false);
456            }
457            classify_annotation(annotation, AnnotationTransition::Add, None)
458        }
459        _ => fact_transition_rule(fact_kind(fact), FactTransition::Define),
460    }
461}
462
463fn classify_undefined_fact(fact: &SchemaFact) -> TransitionRule {
464    match fact {
465        SchemaFact::Annotation(annotation) => {
466            classify_annotation(annotation, AnnotationTransition::Remove, None)
467        }
468        _ => fact_transition_rule(fact_kind(fact), FactTransition::Undefine),
469    }
470}
471
472fn classify_redefinition(
473    expected: &SchemaFact,
474    replacement: &SchemaFact,
475) -> Result<TransitionRule, SafetyClassificationError> {
476    match (expected, replacement) {
477        (SchemaFact::Relates(old), SchemaFact::Relates(new)) => {
478            let transition = match (old.specializes(), new.specializes()) {
479                (None, Some(_)) => FactTransition::Define,
480                (Some(_), None) => FactTransition::Undefine,
481                (Some(_), Some(_)) => FactTransition::Redefine,
482                (None, None) => {
483                    return Err(SafetyClassificationError::UnchangedRelatesSpecialization);
484                }
485            };
486            Ok(fact_transition_rule(
487                FactKind::RelatesSpecialization,
488                transition,
489            ))
490        }
491        (SchemaFact::Annotation(old), SchemaFact::Annotation(new)) => Ok(classify_annotation(
492            new,
493            AnnotationTransition::Change,
494            Some(old),
495        )),
496        (left, right) if std::mem::discriminant(left) == std::mem::discriminant(right) => Ok(
497            fact_transition_rule(fact_kind(right), FactTransition::Redefine),
498        ),
499        _ => Err(SafetyClassificationError::RedefinitionCategoryChanged),
500    }
501}
502
503fn fact_kind(fact: &SchemaFact) -> FactKind {
504    match fact {
505        SchemaFact::Type(_) => FactKind::Type,
506        SchemaFact::Sub(_) => FactKind::Sub,
507        SchemaFact::Value(_) => FactKind::Value,
508        SchemaFact::Owns(_) => FactKind::Owns,
509        SchemaFact::Relates(_) => FactKind::Relates,
510        SchemaFact::Plays(_) => FactKind::Plays,
511        SchemaFact::Annotation(_) => unreachable!("annotations use the annotation registry"),
512        SchemaFact::Function(_) => FactKind::Function,
513        SchemaFact::Struct(_) => FactKind::Struct,
514    }
515}
516
517fn classify_annotation(
518    annotation: &AnnotationFact,
519    transition: AnnotationTransition,
520    expected: Option<&AnnotationFact>,
521) -> TransitionRule {
522    let subject = annotation_subject_kind(annotation.id().subject());
523    let kind = annotation_kind(annotation.id().kind());
524    let mut rule = annotation_transition_rule(subject, kind, transition);
525    if kind == AnnotationKind::Card
526        && let Some(target) = annotation_cardinality(annotation)
527        && let Some(default) = default_cardinality(annotation.id().subject())
528    {
529        let (from, to) = match transition {
530            AnnotationTransition::Add => (default, target),
531            AnnotationTransition::Change => {
532                let Some(source) = expected.and_then(annotation_cardinality) else {
533                    return rule;
534                };
535                (source, target)
536            }
537            AnnotationTransition::Remove => (target, default),
538        };
539        rule.safety = cardinality_transition_safety(from, to);
540    }
541    rule
542}
543
544fn annotation_subject_kind(subject: &AnnotationSubjectId) -> AnnotationSubjectKind {
545    match subject {
546        AnnotationSubjectId::Type(_) => AnnotationSubjectKind::Type,
547        AnnotationSubjectId::Sub(_) => AnnotationSubjectKind::Sub,
548        AnnotationSubjectId::Value(_) => AnnotationSubjectKind::Value,
549        AnnotationSubjectId::Owns(_) => AnnotationSubjectKind::Owns,
550        AnnotationSubjectId::Relates(_) => AnnotationSubjectKind::Relates,
551        AnnotationSubjectId::Plays(_) => AnnotationSubjectKind::Plays,
552        AnnotationSubjectId::Function(_) => AnnotationSubjectKind::Function,
553    }
554}
555
556fn annotation_kind(kind: &AnnotationKindId) -> AnnotationKind {
557    match kind {
558        AnnotationKindId::Abstract => AnnotationKind::Abstract,
559        AnnotationKindId::Independent => AnnotationKind::Independent,
560        AnnotationKindId::Key => AnnotationKind::Key,
561        AnnotationKindId::Unique => AnnotationKind::Unique,
562        AnnotationKindId::Card => AnnotationKind::Card,
563        AnnotationKindId::Regex => AnnotationKind::Regex,
564        AnnotationKindId::Range => AnnotationKind::Range,
565        AnnotationKindId::Values => AnnotationKind::Values,
566        AnnotationKindId::Doc => AnnotationKind::Doc,
567        AnnotationKindId::Meta(_) => AnnotationKind::Meta,
568    }
569}
570
571fn annotation_cardinality(annotation: &AnnotationFact) -> Option<(u64, Option<u64>)> {
572    match annotation.value() {
573        SchemaAnnotationValue::Cardinality(cardinality) => {
574            Some(((*cardinality).min(), (*cardinality).max()))
575        }
576        _ => None,
577    }
578}
579
580fn default_cardinality(subject: &AnnotationSubjectId) -> Option<(u64, Option<u64>)> {
581    let interface = match subject {
582        AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
583        AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
584        AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
585        _ => return None,
586    };
587    TYPEDB_3_12_1_INTERFACE_DEFAULTS
588        .iter()
589        .find(|default| default.interface == interface)
590        .map(|default| (default.min, default.max))
591}
592
593fn cardinality_transition_safety(from: (u64, Option<u64>), to: (u64, Option<u64>)) -> SafetyClass {
594    if from == to {
595        SafetyClass::FormalOnly
596    } else if interval_contains(to, from) {
597        SafetyClass::Additive
598    } else if interval_contains(from, to) {
599        SafetyClass::BackfillRequired
600    } else {
601        SafetyClass::Conditional
602    }
603}
604
605fn interval_contains(outer: (u64, Option<u64>), inner: (u64, Option<u64>)) -> bool {
606    outer.0 <= inner.0
607        && match (outer.1, inner.1) {
608            (None, _) => true,
609            (Some(_), None) => false,
610            (Some(outer), Some(inner)) => outer >= inner,
611        }
612}
613
614fn safety_rank(safety: SafetyClass) -> u8 {
615    match safety {
616        SafetyClass::FormalOnly => 0,
617        SafetyClass::SchemaMetadata => 1,
618        SafetyClass::Additive => 2,
619        SafetyClass::Conditional => 3,
620        SafetyClass::BackfillRequired => 4,
621        SafetyClass::Destructive => 5,
622        SafetyClass::Opaque => 6,
623        SafetyClass::Unsupported => 7,
624    }
625}
626
627fn capabilities(ids: &[&str]) -> CapabilitySet {
628    let mut capabilities = CapabilitySet::new();
629    for id in ids {
630        capabilities
631            .insert(CapabilityId::new(*id).expect("fixed schema-lowering capability id is valid"));
632    }
633    capabilities
634}
635
636fn transition_rule(
637    mechanism: LoweringMechanism,
638    safety: SafetyClass,
639    required: &[&str],
640    keyed_meta: bool,
641) -> TransitionRule {
642    TransitionRule {
643        mechanism,
644        safety,
645        required_capabilities: capabilities(required),
646        keyed_meta,
647    }
648}
649
650fn unsupported(keyed_meta: bool) -> TransitionRule {
651    transition_rule(
652        LoweringMechanism::Unsupported,
653        SafetyClass::Unsupported,
654        &[],
655        keyed_meta,
656    )
657}
658
659fn define(safety: SafetyClass, keyed_meta: bool) -> TransitionRule {
660    transition_rule(LoweringMechanism::Define, safety, &[CAP_DEFINE], keyed_meta)
661}
662
663fn undefine(safety: SafetyClass, keyed_meta: bool) -> TransitionRule {
664    transition_rule(
665        LoweringMechanism::Undefine,
666        safety,
667        &[CAP_UNDEFINE],
668        keyed_meta,
669    )
670}
671
672fn redefine(safety: SafetyClass, capability: &str, keyed_meta: bool) -> TransitionRule {
673    transition_rule(
674        LoweringMechanism::Redefine,
675        safety,
676        &[capability],
677        keyed_meta,
678    )
679}
680
681pub fn fact_transition_rule(fact: FactKind, transition: FactTransition) -> TransitionRule {
682    use FactKind as F;
683    use FactTransition as T;
684    use SafetyClass as S;
685
686    match (fact, transition) {
687        (F::Type, T::Define) => define(S::Additive, false),
688        (F::Type, T::Undefine) => undefine(S::Destructive, false),
689        (F::Type, T::Redefine) => unsupported(false),
690        (F::Sub, T::Define) => define(S::Conditional, false),
691        (F::Sub, T::Undefine) => undefine(S::Destructive, false),
692        (F::Sub, T::Redefine) => redefine(S::Conditional, CAP_REDEFINE_SUB, false),
693        (F::Value, T::Define) => define(S::Additive, false),
694        (F::Value, T::Undefine) => undefine(S::Destructive, false),
695        (F::Value, T::Redefine) => redefine(S::Destructive, CAP_REDEFINE_VALUE, false),
696        (F::Owns | F::Relates | F::Plays, T::Define) => define(S::Additive, false),
697        (F::Owns | F::Relates | F::Plays, T::Undefine) => undefine(S::Destructive, false),
698        (F::Owns | F::Relates | F::Plays, T::Redefine) => unsupported(false),
699        (F::RelatesSpecialization, T::Define) => define(S::Conditional, false),
700        (F::RelatesSpecialization, T::Undefine) => undefine(S::Conditional, false),
701        (F::RelatesSpecialization, T::Redefine) => {
702            redefine(S::Conditional, CAP_REDEFINE_RELATES_SPECIALIZATION, false)
703        }
704        (F::Function, T::Define) => define(S::Additive, false),
705        (F::Function, T::Undefine) => undefine(S::Destructive, false),
706        (F::Function, T::Redefine) => redefine(S::Opaque, CAP_REDEFINE_FUNCTION, false),
707        (F::Struct, _) => unsupported(false),
708    }
709}
710
711fn annotation_is_supported(subject: AnnotationSubjectKind, annotation: AnnotationKind) -> bool {
712    use AnnotationKind as A;
713    use AnnotationSubjectKind as S;
714
715    match subject {
716        S::Type => matches!(annotation, A::Abstract | A::Independent | A::Doc | A::Meta),
717        S::Sub => matches!(annotation, A::Doc | A::Meta),
718        S::Value => matches!(
719            annotation,
720            A::Regex | A::Range | A::Values | A::Doc | A::Meta
721        ),
722        S::Owns => matches!(
723            annotation,
724            A::Key | A::Unique | A::Card | A::Regex | A::Range | A::Values | A::Doc | A::Meta
725        ),
726        S::Relates => matches!(annotation, A::Abstract | A::Card | A::Doc | A::Meta),
727        S::Plays => matches!(annotation, A::Card | A::Doc | A::Meta),
728        S::Function | S::Struct => false,
729    }
730}
731
732pub fn annotation_transition_rule(
733    subject: AnnotationSubjectKind,
734    annotation: AnnotationKind,
735    transition: AnnotationTransition,
736) -> TransitionRule {
737    use AnnotationKind as A;
738    use AnnotationSubjectKind as S;
739    use AnnotationTransition as T;
740    use SafetyClass as C;
741
742    let keyed_meta = annotation == A::Meta;
743    if !annotation_is_supported(subject, annotation) {
744        return unsupported(keyed_meta);
745    }
746    if subject == S::Sub {
747        return match transition {
748            T::Add => define(C::SchemaMetadata, keyed_meta),
749            T::Change => transition_rule(
750                LoweringMechanism::AtomicUndefineDefine,
751                C::SchemaMetadata,
752                &[
753                    CAP_TRANSACTION_ATOMIC,
754                    CAP_UNDEFINE,
755                    CAP_DEFINE,
756                    CAP_REPLACE_SUB_ANNOTATION,
757                ],
758                keyed_meta,
759            ),
760            T::Remove => undefine(C::SchemaMetadata, keyed_meta),
761        };
762    }
763
764    match annotation {
765        A::Doc | A::Meta => match transition {
766            T::Add => define(C::SchemaMetadata, keyed_meta),
767            T::Change => redefine(C::SchemaMetadata, CAP_REDEFINE_ANNOTATION, keyed_meta),
768            T::Remove => undefine(C::SchemaMetadata, keyed_meta),
769        },
770        A::Abstract | A::Independent | A::Key | A::Unique => match transition {
771            T::Change => unsupported(keyed_meta),
772            T::Add => {
773                let safety = match annotation {
774                    A::Abstract => C::Conditional,
775                    A::Independent => C::Additive,
776                    A::Key | A::Unique => C::BackfillRequired,
777                    _ => unreachable!(),
778                };
779                define(safety, keyed_meta)
780            }
781            T::Remove => {
782                let safety = match annotation {
783                    A::Independent => C::Destructive,
784                    A::Abstract | A::Key | A::Unique => C::Additive,
785                    _ => unreachable!(),
786                };
787                undefine(safety, keyed_meta)
788            }
789        },
790        A::Card => match transition {
791            T::Add => define(C::Conditional, keyed_meta),
792            T::Change => redefine(C::Conditional, CAP_REDEFINE_ANNOTATION, keyed_meta),
793            T::Remove => undefine(C::Conditional, keyed_meta),
794        },
795        A::Regex | A::Range | A::Values => match transition {
796            T::Add => define(C::Conditional, keyed_meta),
797            T::Change => redefine(C::Conditional, CAP_REDEFINE_ANNOTATION, keyed_meta),
798            T::Remove => undefine(C::Additive, keyed_meta),
799        },
800    }
801}
802
803fn safety_rule(scenario: SafetyScenario) -> SafetyScenarioRule {
804    use EvidenceRequirement as E;
805    use SafetyClass as C;
806    use SafetyScenario as S;
807
808    let (safety, evidence) = match scenario {
809        S::ExplicitDefaultEquivalent | S::RemoveCardinalityToEqualDefault => {
810            (C::FormalOnly, E::None)
811        }
812        S::DocMetaTransition => (C::SchemaMetadata, E::None),
813        S::AddOptionalInterface
814        | S::WidenCardinality
815        | S::RemoveCardinalityToWiderDefault
816        | S::RemoveValueConstraint
817        | S::RemoveAbstract
818        | S::AddIndependent => (C::Additive, E::None),
819        S::AddRequiredCardinality
820        | S::AddKeyOrUnique
821        | S::NarrowCardinality
822        | S::RemoveCardinalityToNarrowerDefault => (C::BackfillRequired, E::Backfill),
823        S::AddOrTightenValueConstraint
824        | S::AddAbstract
825        | S::ChangeSub
826        | S::ChangeRelatesSpecialization => (C::Conditional, E::ExistingDataSatisfiesTarget),
827        S::RemoveIndependent | S::RemoveFact => (C::Destructive, E::OperatorApproval),
828        S::ChangeValueType => (C::Destructive, E::ExplicitConversion),
829        S::RedefineFunction => (C::Opaque, E::OperatorApproval),
830        S::UnsupportedProviderTransition => (C::Unsupported, E::ProviderSupport),
831    };
832    SafetyScenarioRule {
833        scenario,
834        safety,
835        evidence,
836    }
837}
838
839fn build_profile() -> SchemaLoweringProfile {
840    let mut fact_rules = Vec::with_capacity(FactKind::ALL.len() * FactTransition::ALL.len());
841    for fact in FactKind::ALL {
842        for transition in FactTransition::ALL {
843            fact_rules.push(FactTransitionRule {
844                fact,
845                transition,
846                rule: fact_transition_rule(fact, transition),
847            });
848        }
849    }
850    let mut annotation_rules = Vec::with_capacity(
851        AnnotationSubjectKind::ALL.len()
852            * AnnotationKind::ALL.len()
853            * AnnotationTransition::ALL.len(),
854    );
855    for subject in AnnotationSubjectKind::ALL {
856        for annotation in AnnotationKind::ALL {
857            for transition in AnnotationTransition::ALL {
858                annotation_rules.push(AnnotationTransitionRule {
859                    subject,
860                    annotation,
861                    transition,
862                    rule: annotation_transition_rule(subject, annotation, transition),
863                });
864            }
865        }
866    }
867
868    SchemaLoweringProfile {
869        id: SchemaLoweringProfileId::typedb_3_12_1(),
870        fingerprint_domain: FingerprintDomain::new(SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN)
871            .expect("fixed fingerprint domain is valid"),
872        canonicalization: CanonicalizationVersion::new(SCHEMA_LOWERING_PROFILE_CANONICALIZATION)
873            .expect("fixed canonicalization is valid"),
874        semantic_profile: SemanticProfileId::new(SEMANTIC_PROFILE)
875            .expect("fixed semantic profile is valid"),
876        provider: PROVIDER.to_owned(),
877        provider_version: PROVIDER_VERSION.to_owned(),
878        transactional_schema_queries: true,
879        required_capabilities: capabilities(&REQUIRED_CAPABILITY_IDS),
880        interface_defaults: TYPEDB_3_12_1_INTERFACE_DEFAULTS.to_vec(),
881        fact_rules,
882        annotation_rules,
883        safety_rules: SafetyScenario::ALL.into_iter().map(safety_rule).collect(),
884        evidence: EvidenceFlag::ALL.to_vec(),
885    }
886}
887
888pub fn typedb_3_12_1_profile() -> &'static SchemaLoweringProfile {
889    static PROFILE: OnceLock<SchemaLoweringProfile> = OnceLock::new();
890    PROFILE.get_or_init(build_profile)
891}
892
893pub fn canonical_profile_bytes() -> Vec<u8> {
894    to_canonical_json(typedb_3_12_1_profile())
895        .expect("the trusted schema-lowering profile has canonical bytes")
896}
897
898pub fn profile_fingerprint() -> SchemaLoweringProfileFingerprint {
899    SchemaLoweringProfileFingerprint::compute(&canonical_profile_bytes())
900}
901
902/// Resolve the executable registry's exact canonical profile binding.
903pub fn schema_lowering_profile_binding() -> Result<SchemaLoweringProfileBinding, Diagnostic> {
904    SchemaLoweringProfileBinding::from_canonical_profile_bytes(&canonical_profile_bytes())
905}