1use 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
45pub 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")]
66pub enum FactKind {
68 Type,
70 Sub,
72 Value,
74 Owns,
76 Relates,
78 RelatesSpecialization,
80 Plays,
82 Function,
84 Struct,
86}
87
88impl FactKind {
89 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")]
105pub enum FactTransition {
107 Define,
109 Undefine,
111 Redefine,
113}
114
115impl FactTransition {
116 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")]
122pub enum AnnotationSubjectKind {
124 Type,
126 Sub,
128 Value,
130 Owns,
132 Relates,
134 Plays,
136 Function,
138 Struct,
140}
141
142impl AnnotationSubjectKind {
143 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")]
158pub enum AnnotationKind {
160 Abstract,
162 Independent,
164 Key,
166 Unique,
168 Card,
170 Regex,
172 Range,
174 Values,
176 Doc,
178 Meta,
180}
181
182impl AnnotationKind {
183 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")]
200pub enum AnnotationTransition {
202 Add,
204 Change,
206 Remove,
208}
209
210impl AnnotationTransition {
211 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")]
217pub enum LoweringMechanism {
219 Define,
221 Undefine,
223 Redefine,
225 AtomicUndefineDefine,
227 Unsupported,
229}
230
231#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
232pub struct TransitionRule {
234 pub mechanism: LoweringMechanism,
236 pub safety: SafetyClass,
238 pub required_capabilities: CapabilitySet,
240 pub keyed_meta: bool,
242}
243
244#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
245pub struct FactTransitionRule {
247 pub fact: FactKind,
249 pub transition: FactTransition,
251 pub rule: TransitionRule,
253}
254
255#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
256pub struct AnnotationTransitionRule {
258 pub subject: AnnotationSubjectKind,
260 pub annotation: AnnotationKind,
262 pub transition: AnnotationTransition,
264 pub rule: TransitionRule,
266}
267
268#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
269#[serde(rename_all = "snake_case")]
270pub enum SafetyScenario {
272 ExplicitDefaultEquivalent,
274 DocMetaTransition,
276 AddOptionalInterface,
278 AddRequiredCardinality,
280 AddKeyOrUnique,
282 WidenCardinality,
284 NarrowCardinality,
286 RemoveCardinalityToEqualDefault,
288 RemoveCardinalityToNarrowerDefault,
290 RemoveCardinalityToWiderDefault,
292 AddOrTightenValueConstraint,
294 RemoveValueConstraint,
296 AddAbstract,
298 RemoveAbstract,
300 AddIndependent,
302 RemoveIndependent,
304 ChangeSub,
306 ChangeRelatesSpecialization,
308 ChangeValueType,
310 RemoveFact,
312 RedefineFunction,
314 UnsupportedProviderTransition,
316}
317
318impl SafetyScenario {
319 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")]
348pub enum EvidenceRequirement {
350 None,
352 ExistingDataSatisfiesTarget,
354 Backfill,
356 ExplicitConversion,
358 OperatorApproval,
360 ProviderSupport,
362}
363
364#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
365pub struct SafetyScenarioRule {
367 pub scenario: SafetyScenario,
369 pub safety: SafetyClass,
371 pub evidence: EvidenceRequirement,
373}
374
375#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
376#[serde(rename_all = "snake_case")]
377pub enum InterfaceKind {
379 Owns,
381 Relates,
383 Plays,
385}
386
387#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
388pub struct InterfaceDefault {
390 pub interface: InterfaceKind,
392 pub min: u64,
394 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")]
418pub enum EvidenceFlag {
420 RedefineQueriesAreSingleton,
422 RejectedRedefinePreservesSchema,
424 SchemaTransactionsAreAtomic,
426 RelatesSpecializationPreservesValidData,
428 SubAnnotationDirectRedefineRejected,
430 SubAnnotationAtomicReplacementSupported,
432 MetaRemovalIsKeyed,
434 FunctionRedefineLeavesStoredMetadataStale,
436 FunctionAtomicReplacementUpdatesMetadata,
438 StructTransitionsUnsupported,
440 IndependentRemovalDeletesOwnerlessAttributes,
442 GuardedChangesRejectInvalidData,
444 OwnsCardRemovalRestoresZeroToOneDefault,
446 RelatesCardRemovalRestoresZeroToOneDefault,
448 PlaysCardRemovalRestoresZeroToUnboundedDefault,
450}
451
452impl EvidenceFlag {
453 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)]
474pub struct SchemaLoweringProfile {
476 pub id: SchemaLoweringProfileId,
478 pub fingerprint_domain: FingerprintDomain,
480 pub canonicalization: CanonicalizationVersion,
482 pub semantic_profile: SemanticProfileId,
484 pub provider: String,
486 pub provider_version: String,
488 pub transactional_schema_queries: bool,
490 pub required_capabilities: CapabilitySet,
492 pub interface_defaults: Vec<InterfaceDefault>,
494 pub fact_rules: Vec<FactTransitionRule>,
496 pub annotation_rules: Vec<AnnotationTransitionRule>,
498 pub safety_rules: Vec<SafetyScenarioRule>,
500 pub evidence: Vec<EvidenceFlag>,
502}
503
504impl SchemaLoweringProfile {
505 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 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 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 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
843pub 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
895pub 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
1052pub fn typedb_3_12_1_profile() -> &'static SchemaLoweringProfile {
1054 static PROFILE: OnceLock<SchemaLoweringProfile> = OnceLock::new();
1055 PROFILE.get_or_init(build_profile)
1056}
1057
1058pub 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
1064pub fn profile_fingerprint() -> SchemaLoweringProfileFingerprint {
1066 SchemaLoweringProfileFingerprint::compute(&canonical_profile_bytes())
1067}
1068
1069pub fn schema_lowering_profile_binding() -> Result<SchemaLoweringProfileBinding, Diagnostic> {
1071 SchemaLoweringProfileBinding::from_canonical_profile_bytes(&canonical_profile_bytes())
1072}