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
25pub 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 pub(crate) expanding: Vec<String>,
35 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 pub fn bind(mut self, ast: Arc<Ast>) -> DBResult<BoundStatement> {
67 let bound = self.bind_stmt(&ast.stmt)?;
68 Ok(bound)
69 }
70
71 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 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#[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 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 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}