Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/elaborate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ pub fn elaborate_expr(e: Expr) -> Expr {
filter: filter.map(|f| Box::new(elaborate_expr(*f))),
body: Box::new(elaborate_expr(*body)),
},
Expr::AllReduce {
acc_name,
initial,
step_var,
list_expr,
reduction,
predicate,
} => Expr::AllReduce {
acc_name,
initial: Box::new(elaborate_expr(*initial)),
step_var,
list_expr: Box::new(elaborate_expr(*list_expr)),
reduction: Box::new(elaborate_expr(*reduction)),
predicate: Box::new(elaborate_expr(*predicate)),
},
other => other,
}
}
Expand Down
240 changes: 238 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,248 @@ pub mod syntax;
pub mod typing;

use syntax::path_pattern::PathPattern;
use syntax::query::{MatchStatement, Query};
use syntax::query::{MatchStatement, Query, ReturnItem, Aggregator, SortKey};
use syntax::expr::Expr;
use typing::checker::Typechecker;
use typing::variable_type::Schema;
use std::collections::{HashSet, HashMap};

fn collect_expr_vars(expr: &Expr, acc: &mut HashSet<String>) {
match expr {
Expr::Var(name) => {
acc.insert(name.clone());
}
Expr::AttrLookup { var, .. } => {
acc.insert(var.clone());
}
Expr::FieldAccess { base, .. } => {
collect_expr_vars(base, acc);
}
Expr::Binop { left, right, .. } => {
collect_expr_vars(left, acc);
collect_expr_vars(right, acc);
}
Expr::Unop { operand, .. } | Expr::IsNull { operand, .. } => {
collect_expr_vars(operand, acc);
}
Expr::Coalesce(args) | Expr::Call { args, .. } => {
for a in args {
collect_expr_vars(a, acc);
}
}
Expr::Record { fields } => {
for (_, e) in fields {
collect_expr_vars(e, acc);
}
}
Expr::ValueSubquery { body } | Expr::Exists { body } | Expr::NotExists { body } => {
collect_query_vars(body, acc);
}
Expr::Case { branches, else_expr } => {
for (cond, value) in branches {
collect_expr_vars(cond, acc);
collect_expr_vars(value, acc);
}
if let Some(value) = else_expr {
collect_expr_vars(value, acc);
}
}
Expr::ListComprehension { var, source, filter, body } => {
collect_expr_vars(source, acc);
let mut inner = HashSet::new();
if let Some(f) = filter {
collect_expr_vars(f, &mut inner);
}
collect_expr_vars(body, &mut inner);
inner.remove(var);
acc.extend(inner);
}
Expr::AllReduce {
acc_name,
initial,
step_var,
list_expr,
reduction,
predicate,
} => {
collect_expr_vars(initial, acc);
collect_expr_vars(list_expr, acc);
let mut inner = HashSet::new();
collect_expr_vars(reduction, &mut inner);
collect_expr_vars(predicate, &mut inner);
inner.remove(acc_name);
inner.remove(step_var);
acc.extend(inner);
}
Expr::Agg(agg) => match agg.as_ref() {
Aggregator::CountStar => {}
Aggregator::GeneralSet { expr, .. } => {
collect_expr_vars(expr, acc);
}
},
Expr::Const(_) | Expr::Type(_) => {}
}
}

fn collect_query_vars(q: &Query, acc: &mut HashSet<String>) {
if let Some(returns) = &q.returns {
for item in returns {
match item {
ReturnItem::Expr { expr, .. } => {
collect_expr_vars(expr, acc);
}
ReturnItem::Aggregate { agg, .. } => match agg {
Aggregator::CountStar => {}
Aggregator::GeneralSet { expr, .. } => {
collect_expr_vars(expr, acc);
}
},
}
}
} else {
for m in &q.matches {
acc.extend(m.pattern().freevars());
}
}
if let Some(gb) = &q.group_by {
for e in gb {
collect_expr_vars(e, acc);
}
}
if let Some(ob) = &q.order_by {
for spec in ob {
if let SortKey::Expr(expr) = &spec.key {
collect_expr_vars(expr, acc);
}
}
}
let mut var_counts = HashMap::new();
for m in &q.matches {
for v in m.pattern().freevars() {
*var_counts.entry(v).or_insert(0) += 1;
}
}
for (v, count) in var_counts {
if count > 1 {
acc.insert(v);
}
}
for m in &q.matches {
collect_pattern_filter_vars(m.pattern(), acc);
}
}

