use super::super::ast::{MatchQuery, PatternChain};
use super::core::{MatchExecCtx, bind_node, binding_compatible, execute_triple};
use super::expansion::{VarLenCaps, VarLenCursor, VarLenPattern, resume_variable_length};
use super::overlay_expand;
use super::predicates;
use super::predicates::PropertyLookup;
use super::types::{BindingRow, ContinuationSeed, ExecutionState, MatchOutcome, VarLenResume};
use super::varlen_named::{self, NameOrId};
use crate::engine::graph::csr::{CsrIndex, GraphOverlayDelta};
use crate::engine::graph::edge_store::EdgeStore;
pub(super) fn run_chain_from(
chain: &PatternChain,
start_idx: usize,
initial_rows: Vec<BindingRow>,
csr: &CsrIndex,
state: &mut ExecutionState,
frontier_bitmap: Option<&nodedb_types::SurrogateBitmap>,
overlay: Option<&GraphOverlayDelta>,
) -> Result<Vec<BindingRow>, crate::Error> {
let mut rows = initial_rows;
for (triple_idx, triple) in chain.triples.iter().enumerate().skip(start_idx) {
let mut next_rows = Vec::new();
for row in &rows {
next_rows.extend(execute_triple(
triple,
triple_idx,
csr,
row,
state,
frontier_bitmap,
overlay,
)?);
}
rows = next_rows;
if rows.is_empty() {
break;
}
}
Ok(rows)
}
pub(super) fn finalize_rows(
query: &MatchQuery,
mut rows: Vec<BindingRow>,
csr: &CsrIndex,
edge_store: &EdgeStore,
varlen_caps: VarLenCaps,
props: &PropertyLookup<'_>,
overlay: Option<&GraphOverlayDelta>,
) -> Result<Vec<BindingRow>, crate::Error> {
for predicate in &query.where_predicates {
rows = predicates::apply_predicate(
&rows,
predicate,
csr,
edge_store,
varlen_caps,
props,
overlay,
)?;
}
if let Some(limit) = query.limit {
rows.truncate(limit);
}
if !query.return_columns.is_empty() {
rows = predicates::project_columns(&rows, &query.return_columns, props)?;
}
if query.distinct {
let mut seen = std::collections::HashSet::new();
rows.retain(|row| {
let mut pairs: Vec<(&String, &String)> = row.iter().collect();
pairs.sort_unstable_by_key(|(k, _)| *k);
let key = format!("{pairs:?}");
seen.insert(key)
});
}
Ok(rows)
}
pub fn execute_continuation<'a>(
query: &MatchQuery,
ctx: MatchExecCtx<'a>,
seed: ContinuationSeed,
) -> Result<MatchOutcome, crate::Error> {
let MatchExecCtx {
csr,
edge_store,
frontier_bitmap,
is_remote_node,
varlen_caps,
props,
overlay,
} = ctx;
let chain = match query.clauses.as_slice() {
[clause] if clause.patterns.len() == 1 => &clause.patterns[0],
_ => {
return Err(crate::Error::BadRequest {
detail: "cross-shard MATCH continuation is only supported for a single \
MATCH clause with a single pattern chain; multi-clause / \
multi-pattern continuation is not yet supported"
.to_string(),
});
}
};
if seed.triple_idx > chain.triples.len() {
return Err(crate::Error::BadRequest {
detail: format!(
"cross-shard MATCH continuation resume_triple_idx {} \
exceeds chain length {}",
seed.triple_idx,
chain.triples.len()
),
});
}
let mut state = ExecutionState::new(is_remote_node, varlen_caps);
state.collection_filter =
super::expansion::resolve_collection_filter(query.collection.as_deref(), csr);
let rows = run_chain_from(
chain,
seed.triple_idx,
vec![seed.seed_row],
csr,
&mut state,
frontier_bitmap,
overlay,
)?;
let rows = finalize_rows(
query,
rows,
csr,
edge_store,
state.varlen_caps,
props,
overlay,
)?;
Ok(MatchOutcome {
rows,
truncation: state.varlen_resume,
unresolved_frontier: state.frontier,
})
}
pub fn execute_varlen_resume<'a>(
query: &MatchQuery,
ctx: MatchExecCtx<'a>,
resume: VarLenResume,
) -> Result<MatchOutcome, crate::Error> {
let MatchExecCtx {
csr,
edge_store,
frontier_bitmap,
is_remote_node,
varlen_caps,
props,
overlay,
} = ctx;
let chain = match query.clauses.as_slice() {
[clause] if clause.patterns.len() == 1 => &clause.patterns[0],
_ => {
return Err(crate::Error::BadRequest {
detail: "cross-shard variable-length MATCH resume is only supported for a single \
MATCH clause with a single pattern chain"
.to_string(),
});
}
};
let triple = chain
.triples
.get(resume.triple_idx)
.ok_or_else(|| crate::Error::BadRequest {
detail: format!(
"variable-length MATCH resume triple_idx {} exceeds chain length {}",
resume.triple_idx,
chain.triples.len()
),
})?;
if !triple.edge.is_variable_length() {
return Err(crate::Error::BadRequest {
detail: format!(
"variable-length MATCH resume targets triple {} which is not a \
variable-length edge",
resume.triple_idx
),
});
}
let mut state = ExecutionState::new(is_remote_node, varlen_caps);
state.collection_filter =
super::expansion::resolve_collection_filter(query.collection.as_deref(), csr);
let want_path = triple.edge.name.is_some();
let pattern = VarLenPattern {
label_filter: triple.edge.edge_type.as_deref(),
direction: triple.edge.direction.to_csr_direction(),
min_hops: triple.edge.min_hops,
max_hops: triple.edge.max_hops,
want_path,
collection_filter: state.collection_filter,
};
let cursor = VarLenCursor {
frontier: resume.frontier,
depth: resume.depth,
};
let expansion = resume_variable_length(csr, &cursor, &pattern, varlen_caps, overlay);
if let Some(next_cursor) = expansion.cursor {
state.record_truncation(VarLenResume {
triple_idx: resume.triple_idx,
source_row: resume.source_row.clone(),
frontier: next_cursor.frontier,
depth: next_cursor.depth,
});
}
varlen_named::record_boundary_resumes(
&mut state,
resume.triple_idx,
&resume.source_row,
&expansion.boundary,
);
let src_binding = &triple.src;
let dst_binding = &triple.dst;
let mut resumed_rows: Vec<BindingRow> = Vec::new();
let bind_source = |row: &mut BindingRow| {
if let Some(src_name) = src_binding.name.as_deref()
&& let Some(src_value) = resume.source_row.get(src_name)
&& let Some(src_id) = csr.node_id_raw(src_value)
{
bind_node(row, src_binding, csr, src_id);
}
};
for (dst_id, path) in expansion.results {
if !binding_compatible(dst_binding, csr, &resume.source_row, dst_id) {
continue;
}
let mut row = resume.source_row.clone();
bind_source(&mut row);
bind_node(&mut row, dst_binding, csr, dst_id);
if let Some(ref edge_name) = triple.edge.name {
row.insert(edge_name.clone(), path);
}
resumed_rows.push(row);
}
for (bound, path) in expansion.named_results {
match bound {
NameOrId::Id(dst_id) => {
if !binding_compatible(dst_binding, csr, &resume.source_row, dst_id) {
continue;
}
let mut row = resume.source_row.clone();
bind_source(&mut row);
bind_node(&mut row, dst_binding, csr, dst_id);
if let Some(ref edge_name) = triple.edge.name {
row.insert(edge_name.clone(), path);
}
resumed_rows.push(row);
}
NameOrId::Name(dst_name) => {
if !overlay_expand::dst_compatible(dst_binding, csr, &resume.source_row, &dst_name)
{
continue;
}
let mut row = resume.source_row.clone();
bind_source(&mut row);
overlay_expand::bind_name(&mut row, dst_binding, &dst_name);
if let Some(ref edge_name) = triple.edge.name {
row.insert(edge_name.clone(), path);
}
resumed_rows.push(row);
}
}
}
let rows = run_chain_from(
chain,
resume.triple_idx + 1,
resumed_rows,
csr,
&mut state,
frontier_bitmap,
overlay,
)?;
let rows = finalize_rows(
query,
rows,
csr,
edge_store,
state.varlen_caps,
props,
overlay,
)?;
Ok(MatchOutcome {
rows,
truncation: state.varlen_resume,
unresolved_frontier: state.frontier,
})
}
#[cfg(test)]
mod tests {
use super::super::core::MatchExecCtx;
use super::super::core::execute;
use super::super::core::tests::{make_csr, make_sparse, props_for};
use super::super::expansion::{VarLenCaps, VarLenPattern, expand_variable_length};
use super::super::types::{BindingRow, ContinuationSeed, VarLenResume};
use super::{execute_continuation, execute_varlen_resume};
use crate::engine::graph::edge_store::Direction;
#[test]
fn varlen_resume_zerompk_round_trip() {
let mut source_row = BindingRow::new();
source_row.insert("a".to_string(), "n0".to_string());
source_row.insert("x".to_string(), "anchor".to_string());
let resume = VarLenResume {
triple_idx: 2,
source_row,
frontier: vec![
("n3".to_string(), "n0->n1->n3".to_string()),
("n7".to_string(), "n0->n2->n7".to_string()),
("n11".to_string(), String::new()),
("n0".to_string(), "n0".to_string()),
],
depth: 4,
};
let bytes = zerompk::to_msgpack_vec(&resume).expect("serialize VarLenResume");
let decoded: VarLenResume =
zerompk::from_msgpack(&bytes).expect("deserialize VarLenResume");
assert_eq!(decoded, resume, "VarLenResume must round-trip via zerompk");
}
#[test]
fn varlen_resume_handler_union_equals_uncapped_match() {
let edges: Vec<(String, String, String)> = (0..6)
.map(|i| (format!("n{i}"), "l".to_string(), format!("n{}", i + 1)))
.collect();
let edge_refs: Vec<(&str, &str, &str)> = edges
.iter()
.map(|(s, l, d)| (s.as_str(), l.as_str(), d.as_str()))
.collect();
let (csr, store, _dir) = make_csr(&edge_refs);
let (sparse, _sdir) = make_sparse();
let props = props_for(&sparse, &csr);
let query = super::super::super::compiler::parse(
"MATCH (a)-[:l*1..6]->(b) WHERE a = 'n0' RETURN a, b",
)
.unwrap();
let full_b: std::collections::HashSet<String> = execute(
&query,
MatchExecCtx {
csr: &csr,
edge_store: &store,
frontier_bitmap: None,
is_remote_node: None,
varlen_caps: VarLenCaps::default(),
props: &props,
overlay: None,
},
)
.unwrap()
.rows
.into_iter()
.map(|r| r["b"].clone())
.collect();
assert_eq!(full_b.len(), 6, "uncapped MATCH reaches n1..n6 from n0");
let src = csr.node_id_raw("n0").unwrap();
let pat = VarLenPattern {
label_filter: Some("l"),
direction: Direction::Out,
min_hops: 1,
max_hops: 6,
want_path: false,
collection_filter: super::super::expansion::CollectionFilter::Unscoped,
};
let caps = VarLenCaps {
max_results: 2,
max_frontier: usize::MAX,
};
let first = expand_variable_length(&csr, src, &pat, caps, None);
let cursor = first.cursor.clone().expect("low cap must truncate");
let mut source_row = BindingRow::new();
source_row.insert("a".to_string(), "n0".to_string());
let mut union_b: std::collections::HashSet<String> = first
.results
.iter()
.map(|(dst, _)| csr.node_name_raw(*dst).to_string())
.collect();
let mut next = Some(VarLenResume {
triple_idx: 0,
source_row: source_row.clone(),
frontier: cursor.frontier,
depth: cursor.depth,
});
while let Some(resume) = next.take() {
let outcome = execute_varlen_resume(
&query,
MatchExecCtx {
csr: &csr,
edge_store: &store,
frontier_bitmap: None,
is_remote_node: None,
varlen_caps: VarLenCaps::default(),
props: &props,
overlay: None,
},
resume,
)
.unwrap();
for row in &outcome.rows {
assert_eq!(row["a"], "n0", "source binding carried through resume");
union_b.insert(row["b"].clone());
}
next = outcome.truncation.into_iter().next().map(|t| VarLenResume {
triple_idx: 0,
source_row: source_row.clone(),
frontier: t.frontier,
depth: t.depth,
});
}
assert_eq!(
union_b, full_b,
"first-pass ∪ resumed `b` bindings must equal the uncapped MATCH set"
);
}
#[test]
fn continuation_resumes_tail_with_seed_bindings() {
let (csr, store, _dir) = make_csr(&[("root", "E", "mid"), ("mid", "E", "leaf")]);
let (sparse, _sdir) = make_sparse();
let props = props_for(&sparse, &csr);
let query =
super::super::super::compiler::parse("MATCH (x)-[:E]->(y)-[:E]->(z) RETURN x, y, z")
.unwrap();
let mut seed = BindingRow::new();
seed.insert("x".to_string(), "root".to_string());
seed.insert("y".to_string(), "mid".to_string());
let outcome = execute_continuation(
&query,
MatchExecCtx {
csr: &csr,
edge_store: &store,
frontier_bitmap: None,
is_remote_node: None,
varlen_caps: VarLenCaps::default(),
props: &props,
overlay: None,
},
ContinuationSeed {
triple_idx: 1,
seed_row: seed,
},
)
.unwrap();
assert_eq!(outcome.rows.len(), 1, "expected exactly one tail row");
assert_eq!(
outcome.rows[0]["x"], "root",
"seed binding x carried through"
);
assert_eq!(
outcome.rows[0]["y"], "mid",
"seed binding y carried through"
);
assert_eq!(outcome.rows[0]["z"], "leaf", "tail resolved z=leaf");
assert!(outcome.unresolved_frontier.is_empty());
}
#[test]
fn continuation_no_matching_tail_edge_is_empty() {
let (csr, store, _dir) = make_csr(&[("root", "E", "mid")]);
let (sparse, _sdir) = make_sparse();
let props = props_for(&sparse, &csr);
let query =
super::super::super::compiler::parse("MATCH (x)-[:E]->(y)-[:E]->(z) RETURN x, y, z")
.unwrap();
let mut seed = BindingRow::new();
seed.insert("x".to_string(), "root".to_string());
seed.insert("y".to_string(), "mid".to_string());
let outcome = execute_continuation(
&query,
MatchExecCtx {
csr: &csr,
edge_store: &store,
frontier_bitmap: None,
is_remote_node: None,
varlen_caps: VarLenCaps::default(),
props: &props,
overlay: None,
},
ContinuationSeed {
triple_idx: 1,
seed_row: seed,
},
)
.unwrap();
assert!(
outcome.rows.is_empty(),
"mid has no local out-edge; tail must be empty, got {:?}",
outcome.rows
);
}
#[test]
fn full_execute_unchanged_after_refactor() {
let (csr, store, _dir) = make_csr(&[("root", "E", "mid"), ("mid", "E", "leaf")]);
let (sparse, _sdir) = make_sparse();
let props = props_for(&sparse, &csr);
let query = super::super::super::compiler::parse(
"MATCH (x)-[:E]->(y)-[:E]->(z) WHERE x = 'root' RETURN x, y, z",
)
.unwrap();
let rows = execute(
&query,
MatchExecCtx {
csr: &csr,
edge_store: &store,
frontier_bitmap: None,
is_remote_node: None,
varlen_caps: VarLenCaps::default(),
props: &props,
overlay: None,
},
)
.unwrap()
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["x"], "root");
assert_eq!(rows[0]["y"], "mid");
assert_eq!(rows[0]["z"], "leaf");
}
}