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        }
168    }
169
170    fn analyze_updating_clause(
171        &mut self,
172        uc: &UpdatingClause,
173    ) -> Result<ResolvedClause, SemanticError> {
174        match uc {
175            UpdatingClause::Create(c) => Ok(ResolvedClause::Create(self.analyze_create(c)?)),
176            UpdatingClause::Merge(m) => Ok(ResolvedClause::Merge(self.analyze_merge(m)?)),
177            UpdatingClause::Delete(d) => Ok(ResolvedClause::Delete(self.analyze_delete(d)?)),
178            UpdatingClause::Set(s) => Ok(ResolvedClause::Set(self.analyze_set(s)?)),
179            UpdatingClause::Remove(r) => Ok(ResolvedClause::Remove(self.analyze_remove(r)?)),
180        }
181    }
182
183    pub(super) fn analyze_property_map_expr(
184        &mut self,
185        expr: &Expr,
186    ) -> Result<ResolvedExpr, SemanticError> {
187        match expr {
188            Expr::Map(_, _) | Expr::Parameter(_, _) => self.analyze_expr(expr),
189            _ => Err(SemanticError::ExpectedPropertyMap(
190                expr.span().start,
191                expr.span().end,
192            )),
193        }
194    }
195
196    pub(super) fn resolve_required_variable(&self, name: &str) -> Result<VarId, SemanticError> {
197        self.scopes
198            .resolve(name)
199            .ok_or_else(|| SemanticError::UnknownVariable(name.to_string()))
200    }
201
202    pub(super) fn declare_fresh_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
203        if self.scopes.resolve(name).is_some() {
204            return Err(SemanticError::DuplicateVariable(name.to_string()));
205        }
206
207        let id = self.symbols.new_var();
208        self.scopes.declare(name.to_string(), id);
209        Ok(id)
210    }
211
212    pub(super) fn declare_or_reuse_variable(&mut self, name: &str) -> Result<VarId, SemanticError> {
213        if let Some(id) = self.scopes.resolve(name) {
214            Ok(id)
215        } else {
216            let id = self.symbols.new_var();
217            self.scopes.declare(name.to_string(), id);
218            Ok(id)
219        }
220    }
221
222    pub(super) fn validate_label_name(
223        &self,
224        label: &str,
225        context: PatternContext,
226    ) -> Result<(), SemanticError> {
227        if matches!(
228            context,
229            PatternContext::Write | PatternContext::OptionalRead
230        ) || self.storage.has_label_name(label)
231            || self.storage.node_count() == 0
232        {
233            Ok(())
234        } else {
235            Err(SemanticError::UnknownLabel(label.to_string()))
236        }
237    }
238
239    pub(super) fn validate_relationship_type_name(
240        &self,
241        rel_type: &str,
242        context: PatternContext,
243    ) -> Result<(), SemanticError> {
244        if matches!(
245            context,
246            PatternContext::Write | PatternContext::OptionalRead
247        ) || self.storage.has_relationship_type_name(rel_type)
248            || self.storage.relationship_count() == 0
249        {
250            Ok(())
251        } else {
252            Err(SemanticError::UnknownRelationshipType(rel_type.to_string()))
253        }
254    }
255
256    /// Analyze an expression that is the target of a SET operation.
257    /// Property names on the left side of SET are always allowed (new property creation).
258    pub(super) fn analyze_expr_write_property(
259        &mut self,
260        expr: &Expr,
261    ) -> Result<ResolvedExpr, SemanticError> {
262        match expr {
263            Expr::Property {
264                expr: inner, key, ..
265            } => {
266                let inner_resolved = self.analyze_expr(inner)?;
267                Ok(ResolvedExpr::Property {
268                    expr: Box::new(inner_resolved),
269                    property: key.clone(),
270                })
271            }
272            // Fallback to normal analysis for non-property expressions
273            other => self.analyze_expr(other),
274        }
275    }
276
277    pub(super) fn property_access_allowed(&self, base: &ResolvedExpr, key: &str) -> bool {
278        match base {
279            ResolvedExpr::Map(_) => true,
280            _ => {
281                self.storage.has_property_key(key)
282                    || (self.storage.node_count() == 0 && self.storage.relationship_count() == 0)
283            }
284        }
285    }
286
287    pub(super) fn visible_bindings(&self) -> BTreeMap<String, VarId> {
288        self.scopes.visible_bindings()
289    }
290
291    pub(super) fn replace_scope(&mut self, bindings: BTreeMap<String, VarId>) {
292        self.scopes.clear();
293        for (name, id) in bindings {
294            self.scopes.declare(name, id);
295        }
296    }
297}
298
299/// Extract column names and explicit-alias flags from the RETURN clause.
300fn return_column_info(clauses: &[ResolvedClause]) -> Option<Vec<(String, bool)>> {
301    for clause in clauses.iter().rev() {
302        if let ResolvedClause::Return(ret) = clause {
303            return Some(
304                ret.items
305                    .iter()
306                    .map(|p| (p.name.clone(), p.explicit_alias))
307                    .collect(),
308            );
309        }
310    }
311    None
312}