use std::collections::{BTreeSet, HashSet};
use crate::content::node::NodeState;
use crate::content::property::PropertyValue;
use crate::index::definition::IndexDefinition;
use crate::index::path_filter::PathVerdict;
use crate::index::property::key_encoding::keys_for_property;
use crate::index::property::mirror::{MirrorEntry, MirrorIndex};
use crate::index::property::type_predicate::TypePredicate;
use crate::index::property::unique::UniqueIndex;
use crate::index::{IndexError, IndexResult, values_of};
use crate::segment::record::RecordIdentifier;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct EntryCheckBudget {
maximum_entries: u64,
}
impl EntryCheckBudget {
#[must_use]
pub const fn of_entries(maximum_entries: u64) -> Self {
Self { maximum_entries }
}
#[must_use]
pub const fn maximum_entries(self) -> u64 {
self.maximum_entries
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct NodeCheckBudget {
maximum_nodes: Option<u64>,
}
impl NodeCheckBudget {
#[must_use]
pub const fn of_nodes(maximum_nodes: u64) -> Self {
Self {
maximum_nodes: Some(maximum_nodes),
}
}
#[must_use]
pub const fn unlimited() -> Self {
Self {
maximum_nodes: None,
}
}
#[must_use]
pub const fn maximum_nodes(self) -> Option<u64> {
self.maximum_nodes
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct PropertyIndexReport {
pub stale_entries: Vec<IndexEntryFault>,
pub mismatched_entries: Vec<IndexEntryFault>,
pub missing_entries: Vec<MissingEntry>,
pub duplicate_entries: Vec<DuplicateEntry>,
pub entries_checked: u64,
pub nodes_visited: Option<u64>,
pub approximate_counters: usize,
}
impl PropertyIndexReport {
#[must_use]
pub fn is_consistent(&self) -> bool {
!self.has_definite_faults() && self.missing_entries.is_empty()
}
#[must_use]
pub fn has_definite_faults(&self) -> bool {
!self.stale_entries.is_empty()
|| !self.mismatched_entries.is_empty()
|| !self.duplicate_entries.is_empty()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct IndexEntryFault {
pub key: String,
pub path: String,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct MissingEntry {
pub key: String,
pub path: String,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct DuplicateEntry {
pub key: String,
pub paths: Vec<String>,
}
pub fn check_entries(
definition_node: &NodeState<'_>,
definition: &IndexDefinition,
state_root: &NodeState<'_>,
budget: EntryCheckBudget,
) -> IndexResult<PropertyIndexReport> {
let mut report = PropertyIndexReport::default();
check_entry_half(definition_node, definition, state_root, budget, &mut report)?;
Ok(report)
}
pub fn check(
definition_node: &NodeState<'_>,
definition: &IndexDefinition,
state_root: &NodeState<'_>,
entry_budget: EntryCheckBudget,
node_budget: NodeCheckBudget,
) -> IndexResult<PropertyIndexReport> {
let mut report = check_entries(definition_node, definition, state_root, entry_budget)?;
check_covered_node_half(
definition_node,
definition,
state_root,
node_budget,
&mut report,
)?;
Ok(report)
}
enum Storage {
Mirror { child_names: Vec<String> },
Unique { child_name: String },
}
fn storage_for(definition: &IndexDefinition) -> Option<Storage> {
match definition.index_type.as_ref()? {
crate::index::IndexType::Reference => Some(Storage::Mirror {
child_names: vec![":references".to_owned(), ":weakreferences".to_owned()],
}),
crate::index::IndexType::Property if definition.property.unique => Some(Storage::Unique {
child_name: crate::index::INDEX_CONTENT_NODE_NAME.to_owned(),
}),
crate::index::IndexType::Property => Some(Storage::Mirror {
child_names: vec![crate::index::INDEX_CONTENT_NODE_NAME.to_owned()],
}),
_ => None,
}
}
fn check_entry_half(
definition_node: &NodeState<'_>,
definition: &IndexDefinition,
state_root: &NodeState<'_>,
budget: EntryCheckBudget,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
match storage_for(definition) {
Some(Storage::Mirror { child_names }) => {
let is_reference =
definition.index_type.as_ref() == Some(&crate::index::IndexType::Reference);
for child_name in child_names {
let Some(index) =
MirrorIndex::open(definition_node, &definition.path, &child_name)?
else {
continue;
};
report.approximate_counters += index.approximate_counter_count()?;
index.for_each_entry(|entry| {
charge_entry(budget, definition, report)?;
if is_reference {
check_reference_entry(state_root, entry, report)
} else {
check_mirror_entry(state_root, definition, entry, report)
}
})?;
}
}
Some(Storage::Unique { child_name }) => {
let Some(index) = UniqueIndex::open(definition_node, &child_name)? else {
return Ok(());
};
report.approximate_counters += index.approximate_counter_count()?;
for entry in index.entries()? {
if entry.is_duplicate() {
report.duplicate_entries.push(DuplicateEntry {
key: entry.key.clone(),
paths: entry.paths.clone(),
});
}
for path in &entry.paths {
charge_entry(budget, definition, report)?;
check_unique_entry(state_root, definition, &entry.key, path, report)?;
}
}
}
None => {}
}
Ok(())
}
fn charge_entry(
budget: EntryCheckBudget,
definition: &IndexDefinition,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
report.entries_checked += 1;
if report.entries_checked > budget.maximum_entries() {
return Err(IndexError::Record(crate::Error::InvalidFormat {
details: format!(
"checking the index entries of {} would examine more than {} entries",
definition.path,
budget.maximum_entries()
),
}));
}
Ok(())
}
fn check_mirror_entry(
state_root: &NodeState<'_>,
definition: &IndexDefinition,
entry: &MirrorEntry,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
let path = entry.content_path().to_owned();
let Some(node) = resolve(state_root, &path)? else {
report.stale_entries.push(IndexEntryFault {
key: entry.key.clone(),
path,
});
return Ok(());
};
if !node_has_key(&node, definition, &entry.key)? {
report.mismatched_entries.push(IndexEntryFault {
key: entry.key.clone(),
path,
});
}
Ok(())
}
fn check_reference_entry(
state_root: &NodeState<'_>,
entry: &MirrorEntry,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
let relative = entry.path.trim_start_matches('/');
let fault = |report: &mut PropertyIndexReport,
list: fn(&mut PropertyIndexReport) -> &mut Vec<IndexEntryFault>| {
list(report).push(IndexEntryFault {
key: entry.key.clone(),
path: relative.to_owned(),
});
};
let Some((node_path, property_name)) = relative.rsplit_once('/') else {
fault(report, |report| &mut report.stale_entries);
return Ok(());
};
let Some(node) = resolve(state_root, &format!("/{node_path}"))? else {
fault(report, |report| &mut report.stale_entries);
return Ok(());
};
let Some(property) = node.property(property_name)? else {
fault(report, |report| &mut report.mismatched_entries);
return Ok(());
};
let holds_identifier = values_of(&property)
.iter()
.filter_map(PropertyValue::as_text)
.any(|text| text == entry.key);
if !holds_identifier {
fault(report, |report| &mut report.mismatched_entries);
}
Ok(())
}
fn check_unique_entry(
state_root: &NodeState<'_>,
definition: &IndexDefinition,
key: &str,
path: &str,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
let Some(node) = resolve(state_root, path)? else {
report.stale_entries.push(IndexEntryFault {
key: key.to_owned(),
path: path.to_owned(),
});
return Ok(());
};
if !node_has_key(&node, definition, key)? {
report.mismatched_entries.push(IndexEntryFault {
key: key.to_owned(),
path: path.to_owned(),
});
}
Ok(())
}
fn node_has_key(
node: &NodeState<'_>,
definition: &IndexDefinition,
key: &str,
) -> IndexResult<bool> {
for property_name in &definition.property.property_names {
let Some(property) = node.property(property_name)? else {
continue;
};
let keys = keys_for_property(
&property,
&definition.property.value_pattern,
&definition.path,
)?;
if keys.contains(key) {
return Ok(true);
}
}
Ok(false)
}
fn check_covered_node_half(
definition_node: &NodeState<'_>,
definition: &IndexDefinition,
state_root: &NodeState<'_>,
budget: NodeCheckBudget,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
if definition.index_type.as_ref() == Some(&crate::index::IndexType::Reference) {
return Ok(());
}
let Some(storage) = storage_for(definition) else {
return Ok(());
};
if definition.property.property_names.is_empty() {
return Ok(());
}
let predicate = if definition.property.declaring_node_types.is_empty() {
None
} else {
Some(TypePredicate::new(
state_root,
&definition.property.declaring_node_types,
)?)
};
let indexed = indexed_keys_by_path(definition_node, definition, &storage)?;
let mut visited = 0u64;
let mut records_on_path = HashSet::new();
walk_covered(
state_root,
"/",
definition,
predicate.as_ref(),
&indexed,
budget,
&mut visited,
&mut records_on_path,
report,
)?;
report.nodes_visited = Some(visited);
Ok(())
}
fn indexed_keys_by_path(
definition_node: &NodeState<'_>,
definition: &IndexDefinition,
storage: &Storage,
) -> IndexResult<BTreeSet<(String, String)>> {
let mut indexed = BTreeSet::new();
match storage {
Storage::Mirror { child_names } => {
for child_name in child_names {
let Some(index) = MirrorIndex::open(definition_node, &definition.path, child_name)?
else {
continue;
};
index.for_each_entry(|entry| {
indexed.insert((entry.content_path().to_owned(), entry.key.clone()));
Ok(())
})?;
}
}
Storage::Unique { child_name } => {
let Some(index) = UniqueIndex::open(definition_node, child_name)? else {
return Ok(indexed);
};
for entry in index.entries()? {
for path in entry.paths {
indexed.insert((path, entry.key.clone()));
}
}
}
}
Ok(indexed)
}
#[expect(
clippy::too_many_arguments,
reason = "the walk's state is a stack frame rather than a configuration: every \
argument is either the position in the tree or a per-walk accumulator, \
and grouping them into a struct would hide that the recursion owns them"
)]
fn walk_covered(
node: &NodeState<'_>,
path: &str,
definition: &IndexDefinition,
predicate: Option<&TypePredicate>,
indexed: &BTreeSet<(String, String)>,
budget: NodeCheckBudget,
visited: &mut u64,
records_on_path: &mut HashSet<RecordIdentifier>,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
let record = node.record_identifier();
if !records_on_path.insert(record) {
return Err(IndexError::Record(crate::Error::InvalidFormat {
details: format!(
"the content at {path} contains node record {record} in its own subtree; \
the node records form a cycle"
),
}));
}
*visited += 1;
if let Some(maximum) = budget.maximum_nodes()
&& *visited > maximum
{
return Err(IndexError::Record(crate::Error::InvalidFormat {
details: format!(
"checking {} would visit more than {maximum} content nodes",
definition.path
),
}));
}
let verdict = definition.path_filter.filter(path);
if verdict == PathVerdict::Include {
check_covered_node(node, path, definition, predicate, indexed, report)?;
}
if verdict != PathVerdict::Exclude {
for (name, child) in node.child_node_entries()? {
if name.starts_with(':') {
continue;
}
let child_path = if path == "/" {
format!("/{name}")
} else {
format!("{path}/{name}")
};
walk_covered(
&child,
&child_path,
definition,
predicate,
indexed,
budget,
visited,
records_on_path,
report,
)?;
}
}
records_on_path.remove(&record);
Ok(())
}
fn check_covered_node(
node: &NodeState<'_>,
path: &str,
definition: &IndexDefinition,
predicate: Option<&TypePredicate>,
indexed: &BTreeSet<(String, String)>,
report: &mut PropertyIndexReport,
) -> IndexResult<()> {
if let Some(predicate) = predicate
&& !predicate.test(node)?
{
return Ok(());
}
for property_name in &definition.property.property_names {
let Some(property) = node.property(property_name)? else {
continue;
};
for key in keys_for_property(
&property,
&definition.property.value_pattern,
&definition.path,
)? {
if !indexed.contains(&(path.to_owned(), key.clone())) {
report.missing_entries.push(MissingEntry {
key,
path: path.to_owned(),
});
}
}
}
Ok(())
}
fn resolve<'provider>(
state_root: &NodeState<'provider>,
path: &str,
) -> IndexResult<Option<NodeState<'provider>>> {
let mut current = *state_root;
for name in path.split('/').filter(|name| !name.is_empty()) {
match current.child_node(name)? {
Some(child) => current = child,
None => return Ok(None),
}
}
Ok(Some(current))
}