use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use rushdb_lmpg::reader::try_access as access;
use rushdb_storage::ReadView;
use rushdb_types::ids::TenantId;
use crate::adjacency::{Adjacency, TypeAdjacency};
use crate::planner::{extend_owners, OwnerSet};
use crate::{
eval_expr_ctx, filtered_row, run_matches, EvalCtx, Lmpg, QueryError, QuerySnapshot,
RelatedExpr, SearchRequest,
};
pub(crate) const TRAIL_SEARCH_BUDGET: usize = 100_000;
pub(crate) struct LabelGate<'a> {
snap: &'a ReadView,
tenant: TenantId,
labels: &'a [String],
seen: RefCell<HashMap<u64, bool>>,
}
impl<'a> LabelGate<'a> {
pub(crate) fn new(snap: &'a ReadView, tenant: TenantId, labels: &'a [String]) -> Self {
Self {
snap,
tenant,
labels,
seen: RefCell::default(),
}
}
pub(crate) fn admits(&self, ord: u64) -> Result<bool, QueryError> {
if self.labels.is_empty() {
return Ok(true);
}
if let Some(&known) = self.seen.borrow().get(&ord) {
return Ok(known);
}
let admits = access::record_by_ord(self.snap, self.tenant, ord)?
.is_some_and(|header| self.labels.contains(&header.label));
self.seen.borrow_mut().insert(ord, admits);
Ok(admits)
}
}
pub(crate) struct TrailCheck<'a> {
pub(crate) adjacency: &'a dyn Adjacency,
pub(crate) min: u64,
pub(crate) max: u64,
pub(crate) passable: &'a dyn Fn(u64) -> Result<bool, QueryError>,
pub(crate) target: &'a dyn Fn(u64) -> Result<bool, QueryError>,
pub(crate) cancel: &'a dyn Fn() -> Result<(), QueryError>,
}
pub(crate) fn trail_exists(check: &TrailCheck<'_>, start: u64) -> Result<bool, QueryError> {
let start_is_target = (check.passable)(start)? && (check.target)(start)?;
let undirected = check.adjacency.undirected();
let mut seen: HashMap<u64, (u64, u64, Option<u64>)> = HashMap::new();
seen.insert(start, (0, start, None));
let mut queue = VecDeque::from([start]);
let mut too_short = false;
let mut edges = Vec::new();
while let Some(node) = queue.pop_front() {
(check.cancel)()?;
let (depth, branch, entered_by) = seen[&node];
if depth >= check.max {
continue;
}
edges.clear();
check.adjacency.neighbours(node, true, &mut edges)?;
for &(other, rel) in &edges {
if other == start {
if start_is_target && (!undirected || entered_by != Some(rel)) {
if depth + 1 >= check.min {
return Ok(true);
}
too_short = true;
}
continue;
}
match seen.get(&other) {
None => {
if !(check.passable)(other)? {
continue;
}
let branch = if node == start { other } else { branch };
seen.insert(other, (depth + 1, branch, Some(rel)));
if (check.target)(other)? {
if depth + 1 >= check.min {
return Ok(true);
}
too_short = true;
}
queue.push_back(other);
}
Some(&(other_depth, other_branch, other_entered)) => {
if start_is_target
&& undirected
&& other_branch != branch
&& entered_by != Some(rel)
&& other_entered != Some(rel)
&& depth + other_depth < check.max
{
if depth + other_depth + 1 >= check.min {
return Ok(true);
}
too_short = true;
}
}
}
}
}
if too_short {
return longer_trail(check, start, start_is_target);
}
Ok(false)
}
type TrailFrame = (u64, Vec<(u64, u64)>, usize);
fn longer_trail(
check: &TrailCheck<'_>,
start: u64,
start_is_target: bool,
) -> Result<bool, QueryError> {
let mut used: Vec<u64> = Vec::new();
let mut steps = 0usize;
let mut first = Vec::new();
check.adjacency.neighbours(start, true, &mut first)?;
let mut work: Vec<TrailFrame> = vec![(start, first, 0)];
while let Some((_, edges, at)) = work.last_mut() {
let Some(&(other, rel)) = edges.get(*at) else {
work.pop();
used.pop();
continue;
};
*at += 1;
if used.contains(&rel) {
continue;
}
steps += 1;
if steps > TRAIL_SEARCH_BUDGET {
return Err(QueryError::ResourceLimit(
"$relation hops search exceeds its work budget; narrow hops or add a filter".into(),
));
}
if steps.is_multiple_of(1_024) {
(check.cancel)()?;
}
let depth = work.len() as u64;
let at_target = if other == start {
start_is_target
} else {
(check.passable)(other)? && (check.target)(other)?
};
if at_target && depth >= check.min {
return Ok(true);
}
if depth < check.max && (check.passable)(other)? {
let mut next = Vec::new();
check.adjacency.neighbours(other, true, &mut next)?;
used.push(rel);
work.push((other, next, 0));
}
}
Ok(false)
}
pub(crate) fn reaching_owners(
check: &TrailCheck<'_>,
targets: &OwnerSet,
root: &dyn Fn(u64) -> Result<bool, QueryError>,
owner_limit: usize,
) -> Result<OwnerSet, QueryError> {
let mut out = OwnerSet::new();
let mut hops: HashMap<u64, u64> = targets.iter().map(|ord| (ord, 0)).collect();
let mut undecided: Vec<u64> = targets.iter().collect();
let mut frontier: Vec<u64> = targets.iter().collect();
let mut edges = Vec::new();
for depth in 1..=check.max {
let mut next = Vec::new();
for &node in &frontier {
(check.cancel)()?;
edges.clear();
check.adjacency.neighbours(node, false, &mut edges)?;
for &(other, _) in &edges {
if hops.contains_key(&other) {
continue;
}
hops.insert(other, depth);
if depth >= check.min {
if root(other)? {
extend_owners(&mut out, [other], owner_limit)?;
}
} else {
undecided.push(other);
}
if depth < check.max && (check.passable)(other)? {
next.push(other);
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
for ord in undecided {
if !out.contains(ord) && root(ord)? && trail_exists(check, ord)? {
extend_owners(&mut out, [ord], owner_limit)?;
}
}
Ok(out)
}
pub(crate) fn hop_bounds(rel: &RelatedExpr) -> (u64, u64) {
let (min, max) = rel.hops.unwrap_or((1, 1));
(min.max(1), max.max(min.max(1)))
}
pub(crate) fn related_holds(
ctx: &EvalCtx<'_>,
ord: u64,
rel: &RelatedExpr,
) -> Result<bool, QueryError> {
let cancel = || ctx.check_cancelled();
if rel.cycle {
return ctx
.cycles
.on_cycle(&ctx.snap, ctx.lmpg.tenant, rel, ord, &cancel);
}
let (snap, tenant) = (ctx.snap.as_ref(), ctx.lmpg.tenant);
let adjacency = ctx.adjacency(rel.rel_type.as_deref(), rel.direction)?;
let labels = LabelGate::new(snap, tenant, &rel.target_labels);
let passable = |other: u64| -> Result<bool, QueryError> {
if rel.target_labels.is_empty() {
return Ok(access::record_by_ord(snap, tenant, other)?.is_some());
}
labels.admits(other)
};
let target = |other: u64| -> Result<bool, QueryError> {
let Some(filter) = &rel.filter else {
return Ok(true);
};
let row = filtered_row(snap, &ctx.snap, tenant, filter, other)?;
eval_expr_ctx(ctx, filter, &row, other)
};
let (min, max) = hop_bounds(rel);
trail_exists(
&TrailCheck {
adjacency: adjacency.as_ref(),
min,
max,
passable: &passable,
target: &target,
cancel: &cancel,
},
ord,
)
}
fn related_targets(
lmpg: &Lmpg,
snap: &QuerySnapshot,
rel: &RelatedExpr,
is_cancelled: &dyn Fn() -> bool,
) -> Result<OwnerSet, QueryError> {
let req = SearchRequest {
labels: rel.target_labels.clone(),
where_: rel.filter.as_deref().cloned(),
..SearchRequest::default()
};
Ok(
run_matches(lmpg, &req, is_cancelled, snap, usize::MAX, None)?
.retained
.into_iter()
.collect(),
)
}
pub(crate) fn expand_relation(
lmpg: &Lmpg,
snap: &QuerySnapshot,
rel: &RelatedExpr,
labels: &[String],
owner_limit: usize,
is_cancelled: &dyn Fn() -> bool,
) -> Result<OwnerSet, QueryError> {
let targets = related_targets(lmpg, snap, rel, is_cancelled)?;
let (view, tenant) = (snap.as_ref(), lmpg.tenant);
let passable_labels = LabelGate::new(view, tenant, &rel.target_labels);
let root_labels = LabelGate::new(view, tenant, labels);
let passable = |ord: u64| -> Result<bool, QueryError> {
if rel.target_labels.is_empty() {
return Ok(access::record_by_ord(view, tenant, ord)?.is_some());
}
passable_labels.admits(ord)
};
let target = |ord: u64| Ok(targets.contains(ord));
let root = |ord: u64| root_labels.admits(ord);
let cancel = || {
if is_cancelled() {
Err(QueryError::Cancelled)
} else {
Ok(())
}
};
let adjacency = TypeAdjacency::open(
snap,
tenant,
rel.rel_type.as_deref(),
rel.direction,
&cancel,
)?;
let (min, max) = hop_bounds(rel);
reaching_owners(
&TrailCheck {
adjacency: &adjacency,
min,
max,
passable: &passable,
target: &target,
cancel: &cancel,
},
&targets,
&root,
owner_limit,
)
}