Skip to main content

lora_analyzer/analyzer/
state.rs

1use crate::{errors::*, resolved::*, scope::*, symbols::*};
2use lora_ast::{
3    Document, Expr, Query, QueryPart, ReadingClause, SinglePartQuery, SingleQuery, Statement,
4    UpdatingClause,
5};
6use lora_store::GraphCatalog;
7use std::collections::{BTreeMap, BTreeSet};
8
9pub struct Analyzer<'a, S: GraphCatalog + ?Sized> {
10    /// Analysis deliberately does not read the stored data: whether a
11    /// query is valid must not depend on which labels, types or keys
12    /// happen to exist right now. The catalog type stays in the signature
13    /// so callers keep constructing the analyzer against their store.
14    pub(super) _catalog: std::marker::PhantomData<&'a S>,
15    pub(super) scopes: ScopeStack,
16    pub(super) symbols: SymbolTable,
17    /// Variables whose runtime value shape isn't tracked by the analyzer
18    /// (UNWIND-bound elements, anything that may legitimately hold a map
19    /// or list of maps). Property access on these vars must skip the
20    /// graph-catalog check — the caller can't know which keys the data
21    /// carries until rows are bound at execution time.
22    pub(super) dynamic_property_vars: BTreeSet<VarId>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub(super) enum PatternContext {
27    Read,
28    /// OPTIONAL MATCH — tolerate unknown labels/types (they just won't match).
29    OptionalRead,
30    Write,
31}
32
33impl<'a, S: GraphCatalog + ?Sized> Analyzer<'a, S> {
34    pub fn new(_storage: &'a S) -> Self {
35        Self {
36            _catalog: std::marker::PhantomData,
37            scopes: ScopeStack::new(),
38            symbols: SymbolTable::default(),
39            dynamic_property_vars: BTreeSet::new(),
40        }
41    }
42
43    pub fn analyze(&mut self, doc: &Document) -> Result<ResolvedQuery, SemanticError> {
44        match &doc.statement {
45            Statement::Query(q) => self.analyze_query(q),
46            Statement::Schema(_) => Err(SemanticError::UnsupportedFeature(
47                "schema commands are dispatched outside the analyzer".to_string(),
48            )),
49        }
50    }
51
52    fn analyze_query(&mut self, query: &Query) -> Result<ResolvedQuery, SemanticError> {
53        let mut clauses = Vec::new();
54        let mut unions = Vec::new();
55
56        match query {
57            Query::Regular(r) => {
58                clauses.extend(self.analyze_single_query(&r.head)?);
59
60                for union_part in &r.unions {
61                    // Each UNION branch gets a fresh scope — variables from one
62                    // branch must not leak into another.
63                    self.scopes.clear();
64
65                    let branch_clauses = self.analyze_single_query(&union_part.query)?;
66                    unions.push(ResolvedUnionPart {
67                        all: union_part.all,
68                        clauses: branch_clauses,
69                    });
70                }
71
72                // Validate UNION column compatibility: all branches must
73                // have the same number of columns. Column names are taken
74                // from the first branch (standard Lora semantics).
75                if !unions.is_empty() {
76                    let head_cols = return_column_info(&clauses);
77                    for branch in &unions {
78                        let branch_cols = return_column_info(&branch.clauses);
79                        if let (Some(hc), Some(bc)) = (&head_cols, &branch_cols) {
80                            if hc.len() != bc.len() {
81                                return Err(SemanticError::UnionColumnCountMismatch(
82                                    hc.len(),
83                                    bc.len(),
84                                ));
85                            }
86                            // Validate column names when at least one side
87                            // uses an explicit AS alias.
88                            for ((h_name, h_explicit), (b_name, b_explicit)) in
89                                hc.iter().zip(bc.iter())
90                            {
91                                if (*h_explicit || *b_explicit) && h_name != b_name {
92                                    return Err(SemanticError::UnionColumnNameMismatch(
93                                        h_name.clone(),
94                                        b_name.clone(),
95                                    ));
96                                }
97                            }
98                        }
99                    }
100                }
101            }
102            Query::StandaloneCall(_) => {
103                return Err(SemanticError::UnsupportedFeature(
104                    "Standalone CALL is not yet supported by the analyzer".into(),
105                ));
106            }
107        }
108
109        Ok(ResolvedQuery { clauses, unions })
110    }
111
112    fn analyze_single_query(
113        &mut self,
114        q: &SingleQuery,
115    ) -> Result<Vec<ResolvedClause>, SemanticError> {
116        match q {
117            SingleQuery::SinglePart(sp) => self.analyze_single_part(sp),
118            SingleQuery::MultiPart(mp) => {
119                let mut clauses = Vec::new();
120
121                for part in &mp.parts {
122                    clauses.extend(self.analyze_query_part(part)?);
123                }
124
125                clauses.extend(self.analyze_single_part(&mp.tail)?);
126                Ok(clauses)
127            }
128        }
129    }
130
131    fn analyze_query_part(
132        &mut self,
133        part: &QueryPart,
134    ) -> Result<Vec<ResolvedClause>, SemanticError> {
135        let mut clauses = Vec::new();
136
137        for rc in &part.reading_clauses {
138            clauses.extend(self.analyze_reading_clause(rc)?);
139        }
140
141        for uc in &part.updating_clauses {
142            clauses.push(self.analyze_updating_clause(uc)?);
143        }
144
145        clauses.push(ResolvedClause::With(self.analyze_with(&part.with_clause)?));
146        Ok(clauses)
147    }
148
149    fn analyze_single_part(
150        &mut self,
151        q: &SinglePartQuery,
152    ) -> Result<Vec<ResolvedClause>, SemanticError> {
153        let mut clauses = Vec::new();
154
155        for rc in &q.reading_clauses {
156            clauses.extend(self.analyze_reading_clause(rc)?);
157        }
158
159        for uc in &q.updating_clauses {
160            clauses.push(self.analyze_updating_clause(uc)?);
161        }
162
163        if let Some(ret) = &q.return_clause {
164            clauses.push(ResolvedClause::Return(self.analyze_return(ret)?));
165        } else if q.updating_clauses.is_empty() {
166            // The parser only lets a query end in a reading clause when it
167            // is a `CALL { ... }`; it must be a unit subquery.
168            match clauses.last() {
169                Some(ResolvedClause::CallSubquery(call)) if call.return_vars.is_empty() => {}
170                _ => {
171                    return Err(SemanticError::UnsupportedFeature(
172                        "a query can only end in CALL { ... } when the subquery ends in an \
173                         updating clause; add a RETURN"
174                            .into(),
175                    ))
176                }
177            }
178        }
179
180        Ok(clauses)
181    }
182
183    fn analyze_reading_clause(
184        &mut self,
185        rc: &ReadingClause,
186    ) -> Result<Vec<ResolvedClause>, SemanticError> {
187        Ok(match rc {
188            ReadingClause::Match(m) => vec![ResolvedClause::Match(self.analyze_match(m)?)],
189            ReadingClause::Unwind(u) => vec![ResolvedClause::Unwind(self.analyze_unwind(u)?)],
190            ReadingClause::InQueryCall(c) => self.analyze_in_query_call(c)?,
191            ReadingClause::CallSubquery(c) => {
192                vec![ResolvedClause::CallSubquery(self.analyze_call_subquery(c)?)]
193            }
194        })
195    }
196
197    /// Analyze a CALL { ... } subquery. The inner body is analyzed
198    /// with the outer scope visible (so MATCH inside the CALL can
199    /// reference outer-bound variables). After analysis the outer
200    /// scope is restored and the inner final RETURN's projection
201    /// aliases are injected as new bindings.
202    fn analyze_call_subquery(
203        &mut self,
204        call: &lora_ast::CallSubquery,
205    ) -> Result<ResolvedCallSubquery, SemanticError> {
206        let outer = self.visible_bindings();
207
208        if !call.body.unions.is_empty() {
209            return Err(SemanticError::UnsupportedFeature(
210                "UNION inside CALL { ... } is not yet supported".into(),
211            ));
212        }
213
214        let inner_clauses = self.analyze_single_query(&call.body.head)?;
215
216        let return_items: Vec<(String, VarId)> = match inner_clauses.last() {
217            Some(ResolvedClause::Return(ret)) => ret
218                .items
219                .iter()
220                .map(|p| (p.name.to_string(), p.output))
221                .collect(),
222            // A unit subquery: its body ends in an update and returns
223            // nothing. It runs once per outer row for its side effects and
224            // leaves the outer rows (and scope) unchanged.
225            Some(
226                ResolvedClause::Create(_)
227                | ResolvedClause::Merge(_)
228                | ResolvedClause::Delete(_)
229                | ResolvedClause::Set(_)
230                | ResolvedClause::Remove(_)
231                | ResolvedClause::Foreach(_),
232            ) => Vec::new(),
233            Some(ResolvedClause::CallSubquery(inner)) if inner.return_vars.is_empty() => Vec::new(),
234            _ => {
235                return Err(SemanticError::UnsupportedFeature(
236                    "CALL { ... } subquery must end with RETURN or an updating clause".into(),
237                ));
238            }
239        };
240
241        let mut new_scope = outer;
242        for (name, id) in &return_items {
243            new_scope.insert(name.clone(), *id);
244        }
245        self.replace_scope(new_scope);
246
247        Ok(ResolvedCallSubquery {
248            clauses: inner_clauses,
249            return_vars: return_items.into_iter().map(|(_, id)| id).collect(),
250        })
251    }
252
253    fn analyze_updating_clause(
254        &mut self,
255        uc: &UpdatingClause,
256    ) -> Result<ResolvedClause, SemanticError> {
257        match uc {
258            UpdatingClause::Create(c) => Ok(ResolvedClause::Create(self.analyze_create(c)?)),
259            UpdatingClause::Merge(m) => Ok(ResolvedClause::Merge(self.analyze_merge(m)?)),
260            UpdatingClause::Delete(d) => Ok(ResolvedClause::Delete(self.analyze_delete(d)?)),
261            UpdatingClause::Set(s) => Ok(ResolvedClause::Set(self.analyze_set(s)?)),
262            UpdatingClause::Remove(r) => Ok(ResolvedClause::Remove(self.analyze_remove(r)?)),
263            UpdatingClause::Foreach(f) => Ok(ResolvedClause::Foreach(self.analyze_foreach(f)?)),
264        }
265    }
266
267    pub(super) fn analyze_property_map_expr(
268        &mut self,
269        expr: &Expr,
270    ) -> Result<ResolvedExpr, SemanticError> {
271        match expr {
272            Expr::Map(_, _) | Expr::Parameter(_, _) => self.analyze_expr(expr),
273            _ => Err(SemanticError::ExpectedPropertyMap(
274                expr.span().start,
275                expr.span().end,
276            )),
277        }
278    }
279
280    pub(super) fn resolve_required_variable(&self, name: &str) -> Result<VarId, SemanticError> {
281        self.scopes
282            .resolve(name)
283            .ok_or_else(|| SemanticError::UnknownVariable(name.to_string()))
284    }
285
286    pub(super) fn declare_fresh_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
287        if self.scopes.resolve(name).is_some() {
288            return Err(SemanticError::DuplicateVariable(name.to_string()));
289        }
290
291        let id = self.symbols.new_var();
292        self.scopes.declare(name.to_string(), id);
293        Ok(id)
294    }
295
296    pub(super) fn declare_or_reuse_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
297        if let Some(id) = self.scopes.resolve(name) {
298            Ok(id)
299        } else {
300            let id = self.symbols.new_var();
301            self.scopes.declare(name.to_string(), id);
302            Ok(id)
303        }
304    }
305
306    /// Label names are never rejected. Standard Cypher treats a label no
307    /// node carries as an empty match, not an error, and checking against
308    /// the stored data made the *kind* of answer depend on what happens to
309    /// exist right now: the same query errored after the last `:Comment`
310    /// was deleted, but not on an empty database. A pattern with an unknown
311    /// label simply matches nothing.
312    pub(super) fn validate_label_name(
313        &self,
314        _label: &str,
315        _context: PatternContext,
316    ) -> Result<(), SemanticError> {
317        Ok(())
318    }
319
320    /// Relationship types follow the same rule as labels: an unknown type
321    /// matches nothing.
322    pub(super) fn validate_relationship_type_name(
323        &self,
324        _rel_type: &str,
325        _context: PatternContext,
326    ) -> Result<(), SemanticError> {
327        Ok(())
328    }
329
330    /// Analyze an expression that is the target of a SET operation.
331    /// Property names on the left side of SET are always allowed (new property creation).
332    pub(super) fn analyze_expr_write_property(
333        &mut self,
334        expr: &Expr,
335    ) -> Result<ResolvedExpr, SemanticError> {
336        match expr {
337            Expr::Property {
338                expr: inner, key, ..
339            } => {
340                let inner_resolved = self.analyze_expr(inner)?;
341                Ok(ResolvedExpr::Property {
342                    expr: Box::new(inner_resolved),
343                    property: key.clone(),
344                })
345            }
346            // Fallback to normal analysis for non-property expressions
347            other => self.analyze_expr(other),
348        }
349    }
350
351    /// Property access is always allowed: reading a key no entity carries
352    /// yields `null` (standard Cypher), including a key written earlier in
353    /// the same statement. Checking the stored catalog made the query's
354    /// validity depend on the data.
355    pub(super) fn property_access_allowed(&self, _base: &ResolvedExpr, _key: &str) -> bool {
356        true
357    }
358
359    pub(super) fn visible_bindings(&self) -> BTreeMap<String, VarId> {
360        self.scopes.visible_bindings()
361    }
362
363    pub(super) fn replace_scope(&mut self, bindings: BTreeMap<String, VarId>) {
364        self.scopes.clear();
365        for (name, id) in bindings {
366            self.scopes.declare(name, id);
367        }
368    }
369}
370
371/// Extract column names and explicit-alias flags from the RETURN clause.
372fn return_column_info(clauses: &[ResolvedClause]) -> Option<Vec<(String, bool)>> {
373    for clause in clauses.iter().rev() {
374        if let ResolvedClause::Return(ret) = clause {
375            return Some(
376                ret.items
377                    .iter()
378                    .map(|p| (p.name.to_string(), p.explicit_alias))
379                    .collect(),
380            );
381        }
382    }
383    None
384}