1use std::collections::{BTreeMap, BTreeSet};
4
5use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
6use type_bridge_contract::id::{Label, RoleId, TypeId, TypeKind};
7use type_bridge_contract::schema::{
8 AnnotationSubjectId, DeclaredSchema, RelatesFactId, SchemaFact, SchemaFactId, SchemaOperation,
9 SubFactId, ValueFactId,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct FactDependencyGraph {
15 dependencies: BTreeMap<SchemaFactId, BTreeSet<SchemaFactId>>,
16 dependents: BTreeMap<SchemaFactId, BTreeSet<SchemaFactId>>,
17}
18
19impl FactDependencyGraph {
20 pub fn from_declared(declared: &DeclaredSchema) -> Result<Self, Diagnostic> {
22 Self::from_facts(declared.facts())
23 }
24
25 pub fn from_facts<'a>(
27 facts: impl IntoIterator<Item = &'a SchemaFact>,
28 ) -> Result<Self, Diagnostic> {
29 let mut inventory = BTreeMap::new();
30 for fact in facts {
31 let id = fact.id();
32 if inventory.insert(id.clone(), fact.clone()).is_some() {
33 return Err(failure(
34 "schema_delta_duplicate_fact",
35 format!("duplicate fact identity in dependency inventory: {id:?}"),
36 ));
37 }
38 }
39
40 let mut labels: BTreeMap<Label, Vec<SchemaFactId>> = BTreeMap::new();
41 let mut sub_by_child: BTreeMap<TypeId, Vec<SubFactId>> = BTreeMap::new();
42 for fact in inventory.values() {
43 match fact {
44 SchemaFact::Type(fact) => labels
45 .entry(fact.id().label().clone())
46 .or_default()
47 .push(SchemaFactId::Type(fact.id().clone())),
48 SchemaFact::Struct(fact) => labels
49 .entry(fact.id().label().clone())
50 .or_default()
51 .push(SchemaFactId::Struct(fact.id().clone())),
52 SchemaFact::Sub(fact) => sub_by_child
53 .entry(fact.id().subtype().clone())
54 .or_default()
55 .push(fact.id().clone()),
56 _ => {}
57 }
58 }
59 for candidates in labels.values_mut() {
60 candidates.sort();
61 }
62 for edges in sub_by_child.values_mut() {
63 edges.sort();
64 }
65
66 let mut dependencies = BTreeMap::new();
67 for fact in inventory.values() {
68 dependencies.insert(fact.id(), dependencies_for(fact, &labels, &sub_by_child)?);
69 }
70
71 let mut dependents: BTreeMap<SchemaFactId, BTreeSet<SchemaFactId>> = BTreeMap::new();
72 for id in dependencies.keys() {
73 dependents.entry(id.clone()).or_default();
74 }
75 for (dependent, prerequisites) in &dependencies {
76 for prerequisite in prerequisites {
77 dependents
78 .entry(prerequisite.clone())
79 .or_default()
80 .insert(dependent.clone());
81 }
82 }
83
84 Ok(Self {
85 dependencies,
86 dependents,
87 })
88 }
89
90 #[must_use]
92 pub fn dependencies(&self, id: &SchemaFactId) -> Option<&BTreeSet<SchemaFactId>> {
93 self.dependencies.get(id)
94 }
95
96 #[must_use]
98 pub fn dependents(&self, id: &SchemaFactId) -> Option<&BTreeSet<SchemaFactId>> {
99 self.dependents.get(id)
100 }
101
102 pub fn validate_complete(&self) -> Result<(), Diagnostic> {
104 for (dependent, prerequisites) in &self.dependencies {
105 for prerequisite in prerequisites {
106 if !self.dependencies.contains_key(prerequisite) {
107 return Err(failure(
108 "schema_delta_missing_dependency",
109 format!("fact {dependent:?} requires absent fact {prerequisite:?}"),
110 ));
111 }
112 }
113 }
114 Ok(())
115 }
116}
117
118pub fn plan_schema_operations(
120 source: &DeclaredSchema,
121 target: &DeclaredSchema,
122) -> Result<Vec<SchemaOperation>, Diagnostic> {
123 let source_facts = fact_map(source);
124 let target_facts = fact_map(target);
125 let source_graph = FactDependencyGraph::from_declared(source)?;
126 let target_graph = FactDependencyGraph::from_declared(target)?;
127 source_graph.validate_complete()?;
128 target_graph.validate_complete()?;
129
130 let source_ids: BTreeSet<_> = source_facts.keys().cloned().collect();
131 let target_ids: BTreeSet<_> = target_facts.keys().cloned().collect();
132 let added: BTreeSet<_> = target_ids.difference(&source_ids).cloned().collect();
133 let removed: BTreeSet<_> = source_ids.difference(&target_ids).cloned().collect();
134 let redefined: BTreeSet<_> = source_ids
135 .intersection(&target_ids)
136 .filter(|id| source_facts.get(*id) != target_facts.get(*id))
137 .cloned()
138 .collect();
139
140 for id in added.iter().chain(removed.iter()).chain(redefined.iter()) {
141 if matches!(
142 source_facts.get(id).or_else(|| target_facts.get(id)),
143 Some(SchemaFact::Function(_))
144 ) {
145 return Err(failure(
146 "unsupported_function_migration",
147 "automatic migration of opaque function bodies is unsupported",
148 ));
149 }
150 }
151
152 let mut operations = Vec::new();
153
154 for component in ordered_components(&target_graph, &added) {
156 let facts = component
157 .into_iter()
158 .map(|id| target_facts[&id].clone())
159 .collect();
160 operations.push(SchemaOperation::define(facts)?);
161 }
162
163 for component in ordered_components(&target_graph, &redefined) {
165 for id in component {
166 operations.push(SchemaOperation::redefine(
167 source_facts[&id].clone(),
168 target_facts[&id].clone(),
169 )?);
170 }
171 }
172
173 for component in ordered_components(&source_graph, &removed)
175 .into_iter()
176 .rev()
177 {
178 for id in component.into_iter().rev() {
179 operations.push(SchemaOperation::undefine(source_facts[&id].clone()));
180 }
181 }
182
183 Ok(operations)
184}
185
186fn fact_map(declared: &DeclaredSchema) -> BTreeMap<SchemaFactId, SchemaFact> {
187 declared
188 .facts()
189 .map(|fact| (fact.id(), fact.clone()))
190 .collect()
191}
192
193fn dependencies_for(
194 fact: &SchemaFact,
195 labels: &BTreeMap<Label, Vec<SchemaFactId>>,
196 sub_by_child: &BTreeMap<TypeId, Vec<SubFactId>>,
197) -> Result<BTreeSet<SchemaFactId>, Diagnostic> {
198 let mut dependencies = BTreeSet::new();
199 match fact {
200 SchemaFact::Type(_) | SchemaFact::Struct(_) => {}
201 SchemaFact::Sub(fact) => {
202 dependencies.insert(SchemaFactId::Type(fact.id().subtype().clone()));
203 dependencies.insert(SchemaFactId::Type(fact.id().supertype().clone()));
204 }
205 SchemaFact::Value(fact) => {
206 dependencies.insert(SchemaFactId::Type(attribute_type(fact.id().attribute())?));
207 }
208 SchemaFact::Owns(fact) => {
209 dependencies.insert(SchemaFactId::Type(fact.id().owner().clone()));
210 dependencies.insert(SchemaFactId::Type(attribute_type(fact.id().attribute())?));
211 dependencies.insert(SchemaFactId::Value(ValueFactId::new(
212 fact.id().attribute().clone(),
213 )));
214 }
215 SchemaFact::Relates(fact) => {
216 dependencies.insert(SchemaFactId::Type(fact.id().relation().clone()));
217 if let Some(parent_role) = fact.specializes() {
218 dependencies.insert(SchemaFactId::Relates(relates_id(parent_role)?));
219 let parent_relation = declaring_relation_type(parent_role)?;
220 let path = find_sub_path(
221 fact.id().relation(),
222 &parent_relation,
223 sub_by_child,
224 &mut BTreeSet::new(),
225 )
226 .ok_or_else(|| {
227 failure(
228 "schema_delta_missing_specialization_path",
229 format!(
230 "role specialization has no subtype path from {:?} to {parent_relation:?}",
231 fact.id().relation()
232 ),
233 )
234 })?;
235 dependencies.extend(path.into_iter().map(SchemaFactId::Sub));
236 }
237 }
238 SchemaFact::Plays(fact) => {
239 dependencies.insert(SchemaFactId::Type(fact.id().player().clone()));
240 dependencies.insert(SchemaFactId::Relates(relates_id(fact.id().role())?));
241 }
242 SchemaFact::Annotation(fact) => {
243 dependencies.insert(annotation_subject_id(fact.id().subject()));
244 }
245 SchemaFact::Function(fact) => {
246 for label in fact.schema_references() {
247 let candidates = labels.get(label).ok_or_else(|| {
248 failure(
249 "schema_delta_missing_function_reference",
250 format!("function references absent schema label {label:?}"),
251 )
252 })?;
253 if candidates.len() != 1 {
254 return Err(failure(
255 "schema_delta_ambiguous_function_reference",
256 format!("function schema label {label:?} is ambiguous"),
257 ));
258 }
259 dependencies.insert(candidates[0].clone());
260 }
261 }
262 }
263 Ok(dependencies)
264}
265
266fn annotation_subject_id(subject: &AnnotationSubjectId) -> SchemaFactId {
267 match subject {
268 AnnotationSubjectId::Type(id) => SchemaFactId::Type(id.clone()),
269 AnnotationSubjectId::Sub(id) => SchemaFactId::Sub(id.clone()),
270 AnnotationSubjectId::Value(id) => SchemaFactId::Value(id.clone()),
271 AnnotationSubjectId::Owns(id) => SchemaFactId::Owns(id.clone()),
272 AnnotationSubjectId::Relates(id) => SchemaFactId::Relates(id.clone()),
273 AnnotationSubjectId::Plays(id) => SchemaFactId::Plays(id.clone()),
274 AnnotationSubjectId::Function(id) => SchemaFactId::Function(id.clone()),
275 }
276}
277
278fn attribute_type(attribute: &type_bridge_contract::id::AttributeId) -> Result<TypeId, Diagnostic> {
279 TypeId::new(TypeKind::Attribute, attribute.label().as_str())
280}
281
282fn declaring_relation_type(role: &RoleId) -> Result<TypeId, Diagnostic> {
283 TypeId::new(TypeKind::Relation, role.declaring_relation().as_str())
284}
285
286fn relates_id(role: &RoleId) -> Result<RelatesFactId, Diagnostic> {
287 RelatesFactId::new(declaring_relation_type(role)?, role.clone())
288}
289
290fn find_sub_path(
291 current: &TypeId,
292 target: &TypeId,
293 sub_by_child: &BTreeMap<TypeId, Vec<SubFactId>>,
294 visited: &mut BTreeSet<TypeId>,
295) -> Option<Vec<SubFactId>> {
296 if current == target {
297 return Some(Vec::new());
298 }
299 if !visited.insert(current.clone()) {
300 return None;
301 }
302 for edge in sub_by_child.get(current).into_iter().flatten() {
303 if let Some(mut tail) = find_sub_path(edge.supertype(), target, sub_by_child, visited) {
304 let mut path = vec![edge.clone()];
305 path.append(&mut tail);
306 return Some(path);
307 }
308 }
309 None
310}
311
312fn ordered_components(
313 graph: &FactDependencyGraph,
314 nodes: &BTreeSet<SchemaFactId>,
315) -> Vec<Vec<SchemaFactId>> {
316 if nodes.is_empty() {
317 return Vec::new();
318 }
319 let components = strongly_connected_components(graph, nodes);
320 let mut component_of = BTreeMap::new();
321 for (index, component) in components.iter().enumerate() {
322 for node in component {
323 component_of.insert(node.clone(), index);
324 }
325 }
326
327 let mut prerequisites = vec![BTreeSet::new(); components.len()];
328 let mut dependents = vec![BTreeSet::new(); components.len()];
329 for (index, component) in components.iter().enumerate() {
330 for node in component {
331 for dependency in graph.dependencies(node).into_iter().flatten() {
332 if let Some(dependency_index) = component_of.get(dependency).copied()
333 && dependency_index != index
334 {
335 prerequisites[index].insert(dependency_index);
336 dependents[dependency_index].insert(index);
337 }
338 }
339 }
340 }
341
342 let keys: Vec<_> = components
343 .iter()
344 .map(|component| component[0].clone())
345 .collect();
346 let mut remaining: Vec<_> = prerequisites.iter().map(BTreeSet::len).collect();
347 let mut ready = BTreeSet::new();
348 for (index, count) in remaining.iter().enumerate() {
349 if *count == 0 {
350 ready.insert((keys[index].clone(), index));
351 }
352 }
353
354 let mut ordered = Vec::with_capacity(components.len());
355 while let Some(entry) = ready.iter().next().cloned() {
356 ready.remove(&entry);
357 let index = entry.1;
358 ordered.push(components[index].clone());
359 for dependent in dependents[index].iter().copied() {
360 remaining[dependent] -= 1;
361 if remaining[dependent] == 0 {
362 ready.insert((keys[dependent].clone(), dependent));
363 }
364 }
365 }
366 ordered
367}
368
369fn strongly_connected_components(
370 graph: &FactDependencyGraph,
371 nodes: &BTreeSet<SchemaFactId>,
372) -> Vec<Vec<SchemaFactId>> {
373 struct Tarjan {
374 next_index: usize,
375 stack: Vec<SchemaFactId>,
376 on_stack: BTreeSet<SchemaFactId>,
377 indices: BTreeMap<SchemaFactId, usize>,
378 lowlinks: BTreeMap<SchemaFactId, usize>,
379 components: Vec<Vec<SchemaFactId>>,
380 }
381
382 fn visit(
383 node: SchemaFactId,
384 graph: &FactDependencyGraph,
385 nodes: &BTreeSet<SchemaFactId>,
386 state: &mut Tarjan,
387 ) {
388 let index = state.next_index;
389 state.next_index += 1;
390 state.indices.insert(node.clone(), index);
391 state.lowlinks.insert(node.clone(), index);
392 state.stack.push(node.clone());
393 state.on_stack.insert(node.clone());
394
395 let neighbors: Vec<_> = graph
396 .dependencies(&node)
397 .into_iter()
398 .flatten()
399 .filter(|neighbor| nodes.contains(*neighbor))
400 .cloned()
401 .collect();
402 for neighbor in neighbors {
403 if !state.indices.contains_key(&neighbor) {
404 visit(neighbor.clone(), graph, nodes, state);
405 let neighbor_lowlink = state.lowlinks[&neighbor];
406 let lowlink = state.lowlinks.get_mut(&node).expect("visited node");
407 *lowlink = (*lowlink).min(neighbor_lowlink);
408 } else if state.on_stack.contains(&neighbor) {
409 let neighbor_index = state.indices[&neighbor];
410 let lowlink = state.lowlinks.get_mut(&node).expect("visited node");
411 *lowlink = (*lowlink).min(neighbor_index);
412 }
413 }
414
415 if state.lowlinks[&node] == state.indices[&node] {
416 let mut component = Vec::new();
417 loop {
418 let member = state.stack.pop().expect("SCC root is on stack");
419 state.on_stack.remove(&member);
420 component.push(member.clone());
421 if member == node {
422 break;
423 }
424 }
425 component.sort();
426 state.components.push(component);
427 }
428 }
429
430 let mut state = Tarjan {
431 next_index: 0,
432 stack: Vec::new(),
433 on_stack: BTreeSet::new(),
434 indices: BTreeMap::new(),
435 lowlinks: BTreeMap::new(),
436 components: Vec::new(),
437 };
438 for node in nodes {
439 if !state.indices.contains_key(node) {
440 visit(node.clone(), graph, nodes, &mut state);
441 }
442 }
443 state.components
444}
445
446fn failure(code: &'static str, message: impl Into<String>) -> Diagnostic {
447 Diagnostic::new(
448 DiagnosticCategory::Integrity,
449 DiagnosticCode::new(code).expect("static diagnostic code is canonical"),
450 message,
451 )
452}