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::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
902pub fn schema_lowering_profile_binding() -> Result<SchemaLoweringProfileBinding, Diagnostic> {
904 SchemaLoweringProfileBinding::from_canonical_profile_bytes(&canonical_profile_bytes())
905}