use selene_core::DbString;
use crate::{
BinaryOp, GqlType, Literal, SourceSpan, ValueExpr,
analyze::BindingId,
plan::{BindingDef, FilterPredicate, FilterPredicateKind},
};
#[derive(Clone, Debug)]
pub(crate) struct ParameterRef<'a> {
pub name: DbString,
pub declared_type: Option<&'a GqlType>,
pub span: SourceSpan,
}
#[derive(Clone, Debug)]
pub(crate) enum PropertyPredicateShape<'a> {
Equality(&'a ValueExpr),
Comparison {
op: BinaryOp,
value: &'a ValueExpr,
},
InList(Vec<&'a ValueExpr>),
InListExpression(&'a ValueExpr),
}
#[derive(Clone, Debug)]
pub(crate) struct MatchedPropertyPredicate<'a> {
pub binding: BindingId,
pub key: DbString,
pub shape: PropertyPredicateShape<'a>,
}
pub(crate) fn collect_binding_refs(
expr: &ValueExpr,
bindings: &[BindingDef],
) -> Option<Vec<BindingId>> {
let mut refs = Vec::new();
let mut unresolved = false;
walk_expr(expr, &mut |sub| match sub {
ValueExpr::Variable { name, .. } => {
if let Some(binding) = bindings
.iter()
.find(|binding| binding.name == *name)
.map(|binding| binding.binding)
{
refs.push(binding);
} else {
unresolved = true;
}
}
ValueExpr::Exists { .. } | ValueExpr::ValueSubquery { .. } => {
unresolved = true;
}
_ => {}
});
if unresolved {
return None;
}
refs.sort();
refs.dedup();
Some(refs)
}
pub(crate) fn match_property_predicate<'a>(
pred: &'a FilterPredicate,
bindings: &'a [BindingDef],
) -> Option<MatchedPropertyPredicate<'a>> {
match &pred.kind {
FilterPredicateKind::PropertyEquals {
binding: Some(binding),
key,
} => Some(MatchedPropertyPredicate {
binding: *binding,
key: key.clone(),
shape: PropertyPredicateShape::Equality(&pred.expr),
}),
FilterPredicateKind::Expression => match_property_expr(&pred.expr, bindings),
FilterPredicateKind::PropertyEquals { binding: None, .. } => None,
}
}
pub(crate) fn match_property_access(
expr: &ValueExpr,
bindings: &[BindingDef],
) -> Option<(BindingId, DbString)> {
let ValueExpr::PropertyAccess { target, key, .. } = expr else {
return None;
};
let ValueExpr::Variable { name, .. } = target.as_ref() else {
return None;
};
bindings
.iter()
.find(|binding| binding.name == *name)
.map(|binding| (binding.binding, key.clone()))
}
pub(crate) fn literal(expr: &ValueExpr) -> Option<&Literal> {
let ValueExpr::Literal(literal) = expr else {
return None;
};
(!matches!(literal, Literal::Null(_))).then_some(literal)
}
pub(crate) fn parameter(expr: &ValueExpr) -> Option<ParameterRef<'_>> {
let ValueExpr::Parameter {
name,
declared_type,
span,
} = expr
else {
return None;
};
Some(ParameterRef {
name: name.clone(),
declared_type: declared_type.as_ref(),
span: *span,
})
}
fn match_property_expr<'a>(
expr: &'a ValueExpr,
bindings: &'a [BindingDef],
) -> Option<MatchedPropertyPredicate<'a>> {
match expr {
ValueExpr::BinaryOp { op, lhs, rhs, .. }
if matches!(
op,
BinaryOp::Eq | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge
) =>
{
match_binary_property(*op, lhs, rhs, bindings)
}
ValueExpr::InList {
operand,
list,
negated: false,
..
} => {
let (binding, key) = match_property_access(operand, bindings)?;
Some(MatchedPropertyPredicate {
binding,
key,
shape: PropertyPredicateShape::InList(list.iter().collect()),
})
}
ValueExpr::InListExpression {
operand,
list,
negated: false,
..
} => {
let (binding, key) = match_property_access(operand, bindings)?;
Some(MatchedPropertyPredicate {
binding,
key,
shape: PropertyPredicateShape::InListExpression(list),
})
}
_ => None,
}
}
fn match_binary_property<'a>(
op: BinaryOp,
lhs: &'a ValueExpr,
rhs: &'a ValueExpr,
bindings: &'a [BindingDef],
) -> Option<MatchedPropertyPredicate<'a>> {
if let Some((binding, key)) = match_property_access(lhs, bindings) {
return Some(MatchedPropertyPredicate {
binding,
key,
shape: match op {
BinaryOp::Eq => PropertyPredicateShape::Equality(rhs),
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
PropertyPredicateShape::Comparison { op, value: rhs }
}
_ => return None,
},
});
}
let (binding, key) = match_property_access(rhs, bindings)?;
Some(MatchedPropertyPredicate {
binding,
key,
shape: match op {
BinaryOp::Eq => PropertyPredicateShape::Equality(lhs),
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
PropertyPredicateShape::Comparison {
op: reverse_comparison(op),
value: lhs,
}
}
_ => return None,
},
})
}
fn reverse_comparison(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Lt => BinaryOp::Gt,
BinaryOp::Le => BinaryOp::Ge,
BinaryOp::Gt => BinaryOp::Lt,
BinaryOp::Ge => BinaryOp::Le,
other => other,
}
}
fn walk_expr(expr: &ValueExpr, visit: &mut impl FnMut(&ValueExpr)) {
visit(expr);
expr.for_each_child(&mut |child| walk_expr(child, visit));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::IsCheckKind;
use crate::analyze::types::AnalyzedType;
use crate::plan::BindingElement;
fn name_of(name: &str) -> selene_core::DbString {
selene_core::db_string(name).unwrap()
}
fn binding_def(name: &str, raw: u32, element: BindingElement) -> BindingDef {
BindingDef {
binding: BindingId::new(raw),
name: name_of(name),
element,
ty: AnalyzedType::Dynamic,
label_predicate: None,
span: SourceSpan::new(0, 1),
}
}
fn var(name: &str) -> ValueExpr {
ValueExpr::Variable {
name: name_of(name),
span: SourceSpan::new(0, 1),
}
}
#[test]
fn collect_binding_refs_includes_edge_in_is_source_of() {
let bindings = [
binding_def("n", 0, BindingElement::Node),
binding_def("e", 1, BindingElement::Edge),
];
let expr = ValueExpr::IsCheck {
operand: Box::new(var("n")),
kind: IsCheckKind::SourceOf(Box::new(var("e"))),
negated: false,
span: SourceSpan::new(0, 1),
};
let refs = collect_binding_refs(&expr, &bindings).expect("all variables resolve");
assert_eq!(refs, vec![BindingId::new(0), BindingId::new(1)]);
}
#[test]
fn collect_binding_refs_includes_edge_in_is_destination_of() {
let bindings = [
binding_def("n", 0, BindingElement::Node),
binding_def("e", 1, BindingElement::Edge),
];
let expr = ValueExpr::IsCheck {
operand: Box::new(var("n")),
kind: IsCheckKind::DestinationOf(Box::new(var("e"))),
negated: false,
span: SourceSpan::new(0, 1),
};
let refs = collect_binding_refs(&expr, &bindings).expect("all variables resolve");
assert_eq!(refs, vec![BindingId::new(0), BindingId::new(1)]);
}
}