Skip to main content

lora_analyzer/
analyzer.rs

1use crate::{errors::*, resolved::*, scope::*, symbols::*};
2use lora_ast::{
3    Create, Delete, Document, Expr, InQueryCall, MapProjectionSelector, Match, Merge, NodePattern,
4    Pattern, PatternElement, PatternPart, ProjectionBody, ProjectionItem, Query, QueryPart,
5    ReadingClause, RelationshipPattern, Remove, RemoveItem, Return, Set, SetItem, SinglePartQuery,
6    SingleQuery, Statement, Unwind, UpdatingClause, With,
7};
8use lora_store::GraphStorage;
9use std::collections::{BTreeMap, BTreeSet};
10
11pub struct Analyzer<'a, S: GraphStorage + ?Sized> {
12    storage: &'a S,
13    scopes: ScopeStack,
14    symbols: SymbolTable,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18enum PatternContext {
19    Read,
20    /// OPTIONAL MATCH — tolerate unknown labels/types (they just won't match).
21    OptionalRead,
22    Write,
23}
24
25impl<'a, S: GraphStorage + ?Sized> Analyzer<'a, S> {
26    pub fn new(storage: &'a S) -> Self {
27        Self {
28            storage,
29            scopes: ScopeStack::new(),
30            symbols: SymbolTable::default(),
31        }
32    }
33
34    pub fn analyze(&mut self, doc: &Document) -> Result<ResolvedQuery, SemanticError> {
35        match &doc.statement {
36            Statement::Query(q) => self.analyze_query(q),
37        }
38    }
39
40    fn analyze_query(&mut self, query: &Query) -> Result<ResolvedQuery, SemanticError> {
41        let mut clauses = Vec::new();
42        let mut unions = Vec::new();
43
44        match query {
45            Query::Regular(r) => {
46                clauses.extend(self.analyze_single_query(&r.head)?);
47
48                for union_part in &r.unions {
49                    // Each UNION branch gets a fresh scope — variables from one
50                    // branch must not leak into another.
51                    self.scopes.clear();
52
53                    let branch_clauses = self.analyze_single_query(&union_part.query)?;
54                    unions.push(ResolvedUnionPart {
55                        all: union_part.all,
56                        clauses: branch_clauses,
57                    });
58                }
59
60                // Validate UNION column compatibility: all branches must
61                // have the same number of columns. Column names are taken
62                // from the first branch (standard Lora semantics).
63                if !unions.is_empty() {
64                    let head_cols = return_column_info(&clauses);
65                    for branch in &unions {
66                        let branch_cols = return_column_info(&branch.clauses);
67                        if let (Some(hc), Some(bc)) = (&head_cols, &branch_cols) {
68                            if hc.len() != bc.len() {
69                                return Err(SemanticError::UnionColumnCountMismatch(
70                                    hc.len(),
71                                    bc.len(),
72                                ));
73                            }
74                            // Validate column names when at least one side
75                            // uses an explicit AS alias.
76                            for ((h_name, h_explicit), (b_name, b_explicit)) in
77                                hc.iter().zip(bc.iter())
78                            {
79                                if (*h_explicit || *b_explicit) && h_name != b_name {
80                                    return Err(SemanticError::UnionColumnNameMismatch(
81                                        h_name.clone(),
82                                        b_name.clone(),
83                                    ));
84                                }
85                            }
86                        }
87                    }
88                }
89            }
90            Query::StandaloneCall(_) => {
91                return Err(SemanticError::UnsupportedFeature(
92                    "Standalone CALL is not yet supported by the analyzer".into(),
93                ));
94            }
95        }
96
97        Ok(ResolvedQuery { clauses, unions })
98    }
99
100    fn analyze_single_query(
101        &mut self,
102        q: &SingleQuery,
103    ) -> Result<Vec<ResolvedClause>, SemanticError> {
104        match q {
105            SingleQuery::SinglePart(sp) => self.analyze_single_part(sp),
106            SingleQuery::MultiPart(mp) => {
107                let mut clauses = Vec::new();
108
109                for part in &mp.parts {
110                    clauses.extend(self.analyze_query_part(part)?);
111                }
112
113                clauses.extend(self.analyze_single_part(&mp.tail)?);
114                Ok(clauses)
115            }
116        }
117    }
118
119    fn analyze_query_part(
120        &mut self,
121        part: &QueryPart,
122    ) -> Result<Vec<ResolvedClause>, SemanticError> {
123        let mut clauses = Vec::new();
124
125        for rc in &part.reading_clauses {
126            clauses.push(self.analyze_reading_clause(rc)?);
127        }
128
129        for uc in &part.updating_clauses {
130            clauses.push(self.analyze_updating_clause(uc)?);
131        }
132
133        clauses.push(ResolvedClause::With(self.analyze_with(&part.with_clause)?));
134        Ok(clauses)
135    }
136
137    fn analyze_single_part(
138        &mut self,
139        q: &SinglePartQuery,
140    ) -> Result<Vec<ResolvedClause>, SemanticError> {
141        let mut clauses = Vec::new();
142
143        for rc in &q.reading_clauses {
144            clauses.push(self.analyze_reading_clause(rc)?);
145        }
146
147        for uc in &q.updating_clauses {
148            clauses.push(self.analyze_updating_clause(uc)?);
149        }
150
151        if let Some(ret) = &q.return_clause {
152            clauses.push(ResolvedClause::Return(self.analyze_return(ret)?));
153        }
154
155        Ok(clauses)
156    }
157
158    fn analyze_reading_clause(
159        &mut self,
160        rc: &ReadingClause,
161    ) -> Result<ResolvedClause, SemanticError> {
162        match rc {
163            ReadingClause::Match(m) => Ok(ResolvedClause::Match(self.analyze_match(m)?)),
164            ReadingClause::Unwind(u) => Ok(ResolvedClause::Unwind(self.analyze_unwind(u)?)),
165            ReadingClause::InQueryCall(c) => self.analyze_in_query_call(c),
166        }
167    }
168
169    fn analyze_updating_clause(
170        &mut self,
171        uc: &UpdatingClause,
172    ) -> Result<ResolvedClause, SemanticError> {
173        match uc {
174            UpdatingClause::Create(c) => Ok(ResolvedClause::Create(self.analyze_create(c)?)),
175            UpdatingClause::Merge(m) => Ok(ResolvedClause::Merge(self.analyze_merge(m)?)),
176            UpdatingClause::Delete(d) => Ok(ResolvedClause::Delete(self.analyze_delete(d)?)),
177            UpdatingClause::Set(s) => Ok(ResolvedClause::Set(self.analyze_set(s)?)),
178            UpdatingClause::Remove(r) => Ok(ResolvedClause::Remove(self.analyze_remove(r)?)),
179        }
180    }
181
182    fn analyze_match(&mut self, m: &Match) -> Result<ResolvedMatch, SemanticError> {
183        let ctx = if m.optional {
184            PatternContext::OptionalRead
185        } else {
186            PatternContext::Read
187        };
188        let pattern = self.analyze_pattern(&m.pattern, ctx)?;
189        let where_ = m
190            .where_
191            .as_ref()
192            .map(|e| self.analyze_expr(e))
193            .transpose()?;
194
195        if let Some(ref w) = where_ {
196            if expr_contains_aggregate(w) {
197                return Err(SemanticError::AggregationInWhere);
198            }
199        }
200
201        Ok(ResolvedMatch {
202            optional: m.optional,
203            pattern,
204            where_,
205        })
206    }
207
208    fn analyze_unwind(&mut self, u: &Unwind) -> Result<ResolvedUnwind, SemanticError> {
209        let expr = self.analyze_expr(&u.expr)?;
210        let alias = self.declare_fresh_variable(&u.alias.name)?;
211
212        Ok(ResolvedUnwind { expr, alias })
213    }
214
215    fn analyze_in_query_call(
216        &mut self,
217        _call: &InQueryCall,
218    ) -> Result<ResolvedClause, SemanticError> {
219        Err(SemanticError::UnsupportedFeature(
220            "CALL ... YIELD is not yet supported by the analyzer".into(),
221        ))
222    }
223
224    fn analyze_create(&mut self, c: &Create) -> Result<ResolvedCreate, SemanticError> {
225        let pattern = self.analyze_pattern(&c.pattern, PatternContext::Write)?;
226        Ok(ResolvedCreate { pattern })
227    }
228
229    fn analyze_merge(&mut self, m: &Merge) -> Result<ResolvedMerge, SemanticError> {
230        let pattern_part = self.analyze_pattern_part(&m.pattern_part, PatternContext::Write)?;
231        let mut actions = Vec::with_capacity(m.actions.len());
232
233        for action in &m.actions {
234            actions.push(ResolvedMergeAction {
235                on_match: action.on_match,
236                set: self.analyze_set(&action.set)?,
237            });
238        }
239
240        Ok(ResolvedMerge {
241            pattern_part,
242            actions,
243        })
244    }
245
246    fn analyze_delete(&mut self, d: &Delete) -> Result<ResolvedDelete, SemanticError> {
247        let expressions = d
248            .expressions
249            .iter()
250            .map(|e| self.analyze_expr(e))
251            .collect::<Result<Vec<_>, _>>()?;
252
253        Ok(ResolvedDelete {
254            detach: d.detach,
255            expressions,
256        })
257    }
258
259    fn analyze_set(&mut self, s: &Set) -> Result<ResolvedSet, SemanticError> {
260        let mut items = Vec::with_capacity(s.items.len());
261
262        for item in &s.items {
263            match item {
264                SetItem::SetProperty { target, value, .. } => {
265                    // SET target (e.g. n.prop) allows new property names since
266                    // the SET is creating/updating properties.
267                    items.push(ResolvedSetItem::SetProperty {
268                        target: self.analyze_expr_write_property(target)?,
269                        value: self.analyze_expr(value)?,
270                    });
271                }
272                SetItem::SetVariable {
273                    variable, value, ..
274                } => {
275                    let var = self.resolve_required_variable(&variable.name)?;
276                    items.push(ResolvedSetItem::SetVariable {
277                        variable: var,
278                        value: self.analyze_expr(value)?,
279                    });
280                }
281                SetItem::MutateVariable {
282                    variable, value, ..
283                } => {
284                    let var = self.resolve_required_variable(&variable.name)?;
285                    items.push(ResolvedSetItem::MutateVariable {
286                        variable: var,
287                        value: self.analyze_expr(value)?,
288                    });
289                }
290                SetItem::SetLabels {
291                    variable, labels, ..
292                } => {
293                    let var = self.resolve_required_variable(&variable.name)?;
294                    for label in labels {
295                        self.validate_label_name(label, PatternContext::Write)?;
296                    }
297                    items.push(ResolvedSetItem::SetLabels {
298                        variable: var,
299                        labels: labels.clone(),
300                    });
301                }
302            }
303        }
304
305        Ok(ResolvedSet { items })
306    }
307
308    fn analyze_remove(&mut self, r: &Remove) -> Result<ResolvedRemove, SemanticError> {
309        let mut items = Vec::with_capacity(r.items.len());
310
311        for item in &r.items {
312            match item {
313                RemoveItem::Labels {
314                    variable, labels, ..
315                } => {
316                    let var = self.resolve_required_variable(&variable.name)?;
317                    items.push(ResolvedRemoveItem::Labels {
318                        variable: var,
319                        labels: labels.clone(),
320                    });
321                }
322                RemoveItem::Property { expr, .. } => {
323                    items.push(ResolvedRemoveItem::Property {
324                        expr: self.analyze_expr(expr)?,
325                    });
326                }
327            }
328        }
329
330        Ok(ResolvedRemove { items })
331    }
332
333    fn analyze_pattern(
334        &mut self,
335        p: &Pattern,
336        context: PatternContext,
337    ) -> Result<ResolvedPattern, SemanticError> {
338        let mut parts = Vec::with_capacity(p.parts.len());
339
340        // In read patterns, detect when the same node variable is used at
341        // multiple positions with conflicting labels (e.g. (n:X)-[r]->(n:Y)).
342        if matches!(context, PatternContext::Read | PatternContext::OptionalRead) {
343            let mut node_labels: BTreeMap<String, Vec<String>> = BTreeMap::new();
344            for part in &p.parts {
345                self.collect_node_var_labels(&part.element, &mut node_labels);
346            }
347            for (name, labels_list) in &node_labels {
348                // Only reject if the variable appears with distinct non-empty label sets
349                if labels_list.len() > 1 {
350                    let non_empty: Vec<&String> =
351                        labels_list.iter().filter(|l| !l.is_empty()).collect();
352                    let unique_labels: BTreeSet<&String> = non_empty.iter().copied().collect();
353                    if unique_labels.len() > 1 {
354                        return Err(SemanticError::DuplicateVariable(name.clone()));
355                    }
356                }
357            }
358        }
359
360        for part in &p.parts {
361            parts.push(self.analyze_pattern_part(part, context)?);
362        }
363
364        Ok(ResolvedPattern { parts })
365    }
366
367    /// Collect (variable_name, labels_string) for each node position in a pattern element.
368    fn collect_node_var_labels(
369        &self,
370        el: &PatternElement,
371        map: &mut BTreeMap<String, Vec<String>>,
372    ) {
373        match el {
374            PatternElement::NodeChain { head, chain, .. } => {
375                if let Some(ref v) = head.variable {
376                    let label_str = format_label_groups(&head.labels);
377                    map.entry(v.name.clone()).or_default().push(label_str);
378                }
379                for step in chain {
380                    if let Some(ref v) = step.node.variable {
381                        let label_str = format_label_groups(&step.node.labels);
382                        map.entry(v.name.clone()).or_default().push(label_str);
383                    }
384                }
385            }
386            PatternElement::Parenthesized(inner, _) => {
387                self.collect_node_var_labels(inner, map);
388            }
389            PatternElement::ShortestPath { element, .. } => {
390                self.collect_node_var_labels(element, map);
391            }
392        }
393    }
394
395    fn analyze_pattern_part(
396        &mut self,
397        part: &PatternPart,
398        context: PatternContext,
399    ) -> Result<ResolvedPatternPart, SemanticError> {
400        let binding = part
401            .binding
402            .as_ref()
403            .map(|v| self.declare_or_reuse_variable(&v.name))
404            .transpose()?;
405
406        let element = self.analyze_pattern_element(&part.element, context)?;
407
408        Ok(ResolvedPatternPart { binding, element })
409    }
410
411    fn analyze_pattern_element(
412        &mut self,
413        el: &PatternElement,
414        context: PatternContext,
415    ) -> Result<ResolvedPatternElement, SemanticError> {
416        match el {
417            PatternElement::NodeChain { head, chain, .. } => {
418                if chain.is_empty() {
419                    let node = self.analyze_node(head, context)?;
420                    return Ok(ResolvedPatternElement::Node {
421                        var: node.var,
422                        labels: node.labels,
423                        properties: node.properties,
424                    });
425                }
426
427                let head = self.analyze_node(head, context)?;
428                let mut resolved_chain = Vec::with_capacity(chain.len());
429
430                for step in chain {
431                    let rel = self.analyze_relationship(&step.relationship, context)?;
432                    let node = self.analyze_node(&step.node, context)?;
433                    resolved_chain.push(ResolvedChain { rel, node });
434                }
435
436                Ok(ResolvedPatternElement::NodeChain {
437                    head,
438                    chain: resolved_chain,
439                })
440            }
441
442            PatternElement::Parenthesized(inner, _) => self.analyze_pattern_element(inner, context),
443
444            PatternElement::ShortestPath { all, element, .. } => {
445                let resolved = self.analyze_pattern_element(element, context)?;
446                match resolved {
447                    ResolvedPatternElement::NodeChain { head, chain } => {
448                        Ok(ResolvedPatternElement::ShortestPath {
449                            all: *all,
450                            head,
451                            chain,
452                        })
453                    }
454                    other => Ok(other),
455                }
456            }
457        }
458    }
459
460    fn analyze_node(
461        &mut self,
462        node: &NodePattern,
463        context: PatternContext,
464    ) -> Result<ResolvedNode, SemanticError> {
465        let var = Some(match &node.variable {
466            // Named node — declare in scope so user code can reference it.
467            Some(v) => self.declare_or_reuse_variable(&v.name)?,
468            // Anonymous node (e.g. `(:Person)`) — allocate an internal VarId
469            // but do NOT declare it in the scope, so it cannot be referenced
470            // by user expressions and will not appear in projections.
471            None => self.symbols.new_var(),
472        });
473
474        let labels: Vec<Vec<String>> = node
475            .labels
476            .iter()
477            .map(|group| {
478                group
479                    .iter()
480                    .map(|l| {
481                        self.validate_label_name(l, context)?;
482                        Ok(l.clone())
483                    })
484                    .collect::<Result<Vec<_>, SemanticError>>()
485            })
486            .collect::<Result<Vec<_>, SemanticError>>()?;
487
488        let properties = node
489            .properties
490            .as_ref()
491            .map(|e| self.analyze_property_map_expr(e))
492            .transpose()?;
493
494        Ok(ResolvedNode {
495            var,
496            labels,
497            properties,
498        })
499    }
500
501    fn analyze_relationship(
502        &mut self,
503        rel: &RelationshipPattern,
504        context: PatternContext,
505    ) -> Result<ResolvedRel, SemanticError> {
506        if let Some(detail) = &rel.detail {
507            let var = Some(match &detail.variable {
508                Some(v) => self.declare_or_reuse_variable(&v.name)?,
509                // Anonymous relationship — allocate an internal VarId so the
510                // relationship value is stored in the row (needed for path
511                // materialization).
512                None => self.symbols.new_var(),
513            });
514
515            let types = detail
516                .types
517                .iter()
518                .map(|t| {
519                    self.validate_relationship_type_name(t, context)?;
520                    Ok(t.clone())
521                })
522                .collect::<Result<Vec<_>, SemanticError>>()?;
523
524            if let Some(range) = &detail.range {
525                if let (Some(start), Some(end)) = (range.start, range.end) {
526                    if start > end {
527                        return Err(SemanticError::InvalidRange(
528                            start,
529                            end,
530                            range.span.start,
531                            range.span.end,
532                        ));
533                    }
534                }
535            }
536
537            let properties = detail
538                .properties
539                .as_ref()
540                .map(|e| self.analyze_property_map_expr(e))
541                .transpose()?;
542
543            Ok(ResolvedRel {
544                var,
545                types,
546                direction: rel.direction,
547                range: detail.range.clone(),
548                properties,
549            })
550        } else {
551            Ok(ResolvedRel {
552                var: None,
553                types: Vec::new(),
554                direction: rel.direction,
555                range: None,
556                properties: None,
557            })
558        }
559    }
560
561    /// Analyze an expression, but allow resolution of projection aliases
562    /// (used for ORDER BY which can reference aliases from RETURN/WITH).
563    fn analyze_expr_with_aliases(
564        &mut self,
565        expr: &Expr,
566        aliases: &BTreeMap<String, VarId>,
567    ) -> Result<ResolvedExpr, SemanticError> {
568        match expr {
569            Expr::Variable(v) => {
570                // First try normal scope resolution; only fall back to
571                // projection aliases when the variable is not in scope.
572                if self.scopes.resolve(&v.name).is_some() {
573                    return self.analyze_expr(expr);
574                }
575                if let Some(&id) = aliases.get(&v.name) {
576                    return Ok(ResolvedExpr::Variable(id));
577                }
578                self.analyze_expr(expr)
579            }
580            // For property access like `alias.prop`, check if the base is an alias.
581            Expr::Property {
582                expr: inner,
583                key,
584                span,
585            } => {
586                let inner = self.analyze_expr_with_aliases(inner, aliases)?;
587                if self.property_access_allowed(&inner, key) {
588                    Ok(ResolvedExpr::Property {
589                        expr: Box::new(inner),
590                        property: key.clone(),
591                    })
592                } else {
593                    Err(SemanticError::UnknownPropertyAt(
594                        key.clone(),
595                        span.start,
596                        span.end,
597                    ))
598                }
599            }
600            // For function calls in ORDER BY (e.g. ORDER BY count(p))
601            Expr::FunctionCall {
602                name,
603                distinct,
604                args,
605                span,
606            } => {
607                let fn_name = name.join(".");
608                validate_function_name(&fn_name, span.start, span.end)?;
609                validate_function_arity(&fn_name, args.len())?;
610
611                let args = args
612                    .iter()
613                    .map(|a| self.analyze_expr_with_aliases(a, aliases))
614                    .collect::<Result<Vec<_>, _>>()?;
615
616                Ok(ResolvedExpr::Function {
617                    name: fn_name,
618                    distinct: *distinct,
619                    args,
620                })
621            }
622            _ => self.analyze_expr(expr),
623        }
624    }
625
626    fn analyze_expr(&mut self, expr: &Expr) -> Result<ResolvedExpr, SemanticError> {
627        match expr {
628            Expr::Variable(v) => {
629                let id = self.resolve_required_variable(&v.name)?;
630                Ok(ResolvedExpr::Variable(id))
631            }
632
633            Expr::Integer(v, _) => Ok(ResolvedExpr::Literal(LiteralValue::Integer(*v))),
634            Expr::Float(v, _) => Ok(ResolvedExpr::Literal(LiteralValue::Float(*v))),
635            Expr::String(v, _) => Ok(ResolvedExpr::Literal(LiteralValue::String(v.clone()))),
636            Expr::Bool(v, _) => Ok(ResolvedExpr::Literal(LiteralValue::Bool(*v))),
637            Expr::Null(_) => Ok(ResolvedExpr::Literal(LiteralValue::Null)),
638            Expr::Parameter(name, _) => Ok(ResolvedExpr::Parameter(name.clone())),
639
640            Expr::List(items, _) => {
641                let items = items
642                    .iter()
643                    .map(|e| self.analyze_expr(e))
644                    .collect::<Result<Vec<_>, _>>()?;
645                Ok(ResolvedExpr::List(items))
646            }
647
648            Expr::Map(items, _) => {
649                let mut seen = BTreeSet::new();
650                let mut out = Vec::with_capacity(items.len());
651
652                for (k, v) in items {
653                    if !seen.insert(k.clone()) {
654                        return Err(SemanticError::DuplicateMapKey(k.clone()));
655                    }
656                    out.push((k.clone(), self.analyze_expr(v)?));
657                }
658
659                Ok(ResolvedExpr::Map(out))
660            }
661
662            Expr::Property { expr, key, span } => {
663                let inner = self.analyze_expr(expr)?;
664
665                if self.property_access_allowed(&inner, key) {
666                    Ok(ResolvedExpr::Property {
667                        expr: Box::new(inner),
668                        property: key.clone(),
669                    })
670                } else {
671                    Err(SemanticError::UnknownPropertyAt(
672                        key.clone(),
673                        span.start,
674                        span.end,
675                    ))
676                }
677            }
678
679            Expr::Binary { lhs, op, rhs, .. } => {
680                let lhs = self.analyze_expr(lhs)?;
681                let rhs = self.analyze_expr(rhs)?;
682
683                Ok(ResolvedExpr::Binary {
684                    lhs: Box::new(lhs),
685                    op: *op,
686                    rhs: Box::new(rhs),
687                })
688            }
689
690            Expr::Unary { op, expr, .. } => {
691                let expr = self.analyze_expr(expr)?;
692                Ok(ResolvedExpr::Unary {
693                    op: *op,
694                    expr: Box::new(expr),
695                })
696            }
697
698            Expr::FunctionCall {
699                name,
700                distinct,
701                args,
702                span,
703                ..
704            } => {
705                let fn_name = name.join(".");
706                validate_function_name(&fn_name, span.start, span.end)?;
707                validate_function_arity(&fn_name, args.len())?;
708
709                let args = args
710                    .iter()
711                    .map(|a| self.analyze_expr(a))
712                    .collect::<Result<Vec<_>, _>>()?;
713
714                Ok(ResolvedExpr::Function {
715                    name: fn_name,
716                    distinct: *distinct,
717                    args,
718                })
719            }
720
721            Expr::ListPredicate {
722                kind,
723                variable,
724                list,
725                predicate,
726                ..
727            } => {
728                let list = self.analyze_expr(list)?;
729                let var_id = self.symbols.new_var();
730                self.scopes.push();
731                self.scopes.declare(variable.name.clone(), var_id);
732                let predicate = self.analyze_expr(predicate)?;
733                self.scopes.pop();
734
735                Ok(ResolvedExpr::ListPredicate {
736                    kind: *kind,
737                    variable: var_id,
738                    list: Box::new(list),
739                    predicate: Box::new(predicate),
740                })
741            }
742
743            Expr::ListComprehension {
744                variable,
745                list,
746                filter,
747                map_expr,
748                ..
749            } => {
750                let list = self.analyze_expr(list)?;
751                let var_id = self.symbols.new_var();
752                self.scopes.push();
753                self.scopes.declare(variable.name.clone(), var_id);
754                let filter = filter.as_ref().map(|e| self.analyze_expr(e)).transpose()?;
755                let map_expr = map_expr
756                    .as_ref()
757                    .map(|e| self.analyze_expr(e))
758                    .transpose()?;
759                self.scopes.pop();
760
761                Ok(ResolvedExpr::ListComprehension {
762                    variable: var_id,
763                    list: Box::new(list),
764                    filter: filter.map(Box::new),
765                    map_expr: map_expr.map(Box::new),
766                })
767            }
768
769            Expr::Reduce {
770                accumulator,
771                init,
772                variable,
773                list,
774                expr,
775                ..
776            } => {
777                let init = self.analyze_expr(init)?;
778                let list = self.analyze_expr(list)?;
779                let acc_id = self.symbols.new_var();
780                let var_id = self.symbols.new_var();
781                self.scopes.push();
782                self.scopes.declare(accumulator.name.clone(), acc_id);
783                self.scopes.declare(variable.name.clone(), var_id);
784                let expr = self.analyze_expr(expr)?;
785                self.scopes.pop();
786
787                Ok(ResolvedExpr::Reduce {
788                    accumulator: acc_id,
789                    init: Box::new(init),
790                    variable: var_id,
791                    list: Box::new(list),
792                    expr: Box::new(expr),
793                })
794            }
795
796            Expr::Index {
797                expr: inner, index, ..
798            } => {
799                let expr = self.analyze_expr(inner)?;
800                let index = self.analyze_expr(index)?;
801                Ok(ResolvedExpr::Index {
802                    expr: Box::new(expr),
803                    index: Box::new(index),
804                })
805            }
806
807            Expr::Slice {
808                expr: inner,
809                from,
810                to,
811                ..
812            } => {
813                let expr = self.analyze_expr(inner)?;
814                let from = from
815                    .as_ref()
816                    .map(|e| self.analyze_expr(e))
817                    .transpose()?
818                    .map(Box::new);
819                let to = to
820                    .as_ref()
821                    .map(|e| self.analyze_expr(e))
822                    .transpose()?
823                    .map(Box::new);
824                Ok(ResolvedExpr::Slice {
825                    expr: Box::new(expr),
826                    from,
827                    to,
828                })
829            }
830
831            Expr::MapProjection {
832                base, selectors, ..
833            } => {
834                let base = self.analyze_expr(base)?;
835                let mut resolved_selectors = Vec::new();
836                for sel in selectors {
837                    match sel {
838                        MapProjectionSelector::Property(name) => {
839                            resolved_selectors.push(ResolvedMapSelector::Property(name.clone()));
840                        }
841                        MapProjectionSelector::AllProperties => {
842                            resolved_selectors.push(ResolvedMapSelector::AllProperties);
843                        }
844                        MapProjectionSelector::Literal(key, expr) => {
845                            let resolved = self.analyze_expr(expr)?;
846                            resolved_selectors
847                                .push(ResolvedMapSelector::Literal(key.clone(), resolved));
848                        }
849                    }
850                }
851                Ok(ResolvedExpr::MapProjection {
852                    base: Box::new(base),
853                    selectors: resolved_selectors,
854                })
855            }
856
857            Expr::Case {
858                input,
859                alternatives,
860                else_expr,
861                ..
862            } => {
863                let input = input
864                    .as_ref()
865                    .map(|e| self.analyze_expr(e))
866                    .transpose()?
867                    .map(Box::new);
868
869                let alternatives = alternatives
870                    .iter()
871                    .map(|(when, then)| Ok((self.analyze_expr(when)?, self.analyze_expr(then)?)))
872                    .collect::<Result<Vec<_>, SemanticError>>()?;
873
874                let else_expr = else_expr
875                    .as_ref()
876                    .map(|e| self.analyze_expr(e))
877                    .transpose()?
878                    .map(Box::new);
879
880                Ok(ResolvedExpr::Case {
881                    input,
882                    alternatives,
883                    else_expr,
884                })
885            }
886
887            Expr::ExistsSubquery {
888                pattern, where_, ..
889            } => {
890                let resolved_pattern =
891                    self.analyze_pattern(pattern, PatternContext::OptionalRead)?;
892                let resolved_where = where_.as_ref().map(|e| self.analyze_expr(e)).transpose()?;
893                Ok(ResolvedExpr::ExistsSubquery {
894                    pattern: resolved_pattern,
895                    where_: resolved_where.map(Box::new),
896                })
897            }
898
899            Expr::PatternComprehension {
900                pattern: pat_element,
901                where_,
902                map_expr,
903                ..
904            } => {
905                // Wrap pattern_element in a PatternPart/Pattern for analysis
906                let pat = Pattern {
907                    parts: vec![PatternPart {
908                        binding: None,
909                        element: (**pat_element).clone(),
910                        span: map_expr.span(),
911                    }],
912                    span: map_expr.span(),
913                };
914                let resolved_pattern = self.analyze_pattern(&pat, PatternContext::OptionalRead)?;
915                let resolved_where = where_.as_ref().map(|e| self.analyze_expr(e)).transpose()?;
916                let resolved_map = self.analyze_expr(map_expr)?;
917                Ok(ResolvedExpr::PatternComprehension {
918                    pattern: resolved_pattern,
919                    where_: resolved_where.map(Box::new),
920                    map_expr: Box::new(resolved_map),
921                })
922            }
923        }
924    }
925
926    fn analyze_property_map_expr(&mut self, expr: &Expr) -> Result<ResolvedExpr, SemanticError> {
927        match expr {
928            Expr::Map(_, _) | Expr::Parameter(_, _) => self.analyze_expr(expr),
929            _ => Err(SemanticError::ExpectedPropertyMap(
930                expr.span().start,
931                expr.span().end,
932            )),
933        }
934    }
935
936    fn analyze_return(&mut self, r: &Return) -> Result<ResolvedReturn, SemanticError> {
937        let analyzed = self.analyze_projection_body(&r.body)?;
938
939        Ok(ResolvedReturn {
940            distinct: r.body.distinct,
941            items: analyzed.items,
942            include_existing: analyzed.include_existing,
943            order: analyzed.order,
944            skip: analyzed.skip,
945            limit: analyzed.limit,
946        })
947    }
948
949    fn analyze_with(&mut self, w: &With) -> Result<ResolvedWith, SemanticError> {
950        let old_scope = self.visible_bindings();
951        let analyzed = self.analyze_projection_body(&w.body)?;
952
953        let mut new_scope = BTreeMap::<String, VarId>::new();
954
955        if analyzed.include_existing {
956            for (name, id) in old_scope {
957                new_scope.insert(name, id);
958            }
959        }
960
961        for exported in &analyzed.exported_aliases {
962            new_scope.insert(exported.name.clone(), exported.id);
963        }
964
965        self.replace_scope(new_scope);
966
967        let where_ = w
968            .where_
969            .as_ref()
970            .map(|e| self.analyze_expr(e))
971            .transpose()?;
972
973        Ok(ResolvedWith {
974            distinct: w.body.distinct,
975            items: analyzed.items,
976            include_existing: analyzed.include_existing,
977            order: analyzed.order,
978            skip: analyzed.skip,
979            limit: analyzed.limit,
980            where_,
981        })
982    }
983
984    fn analyze_projection_body(
985        &mut self,
986        body: &ProjectionBody,
987    ) -> Result<AnalyzedProjectionBody, SemanticError> {
988        let mut items = Vec::new();
989        let mut include_existing = false;
990        let mut exported_aliases = Vec::new();
991        let mut seen_alias_names = BTreeSet::new();
992
993        for item in &body.items {
994            match item {
995                ProjectionItem::Expr { expr, alias, span } => {
996                    let resolved = self.analyze_expr(expr)?;
997
998                    let explicit = alias.is_some();
999                    let name = if let Some(var) = alias {
1000                        if !seen_alias_names.insert(var.name.clone()) {
1001                            return Err(SemanticError::DuplicateProjectionAlias(var.name.clone()));
1002                        }
1003                        var.name.clone()
1004                    } else {
1005                        projection_name(expr)
1006                    };
1007
1008                    let output = self.symbols.new_var();
1009
1010                    exported_aliases.push(ExportedAlias {
1011                        name: name.clone(),
1012                        id: output,
1013                    });
1014
1015                    items.push(ResolvedProjection {
1016                        expr: resolved,
1017                        output,
1018                        name,
1019                        explicit_alias: explicit,
1020                        span: *span,
1021                    });
1022                }
1023
1024                ProjectionItem::Star { .. } => {
1025                    include_existing = true;
1026                }
1027            }
1028        }
1029
1030        // Build a lookup from alias names to their output VarIds so ORDER BY
1031        // can reference projection aliases (e.g. ORDER BY name when RETURN p.name AS name).
1032        let alias_map: BTreeMap<String, VarId> = exported_aliases
1033            .iter()
1034            .map(|a| (a.name.clone(), a.id))
1035            .collect();
1036
1037        let order = body
1038            .order
1039            .iter()
1040            .map(|item| {
1041                let expr = self.analyze_expr_with_aliases(&item.expr, &alias_map)?;
1042                Ok(ResolvedSortItem {
1043                    expr,
1044                    direction: item.direction,
1045                })
1046            })
1047            .collect::<Result<Vec<_>, SemanticError>>()?;
1048
1049        let skip = body
1050            .skip
1051            .as_ref()
1052            .map(|e| self.analyze_expr(e))
1053            .transpose()?;
1054        let limit = body
1055            .limit
1056            .as_ref()
1057            .map(|e| self.analyze_expr(e))
1058            .transpose()?;
1059
1060        Ok(AnalyzedProjectionBody {
1061            items,
1062            include_existing,
1063            exported_aliases,
1064            order,
1065            skip,
1066            limit,
1067        })
1068    }
1069
1070    fn resolve_required_variable(&self, name: &str) -> Result<VarId, SemanticError> {
1071        self.scopes
1072            .resolve(name)
1073            .ok_or_else(|| SemanticError::UnknownVariable(name.to_string()))
1074    }
1075
1076    fn declare_fresh_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
1077        if self.scopes.resolve(name).is_some() {
1078            return Err(SemanticError::DuplicateVariable(name.to_string()));
1079        }
1080
1081        let id = self.symbols.new_var();
1082        self.scopes.declare(name.to_string(), id);
1083        Ok(id)
1084    }
1085
1086    fn declare_or_reuse_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
1087        if let Some(id) = self.scopes.resolve(name) {
1088            Ok(id)
1089        } else {
1090            let id = self.symbols.new_var();
1091            self.scopes.declare(name.to_string(), id);
1092            Ok(id)
1093        }
1094    }
1095
1096    fn validate_label_name(
1097        &self,
1098        label: &str,
1099        context: PatternContext,
1100    ) -> Result<(), SemanticError> {
1101        if matches!(
1102            context,
1103            PatternContext::Write | PatternContext::OptionalRead
1104        ) || self.storage.has_label_name(label)
1105            || self.storage.node_count() == 0
1106        {
1107            Ok(())
1108        } else {
1109            Err(SemanticError::UnknownLabel(label.to_string()))
1110        }
1111    }
1112
1113    fn validate_relationship_type_name(
1114        &self,
1115        rel_type: &str,
1116        context: PatternContext,
1117    ) -> Result<(), SemanticError> {
1118        if matches!(
1119            context,
1120            PatternContext::Write | PatternContext::OptionalRead
1121        ) || self.storage.has_relationship_type_name(rel_type)
1122            || self.storage.relationship_count() == 0
1123        {
1124            Ok(())
1125        } else {
1126            Err(SemanticError::UnknownRelationshipType(rel_type.to_string()))
1127        }
1128    }
1129
1130    /// Analyze an expression that is the target of a SET operation.
1131    /// Property names on the left side of SET are always allowed (new property creation).
1132    fn analyze_expr_write_property(&mut self, expr: &Expr) -> Result<ResolvedExpr, SemanticError> {
1133        match expr {
1134            Expr::Property {
1135                expr: inner, key, ..
1136            } => {
1137                let inner_resolved = self.analyze_expr(inner)?;
1138                Ok(ResolvedExpr::Property {
1139                    expr: Box::new(inner_resolved),
1140                    property: key.clone(),
1141                })
1142            }
1143            // Fallback to normal analysis for non-property expressions
1144            other => self.analyze_expr(other),
1145        }
1146    }
1147
1148    fn property_access_allowed(&self, base: &ResolvedExpr, key: &str) -> bool {
1149        match base {
1150            ResolvedExpr::Map(_) => true,
1151            _ => {
1152                self.storage.has_property_key(key)
1153                    || (self.storage.node_count() == 0 && self.storage.relationship_count() == 0)
1154            }
1155        }
1156    }
1157
1158    fn visible_bindings(&self) -> BTreeMap<String, VarId> {
1159        self.scopes.visible_bindings()
1160    }
1161
1162    fn replace_scope(&mut self, bindings: BTreeMap<String, VarId>) {
1163        self.scopes.clear();
1164        for (name, id) in bindings {
1165            self.scopes.declare(name, id);
1166        }
1167    }
1168}
1169
1170/// Known scalar and aggregate function names accepted by the engine.
1171const KNOWN_FUNCTIONS: &[&str] = &[
1172    // Aggregate
1173    "count",
1174    "sum",
1175    "avg",
1176    "min",
1177    "max",
1178    "collect",
1179    "stdev",
1180    "stdevp",
1181    "percentilecont",
1182    "percentiledisc",
1183    // Entity introspection
1184    "id",
1185    "type",
1186    "labels",
1187    "keys",
1188    "properties",
1189    // Path functions
1190    "nodes",
1191    "relationships",
1192    // String
1193    "tolower",
1194    "toupper",
1195    "trim",
1196    "ltrim",
1197    "rtrim",
1198    "replace",
1199    "split",
1200    "substring",
1201    "reverse",
1202    "left",
1203    "right",
1204    "lpad",
1205    "rpad",
1206    "char_length",
1207    "normalize",
1208    // Type conversion / introspection
1209    "tostring",
1210    "tointeger",
1211    "toint",
1212    "tofloat",
1213    "toboolean",
1214    "tobooleanornull",
1215    "valuetype",
1216    // Math — basic
1217    "abs",
1218    "ceil",
1219    "floor",
1220    "round",
1221    "sqrt",
1222    "sign",
1223    // Math — trigonometric / logarithmic
1224    "log",
1225    "ln",
1226    "log10",
1227    "exp",
1228    "sin",
1229    "cos",
1230    "tan",
1231    "asin",
1232    "acos",
1233    "atan",
1234    "atan2",
1235    "degrees",
1236    "radians",
1237    // Math — constants
1238    "pi",
1239    "e",
1240    "rand",
1241    // List / size
1242    "size",
1243    "length",
1244    "head",
1245    "tail",
1246    "last",
1247    "range",
1248    // Other
1249    "coalesce",
1250    "timestamp",
1251    // Temporal
1252    "date",
1253    "datetime",
1254    "time",
1255    "localtime",
1256    "localdatetime",
1257    "duration",
1258    "date.truncate",
1259    "datetime.truncate",
1260    "duration.between",
1261    "duration.indays",
1262    // Spatial
1263    "point",
1264    "distance",
1265];
1266
1267const AGGREGATE_FUNCTIONS: &[&str] = &[
1268    "count",
1269    "sum",
1270    "avg",
1271    "min",
1272    "max",
1273    "collect",
1274    "stdev",
1275    "stdevp",
1276    "percentilecont",
1277    "percentiledisc",
1278];
1279
1280/// Returns (min_args, max_args) for known functions. `None` means no upper bound (variadic).
1281fn function_arity(name: &str) -> Option<(usize, Option<usize>)> {
1282    match name {
1283        // Aggregate — all take exactly 1 argument (count can take 0 for count(*))
1284        "count" => Some((0, Some(1))),
1285        "sum" | "avg" | "min" | "max" | "collect" | "stdev" | "stdevp" => Some((1, Some(1))),
1286        "percentilecont" | "percentiledisc" => Some((2, Some(2))),
1287        // Entity introspection — exactly 1
1288        "id" | "type" | "labels" | "keys" | "properties" | "nodes" | "relationships" => {
1289            Some((1, Some(1)))
1290        }
1291        // String — 1 arg
1292        "tolower" | "toupper" | "trim" | "ltrim" | "rtrim" | "reverse" => Some((1, Some(1))),
1293        // String — 2 args
1294        "split" | "left" | "right" => Some((2, Some(2))),
1295        // String — 3 args
1296        "replace" => Some((3, Some(3))),
1297        // substring: 2 or 3 args
1298        "substring" => Some((2, Some(3))),
1299        // Type conversion — exactly 1
1300        "tostring" | "tointeger" | "toint" | "tofloat" | "toboolean" | "tobooleanornull"
1301        | "valuetype" => Some((1, Some(1))),
1302        // String — lpad/rpad take 3
1303        "lpad" | "rpad" => Some((3, Some(3))),
1304        // String — char_length/normalize take 1
1305        "char_length" | "normalize" => Some((1, Some(1))),
1306        // Math — exactly 1
1307        "abs" | "ceil" | "floor" | "round" | "sqrt" | "sign" => Some((1, Some(1))),
1308        // Math — trig / logarithmic (1 arg)
1309        "log" | "ln" | "log10" | "exp" | "sin" | "cos" | "tan" | "asin" | "acos" | "atan"
1310        | "degrees" | "radians" => Some((1, Some(1))),
1311        // Math — atan2 (2 args)
1312        "atan2" => Some((2, Some(2))),
1313        // Math — constants (0 args)
1314        "pi" | "e" | "rand" => Some((0, Some(0))),
1315        // List / size
1316        "size" | "length" | "head" | "tail" | "last" => Some((1, Some(1))),
1317        // range: 2 or 3
1318        "range" => Some((2, Some(3))),
1319        // coalesce: 1+
1320        "coalesce" => Some((1, None)),
1321        // timestamp: 0
1322        "timestamp" => Some((0, Some(0))),
1323        // Temporal constructors: 0 or 1
1324        "date" | "datetime" | "time" | "localtime" | "localdatetime" => Some((0, Some(1))),
1325        // duration: exactly 1
1326        "duration" => Some((1, Some(1))),
1327        // Temporal namespace functions: exactly 2
1328        "date.truncate" | "datetime.truncate" | "duration.between" | "duration.indays" => {
1329            Some((2, Some(2)))
1330        }
1331        // Spatial
1332        "point" => Some((1, Some(1))),
1333        "distance" => Some((2, Some(2))),
1334        _ => None,
1335    }
1336}
1337
1338fn is_aggregate_function(name: &str) -> bool {
1339    AGGREGATE_FUNCTIONS.contains(&name.to_ascii_lowercase().as_str())
1340}
1341
1342fn validate_function_name(name: &str, start: usize, end: usize) -> Result<(), SemanticError> {
1343    let lower = name.to_ascii_lowercase();
1344    if KNOWN_FUNCTIONS.contains(&lower.as_str()) {
1345        Ok(())
1346    } else {
1347        Err(SemanticError::UnknownFunction(name.to_string(), start, end))
1348    }
1349}
1350
1351fn validate_function_arity(name: &str, arg_count: usize) -> Result<(), SemanticError> {
1352    let lower = name.to_ascii_lowercase();
1353    if let Some((min, max)) = function_arity(&lower) {
1354        if arg_count < min {
1355            let expected = if max == Some(min) {
1356                format!("{min}")
1357            } else if let Some(mx) = max {
1358                format!("{min}..{mx}")
1359            } else {
1360                format!("at least {min}")
1361            };
1362            return Err(SemanticError::WrongArity(
1363                name.to_string(),
1364                expected,
1365                arg_count,
1366            ));
1367        }
1368        if let Some(mx) = max {
1369            if arg_count > mx {
1370                let expected = if mx == min {
1371                    format!("{min}")
1372                } else {
1373                    format!("{min}..{mx}")
1374                };
1375                return Err(SemanticError::WrongArity(
1376                    name.to_string(),
1377                    expected,
1378                    arg_count,
1379                ));
1380            }
1381        }
1382    }
1383    Ok(())
1384}
1385
1386/// Format label groups as a string for duplicate-variable detection.
1387fn format_label_groups(groups: &[impl AsRef<[String]>]) -> String {
1388    groups
1389        .iter()
1390        .map(|g| g.as_ref().join("|"))
1391        .collect::<Vec<_>>()
1392        .join(":")
1393}
1394
1395/// Extract column names and explicit-alias flags from the RETURN clause.
1396fn return_column_info(clauses: &[ResolvedClause]) -> Option<Vec<(String, bool)>> {
1397    for clause in clauses.iter().rev() {
1398        if let ResolvedClause::Return(ret) = clause {
1399            return Some(
1400                ret.items
1401                    .iter()
1402                    .map(|p| (p.name.clone(), p.explicit_alias))
1403                    .collect(),
1404            );
1405        }
1406    }
1407    None
1408}
1409
1410/// Returns true if the resolved expression contains any aggregate function call.
1411fn expr_contains_aggregate(expr: &ResolvedExpr) -> bool {
1412    match expr {
1413        ResolvedExpr::Function { name, args, .. } => {
1414            if is_aggregate_function(name) {
1415                return true;
1416            }
1417            args.iter().any(expr_contains_aggregate)
1418        }
1419        ResolvedExpr::Binary { lhs, rhs, .. } => {
1420            expr_contains_aggregate(lhs) || expr_contains_aggregate(rhs)
1421        }
1422        ResolvedExpr::Unary { expr, .. } => expr_contains_aggregate(expr),
1423        ResolvedExpr::Property { expr, .. } => expr_contains_aggregate(expr),
1424        ResolvedExpr::List(items) => items.iter().any(expr_contains_aggregate),
1425        ResolvedExpr::Map(items) => items.iter().any(|(_, v)| expr_contains_aggregate(v)),
1426        ResolvedExpr::Case {
1427            input,
1428            alternatives,
1429            else_expr,
1430        } => {
1431            input.as_ref().is_some_and(|e| expr_contains_aggregate(e))
1432                || alternatives
1433                    .iter()
1434                    .any(|(w, t)| expr_contains_aggregate(w) || expr_contains_aggregate(t))
1435                || else_expr
1436                    .as_ref()
1437                    .is_some_and(|e| expr_contains_aggregate(e))
1438        }
1439        ResolvedExpr::ListPredicate {
1440            list, predicate, ..
1441        } => expr_contains_aggregate(list) || expr_contains_aggregate(predicate),
1442        ResolvedExpr::ListComprehension {
1443            list,
1444            filter,
1445            map_expr,
1446            ..
1447        } => {
1448            expr_contains_aggregate(list)
1449                || filter.as_ref().is_some_and(|e| expr_contains_aggregate(e))
1450                || map_expr
1451                    .as_ref()
1452                    .is_some_and(|e| expr_contains_aggregate(e))
1453        }
1454        ResolvedExpr::Reduce {
1455            init, list, expr, ..
1456        } => {
1457            expr_contains_aggregate(init)
1458                || expr_contains_aggregate(list)
1459                || expr_contains_aggregate(expr)
1460        }
1461        ResolvedExpr::Index { expr, index } => {
1462            expr_contains_aggregate(expr) || expr_contains_aggregate(index)
1463        }
1464        ResolvedExpr::Slice { expr, from, to } => {
1465            expr_contains_aggregate(expr)
1466                || from.as_ref().is_some_and(|e| expr_contains_aggregate(e))
1467                || to.as_ref().is_some_and(|e| expr_contains_aggregate(e))
1468        }
1469        ResolvedExpr::MapProjection { base, selectors } => expr_contains_aggregate(base)
1470            || selectors.iter().any(
1471                |s| matches!(s, ResolvedMapSelector::Literal(_, e) if expr_contains_aggregate(e)),
1472            ),
1473        ResolvedExpr::ExistsSubquery { .. } | ResolvedExpr::PatternComprehension { .. } => false,
1474        ResolvedExpr::Variable(_) | ResolvedExpr::Literal(_) | ResolvedExpr::Parameter(_) => false,
1475    }
1476}
1477
1478fn projection_name(expr: &Expr) -> String {
1479    match expr {
1480        Expr::Variable(v) => v.name.clone(),
1481        Expr::Property { key, .. } => key.clone(),
1482        Expr::FunctionCall { name, .. } => {
1483            name.last().cloned().unwrap_or_else(|| "expr".to_string())
1484        }
1485        _ => "expr".to_string(),
1486    }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491    use super::*;
1492    use lora_parser::parse_query;
1493    use lora_store::{GraphStorageMut, InMemoryGraph, Properties};
1494
1495    #[test]
1496    fn create_allows_new_relationship_type_when_graph_is_not_empty() {
1497        let mut graph = InMemoryGraph::new();
1498        let alice = graph.create_node(vec!["User".into()], Properties::new());
1499        let bob = graph.create_node(vec!["User".into()], Properties::new());
1500        let _carol = graph.create_node(vec!["User".into()], Properties::new());
1501
1502        graph
1503            .create_relationship(alice.id, bob.id, "FOLLOWS", Properties::new())
1504            .unwrap();
1505
1506        let doc = parse_query(
1507            "MATCH (a:User {id: 2}), (b:User {id: 3}) CREATE (a)-[:KNOWS]->(b) RETURN a, b",
1508        )
1509        .unwrap();
1510
1511        let mut analyzer = Analyzer::new(&graph);
1512        assert!(analyzer.analyze(&doc).is_ok());
1513
1514        let match_doc = parse_query("MATCH (a)-[:KNOWS]->(b) RETURN a, b").unwrap();
1515        let mut analyzer = Analyzer::new(&graph);
1516        assert!(matches!(
1517            analyzer.analyze(&match_doc),
1518            Err(SemanticError::UnknownRelationshipType(rel_type)) if rel_type == "KNOWS"
1519        ));
1520    }
1521}
1522
1523#[derive(Debug, Clone)]
1524struct ExportedAlias {
1525    name: String,
1526    id: VarId,
1527}
1528
1529#[derive(Debug, Clone)]
1530struct AnalyzedProjectionBody {
1531    items: Vec<ResolvedProjection>,
1532    include_existing: bool,
1533    exported_aliases: Vec<ExportedAlias>,
1534    order: Vec<ResolvedSortItem>,
1535    skip: Option<ResolvedExpr>,
1536    limit: Option<ResolvedExpr>,
1537}