use super::super::ast::*;
use crate::graph::core::pattern_matching::{PatternElement, PropertyMatcher};
use crate::graph::schema::DirGraph;
use crate::graph::storage::GraphRead;
use std::collections::{HashMap, HashSet};
pub(super) fn optimize_pattern_start_node(query: &mut CypherQuery, graph: &DirGraph) {
use crate::graph::core::pattern_matching::EdgeDirection;
let mut bound_vars: HashSet<String> = HashSet::new();
for clause in &mut query.clauses {
let (patterns, path_assignments) = match clause {
Clause::Match(m) => (&mut m.patterns, &m.path_assignments),
Clause::OptionalMatch(m) => (&mut m.patterns, &m.path_assignments),
_ => continue,
};
for (pi, pattern) in patterns.iter_mut().enumerate() {
if pattern.elements.len() < 3 {
continue;
}
if path_assignments.iter().any(|pa| pa.pattern_index == pi) {
continue;
}
let first_node = match &pattern.elements[0] {
PatternElement::Node(np) => np,
_ => continue,
};
let last_node = match pattern.elements.last() {
Some(PatternElement::Node(np)) => np,
_ => continue,
};
let first_sel = estimate_node_selectivity_in_context(first_node, graph, &bound_vars);
let last_sel = estimate_node_selectivity_in_context(last_node, graph, &bound_vars);
if last_sel.saturating_mul(5) >= first_sel {
continue;
}
pattern.elements.reverse();
for elem in &mut pattern.elements {
if let PatternElement::Edge(ep) = elem {
ep.direction = match ep.direction {
EdgeDirection::Outgoing => EdgeDirection::Incoming,
EdgeDirection::Incoming => EdgeDirection::Outgoing,
EdgeDirection::Both => EdgeDirection::Both,
};
}
}
}
for pattern in patterns.iter() {
for elem in &pattern.elements {
if let PatternElement::Node(np) = elem {
if let Some(ref v) = np.variable {
bound_vars.insert(v.clone());
}
}
}
}
}
}
fn estimate_node_selectivity_in_context(
np: &crate::graph::core::pattern_matching::NodePattern,
graph: &DirGraph,
bound_vars: &HashSet<String>,
) -> usize {
if let Some(ref v) = np.variable {
if bound_vars.contains(v) {
return 1;
}
}
estimate_node_selectivity(np, graph)
}
pub(super) fn estimate_node_selectivity(
np: &crate::graph::core::pattern_matching::NodePattern,
graph: &DirGraph,
) -> usize {
let (type_count, secondary_count) = np.node_type.as_ref().map_or_else(
|| (GraphRead::node_count(&graph.graph), 0),
|node_type| {
let primary = graph
.type_indices
.get(node_type)
.map_or(0, |indices| indices.len());
let secondary = graph
.secondary_label_index
.get(&crate::graph::schema::InternedKey::from_str(node_type))
.map_or(0, Vec::len);
(primary.saturating_add(secondary), secondary)
},
);
let unconstrained = np.node_type.is_none();
match &np.properties {
None if unconstrained => usize::MAX,
None => type_count.max(1),
Some(props) if props.is_empty() && unconstrained => usize::MAX,
Some(props) if props.is_empty() => type_count.max(1),
Some(props) => {
for (prop, matcher) in props {
if prop == "id" {
match matcher {
PropertyMatcher::Equals(_) | PropertyMatcher::EqualsParam(_) => {
return 1usize.saturating_add(secondary_count);
}
PropertyMatcher::In(vals) => {
if vals.is_empty() {
return 0;
}
let unique: HashSet<_> = vals.iter().collect();
if let Some(node_type) = np.node_type.as_deref() {
let hits: HashSet<_> = unique
.into_iter()
.filter_map(|value| {
graph.lookup_by_id_readonly(node_type, value)
})
.collect();
return hits.len().saturating_add(secondary_count);
}
return unique.len();
}
_ => {}
}
}
}
if let Some(ref nt) = np.node_type {
for (prop, matcher) in props {
match matcher {
PropertyMatcher::Equals(val) => {
let key = (nt.clone(), prop.clone());
if graph.property_indices.contains_key(&key) {
if let Some(results) = graph.lookup_by_index(nt, prop, val) {
return results.len().saturating_add(secondary_count).max(1);
}
return 1;
}
}
PropertyMatcher::In(vals) => {
if vals.is_empty() {
return 0;
}
let key = (nt.clone(), prop.clone());
if graph.property_indices.contains_key(&key) {
let mut hits = HashSet::new();
for value in vals {
if let Some(indices) = graph.lookup_by_index(nt, prop, value) {
hits.extend(indices);
}
}
return hits.len().saturating_add(secondary_count);
}
}
_ => {}
}
}
}
let mut est = type_count;
for (prop, matcher) in props {
match matcher {
PropertyMatcher::Equals(_) | PropertyMatcher::EqualsParam(_) => {
match np
.node_type
.as_ref()
.and_then(|nt| graph.property_ndv(nt, prop))
{
Some(ndv) => est /= ndv.max(1),
None => est /= 100,
}
}
PropertyMatcher::In(values) => {
if values.is_empty() {
return 0;
}
let unique_count = values.iter().collect::<HashSet<_>>().len();
let reduced = match np
.node_type
.as_ref()
.and_then(|nt| graph.property_ndv(nt, prop))
{
Some(ndv) => est
.saturating_mul(unique_count.min(ndv))
.div_ceil(ndv.max(1)),
None => est / 10,
};
let scan_cost_floor = type_count.div_ceil(10);
est = reduced.max(scan_cost_floor).max(1);
}
_ => est /= 10,
}
}
est.max(1)
}
}
}
pub(super) fn reorder_match_clauses(query: &mut CypherQuery, graph: &DirGraph) {
if query.clauses.len() < 3
|| !query.clauses.windows(2).any(|pair| {
matches!(&pair[0], Clause::Match(m) if m.path_assignments.is_empty())
&& matches!(&pair[1], Clause::Match(m) if m.path_assignments.is_empty())
})
{
return;
}
let edge_counts = graph
.has_edge_type_counts_cache()
.then(|| graph.get_edge_type_counts());
let triple_counts: Option<HashMap<(String, String, String), usize>> =
if edge_counts.is_some() && graph.has_type_connectivity_cache() {
graph.get_type_connectivity().map(|triples| {
triples
.into_iter()
.map(|t| ((t.src, t.conn, t.tgt), t.count))
.collect()
})
} else {
None
};
let mut i = 0;
while i < query.clauses.len() {
let mut j = i;
while j < query.clauses.len() {
match &query.clauses[j] {
Clause::Match(m) if m.path_assignments.is_empty() => j += 1,
_ => break,
}
}
if j - i < 2 {
i = j.max(i + 1);
continue;
}
if !shares_variable_across(&query.clauses[i..j]) {
i = j;
continue;
}
let costs = edge_counts.as_ref().and_then(|counts| {
(i..j)
.map(|k| match &query.clauses[k] {
Clause::Match(m) => estimate_match_edge_cost(m, counts, triple_counts.as_ref()),
_ => unreachable!(),
})
.collect::<Option<Vec<_>>>()
});
let mut order: Vec<usize> = (i..j).collect();
if let Some(costs) = costs {
order.sort_by_key(|&absolute| (costs[absolute - i], absolute));
} else {
order.sort_by_key(|&absolute| {
let Clause::Match(m) = &query.clauses[absolute] else {
unreachable!()
};
(!match_is_id_anchored(m), absolute)
});
}
let already_sorted = order
.iter()
.enumerate()
.all(|(offset, &absolute)| i + offset == absolute);
if !already_sorted {
let extracted: Vec<Clause> = query.clauses.drain(i..j).collect();
for (offset, &absolute) in order.iter().enumerate() {
query
.clauses
.insert(i + offset, extracted[absolute - i].clone());
}
}
i = j;
}
}
fn match_is_id_anchored(m: &MatchClause) -> bool {
!m.patterns.is_empty()
&& m.patterns.iter().all(|pattern| {
pattern.elements.first().is_some_and(is_id_anchored)
|| pattern.elements.last().is_some_and(is_id_anchored)
})
}
fn estimate_match_edge_cost(
m: &MatchClause,
edge_counts: &HashMap<String, usize>,
triple_counts: Option<&HashMap<(String, String, String), usize>>,
) -> Option<usize> {
let mut total: usize = 0;
for pattern in &m.patterns {
if pattern.elements.len() < 3 {
return None;
}
let (first, last) = match (pattern.elements.first(), pattern.elements.last()) {
(Some(first), Some(last)) => (first, last),
_ => return None,
};
if !is_id_anchored(first) && !is_id_anchored(last) {
return None;
}
let elems = &pattern.elements;
for idx in 0..elems.len() {
let ep = match &elems[idx] {
PatternElement::Edge(ep) => ep,
_ => continue,
};
let ct = ep.connection_type.as_ref()?;
let mut count: Option<usize> = None;
if let Some(triples) = triple_counts {
let src_label = idx
.checked_sub(1)
.and_then(|i| elems.get(i))
.and_then(node_label);
let tgt_label = elems.get(idx + 1).and_then(node_label);
if let (Some(sl), Some(tl)) = (src_label, tgt_label) {
let key_fwd = (sl.clone(), ct.clone(), tl.clone());
let key_rev = (tl, ct.clone(), sl);
let fwd = triples.get(&key_fwd).copied().unwrap_or(0);
let rev = triples.get(&key_rev).copied().unwrap_or(0);
if fwd > 0 || rev > 0 {
count = Some(fwd + rev);
}
}
}
let resolved = match count {
Some(c) => c,
None => *edge_counts.get(ct)?,
};
total = total.saturating_add(resolved);
}
}
Some(total)
}
fn node_label(elem: &PatternElement) -> Option<String> {
let np = match elem {
PatternElement::Node(np) => np,
_ => return None,
};
np.node_type.clone()
}
fn is_id_anchored(elem: &PatternElement) -> bool {
let np = match elem {
PatternElement::Node(np) => np,
_ => return false,
};
let props = match &np.properties {
Some(p) => p,
None => return false,
};
props.iter().any(|(prop, matcher)| {
prop == "id"
&& matches!(
matcher,
PropertyMatcher::Equals(_) | PropertyMatcher::EqualsParam(_)
)
})
}
fn shares_variable_across(clauses: &[Clause]) -> bool {
let mut common: Option<HashSet<String>> = None;
for clause in clauses {
let m = match clause {
Clause::Match(m) => m,
_ => return false,
};
let vars: HashSet<String> = m
.patterns
.iter()
.flat_map(|p| p.elements.iter())
.filter_map(|e| match e {
PatternElement::Node(np) => np.variable.clone(),
_ => None,
})
.collect();
common = Some(match common {
None => vars,
Some(prev) => prev.intersection(&vars).cloned().collect(),
});
}
common.is_some_and(|s| !s.is_empty())
}
pub(super) fn reorder_match_patterns(query: &mut CypherQuery, graph: &DirGraph) {
let mut bound_vars: HashSet<String> = HashSet::new();
for clause in &mut query.clauses {
let mc = match clause {
Clause::Match(mc) => mc,
Clause::OptionalMatch(mc) => {
for pat in mc.patterns.iter() {
for elem in &pat.elements {
if let PatternElement::Node(np) = elem {
if let Some(ref v) = np.variable {
bound_vars.insert(v.clone());
}
}
}
}
continue;
}
_ => continue,
};
if mc.patterns.len() < 2 || !mc.path_assignments.is_empty() {
for pat in mc.patterns.iter() {
for elem in &pat.elements {
if let PatternElement::Node(np) = elem {
if let Some(ref v) = np.variable {
bound_vars.insert(v.clone());
}
}
}
}
continue;
}
let mut pattern_scores: Vec<(usize, usize)> = mc
.patterns
.iter()
.enumerate()
.map(|(i, pat)| {
let sel = if let Some(PatternElement::Node(np)) = pat.elements.first() {
estimate_node_selectivity_in_context(np, graph, &bound_vars)
} else {
usize::MAX
};
(i, sel)
})
.collect();
pattern_scores.sort_by_key(|&(_, sel)| sel);
let already_ordered = pattern_scores
.iter()
.enumerate()
.all(|(pos, &(idx, _))| pos == idx);
if !already_ordered {
let old_patterns = std::mem::take(&mut mc.patterns);
mc.patterns = pattern_scores
.iter()
.map(|&(idx, _)| old_patterns[idx].clone())
.collect();
}
for pat in mc.patterns.iter() {
for elem in &pat.elements {
if let PatternElement::Node(np) = elem {
if let Some(ref v) = np.variable {
bound_vars.insert(v.clone());
}
}
}
}
}
}
struct SimpleCycle {
nodes: Vec<crate::graph::core::pattern_matching::NodePattern>,
edges: Vec<crate::graph::core::pattern_matching::EdgePattern>,
k: usize,
}
impl SimpleCycle {
fn detect(elements: &[PatternElement]) -> Option<SimpleCycle> {
if elements.len() < 5 || elements.len().is_multiple_of(2) {
return None;
}
let k = elements.len() / 2; if k < 2 {
return None;
}
let (first_var, last_var) = match (&elements[0], elements.last()?) {
(PatternElement::Node(a), PatternElement::Node(b)) => {
(a.variable.as_ref()?, b.variable.as_ref()?)
}
_ => return None,
};
if first_var != last_var {
return None;
}
let mut nodes = Vec::with_capacity(k);
let mut edges = Vec::with_capacity(k);
for (i, el) in elements.iter().enumerate() {
match (i % 2, el) {
(0, PatternElement::Node(np)) if i + 1 < elements.len() => nodes.push(np.clone()),
(0, PatternElement::Node(_)) => {} (1, PatternElement::Edge(ep)) => edges.push(ep.clone()),
_ => return None,
}
}
if nodes.len() != k || edges.len() != k {
return None;
}
let mut seen = HashSet::with_capacity(k);
for np in &nodes {
let v = np.variable.as_ref()?;
if !seen.insert(v.clone()) {
return None;
}
}
for ep in &edges {
if ep.variable.is_some()
|| ep.connection_type.is_none()
|| ep.connection_types.is_some()
|| ep.var_length.is_some()
|| ep.properties.is_some()
|| ep.edge_filter.is_some()
{
return None;
}
}
Some(SimpleCycle { nodes, edges, k })
}
fn linearize(
&self,
root: usize,
edge_counts: Option<&HashMap<String, usize>>,
) -> Vec<PatternElement> {
use crate::graph::core::pattern_matching::EdgeDirection;
let k = self.k;
let cost = |ep: &crate::graph::core::pattern_matching::EdgePattern| -> usize {
match (edge_counts, ep.connection_type.as_ref()) {
(Some(m), Some(ct)) => m.get(ct).copied().unwrap_or(usize::MAX),
_ => 0, }
};
let forward = cost(&self.edges[root]) <= cost(&self.edges[(root + k - 1) % k]);
let flip = |mut ep: crate::graph::core::pattern_matching::EdgePattern| {
ep.direction = match ep.direction {
EdgeDirection::Outgoing => EdgeDirection::Incoming,
EdgeDirection::Incoming => EdgeDirection::Outgoing,
EdgeDirection::Both => EdgeDirection::Both,
};
ep
};
let mut out = Vec::with_capacity(2 * k + 1);
out.push(PatternElement::Node(self.nodes[root].clone()));
for m in 0..k {
let (edge_idx, next_node) = if forward {
((root + m) % k, (root + m + 1) % k)
} else {
let ej = (root + k - 1 - m) % k;
(ej, ej) };
let ep = self.edges[edge_idx].clone();
out.push(PatternElement::Edge(if forward { ep } else { flip(ep) }));
out.push(PatternElement::Node(self.nodes[next_node].clone()));
}
out
}
}
pub(super) fn reorder_cyclic_pattern_edges(query: &mut CypherQuery, graph: &DirGraph) {
const ROOT_GAIN: usize = 4;
let edge_counts = if graph.has_edge_type_counts_cache() {
Some(graph.get_edge_type_counts())
} else {
None
};
for clause in &mut query.clauses {
let (patterns, path_assignments) = match clause {
Clause::Match(m) => (&mut m.patterns, &m.path_assignments),
_ => continue,
};
for (pi, pattern) in patterns.iter_mut().enumerate() {
if path_assignments.iter().any(|pa| pa.pattern_index == pi) {
continue; }
let Some(ring) = SimpleCycle::detect(&pattern.elements) else {
continue;
};
let sels: Vec<usize> = ring
.nodes
.iter()
.map(|np| estimate_node_selectivity(np, graph))
.collect();
let root = (0..ring.k).min_by_key(|&j| sels[j]).unwrap_or(0);
if sels[root].saturating_mul(ROOT_GAIN) >= sels[0] {
continue;
}
pattern.elements = ring.linearize(root, edge_counts.as_ref());
}
}
}