use super::ast::*;
use super::{executor, result};
use crate::datatypes::values::Value;
use crate::graph::core::pattern_matching::{
closure_probe, EdgeDirection, EdgePattern, NodePattern, Pattern, PatternElement,
PropertyMatcher,
};
use crate::graph::schema::DirGraph;
use crate::graph::storage::GraphRead;
fn estimate_match_rows(m: &MatchClause, graph: &DirGraph) -> Option<usize> {
let types = collect_node_types(m);
if types.is_empty() {
Some(graph.graph.node_count())
} else {
types.iter().map(|t| graph.label_cardinality(t)).min()
}
}
fn collect_node_types(m: &MatchClause) -> Vec<String> {
use crate::graph::core::pattern_matching::PatternElement;
let mut types = Vec::new();
for pattern in &m.patterns {
for element in &pattern.elements {
if let PatternElement::Node(np) = element {
for t in np.label_alternatives() {
types.push(t.clone());
}
}
}
}
types
}
fn explain_node_display(node: Option<&NodePattern>) -> String {
let Some(node) = node else {
return "()".to_string();
};
let Some(primary) = node.node_type.as_deref() else {
return "()".to_string();
};
if let Some(alts) = &node.alt_labels {
return format!("(:{})", alts.join("|"));
}
let mut out = format!("(:{primary}");
for label in &node.extra_labels {
out.push(':');
out.push_str(label);
}
out.push(')');
out
}
fn explain_edge_types(edge: &EdgePattern) -> String {
match edge.connection_types.as_deref() {
Some(types) if !types.is_empty() => format!(":{}", types.join("|")),
_ => edge
.connection_type
.as_deref()
.map(|t| format!(":{t}"))
.unwrap_or_default(),
}
}
fn var_length_expand_ops(patterns: &[Pattern]) -> Vec<String> {
let node_at = |pattern: &Pattern, idx: Option<usize>| -> String {
let node = idx
.and_then(|i| pattern.elements.get(i))
.and_then(|e| match e {
PatternElement::Node(n) => Some(n),
PatternElement::Edge(_) => None,
});
explain_node_display(node)
};
let mut ops = Vec::new();
for pattern in patterns {
for (i, element) in pattern.elements.iter().enumerate() {
let PatternElement::Edge(edge) = element else {
continue;
};
let Some((min, max)) = edge.var_length else {
continue;
};
let left = node_at(pattern, i.checked_sub(1));
let right = node_at(pattern, Some(i + 1));
let body = format!("[{}*{min}..{max}]", explain_edge_types(edge));
let rendered = match edge.direction {
EdgeDirection::Outgoing => format!("{left}-{body}->{right}"),
EdgeDirection::Incoming => format!("{left}<-{body}-{right}"),
EdgeDirection::Both => format!("{left}-{body}-{right}"),
};
ops.push(format!("Expand {rendered}"));
}
}
ops
}
fn closure_probe_ops(patterns: &[Pattern], graph: &DirGraph) -> Vec<String> {
let mut ops = Vec::new();
for pattern in patterns {
for element in &pattern.elements {
let PatternElement::Node(np) = element else {
continue;
};
if np.alt_labels.is_some() {
continue;
}
let (Some(node_type), Some(props)) = (np.node_type.as_deref(), np.properties.as_ref())
else {
continue;
};
let equality_props: Vec<&str> = props
.iter()
.filter(|(_, matcher)| matches!(matcher, PropertyMatcher::Equals(_)))
.map(|(name, _)| name.as_str())
.collect();
if let Some(members) =
closure_probe::closure_probe_members(graph, node_type, &equality_props)
{
ops.push(format!(
"ClosureProbe :{node_type} ({})",
members.join(", ")
));
}
}
}
ops
}
pub fn generate_explain_result(query: &CypherQuery, graph: &DirGraph) -> result::CypherResult {
let mut rows = Vec::new();
for clause in query.clauses.iter() {
let step = (rows.len() + 1) as i64;
let operation = executor::clause_display_name(clause);
let est = match clause {
Clause::Match(m) | Clause::OptionalMatch(m) => estimate_match_rows(m, graph)
.map(|e| Value::Int64(e as i64))
.unwrap_or(Value::Null),
Clause::FusedCountAll { .. }
| Clause::FusedCountAllEdges { .. }
| Clause::FusedMatchReturnAggregate { .. }
| Clause::FusedOptionalMatchAggregate { .. }
| Clause::FusedCountTypedEdge { .. }
| Clause::FusedCountAnchoredEdges { .. } => Value::Int64(1),
Clause::FusedCountTypedNode { node_type, .. } => {
let n = graph
.type_indices
.get(node_type.as_str())
.map_or(0, |v| v.len());
Value::Int64(n.min(1) as i64)
}
Clause::FusedCountLabelUnion { .. } => Value::Int64(1),
Clause::FusedCountByType { .. } => Value::Int64(graph.type_indices.len() as i64),
Clause::FusedVectorScoreTopK { limit, .. }
| Clause::FusedTextBm25TopK { limit, .. }
| Clause::FusedOrderByTopK { limit, .. }
| Clause::FusedNodeScanTopK { limit, .. } => Value::Int64(*limit as i64),
_ => Value::Null,
};
rows.push(vec![Value::Int64(step), Value::String(operation), est]);
if let Clause::Match(m) | Clause::OptionalMatch(m) = clause {
for op in closure_probe_ops(&m.patterns, graph)
.into_iter()
.chain(var_length_expand_ops(&m.patterns))
{
rows.push(vec![
Value::Int64((rows.len() + 1) as i64),
Value::String(op),
Value::Null,
]);
}
}
}
for pass in &query.optimizer_tags {
rows.push(vec![
Value::Int64((rows.len() + 1) as i64),
Value::String(format!("OptimizerPass {pass}")),
Value::Null,
]);
}
result::CypherResult {
columns: vec!["step".into(), "operation".into(), "estimated_rows".into()],
rows,
stats: None,
profile: None,
diagnostics: None,
lazy: None,
}
}
#[cfg(test)]
mod var_length_expand_tests {
use super::{var_length_expand_ops, Clause};
fn ops(query: &str) -> Vec<String> {
let parsed = super::super::parser::parse_cypher(query).expect("parse");
parsed
.clauses
.iter()
.filter_map(|c| match c {
Clause::Match(m) | Clause::OptionalMatch(m) => Some(m),
_ => None,
})
.flat_map(|m| var_length_expand_ops(&m.patterns))
.collect()
}
#[test]
fn renders_one_row_per_var_length_edge() {
assert_eq!(
ops("MATCH (a:Person)-[:KNOWS*2..3]->(b:Person) RETURN a"),
["Expand (:Person)-[:KNOWS*2..3]->(:Person)"]
);
}
#[test]
fn fixed_length_edges_produce_no_row() {
assert!(ops("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a").is_empty());
}
#[test]
fn every_var_length_edge_appears_in_pattern_order() {
assert_eq!(
ops("MATCH (a:A)-[:R*1..2]->(b:B)-[:S*3..4]->(c) RETURN a"),
["Expand (:A)-[:R*1..2]->(:B)", "Expand (:B)-[:S*3..4]->()",]
);
}
#[test]
fn renders_direction_types_and_the_star_default_bounds() {
assert_eq!(
ops("MATCH (a:Person)<-[:KNOWS|LIKES*2..3]-(b) RETURN a"),
["Expand (:Person)<-[:KNOWS|LIKES*2..3]-()"]
);
assert_eq!(ops("MATCH (a)-[*]-(b) RETURN a"), ["Expand ()-[*1..10]-()"]);
}
}