use crate::datatypes::values::Value;
use crate::graph::core::filtering::{compare_values, str_values_equal, values_equal};
use crate::graph::languages::cypher::executor::budget::MatchCeiling;
use crate::graph::languages::cypher::result::Bindings;
use crate::graph::schema::{DirGraph, InternedKey, NodeData};
use crate::graph::storage::column_store::ColumnStore;
use crate::graph::storage::{GraphRead, NodeView};
use petgraph::graph::NodeIndex;
use petgraph::Direction;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool;
use std::time::Instant;
use crate::graph::parallel::{self, ParallelInterrupt};
use super::column_filter::{self, ColumnFilter};
use super::pattern::{
AnchorSide, ConnTypeFilter, EdgeDirection, EdgePattern, MatchBinding, NodePattern, PathHop,
Pattern, PatternElement, PatternMatch, PropertyMatcher,
};
const EXPANSION_RAYON_THRESHOLD: usize = 8192;
const CANDIDATE_PARTITIONS_PER_WORKER: usize = 4;
struct ResolvedMatcher<'a> {
field: &'a str,
key: InternedKey,
matcher: &'a PropertyMatcher,
}
struct TypeScanMemo<'a> {
type_key: InternedKey,
type_str: &'a str,
store: Option<&'a std::sync::Arc<ColumnStore>>,
props: Vec<ResolvedMatcher<'a>>,
filter: Option<ColumnFilter<'a>>,
}
fn reuses_bound_relationship(current: &PatternMatch, candidate: &MatchBinding) -> bool {
let fixed_path_uses = |edge| {
current
.exact_path
.as_deref()
.is_some_and(|(_, path)| path.iter().any(|hop| hop.edge == edge))
};
let candidate_edges = match candidate {
MatchBinding::Edge { edge_index, .. } => std::slice::from_ref(edge_index),
MatchBinding::VariableLengthPath { path, .. } => {
return path.iter().any(|hop| {
fixed_path_uses(hop.edge)
|| current.bindings.iter().any(|(_, binding)| match binding {
MatchBinding::Edge { edge_index, .. } => *edge_index == hop.edge,
MatchBinding::VariableLengthPath { path, .. } => {
path.iter().any(|bound| bound.edge == hop.edge)
}
_ => false,
})
});
}
_ => return false,
};
candidate_edges.iter().any(|candidate_edge| {
fixed_path_uses(*candidate_edge)
|| current.bindings.iter().any(|(_, binding)| match binding {
MatchBinding::Edge { edge_index, .. } => *edge_index == *candidate_edge,
MatchBinding::VariableLengthPath { path, .. } => {
path.iter().any(|hop| hop.edge == *candidate_edge)
}
_ => false,
})
})
}
fn extend_fixed_trail(current: &mut PatternMatch, candidate: &MatchBinding) {
let MatchBinding::Edge {
source,
target,
edge_index,
connection_type,
..
} = candidate
else {
return;
};
let hop = PathHop {
node: *target,
edge: *edge_index,
connection_type: *connection_type,
};
if let Some(exact_path) = &mut current.exact_path {
exact_path.1.push(hop);
return;
}
current.exact_path = Some(Box::new((*source, vec![hop])));
}
fn global_alias_candidates(prop: &str, graph: &DirGraph) -> Vec<String> {
let mut out: Vec<String> = vec![prop.to_string()];
let (family, per_type_map): (&[&str], &FxHashMap<String, String>) = match prop {
"title" | "label" | "name" => (&["title", "label", "name"], &graph.title_field_aliases),
"id" => (&["id"], &graph.id_field_aliases),
_ => return out,
};
for &sibling in family {
let s = sibling.to_string();
if !out.contains(&s) {
out.push(s);
}
}
for alias in per_type_map.values() {
if !out.contains(alias) {
out.push(alias.clone());
}
}
out
}
#[inline]
fn str_ends_with(s: &str, suffix: &str) -> bool {
let (haystack, needle) = (s.as_bytes(), suffix.as_bytes());
match (needle.last(), haystack.last()) {
(None, _) => true,
(Some(_), None) => false,
(Some(n), Some(h)) => {
n == h && haystack.len() >= needle.len() && {
let at = haystack.len() - needle.len();
&haystack[at..] == needle
}
}
}
}
fn dedup_candidates(candidates: &mut Vec<NodeIndex>) {
if candidates.len() < 2 {
return;
}
let mut seen: rustc_hash::FxHashSet<NodeIndex> =
rustc_hash::FxHashSet::with_capacity_and_hasher(candidates.len(), Default::default());
candidates.retain(|&idx| seen.insert(idx));
}
#[inline]
fn str_starts_with(s: &str, prefix: &str) -> bool {
let (haystack, needle) = (s.as_bytes(), prefix.as_bytes());
match (needle.first(), haystack.first()) {
(None, _) => true,
(Some(_), None) => false,
(Some(n), Some(h)) => {
n == h && haystack.len() >= needle.len() && &haystack[..needle.len()] == needle
}
}
}
pub(super) fn str_field_test(matcher: &PropertyMatcher) -> Option<impl Fn(&str) -> bool + '_> {
if !matches!(
matcher,
PropertyMatcher::Equals(Value::String(_))
| PropertyMatcher::StartsWith(_)
| PropertyMatcher::EndsWith(_)
| PropertyMatcher::Contains(_)
) {
return None;
}
Some(move |s: &str| match matcher {
PropertyMatcher::Equals(Value::String(target)) => str_values_equal(s, target),
PropertyMatcher::StartsWith(prefix) => str_starts_with(s, prefix),
PropertyMatcher::EndsWith(suffix) => str_ends_with(s, suffix),
PropertyMatcher::Contains(needle) => s.contains(needle.as_str()),
_ => unreachable!("guarded by the matches! above"),
})
}
pub(super) fn value_matches(
params: &HashMap<String, Value>,
value: &Value,
matcher: &PropertyMatcher,
) -> bool {
if matches!(value, Value::Null) {
return false;
}
match matcher {
PropertyMatcher::Equals(expected) => values_equal(value, expected),
PropertyMatcher::EqualsParam(name) => params
.get(name.as_str())
.is_some_and(|expected| values_equal(value, expected)),
PropertyMatcher::EqualsVar(_) | PropertyMatcher::EqualsNodeProp { .. } => false,
PropertyMatcher::In(values) => values.matches(value),
PropertyMatcher::GreaterThan(threshold) => {
compare_values(value, threshold) == Some(std::cmp::Ordering::Greater)
}
PropertyMatcher::GreaterOrEqual(threshold) => {
matches!(
compare_values(value, threshold),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
}
PropertyMatcher::LessThan(threshold) => {
compare_values(value, threshold) == Some(std::cmp::Ordering::Less)
}
PropertyMatcher::LessOrEqual(threshold) => {
matches!(
compare_values(value, threshold),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)
}
PropertyMatcher::Range {
lower,
lower_inclusive,
upper,
upper_inclusive,
} => {
let above_lower = if *lower_inclusive {
matches!(
compare_values(value, lower),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
} else {
compare_values(value, lower) == Some(std::cmp::Ordering::Greater)
};
let below_upper = if *upper_inclusive {
matches!(
compare_values(value, upper),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)
} else {
compare_values(value, upper) == Some(std::cmp::Ordering::Less)
};
above_lower && below_upper
}
PropertyMatcher::StartsWith(prefix) => match value {
Value::String(s) => str_starts_with(s, prefix),
_ => false,
},
PropertyMatcher::Contains(needle) => match value {
Value::String(s) => s.contains(needle.as_str()),
_ => false,
},
PropertyMatcher::EndsWith(suffix) => match value {
Value::String(s) => str_ends_with(s, suffix),
_ => false,
},
}
}
pub struct PatternExecutor<'a> {
graph: &'a DirGraph,
max_matches: Option<usize>,
pre_bindings: &'a Bindings<NodeIndex>,
lightweight: bool,
params: &'a HashMap<String, Value>,
deadline: Option<Instant>,
cancel: Option<&'static AtomicBool>,
distinct_target_var: Option<String>,
distinct_prior: Option<&'a HashSet<NodeIndex>>,
parallel: bool,
cap_truncated: AtomicBool,
match_ceiling: Option<MatchCeiling>,
_arena_guard: Option<crate::graph::storage::disk::graph::DiskQueryGuard>,
}
static EMPTY_PARAMS: std::sync::LazyLock<HashMap<String, Value>> =
std::sync::LazyLock::new(HashMap::new);
static EMPTY_BINDINGS: std::sync::LazyLock<Bindings<NodeIndex>> =
std::sync::LazyLock::new(Bindings::new);
impl<'a> PatternExecutor<'a> {
pub fn new(graph: &'a DirGraph, max_matches: Option<usize>) -> Self {
PatternExecutor {
graph,
max_matches,
pre_bindings: &EMPTY_BINDINGS,
lightweight: false,
params: &EMPTY_PARAMS,
deadline: None,
cancel: None,
distinct_target_var: None,
distinct_prior: None,
parallel: false,
cap_truncated: AtomicBool::new(false),
match_ceiling: None,
_arena_guard: graph.graph.begin_query(),
}
}
pub fn new_lightweight_with_params(
graph: &'a DirGraph,
max_matches: Option<usize>,
params: &'a HashMap<String, Value>,
) -> Self {
PatternExecutor {
graph,
max_matches,
pre_bindings: &EMPTY_BINDINGS,
lightweight: true,
params,
deadline: None,
cancel: None,
distinct_target_var: None,
distinct_prior: None,
parallel: false,
cap_truncated: AtomicBool::new(false),
match_ceiling: None,
_arena_guard: graph.graph.begin_query(),
}
}
pub fn with_bindings_and_params(
graph: &'a DirGraph,
max_matches: Option<usize>,
pre_bindings: &'a Bindings<NodeIndex>,
params: &'a HashMap<String, Value>,
) -> Self {
PatternExecutor {
graph,
max_matches,
pre_bindings,
lightweight: true,
params,
deadline: None,
cancel: None,
distinct_target_var: None,
distinct_prior: None,
parallel: false,
cap_truncated: AtomicBool::new(false),
match_ceiling: None,
_arena_guard: graph.graph.begin_query(),
}
}
pub fn set_deadline(mut self, deadline: Option<Instant>) -> Self {
self.deadline = deadline;
self
}
pub fn set_cancel(mut self, cancel: Option<&'static AtomicBool>) -> Self {
self.cancel = cancel;
self
}
pub fn set_parallel(mut self, parallel: bool) -> Self {
self.parallel = parallel;
self
}
#[inline]
fn interrupt_reason(&self) -> Option<String> {
if let Some(dl) = self.deadline {
if Instant::now() > dl {
return Some("Query timed out".to_string());
}
}
if let Some(c) = &self.cancel {
if c.load(std::sync::atomic::Ordering::Relaxed) {
return Some("Query cancelled".to_string());
}
}
None
}
#[inline]
fn note_cap_truncated(&self) {
self.cap_truncated
.store(true, std::sync::atomic::Ordering::Relaxed);
}
#[inline]
fn take_cap_truncated(&self) -> bool {
self.cap_truncated
.swap(false, std::sync::atomic::Ordering::Relaxed)
}
pub fn set_distinct_target(mut self, var: Option<String>) -> Self {
self.distinct_target_var = var;
self
}
pub fn set_distinct_prior(mut self, prior: Option<&'a HashSet<NodeIndex>>) -> Self {
self.distinct_prior = prior;
self
}
pub fn set_match_ceiling(mut self, ceiling: Option<MatchCeiling>) -> Self {
self.match_ceiling = ceiling;
self
}
#[inline]
fn check_match_ceiling(&self, held: usize) -> Result<(), String> {
match self.match_ceiling {
Some(ceiling) => ceiling.check(held),
None => Ok(()),
}
}
pub fn find_matching_nodes_pub(&self, pattern: &NodePattern) -> Result<Vec<NodeIndex>, String> {
self.find_matching_nodes(pattern)
}
fn find_matching_nodes(&self, pattern: &NodePattern) -> Result<Vec<NodeIndex>, String> {
let extra_keys: Vec<InternedKey> = pattern
.extra_labels
.iter()
.map(|label| InternedKey::from_str(label))
.collect();
if let Some(ref var) = pattern.variable {
if let Some(&idx) = self.pre_bindings.get(var) {
if let Some(node) = self.graph.graph.node_view(idx) {
if let Some(ref node_type) = pattern.node_type {
let primary_key = InternedKey::from_str(node_type);
let labels = self.graph.node_labels(idx);
if !labels.contains(&primary_key) {
return Ok(vec![]);
}
for extra in &pattern.extra_labels {
let key = InternedKey::from_str(extra);
if !labels.contains(&key) {
return Ok(vec![]);
}
}
let _ = node;
}
if let Some(ref props) = pattern.properties {
if !self.node_matches_properties(idx, props) {
return Ok(vec![]);
}
}
return Ok(vec![idx]);
}
return Ok(vec![]);
}
}
if pattern.properties.as_ref().is_some_and(|properties| {
properties
.values()
.any(|matcher| matches!(matcher, PropertyMatcher::In(values) if values.is_empty()))
}) {
return Ok(Vec::new());
}
if let Some(ref node_type) = pattern.node_type {
let secondary = if self.graph.has_secondary_labels {
self.graph
.secondary_label_index
.get(&InternedKey::from_str(node_type))
.filter(|bucket| !bucket.is_empty())
} else {
None
};
if let Some(ref props) = pattern.properties {
if let Some(indexed) = self
.try_index_lookup(node_type, props)
.or_else(|| self.try_global_index_lookup_typed(node_type, props))
{
let mut out = self.filter_node_candidates(&indexed, None, &extra_keys)?;
if let Some(secondary) = secondary {
out.extend(self.filter_node_candidates(
secondary.as_slice(),
Some(props),
&extra_keys,
)?);
}
return Ok(out);
}
}
let mut candidates = self
.graph
.type_indices
.get(node_type)
.map(|indices| indices.to_vec())
.unwrap_or_default();
if let Some(secondary) = secondary {
candidates.extend(secondary.iter().copied());
}
if candidates.is_empty() {
return Ok(Vec::new());
}
if pattern.properties.is_none() && extra_keys.is_empty() {
return Ok(candidates);
}
self.filter_node_candidates(&candidates, pattern.properties.as_ref(), &extra_keys)
} else if let Some(ref props) = pattern.properties {
let id_val_opt = ["id"].iter().find_map(|k| match props.get(*k) {
Some(PropertyMatcher::Equals(v)) => Some(v),
Some(PropertyMatcher::EqualsParam(name)) => self.params.get(name.as_str()),
_ => None,
});
if let Some(id_val) = id_val_opt {
let mut hits: Vec<petgraph::graph::NodeIndex> = Vec::new();
for node_type in self.graph.type_indices.keys() {
if let Some(idx) = self.graph.lookup_by_id_readonly(node_type, id_val) {
if props.len() == 1 || self.node_matches_properties(idx, props) {
hits.push(idx);
}
}
}
hits.sort_unstable();
return Ok(hits);
}
for (prop, matcher) in props {
let alias_candidates = global_alias_candidates(prop, self.graph);
match matcher {
PropertyMatcher::Equals(Value::String(s)) => {
for idx_name in &alias_candidates {
if let Some(candidates) =
self.graph.graph.lookup_by_property_eq_any_type(idx_name, s)
{
if props.len() == 1 {
return Ok(candidates);
}
let filtered = candidates
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Ok(filtered);
}
}
}
PropertyMatcher::StartsWith(prefix) => {
for idx_name in &alias_candidates {
if let Some(candidates) = self
.graph
.graph
.lookup_by_property_prefix_any_type(idx_name, prefix, usize::MAX)
{
if props.len() == 1 {
return Ok(candidates);
}
let filtered = candidates
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Ok(filtered);
}
}
}
_ => {}
}
}
let g = &self.graph.graph;
let mut out = Vec::new();
for (i, idx) in g.node_indices().enumerate() {
if i & 0xFFF == 0 {
self.check_scan_deadline()?;
}
if self.node_matches_properties(idx, props) {
out.push(idx);
}
}
Ok(out)
} else {
let g = &self.graph.graph;
let mut out = Vec::with_capacity(g.node_count());
for (i, idx) in g.node_indices().enumerate() {
if i & 0xFFF == 0 {
self.check_scan_deadline()?;
}
out.push(idx);
}
Ok(out)
}
}
fn filter_node_candidates<'p>(
&'p self,
candidates: &[NodeIndex],
props: Option<&'p HashMap<String, PropertyMatcher>>,
extra_keys: &[InternedKey],
) -> Result<Vec<NodeIndex>, String> {
if self.may_fan_out_candidate_scan(candidates, props) {
return self.filter_candidates_parallel(candidates, props, extra_keys);
}
let interrupt = ParallelInterrupt::new(|| self.check_scan_deadline().err());
self.filter_candidate_partition(candidates, props, extra_keys, &interrupt)
}
fn may_fan_out_candidate_scan(
&self,
candidates: &[NodeIndex],
props: Option<&HashMap<String, PropertyMatcher>>,
) -> bool {
if !self.parallel || self.graph.graph.is_disk() {
return false;
}
if column_filter::scan_overrides_active() {
return false;
}
parallel::should_fan_out(
candidates.len(),
self.candidate_scan_cost(candidates, props),
)
}
fn candidate_scan_cost(
&self,
candidates: &[NodeIndex],
props: Option<&HashMap<String, PropertyMatcher>>,
) -> parallel::CostClass {
let Some(props) = props else {
return parallel::CostClass::Compiled;
};
let compiled = candidates
.first()
.and_then(|&idx| self.graph.graph.node_weight(idx))
.and_then(|data| self.build_type_scan_memo(data.node_type, props))
.is_some_and(|memo| memo.filter.is_some());
if compiled {
parallel::CostClass::Compiled
} else {
parallel::CostClass::Interpreted
}
}
fn filter_candidates_parallel<'p>(
&'p self,
candidates: &[NodeIndex],
props: Option<&'p HashMap<String, PropertyMatcher>>,
extra_keys: &[InternedKey],
) -> Result<Vec<NodeIndex>, String> {
#[cfg(test)]
parallel::PARALLEL_CANDIDATE_SCANS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let interrupt = ParallelInterrupt::new(|| self.check_scan_deadline().err());
let partitions = (rayon::current_num_threads() * CANDIDATE_PARTITIONS_PER_WORKER).max(1);
let chunk_len = candidates.len().div_ceil(partitions).max(1);
let parts: Vec<(Vec<NodeIndex>, usize)> = parallel::install(|| {
candidates
.par_chunks(chunk_len)
.map(|chunk| {
let before = column_filter::local_rows_filtered();
let kept =
self.filter_candidate_partition(chunk, props, extra_keys, &interrupt)?;
Ok((kept, column_filter::local_rows_filtered() - before))
})
.collect::<Result<Vec<_>, String>>()
})?;
let mut out = Vec::with_capacity(parts.iter().map(|(part, _)| part.len()).sum());
let mut filtered = 0usize;
for (part, rows) in parts {
filtered += rows;
out.extend(part);
}
column_filter::add_rows_filtered(filtered);
Ok(out)
}
fn filter_candidate_partition<'p, F>(
&'p self,
candidates: &[NodeIndex],
props: Option<&'p HashMap<String, PropertyMatcher>>,
extra_keys: &[InternedKey],
interrupt: &ParallelInterrupt<F>,
) -> Result<Vec<NodeIndex>, String>
where
F: Fn() -> Option<String> + Sync,
{
let mut out = Vec::new();
let mut memo: Option<TypeScanMemo<'p>> = None;
let scoped_materialization = self.graph.graph.is_disk();
for (i, &idx) in candidates.iter().enumerate() {
interrupt.check(i)?;
if !extra_keys.is_empty()
&& !extra_keys
.iter()
.all(|&key| self.graph.node_has_label(idx, key))
{
continue;
}
let Some(properties) = props else {
out.push(idx);
continue;
};
let owned;
let data = if scoped_materialization {
owned = self.graph.graph.owned_node_data(idx);
owned.as_ref()
} else {
self.graph.graph.node_weight(idx)
};
let Some(data) = data else {
continue;
};
if memo
.as_ref()
.is_none_or(|memo| memo.type_key != data.node_type)
{
memo = self.build_type_scan_memo(data.node_type, properties);
}
let Some(memo) = memo.as_ref() else {
continue;
};
let matched = memo
.filter
.as_ref()
.and_then(|filter| {
let row = data.properties.columnar_row_id()?;
filter.matches(data, row, self.params)
})
.unwrap_or_else(|| {
self.node_matches_resolved(self.node_view_of(data, memo.store), memo)
});
if matched {
out.push(idx);
}
}
Ok(out)
}
#[inline]
fn check_scan_deadline(&self) -> Result<(), String> {
if let Some(dl) = self.deadline {
if Instant::now() > dl {
return Err("Query timed out during node scan. Hint: add an index on a \
predicate property (create_index), anchor with \
MATCH (n {id: ...}), or raise timeout_ms."
.to_string());
}
}
if let Some(c) = &self.cancel {
if c.load(std::sync::atomic::Ordering::Relaxed) {
return Err("Query cancelled".to_string());
}
}
Ok(())
}
fn try_global_index_lookup_typed(
&self,
node_type: &str,
props: &HashMap<String, PropertyMatcher>,
) -> Option<Vec<NodeIndex>> {
let expected = InternedKey::from_str(node_type);
for (prop, matcher) in props {
let aliases = global_alias_candidates(prop, self.graph);
match matcher {
PropertyMatcher::Equals(Value::String(s)) => {
for alias in &aliases {
if let Some(candidates) =
self.graph.graph.lookup_by_property_eq_any_type(alias, s)
{
let filtered: Vec<NodeIndex> = candidates
.into_iter()
.filter(|&idx| self.graph.graph.node_type_of(idx) == Some(expected))
.filter(|&idx| {
props.len() == 1 || self.node_matches_properties(idx, props)
})
.collect();
return Some(filtered);
}
}
}
PropertyMatcher::StartsWith(prefix) => {
for alias in &aliases {
if let Some(candidates) = self
.graph
.graph
.lookup_by_property_prefix_any_type(alias, prefix, usize::MAX)
{
let filtered: Vec<NodeIndex> = candidates
.into_iter()
.filter(|&idx| self.graph.graph.node_type_of(idx) == Some(expected))
.filter(|&idx| {
props.len() == 1 || self.node_matches_properties(idx, props)
})
.collect();
return Some(filtered);
}
}
}
_ => {}
}
}
None
}
fn try_in_list_lookup(
&self,
node_type: &str,
props: &HashMap<String, PropertyMatcher>,
) -> Option<Vec<NodeIndex>> {
if let Some(PropertyMatcher::In(values)) = props.get("id") {
let mut result = Vec::with_capacity(values.len());
for val in values {
if let Some(idx) = self.graph.lookup_by_id_readonly(node_type, val) {
result.push(idx);
}
}
dedup_candidates(&mut result);
if props.len() > 1 {
result.retain(|&idx| self.node_matches_properties(idx, props));
}
return Some(result);
}
for (prop_name, matcher) in props {
if let PropertyMatcher::In(values) = matcher {
if prop_name == "id" {
continue; }
let key = (node_type.to_string(), prop_name.clone());
if !self.graph.property_indices.contains_key(&key) {
continue;
}
let mut result = Vec::with_capacity(values.len());
for val in values {
if let Some(indices) = self.graph.lookup_by_index(node_type, prop_name, val) {
result.extend(indices);
}
}
dedup_candidates(&mut result);
if props.len() > 1 {
result.retain(|&idx| self.node_matches_properties(idx, props));
}
return Some(result);
}
}
None
}
fn try_index_lookup(
&self,
node_type: &str,
props: &HashMap<String, PropertyMatcher>,
) -> Option<Vec<NodeIndex>> {
if props
.values()
.any(|matcher| matches!(matcher, PropertyMatcher::In(values) if values.is_empty()))
{
return Some(Vec::new());
}
if let Some(result) = self.try_in_list_lookup(node_type, props) {
return Some(result);
}
let mut equality_props: Vec<(&String, &Value)> = props
.iter()
.filter_map(|(k, v)| match v {
PropertyMatcher::Equals(val) => Some((k, val)),
PropertyMatcher::EqualsParam(name) => {
self.params.get(name.as_str()).map(|val| (k, val))
}
_ => None,
})
.collect();
let has_comparison = props.values().any(|m| {
matches!(
m,
PropertyMatcher::GreaterThan(_)
| PropertyMatcher::GreaterOrEqual(_)
| PropertyMatcher::LessThan(_)
| PropertyMatcher::LessOrEqual(_)
| PropertyMatcher::Range { .. }
)
});
let has_prefix = props
.values()
.any(|matcher| matches!(matcher, PropertyMatcher::StartsWith(_)));
if equality_props.is_empty() && !has_comparison && !has_prefix {
return None;
}
if equality_props.len() == 1 {
let (prop_name, value) = equality_props[0];
let is_id_alias = prop_name.as_str() == "id"
|| self
.graph
.id_field_aliases
.get(node_type)
.map(|alias| alias == prop_name.as_str())
.unwrap_or(false);
if is_id_alias {
if let Some(idx) = self.graph.lookup_by_id_readonly(node_type, value) {
return Some(vec![idx]);
}
return Some(Vec::new()); }
}
if equality_props.len() >= 2 {
equality_props.sort_by(|a, b| a.0.cmp(b.0));
let names: Vec<String> = equality_props.iter().map(|(k, _)| (*k).clone()).collect();
let values: Vec<Value> = equality_props.iter().map(|(_, v)| (*v).clone()).collect();
if let Some(results) = self
.graph
.lookup_by_composite_index(node_type, &names, &values)
{
if equality_props.len() == props.len() {
return Some(results);
}
let filtered = results
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Some(filtered);
}
}
for (prop, value) in &equality_props {
if let Some(results) = self.graph.lookup_by_index(node_type, prop, value) {
if equality_props.len() == 1 && props.len() == 1 {
return Some(results);
} else {
let filtered = results
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Some(filtered);
}
}
}
for (prop, value) in &equality_props {
if let Value::String(s) = value {
if let Some(results) = self.graph.graph.lookup_by_property_eq(node_type, prop, s) {
if equality_props.len() == 1 && props.len() == 1 {
return Some(results);
}
let filtered = results
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Some(filtered);
}
}
}
for (prop, matcher) in props {
if let PropertyMatcher::StartsWith(prefix) = matcher {
if let Some(results) =
self.graph
.graph
.lookup_by_property_prefix(node_type, prop, prefix, usize::MAX)
{
if props.len() == 1 {
return Some(results);
}
let filtered = results
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Some(filtered);
}
}
}
for (prop, matcher) in props {
use std::ops::Bound;
let bounds: Option<(Bound<&Value>, Bound<&Value>)> = match matcher {
PropertyMatcher::GreaterThan(v) => Some((Bound::Excluded(v), Bound::Unbounded)),
PropertyMatcher::GreaterOrEqual(v) => Some((Bound::Included(v), Bound::Unbounded)),
PropertyMatcher::LessThan(v) => Some((Bound::Unbounded, Bound::Excluded(v))),
PropertyMatcher::LessOrEqual(v) => Some((Bound::Unbounded, Bound::Included(v))),
PropertyMatcher::Range {
lower,
lower_inclusive,
upper,
upper_inclusive,
} => {
let lo = if *lower_inclusive {
Bound::Included(lower)
} else {
Bound::Excluded(lower)
};
let hi = if *upper_inclusive {
Bound::Included(upper)
} else {
Bound::Excluded(upper)
};
Some((lo, hi))
}
_ => None,
};
if let Some((lo, hi)) = bounds {
if let Some(results) = self.graph.lookup_range(node_type, prop, lo, hi) {
if props.len() == 1 {
return Some(results);
}
let filtered = results
.into_iter()
.filter(|&idx| self.node_matches_properties(idx, props))
.collect();
return Some(filtered);
}
}
}
None
}
pub fn node_matches_properties_pub(
&self,
idx: NodeIndex,
props: &HashMap<String, PropertyMatcher>,
) -> bool {
self.node_matches_properties(idx, props)
}
fn node_matches_properties(
&self,
idx: NodeIndex,
props: &HashMap<String, PropertyMatcher>,
) -> bool {
let owned;
let data = if self.graph.graph.is_disk() {
owned = self.graph.graph.owned_node_data(idx);
owned.as_ref()
} else {
self.graph.graph.node_weight(idx)
};
let Some(data) = data else {
return false;
};
self.node_data_matches_properties(data, props)
}
#[inline]
fn node_data_matches_properties(
&self,
data: &NodeData,
props: &HashMap<String, PropertyMatcher>,
) -> bool {
let Some(type_str) = self.graph.interner.try_resolve(data.node_type) else {
return false;
};
let node = self.node_view_of(data, self.graph.graph.column_store(data.node_type));
props.iter().all(|(key, matcher)| {
let field = self.graph.resolve_alias(type_str, key);
self.prop_matches(node, type_str, field, InternedKey::from_str(field), matcher)
})
}
#[inline]
fn node_view_of<'d>(
&self,
data: &'d NodeData,
store: Option<&'d std::sync::Arc<ColumnStore>>,
) -> NodeView<'d> {
let resolved = data
.properties
.columnar_row_id()
.and_then(|row_id| store.map(|store| (&**store, row_id)));
NodeView::new(data, resolved)
}
fn node_matches_resolved(&self, node: NodeView<'_>, memo: &TypeScanMemo<'_>) -> bool {
memo.props.iter().all(|resolved| {
self.prop_matches(
node,
memo.type_str,
resolved.field,
resolved.key,
resolved.matcher,
)
})
}
#[inline]
fn prop_matches(
&self,
node: NodeView<'_>,
type_str: &str,
field: &str,
key: InternedKey,
matcher: &PropertyMatcher,
) -> bool {
if !matches!(
field,
"name" | "title" | "id" | "type" | "node_type" | "label"
) {
if let PropertyMatcher::Equals(Value::String(target)) = matcher {
return node.str_prop_eq(key, target) == Some(true);
}
}
if let Some(test) = str_field_test(matcher) {
return node.resolved_field_str(type_str, field, key).is(test);
}
match node.resolved_field(type_str, field, key) {
Some(v) => self.value_matches(&v, matcher),
None => false,
}
}
fn build_type_scan_memo<'m>(
&'m self,
type_key: InternedKey,
props: &'m HashMap<String, PropertyMatcher>,
) -> Option<TypeScanMemo<'m>> {
let type_str = self.graph.interner.try_resolve(type_key)?;
let store = self.graph.graph.column_store(type_key);
let resolved: Vec<ResolvedMatcher<'m>> = props
.iter()
.map(|(key, matcher)| {
let field = self.graph.resolve_alias(type_str, key);
ResolvedMatcher {
field,
key: InternedKey::from_str(field),
matcher,
}
})
.collect();
let filter = column_filter::column_filter_enabled()
.then(|| {
ColumnFilter::compile(store, resolved.iter().map(|r| (r.field, r.key, r.matcher)))
})
.flatten();
Some(TypeScanMemo {
type_key,
type_str,
store,
props: resolved,
filter,
})
}
#[inline]
fn value_matches(&self, value: &Value, matcher: &PropertyMatcher) -> bool {
value_matches(self.params, value, matcher)
}
fn node_matches_pattern_labels(&self, idx: NodeIndex, node_pattern: &NodePattern) -> bool {
if let Some(ref nt) = node_pattern.node_type {
if !self.graph.node_has_label(idx, InternedKey::from_str(nt)) {
return false;
}
}
node_pattern
.extra_labels
.iter()
.all(|l| self.graph.node_has_label(idx, InternedKey::from_str(l)))
}
fn bound_target(
&self,
node_pattern: &NodePattern,
current_match: &PatternMatch,
) -> Option<NodeIndex> {
let var = node_pattern.variable.as_ref()?;
if let Some(&idx) = self.pre_bindings.get(var) {
return Some(idx);
}
current_match.bindings.iter().find_map(|(name, binding)| {
if name == var {
match binding {
MatchBinding::Node { index, .. } | MatchBinding::NodeRef(index) => Some(*index),
_ => None,
}
} else {
None
}
})
}
fn disk_peer_sweep_applies(&self, edge_pattern: &EdgePattern) -> bool {
edge_pattern.variable.is_none()
&& edge_pattern.properties.is_none()
&& !edge_pattern.needs_path_info
&& edge_pattern.connection_types.is_none()
&& self.graph.graph.is_disk()
}
fn expand_disk_peers(
&self,
source: NodeIndex,
edge_pattern: &EdgePattern,
node_pattern: &NodePattern,
max_results: Option<usize>,
target_hint: Option<NodeIndex>,
) -> Vec<(NodeIndex, MatchBinding)> {
let conn_u64 = edge_pattern
.connection_type
.as_ref()
.map(|ct| InternedKey::from_str(ct).as_u64());
let directions: &[Direction] = match edge_pattern.direction {
EdgeDirection::Outgoing => &[Direction::Outgoing],
EdgeDirection::Incoming => &[Direction::Incoming],
EdgeDirection::Both => &[Direction::Outgoing, Direction::Incoming],
};
let mut results = Vec::new();
for &dir in directions {
for (peer_idx, _edge_idx) in self.graph.graph.iter_peers_filtered(source, dir, conn_u64)
{
if max_results.is_some_and(|max| results.len() >= max) {
break;
}
if target_hint.is_some_and(|hint| peer_idx != hint) {
continue;
}
if !edge_pattern.skip_target_type_check
&& !self.node_matches_pattern_labels(peer_idx, node_pattern)
{
continue;
}
if let Some(ref props) = node_pattern.properties {
if !self.node_matches_properties(peer_idx, props) {
continue;
}
}
results.push((peer_idx, MatchBinding::NodeRef(peer_idx)));
}
}
results
}
fn expand_from_node(
&self,
source: NodeIndex,
edge_pattern: &EdgePattern,
node_pattern: &NodePattern,
max_results: Option<usize>,
target_hint: Option<NodeIndex>,
visited: &mut VisitedStamps,
) -> Result<Vec<(NodeIndex, MatchBinding)>, String> {
if let Some(ref types) = edge_pattern.connection_types {
if !types.iter().any(|t| self.graph.has_connection_type(t)) {
return Ok(Vec::new());
}
} else if let Some(ref conn_type) = edge_pattern.connection_type {
if !self.graph.has_connection_type(conn_type) {
return Ok(Vec::new());
}
}
if let Some((min_hops, max_hops)) = edge_pattern.var_length {
return self.expand_var_length(
source,
&VarLengthSegment {
edge: edge_pattern,
node: node_pattern,
min_hops,
max_hops,
},
max_results,
visited,
);
}
if self.disk_peer_sweep_applies(edge_pattern) {
return Ok(self.expand_disk_peers(
source,
edge_pattern,
node_pattern,
max_results,
target_hint,
));
}
let mut results = Vec::new();
let directions: &[Direction] = match edge_pattern.direction {
EdgeDirection::Outgoing => &[Direction::Outgoing],
EdgeDirection::Incoming => &[Direction::Incoming],
EdgeDirection::Both => &[Direction::Outgoing, Direction::Incoming],
};
let conn_keys: Option<Vec<InternedKey>> = edge_pattern
.connection_types
.as_ref()
.map(|types| types.iter().map(|t| InternedKey::from_str(t)).collect());
let conn_key = if conn_keys.is_none() {
edge_pattern
.connection_type
.as_ref()
.map(|ct| InternedKey::from_str(ct))
} else {
None
};
for &direction in directions {
let edges = self
.graph
.graph
.edges_directed_filtered(source, direction, conn_key);
for edge in edges {
let conn_type = edge.connection_type();
if let Some(ref keys) = conn_keys {
if !keys.contains(&conn_type) {
continue;
}
} else if let Some(key) = conn_key {
if conn_type != key {
continue;
}
}
if let Some(ref filter) = edge_pattern.edge_filter {
let edge_data = edge.weight();
let edge_source = edge.source();
let edge_target = edge.target();
let peer_is_start = match (filter.anchor, direction) {
(AnchorSide::Source, Direction::Outgoing) => false,
(AnchorSide::Source, Direction::Incoming) => true,
(AnchorSide::Target, Direction::Outgoing) => true,
(AnchorSide::Target, Direction::Incoming) => false,
};
let keep = filter.predicate.eval(
conn_type,
peer_is_start,
edge_source,
edge_target,
&|prop: &str| edge_data.get_property(prop).cloned(),
);
if !keep {
continue;
}
}
if let Some(ref props) = edge_pattern.properties {
let edge_data = edge.weight();
let matches = props.iter().all(|(key, matcher)| {
edge_data
.get_property(key)
.map(|v| self.value_matches(v, matcher))
.unwrap_or(false)
});
if !matches {
continue;
}
}
let target = match direction {
Direction::Outgoing => edge.target(),
Direction::Incoming => edge.source(),
};
if target_hint.is_some_and(|h| target != h) {
continue;
}
if !edge_pattern.skip_target_type_check
&& !self.node_matches_pattern_labels(target, node_pattern)
{
continue;
}
if let Some(ref props) = node_pattern.properties {
if !self.node_matches_properties(target, props) {
continue;
}
}
let edge_binding = MatchBinding::Edge {
source,
target,
edge_index: edge.id(),
connection_type: conn_type,
};
results.push((target, edge_binding));
if max_results.is_some_and(|max| results.len() >= max) {
return Ok(results);
}
}
}
Ok(results)
}
fn node_to_binding(&self, idx: NodeIndex) -> MatchBinding {
if self.lightweight {
return MatchBinding::NodeRef(idx);
}
if let Some(node) = self.graph.graph.node_view(idx) {
let node_title = node.title();
let title_str = match &*node_title {
Value::String(s) => s.clone(),
Value::Int64(i) => i.to_string(),
Value::Float64(f) => f.to_string(),
Value::UniqueId(u) => u.to_string(),
_ => format!("{:?}", *node_title),
};
MatchBinding::Node {
index: idx,
node_type: node.node_type_str(&self.graph.interner).to_string(),
title: title_str,
id: node.id().into_owned(),
properties: node.properties_cloned(&self.graph.interner),
}
} else {
MatchBinding::Node {
index: idx,
node_type: "Unknown".to_string(),
title: "Unknown".to_string(),
id: Value::Null,
properties: HashMap::new(),
}
}
}
}
#[path = "matcher_expansion.rs"]
mod expansion;
#[path = "matcher_var_length.rs"]
mod var_length;
use var_length::{VarLengthSegment, VisitedStamps};
#[cfg(test)]
#[path = "matcher_id_lookup_tests.rs"]
mod id_lookup_tests;
#[cfg(test)]
#[path = "matcher_limit_seed_tests.rs"]
mod limit_seed_tests;
#[cfg(test)]
#[path = "matcher_ceiling_tests.rs"]
mod ceiling_tests;