1use std::collections::HashMap;
7
8use kyu_catalog::CatalogContent;
9use kyu_common::{KyuError, KyuResult};
10use kyu_expression::bound_expr::BoundExpression;
11use kyu_expression::{
12 FunctionRegistry, coerce_binary_arithmetic, coerce_comparison, coerce_concat, common_type,
13 try_coerce,
14};
15use kyu_parser::ast::{BinaryOp, ComparisonOp, Expression, Literal};
16use kyu_parser::span::Spanned;
17use kyu_types::{LogicalType, TypedValue};
18use smol_str::SmolStr;
19
20use crate::scope::BinderScope;
21
22pub struct BindContext {
30 pub params: HashMap<SmolStr, TypedValue>,
31 pub env: HashMap<SmolStr, TypedValue>,
32}
33
34impl BindContext {
35 pub fn empty() -> Self {
36 Self {
37 params: HashMap::new(),
38 env: HashMap::new(),
39 }
40 }
41
42 pub fn with_params_json(params: serde_json::Value) -> Self {
49 Self {
50 params: kyu_types::json_object_to_map(params),
51 env: HashMap::new(),
52 }
53 }
54
55 pub fn with_env_json(env: serde_json::Value) -> Self {
62 Self {
63 params: HashMap::new(),
64 env: kyu_types::json_object_to_map(env),
65 }
66 }
67
68 pub fn from_json(params: serde_json::Value, env: serde_json::Value) -> Self {
70 Self {
71 params: kyu_types::json_object_to_map(params),
72 env: kyu_types::json_object_to_map(env),
73 }
74 }
75
76 pub fn with_params_str(json: &str) -> Result<Self, serde_json::Error> {
80 Ok(Self {
81 params: kyu_types::json_str_to_map(json)?,
82 env: HashMap::new(),
83 })
84 }
85
86 pub fn with_env_str(json: &str) -> Result<Self, serde_json::Error> {
90 Ok(Self {
91 params: HashMap::new(),
92 env: kyu_types::json_str_to_map(json)?,
93 })
94 }
95
96 pub fn from_json_str(params: &str, env: &str) -> Result<Self, serde_json::Error> {
100 Ok(Self {
101 params: kyu_types::json_str_to_map(params)?,
102 env: kyu_types::json_str_to_map(env)?,
103 })
104 }
105}
106
107pub fn bind_expression(
109 expr: &Spanned<Expression>,
110 scope: &BinderScope,
111 catalog: &CatalogContent,
112 registry: &FunctionRegistry,
113 ctx: &BindContext,
114) -> KyuResult<BoundExpression> {
115 match &expr.0 {
116 Expression::Literal(lit) => bind_literal(lit),
117
118 Expression::Variable(name) => bind_variable(name, scope),
119
120 Expression::Parameter(name) => match ctx.params.get(name.as_str()) {
121 Some(value) => Ok(BoundExpression::Literal {
122 value: value.clone(),
123 result_type: value.logical_type(),
124 }),
125 None => Err(KyuError::Binder(format!("unresolved parameter '${name}'"))),
126 },
127
128 Expression::Property { object, key } => {
129 bind_property(object, key, scope, catalog, registry, ctx)
130 }
131
132 Expression::FunctionCall {
133 name,
134 distinct,
135 args,
136 } => bind_function_call(name, *distinct, args, scope, catalog, registry, ctx),
137
138 Expression::CountStar => Ok(BoundExpression::CountStar),
139
140 Expression::UnaryOp { op, operand } => {
141 bind_unary_op(*op, operand, scope, catalog, registry, ctx)
142 }
143
144 Expression::BinaryOp { left, op, right } => {
145 bind_binary_op(*op, left, right, scope, catalog, registry, ctx)
146 }
147
148 Expression::Comparison { left, ops } => {
149 bind_comparison(left, ops, scope, catalog, registry, ctx)
150 }
151
152 Expression::IsNull {
153 expr: inner,
154 negated,
155 } => {
156 let bound = bind_expression(inner, scope, catalog, registry, ctx)?;
157 Ok(BoundExpression::IsNull {
158 expr: Box::new(bound),
159 negated: *negated,
160 })
161 }
162
163 Expression::InList {
164 expr: inner,
165 list,
166 negated,
167 } => bind_in_list(inner, list, *negated, scope, catalog, registry, ctx),
168
169 Expression::ListLiteral(elements) => {
170 bind_list_literal(elements, scope, catalog, registry, ctx)
171 }
172
173 Expression::MapLiteral(entries) => bind_map_literal(entries, scope, catalog, registry, ctx),
174
175 Expression::Subscript { expr: inner, index } => {
176 let bound_expr = bind_expression(inner, scope, catalog, registry, ctx)?;
177 let bound_index = bind_expression(index, scope, catalog, registry, ctx)?;
178 let result_type = match bound_expr.result_type() {
179 LogicalType::List(elem) => *elem.clone(),
180 _ => LogicalType::Any,
181 };
182 Ok(BoundExpression::Subscript {
183 expr: Box::new(bound_expr),
184 index: Box::new(bound_index),
185 result_type,
186 })
187 }
188
189 Expression::Slice {
190 expr: inner,
191 from,
192 to,
193 } => {
194 let bound_expr = bind_expression(inner, scope, catalog, registry, ctx)?;
195 let bound_from = from
196 .as_ref()
197 .map(|e| bind_expression(e, scope, catalog, registry, ctx))
198 .transpose()?
199 .map(Box::new);
200 let bound_to = to
201 .as_ref()
202 .map(|e| bind_expression(e, scope, catalog, registry, ctx))
203 .transpose()?
204 .map(Box::new);
205 let result_type = bound_expr.result_type().clone();
206 Ok(BoundExpression::Slice {
207 expr: Box::new(bound_expr),
208 from: bound_from,
209 to: bound_to,
210 result_type,
211 })
212 }
213
214 Expression::Case {
215 operand,
216 whens,
217 else_expr,
218 } => bind_case(operand, whens, else_expr, scope, catalog, registry, ctx),
219
220 Expression::StringOp { left, op, right } => {
221 let bound_left = bind_expression(left, scope, catalog, registry, ctx)?;
222 let bound_right = bind_expression(right, scope, catalog, registry, ctx)?;
223 let bound_left = try_coerce(bound_left, &LogicalType::String)?;
224 let bound_right = try_coerce(bound_right, &LogicalType::String)?;
225 Ok(BoundExpression::StringOp {
226 op: *op,
227 left: Box::new(bound_left),
228 right: Box::new(bound_right),
229 })
230 }
231
232 Expression::HasLabel {
233 expr: inner,
234 labels,
235 } => {
236 let bound = bind_expression(inner, scope, catalog, registry, ctx)?;
237 let mut table_ids = Vec::with_capacity(labels.len());
238 for label in labels {
239 let entry = catalog
240 .find_by_name(&label.0)
241 .ok_or_else(|| KyuError::Binder(format!("label '{}' not found", label.0)))?;
242 table_ids.push(entry.table_id());
243 }
244 Ok(BoundExpression::HasLabel {
245 expr: Box::new(bound),
246 table_ids,
247 })
248 }
249
250 Expression::ExistsSubquery(_)
251 | Expression::CountSubquery(_)
252 | Expression::Quantifier { .. }
253 | Expression::ListComprehension { .. } => Err(KyuError::NotImplemented(
254 "subqueries and quantifiers not yet supported in binder".into(),
255 )),
256 }
257}
258
259fn bind_literal(lit: &Literal) -> KyuResult<BoundExpression> {
260 let (value, result_type) = match lit {
261 Literal::Integer(v) => (TypedValue::Int64(*v), LogicalType::Int64),
262 Literal::Float(v) => (TypedValue::Double(*v), LogicalType::Double),
263 Literal::String(s) => (TypedValue::String(s.clone()), LogicalType::String),
264 Literal::Bool(b) => (TypedValue::Bool(*b), LogicalType::Bool),
265 Literal::Null => (TypedValue::Null, LogicalType::Any),
266 };
267 Ok(BoundExpression::Literal { value, result_type })
268}
269
270fn bind_variable(name: &SmolStr, scope: &BinderScope) -> KyuResult<BoundExpression> {
271 let info = scope
272 .resolve(name)
273 .ok_or_else(|| KyuError::Binder(format!("variable '{name}' is not defined")))?;
274 Ok(BoundExpression::Variable {
275 index: info.index,
276 result_type: info.data_type.clone(),
277 })
278}
279
280fn bind_property(
281 object: &Spanned<Expression>,
282 key: &Spanned<SmolStr>,
283 scope: &BinderScope,
284 catalog: &CatalogContent,
285 registry: &FunctionRegistry,
286 ctx: &BindContext,
287) -> KyuResult<BoundExpression> {
288 let bound_object = bind_expression(object, scope, catalog, registry, ctx)?;
289
290 if let BoundExpression::Variable { index, .. } = &bound_object {
292 let var_info = scope
294 .current_variables()
295 .iter()
296 .chain(std::iter::empty()) .find(|(_, info)| info.index == *index)
298 .map(|(_, info)| info);
299
300 let var_info = var_info.or_else(|| {
302 find_variable_by_index(scope, *index)
304 });
305
306 if let Some(info) = var_info
307 && let Some(table_id) = info.table_id
308 {
309 let entry = catalog.find_by_id(table_id).ok_or_else(|| {
310 KyuError::Binder(format!("table id {table_id:?} not found in catalog"))
311 })?;
312 let prop_name = &key.0;
313 let prop = find_property_on_entry(entry, prop_name)?;
314 return Ok(BoundExpression::Property {
315 object: Box::new(bound_object),
316 property_id: prop.id,
317 property_name: prop.name.clone(),
318 result_type: prop.data_type.clone(),
319 });
320 }
321 }
322
323 Ok(BoundExpression::Property {
326 object: Box::new(bound_object),
327 property_id: kyu_common::id::PropertyId(0),
328 property_name: key.0.clone(),
329 result_type: LogicalType::Any,
330 })
331}
332
333fn find_variable_by_index(scope: &BinderScope, index: u32) -> Option<&crate::scope::VariableInfo> {
334 scope
335 .current_variables()
336 .iter()
337 .find(|(_, info)| info.index == index)
338 .map(|(_, info)| info)
339}
340
341fn find_property_on_entry<'a>(
342 entry: &'a kyu_catalog::CatalogEntry,
343 name: &str,
344) -> KyuResult<&'a kyu_catalog::Property> {
345 let lower = name.to_lowercase();
346 entry
347 .properties()
348 .iter()
349 .find(|p| p.name.to_lowercase() == lower)
350 .ok_or_else(|| {
351 KyuError::Binder(format!(
352 "property '{}' not found on table '{}'",
353 name,
354 entry.name()
355 ))
356 })
357}
358
359fn bind_function_call(
360 name: &[Spanned<SmolStr>],
361 distinct: bool,
362 args: &[Spanned<Expression>],
363 scope: &BinderScope,
364 catalog: &CatalogContent,
365 registry: &FunctionRegistry,
366 ctx: &BindContext,
367) -> KyuResult<BoundExpression> {
368 let func_name: String = name
370 .iter()
371 .map(|(s, _)| s.as_str())
372 .collect::<Vec<_>>()
373 .join(".");
374
375 if func_name == "env" {
378 if args.len() != 1 {
379 return Err(KyuError::Binder(
380 "env() requires exactly one argument".into(),
381 ));
382 }
383 let bound_arg = bind_expression(&args[0], scope, catalog, registry, ctx)?;
384 let key = match &bound_arg {
385 BoundExpression::Literal {
386 value: TypedValue::String(s),
387 ..
388 } => s.clone(),
389 _ => {
390 return Err(KyuError::Binder(
391 "env() argument must be a string literal".into(),
392 ));
393 }
394 };
395 return match ctx.env.get(key.as_str()) {
396 Some(value) => Ok(BoundExpression::Literal {
397 value: value.clone(),
398 result_type: value.logical_type(),
399 }),
400 None => Ok(BoundExpression::Literal {
401 value: TypedValue::Null,
402 result_type: LogicalType::String,
403 }),
404 };
405 }
406
407 let bound_args: Vec<BoundExpression> = args
409 .iter()
410 .map(|a| bind_expression(a, scope, catalog, registry, ctx))
411 .collect::<KyuResult<_>>()?;
412
413 let arg_types: Vec<LogicalType> = bound_args.iter().map(|a| a.result_type().clone()).collect();
414
415 let sig = registry.resolve(&func_name, &arg_types)?;
417
418 Ok(BoundExpression::FunctionCall {
419 function_id: sig.id,
420 function_name: sig.name.clone(),
421 args: bound_args,
422 distinct,
423 result_type: sig.return_type.clone(),
424 })
425}
426
427fn bind_unary_op(
428 op: kyu_parser::ast::UnaryOp,
429 operand: &Spanned<Expression>,
430 scope: &BinderScope,
431 catalog: &CatalogContent,
432 registry: &FunctionRegistry,
433 ctx: &BindContext,
434) -> KyuResult<BoundExpression> {
435 let bound = bind_expression(operand, scope, catalog, registry, ctx)?;
436 let result_type = match op {
437 kyu_parser::ast::UnaryOp::Not => {
438 let bound = try_coerce(bound, &LogicalType::Bool)?;
439 return Ok(BoundExpression::UnaryOp {
440 op,
441 operand: Box::new(bound),
442 result_type: LogicalType::Bool,
443 });
444 }
445 kyu_parser::ast::UnaryOp::Minus => bound.result_type().clone(),
446 kyu_parser::ast::UnaryOp::BitwiseNot => bound.result_type().clone(),
447 };
448 Ok(BoundExpression::UnaryOp {
449 op,
450 operand: Box::new(bound),
451 result_type,
452 })
453}
454
455fn bind_binary_op(
456 op: BinaryOp,
457 left: &Spanned<Expression>,
458 right: &Spanned<Expression>,
459 scope: &BinderScope,
460 catalog: &CatalogContent,
461 registry: &FunctionRegistry,
462 ctx: &BindContext,
463) -> KyuResult<BoundExpression> {
464 let bound_left = bind_expression(left, scope, catalog, registry, ctx)?;
465 let bound_right = bind_expression(right, scope, catalog, registry, ctx)?;
466
467 match op {
468 BinaryOp::Add
469 | BinaryOp::Sub
470 | BinaryOp::Mul
471 | BinaryOp::Div
472 | BinaryOp::Mod
473 | BinaryOp::Pow => {
474 let (l, r, result_type) = coerce_binary_arithmetic(bound_left, bound_right)?;
475 Ok(BoundExpression::BinaryOp {
476 op,
477 left: Box::new(l),
478 right: Box::new(r),
479 result_type,
480 })
481 }
482 BinaryOp::And | BinaryOp::Or | BinaryOp::Xor => {
483 let l = try_coerce(bound_left, &LogicalType::Bool)?;
484 let r = try_coerce(bound_right, &LogicalType::Bool)?;
485 Ok(BoundExpression::BinaryOp {
486 op,
487 left: Box::new(l),
488 right: Box::new(r),
489 result_type: LogicalType::Bool,
490 })
491 }
492 BinaryOp::Concat => {
493 let (l, r) = coerce_concat(bound_left, bound_right)?;
494 Ok(BoundExpression::BinaryOp {
495 op,
496 left: Box::new(l),
497 right: Box::new(r),
498 result_type: LogicalType::String,
499 })
500 }
501 BinaryOp::BitwiseAnd | BinaryOp::BitwiseOr | BinaryOp::ShiftLeft | BinaryOp::ShiftRight => {
502 Ok(BoundExpression::BinaryOp {
503 op,
504 left: Box::new(bound_left),
505 right: Box::new(bound_right),
506 result_type: LogicalType::Int64,
507 })
508 }
509 }
510}
511
512fn bind_comparison(
514 left: &Spanned<Expression>,
515 ops: &[(ComparisonOp, Spanned<Expression>)],
516 scope: &BinderScope,
517 catalog: &CatalogContent,
518 registry: &FunctionRegistry,
519 ctx: &BindContext,
520) -> KyuResult<BoundExpression> {
521 if ops.is_empty() {
522 return bind_expression(left, scope, catalog, registry, ctx);
523 }
524
525 if ops.len() == 1 {
527 let (op, ref right_expr) = ops[0];
528 let bound_left = bind_expression(left, scope, catalog, registry, ctx)?;
529 let bound_right = bind_expression(right_expr, scope, catalog, registry, ctx)?;
530 let (l, r) = coerce_comparison(bound_left, bound_right)?;
531 return Ok(BoundExpression::Comparison {
532 op,
533 left: Box::new(l),
534 right: Box::new(r),
535 });
536 }
537
538 let mut conjuncts = Vec::new();
540 let mut prev = bind_expression(left, scope, catalog, registry, ctx)?;
541
542 for (op, right_expr) in ops {
543 let right = bind_expression(right_expr, scope, catalog, registry, ctx)?;
544 let (l, r) = coerce_comparison(prev.clone(), right.clone())?;
545 conjuncts.push(BoundExpression::Comparison {
546 op: *op,
547 left: Box::new(l),
548 right: Box::new(r),
549 });
550 prev = right;
551 }
552
553 let mut result = conjuncts.pop().unwrap();
555 while let Some(cmp) = conjuncts.pop() {
556 result = BoundExpression::BinaryOp {
557 op: BinaryOp::And,
558 left: Box::new(cmp),
559 right: Box::new(result),
560 result_type: LogicalType::Bool,
561 };
562 }
563
564 Ok(result)
565}
566
567fn bind_in_list(
568 expr: &Spanned<Expression>,
569 list: &Spanned<Expression>,
570 negated: bool,
571 scope: &BinderScope,
572 catalog: &CatalogContent,
573 registry: &FunctionRegistry,
574 ctx: &BindContext,
575) -> KyuResult<BoundExpression> {
576 let bound_expr = bind_expression(expr, scope, catalog, registry, ctx)?;
577 let bound_list = bind_expression(list, scope, catalog, registry, ctx)?;
578
579 let list_items = match bound_list {
581 BoundExpression::ListLiteral { elements, .. } => elements,
582 other => vec![other],
583 };
584
585 Ok(BoundExpression::InList {
586 expr: Box::new(bound_expr),
587 list: list_items,
588 negated,
589 })
590}
591
592fn bind_list_literal(
593 elements: &[Spanned<Expression>],
594 scope: &BinderScope,
595 catalog: &CatalogContent,
596 registry: &FunctionRegistry,
597 ctx: &BindContext,
598) -> KyuResult<BoundExpression> {
599 let bound: Vec<BoundExpression> = elements
600 .iter()
601 .map(|e| bind_expression(e, scope, catalog, registry, ctx))
602 .collect::<KyuResult<_>>()?;
603
604 let elem_types: Vec<LogicalType> = bound.iter().map(|e| e.result_type().clone()).collect();
605 let elem_type = if elem_types.is_empty() {
606 LogicalType::Any
607 } else {
608 common_type(&elem_types)?
609 };
610
611 Ok(BoundExpression::ListLiteral {
612 elements: bound,
613 result_type: LogicalType::List(Box::new(elem_type)),
614 })
615}
616
617fn bind_map_literal(
618 entries: &[(Spanned<SmolStr>, Spanned<Expression>)],
619 scope: &BinderScope,
620 catalog: &CatalogContent,
621 registry: &FunctionRegistry,
622 ctx: &BindContext,
623) -> KyuResult<BoundExpression> {
624 let bound: Vec<(BoundExpression, BoundExpression)> = entries
625 .iter()
626 .map(|(k, v)| {
627 let key = BoundExpression::Literal {
628 value: TypedValue::String(k.0.clone()),
629 result_type: LogicalType::String,
630 };
631 let val = bind_expression(v, scope, catalog, registry, ctx)?;
632 Ok((key, val))
633 })
634 .collect::<KyuResult<_>>()?;
635
636 let val_types: Vec<LogicalType> = bound.iter().map(|(_, v)| v.result_type().clone()).collect();
637 let val_type = if val_types.is_empty() {
638 LogicalType::Any
639 } else {
640 common_type(&val_types)?
641 };
642
643 Ok(BoundExpression::MapLiteral {
644 entries: bound,
645 result_type: LogicalType::Map {
646 key: Box::new(LogicalType::String),
647 value: Box::new(val_type),
648 },
649 })
650}
651
652fn bind_case(
653 operand: &Option<Box<Spanned<Expression>>>,
654 whens: &[(Spanned<Expression>, Spanned<Expression>)],
655 else_expr: &Option<Box<Spanned<Expression>>>,
656 scope: &BinderScope,
657 catalog: &CatalogContent,
658 registry: &FunctionRegistry,
659 ctx: &BindContext,
660) -> KyuResult<BoundExpression> {
661 let bound_operand = operand
662 .as_ref()
663 .map(|e| bind_expression(e, scope, catalog, registry, ctx))
664 .transpose()?
665 .map(Box::new);
666
667 let mut bound_whens = Vec::with_capacity(whens.len());
668 let mut result_types = Vec::new();
669
670 for (when_expr, then_expr) in whens {
671 let w = bind_expression(when_expr, scope, catalog, registry, ctx)?;
672 let t = bind_expression(then_expr, scope, catalog, registry, ctx)?;
673 result_types.push(t.result_type().clone());
674 bound_whens.push((w, t));
675 }
676
677 let bound_else = else_expr
678 .as_ref()
679 .map(|e| bind_expression(e, scope, catalog, registry, ctx))
680 .transpose()?;
681
682 if let Some(ref e) = bound_else {
683 result_types.push(e.result_type().clone());
684 }
685
686 let result_type = if result_types.is_empty() {
687 LogicalType::Any
688 } else {
689 common_type(&result_types)?
690 };
691
692 Ok(BoundExpression::Case {
693 operand: bound_operand,
694 whens: bound_whens,
695 else_expr: bound_else.map(Box::new),
696 result_type,
697 })
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use kyu_catalog::{CatalogContent, NodeTableEntry, Property, RelTableEntry};
704 use kyu_common::id::{PropertyId, TableId};
705 use kyu_expression::FunctionRegistry;
706
707 fn make_catalog() -> CatalogContent {
708 let mut catalog = CatalogContent::new();
709 catalog
710 .add_node_table(NodeTableEntry {
711 table_id: TableId(0),
712 name: SmolStr::new("Person"),
713 properties: vec![
714 Property::new(PropertyId(0), "name", LogicalType::String, true),
715 Property::new(PropertyId(1), "age", LogicalType::Int64, false),
716 ],
717 primary_key_idx: 0,
718 num_rows: 0,
719 comment: None,
720 })
721 .unwrap();
722 catalog
723 .add_rel_table(RelTableEntry {
724 table_id: TableId(1),
725 name: SmolStr::new("KNOWS"),
726 from_table_id: TableId(0),
727 to_table_id: TableId(0),
728 properties: vec![Property::new(
729 PropertyId(2),
730 "since",
731 LogicalType::Int64,
732 false,
733 )],
734 num_rows: 0,
735 comment: None,
736 })
737 .unwrap();
738 catalog
739 }
740
741 fn parse_expr(s: &str) -> Spanned<Expression> {
742 let result = kyu_parser::parse(&format!("RETURN {s}"));
744 let stmt = result.ast.expect("parse failed");
745 match stmt {
746 kyu_parser::ast::Statement::Query(q) => {
747 let proj = q.parts[0].projection.as_ref().unwrap();
748 match &proj.items {
749 kyu_parser::ast::ProjectionItems::Expressions(exprs) => exprs[0].0.clone(),
750 _ => panic!("expected expressions"),
751 }
752 }
753 _ => panic!("expected query"),
754 }
755 }
756
757 #[test]
758 fn bind_integer_literal() {
759 let catalog = make_catalog();
760 let scope = BinderScope::new();
761 let registry = FunctionRegistry::with_builtins();
762 let ctx = BindContext::empty();
763 let expr = parse_expr("42");
764 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
765 assert_eq!(bound.result_type(), &LogicalType::Int64);
766 assert!(bound.is_constant());
767 }
768
769 #[test]
770 fn bind_string_literal() {
771 let catalog = make_catalog();
772 let scope = BinderScope::new();
773 let registry = FunctionRegistry::with_builtins();
774 let ctx = BindContext::empty();
775 let expr = parse_expr("'hello'");
776 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
777 assert_eq!(bound.result_type(), &LogicalType::String);
778 }
779
780 #[test]
781 fn bind_bool_literal() {
782 let catalog = make_catalog();
783 let scope = BinderScope::new();
784 let registry = FunctionRegistry::with_builtins();
785 let ctx = BindContext::empty();
786 let expr = parse_expr("true");
787 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
788 assert_eq!(bound.result_type(), &LogicalType::Bool);
789 }
790
791 #[test]
792 fn bind_null_literal() {
793 let catalog = make_catalog();
794 let scope = BinderScope::new();
795 let registry = FunctionRegistry::with_builtins();
796 let ctx = BindContext::empty();
797 let expr = parse_expr("null");
798 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
799 assert_eq!(bound.result_type(), &LogicalType::Any);
800 }
801
802 #[test]
803 fn bind_variable_found() {
804 let catalog = make_catalog();
805 let mut scope = BinderScope::new();
806 scope
807 .define("p", LogicalType::Node, Some(TableId(0)))
808 .unwrap();
809 let registry = FunctionRegistry::with_builtins();
810 let ctx = BindContext::empty();
811 let expr = parse_expr("p");
812 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
813 assert!(matches!(bound, BoundExpression::Variable { index: 0, .. }));
814 }
815
816 #[test]
817 fn bind_variable_not_found() {
818 let catalog = make_catalog();
819 let scope = BinderScope::new();
820 let registry = FunctionRegistry::with_builtins();
821 let ctx = BindContext::empty();
822 let expr = parse_expr("unknown_var");
823 let result = bind_expression(&expr, &scope, &catalog, ®istry, &ctx);
824 assert!(result.is_err());
825 }
826
827 #[test]
828 fn bind_property_access() {
829 let catalog = make_catalog();
830 let mut scope = BinderScope::new();
831 scope
832 .define("p", LogicalType::Node, Some(TableId(0)))
833 .unwrap();
834 let registry = FunctionRegistry::with_builtins();
835 let ctx = BindContext::empty();
836 let expr = parse_expr("p.name");
837 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
838 assert_eq!(bound.result_type(), &LogicalType::String);
839 if let BoundExpression::Property { property_id, .. } = &bound {
840 assert_eq!(*property_id, PropertyId(0));
841 } else {
842 panic!("expected Property");
843 }
844 }
845
846 #[test]
847 fn bind_property_not_found() {
848 let catalog = make_catalog();
849 let mut scope = BinderScope::new();
850 scope
851 .define("p", LogicalType::Node, Some(TableId(0)))
852 .unwrap();
853 let registry = FunctionRegistry::with_builtins();
854 let ctx = BindContext::empty();
855 let expr = parse_expr("p.nonexistent");
856 let result = bind_expression(&expr, &scope, &catalog, ®istry, &ctx);
857 assert!(result.is_err());
858 }
859
860 #[test]
861 fn bind_binary_add_coercion() {
862 let catalog = make_catalog();
863 let scope = BinderScope::new();
864 let registry = FunctionRegistry::with_builtins();
865 let ctx = BindContext::empty();
866 let expr = parse_expr("1 + 2.0");
867 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
868 assert_eq!(bound.result_type(), &LogicalType::Double);
869 }
870
871 #[test]
872 fn bind_comparison_gt() {
873 let catalog = make_catalog();
874 let scope = BinderScope::new();
875 let registry = FunctionRegistry::with_builtins();
876 let ctx = BindContext::empty();
877 let expr = parse_expr("1 > 2");
878 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
879 assert_eq!(bound.result_type(), &LogicalType::Bool);
880 }
881
882 #[test]
883 fn bind_function_call_test() {
884 let catalog = make_catalog();
885 let scope = BinderScope::new();
886 let registry = FunctionRegistry::with_builtins();
887 let ctx = BindContext::empty();
888 let expr = parse_expr("upper('hello')");
889 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
890 assert_eq!(bound.result_type(), &LogicalType::String);
891 }
892
893 #[test]
894 fn bind_count_star() {
895 let catalog = make_catalog();
896 let scope = BinderScope::new();
897 let registry = FunctionRegistry::with_builtins();
898 let ctx = BindContext::empty();
899 let expr = parse_expr("count(*)");
900 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
901 assert_eq!(bound.result_type(), &LogicalType::Int64);
902 }
903
904 #[test]
905 fn bind_is_null() {
906 let catalog = make_catalog();
907 let scope = BinderScope::new();
908 let registry = FunctionRegistry::with_builtins();
909 let ctx = BindContext::empty();
910 let expr = parse_expr("null IS NULL");
911 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
912 assert_eq!(bound.result_type(), &LogicalType::Bool);
913 }
914
915 #[test]
916 fn bind_case_expression() {
917 let catalog = make_catalog();
918 let scope = BinderScope::new();
919 let registry = FunctionRegistry::with_builtins();
920 let ctx = BindContext::empty();
921 let expr = parse_expr("CASE WHEN true THEN 1 ELSE 2 END");
922 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
923 assert_eq!(bound.result_type(), &LogicalType::Int64);
924 }
925
926 #[test]
927 fn bind_string_starts_with() {
928 let catalog = make_catalog();
929 let scope = BinderScope::new();
930 let registry = FunctionRegistry::with_builtins();
931 let ctx = BindContext::empty();
932 let expr = parse_expr("'hello' STARTS WITH 'he'");
933 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
934 assert_eq!(bound.result_type(), &LogicalType::Bool);
935 }
936
937 #[test]
938 fn bind_list_literal_test() {
939 let catalog = make_catalog();
940 let scope = BinderScope::new();
941 let registry = FunctionRegistry::with_builtins();
942 let ctx = BindContext::empty();
943 let expr = parse_expr("[1, 2, 3]");
944 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
945 assert_eq!(
946 bound.result_type(),
947 &LogicalType::List(Box::new(LogicalType::Int64))
948 );
949 }
950
951 #[test]
952 fn bind_arithmetic_type_error() {
953 let catalog = make_catalog();
954 let scope = BinderScope::new();
955 let registry = FunctionRegistry::with_builtins();
956 let ctx = BindContext::empty();
957 let expr = parse_expr("'hello' + 42");
958 let result = bind_expression(&expr, &scope, &catalog, ®istry, &ctx);
959 assert!(result.is_err());
960 }
961
962 #[test]
963 fn bind_unary_not() {
964 let catalog = make_catalog();
965 let scope = BinderScope::new();
966 let registry = FunctionRegistry::with_builtins();
967 let ctx = BindContext::empty();
968 let expr = parse_expr("NOT true");
969 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
970 assert_eq!(bound.result_type(), &LogicalType::Bool);
971 }
972
973 #[test]
976 fn bind_param_resolved() {
977 let catalog = make_catalog();
978 let scope = BinderScope::new();
979 let registry = FunctionRegistry::with_builtins();
980 let mut ctx = BindContext::empty();
981 ctx.params.insert(SmolStr::new("x"), TypedValue::Int64(42));
982 let expr = parse_expr("$x");
983 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
984 assert_eq!(bound.result_type(), &LogicalType::Int64);
985 match &bound {
986 BoundExpression::Literal { value, .. } => {
987 assert_eq!(value, &TypedValue::Int64(42));
988 }
989 _ => panic!("expected Literal"),
990 }
991 }
992
993 #[test]
994 fn bind_param_unresolved_error() {
995 let catalog = make_catalog();
996 let scope = BinderScope::new();
997 let registry = FunctionRegistry::with_builtins();
998 let ctx = BindContext::empty();
999 let expr = parse_expr("$missing");
1000 let result = bind_expression(&expr, &scope, &catalog, ®istry, &ctx);
1001 assert!(result.is_err());
1002 let err = result.unwrap_err().to_string();
1003 assert!(err.contains("unresolved parameter '$missing'"));
1004 }
1005
1006 #[test]
1007 fn bind_param_in_comparison() {
1008 let catalog = make_catalog();
1009 let scope = BinderScope::new();
1010 let registry = FunctionRegistry::with_builtins();
1011 let mut ctx = BindContext::empty();
1012 ctx.params
1013 .insert(SmolStr::new("age"), TypedValue::Int64(30));
1014 let expr = parse_expr("42 > $age");
1015 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1016 assert_eq!(bound.result_type(), &LogicalType::Bool);
1017 }
1018
1019 #[test]
1020 fn bind_param_string_type() {
1021 let catalog = make_catalog();
1022 let scope = BinderScope::new();
1023 let registry = FunctionRegistry::with_builtins();
1024 let mut ctx = BindContext::empty();
1025 ctx.params.insert(
1026 SmolStr::new("name"),
1027 TypedValue::String(SmolStr::new("Alice")),
1028 );
1029 let expr = parse_expr("$name");
1030 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1031 assert_eq!(bound.result_type(), &LogicalType::String);
1032 }
1033
1034 #[test]
1037 fn bind_env_resolved() {
1038 let catalog = make_catalog();
1039 let scope = BinderScope::new();
1040 let registry = FunctionRegistry::with_builtins();
1041 let mut ctx = BindContext::empty();
1042 ctx.env.insert(
1043 SmolStr::new("DATA_DIR"),
1044 TypedValue::String(SmolStr::new("/data")),
1045 );
1046 let expr = parse_expr("env('DATA_DIR')");
1047 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1048 assert_eq!(bound.result_type(), &LogicalType::String);
1049 match &bound {
1050 BoundExpression::Literal { value, .. } => {
1051 assert_eq!(value, &TypedValue::String(SmolStr::new("/data")));
1052 }
1053 _ => panic!("expected Literal"),
1054 }
1055 }
1056
1057 #[test]
1058 fn bind_env_missing_returns_null() {
1059 let catalog = make_catalog();
1060 let scope = BinderScope::new();
1061 let registry = FunctionRegistry::with_builtins();
1062 let ctx = BindContext::empty();
1063 let expr = parse_expr("env('MISSING')");
1064 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1065 match &bound {
1066 BoundExpression::Literal { value, .. } => {
1067 assert_eq!(value, &TypedValue::Null);
1068 }
1069 _ => panic!("expected Literal"),
1070 }
1071 }
1072
1073 #[test]
1074 fn bind_env_non_string_arg_error() {
1075 let catalog = make_catalog();
1076 let scope = BinderScope::new();
1077 let registry = FunctionRegistry::with_builtins();
1078 let ctx = BindContext::empty();
1079 let expr = parse_expr("env(42)");
1080 let result = bind_expression(&expr, &scope, &catalog, ®istry, &ctx);
1081 assert!(result.is_err());
1082 let err = result.unwrap_err().to_string();
1083 assert!(err.contains("string literal"));
1084 }
1085
1086 #[test]
1089 fn bind_ctx_with_params_json() {
1090 let catalog = make_catalog();
1091 let scope = BinderScope::new();
1092 let registry = FunctionRegistry::with_builtins();
1093 let ctx = BindContext::with_params_json(serde_json::json!({"x": 42}));
1094 let expr = parse_expr("$x");
1095 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1096 assert_eq!(bound.result_type(), &LogicalType::Int64);
1097 match &bound {
1098 BoundExpression::Literal { value, .. } => {
1099 assert_eq!(value, &TypedValue::Int64(42));
1100 }
1101 _ => panic!("expected Literal"),
1102 }
1103 }
1104
1105 #[test]
1106 fn bind_ctx_with_env_json() {
1107 let catalog = make_catalog();
1108 let scope = BinderScope::new();
1109 let registry = FunctionRegistry::with_builtins();
1110 let ctx = BindContext::with_env_json(serde_json::json!({"DIR": "/data"}));
1111 let expr = parse_expr("env('DIR')");
1112 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1113 match &bound {
1114 BoundExpression::Literal { value, .. } => {
1115 assert_eq!(value, &TypedValue::String(SmolStr::new("/data")));
1116 }
1117 _ => panic!("expected Literal"),
1118 }
1119 }
1120
1121 #[test]
1122 fn bind_ctx_from_json() {
1123 let catalog = make_catalog();
1124 let scope = BinderScope::new();
1125 let registry = FunctionRegistry::with_builtins();
1126 let ctx = BindContext::from_json(
1127 serde_json::json!({"x": 100}),
1128 serde_json::json!({"KEY": "val"}),
1129 );
1130 let expr = parse_expr("$x");
1131 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1132 assert_eq!(bound.result_type(), &LogicalType::Int64);
1133 let expr2 = parse_expr("env('KEY')");
1134 let bound2 = bind_expression(&expr2, &scope, &catalog, ®istry, &ctx).unwrap();
1135 assert_eq!(bound2.result_type(), &LogicalType::String);
1136 }
1137
1138 #[test]
1139 fn bind_ctx_with_params_str() {
1140 let catalog = make_catalog();
1141 let scope = BinderScope::new();
1142 let registry = FunctionRegistry::with_builtins();
1143 let ctx = BindContext::with_params_str(r#"{"n": 7}"#).unwrap();
1144 let expr = parse_expr("$n");
1145 let bound = bind_expression(&expr, &scope, &catalog, ®istry, &ctx).unwrap();
1146 assert_eq!(bound.result_type(), &LogicalType::Int64);
1147 }
1148
1149 #[test]
1150 fn bind_ctx_with_params_str_invalid() {
1151 let result = BindContext::with_params_str("not json");
1152 assert!(result.is_err());
1153 }
1154}