use std::{collections::BTreeMap, sync::Arc};
use lora_analyzer::symbols::VarId;
use lora_analyzer::ResolvedExpr;
use lora_ast::{Direction, RangeLiteral};
use lora_store::{GraphStorage, NodeId, RelationshipId};
use crate::errors::{value_kind, ExecResult, ExecutorError};
use crate::eval::eval_expr;
use crate::executor::{
bound_node_id_for_expand, bound_relationship_id_for_expand, resolve_range,
value_matches_property_value,
};
use crate::value::{LoraValue, Row};
use super::{RowSource, StreamCtx};
pub struct ExpandSource<'a, S: GraphStorage> {
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
src: VarId,
rel: Option<VarId>,
dst: VarId,
types: &'a [String],
direction: Direction,
rel_properties: Option<&'a ResolvedExpr>,
cur_row: Option<Row>,
cur_edges: Vec<(u64, NodeId)>,
cur_rel_property_filter: Option<BTreeMap<String, LoraValue>>,
cur_idx: usize,
}
impl<'a, S: GraphStorage> ExpandSource<'a, S> {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
src: VarId,
rel: Option<VarId>,
dst: VarId,
types: &'a [String],
direction: Direction,
rel_properties: Option<&'a ResolvedExpr>,
) -> Self {
Self {
upstream,
ctx,
src,
rel,
dst,
types,
direction,
rel_properties,
cur_row: None,
cur_edges: Vec::new(),
cur_rel_property_filter: None,
cur_idx: 0,
}
}
#[inline]
fn clear_current(&mut self) {
self.cur_row = None;
self.cur_edges.clear();
self.cur_rel_property_filter = None;
self.cur_idx = 0;
}
}
impl<'a, S: GraphStorage> RowSource for ExpandSource<'a, S> {
fn next_row(&mut self) -> ExecResult<Option<Row>> {
loop {
if self.cur_row.is_none() {
match self.upstream.next_row()? {
Some(row) => {
let Some(src_id) = bound_node_id_for_expand(&row, self.src)? else {
continue;
};
self.cur_edges =
self.ctx
.storage
.expand_ids(src_id, self.direction, self.types);
self.cur_rel_property_filter = match self.rel_properties {
Some(expr) if !self.cur_edges.is_empty() => {
let eval_ctx = self.ctx.eval_ctx();
let expected = eval_expr(expr, &row, &eval_ctx);
let LoraValue::Map(map) = expected else {
return Err(ExecutorError::ExpectedPropertyMap {
found: value_kind(&expected),
});
};
Some(map)
}
_ => None,
};
self.cur_idx = 0;
self.cur_row = Some(row);
}
None => return Ok(None),
}
}
let Some(row_ref) = self.cur_row.as_ref() else {
return Err(ExecutorError::RuntimeError(
"EXPAND cursor lost its current input row".into(),
));
};
while self.cur_idx < self.cur_edges.len() {
let (rel_id, dst_id) = self.cur_edges[self.cur_idx];
self.cur_idx += 1;
if self.rel_properties.is_some() {
let Some(map) = self.cur_rel_property_filter.as_ref() else {
continue;
};
let matches = self
.ctx
.storage
.with_relationship(rel_id, |rel| {
map.iter().all(|(k, v)| {
rel.properties
.get(k.as_str())
.map(|actual| value_matches_property_value(v, actual))
.unwrap_or(false)
})
})
.unwrap_or(false);
if !matches {
continue;
}
}
if let Some(id) = bound_node_id_for_expand(row_ref, self.dst)? {
if id != dst_id {
continue;
}
}
if let Some(rel_var) = self.rel {
if let Some(id) = bound_relationship_id_for_expand(row_ref, rel_var)? {
if id != rel_id {
continue;
}
}
}
let mut new_row = row_ref.clone();
if !new_row.contains_key(self.dst) {
new_row.insert(self.dst, LoraValue::Node(dst_id));
}
if let Some(rel_var) = self.rel {
if !new_row.contains_key(rel_var) {
new_row.insert(rel_var, LoraValue::Relationship(rel_id));
}
}
return Ok(Some(new_row));
}
self.clear_current();
}
}
}
#[derive(Debug)]
struct PathSegment {
rel: RelationshipId,
parent: Option<Arc<PathSegment>>,
}
impl PathSegment {
fn contains(self_: &Option<Arc<Self>>, rel_id: RelationshipId) -> bool {
let mut cur = self_.as_ref();
while let Some(node) = cur {
if node.rel == rel_id {
return true;
}
cur = node.parent.as_ref();
}
false
}
fn len(self_: &Option<Arc<Self>>) -> usize {
let mut cur = self_.as_ref();
let mut n = 0;
while let Some(node) = cur {
n += 1;
cur = node.parent.as_ref();
}
n
}
fn to_vec(self_: &Option<Arc<Self>>) -> Vec<RelationshipId> {
let len = Self::len(self_);
let mut out = Vec::with_capacity(len);
let mut cur = self_.as_ref();
while let Some(node) = cur {
out.push(node.rel);
cur = node.parent.as_ref();
}
out.reverse();
out
}
}
pub struct VariableLengthExpandSource<'a, S: GraphStorage> {
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
src: VarId,
rel: Option<VarId>,
dst: VarId,
types: &'a [String],
direction: Direction,
min_hops: u64,
max_hops: u64,
cur_row: Option<Row>,
pending_zero_hop: bool,
frontier: Vec<(NodeId, Option<Arc<PathSegment>>)>,
frontier_idx: usize,
next_frontier: Vec<(NodeId, Option<Arc<PathSegment>>)>,
depth: u64,
cur_path_node: Option<NodeId>,
cur_path: Option<Arc<PathSegment>>,
cur_edges: Vec<(u64, NodeId)>,
cur_edge_idx: usize,
}
impl<'a, S: GraphStorage> VariableLengthExpandSource<'a, S> {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
upstream: Box<dyn RowSource + 'a>,
ctx: StreamCtx<'a, S>,
src: VarId,
rel: Option<VarId>,
dst: VarId,
types: &'a [String],
direction: Direction,
range: &'a RangeLiteral,
) -> Self {
let (min_hops, max_hops) = resolve_range(range);
Self {
upstream,
ctx,
src,
rel,
dst,
types,
direction,
min_hops,
max_hops,
cur_row: None,
pending_zero_hop: false,
frontier: Vec::new(),
frontier_idx: 0,
next_frontier: Vec::new(),
depth: 1,
cur_path_node: None,
cur_path: None,
cur_edges: Vec::new(),
cur_edge_idx: 0,
}
}
fn start_row(&mut self, row: Row, src_id: NodeId) {
self.cur_row = Some(row);
self.pending_zero_hop = self.min_hops == 0;
self.reset_search_state();
self.frontier.push((src_id, None));
}
#[inline]
fn reset_search_state(&mut self) {
self.frontier.clear();
self.frontier_idx = 0;
self.next_frontier.clear();
self.depth = 1;
self.cur_path_node = None;
self.cur_path = None;
self.cur_edges.clear();
self.cur_edge_idx = 0;
}
fn clear_current_row(&mut self) {
self.cur_row = None;
self.pending_zero_hop = false;
self.reset_search_state();
}
fn row_for_path(&self, dst_node_id: NodeId, rel_ids: &[u64]) -> ExecResult<Row> {
let Some(cur_row) = self.cur_row.as_ref() else {
return Err(ExecutorError::RuntimeError(
"variable-length EXPAND cursor lost its current input row".into(),
));
};
let mut new_row = cur_row.clone();
new_row.insert(self.dst, LoraValue::Node(dst_node_id));
if let Some(rel_var) = self.rel {
let rels = rel_ids
.iter()
.copied()
.map(LoraValue::Relationship)
.collect();
new_row.insert(rel_var, LoraValue::List(rels));
}
Ok(new_row)
}
fn advance_frontier(&mut self) -> bool {
if self.next_frontier.is_empty() || self.depth >= self.max_hops {
return false;
}
std::mem::swap(&mut self.frontier, &mut self.next_frontier);
self.next_frontier.clear();
self.frontier_idx = 0;
self.depth += 1;
true
}
}
impl<'a, S: GraphStorage> RowSource for VariableLengthExpandSource<'a, S> {
fn next_row(&mut self) -> ExecResult<Option<Row>> {
loop {
if self.cur_row.is_none() {
match self.upstream.next_row()? {
Some(row) => {
let Some(src_id) = bound_node_id_for_expand(&row, self.src)? else {
continue;
};
self.start_row(row, src_id);
}
None => return Ok(None),
}
}
if self.pending_zero_hop {
self.pending_zero_hop = false;
let Some(cur_row) = self.cur_row.as_ref() else {
return Err(ExecutorError::RuntimeError(
"variable-length EXPAND cursor lost its zero-hop input row".into(),
));
};
if let Some(LoraValue::Node(src_id)) = cur_row.get(self.src) {
return Ok(Some(self.row_for_path(*src_id, &[])?));
}
}
loop {
if self.depth > self.max_hops {
self.clear_current_row();
break;
}
if self.cur_path_node.is_none() {
if self.frontier_idx >= self.frontier.len() {
if self.advance_frontier() {
continue;
}
self.clear_current_row();
break;
}
let (node_id, path) = &self.frontier[self.frontier_idx];
self.frontier_idx += 1;
self.cur_path_node = Some(*node_id);
self.cur_path = path.clone();
self.cur_edges =
self.ctx
.storage
.expand_ids(*node_id, self.direction, self.types);
self.cur_edge_idx = 0;
}
while self.cur_edge_idx < self.cur_edges.len() {
let (rel_id, neighbor_id) = self.cur_edges[self.cur_edge_idx];
self.cur_edge_idx += 1;
if PathSegment::contains(&self.cur_path, rel_id) {
continue;
}
if self.depth == self.max_hops && self.rel.is_none() {
if self.depth >= self.min_hops {
return Ok(Some(self.row_for_path(neighbor_id, &[])?));
}
continue;
}
let new_path = Arc::new(PathSegment {
rel: rel_id,
parent: self.cur_path.clone(),
});
if self.depth < self.max_hops {
self.next_frontier
.push((neighbor_id, Some(new_path.clone())));
}
if self.depth >= self.min_hops {
if self.rel.is_some() {
let rel_ids = PathSegment::to_vec(&Some(new_path));
return Ok(Some(self.row_for_path(neighbor_id, &rel_ids)?));
}
return Ok(Some(self.row_for_path(neighbor_id, &[])?));
}
}
self.cur_path_node = None;
self.cur_path = None;
self.cur_edges.clear();
self.cur_edge_idx = 0;
}
}
}
}