use super::ast::*;
use super::result::*;
use crate::datatypes::values::Value;
use crate::graph::core::pattern_matching::{
EdgeDirection, Pattern, PatternElement, PatternExecutor, PropertyMatcher,
};
use crate::graph::schema::{DirGraph, InternedKey};
use crate::graph::storage::GraphRead;
use rayon::prelude::*;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::{Mutex, OnceLock, RwLock};
use std::time::Instant;
#[cfg(test)]
thread_local! {
static TEST_PERIODIC_POLLS_BEFORE_INTERRUPT: std::cell::Cell<Option<usize>> = const {
std::cell::Cell::new(None)
};
}
use budget::ExecutionBudget;
use execution_support::*;
use interrupt::check_interrupt;
pub(super) const INTERRUPT_POLL_INTERVAL: usize = 4096;
type SpatialCacheShard = RwLock<HashMap<usize, Option<NodeSpatialData>>>;
#[derive(Clone, Copy)]
pub(super) struct SubquerySetSeed<'r> {
outer_row: &'r ResultRow,
imports: &'r [String],
arm_local: bool,
}
pub(super) struct RowLimitOutcome {
cap: usize,
total_rows: Option<u64>,
}
pub(super) fn apply_row_limit(
rows: &mut Vec<ResultRow>,
row_limit: Option<usize>,
) -> Option<RowLimitOutcome> {
let cap = row_limit?;
let total = rows.len();
if total > cap {
rows.truncate(cap);
Some(RowLimitOutcome {
cap,
total_rows: Some(total as u64),
})
} else {
Some(RowLimitOutcome {
cap,
total_rows: None,
})
}
}
pub(super) fn stamp_row_limit(result: &mut CypherResult, outcome: Option<RowLimitOutcome>) {
let Some(outcome) = outcome else {
return;
};
let diagnostics = result
.diagnostics
.get_or_insert_with(QueryDiagnostics::default);
diagnostics.row_limit = Some(outcome.cap);
diagnostics.total_rows = outcome.total_rows;
if let Some(total) = outcome.total_rows {
let message = format!(
"Result truncated by row_limit: showing {} of {total} rows. Raise row_limit, \
or add ORDER BY/LIMIT so the rows you keep are the ones you meant to keep.",
outcome.cap
);
super::emit_query_warnings(std::slice::from_ref(&message));
diagnostics.warnings.push(message);
}
}
pub struct CypherExecutor<'a> {
pub(super) graph: &'a DirGraph,
pub(super) params: &'a HashMap<String, Value>,
vs_cache: VectorScoreCaches,
tb_cache: OnceLock<TextBm25Cache>,
pub(super) deadline: Option<Instant>,
pub(super) cancel: Option<&'static AtomicBool>,
pub(super) budget: ExecutionBudget,
spatial_node_cache: OnceLock<Vec<SpatialCacheShard>>,
alias_name_hashes: OnceLock<rustc_hash::FxHashSet<u64>>,
streaming: bool,
pub(super) parallel: bool,
pub(super) csv_import: load_csv::CsvImportPolicy,
runtime_warnings: Mutex<Vec<String>>,
runtime_retrieval: Mutex<Vec<RetrievalDiagnostics>>,
_arena_guard: Option<crate::graph::storage::disk::graph::DiskQueryGuard>,
pub(super) row_limit: Option<usize>,
}
impl<'a> CypherExecutor<'a> {
pub fn with_params(
graph: &'a DirGraph,
params: &'a HashMap<String, Value>,
deadline: Option<Instant>,
) -> Self {
CypherExecutor {
graph,
params,
vs_cache: VectorScoreCaches::default(),
tb_cache: OnceLock::new(),
deadline,
cancel: None,
budget: ExecutionBudget::default(),
spatial_node_cache: OnceLock::new(),
alias_name_hashes: OnceLock::new(),
streaming: true,
parallel: false,
csv_import: load_csv::CsvImportPolicy::Denied,
runtime_warnings: Mutex::new(Vec::new()),
runtime_retrieval: Mutex::new(Vec::new()),
_arena_guard: graph.graph.begin_query(),
row_limit: None,
}
}
pub fn with_csv_import(mut self, policy: load_csv::CsvImportPolicy) -> Self {
self.csv_import = policy;
self
}
#[inline]
pub(super) fn property_might_be_alias(&self, property: &str) -> bool {
let set = self.alias_name_hashes.get_or_init(|| {
let mut s = rustc_hash::FxHashSet::default();
for alias in self.graph.id_field_aliases.values() {
s.insert(InternedKey::from_str(alias).as_u64());
}
for alias in self.graph.title_field_aliases.values() {
s.insert(InternedKey::from_str(alias).as_u64());
}
s
});
if set.is_empty() {
return false;
}
set.contains(&InternedKey::from_str(property).as_u64())
}
pub fn with_max_work_units(mut self, max_work_units: Option<usize>) -> Self {
self.budget = ExecutionBudget::new(max_work_units);
self
}
pub fn with_row_limit(mut self, row_limit: Option<usize>) -> Self {
self.row_limit = row_limit;
self
}
#[inline]
pub(super) fn with_budget(mut self, budget: ExecutionBudget) -> Self {
self.budget = budget;
self
}
#[inline]
pub(super) fn budget_probe_limit(&self, requested: Option<usize>) -> Option<usize> {
let probe = self
.budget
.max_work_units()
.and_then(|max| max.checked_add(1));
match (requested, probe) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
#[inline]
pub(super) fn materializing_executor<'p>(
&'p self,
max_matches: Option<usize>,
pre_bindings: &'p Bindings<petgraph::graph::NodeIndex>,
operator: &'static str,
) -> PatternExecutor<'p> {
PatternExecutor::with_bindings_and_params(
self.graph,
max_matches,
pre_bindings,
self.params,
)
.set_deadline(self.deadline)
.set_cancel(self.cancel)
.set_parallel(self.parallel)
.set_match_ceiling(self.budget.match_ceiling(operator))
}
pub fn with_streaming(mut self, streaming: bool) -> Self {
self.streaming = streaming;
self
}
pub fn with_parallel(mut self, parallel: bool) -> Self {
self.parallel = parallel;
self
}
#[inline]
pub(super) fn interrupt(&self) -> crate::graph::algorithms::Interrupt {
crate::graph::algorithms::Interrupt {
deadline: self.deadline,
cancel: self.cancel,
}
}
pub fn with_cancel(mut self, cancel: Option<&'static AtomicBool>) -> Self {
self.cancel = cancel;
self
}
#[inline]
fn spatial_shard(&self, idx_raw: usize) -> &SpatialCacheShard {
const SHARDS: usize = 64;
let shards = self
.spatial_node_cache
.get_or_init(|| (0..SHARDS).map(|_| RwLock::new(HashMap::new())).collect());
&shards[idx_raw & (SHARDS - 1)]
}
#[inline]
pub(super) fn check_deadline(&self) -> Result<(), String> {
check_interrupt(&self.interrupt())
}
#[inline]
pub(super) fn check_interrupt_periodic(&self, iteration: usize) -> Result<(), String> {
const POLL_MASK: usize = INTERRUPT_POLL_INTERVAL - 1;
if iteration & POLL_MASK == 0 {
#[cfg(test)]
TEST_PERIODIC_POLLS_BEFORE_INTERRUPT.with(|remaining| {
if let Some(count) = remaining.get() {
if count == 0 {
remaining.set(None);
return Err("Query interrupted by test hook".to_string());
}
remaining.set(Some(count - 1));
}
Ok(())
})?;
self.check_deadline()?;
}
Ok(())
}
#[cfg(test)]
pub(super) fn interrupt_after_periodic_polls(polls: usize) {
TEST_PERIODIC_POLLS_BEFORE_INTERRUPT.with(|remaining| remaining.set(Some(polls)));
}
pub fn execute(&self, query: &CypherQuery) -> Result<CypherResult, String> {
self.execute_with_cap(query, self.row_limit)
}
fn execute_fused_count_by_type(
&self,
type_alias: &str,
count_alias: &str,
type_as_list: bool,
) -> Result<ResultSet, String> {
self.budget
.check_work(self.graph.graph.node_count(), "fused count by node type")?;
let mut result_rows = Vec::with_capacity(self.graph.type_indices.len());
for (node_type, indices) in self.graph.type_indices.iter() {
let mut projected = Bindings::with_capacity(2);
let type_value = if type_as_list {
Value::List(vec![Value::String(node_type.to_string())])
} else {
Value::String(node_type.to_string())
};
projected.insert(type_alias.to_string(), type_value);
projected.insert(count_alias.to_string(), Value::Int64(indices.len() as i64));
result_rows.push(ResultRow::from_projected(projected));
}
Ok(ResultSet {
rows: result_rows,
columns: vec![type_alias.to_string(), count_alias.to_string()],
lazy_return_items: None,
})
}
fn execute_fused_count_edges_by_type(
&self,
type_alias: &str,
count_alias: &str,
) -> Result<ResultSet, String> {
self.budget
.check_work(self.graph.graph.edge_count(), "fused count by edge type")?;
let counts = self.graph.get_edge_type_counts();
let mut result_rows = Vec::with_capacity(counts.len());
for (edge_type, count) in counts.iter() {
let mut projected = Bindings::with_capacity(2);
projected.insert(type_alias.to_string(), Value::String(edge_type.clone()));
projected.insert(count_alias.to_string(), Value::Int64(*count as i64));
result_rows.push(ResultRow::from_projected(projected));
}
Ok(ResultSet {
rows: result_rows,
columns: vec![type_alias.to_string(), count_alias.to_string()],
lazy_return_items: None,
})
}
fn execute_fused_count_typed_node(
&self,
node_type: &str,
alias: &str,
) -> Result<ResultSet, String> {
let count = self.graph.label_cardinality(node_type) as i64;
self.budget
.check_work(count as usize, "fused typed node count")?;
Ok(single_count_result(alias, count))
}
fn execute_fused_count_label_union(
&self,
labels: &[String],
alias: &str,
) -> Result<ResultSet, String> {
let count: i64 = labels
.iter()
.map(|label| self.graph.label_cardinality(label) as i64)
.sum();
self.budget
.check_work(count as usize, "fused label-union count")?;
Ok(single_count_result(alias, count))
}
fn execute_fused_count_typed_edge(
&self,
edge_type: &str,
alias: &str,
) -> Result<ResultSet, String> {
let counts = self.graph.get_edge_type_counts();
let count = counts.get(edge_type).copied().unwrap_or(0) as i64;
self.budget
.check_work(count as usize, "fused typed edge count")?;
Ok(single_count_result(alias, count))
}
fn execute_fused_count_anchored_edges(
&self,
anchor_idx: u32,
anchor_direction: petgraph::Direction,
edge_types: Option<&[String]>,
alias: &str,
) -> Result<ResultSet, String> {
let idx = petgraph::graph::NodeIndex::new(anchor_idx as usize);
let mut count: i64 = 0;
match edge_types {
None => {
count = self.graph.graph.count_edges_filtered(
idx,
anchor_direction,
None,
None,
self.deadline,
)? as i64;
}
Some(types) => {
for edge_type in types {
count += self.graph.graph.count_edges_filtered(
idx,
anchor_direction,
Some(InternedKey::from_str(edge_type)),
None,
self.deadline,
)? as i64;
}
}
}
self.budget
.check_work(count as usize, "fused anchored edge count")?;
Ok(single_count_result(alias, count))
}
fn execute_fused_count_clause(&self, clause: &Clause) -> Result<ResultSet, String> {
match clause {
Clause::FusedCountAll { alias } => {
self.budget
.check_work(self.graph.graph.node_count(), "fused node count")?;
let count = self.graph.graph.node_count() as i64;
Ok(single_count_result(alias, count))
}
Clause::FusedCountAllEdges { alias } => {
let edge_count = self.graph.graph.edge_count();
self.budget.check_work(edge_count, "fused all-edge count")?;
let count = i64::try_from(edge_count)
.map_err(|_| "edge count exceeds Cypher integer range".to_string())?;
Ok(single_count_result(alias, count))
}
Clause::FusedCountByType {
type_alias,
count_alias,
type_as_list,
} => self.execute_fused_count_by_type(type_alias, count_alias, *type_as_list),
Clause::FusedCountEdgesByType {
type_alias,
count_alias,
} => self.execute_fused_count_edges_by_type(type_alias, count_alias),
Clause::FusedCountTypedNode { node_type, alias } => {
self.execute_fused_count_typed_node(node_type, alias)
}
Clause::FusedCountLabelUnion { labels, alias } => {
self.execute_fused_count_label_union(labels, alias)
}
Clause::FusedCountTypedEdge { edge_type, alias } => {
self.execute_fused_count_typed_edge(edge_type, alias)
}
Clause::FusedCountAnchoredEdges {
anchor_idx,
anchor_direction,
edge_types,
alias,
} => self.execute_fused_count_anchored_edges(
*anchor_idx,
*anchor_direction,
edge_types.as_deref(),
alias,
),
_ => unreachable!("non-count clause routed to fused-count dispatcher"),
}
}
pub fn execute_single_clause(
&self,
clause: &Clause,
result_set: ResultSet,
) -> Result<ResultSet, String> {
match clause {
Clause::Match(m) => self.execute_match(m, result_set, None),
Clause::OptionalMatch(m) => self.execute_optional_match(m, result_set),
Clause::Where(w) => self.execute_where(w, result_set),
Clause::Filter(w) => self.execute_where(w, result_set),
Clause::Return(r) => self.execute_return(r, result_set),
Clause::Finish => Ok(ResultSet::new()),
Clause::With(w) => self.execute_with(w, result_set),
Clause::OrderBy(o) => self.execute_order_by(o, result_set),
Clause::Limit(l) => self.execute_limit(l, result_set),
Clause::Skip(s) => self.execute_skip(s, result_set),
Clause::Unwind(u) => self.execute_unwind(u, result_set),
Clause::LoadCsv(_) => Err(load_csv::MISDISPATCHED.to_string()),
Clause::Union(u) => self.execute_union(u, result_set),
Clause::FusedOptionalMatchAggregate {
match_clause,
with_clause,
} => {
self.budget.check_work(
self.graph.graph.node_count(),
"fused OPTIONAL MATCH aggregate",
)?;
self.execute_fused_optional_match_aggregate(match_clause, with_clause, result_set)
}
Clause::FusedVectorScoreTopK {
return_clause,
score_item_index,
descending,
limit,
} => self.execute_fused_vector_score_top_k(
return_clause,
*score_item_index,
*descending,
*limit,
result_set,
),
Clause::FusedTextBm25TopK {
return_clause,
score_item_index,
sort_keys,
limit,
} => self.execute_fused_text_bm25_top_k(
return_clause,
*score_item_index,
sort_keys,
*limit,
result_set,
),
Clause::FusedOrderByTopK {
return_clause,
sort_keys,
limit,
} => {
self.record_exact_ordering(sort_keys, &result_set, *limit)?;
self.execute_fused_order_by_top_k(return_clause, sort_keys, *limit, result_set)
}
Clause::FusedMatchReturnAggregate {
match_clause,
return_clause,
top_k,
candidate_emit,
distinct_count,
} => {
self.budget.check_work(
self.graph.graph.node_count(),
"fused MATCH/RETURN aggregate",
)?;
self.execute_fused_match_return_aggregate(
match_clause,
return_clause,
top_k,
candidate_emit,
*distinct_count,
result_set,
)
}
Clause::FusedMatchWithAggregate {
match_clause,
with_clause,
secondary_match,
top_k,
distinct_count,
} => {
self.budget
.check_work(self.graph.graph.node_count(), "fused MATCH/WITH aggregate")?;
self.execute_fused_match_with_aggregate(
match_clause,
with_clause,
secondary_match.as_ref(),
top_k.as_ref(),
*distinct_count,
result_set,
)
}
Clause::FusedCountAll { .. }
| Clause::FusedCountAllEdges { .. }
| Clause::FusedCountByType { .. }
| Clause::FusedCountEdgesByType { .. }
| Clause::FusedCountTypedNode { .. }
| Clause::FusedCountLabelUnion { .. }
| Clause::FusedCountTypedEdge { .. }
| Clause::FusedCountAnchoredEdges { .. } => self.execute_fused_count_clause(clause),
Clause::FusedNodeScanAggregate {
match_clause,
where_predicate,
return_clause,
} => {
self.budget
.check_work(self.graph.graph.node_count(), "fused node-scan aggregate")?;
self.execute_fused_node_scan_aggregate(
match_clause,
where_predicate.as_ref(),
return_clause,
)
}
Clause::FusedNodeScanTopK {
match_clause,
where_predicate,
return_clause,
sort_keys,
limit,
} => {
self.budget
.check_work(self.graph.graph.node_count(), "fused node-scan top-k")?;
self.execute_fused_node_scan_top_k(
match_clause,
where_predicate.as_ref(),
return_clause,
sort_keys,
*limit,
)
}
Clause::SpatialJoin {
container_var,
probe_var,
container_type,
probe_type,
probe_kind,
remainder,
} => self.execute_spatial_join(
container_var,
probe_var,
container_type,
probe_type,
*probe_kind,
remainder.as_ref(),
),
Clause::Call(c) => self.execute_call(c, result_set),
Clause::CallSubquery { import, body } => {
let declared = declared_from_rows(&result_set);
self.execute_call_subquery(import, body, result_set, &declared)
}
Clause::Schema(command) if schema_ddl::is_schema_read(command) => {
schema_ddl::execute_schema_read(self.graph, command)
}
Clause::Create(_)
| Clause::Set(_)
| Clause::Delete(_)
| Clause::Remove(_)
| Clause::Merge(_)
| Clause::Foreach { .. }
| Clause::Schema(_) => {
Err("Mutation clauses cannot be executed in read-only mode".to_string())
}
}
}
}
pub mod affected_tests;
mod analysis_procedures;
pub(crate) mod budget;
pub mod call_clause;
pub mod call_subquery;
mod cdc_procedures;
mod centrality_procedures;
mod clause_pipeline;
mod columnar_write;
#[cfg(test)]
#[path = "comparison_tristate_tests.rs"]
mod comparison_tristate_tests;
pub mod dead_code;
mod edge_property_write;
mod execution_support;
pub mod expression;
pub mod helpers;
mod identity_fields;
mod interrupt;
#[cfg(test)]
#[path = "keys_map_tests.rs"]
mod keys_map_tests;
pub mod load_csv;
pub mod match_clause;
pub mod match_execution;
mod node_ontology;
pub(crate) mod ontology_procedures;
pub(crate) mod ordering;
mod procedure_registry;
pub mod refresh_stats;
pub mod regex_cache;
mod rel_constraint_ddl;
mod retrieval;
mod retrieval_diagnostics;
mod retrieval_text;
pub mod return_clause;
pub mod rev_procedures;
pub mod rule_procedures;
pub mod scalar_functions;
mod scan_eval;
pub(crate) mod schema_ddl;
mod schema_procedures;
mod set_path;
mod set_row;
pub mod shortest_path;
pub(crate) mod show_indexes;
mod show_ontology;
pub mod spatial_join;
pub mod stream;
mod table_procedures;
#[cfg(test)]
pub mod tests;
pub mod transient_index;
mod vector_options;
pub mod where_clause;
pub mod write;
pub(crate) mod write_scope;
pub(super) use clause_pipeline::order_by_scope_after;
pub use execution_support::clause_display_name;
pub use helpers::return_item_column_name;
pub use write::is_mutation_query;
fn single_count_result(alias: &str, count: i64) -> ResultSet {
let mut projected = Bindings::with_capacity(1);
projected.insert(alias.to_string(), Value::Int64(count));
ResultSet {
rows: vec![ResultRow::from_projected(projected)],
columns: vec![alias.to_string()],
lazy_return_items: None,
}
}
fn declared_from_rows(result_set: &ResultSet) -> std::collections::HashSet<String> {
let mut declared = std::collections::HashSet::new();
for row in &result_set.rows {
for k in row.node_bindings.keys() {
declared.insert(k.clone());
}
for k in row.edge_bindings.keys() {
declared.insert(k.clone());
}
for k in row.path_bindings.keys() {
declared.insert(k.clone());
}
for k in row.projected.keys() {
declared.insert(k.clone());
}
}
declared
}