1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4use type_bridge_contract::capability::CapabilitySet;
5use type_bridge_contract::codec::{FormatVersion, to_canonical_json};
6use type_bridge_contract::diagnostic::DiagnosticCategory;
7use type_bridge_contract::fingerprint::SemanticProfileId;
8use type_bridge_contract::managed_scope::{
9 ManagedScopeBinding, ManagedScopeId, SemanticProfileFingerprint,
10};
11use type_bridge_contract::schema::{
12 AnnotationFactId, AnnotationKindId, AnnotationSubjectId, CollectionMode, DeclaredSchema,
13 InterfaceKind, ManagedDeclaredIdentityFingerprint, ManagedSemanticSchemaFingerprint,
14 SchemaAnnotationValue, SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId,
15 SemanticProfile, SemanticSchemaFingerprint,
16};
17use type_bridge_contract::value::Cardinality;
18
19use crate::TYPEDB_3_12_1_TIMEZONE_POLICY_ID;
20
21#[derive(Clone, Debug, Default, Eq, PartialEq)]
26pub struct ManagedSchemaScope {
27 facts: BTreeSet<SchemaFactId>,
28}
29
30impl ManagedSchemaScope {
31 #[must_use]
33 pub fn new(facts: impl IntoIterator<Item = SchemaFactId>) -> Self {
34 Self {
35 facts: facts.into_iter().collect(),
36 }
37 }
38
39 pub fn bind_exclusive(
41 scope_id: ManagedScopeId,
42 declared: &DeclaredSchema,
43 ) -> Result<BoundManagedSchemaScope, SchemaDiagnostics> {
44 let binding = ManagedScopeBinding::exclusive(scope_id).map_err(|diagnostic| {
45 SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
46 })?;
47 Ok(Self::new(declared.facts().map(SchemaFact::id)).bind(binding))
48 }
49
50 fn bind(self, binding: ManagedScopeBinding) -> BoundManagedSchemaScope {
51 BoundManagedSchemaScope {
52 selection: self,
53 binding,
54 }
55 }
56
57 #[must_use]
59 pub fn contains(&self, fact: &SchemaFactId) -> bool {
60 self.facts.contains(fact)
61 }
62
63 pub fn iter(&self) -> impl ExactSizeIterator<Item = &SchemaFactId> {
65 self.facts.iter()
66 }
67
68 #[must_use]
70 pub fn len(&self) -> usize {
71 self.facts.len()
72 }
73
74 #[must_use]
76 pub fn is_empty(&self) -> bool {
77 self.facts.is_empty()
78 }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct BoundManagedSchemaScope {
84 selection: ManagedSchemaScope,
85 binding: ManagedScopeBinding,
86}
87
88impl BoundManagedSchemaScope {
89 pub const fn selection(&self) -> &ManagedSchemaScope {
91 &self.selection
92 }
93
94 pub const fn binding(&self) -> &ManagedScopeBinding {
96 &self.binding
97 }
98}
99
100#[derive(Serialize)]
101struct SemanticSchemaView<'a> {
102 format_version: FormatVersion,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 managed_scope: Option<&'a ManagedScopeBinding>,
105 semantic_profile: &'a SemanticProfileId,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 semantic_profile_fingerprint: Option<&'a SemanticProfileFingerprint>,
108 timezone_policy: &'static str,
109 required_capabilities: &'a CapabilitySet,
110 facts: Vec<SemanticFactView<'a>>,
111}
112
113#[derive(Serialize)]
114struct SemanticFactView<'a> {
115 id: SchemaFactId,
116 value: SemanticFactValue<'a>,
117}
118
119#[derive(Serialize)]
120#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
121enum SemanticFactValue<'a> {
122 Direct(&'a SchemaFact),
123 TimeZoneRange {
124 subject: AnnotationSubjectId,
125 lower_utc_nanoseconds: Option<String>,
126 upper_utc_nanoseconds: Option<String>,
127 },
128 TimeZoneValues {
129 subject: AnnotationSubjectId,
130 utc_nanoseconds: Vec<String>,
131 },
132 MaterializedCardinality {
133 subject: AnnotationSubjectId,
134 cardinality: Cardinality,
135 },
136}
137
138#[derive(Serialize)]
139struct ManagedDeclaredView<'a> {
140 format_version: FormatVersion,
141 managed_scope: &'a ManagedScopeBinding,
142 required_capabilities: &'a CapabilitySet,
143 facts: Vec<&'a SchemaFact>,
144}
145
146pub fn canonical_semantic_schema_bytes(
148 declared: &DeclaredSchema,
149 profile: &SemanticProfileId,
150) -> Result<Vec<u8>, SchemaDiagnostics> {
151 canonical_semantic_schema_bytes_for_scope(declared, profile, None)
152}
153
154pub fn semantic_schema_fingerprint(
156 declared: &DeclaredSchema,
157 profile: &SemanticProfileId,
158) -> Result<SemanticSchemaFingerprint, SchemaDiagnostics> {
159 let canonical = canonical_semantic_schema_bytes(declared, profile)?;
160 SemanticSchemaFingerprint::compute(profile.clone(), &canonical)
161 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
162}
163
164pub fn managed_declared_identity_fingerprint(
166 declared: &DeclaredSchema,
167 scope: &BoundManagedSchemaScope,
168) -> Result<ManagedDeclaredIdentityFingerprint, SchemaDiagnostics> {
169 let canonical = canonical_managed_declared_identity_bytes(declared, scope)?;
170 ManagedDeclaredIdentityFingerprint::compute(&canonical)
171 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
172}
173
174pub fn canonical_managed_declared_identity_bytes(
176 declared: &DeclaredSchema,
177 scope: &BoundManagedSchemaScope,
178) -> Result<Vec<u8>, SchemaDiagnostics> {
179 let view = ManagedDeclaredView {
180 format_version: declared.format(),
181 managed_scope: scope.binding(),
182 required_capabilities: declared.required_capabilities(),
183 facts: declared
184 .facts()
185 .filter(|fact| scope.selection().contains(&fact.id()))
186 .collect(),
187 };
188 to_canonical_json(&view)
189 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
190}
191
192pub fn managed_semantic_schema_fingerprint(
194 declared: &DeclaredSchema,
195 profile: &SemanticProfileId,
196 scope: &BoundManagedSchemaScope,
197) -> Result<ManagedSemanticSchemaFingerprint, SchemaDiagnostics> {
198 let canonical = canonical_managed_semantic_schema_bytes(declared, profile, scope)?;
199 ManagedSemanticSchemaFingerprint::compute(profile.clone(), &canonical)
200 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
201}
202
203pub fn canonical_managed_semantic_schema_bytes(
205 declared: &DeclaredSchema,
206 profile: &SemanticProfileId,
207 scope: &BoundManagedSchemaScope,
208) -> Result<Vec<u8>, SchemaDiagnostics> {
209 canonical_semantic_schema_bytes_for_scope(declared, profile, Some(scope))
210}
211
212fn canonical_semantic_schema_bytes_for_scope(
213 declared: &DeclaredSchema,
214 profile: &SemanticProfileId,
215 scope: Option<&BoundManagedSchemaScope>,
216) -> Result<Vec<u8>, SchemaDiagnostics> {
217 let semantic_profile = SemanticProfile::resolve(profile)
218 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))?;
219 validate_collection_profile(declared, profile)?;
220 let semantic_profile_fingerprint = scope
221 .map(|_| semantic_profile.content_fingerprint())
222 .transpose()
223 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))?;
224 let facts = semantic_facts(
225 declared,
226 scope.map(BoundManagedSchemaScope::selection),
227 &semantic_profile,
228 );
229 let view = SemanticSchemaView {
230 format_version: declared.format(),
231 managed_scope: scope.map(BoundManagedSchemaScope::binding),
232 semantic_profile: profile,
233 semantic_profile_fingerprint: semantic_profile_fingerprint.as_ref(),
234 timezone_policy: TYPEDB_3_12_1_TIMEZONE_POLICY_ID,
235 required_capabilities: declared.required_capabilities(),
236 facts,
237 };
238 to_canonical_json(&view)
239 .map_err(|diagnostic| SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None)))
240}
241
242pub(crate) fn validate_collection_profile(
243 declared: &DeclaredSchema,
244 profile: &SemanticProfileId,
245) -> Result<(), SchemaDiagnostics> {
246 if profile.as_str() == "typedb-3.12.1/v1" {
247 return Ok(());
248 }
249 for fact in declared.facts() {
250 let ordered = match fact {
251 SchemaFact::Owns(fact) => fact.collection_mode() == CollectionMode::OrderedList,
252 SchemaFact::Relates(fact) => fact.collection_mode() == CollectionMode::OrderedList,
253 _ => false,
254 };
255 if ordered {
256 return Err(crate::yaml::diagnostic(
257 DiagnosticCategory::UnsupportedCapability,
258 "ordered_collection_profile_unsupported",
259 "ordered collection facts require the typedb-3.12.1/v1 semantic profile",
260 declared.source(&fact.id()).cloned(),
261 ));
262 }
263 }
264 Ok(())
265}
266
267fn semantic_facts<'a>(
268 declared: &'a DeclaredSchema,
269 scope: Option<&ManagedSchemaScope>,
270 profile: &SemanticProfile,
271) -> Vec<SemanticFactView<'a>> {
272 let mut facts = BTreeMap::<SchemaFactId, SemanticFactValue<'a>>::new();
273 let mut interfaces = Vec::<(AnnotationSubjectId, InterfaceKind)>::new();
274 let key_owns = declared
275 .facts()
276 .filter(|fact| scope.is_none_or(|scope| scope.contains(&fact.id())))
277 .filter_map(|fact| match fact {
278 SchemaFact::Annotation(annotation)
279 if annotation.id().kind() == &AnnotationKindId::Key
280 && matches!(annotation.id().subject(), AnnotationSubjectId::Owns(_)) =>
281 {
282 Some(annotation.id().subject().clone())
283 }
284 _ => None,
285 })
286 .collect::<BTreeSet<_>>();
287
288 for fact in declared
289 .facts()
290 .filter(|fact| scope.is_none_or(|scope| scope.contains(&fact.id())))
291 {
292 let id = fact.id();
293 match fact {
294 SchemaFact::Owns(owns) => interfaces.push((
295 AnnotationSubjectId::Owns(owns.id().clone()),
296 InterfaceKind::Owns,
297 )),
298 SchemaFact::Relates(relates) => interfaces.push((
299 AnnotationSubjectId::Relates(relates.id().clone()),
300 InterfaceKind::Relates,
301 )),
302 SchemaFact::Plays(plays) => interfaces.push((
303 AnnotationSubjectId::Plays(plays.id().clone()),
304 InterfaceKind::Plays,
305 )),
306 _ => {}
307 }
308
309 let value = match fact {
310 SchemaFact::Annotation(annotation)
311 if annotation.id().kind() == &AnnotationKindId::Card =>
312 {
313 let Some(kind) = interface_kind(annotation.id().subject()) else {
314 facts.insert(id, SemanticFactValue::Direct(fact));
315 continue;
316 };
317 match annotation.value() {
318 SchemaAnnotationValue::Cardinality(cardinality)
319 if *cardinality
320 == profile.effective_cardinality(
321 kind,
322 None,
323 key_owns.contains(annotation.id().subject()),
324 ) =>
325 {
326 SemanticFactValue::MaterializedCardinality {
327 subject: annotation.id().subject().clone(),
328 cardinality: profile.effective_cardinality(
329 kind,
330 None,
331 key_owns.contains(annotation.id().subject()),
332 ),
333 }
334 }
335 _ => SemanticFactValue::Direct(fact),
336 }
337 }
338 SchemaFact::Annotation(annotation) => {
339 semantic_timezone_value(annotation).unwrap_or(SemanticFactValue::Direct(fact))
340 }
341 _ => SemanticFactValue::Direct(fact),
342 };
343 facts.insert(id, value);
344 }
345
346 for (subject, kind) in interfaces {
347 let id = SchemaFactId::Annotation(AnnotationFactId::new(
348 subject.clone(),
349 AnnotationKindId::Card,
350 ));
351 let cardinality = profile.effective_cardinality(kind, None, key_owns.contains(&subject));
352 facts
353 .entry(id)
354 .or_insert_with(|| SemanticFactValue::MaterializedCardinality {
355 subject,
356 cardinality,
357 });
358 }
359
360 facts
361 .into_iter()
362 .map(|(id, value)| SemanticFactView { id, value })
363 .collect()
364}
365
366fn semantic_timezone_value<'a>(
367 annotation: &'a type_bridge_contract::schema::AnnotationFact,
368) -> Option<SemanticFactValue<'a>> {
369 match annotation.value() {
370 SchemaAnnotationValue::Range(range) => {
371 let lower = range.lower().and_then(timezone_key);
372 let upper = range.upper().and_then(timezone_key);
373 (lower.is_some() || upper.is_some()).then(|| SemanticFactValue::TimeZoneRange {
374 subject: annotation.id().subject().clone(),
375 lower_utc_nanoseconds: lower,
376 upper_utc_nanoseconds: upper,
377 })
378 }
379 SchemaAnnotationValue::Values(values)
380 if values.iter().next().is_some_and(|value| {
381 matches!(
382 value,
383 type_bridge_contract::value::CanonicalValue::DateTimeTz(_)
384 )
385 }) =>
386 {
387 let mut keys = values.iter().filter_map(timezone_key).collect::<Vec<_>>();
388 keys.sort_by(|left, right| {
389 left.parse::<i128>()
390 .expect("semantic timezone key is an i128")
391 .cmp(
392 &right
393 .parse::<i128>()
394 .expect("semantic timezone key is an i128"),
395 )
396 });
397 Some(SemanticFactValue::TimeZoneValues {
398 subject: annotation.id().subject().clone(),
399 utc_nanoseconds: keys,
400 })
401 }
402 _ => None,
403 }
404}
405
406fn timezone_key(value: &type_bridge_contract::value::CanonicalValue) -> Option<String> {
407 let type_bridge_contract::value::CanonicalValue::DateTimeTz(value) = value else {
408 return None;
409 };
410 Some(value.semantic_utc_nanoseconds().to_string())
411}
412
413fn interface_kind(subject: &AnnotationSubjectId) -> Option<InterfaceKind> {
414 match subject {
415 AnnotationSubjectId::Owns(_) => Some(InterfaceKind::Owns),
416 AnnotationSubjectId::Relates(_) => Some(InterfaceKind::Relates),
417 AnnotationSubjectId::Plays(_) => Some(InterfaceKind::Plays),
418 _ => None,
419 }
420}