use std::collections::{HashMap, HashSet, VecDeque};
use super::InMemory;
use crate::{Result, backend::errors::BackendError, entry::ID};
pub(crate) fn build_path_from_root(
backend: &InMemory,
tree: &ID,
subtree: &str,
target_entry: &ID,
) -> Result<Vec<ID>> {
let mut path = Vec::new();
let mut current = target_entry.clone();
let mut visited = HashSet::new();
loop {
if visited.contains(¤t) {
return Err(BackendError::CycleDetected { entry_id: current }.into());
}
visited.insert(current.clone());
path.push(current.clone());
let entry = super::storage::get(backend, ¤t)?;
if current == *tree || entry.is_root() {
break;
}
let parents = if subtree.is_empty() || entry.subtree_parents(subtree).is_err() {
entry.parents()?
} else {
entry.subtree_parents(subtree)?
};
if parents.is_empty() {
break;
} else {
current = parents[0].clone();
}
}
path.reverse();
Ok(path)
}
pub(crate) fn collect_root_to_target(
backend: &InMemory,
tree: &ID,
subtree: &str,
target_entry: &ID,
) -> Result<Vec<ID>> {
build_path_from_root(backend, tree, subtree, target_entry)
}
pub(crate) fn get_path_from_to(
backend: &InMemory,
tree_id: &ID,
subtree: &str,
from_id: &ID,
to_ids: &[ID],
) -> Result<Vec<ID>> {
if to_ids.is_empty() {
return Ok(vec![]);
}
let mut result = Vec::new();
let mut to_process = VecDeque::new();
let mut processed = HashSet::new();
for to_id in to_ids {
if to_id != from_id {
to_process.push_back(to_id.clone());
}
}
while let Some(current) = to_process.pop_front() {
if processed.contains(¤t) {
continue;
}
if current == *from_id {
processed.insert(current);
continue;
}
result.push(current.clone());
processed.insert(current.clone());
let parents = get_sorted_store_parents(backend, tree_id, ¤t, subtree)?;
for parent in parents {
if !processed.contains(&parent) {
to_process.push_back(parent);
}
}
}
result.sort();
result.dedup();
if !result.is_empty() {
let heights = super::cache::calculate_heights(backend, tree_id, Some(subtree))?;
result.sort_by(|a, b| {
let a_height = *heights.get(a).unwrap_or(&0);
let b_height = *heights.get(b).unwrap_or(&0);
a_height.cmp(&b_height).then_with(|| a.cmp(b))
});
}
Ok(result)
}
pub(crate) fn get_sorted_store_parents(
backend: &InMemory,
tree_id: &ID,
entry_id: &ID,
subtree: &str,
) -> Result<Vec<ID>> {
let entries = backend.entries.read().unwrap();
let entry = entries
.get(entry_id)
.ok_or_else(|| BackendError::EntryNotFound {
id: entry_id.clone(),
})?;
if !entry.in_tree(tree_id) || !entry.in_subtree(subtree) {
return Ok(Vec::new());
}
let mut parents = match entry.subtree_parents(subtree) {
Ok(parents) => parents,
Err(_) => return Ok(Vec::new()),
};
drop(entries);
if !parents.is_empty() {
let heights = super::cache::calculate_heights(backend, tree_id, Some(subtree))?;
parents.sort_by(|a, b| {
let a_height = *heights.get(a).unwrap_or(&0);
let b_height = *heights.get(b).unwrap_or(&0);
a_height.cmp(&b_height).then_with(|| a.cmp(b))
});
}
Ok(parents)
}
pub(crate) fn find_lca(
backend: &InMemory,
tree: &ID,
subtree: &str,
entry_ids: &[ID],
) -> Result<ID> {
if entry_ids.is_empty() {
return Err(BackendError::EmptyEntryList {
operation: "LCA".to_string(),
}
.into());
}
if entry_ids.len() == 1 {
return Ok(entry_ids[0].clone());
}
tracing::debug!(
tree_id = %tree,
subtree = subtree,
entry_count = entry_ids.len(),
entry_ids = ?entry_ids,
"Starting LCA algorithm"
);
for entry_id in entry_ids {
match super::storage::get(backend, entry_id) {
Ok(entry) => {
if let Err(validation_error) = entry.validate() {
tracing::error!(
entry_id = %entry_id,
error = %validation_error,
"Entry failed validation in LCA algorithm"
);
return Err(BackendError::EntryValidationFailed {
entry_id: entry_id.clone(),
reason: validation_error.to_string(),
}
.into());
}
if !entry.in_tree(tree) {
tracing::warn!(
entry_id = %entry_id,
tree_id = %tree,
actual_tree = %entry.root(),
"Entry is not in the expected tree"
);
return Err(BackendError::EntryNotInTree {
entry_id: entry_id.clone(),
tree_id: tree.clone(),
}
.into());
}
tracing::debug!(
entry_id = %entry_id,
parents = ?entry.parents().unwrap_or_default(),
"Entry verified and belongs to tree"
);
}
Err(_) => {
tracing::error!(entry_id = %entry_id, "Entry not found");
return Err(BackendError::EntryNotFound {
id: entry_id.clone(),
}
.into());
}
}
}
let mut ancestors: HashMap<ID, HashSet<usize>> = HashMap::new();
let mut queues: Vec<VecDeque<ID>> = Vec::new();
for (idx, entry_id) in entry_ids.iter().enumerate() {
let mut queue = VecDeque::new();
queue.push_back(entry_id.clone());
ancestors.entry(entry_id.clone()).or_default().insert(idx);
queues.push(queue);
}
let mut iteration = 0;
loop {
iteration += 1;
let mut any_progress = false;
tracing::trace!(iteration = iteration, "LCA BFS iteration starting");
for (idx, queue) in queues.iter_mut().enumerate() {
if let Some(current) = queue.pop_front() {
any_progress = true;
tracing::trace!(
iteration = iteration,
entry_idx = idx,
current_entry = %current,
"Processing entry in BFS"
);
let reachable_by = ancestors.entry(current.clone()).or_default();
reachable_by.insert(idx);
tracing::trace!(
current_entry = %current,
reachable_by_count = reachable_by.len(),
required_count = entry_ids.len(),
reachable_by = ?reachable_by,
"Checking if entry is reachable by all"
);
if reachable_by.len() == entry_ids.len() {
tracing::debug!(
lca = %current,
iteration = iteration,
"Found LCA successfully"
);
return Ok(current);
}
if let Ok(entry) = super::storage::get(backend, ¤t) {
match entry.subtree_parents(subtree) {
Ok(parents) => {
if parents.is_empty() {
tracing::trace!(
entry = %current,
subtree = subtree,
"Entry is subtree root (empty parents)"
);
} else {
tracing::trace!(
entry = %current,
subtree_parents = ?parents,
"Adding subtree parents to queue"
);
for parent in parents {
tracing::trace!(
entry = %current,
parent = %parent,
"Adding parent to queue"
);
queue.push_back(parent);
}
}
}
Err(_) => {
tracing::error!(
entry = %current,
subtree = subtree,
"Entry encountered in subtree LCA that doesn't contain the subtree"
);
return Err(BackendError::EntryNotInSubtree {
entry_id: current,
tree_id: tree.clone(),
subtree: subtree.to_string(),
}
.into());
}
}
}
}
}
if !any_progress {
tracing::debug!(
iteration = iteration,
final_ancestors_count = ancestors.len(),
subtree = subtree,
"BFS terminated without finding perfect LCA - using fallback strategy"
);
if let Some((best_ancestor, reachable_set)) = ancestors
.iter()
.max_by_key(|(_, reachable_by)| reachable_by.len())
{
tracing::debug!(
best_ancestor = %best_ancestor,
reachable_by_count = reachable_set.len(),
required_count = entry_ids.len(),
"Using best available ancestor as fallback LCA"
);
return Ok(best_ancestor.clone());
}
break;
}
}
Err(BackendError::NoCommonAncestor {
entry_ids: entry_ids.to_vec(),
}
.into())
}
pub(crate) fn get_tips(backend: &InMemory, tree: &ID) -> Result<Vec<ID>> {
let tips_cache = backend.tips.read().unwrap();
if let Some(cache) = tips_cache.get(tree) {
let cached_tips: Vec<ID> = cache.tree_tips.iter().cloned().collect();
return Ok(cached_tips);
}
drop(tips_cache);
let mut tips = Vec::new();
let entries = backend.entries.read().unwrap();
for (id, entry) in entries.iter() {
if entry.root() == tree && super::storage::is_tip(backend, tree, id) {
tips.push(id.clone());
} else if entry.is_root()
&& entry.id() == *tree
&& super::storage::is_tip(backend, tree, id)
{
tips.push(id.clone());
}
}
drop(entries);
let tips_set: HashSet<ID> = tips.iter().cloned().collect();
let mut tips_cache = backend.tips.write().unwrap();
let cache = tips_cache.entry(tree.clone()).or_default();
cache.tree_tips = tips_set;
Ok(tips)
}
pub(crate) fn get_store_tips(backend: &InMemory, tree: &ID, subtree: &str) -> Result<Vec<ID>> {
let tips_cache = backend.tips.read().unwrap();
if let Some(cache) = tips_cache.get(tree)
&& let Some(subtree_tips) = cache.subtree_tips.get(subtree)
{
return Ok(subtree_tips.iter().cloned().collect());
}
drop(tips_cache);
let tree_tips = get_tips(backend, tree)?;
let subtree_tips = get_store_tips_up_to_entries(backend, tree, subtree, &tree_tips)?;
let tips_set: HashSet<ID> = subtree_tips.iter().cloned().collect();
let mut tips_cache = backend.tips.write().unwrap();
let cache = tips_cache.entry(tree.clone()).or_default();
cache.subtree_tips.insert(subtree.to_string(), tips_set);
Ok(subtree_tips)
}
pub(crate) fn get_store_tips_up_to_entries(
backend: &InMemory,
tree: &ID,
subtree: &str,
main_entries: &[ID],
) -> Result<Vec<ID>> {
if main_entries.is_empty() {
return Ok(Vec::new());
}
let current_tree_tips = get_tips(backend, tree)?;
if main_entries == current_tree_tips {
let mut tips = Vec::new();
let entries = backend.entries.read().unwrap();
for (id, entry) in entries.iter() {
if entry.in_tree(tree)
&& entry.in_subtree(subtree)
&& super::storage::is_subtree_tip(backend, tree, subtree, id)
{
tips.push(id.clone());
}
}
return Ok(tips);
}
let all_tree_entries = super::storage::get_tree_from_tips(backend, tree, main_entries)?;
let subtree_entries: Vec<_> = all_tree_entries
.into_iter()
.filter(|entry| entry.in_subtree(subtree))
.collect();
if subtree_entries.is_empty() {
return Ok(Vec::new());
}
let mut tips = Vec::new();
for entry in &subtree_entries {
let entry_id = entry.id();
let is_tip = !subtree_entries.iter().any(|other_entry| {
if let Ok(parents) = other_entry.subtree_parents(subtree) {
parents.contains(&entry_id)
} else {
false
}
});
if is_tip {
tips.push(entry_id);
}
}
Ok(tips)
}