fn collect_pattern_filter_vars(p: &PathPattern, acc: &mut HashSet<String>) {
match p {
PathPattern::Filter(inner, expr) => {
collect_expr_vars(expr, acc);
collect_pattern_filter_vars(inner, acc);
}
PathPattern::Concat(a, b) | PathPattern::Union(a, b) | PathPattern::Join(a, b) => {
collect_pattern_filter_vars(a, acc);
collect_pattern_filter_vars(b, acc);
}
PathPattern::Repeat { pattern, .. } | PathPattern::Questioned(pattern)
| PathPattern::Selected { pattern, .. } | PathPattern::Named { pattern, .. } => {
collect_pattern_filter_vars(pattern, acc);
}
_ => {}
}
}

fn remove_unused_pattern_vars(p: PathPattern, used: &HashSet<String>) -> PathPattern {
match p {
PathPattern::Node(Some(mut d)) => {
if let Some(ref var) = d.var {
if !used.contains(var) {
d.var = None;
}
}
PathPattern::Node(Some(d))
}
PathPattern::EdgeRight(Some(mut d)) => {
if let Some(ref var) = d.var {
if !used.contains(var) {
d.var = None;
}
}
PathPattern::EdgeRight(Some(d))
}
PathPattern::EdgeLeft(Some(mut d)) => {
if let Some(ref var) = d.var {
if !used.contains(var) {
d.var = None;
}
}
PathPattern::EdgeLeft(Some(d))
}
PathPattern::EdgeUndirected(Some(mut d)) => {
if let Some(ref var) = d.var {
if !used.contains(var) {
d.var = None;
}
}
PathPattern::EdgeUndirected(Some(d))
}
PathPattern::EdgeAnyDirection(Some(mut d)) => {
if let Some(ref var) = d.var {
if !used.contains(var) {
d.var = None;
}
}
PathPattern::EdgeAnyDirection(Some(d))
}
PathPattern::Concat(a, b) => PathPattern::Concat(
Box::new(remove_unused_pattern_vars(*a, used)),
Box::new(remove_unused_pattern_vars(*b, used)),
),
PathPattern::Union(a, b) => PathPattern::Union(
Box::new(remove_unused_pattern_vars(*a, used)),
Box::new(remove_unused_pattern_vars(*b, used)),
),
PathPattern::Join(a, b) => PathPattern::Join(
Box::new(remove_unused_pattern_vars(*a, used)),
Box::new(remove_unused_pattern_vars(*b, used)),
),
PathPattern::Filter(inner, expr) => PathPattern::Filter(
Box::new(remove_unused_pattern_vars(*inner, used)),
expr,
),
PathPattern::Repeat { pattern, lb, ub } => PathPattern::Repeat {
pattern: Box::new(remove_unused_pattern_vars(*pattern, used)),
lb,
ub,
},
PathPattern::Questioned(inner) => {
PathPattern::Questioned(Box::new(remove_unused_pattern_vars(*inner, used)))
}
PathPattern::Selected { prefix, pattern } => PathPattern::Selected {
prefix,
pattern: Box::new(remove_unused_pattern_vars(*pattern, used)),
},
PathPattern::Named { var, pattern } => PathPattern::Named {
var,
pattern: Box::new(remove_unused_pattern_vars(*pattern, used)),
},
other => other,
}
}

