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