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