Skip to main content

type_bridge_schema/
delta_dependencies.rs

1//! Provider-neutral fact dependency planning for schema deltas.
2
3use 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/// The exact fact-level dependency graph used to order schema operations.
13#[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    /// Build the graph from a complete declared fact inventory.
21    pub fn from_declared(declared: &DeclaredSchema) -> Result<Self, Diagnostic> {
22        Self::from_facts(declared.facts())
23    }
24
25    /// Build the graph from a complete fact inventory.
26    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    /// Return the direct prerequisites of one fact.
91    #[must_use]
92    pub fn dependencies(&self, id: &SchemaFactId) -> Option<&BTreeSet<SchemaFactId>> {
93        self.dependencies.get(id)
94    }
95
96    /// Return the direct dependents of one fact.
97    #[must_use]
98    pub fn dependents(&self, id: &SchemaFactId) -> Option<&BTreeSet<SchemaFactId>> {
99        self.dependents.get(id)
100    }
101
102    /// Reject an inventory with a dependency outside the inventory.
103    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
118/// Compute the exact formal diff and deterministic dependency-safe operation order.
119pub 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    // Establish all new identities before replacements can refer to them.
155    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    // Replacements retain their identity, so SCC members can be emitted in stable ID order.
164    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    // Dependents must disappear before their prerequisites.
174    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}