/// Optimize the query while preserving OPTIONAL and §16.6 prefix boundaries.
fn optimize_query(q: Query, schema: &Schema) -> Query {
fn optimize_query(mut q: Query, schema: &Schema) -> Query {
let mut used_vars = HashSet::new();
collect_query_vars(&q, &mut used_vars);
for m in &mut q.matches {
match m {
MatchStatement::Simple { pattern } => {
*pattern = remove_unused_pattern_vars(std::mem::replace(pattern, PathPattern::Node(None)), &used_vars);
}
MatchStatement::Optional { pattern } => {
*pattern = remove_unused_pattern_vars(std::mem::replace(pattern, PathPattern::Node(None)), &used_vars);
}
}
}

// Selected patterns are evaluated in isolation; do not collapse across them.
let mut q = if !q.has_any_optional() && !q.has_any_selected() {
let pattern = optimizer::compile(q.collapsed_pattern());
Expand Down
12 changes: 12 additions & 0 deletions src/optimizer/existential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ fn fold_in_expr(e: &mut Expr, schema: &Schema) {
}
fold_in_expr(body, schema);
}
Expr::AllReduce {
initial,
list_expr,
reduction,
predicate,
..
} => {
fold_in_expr(initial, schema);
fold_in_expr(list_expr, schema);
fold_in_expr(reduction, schema);
fold_in_expr(predicate, schema);
}
Expr::Const(_) | Expr::Var(_) | Expr::AttrLookup { .. } | Expr::Type(_) => {}
}

Expand Down
11 changes: 6 additions & 5 deletions src/optimizer/pushdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,16 @@ fn merge_constraints(p: PathPattern, c: &Constraints) -> PathPattern {
fn boundary_node_vars(p: &PathPattern) -> [Option<String>; 2] {
let mut vars = Vec::new();
collect_top_node_vars(p, &mut vars);
[vars.first().cloned(), vars.last().cloned()]
[
vars.first().cloned().flatten(),
vars.last().cloned().flatten(),
]
}

fn collect_top_node_vars(p: &PathPattern, out: &mut Vec<String>) {
fn collect_top_node_vars(p: &PathPattern, out: &mut Vec<Option<String>>) {
match p {
PathPattern::Node(desc) => {
if let Some(var) = desc.as_ref().and_then(|d| d.var.clone()) {
out.push(var);
}
out.push(desc.as_ref().and_then(|d| d.var.clone()));
}
PathPattern::Concat(a, b) => {
collect_top_node_vars(a, out);
Expand Down
34 changes: 34 additions & 0 deletions src/parser/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,10 @@ impl Parser {
body: Box::new(body),
})
}
Token::AllReduce => {
self.advance();
self.parse_all_reduce()
}
_ => Err(format!("expected expression, got {:?}", self.peek())),
}
}
Expand Down Expand Up @@ -2039,6 +2043,36 @@ impl Parser {
})
}

fn parse_all_reduce(&mut self) -> Result<Expr, String> {
self.expect(&Token::LParen)?;
let acc_name = match self.advance() {
Token::Name(n) => n,
t => return Err(format!("allReduce: expected accumulator variable name, got {t:?}")),
};
self.expect(&Token::Eq)?;
let initial = self.expr()?;
self.expect(&Token::Comma)?;
let step_var = match self.advance() {
Token::Name(n) => n,
t => return Err(format!("allReduce: expected step variable name, got {t:?}")),
};
self.expect(&Token::In)?;
let list_expr = self.expr()?;
self.expect(&Token::Pipe)?;
let reduction = self.expr()?;
self.expect(&Token::Comma)?;
let predicate = self.expr()?;
self.expect(&Token::RParen)?;
Ok(Expr::AllReduce {
acc_name,
initial: Box::new(initial),
step_var,
list_expr: Box::new(list_expr),
reduction: Box::new(reduction),
predicate: Box::new(predicate),
})
}

// ===== Top-level statement: query or DDL =====

fn statement(&mut self) -> Result<Statement, String> {
Expand Down
3 changes: 3 additions & 0 deletions src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ pub enum Token {
/// Soft keyword (only before `{`) so `value` stays usable as an
/// identifier. Runs a correlated subquery and projects one value.
Value,
/// Cypher 25 `allReduce` function. Soft keyword (only before `(`).
AllReduce,

// Symbols
LParen, // (
Expand Down Expand Up @@ -616,6 +618,7 @@ impl Lexer {
}
"FLOOR" | "floor" if self.peek_non_space() == Some('(') => Token::Floor,
"CAST" | "cast" if self.peek_non_space() == Some('(') => Token::Cast,
"allReduce" | "ALLREDUCE" if self.peek_non_space() == Some('(') => Token::AllReduce,
// RECORD / VALUE are gated on a following `{` so the
// lowercase forms stay usable as identifiers.
"RECORD" | "record" if self.peek_non_space() == Some('{') => Token::Record,
Expand Down
Loading