use crate::expr::expression::Expr;
use crate::expr::idiom::Idiom;
use crate::expr::part::Part;
use crate::expr::visit::{Visit, Visitor};
pub(crate) const ROW_SCOPED_PARAMS: &[&str] = &["this", "self", "parent"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RowScopeKind {
This,
Self_,
Parent,
}
impl RowScopeKind {
pub(crate) fn from_param_name(name: &str) -> Option<Self> {
match name {
"this" => Some(RowScopeKind::This),
"self" => Some(RowScopeKind::Self_),
"parent" => Some(RowScopeKind::Parent),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IdiomRoot {
ThisRow,
OuterRow,
Opaque,
}
pub(crate) fn classify_idiom_root(idiom: &Idiom) -> IdiomRoot {
match idiom.0.first() {
None => IdiomRoot::ThisRow,
Some(Part::Start(Expr::Param(p))) => match RowScopeKind::from_param_name(p.as_str()) {
Some(RowScopeKind::This | RowScopeKind::Self_) => IdiomRoot::ThisRow,
Some(RowScopeKind::Parent) => IdiomRoot::OuterRow,
None => IdiomRoot::Opaque,
},
Some(Part::Start(_)) => IdiomRoot::Opaque,
Some(_) => IdiomRoot::ThisRow,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AnalysisScope {
CurrentSelect,
Recursive,
}
pub(crate) fn references_parent(expr: &Expr) -> bool {
let mut walker =
ParamWalker::new(|kind| kind == RowScopeKind::Parent, AnalysisScope::CurrentSelect);
let _ = walker.visit_expr(expr);
walker.found
}
pub(crate) fn first_row_scoped_reference(
expr: &Expr,
scope: AnalysisScope,
) -> Option<RowScopeKind> {
let mut walker = ParamWalker::new(|_| true, scope);
let _ = walker.visit_expr(expr);
walker.first_kind
}
struct ParamWalker<F: FnMut(RowScopeKind) -> bool> {
predicate: F,
scope: AnalysisScope,
found: bool,
first_kind: Option<RowScopeKind>,
}
impl<F: FnMut(RowScopeKind) -> bool> ParamWalker<F> {
fn new(predicate: F, scope: AnalysisScope) -> Self {
Self {
predicate,
scope,
found: false,
first_kind: None,
}
}
}
impl<F: FnMut(RowScopeKind) -> bool> Visitor for ParamWalker<F> {
type Error = std::convert::Infallible;
fn visit_expr(&mut self, e: &Expr) -> Result<(), Self::Error> {
if self.found {
return Ok(());
}
if let Expr::Param(p) = e
&& let Some(kind) = RowScopeKind::from_param_name(p.as_str())
{
if self.first_kind.is_none() {
self.first_kind = Some(kind);
}
if (self.predicate)(kind) {
self.found = true;
return Ok(());
}
}
e.visit(self)
}
fn visit_select(
&mut self,
s: &crate::expr::statements::SelectStatement,
) -> Result<(), Self::Error> {
match self.scope {
AnalysisScope::CurrentSelect => Ok(()),
AnalysisScope::Recursive => s.visit(self),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syn;
fn parse_expr(src: &str) -> Expr {
let ast = syn::parse(&format!("RETURN {src};")).expect("parse");
let mut exprs = ast.expressions;
assert_eq!(exprs.len(), 1, "expected one statement");
let top: crate::expr::TopLevelExpr = exprs.remove(0).into();
match top {
crate::expr::TopLevelExpr::Expr(Expr::Return(ret)) => ret.what.clone(),
other => panic!("unexpected statement shape: {other:?}"),
}
}
fn parse_idiom(src: &str) -> Idiom {
match parse_expr(src) {
Expr::Idiom(i) => i,
other => panic!("expected Idiom, got {other:?}"),
}
}
#[test]
fn references_parent_top_level() {
assert!(references_parent(&parse_expr("$parent.x")));
assert_eq!(
first_row_scoped_reference(&parse_expr("$parent.x"), AnalysisScope::CurrentSelect),
Some(RowScopeKind::Parent),
);
}
#[test]
fn references_parent_in_method_arg() {
assert!(references_parent(&parse_expr("array::find($parent.refs, 'foo')")));
}
#[test]
fn references_parent_in_graph_where() {
assert!(references_parent(&parse_expr("user->knows[WHERE out = $parent.id]")));
}
#[test]
fn references_parent_skips_select_subquery() {
let e = parse_expr("(SELECT * FROM $parent->edge ORDER BY x)");
assert!(!references_parent(&e));
}
#[test]
fn references_parent_skips_create_subquery() {
let e = parse_expr("(CREATE x SET y = $parent.id)");
assert!(references_parent(&e));
}
#[test]
fn references_parent_deep_path() {
assert!(references_parent(&parse_expr("$parent.visibility.tags")));
}
#[test]
fn nested_select_does_not_propagate_inner_parent() {
let e = parse_expr("(SELECT (SELECT * FROM tbl WHERE id = $parent.id) FROM x)");
assert!(!references_parent(&e));
}
#[test]
fn bare_parent_field_is_not_param() {
let e = parse_expr("parent.sub");
assert!(!references_parent(&e));
assert!(first_row_scoped_reference(&e, AnalysisScope::Recursive).is_none());
}
#[test]
fn classify_idiom_root_this() {
assert_eq!(classify_idiom_root(&parse_idiom("$this.cat")), IdiomRoot::ThisRow);
assert_eq!(classify_idiom_root(&parse_idiom("$self.cat")), IdiomRoot::ThisRow);
}
#[test]
fn classify_idiom_root_parent() {
assert_eq!(classify_idiom_root(&parse_idiom("$parent.cat")), IdiomRoot::OuterRow);
}
#[test]
fn classify_idiom_root_unrooted() {
assert_eq!(classify_idiom_root(&parse_idiom("name")), IdiomRoot::ThisRow);
assert_eq!(classify_idiom_root(&parse_idiom("user.name")), IdiomRoot::ThisRow);
}
#[test]
fn classify_idiom_root_opaque() {
assert_eq!(classify_idiom_root(&parse_idiom("$other.cat")), IdiomRoot::Opaque);
}
#[test]
fn references_any_for_this_only() {
let e = parse_expr("$this.x + 1");
assert!(!references_parent(&e));
assert_eq!(
first_row_scoped_reference(&e, AnalysisScope::CurrentSelect),
Some(RowScopeKind::This),
);
}
#[test]
fn recursive_scope_descends_into_subqueries() {
let e = parse_expr("(SELECT $parent FROM 1)");
assert!(!references_parent(&e), "current-scope walk skips subqueries");
assert_eq!(
first_row_scoped_reference(&e, AnalysisScope::Recursive),
Some(RowScopeKind::Parent),
"recursive walk must find $parent inside the subquery",
);
}
#[test]
fn references_parent_in_where_part() {
assert!(references_parent(&parse_expr("foo[WHERE bar = $parent.id]")));
assert!(references_parent(&parse_expr("array::find(foo, |$x| $x.bar = $parent.id)")));
}
}