use std::collections::HashMap;
use crate::ast::clause::{
Create, Delete, Foreach, ForeachUpdate, LoadCsv, Match, Merge, Remove, RemoveItem, Return, Set,
SetItem, SetOperator, Unwind, With,
};
use crate::ast::expr::{
BinaryOperator, ComparisonOperator, ExistsInner, Expression, FunctionInvocation, Literal,
MapProjectionItem, NumberLiteral, UnaryOperator,
};
use crate::ast::names::Variable;
use crate::ast::pattern::{
LabelExpression, NodePattern, Pattern, PatternElement, Properties, RelationshipDirection,
RelationshipPattern,
};
use crate::ast::procedure::{InQueryCall, ProcedureInvocation, YieldSpec};
use crate::ast::query::{
CallSubquery, MultiPartQuery, OnErrorBehavior, Query, QueryBody, ReadingClause, RegularQuery,
SinglePartBody, SinglePartQuery, SingleQuery, SingleQueryKind, UpdatingClause,
};
use crate::error::{Diagnostics, Span};
use super::arena::{BindingId, ExprId, Id, LabelId, PropertyKeyId, RelTypeId, ScopeId};
use super::binding::{Binding, BindingKind, Scope};
use super::config::LowerConfig;
use super::expr::{
BinaryOp, CaseAlternative as HirCaseAlternative, CaseExpr as HirCaseExpr, CollectSubquery,
CollectionQuantifier, ComparisonOperator as HirComparisonOperator, CountSubquery,
ExistsSubquery, ExprKind, HirExpr, ListComprehension as HirListComprehension,
Literal as HirLiteral, MapProjectionItem as HirMapProjectionItem,
PatternComprehension as HirPatternComprehension, UnaryOp,
};
use super::ops::{
AggregateItem, AggregateOp, CallProcedureOp, CallSubqueryOp, CreateOp, DeleteOp, FilterOp,
ForeachOp, InTransactions as HirInTransactions, LimitOp, LoadCsvOp, MatchOp, MergeOp,
OnErrorBehavior as HirOnErrorBehavior, Operation, ProjectOp, ProjectionItem,
RemoveItem as HirRemoveItem, RemoveOp, ReturnOp, SetItem as HirSetItem, SetOp, SkipOp,
SortDirection, SortItem, SortOp, UnwindOp,
};
use super::pattern::{
GraphPattern, NodeIndex, NodePattern as HirNodePattern, PathBinding, RelIndex,
RelationshipDirection as HirRelationshipDirection, RelationshipLength,
RelationshipPattern as HirRelationshipPattern,
};
use super::{HirArenas, HirDiagnostic, HirQuery, QueryPart};
pub fn lower(query: &Query, config: &LowerConfig) -> Result<HirQuery, Diagnostics> {
let mut ctx = LoweringContext::new(config);
let mut parts = Vec::new();
for stmt in &query.statements {
let stmt_parts = ctx.lower_query_body(stmt);
parts.extend(stmt_parts);
}
let arenas = ctx.arenas;
let diagnostics = ctx.diagnostics;
if diagnostics.is_empty() {
Ok(HirQuery {
arenas,
parts,
diagnostics: Vec::new(),
})
} else {
Err(Diagnostics {
errors: diagnostics
.into_iter()
.map(|d| d.into_error(None))
.collect(),
})
}
}
struct LoweringContext<'cfg> {
arenas: HirArenas,
diagnostics: Vec<HirDiagnostic>,
scope_stack: LoweringScopeStack,
current_part_ops: Vec<Operation>,
config: &'cfg LowerConfig,
}
struct LoweringScopeStack {
scopes: Vec<ScopeId>,
bindings_by_name: Vec<HashMap<String, BindingId>>,
}
impl LoweringScopeStack {
fn new() -> Self {
Self {
scopes: Vec::new(),
bindings_by_name: Vec::new(),
}
}
fn push_scope(&mut self, arenas: &mut HirArenas) -> ScopeId {
let parent = self.scopes.last().copied();
let scope = Scope {
parent,
bindings: Vec::new(),
};
let scope_id = arenas.scopes.alloc(scope);
self.scopes.push(scope_id);
self.bindings_by_name.push(HashMap::new());
scope_id
}
fn pop_scope(&mut self) {
self.scopes.pop();
self.bindings_by_name.pop();
}
fn bind(
&mut self,
arenas: &mut HirArenas,
name: &str,
kind: BindingKind,
span: Span,
) -> BindingId {
let id = arenas.bindings.alloc(Binding {
id: Id(usize::MAX), name: name.to_string(),
kind,
introduced_at: span,
});
arenas.bindings.get_mut(id).id = id;
let scope_id = *self.scopes.last().unwrap();
arenas.scopes.get_mut(scope_id).bindings.push(id);
let map = self.bindings_by_name.last_mut().unwrap();
if let Some(&existing) = map.get(name) {
let _first_span = arenas.bindings.get(existing).introduced_at;
arenas.bindings.get_mut(id).id = id;
}
map.insert(name.to_string(), id);
id
}
fn resolve(&self, name: &str) -> Option<BindingId> {
for map in self.bindings_by_name.iter().rev() {
if let Some(&id) = map.get(name) {
return Some(id);
}
}
None
}
}
impl<'cfg> LoweringContext<'cfg> {
fn new(config: &'cfg LowerConfig) -> Self {
Self {
arenas: HirArenas::new(),
diagnostics: Vec::new(),
scope_stack: LoweringScopeStack::new(),
current_part_ops: Vec::new(),
config,
}
}
fn lower_query_body(&mut self, stmt: &QueryBody) -> Vec<QueryPart> {
let mut parts = Vec::new();
match stmt {
QueryBody::SingleQuery(sq) => {
parts.extend(self.lower_single_query(sq));
}
QueryBody::Regular(rq) => {
parts.extend(self.lower_regular_query(rq));
}
QueryBody::Standalone(call) => {
let scope = self.scope_stack.push_scope(&mut self.arenas);
let op = self.lower_standalone_call(call);
self.current_part_ops.push(op);
let out_scope = self.scope_stack.push_scope(&mut self.arenas);
parts.push(QueryPart {
input_scope: scope,
operations: std::mem::take(&mut self.current_part_ops),
output_scope: out_scope,
});
self.scope_stack.pop_scope();
self.scope_stack.pop_scope();
}
QueryBody::SchemaCommand(_) | QueryBody::Show(_) | QueryBody::Use(_) => {
}
}
parts
}
fn lower_regular_query(&mut self, rq: &RegularQuery) -> Vec<QueryPart> {
let mut parts = self.lower_single_query(&rq.single_query);
for union in &rq.unions {
let union_parts = self.lower_single_query(&union.single_query);
parts.extend(union_parts);
}
parts
}
fn lower_single_query(&mut self, sq: &SingleQuery) -> Vec<QueryPart> {
match &sq.kind {
SingleQueryKind::SinglePart(sp) => self.lower_single_part_query(sp),
SingleQueryKind::MultiPart(mp) => self.lower_multi_part_query(mp),
}
}
fn lower_single_part_query(&mut self, sp: &SinglePartQuery) -> Vec<QueryPart> {
let in_scope = self.scope_stack.push_scope(&mut self.arenas);
for rc in &sp.reading_clauses {
self.lower_reading_clause(rc);
}
match &sp.body {
SinglePartBody::Return(ret) => {
self.lower_return(ret);
}
SinglePartBody::Updating {
updating,
return_clause,
} => {
for uc in updating {
self.lower_updating_clause(uc);
}
if let Some(ret) = return_clause {
self.lower_return(ret);
} else {
self.current_part_ops.push(Operation::Finish);
}
}
SinglePartBody::Finish(_) => {
self.current_part_ops.push(Operation::Finish);
}
}
let out_scope = self.scope_stack.push_scope(&mut self.arenas);
let part = QueryPart {
input_scope: in_scope,
operations: std::mem::take(&mut self.current_part_ops),
output_scope: out_scope,
};
self.scope_stack.pop_scope();
self.scope_stack.pop_scope();
vec![part]
}
fn lower_multi_part_query(&mut self, mp: &MultiPartQuery) -> Vec<QueryPart> {
let mut parts = Vec::new();
let mut in_scope = self.scope_stack.push_scope(&mut self.arenas);
for part in &mp.parts {
for rc in &part.reading_clauses {
self.lower_reading_clause(rc);
}
for uc in &part.updating_clauses {
self.lower_updating_clause(uc);
}
self.lower_with(&part.with);
let out_scope = self.scope_stack.push_scope(&mut self.arenas);
parts.push(QueryPart {
input_scope: in_scope,
operations: std::mem::take(&mut self.current_part_ops),
output_scope: out_scope,
});
self.scope_stack.pop_scope();
in_scope = out_scope;
}
for rc in &mp.final_part.reading_clauses {
self.lower_reading_clause(rc);
}
match &mp.final_part.body {
SinglePartBody::Return(ret) => {
self.lower_return(ret);
}
SinglePartBody::Updating {
updating,
return_clause,
} => {
for uc in updating {
self.lower_updating_clause(uc);
}
if let Some(ret) = return_clause {
self.lower_return(ret);
} else {
self.current_part_ops.push(Operation::Finish);
}
}
SinglePartBody::Finish(_) => {
self.current_part_ops.push(Operation::Finish);
}
}
let out_scope = self.scope_stack.push_scope(&mut self.arenas);
parts.push(QueryPart {
input_scope: in_scope,
operations: std::mem::take(&mut self.current_part_ops),
output_scope: out_scope,
});
self.scope_stack.pop_scope();
self.scope_stack.pop_scope();
parts
}
fn lower_reading_clause(&mut self, rc: &ReadingClause) {
match rc {
ReadingClause::Match(m) => self.lower_match(m),
ReadingClause::Unwind(u) => self.lower_unwind(u),
ReadingClause::InQueryCall(i) => self.lower_in_query_call(i),
ReadingClause::CallSubquery(c) => self.lower_call_subquery(c),
ReadingClause::LoadCsv(l) => self.lower_load_csv(l),
}
}
fn lower_updating_clause(&mut self, uc: &UpdatingClause) {
match uc {
UpdatingClause::Create(c) => self.lower_create(c),
UpdatingClause::Merge(m) => self.lower_merge(m),
UpdatingClause::Delete(d) => self.lower_delete(d),
UpdatingClause::Set(s) => self.lower_set(s),
UpdatingClause::Remove(r) => self.lower_remove(r),
UpdatingClause::Foreach(f) => self.lower_foreach(f),
}
}
fn lower_match(&mut self, m: &Match) {
let pattern = self.lower_pattern(&m.pattern);
let mut predicates = Vec::new();
if let Some(where_expr) = &m.where_clause {
predicates.push(self.lower_expr(where_expr));
}
let op = if m.optional {
Operation::OptionalMatch(MatchOp {
pattern,
predicates,
})
} else {
Operation::Match(MatchOp {
pattern,
predicates,
})
};
self.current_part_ops.push(op);
}
fn lower_unwind(&mut self, u: &Unwind) {
let expr = self.lower_expr(&u.expression);
let var = self.scope_stack.bind(
&mut self.arenas,
&u.variable.name.name,
BindingKind::UnwindBound,
u.variable.name.span,
);
self.current_part_ops.push(Operation::Unwind(UnwindOp {
expression: expr,
variable: var,
}));
}
fn lower_in_query_call(&mut self, i: &InQueryCall) {
let proc_op = self.lower_procedure_invocation(&i.call);
self.current_part_ops
.push(Operation::CallProcedure(proc_op));
}
fn lower_call_subquery(&mut self, c: &CallSubquery) {
let imported = self
.scope_stack
.bindings_by_name
.iter()
.rev()
.flat_map(|m| m.values())
.copied()
.collect();
let query_ast = Query {
statements: vec![QueryBody::Regular(c.query.clone())],
span: Span::new(0, 0),
};
let query = lower(&query_ast, self.config).unwrap_or_else(|_| HirQuery {
arenas: HirArenas::new(),
parts: Vec::new(),
diagnostics: Vec::new(),
});
let in_transactions = c.in_transactions.as_ref().map(|it| HirInTransactions {
of_rows: it.of_rows.as_ref().map(|e| self.lower_expr(e)),
on_error: match it.on_error {
Some(OnErrorBehavior::Continue) => HirOnErrorBehavior::Continue,
Some(OnErrorBehavior::Break) => HirOnErrorBehavior::Break,
Some(OnErrorBehavior::Fail) => HirOnErrorBehavior::Fail,
None => HirOnErrorBehavior::Fail,
},
});
self.current_part_ops
.push(Operation::CallSubquery(CallSubqueryOp {
imported_bindings: imported,
query: Box::new(query),
in_transactions,
}));
}
fn lower_load_csv(&mut self, l: &LoadCsv) {
let source = self.lower_expr(&l.source);
let var = self.scope_stack.bind(
&mut self.arenas,
&l.variable.name.name,
BindingKind::PatternBound,
l.variable.name.span,
);
self.current_part_ops.push(Operation::LoadCsv(LoadCsvOp {
source,
variable: var,
with_headers: l.with_headers,
}));
}
fn lower_create(&mut self, c: &Create) {
let pattern = self.lower_pattern(&c.pattern);
self.current_part_ops
.push(Operation::Create(CreateOp { pattern }));
}
fn lower_merge(&mut self, m: &Merge) {
let pattern = self.lower_pattern_part_as_graph(&m.pattern);
let mut on_create = Vec::new();
let mut on_match = Vec::new();
for action in &m.actions {
let items: Vec<HirSetItem> = action
.set_items
.iter()
.map(|si| self.lower_set_item(si))
.collect();
if action.on_match {
on_match.extend(items);
} else {
on_create.extend(items);
}
}
self.current_part_ops.push(Operation::Merge(MergeOp {
pattern,
on_create,
on_match,
}));
}
fn lower_delete(&mut self, d: &Delete) {
let targets = d.targets.iter().map(|t| self.lower_expr(t)).collect();
self.current_part_ops.push(Operation::Delete(DeleteOp {
detach: d.detach,
targets,
}));
}
fn lower_set(&mut self, s: &Set) {
let items: Vec<HirSetItem> = s.items.iter().map(|si| self.lower_set_item(si)).collect();
self.current_part_ops.push(Operation::Set(SetOp { items }));
}
fn lower_set_item(&mut self, si: &SetItem) -> HirSetItem {
match si {
SetItem::DynamicProperty {
property,
key,
value,
..
} => {
let entity = self.lower_expr(property);
let key_id = self.lower_expr(key);
let val = self.lower_expr(value);
HirSetItem::SetDynamicProperty {
entity,
key: key_id,
value: val,
}
}
SetItem::Property {
property,
value,
operator,
} => {
let target = self.lower_expr(property);
let val = self.lower_expr(value);
match operator {
SetOperator::Assign => HirSetItem::SetProperty { target, value: val },
SetOperator::Add => HirSetItem::MergeProperties {
entity: Id(usize::MAX), value: val,
},
}
}
SetItem::Variable {
variable,
value,
operator,
} => {
let target = self.resolve_or_bind_variable(variable);
let val = self.lower_expr(value);
match operator {
SetOperator::Assign => HirSetItem::SetVariable { target, value: val },
SetOperator::Add => HirSetItem::MergeProperties {
entity: target,
value: val,
},
}
}
SetItem::Labels { variable, labels } => {
let node = self.resolve_or_bind_variable(variable);
let label_ids: Vec<LabelId> = labels
.iter()
.map(|l| self.arenas.labels.intern(&l.name, Id))
.collect();
HirSetItem::SetLabels {
node,
labels: label_ids,
}
}
}
}
fn lower_remove(&mut self, r: &Remove) {
let items: Vec<HirRemoveItem> = r
.items
.iter()
.map(|ri| match ri {
RemoveItem::Labels { variable, labels } => {
let node = self.resolve_or_bind_variable(variable);
let label_ids: Vec<LabelId> = labels
.iter()
.map(|l| self.arenas.labels.intern(&l.name, Id))
.collect();
HirRemoveItem::Labels {
node,
labels: label_ids,
}
}
RemoveItem::Property(expr) => HirRemoveItem::Property {
target: self.lower_expr(expr),
},
})
.collect();
self.current_part_ops
.push(Operation::Remove(RemoveOp { items }));
}
fn lower_foreach(&mut self, f: &Foreach) {
let list = self.lower_expr(&f.list);
let var = self.scope_stack.bind(
&mut self.arenas,
&f.variable.name.name,
BindingKind::ForeachVar,
f.variable.name.span,
);
let mut ops = Vec::new();
for update in &f.updates {
match update {
ForeachUpdate::Create(c) => {
let pattern = self.lower_pattern(&c.pattern);
ops.push(Operation::Create(CreateOp { pattern }));
}
ForeachUpdate::Merge(m) => {
let pattern = self.lower_pattern_part_as_graph(&m.pattern);
ops.push(Operation::Merge(MergeOp {
pattern,
on_create: Vec::new(),
on_match: Vec::new(),
}));
}
ForeachUpdate::Delete(d) => {
let targets = d.targets.iter().map(|t| self.lower_expr(t)).collect();
ops.push(Operation::Delete(DeleteOp {
detach: d.detach,
targets,
}));
}
ForeachUpdate::Set(s) => {
let items: Vec<HirSetItem> =
s.items.iter().map(|si| self.lower_set_item(si)).collect();
ops.push(Operation::Set(SetOp { items }));
}
ForeachUpdate::Remove(r) => {
let items: Vec<HirRemoveItem> = r
.items
.iter()
.map(|ri| match ri {
RemoveItem::Labels { variable, labels } => {
let node = self.resolve_or_bind_variable(variable);
let label_ids: Vec<LabelId> = labels
.iter()
.map(|l| self.arenas.labels.intern(&l.name, Id))
.collect();
HirRemoveItem::Labels {
node,
labels: label_ids,
}
}
RemoveItem::Property(expr) => HirRemoveItem::Property {
target: self.lower_expr(expr),
},
})
.collect();
ops.push(Operation::Remove(RemoveOp { items }));
}
ForeachUpdate::Foreach(inner) => {
self.lower_foreach(inner);
}
}
}
self.current_part_ops.push(Operation::Foreach(ForeachOp {
variable: var,
list,
operations: ops,
}));
}
fn lower_with(&mut self, w: &With) {
if w.star {
}
let mut items: Vec<ProjectionItem> = Vec::new();
for pi in &w.items {
let expr = self.lower_expr(&pi.expression);
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::WithAlias,
expr_span(&pi.expression),
);
items.push(ProjectionItem {
expression: expr,
alias,
});
}
let has_aggregate = w.items.iter().any(|pi| self.has_aggregate(&pi.expression));
if has_aggregate {
let mut grouping_keys = Vec::new();
let mut aggregates = Vec::new();
for pi in &w.items {
if self.has_aggregate(&pi.expression) {
let (func_id, args, distinct) = match &pi.expression {
Expression::FunctionCall(fc) => {
let key = Self::qualified_function_key(fc);
let display = Self::qualified_function_name(fc);
let fid = self
.arenas
.functions
.intern_with_display(&key, &display, Id);
let a = fc.arguments.iter().map(|a| self.lower_expr(a)).collect();
(fid, a, fc.distinct)
}
Expression::CountStar { .. } => {
let fid = self.arenas.functions.intern("COUNT", Id);
(fid, vec![], false)
}
_ => {
let fid = self.arenas.functions.intern("UNKNOWN", Id);
(fid, vec![], false)
}
};
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::WithAlias,
expr_span(&pi.expression),
);
aggregates.push(AggregateItem {
function: func_id,
args,
distinct,
alias,
});
} else {
let expr = self.lower_expr(&pi.expression);
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::WithAlias,
expr_span(&pi.expression),
);
grouping_keys.push(ProjectionItem {
expression: expr,
alias,
});
}
}
self.current_part_ops
.push(Operation::Aggregate(AggregateOp {
grouping_keys,
aggregates,
}));
} else {
self.current_part_ops.push(Operation::Project(ProjectOp {
items,
distinct: w.distinct,
}));
}
if let Some(where_expr) = &w.where_clause {
let pred = self.lower_expr(where_expr);
self.current_part_ops
.push(Operation::Filter(FilterOp { predicate: pred }));
}
if let Some(order) = &w.order {
let sort_items = order
.items
.iter()
.map(|si| SortItem {
expression: self.lower_expr(&si.expression),
direction: match si.direction {
Some(crate::ast::clause::SortDirection::Ascending) => {
SortDirection::Ascending
}
Some(crate::ast::clause::SortDirection::Descending) => {
SortDirection::Descending
}
None => SortDirection::Ascending,
},
})
.collect();
self.current_part_ops
.push(Operation::Sort(SortOp { items: sort_items }));
}
if let Some(skip_expr) = &w.skip {
let count = self.lower_expr(skip_expr);
self.current_part_ops
.push(Operation::Skip(SkipOp { count }));
}
if let Some(limit_expr) = &w.limit {
let count = self.lower_expr(limit_expr);
self.current_part_ops
.push(Operation::Limit(LimitOp { count }));
}
}
fn lower_return(&mut self, r: &Return) {
if r.star {
let visible: Vec<(String, BindingId)> = self
.scope_stack
.bindings_by_name
.iter()
.rev()
.flat_map(|m| m.iter().map(|(k, &v)| (k.clone(), v)))
.collect();
let mut seen = std::collections::HashSet::new();
let mut items = Vec::new();
for (name, binding_id) in visible {
if seen.insert(name.clone()) {
let expr_id = self.arenas.expressions.alloc(HirExpr {
kind: ExprKind::Binding(binding_id),
span: Span::new(0, 0),
});
items.push(ProjectionItem {
expression: expr_id,
alias: binding_id,
});
}
}
self.current_part_ops.push(Operation::Project(ProjectOp {
items,
distinct: r.distinct,
}));
} else {
let mut items: Vec<ProjectionItem> = Vec::new();
for pi in &r.items {
let expr = self.lower_expr(&pi.expression);
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::ReturnAlias,
expr_span(&pi.expression),
);
items.push(ProjectionItem {
expression: expr,
alias,
});
}
let has_aggregate = r.items.iter().any(|pi| self.has_aggregate(&pi.expression));
if has_aggregate {
let mut grouping_keys = Vec::new();
let mut aggregates = Vec::new();
for pi in &r.items {
if self.has_aggregate(&pi.expression) {
let (func_id, args, distinct) = match &pi.expression {
Expression::FunctionCall(fc) => {
let key = Self::qualified_function_key(fc);
let display = Self::qualified_function_name(fc);
let fid = self
.arenas
.functions
.intern_with_display(&key, &display, Id);
let a = fc.arguments.iter().map(|a| self.lower_expr(a)).collect();
(fid, a, fc.distinct)
}
Expression::CountStar { .. } => {
let fid = self.arenas.functions.intern("COUNT", Id);
(fid, vec![], false)
}
_ => {
let fid = self.arenas.functions.intern("UNKNOWN", Id);
(fid, vec![], false)
}
};
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::ReturnAlias,
expr_span(&pi.expression),
);
aggregates.push(AggregateItem {
function: func_id,
args,
distinct,
alias,
});
} else {
let expr = self.lower_expr(&pi.expression);
let alias_name = pi
.alias
.as_ref()
.map(|v| v.name.name.clone())
.unwrap_or_else(|| self.infer_alias_name(&pi.expression));
let alias = self.scope_stack.bind(
&mut self.arenas,
&alias_name,
BindingKind::ReturnAlias,
expr_span(&pi.expression),
);
grouping_keys.push(ProjectionItem {
expression: expr,
alias,
});
}
}
self.current_part_ops
.push(Operation::Aggregate(AggregateOp {
grouping_keys,
aggregates,
}));
} else {
self.current_part_ops.push(Operation::Project(ProjectOp {
items,
distinct: r.distinct,
}));
}
}
if let Some(order) = &r.order {
let sort_items = order
.items
.iter()
.map(|si| SortItem {
expression: self.lower_expr(&si.expression),
direction: match si.direction {
Some(crate::ast::clause::SortDirection::Ascending) => {
SortDirection::Ascending
}
Some(crate::ast::clause::SortDirection::Descending) => {
SortDirection::Descending
}
None => SortDirection::Ascending,
},
})
.collect();
self.current_part_ops
.push(Operation::Sort(SortOp { items: sort_items }));
}
if let Some(skip_expr) = &r.skip {
let count = self.lower_expr(skip_expr);
self.current_part_ops
.push(Operation::Skip(SkipOp { count }));
}
if let Some(limit_expr) = &r.limit {
let count = self.lower_expr(limit_expr);
self.current_part_ops
.push(Operation::Limit(LimitOp { count }));
}
self.current_part_ops.push(Operation::Return(ReturnOp {
items: Vec::new(), distinct: r.distinct,
}));
}
fn lower_standalone_call(&mut self, call: &crate::ast::procedure::StandaloneCall) -> Operation {
let proc_op = self.lower_procedure_invocation(&call.call);
if let Some(yield_spec) = &call.yield_items {
match yield_spec {
YieldSpec::Star { .. } => {}
YieldSpec::Items(_yi) => {
}
}
}
Operation::CallProcedure(proc_op)
}
fn lower_procedure_invocation(&mut self, proc: &ProcedureInvocation) -> CallProcedureOp {
let key = proc
.name
.name
.iter()
.map(|s| s.name.replace('\x00', "\x00\x00"))
.collect::<Vec<_>>()
.join("\x00");
let display = proc
.name
.name
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".");
let procedure = self
.arenas
.functions
.intern_with_display(&key, &display, Id);
let args = proc
.name
.arguments
.iter()
.map(|a| self.lower_expr(a))
.collect();
CallProcedureOp {
procedure,
args,
yields: Vec::new(),
}
}
fn lower_expr(&mut self, expr: &Expression) -> ExprId {
let kind = match expr {
Expression::Literal(lit) => match lit {
Literal::List(l) => {
let elements = l.elements.iter().map(|e| self.lower_expr(e)).collect();
ExprKind::List(elements)
}
Literal::Map(m) => {
let entries = m
.entries
.iter()
.map(|(key, val)| {
let property_key = self.arenas.property_keys.intern(&key.name.name, Id);
let expr_id = self.lower_expr(val);
(property_key, expr_id)
})
.collect();
ExprKind::Map(entries)
}
_ => ExprKind::Literal(self.lower_literal(lit)),
},
Expression::Variable(v) => match self.scope_stack.resolve(&v.name.name) {
Some(binding_id) => ExprKind::Binding(binding_id),
None => {
self.diagnostics.push(HirDiagnostic::UnknownVariable {
name: v.name.name.clone(),
span: v.name.span,
});
ExprKind::Binding(Id(usize::MAX))
}
},
Expression::Parameter(p) => {
let param_id = self.arenas.parameters.intern(&p.name.name, Id);
ExprKind::Parameter(param_id)
}
Expression::PropertyLookup { base, property, .. } => {
let base_id = self.lower_expr(base);
let key_id = self.arenas.property_keys.intern(&property.name.name, Id);
ExprKind::Property {
base: base_id,
key: key_id,
}
}
Expression::NodeLabels { base, labels, .. } => {
let base_id = self.lower_expr(base);
let label_ids: Vec<LabelId> = labels
.iter()
.map(|l| self.lower_label_expression_id(l))
.collect();
ExprKind::NodeLabels {
base: base_id,
labels: label_ids,
}
}
Expression::BinaryOp { op, lhs, rhs, .. } => {
let left = self.lower_expr(lhs);
let right = self.lower_expr(rhs);
let hir_op = self.lower_binary_op(op);
ExprKind::Binary {
op: hir_op,
left,
right,
}
}
Expression::UnaryOp { op, operand, .. } => {
let expr = self.lower_expr(operand);
let hir_op = match op {
UnaryOperator::Negate => UnaryOp::Negate,
UnaryOperator::Plus => UnaryOp::Plus,
UnaryOperator::Not => UnaryOp::Not,
};
ExprKind::Unary { op: hir_op, expr }
}
Expression::Comparison { lhs, operators, .. } => {
let left = self.lower_expr(lhs);
let ops: Vec<(HirComparisonOperator, ExprId)> = operators
.iter()
.map(|(op, rhs)| {
let r = self.lower_expr(rhs);
let hir_op = self.lower_comparison_op(op);
(hir_op, r)
})
.collect();
ExprKind::Comparison {
left,
operators: ops,
}
}
Expression::ListIndex { list, index, .. } => {
let list_id = self.lower_expr(list);
let index_id = self.lower_expr(index);
ExprKind::ListIndex {
list: list_id,
index: index_id,
}
}
Expression::ListSlice {
list, start, end, ..
} => {
let list_id = self.lower_expr(list);
let start_id = start.as_ref().map(|s| self.lower_expr(s));
let end_id = end.as_ref().map(|e| self.lower_expr(e));
ExprKind::ListSlice {
list: list_id,
start: start_id,
end: end_id,
}
}
Expression::In { lhs, rhs, .. } => {
let left = self.lower_expr(lhs);
let right = self.lower_expr(rhs);
ExprKind::In {
lhs: left,
rhs: right,
}
}
Expression::IsNull {
operand, negated, ..
} => {
let expr = self.lower_expr(operand);
ExprKind::IsNull {
operand: expr,
negated: *negated,
}
}
Expression::FunctionCall(fc) => {
let key = Self::qualified_function_key(fc);
let display = Self::qualified_function_name(fc);
let func_id = self
.arenas
.functions
.intern_with_display(&key, &display, Id);
let args = fc.arguments.iter().map(|a| self.lower_expr(a)).collect();
ExprKind::FunctionCall {
function: func_id,
args,
distinct: fc.distinct,
}
}
Expression::CountStar { .. } => ExprKind::CountStar,
Expression::Case(case) => {
let scrutinee = case.scrutinee.as_ref().map(|s| self.lower_expr(s));
let alternatives = case
.alternatives
.iter()
.map(|alt| HirCaseAlternative {
when: self.lower_expr(&alt.when),
then: self.lower_expr(&alt.then),
})
.collect();
let default = case.default.as_ref().map(|d| self.lower_expr(d));
ExprKind::Case(HirCaseExpr {
scrutinee,
alternatives,
default,
})
}
Expression::ListComprehension(lc) => {
let _scope = self.scope_stack.push_scope(&mut self.arenas);
let var = self.scope_stack.bind(
&mut self.arenas,
&lc.variable.name.name,
BindingKind::ComprehensionVar,
lc.variable.name.span,
);
let collection = self.lower_expr(&Expression::Variable(lc.variable.clone()));
let filter = lc.filter.as_ref().map(|f| self.lower_expr(f));
let map = lc.map.as_ref().map(|m| self.lower_expr(m));
self.scope_stack.pop_scope();
ExprKind::ListComprehension(HirListComprehension {
variable: var,
collection,
filter,
map,
})
}
Expression::PatternComprehension(pc) => {
let var = pc.variable.as_ref().map(|v| {
self.scope_stack.bind(
&mut self.arenas,
&v.name.name,
BindingKind::ComprehensionVar,
v.name.span,
)
});
let pattern = self.lower_relationships_pattern(&pc.pattern);
let filter = pc.where_clause.as_ref().map(|w| self.lower_expr(w));
let map = self.lower_expr(&pc.map);
ExprKind::PatternComprehension(HirPatternComprehension {
variable: var,
pattern,
filter,
map,
})
}
Expression::All(fe)
| Expression::Any(fe)
| Expression::None(fe)
| Expression::Single(fe) => {
let quantifier = match expr {
Expression::All(_) => CollectionQuantifier::All,
Expression::Any(_) => CollectionQuantifier::Any,
Expression::None(_) => CollectionQuantifier::None,
Expression::Single(_) => CollectionQuantifier::Single,
_ => unreachable!(),
};
let _scope = self.scope_stack.push_scope(&mut self.arenas);
let var = self.scope_stack.bind(
&mut self.arenas,
&fe.variable.name.name,
BindingKind::ComprehensionVar,
fe.variable.name.span,
);
let collection = self.lower_expr(&fe.collection);
let predicate = fe.predicate.as_ref().map(|p| self.lower_expr(p));
self.scope_stack.pop_scope();
ExprKind::CollectionFilter {
quantifier,
variable: var,
collection,
predicate,
}
}
Expression::Parenthesized(inner) => {
return self.lower_expr(inner);
}
Expression::Pattern(rp) => {
let pattern = self.lower_relationships_pattern(rp);
ExprKind::PatternExpr(pattern)
}
Expression::Exists(exists) => {
let inner = self.lower_exists_inner(&exists.inner);
ExprKind::ExistsSubquery(inner)
}
Expression::CountSubquery(count) => {
let inner = self.lower_count_subquery(&count.query);
ExprKind::CountSubquery(inner)
}
Expression::CollectSubquery(collect) => {
let inner = self.lower_collect_subquery(&collect.query);
ExprKind::CollectSubquery(inner)
}
Expression::MapProjection(mp) => {
let base = self.resolve_or_create_binding(&mp.base);
let items = mp
.items
.iter()
.map(|item| match item {
MapProjectionItem::AllProperties { .. } => {
HirMapProjectionItem::AllProperties
}
MapProjectionItem::PropertyLookup { property } => {
let key = self.arenas.property_keys.intern(&property.name.name, Id);
HirMapProjectionItem::PropertyLookup { key }
}
MapProjectionItem::Literal { key, value } => {
let k = self.arenas.property_keys.intern(&key.name.name, Id);
let v = self.lower_expr(value);
HirMapProjectionItem::Literal { key: k, value: v }
}
})
.collect();
ExprKind::MapProjection { base, items }
}
};
let span = expr_span(expr);
self.arenas.expressions.alloc(HirExpr { kind, span })
}
fn lower_literal(&mut self, lit: &Literal) -> HirLiteral {
match lit {
Literal::Number(n) => match n {
NumberLiteral::Integer(i) => HirLiteral::Integer(*i),
NumberLiteral::Float(f) => HirLiteral::Float(*f),
},
Literal::String(s) => HirLiteral::String(s.value.clone()),
Literal::Boolean(b) => HirLiteral::Boolean(*b),
Literal::Null => HirLiteral::Null,
Literal::List(_) | Literal::Map(_) => {
unreachable!(
"list/map literals are intercepted in lower_expr before lower_literal is called"
)
}
}
}
fn lower_binary_op(&mut self, op: &BinaryOperator) -> BinaryOp {
match op {
BinaryOperator::Add => BinaryOp::Add,
BinaryOperator::Subtract => BinaryOp::Subtract,
BinaryOperator::Multiply => BinaryOp::Multiply,
BinaryOperator::Divide => BinaryOp::Divide,
BinaryOperator::Modulo => BinaryOp::Modulo,
BinaryOperator::Power => BinaryOp::Power,
BinaryOperator::And => BinaryOp::And,
BinaryOperator::Or => BinaryOp::Or,
BinaryOperator::Xor => BinaryOp::Xor,
}
}
fn lower_comparison_op(&mut self, op: &ComparisonOperator) -> HirComparisonOperator {
match op {
ComparisonOperator::Eq => HirComparisonOperator::Eq,
ComparisonOperator::Ne => HirComparisonOperator::Ne,
ComparisonOperator::Lt => HirComparisonOperator::Lt,
ComparisonOperator::Gt => HirComparisonOperator::Gt,
ComparisonOperator::Le => HirComparisonOperator::Le,
ComparisonOperator::Ge => HirComparisonOperator::Ge,
ComparisonOperator::RegexMatch => HirComparisonOperator::RegexMatch,
ComparisonOperator::StartsWith => HirComparisonOperator::StartsWith,
ComparisonOperator::EndsWith => HirComparisonOperator::EndsWith,
ComparisonOperator::Contains => HirComparisonOperator::Contains,
}
}
fn lower_exists_inner(&mut self, inner: &ExistsInner) -> ExistsSubquery {
match inner {
ExistsInner::Pattern(pat, where_clause) => {
let pattern = self.lower_pattern(pat);
let mut ops = vec![Operation::Match(MatchOp {
pattern,
predicates: Vec::new(),
})];
if let Some(w) = where_clause {
let pred = self.lower_expr(w);
ops.push(Operation::Filter(FilterOp { predicate: pred }));
}
ops.push(Operation::Return(ReturnOp {
items: Vec::new(),
distinct: false,
}));
let scope = self.scope_stack.push_scope(&mut self.arenas);
let out_scope = self.scope_stack.push_scope(&mut self.arenas);
let query = HirQuery {
arenas: HirArenas::new(),
parts: vec![QueryPart {
input_scope: scope,
operations: ops,
output_scope: out_scope,
}],
diagnostics: Vec::new(),
};
self.scope_stack.pop_scope();
self.scope_stack.pop_scope();
ExistsSubquery {
query: Box::new(query),
imported_bindings: Vec::new(),
}
}
ExistsInner::RegularQuery(rq) => {
let imported: Vec<BindingId> = self
.scope_stack
.bindings_by_name
.iter()
.rev()
.flat_map(|m| m.values().copied())
.collect();
let query_ast = Query {
statements: vec![QueryBody::Regular((**rq).clone())],
span: Span::new(0, 0),
};
let query = lower(&query_ast, self.config).unwrap_or_else(|_| HirQuery {
arenas: HirArenas::new(),
parts: Vec::new(),
diagnostics: Vec::new(),
});
ExistsSubquery {
query: Box::new(query),
imported_bindings: imported,
}
}
}
}
fn lower_count_subquery(&mut self, query: &RegularQuery) -> CountSubquery {
let imported: Vec<BindingId> = self
.scope_stack
.bindings_by_name
.iter()
.rev()
.flat_map(|m| m.values().copied())
.collect();
let query_ast = Query {
statements: vec![QueryBody::Regular(query.clone())],
span: Span::new(0, 0),
};
let q = lower(&query_ast, self.config).unwrap_or_else(|_| HirQuery {
arenas: HirArenas::new(),
parts: Vec::new(),
diagnostics: Vec::new(),
});
CountSubquery {
query: Box::new(q),
imported_bindings: imported,
}
}
fn lower_collect_subquery(&mut self, query: &RegularQuery) -> CollectSubquery {
let imported: Vec<BindingId> = self
.scope_stack
.bindings_by_name
.iter()
.rev()
.flat_map(|m| m.values().copied())
.collect();
let query_ast = Query {
statements: vec![QueryBody::Regular(query.clone())],
span: Span::new(0, 0),
};
let q = lower(&query_ast, self.config).unwrap_or_else(|_| HirQuery {
arenas: HirArenas::new(),
parts: Vec::new(),
diagnostics: Vec::new(),
});
CollectSubquery {
query: Box::new(q),
imported_bindings: imported,
}
}
fn lower_pattern(&mut self, pattern: &Pattern) -> GraphPattern {
let mut nodes = Vec::new();
let mut relationships = Vec::new();
let mut path_bindings = Vec::new();
for part in &pattern.parts {
let path_binding = part.variable.as_ref().map(|v| {
self.scope_stack.bind(
&mut self.arenas,
&v.name.name,
BindingKind::PatternBound,
v.name.span,
)
});
let (part_nodes, part_rels) = self.lower_pattern_element(&part.anonymous.element);
let node_start = nodes.len();
nodes.extend(part_nodes);
let rel_start = relationships.len();
relationships.extend(part_rels);
if let Some(binding_id) = path_binding {
let node_indices: Vec<NodeIndex> = (node_start..nodes.len()).collect();
let rel_indices: Vec<RelIndex> = (rel_start..relationships.len()).collect();
path_bindings.push(PathBinding {
binding: binding_id,
nodes: node_indices,
relationships: rel_indices,
});
}
}
GraphPattern {
nodes,
relationships,
path_bindings,
}
}
fn lower_pattern_element(
&mut self,
element: &PatternElement,
) -> (Vec<HirNodePattern>, Vec<HirRelationshipPattern>) {
let mut nodes = Vec::new();
let mut relationships = Vec::new();
match element {
PatternElement::Path { start, chains } => {
let start_idx = nodes.len();
let start_node = self.lower_node_pattern(start);
nodes.push(start_node);
let mut current = start_idx;
for chain in chains {
let end_node = self.lower_node_pattern(&chain.node);
let end_idx = nodes.len();
nodes.push(end_node);
let rel =
self.lower_relationship_pattern(&chain.relationship, current, end_idx);
relationships.push(rel);
current = end_idx;
}
}
PatternElement::Parenthesized(inner) => {
return self.lower_pattern_element(inner);
}
PatternElement::Quantified { element, .. } => {
return self.lower_pattern_element(element);
}
}
(nodes, relationships)
}
fn lower_node_pattern(&mut self, node: &NodePattern) -> HirNodePattern {
let binding = node.variable.as_ref().map(|v| {
self.scope_stack.bind(
&mut self.arenas,
&v.name.name,
BindingKind::PatternBound,
v.name.span,
)
});
let labels: Vec<LabelId> = node
.labels
.iter()
.map(|l| self.lower_label_expression_id(l))
.collect();
let properties = node.properties.as_ref().map(|p| self.lower_properties(p));
HirNodePattern {
binding,
labels,
properties,
}
}
fn lower_relationship_pattern(
&mut self,
rel: &RelationshipPattern,
left: NodeIndex,
right: NodeIndex,
) -> HirRelationshipPattern {
let (binding, types, length, properties) = if let Some(detail) = &rel.detail {
let binding = detail.variable.as_ref().map(|v| {
self.scope_stack.bind(
&mut self.arenas,
&v.name.name,
BindingKind::PatternBound,
v.name.span,
)
});
let types: Vec<RelTypeId> = detail
.types
.as_ref()
.map(|t| self.lower_label_expression_to_rel_types(t))
.unwrap_or_default();
let length = detail
.range
.as_ref()
.map(|r| RelationshipLength::Variable {
min: r.start.map(|n| n as u32),
max: r.end.map(|n| n as u32),
})
.unwrap_or(RelationshipLength::Single);
let properties = detail.properties.as_ref().map(|p| self.lower_properties(p));
(binding, types, length, properties)
} else {
(None, Vec::new(), RelationshipLength::Single, None)
};
let direction = match rel.direction {
RelationshipDirection::Left => HirRelationshipDirection::RightToLeft,
RelationshipDirection::Right => HirRelationshipDirection::LeftToRight,
RelationshipDirection::Both => HirRelationshipDirection::Both,
RelationshipDirection::Undirected => HirRelationshipDirection::Undirected,
};
HirRelationshipPattern {
binding,
direction,
left,
right,
types,
length,
properties,
}
}
fn lower_relationships_pattern(
&mut self,
rp: &crate::ast::pattern::RelationshipsPattern,
) -> GraphPattern {
let mut nodes = Vec::new();
let mut relationships = Vec::new();
let start_idx = nodes.len();
nodes.push(self.lower_node_pattern(&rp.start));
let mut current = start_idx;
for chain in &rp.chains {
let end_idx = nodes.len();
nodes.push(self.lower_node_pattern(&chain.node));
let rel = self.lower_relationship_pattern(&chain.relationship, current, end_idx);
relationships.push(rel);
current = end_idx;
}
GraphPattern {
nodes,
relationships,
path_bindings: Vec::new(),
}
}
fn lower_pattern_part_as_graph(
&mut self,
part: &crate::ast::pattern::PatternPart,
) -> GraphPattern {
let mut nodes = Vec::new();
let mut relationships = Vec::new();
let (ns, rs) = self.lower_pattern_element(&part.anonymous.element);
nodes.extend(ns);
relationships.extend(rs);
GraphPattern {
nodes,
relationships,
path_bindings: Vec::new(),
}
}
fn lower_label_expression_id(&mut self, expr: &LabelExpression) -> LabelId {
match expr {
LabelExpression::Static(sym) => self.arenas.labels.intern(&sym.name, Id),
LabelExpression::Dynamic { span, .. }
| LabelExpression::Or { span, .. }
| LabelExpression::And { span, .. }
| LabelExpression::Not { span, .. }
| LabelExpression::Group { span, .. } => {
self.diagnostics.push(HirDiagnostic::UnsupportedFeature {
feature: "dynamic label expression".to_string(),
span: *span,
});
Id(usize::MAX)
}
}
}
fn lower_label_expression_to_rel_types(&mut self, expr: &LabelExpression) -> Vec<RelTypeId> {
match expr {
LabelExpression::Static(sym) => {
vec![self.arenas.relationship_types.intern(&sym.name, Id)]
}
LabelExpression::Or { lhs, rhs, .. } => {
let mut types = self.lower_label_expression_to_rel_types(lhs);
types.extend(self.lower_label_expression_to_rel_types(rhs));
types
}
LabelExpression::Dynamic { span, .. }
| LabelExpression::And { span, .. }
| LabelExpression::Not { span, .. }
| LabelExpression::Group { span, .. } => {
self.diagnostics.push(HirDiagnostic::UnsupportedFeature {
feature: "dynamic relationship type expression".to_string(),
span: *span,
});
Vec::new()
}
}
}
fn lower_properties(&mut self, props: &Properties) -> ExprId {
match props {
Properties::Map(m) => {
let entries: Vec<(PropertyKeyId, ExprId)> = m
.entries
.iter()
.map(|(key, val)| {
let k = self.arenas.property_keys.intern(&key.name.name, Id);
let v = self.lower_expr(val);
(k, v)
})
.collect();
let kind = ExprKind::Map(entries);
self.arenas
.expressions
.alloc(HirExpr { kind, span: m.span })
}
Properties::Parameter(p) => {
let param_id = self.arenas.parameters.intern(&p.name.name, Id);
let kind = ExprKind::Parameter(param_id);
self.arenas
.expressions
.alloc(HirExpr { kind, span: p.span })
}
}
}
fn resolve_or_bind_variable(&mut self, var: &Variable) -> BindingId {
match self.scope_stack.resolve(&var.name.name) {
Some(id) => id,
None => self.scope_stack.bind(
&mut self.arenas,
&var.name.name,
BindingKind::Value,
var.name.span,
),
}
}
fn resolve_or_create_binding(&mut self, var: &Variable) -> BindingId {
self.resolve_or_bind_variable(var)
}
fn qualified_function_name(fc: &FunctionInvocation) -> String {
let mut name = String::new();
for (idx, segment) in fc.name.iter().enumerate() {
if idx > 0 {
name.push('.');
}
name.push_str(&segment.name);
}
name
}
fn qualified_function_key(fc: &FunctionInvocation) -> String {
fc.name
.iter()
.map(|s| s.name.replace('\x00', "\x00\x00"))
.collect::<Vec<_>>()
.join("\x00")
}
fn infer_alias_name(&self, expr: &Expression) -> String {
match expr {
Expression::Variable(v) => v.name.name.clone(),
Expression::PropertyLookup { property, .. } => property.name.name.clone(),
Expression::FunctionCall(fc) => {
let name = Self::qualified_function_name(fc);
if name.is_empty() {
"expr".to_string()
} else {
name
}
}
_ => "expr".to_string(),
}
}
fn has_aggregate(&self, expr: &Expression) -> bool {
match expr {
Expression::FunctionCall(fc) => {
let name = Self::qualified_function_name(fc);
self.config.aggregates.contains(&name)
}
Expression::CountStar { .. } => true,
Expression::BinaryOp { lhs, rhs, .. } => {
self.has_aggregate(lhs) || self.has_aggregate(rhs)
}
Expression::UnaryOp { operand, .. } => self.has_aggregate(operand),
Expression::Comparison { lhs, operators, .. } => {
self.has_aggregate(lhs) || operators.iter().any(|(_, rhs)| self.has_aggregate(rhs))
}
Expression::Case(case) => {
case.alternatives
.iter()
.any(|alt| self.has_aggregate(&alt.when) || self.has_aggregate(&alt.then))
|| case
.scrutinee
.as_ref()
.is_some_and(|s| self.has_aggregate(s))
|| case.default.as_ref().is_some_and(|d| self.has_aggregate(d))
}
Expression::Parenthesized(inner) => self.has_aggregate(inner),
_ => false,
}
}
}
fn expr_span(expr: &Expression) -> Span {
match expr {
Expression::Literal(lit) => match lit {
Literal::Number(n) => match n {
NumberLiteral::Integer(_) => Span::new(0, 0),
NumberLiteral::Float(_) => Span::new(0, 0),
},
Literal::String(s) => s.span,
Literal::Boolean(_) => Span::new(0, 0),
Literal::Null => Span::new(0, 0),
Literal::List(l) => l.span,
Literal::Map(m) => m.span,
},
Expression::Variable(v) => v.name.span,
Expression::Parameter(p) => p.span,
Expression::PropertyLookup { span, .. } => *span,
Expression::NodeLabels { span, .. } => *span,
Expression::BinaryOp { span, .. } => *span,
Expression::UnaryOp { span, .. } => *span,
Expression::Comparison { span, .. } => *span,
Expression::ListIndex { span, .. } => *span,
Expression::ListSlice { span, .. } => *span,
Expression::In { span, .. } => *span,
Expression::IsNull { span, .. } => *span,
Expression::FunctionCall(fc) => fc.span,
Expression::CountStar { span } => *span,
Expression::Case(case) => case.span,
Expression::ListComprehension(lc) => lc.span,
Expression::PatternComprehension(pc) => pc.span,
Expression::All(fe)
| Expression::Any(fe)
| Expression::None(fe)
| Expression::Single(fe) => fe.span,
Expression::Parenthesized(_) => Span::new(0, 0),
Expression::Pattern(rp) => rp.span,
Expression::Exists(ex) => ex.span,
Expression::CountSubquery(c) => c.span,
Expression::CollectSubquery(c) => c.span,
Expression::MapProjection(mp) => mp.span,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::LowerConfig;
use crate::parse;
#[test]
fn test_simple_match_return() {
let query = parse("MATCH (p:Person) RETURN p.name").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
assert_eq!(hir.parts.len(), 1);
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Project(_)));
assert!(matches!(&ops[2], Operation::Return(_)));
}
#[test]
fn test_with_aggregation() {
let query = parse("MATCH (p:Person) WITH count(*) AS n RETURN n").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
assert_eq!(hir.parts.len(), 2);
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Aggregate(_)));
}
#[test]
fn test_multi_part_query() {
let query = parse("MATCH (p:Person) WITH p MATCH (p)-[:KNOWS]->(f) RETURN f.name").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
assert_eq!(hir.parts.len(), 2);
}
#[test]
fn test_unknown_variable_error() {
let query = parse("MATCH (p:Person) RETURN x.name").unwrap();
let result = lower(&query, &LowerConfig::default());
assert!(result.is_err());
}
#[test]
fn test_where_after_match() {
let query = parse("MATCH (p:Person) WHERE p.age > 18 RETURN p.name").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
match &ops[0] {
Operation::Match(m) => {
assert!(!m.predicates.is_empty());
}
_ => panic!("Expected MatchOp"),
}
}
#[test]
fn test_where_after_with() {
let query = parse("MATCH (p:Person) WITH p WHERE p.age > 18 RETURN p.name").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Project(_)));
assert!(matches!(&ops[2], Operation::Filter(_)));
}
#[test]
fn test_optional_match() {
let query = parse("OPTIONAL MATCH (p:Person) RETURN p.name").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::OptionalMatch(_)));
}
#[test]
fn test_create_clause() {
let query = parse("CREATE (p:Person {name: 'Alice'})").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Create(_)));
}
#[test]
fn test_delete_clause() {
let query = parse("MATCH (p:Person) DELETE p").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Delete(_)));
}
#[test]
fn test_unwind_clause() {
let query = parse("UNWIND [1, 2, 3] AS x RETURN x").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Unwind(_)));
}
#[test]
fn test_return_star() {
let query = parse("MATCH (p:Person) RETURN *").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Project(_)));
}
#[test]
fn test_order_by_and_limit() {
let query = parse("MATCH (p:Person) RETURN p.name ORDER BY p.name ASC LIMIT 10").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
assert!(matches!(&ops[0], Operation::Match(_)));
assert!(matches!(&ops[1], Operation::Project(_)));
assert!(matches!(&ops[2], Operation::Sort(_)));
assert!(matches!(&ops[3], Operation::Limit(_)));
assert!(matches!(&ops[4], Operation::Return(_)));
}
#[test]
fn test_function_call_preserves_qualified_name() {
let query = parse("RETURN apoc.text.distance('hello', 'world') AS d").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
let function = match &ops[0] {
Operation::Project(ProjectOp { items, .. }) => {
match &hir.arenas.expressions.get(items[0].expression).kind {
ExprKind::FunctionCall { function, .. } => *function,
_ => panic!("Expected function call expression"),
}
}
_ => panic!("Expected ProjectOp"),
};
assert_eq!(
hir.arenas.functions.name_of(function),
Some("apoc.text.distance")
);
}
#[test]
fn test_infer_alias_name_uses_full_qualified_name() {
let query = parse("RETURN apoc.text.distance('hello', 'world')").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
let alias_id = match &ops[0] {
Operation::Project(ProjectOp { items, .. }) => items[0].alias,
_ => panic!("Expected ProjectOp"),
};
assert_eq!(hir.arenas.bindings.get(alias_id).name, "apoc.text.distance");
}
#[test]
fn test_qualified_function_name_does_not_collide_with_builtin() {
let query = parse(
"RETURN replace('hello', 'l', 'x') AS a, apoc.text.replace('hello', 'l', 'x') AS b",
)
.unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
let (builtin, qualified) = match &ops[0] {
Operation::Project(ProjectOp { items, .. }) => {
let builtin = match &hir.arenas.expressions.get(items[0].expression).kind {
ExprKind::FunctionCall { function, .. } => *function,
_ => panic!("Expected first expression to be a function call"),
};
let qualified = match &hir.arenas.expressions.get(items[1].expression).kind {
ExprKind::FunctionCall { function, .. } => *function,
_ => panic!("Expected second expression to be a function call"),
};
(builtin, qualified)
}
_ => panic!("Expected ProjectOp"),
};
assert_ne!(builtin, qualified);
assert_eq!(hir.arenas.functions.name_of(builtin), Some("replace"));
assert_eq!(
hir.arenas.functions.name_of(qualified),
Some("apoc.text.replace")
);
}
#[test]
fn test_namespaced_count_is_not_aggregate() {
let query = parse("MATCH (n) WITH apoc.coll.count(n.name) AS c RETURN c").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
assert_eq!(hir.parts.len(), 2);
let ops = &hir.parts[0].operations;
assert!(
matches!(&ops[1], Operation::Project(_)),
"expected ProjectOp but got: {:?}",
&ops[1]
);
}
#[test]
fn test_custom_registered_aggregate() {
let mut config = LowerConfig::default();
config.aggregates.register("apoc.agg.percentiles");
let query = parse("MATCH (n) WITH apoc.agg.percentiles(n.value) AS p RETURN p").unwrap();
let hir = lower(&query, &config).unwrap();
assert_eq!(hir.parts.len(), 2);
let ops = &hir.parts[0].operations;
assert!(
matches!(&ops[1], Operation::Aggregate(_)),
"expected AggregateOp but got: {:?}",
&ops[1]
);
}
#[test]
fn test_escaped_segment_does_not_collide_with_dotted_namespace() {
let query =
parse("RETURN `apoc.text`.distance('a', 'b'), apoc.text.distance('a', 'b')").unwrap();
let hir = lower(&query, &LowerConfig::default()).unwrap();
let ops = &hir.parts[0].operations;
let (escaped_id, plain_id) = match &ops[0] {
Operation::Project(ProjectOp { items, .. }) => {
let escaped = match &hir.arenas.expressions.get(items[0].expression).kind {
ExprKind::FunctionCall { function, .. } => *function,
_ => panic!("Expected FunctionCall for item 0"),
};
let plain = match &hir.arenas.expressions.get(items[1].expression).kind {
ExprKind::FunctionCall { function, .. } => *function,
_ => panic!("Expected FunctionCall for item 1"),
};
(escaped, plain)
}
_ => panic!("Expected ProjectOp"),
};
assert_ne!(
escaped_id, plain_id,
"`apoc.text`.distance and apoc.text.distance must have distinct FunctionIds"
);
}
}