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;
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}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub(super) enum PatternContext {
17    Read,
18    /// OPTIONAL MATCH — tolerate unknown labels/types (they just won't match).
19    OptionalRead,
20    Write,
21}
22
23impl<'a, S: GraphCatalog + ?Sized> Analyzer<'a, S> {
24    pub fn new(storage: &'a S) -> Self {
25        Self {
26            storage,
27            scopes: ScopeStack::new(),
28            symbols: SymbolTable::default(),
29        }
30    }
31
32    pub fn analyze(&mut self, doc: &Document) -> Result<ResolvedQuery, SemanticError> {
33        match &doc.statement {
34            Statement::Query(q) => self.analyze_query(q),
35        }
36    }
37
38    fn analyze_query(&mut self, query: &Query) -> Result<ResolvedQuery, SemanticError> {
39        let mut clauses = Vec::new();
40        let mut unions = Vec::new();
41
42        match query {
43            Query::Regular(r) => {
44                clauses.extend(self.analyze_single_query(&r.head)?);
45
46                for union_part in &r.unions {
47                    // Each UNION branch gets a fresh scope — variables from one
48                    // branch must not leak into another.
49                    self.scopes.clear();
50
51                    let branch_clauses = self.analyze_single_query(&union_part.query)?;
52                    unions.push(ResolvedUnionPart {
53                        all: union_part.all,
54                        clauses: branch_clauses,
55                    });
56                }
57
58                // Validate UNION column compatibility: all branches must
59                // have the same number of columns. Column names are taken
60                // from the first branch (standard Lora semantics).
61                if !unions.is_empty() {
62                    let head_cols = return_column_info(&clauses);
63                    for branch in &unions {
64                        let branch_cols = return_column_info(&branch.clauses);
65                        if let (Some(hc), Some(bc)) = (&head_cols, &branch_cols) {
66                            if hc.len() != bc.len() {
67                                return Err(SemanticError::UnionColumnCountMismatch(
68                                    hc.len(),
69                                    bc.len(),
70                                ));
71                            }
72                            // Validate column names when at least one side
73                            // uses an explicit AS alias.
74                            for ((h_name, h_explicit), (b_name, b_explicit)) in
75                                hc.iter().zip(bc.iter())
76                            {
77                                if (*h_explicit || *b_explicit) && h_name != b_name {
78                                    return Err(SemanticError::UnionColumnNameMismatch(
79                                        h_name.clone(),
80                                        b_name.clone(),
81                                    ));
82                                }
83                            }
84                        }
85                    }
86                }
87            }
88            Query::StandaloneCall(_) => {
89                return Err(SemanticError::UnsupportedFeature(
90                    "Standalone CALL is not yet supported by the analyzer".into(),
91                ));
92            }
93        }
94
95        Ok(ResolvedQuery { clauses, unions })
96    }
97
98    fn analyze_single_query(
99        &mut self,
100        q: &SingleQuery,
101    ) -> Result<Vec<ResolvedClause>, SemanticError> {
102        match q {
103            SingleQuery::SinglePart(sp) => self.analyze_single_part(sp),
104            SingleQuery::MultiPart(mp) => {
105                let mut clauses = Vec::new();
106
107                for part in &mp.parts {
108                    clauses.extend(self.analyze_query_part(part)?);
109                }
110
111                clauses.extend(self.analyze_single_part(&mp.tail)?);
112                Ok(clauses)
113            }
114        }
115    }
116
117    fn analyze_query_part(
118        &mut self,
119        part: &QueryPart,
120    ) -> Result<Vec<ResolvedClause>, SemanticError> {
121        let mut clauses = Vec::new();
122
123        for rc in &part.reading_clauses {
124            clauses.push(self.analyze_reading_clause(rc)?);
125        }
126
127        for uc in &part.updating_clauses {
128            clauses.push(self.analyze_updating_clause(uc)?);
129        }
130
131        clauses.push(ResolvedClause::With(self.analyze_with(&part.with_clause)?));
132        Ok(clauses)
133    }
134
135    fn analyze_single_part(
136        &mut self,
137        q: &SinglePartQuery,
138    ) -> Result<Vec<ResolvedClause>, SemanticError> {
139        let mut clauses = Vec::new();
140
141        for rc in &q.reading_clauses {
142            clauses.push(self.analyze_reading_clause(rc)?);
143        }
144
145        for uc in &q.updating_clauses {
146            clauses.push(self.analyze_updating_clause(uc)?);
147        }
148
149        if let Some(ret) = &q.return_clause {
150            clauses.push(ResolvedClause::Return(self.analyze_return(ret)?));
151        }
152
153        Ok(clauses)
154    }
155
156    fn analyze_reading_clause(
157        &mut self,
158        rc: &ReadingClause,
159    ) -> Result<ResolvedClause, SemanticError> {
160        match rc {
161            ReadingClause::Match(m) => Ok(ResolvedClause::Match(self.analyze_match(m)?)),
162            ReadingClause::Unwind(u) => Ok(ResolvedClause::Unwind(self.analyze_unwind(u)?)),
163            ReadingClause::InQueryCall(c) => self.analyze_in_query_call(c),
164        }
165    }
166
167    fn analyze_updating_clause(
168        &mut self,
169        uc: &UpdatingClause,
170    ) -> Result<ResolvedClause, SemanticError> {
171        match uc {
172            UpdatingClause::Create(c) => Ok(ResolvedClause::Create(self.analyze_create(c)?)),
173            UpdatingClause::Merge(m) => Ok(ResolvedClause::Merge(self.analyze_merge(m)?)),
174            UpdatingClause::Delete(d) => Ok(ResolvedClause::Delete(self.analyze_delete(d)?)),
175            UpdatingClause::Set(s) => Ok(ResolvedClause::Set(self.analyze_set(s)?)),
176            UpdatingClause::Remove(r) => Ok(ResolvedClause::Remove(self.analyze_remove(r)?)),
177        }
178    }
179
180    pub(super) fn analyze_property_map_expr(
181        &mut self,
182        expr: &Expr,
183    ) -> Result<ResolvedExpr, SemanticError> {
184        match expr {
185            Expr::Map(_, _) | Expr::Parameter(_, _) => self.analyze_expr(expr),
186            _ => Err(SemanticError::ExpectedPropertyMap(
187                expr.span().start,
188                expr.span().end,
189            )),
190        }
191    }
192
193    pub(super) fn resolve_required_variable(&self, name: &str) -> Result<VarId, SemanticError> {
194        self.scopes
195            .resolve(name)
196            .ok_or_else(|| SemanticError::UnknownVariable(name.to_string()))
197    }
198
199    pub(super) fn declare_fresh_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
200        if self.scopes.resolve(name).is_some() {
201            return Err(SemanticError::DuplicateVariable(name.to_string()));
202        }
203
204        let id = self.symbols.new_var();
205        self.scopes.declare(name.to_string(), id);
206        Ok(id)
207    }
208
209    pub(super) fn declare_or_reuse_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
210        if let Some(id) = self.scopes.resolve(name) {
211            Ok(id)
212        } else {
213            let id = self.symbols.new_var();
214            self.scopes.declare(name.to_string(), id);
215            Ok(id)
216        }
217    }
218
219    pub(super) fn validate_label_name(
220        &self,
221        label: &str,
222        context: PatternContext,
223    ) -> Result<(), SemanticError> {
224        if matches!(
225            context,
226            PatternContext::Write | PatternContext::OptionalRead
227        ) || self.storage.has_label_name(label)
228            || self.storage.node_count() == 0
229        {
230            Ok(())
231        } else {
232            Err(SemanticError::UnknownLabel(label.to_string()))
233        }
234    }
235
236    pub(super) fn validate_relationship_type_name(
237        &self,
238        rel_type: &str,
239        context: PatternContext,
240    ) -> Result<(), SemanticError> {
241        if matches!(
242            context,
243            PatternContext::Write | PatternContext::OptionalRead
244        ) || self.storage.has_relationship_type_name(rel_type)
245            || self.storage.relationship_count() == 0
246        {
247            Ok(())
248        } else {
249            Err(SemanticError::UnknownRelationshipType(rel_type.to_string()))
250        }
251    }
252
253    /// Analyze an expression that is the target of a SET operation.
254    /// Property names on the left side of SET are always allowed (new property creation).
255    pub(super) fn analyze_expr_write_property(
256        &mut self,
257        expr: &Expr,
258    ) -> Result<ResolvedExpr, SemanticError> {
259        match expr {
260            Expr::Property {
261                expr: inner, key, ..
262            } => {
263                let inner_resolved = self.analyze_expr(inner)?;
264                Ok(ResolvedExpr::Property {
265                    expr: Box::new(inner_resolved),
266                    property: key.clone(),
267                })
268            }
269            // Fallback to normal analysis for non-property expressions
270            other => self.analyze_expr(other),
271        }
272    }
273
274    pub(super) fn property_access_allowed(&self, base: &ResolvedExpr, key: &str) -> bool {
275        match base {
276            ResolvedExpr::Map(_) => true,
277            _ => {
278                self.storage.has_property_key(key)
279                    || (self.storage.node_count() == 0 && self.storage.relationship_count() == 0)
280            }
281        }
282    }
283
284    pub(super) fn visible_bindings(&self) -> BTreeMap<String, VarId> {
285        self.scopes.visible_bindings()
286    }
287
288    pub(super) fn replace_scope(&mut self, bindings: BTreeMap<String, VarId>) {
289        self.scopes.clear();
290        for (name, id) in bindings {
291            self.scopes.declare(name, id);
292        }
293    }
294}
295
296/// Extract column names and explicit-alias flags from the RETURN clause.
297fn return_column_info(clauses: &[ResolvedClause]) -> Option<Vec<(String, bool)>> {
298    for clause in clauses.iter().rev() {
299        if let ResolvedClause::Return(ret) = clause {
300            return Some(
301                ret.items
302                    .iter()
303                    .map(|p| (p.name.clone(), p.explicit_alias))
304                    .collect(),
305            );
306        }
307    }
308    None
309}