use std::collections::HashSet;
use std::collections::VecDeque;
use crate::id::id_from_value;
use crate::id::id_into_value;
use crate::id::RawId;
use crate::id::ID_LEN;
use crate::query::intersectionconstraint::IntersectionConstraint;
use crate::query::Binding;
use crate::query::Constraint;
use crate::query::Query;
use crate::query::TriblePattern;
use crate::query::Variable;
use crate::query::VariableContext;
use crate::query::VariableId;
use crate::query::VariableSet;
use crate::trible::TribleSet;
use crate::value::schemas::genid::GenId;
use crate::value::RawValue;
use crate::value::ToValue;
#[derive(Clone)]
pub enum PathOp {
Attr(RawId),
Concat,
Union,
Star,
Plus,
}
#[derive(Clone)]
enum PathExpr {
Attr(RawId),
Concat(Box<PathExpr>, Box<PathExpr>),
Union(Box<PathExpr>, Box<PathExpr>),
Star(Box<PathExpr>),
Plus(Box<PathExpr>),
}
impl PathExpr {
fn from_postfix(ops: &[PathOp]) -> Self {
let mut stack: Vec<PathExpr> = Vec::new();
for op in ops {
match op {
PathOp::Attr(id) => stack.push(PathExpr::Attr(*id)),
PathOp::Concat => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(PathExpr::Concat(Box::new(a), Box::new(b)));
}
PathOp::Union => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(PathExpr::Union(Box::new(a), Box::new(b)));
}
PathOp::Star => {
let a = stack.pop().unwrap();
stack.push(PathExpr::Star(Box::new(a)));
}
PathOp::Plus => {
let a = stack.pop().unwrap();
stack.push(PathExpr::Plus(Box::new(a)));
}
}
}
stack.pop().unwrap()
}
fn build_constraint(
&self,
set: &TribleSet,
ctx: &mut VariableContext,
start: Variable<GenId>,
constraints: &mut Vec<Box<dyn Constraint<'static> + 'static>>,
) -> Variable<GenId> {
match self {
PathExpr::Attr(attr_id) => {
let a = ctx.next_variable::<GenId>();
let dest = ctx.next_variable::<GenId>();
constraints.push(Box::new(a.is(attr_id.to_value())));
constraints.push(Box::new(set.pattern(start, a, dest)));
dest
}
PathExpr::Concat(lhs, rhs) => {
let mid = lhs.build_constraint(set, ctx, start, constraints);
rhs.build_constraint(set, ctx, mid, constraints)
}
PathExpr::Union(..) | PathExpr::Star(..) | PathExpr::Plus(..) => {
unreachable!("closures and unions handled at eval_from level")
}
}
}
}
fn build_join(
set: &TribleSet,
expr: &PathExpr,
start: &RawId,
) -> (
IntersectionConstraint<Box<dyn Constraint<'static>>>,
VariableId,
) {
let mut ctx = VariableContext::new();
let start_var = ctx.next_variable::<GenId>();
let mut constraints: Vec<Box<dyn Constraint<'static> + 'static>> = Vec::new();
constraints.push(Box::new(start_var.is(start.to_value())));
let dest_var = expr.build_constraint(set, &mut ctx, start_var, &mut constraints);
(IntersectionConstraint::new(constraints), dest_var.index)
}
fn eval_attr(set: &TribleSet, attr: &RawId, start: &RawId) -> HashSet<RawId> {
let mut results = HashSet::new();
let mut prefix = [0u8; ID_LEN * 2];
prefix[..ID_LEN].copy_from_slice(start);
prefix[ID_LEN..].copy_from_slice(attr);
set.eav
.infixes::<{ ID_LEN * 2 }, 32, _>(&prefix, |value: &[u8; 32]| {
if value[..ID_LEN] == [0; ID_LEN] {
let dest: RawId = value[ID_LEN..].try_into().unwrap();
results.insert(dest);
}
});
results
}
fn eval_from(set: &TribleSet, expr: &PathExpr, start: &RawId) -> HashSet<RawId> {
match expr {
PathExpr::Attr(attr) => eval_attr(set, attr, start),
PathExpr::Concat(_, _) => {
let (constraint, dest_idx) = build_join(set, expr, start);
Query::new(constraint, move |binding: &Binding| {
let raw = binding.get(dest_idx)?;
id_from_value(raw)
})
.collect()
}
PathExpr::Union(lhs, rhs) => {
let mut results = eval_from(set, lhs, start);
results.extend(eval_from(set, rhs, start));
results
}
PathExpr::Plus(body) => {
let mut visited: HashSet<RawId> = HashSet::new();
let mut results: HashSet<RawId> = HashSet::new();
let mut frontier: VecDeque<RawId> = VecDeque::new();
frontier.push_back(*start);
visited.insert(*start);
while let Some(node) = frontier.pop_front() {
for dest in eval_from(set, body, &node) {
results.insert(dest);
if visited.insert(dest) {
frontier.push_back(dest);
}
}
}
results
}
PathExpr::Star(body) => {
let mut results = eval_from(set, &PathExpr::Plus(body.clone()), start);
results.insert(*start);
results
}
}
}
fn has_path(set: &TribleSet, expr: &PathExpr, from: &RawId, to: &RawId) -> bool {
match expr {
PathExpr::Attr(attr) => eval_attr(set, attr, from).contains(to),
PathExpr::Concat(_, _) => {
let (constraint, dest_idx) = build_join(set, expr, from);
Query::new(constraint, move |binding: &Binding| {
let raw = binding.get(dest_idx)?;
id_from_value(raw)
})
.any(|dest| dest == *to)
}
PathExpr::Union(lhs, rhs) => has_path(set, lhs, from, to) || has_path(set, rhs, from, to),
PathExpr::Plus(body) => {
let mut visited: HashSet<RawId> = HashSet::new();
let mut frontier: VecDeque<RawId> = VecDeque::new();
frontier.push_back(*from);
visited.insert(*from);
while let Some(node) = frontier.pop_front() {
for dest in eval_from(set, body, &node) {
if dest == *to {
return true;
}
if visited.insert(dest) {
frontier.push_back(dest);
}
}
}
false
}
PathExpr::Star(body) => {
if from == to {
return true;
}
has_path(set, &PathExpr::Plus(body.clone()), from, to)
}
}
}
fn estimate_from(set: &TribleSet, expr: &PathExpr, start: &RawId) -> usize {
let body = match expr {
PathExpr::Star(inner) | PathExpr::Plus(inner) => inner.as_ref(),
other => other,
};
match body {
PathExpr::Attr(attr) => {
let mut prefix = [0u8; ID_LEN * 2];
prefix[..ID_LEN].copy_from_slice(start);
prefix[ID_LEN..].copy_from_slice(attr);
set.eav.segmented_len(&prefix) as usize
}
PathExpr::Union(lhs, rhs) => {
estimate_from(set, lhs, start) + estimate_from(set, rhs, start)
}
_ => {
let (constraint, dest_idx) = build_join(set, body, start);
let mut binding = Binding::default();
binding.set(0, &start.to_value().raw);
constraint.estimate(dest_idx, &binding).unwrap_or(0)
}
}
}
pub struct RegularPathConstraint {
start: VariableId,
end: VariableId,
expr: PathExpr,
set: TribleSet,
}
impl RegularPathConstraint {
pub fn new(
set: TribleSet,
start: Variable<GenId>,
end: Variable<GenId>,
ops: &[PathOp],
) -> Self {
let expr = PathExpr::from_postfix(ops);
RegularPathConstraint {
start: start.index,
end: end.index,
expr,
set,
}
}
fn all_nodes(&self) -> Vec<RawValue> {
let mut node_set: HashSet<RawValue> = HashSet::new();
for t in self.set.iter() {
let v = &t.data[32..64];
if v[..ID_LEN] == [0; ID_LEN] {
let dest: RawId = v[ID_LEN..].try_into().unwrap();
node_set.insert(id_into_value(&dest));
let e: RawId = t.data[..ID_LEN].try_into().unwrap();
node_set.insert(id_into_value(&e));
}
}
node_set.into_iter().collect()
}
}
impl<'a> Constraint<'a> for RegularPathConstraint {
fn variables(&self) -> VariableSet {
let mut vars = VariableSet::new_empty();
vars.set(self.start);
vars.set(self.end);
vars
}
fn estimate(&self, variable: VariableId, binding: &Binding) -> Option<usize> {
if variable == self.end {
if let Some(start_val) = binding.get(self.start) {
if let Some(start_id) = id_from_value(start_val) {
return Some(estimate_from(&self.set, &self.expr, &start_id).max(1));
}
return Some(0);
}
Some(self.set.len())
} else if variable == self.start {
Some(self.set.len())
} else {
None
}
}
fn propose(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
if variable == self.end {
if let Some(start_val) = binding.get(self.start) {
if let Some(start_id) = id_from_value(start_val) {
let reachable = eval_from(&self.set, &self.expr, &start_id);
proposals.extend(reachable.iter().map(id_into_value));
}
return;
}
}
if variable == self.start || variable == self.end {
proposals.extend(self.all_nodes());
}
}
fn confirm(&self, variable: VariableId, binding: &Binding, proposals: &mut Vec<RawValue>) {
if variable == self.start {
if let Some(end_val) = binding.get(self.end) {
if let Some(end_id) = id_from_value(end_val) {
proposals.retain(|v| {
id_from_value(v)
.map_or(false, |sid| has_path(&self.set, &self.expr, &sid, &end_id))
});
} else {
proposals.clear();
}
}
} else if variable == self.end {
if let Some(start_val) = binding.get(self.start) {
if let Some(start_id) = id_from_value(start_val) {
proposals.retain(|v| {
id_from_value(v).map_or(false, |eid| {
has_path(&self.set, &self.expr, &start_id, &eid)
})
});
} else {
proposals.clear();
}
}
}
}
}