Skip to main content

linkrs_query/binder/
bind.rs

1mod bind_ddl;
2mod bind_dml;
3mod bind_expressions;
4mod bind_fetch;
5mod bind_go;
6mod bind_lookup;
7mod bind_match;
8mod bind_path;
9mod bind_return;
10mod bind_subgraph;
11
12use std::sync::Arc;
13
14use crate::parser::ast::stmt::Ast;
15use crate::parser::ast::Stmt;
16use linkrs_core::metadata::{MacroManager, SchemaManager};
17use linkrs_core::types::expr::Expression;
18use linkrs_core::DBResult;
19
20use super::bound::BoundStatement;
21use super::scope::BinderScope;
22
23use crate::executor::streaming::interner::StrInterner;
24
25/// The Binder transforms a parsed AST into a fully resolved BoundStatement.
26pub struct Binder {
27    scope: BinderScope,
28    schema_manager: Option<Arc<SchemaManager>>,
29    pub(crate) macro_manager: Option<Arc<MacroManager>>,
30    space_name: Option<String>,
31    space_id: u64,
32    interner: StrInterner,
33    /// Stack of macro names currently being expanded (recursion guard).
34    pub(crate) expanding: Vec<String>,
35    /// Stack of enclosing CTE names whose working tables are visible to
36    /// patterns (recursive-step binding). Innermost last.
37    pub(crate) cte_stack: Vec<String>,
38}
39
40impl Binder {
41    pub fn new() -> Self {
42        Self {
43            scope: BinderScope::new(),
44            schema_manager: None,
45            macro_manager: None,
46            space_name: None,
47            space_id: 0,
48            interner: StrInterner::new(),
49            expanding: Vec::new(),
50            cte_stack: Vec::new(),
51        }
52    }
53
54    pub fn with_schema_manager(mut self, sm: Arc<SchemaManager>) -> Self {
55        self.schema_manager = Some(sm);
56        self
57    }
58
59    pub fn with_space(mut self, space_name: Option<String>, space_id: u64) -> Self {
60        self.space_name = space_name;
61        self.space_id = space_id;
62        self
63    }
64
65    /// Bind an AST into a fully resolved BoundStatement.
66    pub fn bind(mut self, ast: Arc<Ast>) -> DBResult<BoundStatement> {
67        let bound = self.bind_stmt(&ast.stmt)?;
68        Ok(bound)
69    }
70
71    // ── Statement dispatch ────────────────────────────────────────────────
72
73    fn bind_stmt(&mut self, stmt: &Stmt) -> DBResult<BoundStatement> {
74        match stmt {
75            Stmt::Match(m) => self.bind_match(m),
76            Stmt::Go(g) => self.bind_go(g),
77            Stmt::Lookup(l) => self.bind_lookup(l),
78            Stmt::Fetch(f) => self.bind_fetch(f),
79            Stmt::FindPath(p) => self.bind_find_path(p),
80            Stmt::Subgraph(s) => self.bind_subgraph(s),
81            Stmt::Return(r) => self.bind_return(r),
82            Stmt::With(w) => self.bind_with(w),
83            Stmt::Unwind(u) => self.bind_unwind(u),
84            Stmt::Pipe(p) => self.bind_pipe(p),
85            Stmt::SetOperation(s) => self.bind_set_operation(s),
86            Stmt::GroupBy(g) => self.bind_group_by(g),
87            Stmt::Insert(s) => self.bind_insert(s),
88            Stmt::Update(s) => self.bind_update(s),
89            Stmt::Delete(s) => self.bind_delete(s),
90            Stmt::Merge(s) => self.bind_merge(s),
91            Stmt::Set(s) => self.bind_set(s),
92            Stmt::Remove(s) => self.bind_remove(s),
93            Stmt::Copy(s) => self.bind_copy(s),
94            Stmt::Create(s) => self.bind_create(s),
95            Stmt::Drop(s) => self.bind_drop(s),
96            Stmt::Alter(s) => self.bind_alter(s),
97            Stmt::Desc(s) => self.bind_desc(s),
98            Stmt::Show(s) => self.bind_show(s),
99            Stmt::ShowCreate(s) => self.bind_show_create(s),
100            Stmt::ClearSpace(s) => self.bind_clear_space(s),
101            Stmt::BeginTransaction(s) => self.bind_begin_transaction(s),
102            Stmt::CommitTransaction(s) => self.bind_commit(s),
103            Stmt::RollbackTransaction(s) => self.bind_rollback(s),
104            Stmt::Savepoint(s) => self.bind_savepoint(s),
105            Stmt::ReleaseSavepoint(s) => self.bind_release_savepoint(s),
106            Stmt::Use(s) => self.bind_use(s),
107            Stmt::Assignment(s) => self.bind_assignment_statement(s),
108            Stmt::AssignVariable(s) => self.bind_assign_variable(s),
109            Stmt::Filter(s) => self.bind_filter(s),
110            Stmt::Yield(s) => self.bind_yield(s),
111            Stmt::Collect(s) => self.bind_collect(s),
112            Stmt::Explain(s) => self.bind_explain(s),
113            Stmt::Profile(s) => self.bind_profile(s),
114            Stmt::CreateUser(s) => self.bind_create_user(s),
115            Stmt::DropUser(s) => self.bind_drop_user(s),
116            Stmt::AlterUser(s) => self.bind_alter_user(s),
117            Stmt::CreateFulltextIndex(s) => self.bind_create_fulltext_index(s),
118            Stmt::CreateVectorIndex(s) => self.bind_create_vector_index(s),
119            Stmt::Checkpoint(_)
120            | Stmt::LoadFrom(_)
121            | Stmt::InQueryCall(_)
122            | Stmt::ExportDatabase(_)
123            | Stmt::ImportDatabase(_)
124            | Stmt::PurgeExpired(_) => Ok(BoundStatement::Other(Box::new(stmt.clone()))),
125            _ => Ok(BoundStatement::Other(Box::new(stmt.clone()))),
126        }
127    }
128
129    /// Wrap a raw expression into a contextual expression for binding.
130    fn plain_expression(expr: Expression) -> linkrs_core::types::ContextualExpression {
131        let ctx = Arc::new(
132            linkrs_core::types::expr::expression_context::ExpressionAnalysisContext::new(),
133        );
134        let id = ctx.register_expression(linkrs_core::types::expr::ExpressionMeta::new(expr));
135        linkrs_core::types::ContextualExpression::new(id, ctx)
136    }
137}
138
139impl Default for Binder {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145// ── Tests ──────────────────────────────────────────────────────────────────
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::binder::bound::BoundExpression;
151    use linkrs_core::error::DBError;
152
153    fn bind_query(query: &str) -> DBResult<BoundStatement> {
154        let mut parser = crate::parser::Parser::new(query);
155        let result = parser
156            .parse()
157            .map_err(|e| DBError::from(linkrs_core::error::QueryError::pipeline_parse_error(e)))?;
158        Binder::new().with_space(None, 0).bind(result.ast)
159    }
160
161    #[test]
162    fn test_bind_exists_subquery() {
163        let bound = bind_query(
164            "MATCH (t:person) WHERE EXISTS { MATCH (p:person) WHERE p.age > 30 } RETURN t.name",
165        )
166        .expect("EXISTS query should bind");
167        let stmt = match bound {
168            BoundStatement::Match(s) => s,
169            other => panic!("expected Match, got {:?}", other.kind()),
170        };
171        let where_clause = stmt.where_clause.expect("where clause expected");
172        match where_clause.condition {
173            BoundExpression::Exists { query, .. } => {
174                let sub = query.as_match().expect("subquery should be a Match");
175                assert!(sub.where_clause.is_some(), "subquery WHERE must be bound");
176                assert_eq!(sub.query_graph.nodes.len(), 1);
177            }
178            other => panic!("expected BoundExpression::Exists, got {:?}", other),
179        }
180    }
181
182    #[test]
183    fn test_bind_exists_bare_pattern() {
184        let bound = bind_query(
185            "MATCH (t:person) WHERE EXISTS { p:person-[:knows]->q:person } RETURN t.name",
186        )
187        .expect("bare-pattern EXISTS query should bind");
188        let stmt = match bound {
189            BoundStatement::Match(s) => s,
190            other => panic!("expected Match, got {:?}", other.kind()),
191        };
192        let where_clause = stmt.where_clause.expect("where clause expected");
193        match where_clause.condition {
194            BoundExpression::Exists { query, .. } => {
195                let sub = query.as_match().expect("subquery should be a Match");
196                assert_eq!(sub.query_graph.nodes.len(), 2, "two nodes in pattern");
197                assert_eq!(sub.query_graph.edges.len(), 1, "one edge in pattern");
198            }
199            other => panic!("expected BoundExpression::Exists, got {:?}", other),
200        }
201    }
202
203    #[test]
204    fn test_bind_in_subquery() {
205        let bound = bind_query(
206            "MATCH (t:person) WHERE t.name IN { MATCH (p:person) RETURN p.name } RETURN t.name",
207        )
208        .expect("IN query should bind");
209        let stmt = match bound {
210            BoundStatement::Match(s) => s,
211            other => panic!("expected Match, got {:?}", other.kind()),
212        };
213        let where_clause = stmt.where_clause.expect("where clause expected");
214        match where_clause.condition {
215            BoundExpression::In {
216                negated, subquery, ..
217            } => {
218                assert!(!negated);
219                assert!(subquery.as_match().is_some());
220            }
221            other => panic!("expected BoundExpression::In, got {:?}", other),
222        }
223    }
224
225    #[test]
226    fn test_bind_correlated_subquery_resolves_outer_variable() {
227        // `t` is defined by the outer MATCH and referenced inside the
228        // subquery WHERE; binding must succeed via the parent scope.
229        let bound = bind_query(
230            "MATCH (t:person) WHERE EXISTS { MATCH (p:person) WHERE p.name = t.name } RETURN t.name",
231        )
232        .expect("correlated EXISTS query should bind");
233        let stmt = match bound {
234            BoundStatement::Match(s) => s,
235            other => panic!("expected Match, got {:?}", other.kind()),
236        };
237        assert!(stmt.where_clause.is_some());
238    }
239
240    #[test]
241    fn test_bind_named_path_registers_path_variables() {
242        let bound = bind_query("MATCH p = (a:person)-[e:knows]->(b:person) RETURN a")
243            .expect("named path query should bind");
244        let stmt = match bound {
245            BoundStatement::Match(s) => s,
246            other => panic!("expected Match, got {:?}", other.kind()),
247        };
248        assert_eq!(stmt.query_graph.nodes.len(), 2);
249        assert_eq!(stmt.query_graph.edges.len(), 1);
250    }
251
252    #[test]
253    fn test_bind_named_path_rejects_duplicate_variable() {
254        let err = bind_query("MATCH p = (p:person)-[e:knows]->(b:person) RETURN p")
255            .expect_err("path name colliding with a node variable must fail");
256        assert!(
257            err.to_string().contains("Duplicate variable"),
258            "unexpected error: {err}"
259        );
260    }
261
262    #[test]
263    fn test_bind_inline_pattern_predicate_matches_exists() {
264        // The inline form must bind exactly like the explicit EXISTS form:
265        // a correlated existence check over the same pattern.
266        let bound = bind_query("MATCH (t:person) WHERE (t)-[:knows]->(p:person) RETURN t.name")
267            .expect("inline pattern predicate should bind");
268        let stmt = match bound {
269            BoundStatement::Match(s) => s,
270            other => panic!("expected Match, got {:?}", other.kind()),
271        };
272        let where_clause = stmt.where_clause.expect("where clause expected");
273        match where_clause.condition {
274            BoundExpression::Exists { query, .. } => {
275                let sub = query.as_match().expect("subquery should be a Match");
276                assert_eq!(sub.query_graph.nodes.len(), 2);
277                assert_eq!(sub.query_graph.edges.len(), 1);
278            }
279            other => panic!("expected BoundExpression::Exists, got {:?}", other),
280        }
281    }
282
283    #[test]
284    fn test_bind_using_join_binary_hint() {
285        let bound = bind_query(
286            "MATCH (a)-[e1:knows]->(b), (a)-[e2:knows]->(c) USING JOIN BINARY(e1, e2) RETURN a",
287        )
288        .expect("hint query should bind");
289        let stmt = match bound {
290            BoundStatement::Match(s) => s,
291            other => panic!("expected Match, got {:?}", other.kind()),
292        };
293        let hint = stmt.join_hint.expect("hint must be bound");
294        assert_eq!(hint.variables(), vec!["e1", "e2"]);
295    }
296
297    #[test]
298    fn test_bind_using_join_multiway_hint() {
299        let bound = bind_query(
300            "MATCH (a)-[e1:knows]->(b), (a)-[e2:knows]->(c) USING JOIN MULTIWAY(e1, e2) RETURN a",
301        )
302        .expect("hint query should bind");
303        let stmt = match bound {
304            BoundStatement::Match(s) => s,
305            other => panic!("expected Match, got {:?}", other.kind()),
306        };
307        match stmt.join_hint.expect("hint must be bound") {
308            crate::binder::bound::BoundJoinHint::Multiway { probe, builds } => {
309                assert_eq!(probe, "e1");
310                assert_eq!(builds, vec!["e2".to_string()]);
311            }
312            other => panic!("expected multiway, got {:?}", other),
313        }
314    }
315
316    #[test]
317    fn test_bind_using_join_unknown_variable_is_error() {
318        let err = bind_query("MATCH (a)-[e1:knows]->(b) USING JOIN BINARY(e1, zzz) RETURN a")
319            .expect_err("unknown hint variable must fail");
320        assert!(err.to_string().contains("zzz"), "error: {err}");
321    }
322
323    #[test]
324    fn test_bind_match_without_hint_has_none() {
325        let bound = bind_query("MATCH (a)-[e1]->(b) RETURN a").expect("must bind");
326        let stmt = match bound {
327            BoundStatement::Match(s) => s,
328            other => panic!("expected Match, got {:?}", other.kind()),
329        };
330        assert!(stmt.join_hint.is_none());
331    }
332
333    #[test]
334    fn test_bind_nested_exists() {
335        let bound = bind_query(
336            "MATCH (t:person) WHERE EXISTS { MATCH (p:person) \
337             WHERE EXISTS { MATCH (q:person) WHERE q.age > p.age } } RETURN t.name",
338        )
339        .expect("nested EXISTS query should bind");
340        let stmt = match bound {
341            BoundStatement::Match(s) => s,
342            other => panic!("expected Match, got {:?}", other.kind()),
343        };
344        let where_clause = stmt.where_clause.expect("where clause expected");
345        match where_clause.condition {
346            BoundExpression::Exists { query, .. } => {
347                let sub = query.as_match().expect("outer subquery");
348                let sub_where = sub.where_clause.as_ref().expect("inner WHERE");
349                assert!(matches!(
350                    sub_where.condition,
351                    BoundExpression::Exists { .. }
352                ));
353            }
354            other => panic!("expected BoundExpression::Exists, got {:?}", other),
355        }
356    }
357
358    fn bind_query_with_macros(
359        query: &str,
360        manager: Arc<linkrs_core::metadata::MacroManager>,
361    ) -> DBResult<BoundStatement> {
362        let mut parser = crate::parser::Parser::new(query);
363        let result = parser
364            .parse()
365            .map_err(|e| DBError::from(linkrs_core::error::QueryError::pipeline_parse_error(e)))?;
366        Binder::new()
367            .with_space(None, 0)
368            .with_macro_manager(manager)
369            .bind(result.ast)
370    }
371
372    fn double_macro() -> linkrs_core::metadata::MacroDef {
373        use linkrs_core::types::expr::Expression;
374        use linkrs_core::types::operators::BinaryOperator;
375        linkrs_core::metadata::MacroDef::new(
376            "double".to_string(),
377            vec![linkrs_core::metadata::MacroParamDef {
378                name: "x".to_string(),
379                default: None,
380            }],
381            Expression::Binary {
382                left: Box::new(Expression::variable("x")),
383                op: BinaryOperator::Multiply,
384                right: Box::new(Expression::literal(linkrs_core::Value::Int(2))),
385            },
386        )
387    }
388
389    #[test]
390    fn test_bind_macro_expansion() {
391        let manager = Arc::new(linkrs_core::metadata::MacroManager::new());
392        manager.create_macro(double_macro()).unwrap();
393        let bound =
394            bind_query_with_macros("RETURN double(21)", manager).expect("macro call should expand");
395        match bound {
396            BoundStatement::Return(ret) => {
397                assert_eq!(ret.items.len(), 1);
398                assert!(
399                    matches!(ret.items[0].expression, BoundExpression::BinaryOp { .. }),
400                    "macro body must expand to a binary expression"
401                );
402            }
403            other => panic!("expected Return, got {:?}", other.kind()),
404        }
405    }
406
407    #[test]
408    fn test_bind_macro_missing_arg() {
409        let manager = Arc::new(linkrs_core::metadata::MacroManager::new());
410        manager.create_macro(double_macro()).unwrap();
411        let err = bind_query_with_macros("RETURN double()", manager).unwrap_err();
412        assert!(
413            err.to_string().contains("missing required argument"),
414            "unexpected error: {err}"
415        );
416    }
417
418    #[test]
419    fn test_bind_macro_recursion_rejected() {
420        use linkrs_core::types::expr::Expression;
421        let manager = Arc::new(linkrs_core::metadata::MacroManager::new());
422        manager
423            .create_macro(linkrs_core::metadata::MacroDef::new(
424                "loopm".to_string(),
425                vec![linkrs_core::metadata::MacroParamDef {
426                    name: "x".to_string(),
427                    default: None,
428                }],
429                Expression::Function {
430                    name: "loopm".to_string(),
431                    args: vec![linkrs_core::types::expr::FunctionArg::positional(
432                        Expression::variable("x"),
433                    )],
434                },
435            ))
436            .unwrap();
437        let err = bind_query_with_macros("RETURN loopm(1)", manager).unwrap_err();
438        assert!(
439            err.to_string().contains("Recursive macro"),
440            "unexpected error: {err}"
441        );
442    }
443
444    #[test]
445    fn test_bind_scalar_subquery() {
446        let bound = bind_query(
447            "MATCH (t:person) WHERE t.age > SUBQUERY { MATCH (p:person) RETURN p.age } RETURN t.name",
448        )
449        .expect("scalar subquery should bind");
450        let stmt = match bound {
451            BoundStatement::Match(s) => s,
452            other => panic!("expected Match, got {:?}", other.kind()),
453        };
454        let where_clause = stmt.where_clause.expect("where clause expected");
455        match where_clause.condition {
456            BoundExpression::BinaryOp { right, .. } => {
457                assert!(
458                    matches!(
459                        right.as_ref(),
460                        BoundExpression::Subquery {
461                            original_body: Some(_),
462                            ..
463                        }
464                    ),
465                    "scalar subquery must keep its original body"
466                );
467            }
468            other => panic!("expected BinaryOp, got {:?}", other),
469        }
470    }
471}