use std::collections::BTreeMap;
use petgraph::graph::NodeIndex;
use crate::graph::ontology::ManagedLabelState;
use crate::graph::schema::{DirGraph, InternedKey};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterializedLabel {
pub label: String,
pub stamped: usize,
pub state: ManagedLabelState,
}
impl DirGraph {
pub(crate) fn ontology_ancestors_of(&self, node_type: InternedKey) -> &[InternedKey] {
self.ontology_closures
.get(&node_type)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub(crate) fn rebuild_ontology_closures(&mut self) {
self.ontology_closures.clear();
if self.ontology.is_empty() {
return;
}
let store = std::sync::Arc::clone(&self.ontology);
for name in store.classes.keys() {
let ancestors = store.ancestors(name);
if ancestors.is_empty() {
continue;
}
let type_key = self.interner.get_or_intern(name);
let keys: Vec<InternedKey> = ancestors
.iter()
.map(|a| self.interner.get_or_intern(a))
.collect();
self.ontology_closures.insert(type_key, keys);
}
}
pub(crate) fn managed_label_closed(&self, label: &str) -> bool {
matches!(
self.managed_labels.get(label),
Some(ManagedLabelState::Closed)
)
}
pub(crate) fn open_managed_label(&mut self, label: &str) {
if let Some(state) = self.managed_labels.get_mut(label) {
*state = ManagedLabelState::Open;
}
}
pub(crate) fn stamp_ontology_closure_on_tail(&mut self, node_type: &str, created: usize) {
if created == 0 || self.managed_labels.is_empty() || self.suppress_ontology_stamp {
return;
}
let type_key = InternedKey::from_str(node_type);
let ancestors: Vec<InternedKey> = self.ontology_ancestors_of(type_key).to_vec();
if ancestors.is_empty() {
return;
}
let Some(nodes) = self.type_indices.get(node_type) else {
return;
};
let all: Vec<NodeIndex> = nodes.iter().collect();
let tail: Vec<NodeIndex> = all[all.len().saturating_sub(created)..].to_vec();
for ancestor in ancestors {
self.add_node_labels_bulk(&tail, ancestor);
}
}
pub(crate) fn reject_abstract_batch_type(&self, node_type: &str) -> Result<(), String> {
if self.suppress_ontology_stamp {
return Ok(());
}
crate::graph::languages::cypher::executor::write::reject_abstract_create(self, node_type)
.map_err(|e| format!("add_nodes: {e}"))
}
pub fn materialize_ontology(&mut self, adopt: bool) -> Result<Vec<MaterializedLabel>, String> {
if self.ontology.is_empty() {
return Err("no ontology declared — call define_ontology first".to_string());
}
self.rebuild_ontology_closures();
let mut closure_members: BTreeMap<String, Vec<NodeIndex>> = BTreeMap::new();
let per_type: Vec<(String, Vec<String>)> = self
.ontology
.classes
.keys()
.map(|t| (t.clone(), self.ontology.ancestors(t)))
.collect();
for (node_type, ancestors) in per_type {
if ancestors.is_empty() {
continue;
}
let Some(nodes) = self.type_indices.get(&node_type) else {
continue;
};
let members: Vec<NodeIndex> = nodes.iter().collect();
for ancestor in ancestors {
closure_members
.entry(ancestor)
.or_default()
.extend(members.iter().copied());
}
}
let mut states: BTreeMap<String, ManagedLabelState> = BTreeMap::new();
for (label, members) in &closure_members {
let key = InternedKey::from_str(label);
let foreign = self
.secondary_label_index
.get(&key)
.map(|bucket| {
let mut sorted = members.clone();
sorted.sort_unstable();
bucket
.iter()
.filter(|idx| sorted.binary_search(idx).is_err())
.count()
})
.unwrap_or(0);
if foreign > 0 && !adopt {
return Err(format!(
"label '{label}' already has {foreign} member(s) outside the declared \
closure — materialize with adopt=True to manage it Open (correct, but \
closure-reliant optimizations stay off), or REMOVE the manual labels \
first"
));
}
let state = if foreign > 0 {
ManagedLabelState::Open
} else {
self.managed_labels
.get(label)
.copied()
.unwrap_or(ManagedLabelState::Closed)
};
states.insert(label.clone(), state);
}
let mut report = Vec::new();
for (label, members) in closure_members {
let key = self.interner.get_or_intern(&label);
let (stamped, _skipped) = self.add_node_labels_bulk(&members, key);
let state = states[&label];
self.managed_labels.insert(label.clone(), state);
report.push(MaterializedLabel {
label,
stamped,
state,
});
}
Ok(report)
}
pub fn dematerialize_ontology(&mut self) -> usize {
let labels: Vec<String> = self.managed_labels.keys().cloned().collect();
let mut removed_total = 0usize;
for label in labels {
removed_total += self.remove_label_bucket(InternedKey::from_str(&label));
}
self.managed_labels.clear();
removed_total
}
pub fn ontology_label_diff(&self) -> Vec<(String, ManagedLabelState, usize, usize)> {
let mut out = Vec::new();
for (label, state) in &self.managed_labels {
let key = InternedKey::from_str(label);
let bucket = self
.secondary_label_index
.get(&key)
.map(|b| b.as_slice())
.unwrap_or(&[]);
let mut expected: Vec<NodeIndex> = Vec::new();
for class in self.ontology.classes.keys() {
if self.ontology.ancestors(class).iter().any(|a| a == label) {
if let Some(nodes) = self.type_indices.get(class) {
expected.extend(nodes.iter());
}
}
}
expected.sort_unstable();
let extra = bucket
.iter()
.filter(|idx| expected.binary_search(idx).is_err())
.count();
let missing = expected
.iter()
.filter(|idx| bucket.binary_search(idx).is_err())
.count();
out.push((label.clone(), *state, extra, missing));
}
out
}
}