use super::super::ast::{ComparisonOp, Expression, Predicate};
use super::helpers::evaluate_comparison;
use super::CypherExecutor;
use crate::datatypes::values::Value;
use crate::graph::core::filtering::str_values_equal;
use crate::graph::core::membership::MembershipSet;
use crate::graph::languages::cypher::result::ResultRow;
use crate::graph::schema::{soft_alias_fallback, DirGraph, InternedKey, SoftAliasFallback};
use crate::graph::storage::{ColumnStore, GraphRead, NodeView, StrField};
use petgraph::graph::NodeIndex;
use std::borrow::Cow;
use std::sync::Arc;
#[derive(Clone, Copy, Debug)]
enum PropRoute {
Id,
Title,
Stored(InternedKey),
SoftTitle(InternedKey),
SoftType(InternedKey),
}
impl PropRoute {
fn resolve(graph: &DirGraph, type_str: &str, property: &str) -> PropRoute {
let resolved = graph.resolve_alias(type_str, property);
match resolved {
"id" => PropRoute::Id,
"title" => PropRoute::Title,
_ => {
let key = InternedKey::from_str(resolved);
match soft_alias_fallback(resolved) {
None => PropRoute::Stored(key),
Some(SoftAliasFallback::Title) => PropRoute::SoftTitle(key),
Some(SoftAliasFallback::TypeString) => PropRoute::SoftType(key),
}
}
}
}
#[inline]
fn read(self, node: NodeView<'_>, type_str: &str) -> Value {
match self {
PropRoute::Id => node.id().into_owned(),
PropRoute::Title => node.title().into_owned(),
PropRoute::Stored(key) => node.get_value(key).unwrap_or(Value::Null),
PropRoute::SoftTitle(key) => node
.get_value(key)
.unwrap_or_else(|| node.title().into_owned()),
PropRoute::SoftType(key) => node
.get_value(key)
.unwrap_or_else(|| Value::String(type_str.to_string())),
}
}
#[inline]
fn read_str<'v>(self, node: NodeView<'v>, type_str: &'v str) -> StrField<'v> {
match self {
PropRoute::Id => node.id_field(),
PropRoute::Title => node.title_field(),
PropRoute::Stored(key) => node.str_field(key),
PropRoute::SoftTitle(key) => match node.str_field(key) {
StrField::Absent => node.title_field(),
resolved => resolved,
},
PropRoute::SoftType(key) => match node.str_field(key) {
StrField::Absent => StrField::Str(Cow::Borrowed(type_str)),
resolved => resolved,
},
}
}
}
pub(super) struct ScanRuntime<'g> {
names: Vec<&'g str>,
routes: Vec<PropRoute>,
current_type: Option<InternedKey>,
type_str: &'g str,
store: Option<(InternedKey, Option<&'g Arc<ColumnStore>>)>,
}
impl<'g> ScanRuntime<'g> {
pub(super) fn fork(&self) -> ScanRuntime<'g> {
ScanRuntime {
names: self.names.clone(),
routes: Vec::with_capacity(self.names.len()),
current_type: None,
type_str: "",
store: None,
}
}
pub(super) fn is_empty(&self) -> bool {
self.names.is_empty()
}
#[inline]
pub(super) fn bind(&mut self, graph: &'g DirGraph, idx: NodeIndex) -> Option<NodeView<'g>> {
let data = graph.graph.node_weight(idx)?;
let type_key = data.node_type;
let store = match self.store {
Some((memo_key, store)) if memo_key == type_key => store,
_ => {
let store = graph.graph.column_store(type_key);
self.store = Some((type_key, store));
store
}
};
if self.current_type != Some(type_key) {
self.retarget(graph, type_key);
}
let resolved = data
.properties
.columnar_row_id()
.and_then(|row_id| store.map(|store| (&**store, row_id)));
Some(NodeView::new(data, resolved))
}
#[cold]
fn retarget(&mut self, graph: &'g DirGraph, type_key: InternedKey) {
self.type_str = graph.interner.try_resolve(type_key).unwrap_or("");
self.routes.clear();
for slot in 0..self.names.len() {
self.routes
.push(PropRoute::resolve(graph, self.type_str, self.names[slot]));
}
self.current_type = Some(type_key);
}
#[inline]
fn read(&self, node: Option<NodeView<'_>>, slot: usize) -> Value {
match node {
Some(node) => self.routes[slot].read(node, self.type_str),
None => Value::Null,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum ScanBinOp {
Add,
Subtract,
Multiply,
Divide,
Modulo,
Concat,
}
pub(super) enum ScanExpr<'q> {
Prop(usize),
Const(Value),
Binary(ScanBinOp, Box<ScanExpr<'q>>, Box<ScanExpr<'q>>),
Negate(Box<ScanExpr<'q>>),
Generic(&'q Expression),
}
pub(super) enum ScanPred<'q> {
And(Box<ScanPred<'q>>, Box<ScanPred<'q>>),
Or(Box<ScanPred<'q>>, Box<ScanPred<'q>>),
Xor(Box<ScanPred<'q>>, Box<ScanPred<'q>>),
Not(Box<ScanPred<'q>>),
Comparison {
left: ScanExpr<'q>,
operator: ComparisonOp,
right: ScanExpr<'q>,
},
StrCmp {
slot: usize,
op: StrOp,
needle: String,
},
IsNull(ScanExpr<'q>),
IsNotNull(ScanExpr<'q>),
InLiteralSet {
expr: ScanExpr<'q>,
values: &'q MembershipSet,
},
Generic(&'q Predicate),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum StrOp {
Equals,
NotEquals,
LessThan,
LessThanEq,
GreaterThan,
GreaterThanEq,
StartsWith,
EndsWith,
Contains,
}
impl StrOp {
fn as_comparison(self) -> Option<ComparisonOp> {
Some(match self {
StrOp::Equals => ComparisonOp::Equals,
StrOp::NotEquals => ComparisonOp::NotEquals,
StrOp::LessThan => ComparisonOp::LessThan,
StrOp::LessThanEq => ComparisonOp::LessThanEq,
StrOp::GreaterThan => ComparisonOp::GreaterThan,
StrOp::GreaterThanEq => ComparisonOp::GreaterThanEq,
StrOp::StartsWith | StrOp::EndsWith | StrOp::Contains => return None,
})
}
fn flipped(self) -> Self {
match self {
StrOp::LessThan => StrOp::GreaterThan,
StrOp::LessThanEq => StrOp::GreaterThanEq,
StrOp::GreaterThan => StrOp::LessThan,
StrOp::GreaterThanEq => StrOp::LessThanEq,
other => other,
}
}
#[inline]
fn test(self, value: &str, needle: &str) -> bool {
match self {
StrOp::Equals => str_values_equal(value, needle),
StrOp::NotEquals => !str_values_equal(value, needle),
StrOp::LessThan => value < needle,
StrOp::LessThanEq => value <= needle,
StrOp::GreaterThan => value > needle,
StrOp::GreaterThanEq => value >= needle,
StrOp::StartsWith => value.starts_with(needle),
StrOp::EndsWith => value.ends_with(needle),
StrOp::Contains => value.contains(needle),
}
}
}
impl ScanExpr<'_> {
pub(super) fn is_compiled(&self) -> bool {
match self {
ScanExpr::Prop(_) | ScanExpr::Const(_) => true,
ScanExpr::Binary(_, lhs, rhs) => lhs.is_compiled() && rhs.is_compiled(),
ScanExpr::Negate(inner) => inner.is_compiled(),
ScanExpr::Generic(_) => false,
}
}
}
impl ScanPred<'_> {
pub(super) fn is_compiled(&self) -> bool {
match self {
ScanPred::And(lhs, rhs) | ScanPred::Or(lhs, rhs) | ScanPred::Xor(lhs, rhs) => {
lhs.is_compiled() && rhs.is_compiled()
}
ScanPred::Not(inner) => inner.is_compiled(),
ScanPred::Comparison { left, right, .. } => left.is_compiled() && right.is_compiled(),
ScanPred::StrCmp { .. } => true,
ScanPred::IsNull(expr) | ScanPred::IsNotNull(expr) => expr.is_compiled(),
ScanPred::InLiteralSet { expr, .. } => expr.is_compiled(),
ScanPred::Generic(_) => false,
}
}
}
pub(super) struct ScanCompiler<'q> {
node_var: &'q str,
names: Vec<&'q str>,
enabled: bool,
}
impl<'q> ScanCompiler<'q> {
pub(super) fn new(executor: &CypherExecutor<'_>, node_var: &'q str) -> Self {
let enabled = !executor.graph.graph.is_disk() && executor.graph.spatial_configs.is_empty();
ScanCompiler {
node_var,
names: Vec::new(),
enabled,
}
}
pub(super) fn finish(self) -> ScanRuntime<'q> {
ScanRuntime {
routes: Vec::with_capacity(self.names.len()),
names: self.names,
current_type: None,
type_str: "",
store: None,
}
}
fn slot(&mut self, property: &'q str) -> usize {
match self.names.iter().position(|name| *name == property) {
Some(slot) => slot,
None => {
self.names.push(property);
self.names.len() - 1
}
}
}
fn prop_slot(&mut self, expr: &'q Expression) -> Option<usize> {
if !self.enabled {
return None;
}
let Expression::PropertyAccess { variable, property } = expr else {
return None;
};
if variable != self.node_var {
return None;
}
Some(self.slot(property))
}
pub(super) fn expr(&mut self, expr: &'q Expression) -> ScanExpr<'q> {
if let Some(slot) = self.prop_slot(expr) {
return ScanExpr::Prop(slot);
}
if !self.enabled {
return ScanExpr::Generic(expr);
}
match expr {
Expression::Literal(value) => ScanExpr::Const(value.clone()),
Expression::Add(left, right) => self.binary(ScanBinOp::Add, left, right, expr),
Expression::Subtract(left, right) => {
self.binary(ScanBinOp::Subtract, left, right, expr)
}
Expression::Multiply(left, right) => {
self.binary(ScanBinOp::Multiply, left, right, expr)
}
Expression::Divide(left, right) => self.binary(ScanBinOp::Divide, left, right, expr),
Expression::Modulo(left, right) => self.binary(ScanBinOp::Modulo, left, right, expr),
Expression::Concat(left, right) => self.binary(ScanBinOp::Concat, left, right, expr),
Expression::Negate(inner) => {
let inner = self.expr(inner);
if inner.is_generic() {
ScanExpr::Generic(expr)
} else {
ScanExpr::Negate(Box::new(inner))
}
}
_ => ScanExpr::Generic(expr),
}
}
fn binary(
&mut self,
op: ScanBinOp,
left: &'q Expression,
right: &'q Expression,
whole: &'q Expression,
) -> ScanExpr<'q> {
let left = self.expr(left);
let right = self.expr(right);
if left.is_generic() && right.is_generic() {
return ScanExpr::Generic(whole);
}
ScanExpr::Binary(op, Box::new(left), Box::new(right))
}
pub(super) fn pred(&mut self, pred: &'q Predicate) -> ScanPred<'q> {
if !self.enabled {
return ScanPred::Generic(pred);
}
match pred {
Predicate::And(left, right) => {
ScanPred::And(Box::new(self.pred(left)), Box::new(self.pred(right)))
}
Predicate::Or(left, right) => {
ScanPred::Or(Box::new(self.pred(left)), Box::new(self.pred(right)))
}
Predicate::Xor(left, right) => {
ScanPred::Xor(Box::new(self.pred(left)), Box::new(self.pred(right)))
}
Predicate::Not(inner) => ScanPred::Not(Box::new(self.pred(inner))),
Predicate::Comparison {
left,
operator,
right,
} => self.comparison(pred, left, *operator, right),
Predicate::StartsWith { expr, pattern } => {
self.text(pred, expr, pattern, StrOp::StartsWith)
}
Predicate::EndsWith { expr, pattern } => {
self.text(pred, expr, pattern, StrOp::EndsWith)
}
Predicate::Contains { expr, pattern } => {
self.text(pred, expr, pattern, StrOp::Contains)
}
Predicate::IsNull(expr) => match self.expr(expr) {
ScanExpr::Generic(_) => ScanPred::Generic(pred),
compiled => ScanPred::IsNull(compiled),
},
Predicate::IsNotNull(expr) => match self.expr(expr) {
ScanExpr::Generic(_) => ScanPred::Generic(pred),
compiled => ScanPred::IsNotNull(compiled),
},
Predicate::InLiteralSet { expr, values } => match self.expr(expr) {
ScanExpr::Generic(_) => ScanPred::Generic(pred),
compiled => ScanPred::InLiteralSet {
expr: compiled,
values,
},
},
_ => ScanPred::Generic(pred),
}
}
fn comparison(
&mut self,
whole: &'q Predicate,
left: &'q Expression,
operator: ComparisonOp,
right: &'q Expression,
) -> ScanPred<'q> {
let str_op = match operator {
ComparisonOp::Equals => Some(StrOp::Equals),
ComparisonOp::NotEquals => Some(StrOp::NotEquals),
ComparisonOp::LessThan => Some(StrOp::LessThan),
ComparisonOp::LessThanEq => Some(StrOp::LessThanEq),
ComparisonOp::GreaterThan => Some(StrOp::GreaterThan),
ComparisonOp::GreaterThanEq => Some(StrOp::GreaterThanEq),
ComparisonOp::RegexMatch => None,
};
if let Some(op) = str_op {
if let Some(pred) = self.str_cmp(left, op, right) {
return pred;
}
if let Some(pred) = self.str_cmp(right, op.flipped(), left) {
return pred;
}
}
let left = self.expr(left);
let right = self.expr(right);
if left.is_generic() && right.is_generic() {
return ScanPred::Generic(whole);
}
ScanPred::Comparison {
left,
operator,
right,
}
}
fn str_cmp(
&mut self,
prop_side: &'q Expression,
op: StrOp,
literal_side: &Expression,
) -> Option<ScanPred<'q>> {
let Expression::Literal(Value::String(needle)) = literal_side else {
return None;
};
let slot = self.prop_slot(prop_side)?;
Some(ScanPred::StrCmp {
slot,
op,
needle: needle.clone(),
})
}
fn text(
&mut self,
whole: &'q Predicate,
expr: &'q Expression,
pattern: &'q Expression,
op: StrOp,
) -> ScanPred<'q> {
self.str_cmp(expr, op, pattern)
.unwrap_or(ScanPred::Generic(whole))
}
}
impl ScanExpr<'_> {
fn is_generic(&self) -> bool {
matches!(self, ScanExpr::Generic(_))
}
pub(super) fn eval(
&self,
executor: &CypherExecutor<'_>,
runtime: &ScanRuntime<'_>,
node: Option<NodeView<'_>>,
row: &ResultRow,
) -> Result<Value, String> {
use crate::graph::core::value_operations as ops;
match self {
ScanExpr::Prop(slot) => Ok(runtime.read(node, *slot)),
ScanExpr::Const(value) => Ok(value.clone()),
ScanExpr::Generic(expr) => executor.evaluate_expression(expr, row),
ScanExpr::Negate(inner) => {
super::helpers::arithmetic_negate(&inner.eval(executor, runtime, node, row)?)
}
ScanExpr::Binary(op, left, right) => {
let left = left.eval(executor, runtime, node, row)?;
let right = right.eval(executor, runtime, node, row)?;
match op {
ScanBinOp::Add => ops::arithmetic_add_checked(&left, &right),
ScanBinOp::Subtract => ops::arithmetic_sub_checked(&left, &right),
ScanBinOp::Multiply => ops::arithmetic_mul_checked(&left, &right),
ScanBinOp::Divide => super::helpers::arithmetic_div(&left, &right),
ScanBinOp::Modulo => super::helpers::arithmetic_mod(&left, &right),
ScanBinOp::Concat => Ok(ops::string_concat(&left, &right)),
}
}
}
}
}
impl ScanPred<'_> {
pub(super) fn eval(
&self,
executor: &CypherExecutor<'_>,
runtime: &ScanRuntime<'_>,
node: Option<NodeView<'_>>,
row: &ResultRow,
) -> Result<Option<bool>, String> {
match self {
ScanPred::Generic(pred) => executor.evaluate_predicate_tristate(pred, row),
ScanPred::And(left, right) => {
let lv = left.eval(executor, runtime, node, row)?;
if lv == Some(false) {
return Ok(Some(false));
}
let rv = right.eval(executor, runtime, node, row)?;
if rv == Some(false) {
return Ok(Some(false));
}
if lv.is_none() || rv.is_none() {
return Ok(None);
}
Ok(Some(true))
}
ScanPred::Or(left, right) => {
let lv = left.eval(executor, runtime, node, row)?;
if lv == Some(true) {
return Ok(Some(true));
}
let rv = right.eval(executor, runtime, node, row)?;
if rv == Some(true) {
return Ok(Some(true));
}
if lv.is_none() || rv.is_none() {
return Ok(None);
}
Ok(Some(false))
}
ScanPred::Xor(left, right) => {
let lv = left.eval(executor, runtime, node, row)?;
let rv = right.eval(executor, runtime, node, row)?;
match (lv, rv) {
(Some(a), Some(b)) => Ok(Some(a ^ b)),
_ => Ok(None),
}
}
ScanPred::Not(inner) => Ok(inner.eval(executor, runtime, node, row)?.map(|b| !b)),
ScanPred::Comparison {
left,
operator,
right,
} => {
let left = left.eval(executor, runtime, node, row)?;
let right = right.eval(executor, runtime, node, row)?;
if matches!(left, Value::Null) || matches!(right, Value::Null) {
return Ok(None);
}
evaluate_comparison(&left, operator, &right).map(Some)
}
ScanPred::IsNull(expr) => Ok(Some(matches!(
expr.eval(executor, runtime, node, row)?,
Value::Null
))),
ScanPred::IsNotNull(expr) => Ok(Some(!matches!(
expr.eval(executor, runtime, node, row)?,
Value::Null
))),
ScanPred::InLiteralSet { expr, values } => {
let value = expr.eval(executor, runtime, node, row)?;
if matches!(value, Value::Null) {
return Ok(None);
}
if values.matches(&value) {
return Ok(Some(true));
}
if values.has_null() {
return Ok(None);
}
Ok(Some(false))
}
ScanPred::StrCmp { slot, op, needle } => {
Self::eval_str_cmp(runtime, node, *slot, *op, needle)
}
}
}
pub(super) fn keeps_row(
&self,
executor: &CypherExecutor<'_>,
runtime: &ScanRuntime<'_>,
node: Option<NodeView<'_>>,
row: &ResultRow,
) -> Result<bool, String> {
match self.eval(executor, runtime, node, row) {
Ok(outcome) => Ok(outcome == Some(true)),
Err(e) if super::helpers::is_user_input_error(&e) => Err(e),
Err(_) => Ok(false),
}
}
#[inline]
fn eval_str_cmp(
runtime: &ScanRuntime<'_>,
node: Option<NodeView<'_>>,
slot: usize,
op: StrOp,
needle: &str,
) -> Result<Option<bool>, String> {
let Some(node) = node else {
return Ok(None);
};
let route = runtime.routes[slot];
match route.read_str(node, runtime.type_str) {
StrField::Str(value) => Ok(Some(op.test(&value, needle))),
StrField::Absent => Ok(None),
StrField::NotString => {
let value = route.read(node, runtime.type_str);
if matches!(value, Value::Null) {
return Ok(None);
}
let needle = Value::String(needle.to_string());
match op.as_comparison() {
Some(operator) => evaluate_comparison(&value, &operator, &needle).map(Some),
None => Ok(Some(false)),
}
}
}
}
}