1use crate::ast::{
2 BinOp, CompareOp, DeleteQuery, Expr, FieldRef, Filter, GraphQuery, InsertQuery, JoinQuery,
3 PathQuery, Projection, SelectItem, Span, TableQuery, UnaryOp, UpdateQuery, VectorQuery,
4};
5use reddb_types::types::Value;
6use reddb_types::vector_metadata::MetadataFilter;
7
8pub const PARAMETER_PROJECTION_PREFIX: &str = "__user_param_projection__:";
9
10pub fn expr_contains_parameter(expr: &Expr) -> bool {
14 match expr {
15 Expr::Parameter { .. } => true,
16 Expr::Literal { .. } | Expr::Column { .. } => false,
17 Expr::BinaryOp { lhs, rhs, .. } => {
18 expr_contains_parameter(lhs) || expr_contains_parameter(rhs)
19 }
20 Expr::UnaryOp { operand, .. } => expr_contains_parameter(operand),
21 Expr::Cast { inner, .. } => expr_contains_parameter(inner),
22 Expr::FunctionCall { args, .. } => args.iter().any(expr_contains_parameter),
23 Expr::Case {
24 branches, else_, ..
25 } => {
26 branches
27 .iter()
28 .any(|(c, v)| expr_contains_parameter(c) || expr_contains_parameter(v))
29 || else_.as_deref().is_some_and(expr_contains_parameter)
30 }
31 Expr::IsNull { operand, .. } => expr_contains_parameter(operand),
32 Expr::InList { target, values, .. } => {
33 expr_contains_parameter(target) || values.iter().any(expr_contains_parameter)
34 }
35 Expr::Between {
36 target, low, high, ..
37 } => {
38 expr_contains_parameter(target)
39 || expr_contains_parameter(low)
40 || expr_contains_parameter(high)
41 }
42 Expr::Subquery { .. } => false,
43 Expr::WindowFunctionCall { args, window, .. } => {
44 args.iter().any(expr_contains_parameter)
45 || window.partition_by.iter().any(expr_contains_parameter)
46 || window
47 .order_by
48 .iter()
49 .any(|o| expr_contains_parameter(&o.expr))
50 }
51 }
52}
53
54pub fn expr_to_projection(expr: &Expr) -> Option<Projection> {
55 match expr {
56 Expr::Literal { value, .. } => projection_from_literal(value),
57 Expr::Column { field, .. } => {
58 if matches!(
59 field,
60 FieldRef::TableColumn { table, column } if table.is_empty() && column == "*"
61 ) {
62 Some(Projection::All)
63 } else {
64 Some(Projection::Field(field.clone(), None))
65 }
66 }
67 Expr::Parameter { index, .. } => Some(Projection::Column(format!(
68 "{PARAMETER_PROJECTION_PREFIX}{index}"
69 ))),
70 Expr::BinaryOp { op, lhs, rhs, .. } => match op {
71 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
72 Some(Projection::Function(
73 projection_binop_name(*op).to_string(),
74 vec![expr_to_projection(lhs)?, expr_to_projection(rhs)?],
75 ))
76 }
77 _ => Some(boolean_expr_projection(expr.clone())),
78 },
79 Expr::UnaryOp { op, operand, .. } => match op {
80 UnaryOp::Neg => Some(Projection::Function(
81 "SUB".to_string(),
82 vec![
83 Projection::Column("LIT:0".to_string()),
84 expr_to_projection(operand)?,
85 ],
86 )),
87 UnaryOp::Not => Some(boolean_expr_projection(expr.clone())),
88 },
89 Expr::Cast { inner, target, .. } => Some(Projection::Function(
90 "CAST".to_string(),
91 vec![
92 expr_to_projection(inner)?,
93 Projection::Column(format!("TYPE:{target}")),
94 ],
95 )),
96 Expr::FunctionCall { name, args, .. } => Some(Projection::Function(
97 name.to_uppercase(),
98 args.iter()
99 .map(expr_to_projection)
100 .collect::<Option<Vec<_>>>()?,
101 )),
102 Expr::Case {
103 branches, else_, ..
104 } => {
105 let mut args = Vec::with_capacity(branches.len() * 2 + usize::from(else_.is_some()));
106 for (cond, value) in branches {
107 args.push(case_condition_projection(cond.clone()));
108 args.push(expr_to_projection(value)?);
109 }
110 if let Some(else_expr) = else_ {
111 args.push(expr_to_projection(else_expr)?);
112 }
113 Some(Projection::Function("CASE".to_string(), args))
114 }
115 Expr::IsNull { .. }
116 | Expr::InList { .. }
117 | Expr::Between { .. }
118 | Expr::Subquery { .. } => Some(boolean_expr_projection(expr.clone())),
119 Expr::WindowFunctionCall {
120 name, args, window, ..
121 } => {
122 let lowered_args = args
123 .iter()
124 .map(expr_to_projection)
125 .collect::<Option<Vec<_>>>()?;
126 Some(crate::ast::Projection::Window {
127 name: name.to_uppercase(),
128 args: lowered_args,
129 window: Box::new(window.clone()),
130 alias: None,
131 })
132 }
133 }
134}
135
136pub fn select_item_to_projection(item: &SelectItem) -> Option<Projection> {
137 match item {
138 SelectItem::Wildcard => Some(Projection::All),
139 SelectItem::Expr { expr, alias } => {
140 let projection = expr_to_projection(expr)?;
141 Some(attach_projection_alias(projection, alias.clone()))
150 }
151 }
152}
153
154pub fn effective_table_projections(query: &TableQuery) -> Vec<Projection> {
155 if !query.select_items.is_empty() {
156 return query
157 .select_items
158 .iter()
159 .filter_map(select_item_to_projection)
160 .collect();
161 }
162 if query.columns.is_empty() {
163 vec![Projection::All]
164 } else {
165 query.columns.clone()
166 }
167}
168
169pub fn effective_table_filter(query: &TableQuery) -> Option<Filter> {
170 query
171 .filter
172 .clone()
173 .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
174 .map(|f| f.optimize()) }
176
177pub fn effective_table_group_by_exprs(query: &TableQuery) -> Vec<Expr> {
178 if !query.group_by_exprs.is_empty() {
179 query.group_by_exprs.clone()
180 } else {
181 query
182 .group_by
183 .iter()
184 .map(|column| Expr::Column {
185 field: FieldRef::TableColumn {
186 table: String::new(),
187 column: column.clone(),
188 },
189 span: Span::synthetic(),
190 })
191 .collect()
192 }
193}
194
195pub fn effective_table_having_filter(query: &TableQuery) -> Option<Filter> {
196 query
197 .having
198 .clone()
199 .or_else(|| query.having_expr.as_ref().map(expr_to_filter))
200}
201
202pub fn effective_update_filter(query: &UpdateQuery) -> Option<Filter> {
203 query
204 .filter
205 .clone()
206 .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
207}
208
209pub fn effective_insert_rows(query: &InsertQuery) -> Result<Vec<Vec<Value>>, String> {
210 if !query.value_exprs.is_empty() {
211 return query
212 .value_exprs
213 .iter()
214 .cloned()
215 .map(|row| row.into_iter().map(fold_expr_to_value).collect())
216 .collect();
217 }
218 Ok(query.values.clone())
219}
220
221pub fn effective_delete_filter(query: &DeleteQuery) -> Option<Filter> {
222 query
223 .filter
224 .clone()
225 .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
226}
227
228pub fn effective_join_filter(query: &JoinQuery) -> Option<Filter> {
229 query.filter.clone()
230}
231
232pub fn effective_graph_filter(query: &GraphQuery) -> Option<Filter> {
233 query.filter.clone()
234}
235
236pub fn effective_graph_projections(query: &GraphQuery) -> Vec<Projection> {
237 query.return_.clone()
238}
239
240pub fn effective_path_filter(query: &PathQuery) -> Option<Filter> {
241 query.filter.clone()
242}
243
244pub fn effective_path_projections(query: &PathQuery) -> Vec<Projection> {
245 query.return_.clone()
246}
247
248pub fn effective_vector_filter(query: &VectorQuery) -> Option<MetadataFilter> {
249 query.filter.clone()
250}
251
252pub fn projection_to_expr(projection: &Projection) -> Option<(Expr, Option<String>)> {
253 match projection {
254 Projection::All => Some((
255 Expr::Column {
256 field: FieldRef::TableColumn {
257 table: String::new(),
258 column: "*".to_string(),
259 },
260 span: Span::synthetic(),
261 },
262 None,
263 )),
264 Projection::Column(column) => Some((projection_column_to_expr(column), None)),
265 Projection::Alias(column, alias) => {
266 Some((projection_column_to_expr(column), Some(alias.clone())))
267 }
268 Projection::Function(name, args) => {
269 let (name, alias) = split_projection_function_alias(name);
270 let args = args
271 .iter()
272 .map(projection_to_expr)
273 .collect::<Option<Vec<_>>>()?
274 .into_iter()
275 .map(|(expr, _)| expr)
276 .collect();
277 Some((
278 Expr::FunctionCall {
279 name,
280 args,
281 span: Span::synthetic(),
282 },
283 alias,
284 ))
285 }
286 Projection::Expression(filter, alias) => Some((filter_to_expr(filter), alias.clone())),
287 Projection::Field(field, alias) => Some((
288 Expr::Column {
289 field: field.clone(),
290 span: Span::synthetic(),
291 },
292 alias.clone(),
293 )),
294 Projection::Window {
295 name,
296 args,
297 window,
298 alias,
299 } => {
300 let args = args
301 .iter()
302 .map(projection_to_expr)
303 .collect::<Option<Vec<_>>>()?
304 .into_iter()
305 .map(|(expr, _)| expr)
306 .collect();
307 Some((
308 Expr::WindowFunctionCall {
309 name: name.clone(),
310 args,
311 window: (**window).clone(),
312 span: Span::synthetic(),
313 },
314 alias.clone(),
315 ))
316 }
317 }
318}
319
320fn projection_column_to_expr(column: &str) -> Expr {
321 if let Some(value) = projection_literal_value(column) {
322 return Expr::Literal {
323 value,
324 span: Span::synthetic(),
325 };
326 }
327
328 Expr::Column {
329 field: FieldRef::TableColumn {
330 table: String::new(),
331 column: column.to_string(),
332 },
333 span: Span::synthetic(),
334 }
335}
336
337fn projection_literal_value(column: &str) -> Option<Value> {
338 let literal = column.strip_prefix("LIT:")?;
339 if literal.is_empty() {
340 return Some(Value::Null);
341 }
342 if let Ok(value) = literal.parse::<i64>() {
343 return Some(Value::Integer(value));
344 }
345 if let Ok(value) = literal.parse::<f64>() {
346 return Some(Value::Float(value));
347 }
348 Some(Value::text(literal.to_string()))
349}
350
351pub fn projection_to_select_item(projection: &Projection) -> Option<SelectItem> {
352 match projection {
353 Projection::All => Some(SelectItem::Wildcard),
354 other => {
355 let (expr, alias) = projection_to_expr(other)?;
356 Some(SelectItem::Expr { expr, alias })
357 }
358 }
359}
360
361pub fn effective_join_projections(query: &JoinQuery) -> Vec<Projection> {
362 if !query.return_items.is_empty() {
363 return query
364 .return_items
365 .iter()
366 .filter_map(select_item_to_projection)
367 .collect();
368 }
369 query.return_.clone()
370}
371
372pub fn expr_to_filter(expr: &Expr) -> Filter {
373 match expr {
374 Expr::BinaryOp { op, lhs, rhs, .. } => match op {
375 BinOp::And => Filter::And(Box::new(expr_to_filter(lhs)), Box::new(expr_to_filter(rhs))),
376 BinOp::Or => Filter::Or(Box::new(expr_to_filter(lhs)), Box::new(expr_to_filter(rhs))),
377 BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
378 try_specialized_compare_filter(lhs, *op, rhs).unwrap_or_else(|| {
379 Filter::CompareExpr {
380 lhs: lhs.as_ref().clone(),
381 op: binop_to_compare_op(*op),
382 rhs: rhs.as_ref().clone(),
383 }
384 })
385 }
386 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
387 Filter::CompareExpr {
388 lhs: expr.clone(),
389 op: CompareOp::Eq,
390 rhs: Expr::lit(Value::Boolean(true)),
391 }
392 }
393 },
394 Expr::UnaryOp {
395 op: UnaryOp::Not,
396 operand,
397 ..
398 } => Filter::Not(Box::new(expr_to_filter(operand))),
399 Expr::IsNull {
400 operand, negated, ..
401 } => match operand.as_ref() {
402 Expr::Column { field, .. } => {
403 if *negated {
404 Filter::IsNotNull(field.clone())
405 } else {
406 Filter::IsNull(field.clone())
407 }
408 }
409 _ => Filter::CompareExpr {
410 lhs: expr.clone(),
411 op: CompareOp::Eq,
412 rhs: Expr::lit(Value::Boolean(true)),
413 },
414 },
415 Expr::InList {
416 target,
417 values,
418 negated,
419 ..
420 } => match (target.as_ref(), all_literal_values(values)) {
421 (Expr::Column { field, .. }, Some(values)) if !negated => Filter::In {
422 field: field.clone(),
423 values,
424 },
425 _ => Filter::CompareExpr {
426 lhs: expr.clone(),
427 op: CompareOp::Eq,
428 rhs: Expr::lit(Value::Boolean(true)),
429 },
430 },
431 Expr::Between {
432 target,
433 low,
434 high,
435 negated,
436 ..
437 } => match (
438 target.as_ref(),
439 literal_expr_value(low),
440 literal_expr_value(high),
441 ) {
442 (Expr::Column { field, .. }, Some(low), Some(high)) if !negated => Filter::Between {
443 field: field.clone(),
444 low,
445 high,
446 },
447 _ => Filter::CompareExpr {
448 lhs: expr.clone(),
449 op: CompareOp::Eq,
450 rhs: Expr::lit(Value::Boolean(true)),
451 },
452 },
453 Expr::Subquery { .. } => Filter::CompareExpr {
454 lhs: expr.clone(),
455 op: CompareOp::Eq,
456 rhs: Expr::lit(Value::Boolean(true)),
457 },
458 Expr::FunctionCall { name, args, .. } => string_predicate_from_function_call(name, args)
470 .unwrap_or_else(|| Filter::CompareExpr {
471 lhs: expr.clone(),
472 op: CompareOp::Eq,
473 rhs: Expr::lit(Value::Boolean(true)),
474 }),
475 _ => Filter::CompareExpr {
476 lhs: expr.clone(),
477 op: CompareOp::Eq,
478 rhs: Expr::lit(Value::Boolean(true)),
479 },
480 }
481}
482
483fn string_predicate_from_function_call(name: &str, args: &[Expr]) -> Option<Filter> {
484 if args.len() != 2 {
485 return None;
486 }
487 let field = match &args[0] {
488 Expr::Column { field, .. } => field.clone(),
489 _ => return None,
490 };
491 let text = match &args[1] {
492 Expr::Literal {
493 value: Value::Text(value),
494 ..
495 } => value.as_ref().to_string(),
496 _ => return None,
497 };
498 if name.eq_ignore_ascii_case("LIKE") {
499 Some(Filter::Like {
500 field,
501 pattern: text,
502 })
503 } else if name.eq_ignore_ascii_case("STARTS_WITH") {
504 Some(Filter::StartsWith {
505 field,
506 prefix: text,
507 })
508 } else if name.eq_ignore_ascii_case("ENDS_WITH") {
509 Some(Filter::EndsWith {
510 field,
511 suffix: text,
512 })
513 } else if name.eq_ignore_ascii_case("CONTAINS") {
514 Some(Filter::Contains {
515 field,
516 substring: text,
517 })
518 } else {
519 None
520 }
521}
522
523pub fn boolean_expr_projection(expr: Expr) -> Projection {
524 Projection::Expression(
525 Box::new(Filter::CompareExpr {
526 lhs: expr,
527 op: CompareOp::Eq,
528 rhs: Expr::Literal {
529 value: Value::Boolean(true),
530 span: Span::synthetic(),
531 },
532 }),
533 None,
534 )
535}
536
537pub fn filter_to_expr(filter: &Filter) -> Expr {
538 match filter {
539 Filter::Compare { field, op, value } => Expr::BinaryOp {
540 op: compare_op_to_binop(*op),
541 lhs: Box::new(Expr::Column {
542 field: field.clone(),
543 span: Span::synthetic(),
544 }),
545 rhs: Box::new(Expr::Literal {
546 value: value.clone(),
547 span: Span::synthetic(),
548 }),
549 span: Span::synthetic(),
550 },
551 Filter::CompareFields { left, op, right } => Expr::BinaryOp {
552 op: compare_op_to_binop(*op),
553 lhs: Box::new(Expr::Column {
554 field: left.clone(),
555 span: Span::synthetic(),
556 }),
557 rhs: Box::new(Expr::Column {
558 field: right.clone(),
559 span: Span::synthetic(),
560 }),
561 span: Span::synthetic(),
562 },
563 Filter::CompareExpr { lhs, op, rhs } => Expr::BinaryOp {
564 op: compare_op_to_binop(*op),
565 lhs: Box::new(lhs.clone()),
566 rhs: Box::new(rhs.clone()),
567 span: Span::synthetic(),
568 },
569 Filter::And(left, right) => Expr::BinaryOp {
570 op: BinOp::And,
571 lhs: Box::new(filter_to_expr(left)),
572 rhs: Box::new(filter_to_expr(right)),
573 span: Span::synthetic(),
574 },
575 Filter::Or(left, right) => Expr::BinaryOp {
576 op: BinOp::Or,
577 lhs: Box::new(filter_to_expr(left)),
578 rhs: Box::new(filter_to_expr(right)),
579 span: Span::synthetic(),
580 },
581 Filter::Not(inner) => Expr::UnaryOp {
582 op: UnaryOp::Not,
583 operand: Box::new(filter_to_expr(inner)),
584 span: Span::synthetic(),
585 },
586 Filter::IsNull(field) => Expr::IsNull {
587 operand: Box::new(Expr::Column {
588 field: field.clone(),
589 span: Span::synthetic(),
590 }),
591 negated: false,
592 span: Span::synthetic(),
593 },
594 Filter::IsNotNull(field) => Expr::IsNull {
595 operand: Box::new(Expr::Column {
596 field: field.clone(),
597 span: Span::synthetic(),
598 }),
599 negated: true,
600 span: Span::synthetic(),
601 },
602 Filter::In { field, values } => Expr::InList {
603 target: Box::new(Expr::Column {
604 field: field.clone(),
605 span: Span::synthetic(),
606 }),
607 values: values
608 .iter()
609 .cloned()
610 .map(|value| Expr::Literal {
611 value,
612 span: Span::synthetic(),
613 })
614 .collect(),
615 negated: false,
616 span: Span::synthetic(),
617 },
618 Filter::Between { field, low, high } => Expr::Between {
619 target: Box::new(Expr::Column {
620 field: field.clone(),
621 span: Span::synthetic(),
622 }),
623 low: Box::new(Expr::Literal {
624 value: low.clone(),
625 span: Span::synthetic(),
626 }),
627 high: Box::new(Expr::Literal {
628 value: high.clone(),
629 span: Span::synthetic(),
630 }),
631 negated: false,
632 span: Span::synthetic(),
633 },
634 Filter::Like { field, pattern } => Expr::FunctionCall {
635 name: "LIKE".to_string(),
636 args: vec![
637 Expr::Column {
638 field: field.clone(),
639 span: Span::synthetic(),
640 },
641 Expr::Literal {
642 value: Value::text(pattern.clone()),
643 span: Span::synthetic(),
644 },
645 ],
646 span: Span::synthetic(),
647 },
648 Filter::StartsWith { field, prefix } => Expr::FunctionCall {
649 name: "STARTS_WITH".to_string(),
650 args: vec![
651 Expr::Column {
652 field: field.clone(),
653 span: Span::synthetic(),
654 },
655 Expr::Literal {
656 value: Value::text(prefix.clone()),
657 span: Span::synthetic(),
658 },
659 ],
660 span: Span::synthetic(),
661 },
662 Filter::EndsWith { field, suffix } => Expr::FunctionCall {
663 name: "ENDS_WITH".to_string(),
664 args: vec![
665 Expr::Column {
666 field: field.clone(),
667 span: Span::synthetic(),
668 },
669 Expr::Literal {
670 value: Value::text(suffix.clone()),
671 span: Span::synthetic(),
672 },
673 ],
674 span: Span::synthetic(),
675 },
676 Filter::Contains { field, substring } => Expr::FunctionCall {
677 name: "CONTAINS".to_string(),
678 args: vec![
679 Expr::Column {
680 field: field.clone(),
681 span: Span::synthetic(),
682 },
683 Expr::Literal {
684 value: Value::text(substring.clone()),
685 span: Span::synthetic(),
686 },
687 ],
688 span: Span::synthetic(),
689 },
690 }
691}
692
693pub fn projection_from_literal(value: &Value) -> Option<Projection> {
694 match value {
695 Value::Boolean(_) => Some(boolean_expr_projection(Expr::Literal {
696 value: value.clone(),
697 span: Span::synthetic(),
698 })),
699 _ => Some(Projection::Column(format!(
700 "LIT:{}",
701 render_projection_literal(value)
702 ))),
703 }
704}
705
706pub fn case_condition_projection(condition: Expr) -> Projection {
707 Projection::Expression(
708 Box::new(Filter::CompareExpr {
709 lhs: condition,
710 op: CompareOp::Eq,
711 rhs: Expr::Literal {
712 value: Value::Boolean(true),
713 span: Span::synthetic(),
714 },
715 }),
716 None,
717 )
718}
719
720pub fn fold_expr_to_value(expr: Expr) -> Result<Value, String> {
721 match expr {
722 Expr::Literal { value, .. } => Ok(value),
723 Expr::FunctionCall { name, args, .. } => {
724 if (name.eq_ignore_ascii_case("PASSWORD") || name.eq_ignore_ascii_case("SECRET"))
725 && args.len() == 1
726 {
727 let plaintext = match fold_expr_to_value(args.into_iter().next().unwrap())? {
728 Value::Text(text) => text,
729 other => {
730 return Err(format!(
731 "{name}() expects a string literal argument, got {other:?}"
732 ))
733 }
734 };
735 return Ok(if name.eq_ignore_ascii_case("PASSWORD") {
736 Value::Password(format!("@@plain@@{plaintext}"))
737 } else {
738 Value::Secret(format!("@@plain@@{plaintext}").into_bytes())
739 });
740 }
741 if name.eq_ignore_ascii_case("JSON_PARSE") && args.len() == 1 {
747 let raw = match fold_expr_to_value(args.into_iter().next().unwrap())? {
748 Value::Text(text) => text,
749 other => {
750 return Err(format!(
751 "JSON_PARSE() expects a string literal argument, got {other:?}"
752 ))
753 }
754 };
755 let parsed = reddb_types::utils::json::parse_json(raw.as_ref())
756 .map_err(|err| format!("JSON_PARSE failed to parse JSON: {err}"))?;
757 let canonical = reddb_types::serde_json::Value::from(parsed);
758 let bytes = reddb_types::json::to_vec(&canonical)
759 .map_err(|err| format!("JSON_PARSE failed to encode JSON: {err}"))?;
760 return Ok(Value::Json(bytes));
761 }
762 Err(format!(
763 "expression is not a foldable literal: FunctionCall({name})"
764 ))
765 }
766 Expr::UnaryOp { op, operand, .. } => {
767 let inner = fold_expr_to_value(*operand)?;
768 match (op, inner) {
769 (UnaryOp::Neg, Value::Integer(n)) => Ok(Value::Integer(-n)),
770 (UnaryOp::Neg, Value::UnsignedInteger(n)) => Ok(Value::Integer(-(n as i64))),
771 (UnaryOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
772 (UnaryOp::Not, Value::Boolean(b)) => Ok(Value::Boolean(!b)),
773 (other_op, other) => Err(format!(
774 "unary `{other_op:?}` cannot fold to literal Value (operand: {other:?})"
775 )),
776 }
777 }
778 Expr::Cast { inner, .. } => fold_expr_to_value(*inner),
779 other => Err(format!("expression is not a foldable literal: {other:?}")),
780 }
781}
782
783fn projection_binop_name(op: BinOp) -> &'static str {
784 match op {
785 BinOp::Add => "ADD",
786 BinOp::Sub => "SUB",
787 BinOp::Mul => "MUL",
788 BinOp::Div => "DIV",
789 BinOp::Mod => "MOD",
790 BinOp::Concat => "CONCAT",
791 BinOp::Eq
792 | BinOp::Ne
793 | BinOp::Lt
794 | BinOp::Le
795 | BinOp::Gt
796 | BinOp::Ge
797 | BinOp::And
798 | BinOp::Or => {
799 unreachable!("boolean operators are lowered through Projection::Expression")
800 }
801 }
802}
803
804#[allow(dead_code)]
808fn render_expr_label(expr: &Expr) -> String {
809 render_expr_label_prec(expr, 0)
810}
811
812#[allow(dead_code)]
813fn render_expr_label_prec(expr: &Expr, parent_prec: u8) -> String {
814 match expr {
815 Expr::Literal { value, .. } => render_sql_literal_label(value),
816 Expr::Column { field, .. } => render_field_label(field),
817 Expr::Parameter { index, .. } => format!("${index}"),
818 Expr::BinaryOp { op, lhs, rhs, .. } => {
819 let prec = op.precedence();
820 let rendered = format!(
821 "{} {} {}",
822 render_expr_label_prec(lhs, prec),
823 render_binop_label(*op),
824 render_expr_label_prec(rhs, prec + 1)
825 );
826 if prec < parent_prec {
827 format!("({rendered})")
828 } else {
829 rendered
830 }
831 }
832 Expr::UnaryOp { op, operand, .. } => match op {
833 UnaryOp::Neg => format!("-{}", render_expr_label_prec(operand, u8::MAX)),
834 UnaryOp::Not => format!("NOT {}", render_expr_label_prec(operand, u8::MAX)),
835 },
836 Expr::Cast { inner, target, .. } => {
837 format!("CAST({} AS {target})", render_expr_label(inner))
838 }
839 Expr::FunctionCall { name, args, .. } => {
840 let args = args
841 .iter()
842 .map(render_expr_label)
843 .collect::<Vec<_>>()
844 .join(", ");
845 format!("{name}({args})")
846 }
847 Expr::Case {
848 branches, else_, ..
849 } => {
850 let mut out = String::from("CASE");
851 for (condition, value) in branches {
852 out.push_str(" WHEN ");
853 out.push_str(&render_expr_label(condition));
854 out.push_str(" THEN ");
855 out.push_str(&render_expr_label(value));
856 }
857 if let Some(else_expr) = else_ {
858 out.push_str(" ELSE ");
859 out.push_str(&render_expr_label(else_expr));
860 }
861 out.push_str(" END");
862 out
863 }
864 Expr::IsNull {
865 operand, negated, ..
866 } => {
867 let op = if *negated { "IS NOT NULL" } else { "IS NULL" };
868 format!("{} {op}", render_expr_label_prec(operand, u8::MAX))
869 }
870 Expr::InList {
871 target,
872 values,
873 negated,
874 ..
875 } => {
876 let op = if *negated { "NOT IN" } else { "IN" };
877 let values = values
878 .iter()
879 .map(render_expr_label)
880 .collect::<Vec<_>>()
881 .join(", ");
882 format!("{} {op} ({values})", render_expr_label(target))
883 }
884 Expr::Between {
885 target,
886 low,
887 high,
888 negated,
889 ..
890 } => {
891 let op = if *negated { "NOT BETWEEN" } else { "BETWEEN" };
892 format!(
893 "{} {op} {} AND {}",
894 render_expr_label(target),
895 render_expr_label(low),
896 render_expr_label(high)
897 )
898 }
899 Expr::Subquery { .. } => "subquery".to_string(),
900 Expr::WindowFunctionCall { name, args, .. } => {
901 let args = args
902 .iter()
903 .map(render_expr_label)
904 .collect::<Vec<_>>()
905 .join(", ");
906 format!("{name}({args}) OVER (...)")
907 }
908 }
909}
910
911#[allow(dead_code)]
912fn render_binop_label(op: BinOp) -> &'static str {
913 match op {
914 BinOp::Add => "+",
915 BinOp::Sub => "-",
916 BinOp::Mul => "*",
917 BinOp::Div => "/",
918 BinOp::Mod => "%",
919 BinOp::Concat => "||",
920 BinOp::Eq => "=",
921 BinOp::Ne => "!=",
922 BinOp::Lt => "<",
923 BinOp::Le => "<=",
924 BinOp::Gt => ">",
925 BinOp::Ge => ">=",
926 BinOp::And => "AND",
927 BinOp::Or => "OR",
928 }
929}
930
931#[allow(dead_code)]
932fn render_field_label(field: &FieldRef) -> String {
933 match field {
934 FieldRef::TableColumn { table, column } => {
935 if table.is_empty() {
936 column.clone()
937 } else {
938 format!("{table}.{column}")
939 }
940 }
941 FieldRef::NodeProperty { alias, property } => format!("{alias}.{property}"),
942 FieldRef::EdgeProperty { alias, property } => format!("{alias}.{property}"),
943 FieldRef::NodeId { alias } => format!("{alias}.id"),
944 }
945}
946
947#[allow(dead_code)]
948fn render_sql_literal_label(value: &Value) -> String {
949 match value {
950 Value::Null => "NULL".to_string(),
951 Value::Text(value) => format!("'{}'", value.replace('\'', "''")),
952 Value::Boolean(value) => value.to_string(),
953 Value::Integer(value) => value.to_string(),
954 Value::UnsignedInteger(value) => value.to_string(),
955 Value::Float(value) => {
956 if value.fract().abs() < f64::EPSILON {
957 (*value as i64).to_string()
958 } else {
959 value.to_string()
960 }
961 }
962 other => other.to_string(),
963 }
964}
965
966fn binop_to_compare_op(op: BinOp) -> CompareOp {
967 match op {
968 BinOp::Eq => CompareOp::Eq,
969 BinOp::Ne => CompareOp::Ne,
970 BinOp::Lt => CompareOp::Lt,
971 BinOp::Le => CompareOp::Le,
972 BinOp::Gt => CompareOp::Gt,
973 BinOp::Ge => CompareOp::Ge,
974 other => unreachable!("non-compare binop cannot lower to CompareOp: {other:?}"),
975 }
976}
977
978fn compare_op_to_binop(op: CompareOp) -> BinOp {
979 match op {
980 CompareOp::Eq => BinOp::Eq,
981 CompareOp::Ne => BinOp::Ne,
982 CompareOp::Lt => BinOp::Lt,
983 CompareOp::Le => BinOp::Le,
984 CompareOp::Gt => BinOp::Gt,
985 CompareOp::Ge => BinOp::Ge,
986 }
987}
988
989fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
990 let Some(alias) = alias else { return proj };
991 match proj {
992 Projection::Field(f, _) => Projection::Field(f, Some(alias)),
993 Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
994 Projection::Function(name, args) => {
995 if name.contains(':') {
996 Projection::Function(name, args)
997 } else {
998 Projection::Function(format!("{name}:{alias}"), args)
999 }
1000 }
1001 Projection::Column(c) => Projection::Alias(c, alias),
1002 Projection::Window {
1003 name, args, window, ..
1004 } => Projection::Window {
1005 name,
1006 args,
1007 window,
1008 alias: Some(alias),
1009 },
1010 other => other,
1011 }
1012}
1013
1014fn split_projection_function_alias(name: &str) -> (String, Option<String>) {
1015 match name.split_once(':') {
1016 Some((function, alias)) if !function.is_empty() && !alias.is_empty() => {
1017 (function.to_string(), Some(alias.to_string()))
1018 }
1019 _ => (name.to_string(), None),
1020 }
1021}
1022
1023fn render_projection_literal(value: &Value) -> String {
1024 match value {
1025 Value::Null => String::new(),
1026 Value::Integer(v) => v.to_string(),
1027 Value::UnsignedInteger(v) => v.to_string(),
1028 Value::Float(v) => {
1029 if v.fract().abs() < f64::EPSILON {
1030 (*v as i64).to_string()
1031 } else {
1032 v.to_string()
1033 }
1034 }
1035 Value::Text(v) => v.to_string(),
1036 Value::Boolean(true) => "true".to_string(),
1037 Value::Boolean(false) => "false".to_string(),
1038 Value::Array(_) | Value::Vector(_) | Value::Json(_) | Value::Blob(_) => {
1043 format!("@RL:{}", serialize_value_json(value))
1044 }
1045 other => other.to_string(),
1046 }
1047}
1048
1049fn serialize_value_json(value: &Value) -> String {
1050 match value {
1052 Value::Array(items) => {
1053 let mut out = String::from("[");
1054 for (i, item) in items.iter().enumerate() {
1055 if i > 0 {
1056 out.push(',');
1057 }
1058 out.push_str(&serialize_value_json(item));
1059 }
1060 out.push(']');
1061 out
1062 }
1063 Value::Vector(items) => {
1064 let mut out = String::from("V[");
1065 for (i, f) in items.iter().enumerate() {
1066 if i > 0 {
1067 out.push(',');
1068 }
1069 out.push_str(&f.to_string());
1070 }
1071 out.push(']');
1072 out
1073 }
1074 Value::Integer(n) | Value::BigInt(n) => n.to_string(),
1075 Value::UnsignedInteger(n) => n.to_string(),
1076 Value::Float(f) => f.to_string(),
1077 Value::Text(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
1078 Value::Boolean(b) => b.to_string(),
1079 Value::Null => "null".to_string(),
1080 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
1081 }
1082}
1083
1084fn try_specialized_compare_filter(lhs: &Expr, op: BinOp, rhs: &Expr) -> Option<Filter> {
1085 let op = binop_to_compare_op(op);
1086 match (lhs, rhs) {
1087 (Expr::Column { field, .. }, Expr::Literal { value, .. }) => Some(Filter::Compare {
1088 field: field.clone(),
1089 op,
1090 value: value.clone(),
1091 }),
1092 (Expr::Literal { value, .. }, Expr::Column { field, .. }) => Some(Filter::Compare {
1093 field: field.clone(),
1094 op: flipped_compare_op(op),
1095 value: value.clone(),
1096 }),
1097 (Expr::Column { field: left, .. }, Expr::Column { field: right, .. }) => {
1098 Some(Filter::CompareFields {
1099 left: left.clone(),
1100 op,
1101 right: right.clone(),
1102 })
1103 }
1104 _ => None,
1105 }
1106}
1107
1108fn flipped_compare_op(op: CompareOp) -> CompareOp {
1109 match op {
1110 CompareOp::Eq => CompareOp::Eq,
1111 CompareOp::Ne => CompareOp::Ne,
1112 CompareOp::Lt => CompareOp::Gt,
1113 CompareOp::Le => CompareOp::Ge,
1114 CompareOp::Gt => CompareOp::Lt,
1115 CompareOp::Ge => CompareOp::Le,
1116 }
1117}
1118
1119fn literal_expr_value(expr: &Expr) -> Option<Value> {
1120 match expr {
1121 Expr::Literal { value, .. } => Some(value.clone()),
1122 _ => None,
1123 }
1124}
1125
1126fn all_literal_values(values: &[Expr]) -> Option<Vec<Value>> {
1127 values.iter().map(literal_expr_value).collect()
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133 use crate::ast::{
1134 GraphPattern, GraphQuery, JoinCondition, JoinQuery, NodeSelector, OrderByClause, PathQuery,
1135 QueryExpr, VectorQuery, VectorSource, WindowOrderItem, WindowSpec,
1136 };
1137
1138 fn field(name: &str) -> FieldRef {
1139 FieldRef::column("", name)
1140 }
1141
1142 fn col(name: &str) -> Expr {
1143 Expr::Column {
1144 field: field(name),
1145 span: Span::synthetic(),
1146 }
1147 }
1148
1149 fn lit(value: Value) -> Expr {
1150 Expr::Literal {
1151 value,
1152 span: Span::synthetic(),
1153 }
1154 }
1155
1156 fn parameter(index: usize) -> Expr {
1157 Expr::Parameter {
1158 index,
1159 span: Span::synthetic(),
1160 }
1161 }
1162
1163 fn bin(op: BinOp, lhs: Expr, rhs: Expr) -> Expr {
1164 Expr::BinaryOp {
1165 op,
1166 lhs: Box::new(lhs),
1167 rhs: Box::new(rhs),
1168 span: Span::synthetic(),
1169 }
1170 }
1171
1172 #[test]
1173 fn expr_contains_parameter_walks_nested_expression_shapes() {
1174 assert!(expr_contains_parameter(&bin(
1175 BinOp::Add,
1176 col("age"),
1177 parameter(1)
1178 )));
1179
1180 let case = Expr::Case {
1181 branches: vec![(col("active"), lit(Value::Integer(1)))],
1182 else_: Some(Box::new(parameter(2))),
1183 span: Span::synthetic(),
1184 };
1185 assert!(expr_contains_parameter(&case));
1186
1187 let window = Expr::WindowFunctionCall {
1188 name: "row_number".to_string(),
1189 args: Vec::new(),
1190 window: WindowSpec {
1191 partition_by: vec![col("tenant_id")],
1192 order_by: vec![WindowOrderItem {
1193 expr: parameter(3),
1194 ascending: true,
1195 nulls_first: false,
1196 }],
1197 frame: None,
1198 },
1199 span: Span::synthetic(),
1200 };
1201 assert!(expr_contains_parameter(&window));
1202
1203 let no_parameter = Expr::FunctionCall {
1204 name: "lower".to_string(),
1205 args: vec![col("name")],
1206 span: Span::synthetic(),
1207 };
1208 assert!(!expr_contains_parameter(&no_parameter));
1209 }
1210
1211 #[test]
1212 fn expressions_lower_to_legacy_projections_with_aliases_preserved() {
1213 assert!(matches!(
1214 expr_to_projection(&col("*")),
1215 Some(Projection::All)
1216 ));
1217
1218 let param_projection = expr_to_projection(¶meter(7)).unwrap();
1219 assert!(matches!(
1220 param_projection,
1221 Projection::Column(value) if value == format!("{PARAMETER_PROJECTION_PREFIX}7")
1222 ));
1223
1224 let arithmetic = bin(BinOp::Add, col("age"), lit(Value::Integer(1)));
1225 let projection = select_item_to_projection(&SelectItem::Expr {
1226 expr: arithmetic,
1227 alias: Some("age_plus_one".to_string()),
1228 })
1229 .unwrap();
1230 assert!(matches!(
1231 projection,
1232 Projection::Function(ref name, ref args)
1233 if name == "ADD:age_plus_one" && args.len() == 2
1234 ));
1235
1236 let negated = Expr::UnaryOp {
1237 op: UnaryOp::Neg,
1238 operand: Box::new(col("age")),
1239 span: Span::synthetic(),
1240 };
1241 assert!(matches!(
1242 expr_to_projection(&negated),
1243 Some(Projection::Function(name, args)) if name == "SUB" && args.len() == 2
1244 ));
1245
1246 let cast = Expr::Cast {
1247 inner: Box::new(col("age")),
1248 target: reddb_types::types::DataType::Text,
1249 span: Span::synthetic(),
1250 };
1251 assert!(matches!(
1252 expr_to_projection(&cast),
1253 Some(Projection::Function(name, args)) if name == "CAST" && args.len() == 2
1254 ));
1255
1256 let window = Expr::WindowFunctionCall {
1257 name: "sum".to_string(),
1258 args: vec![col("amount")],
1259 window: WindowSpec::default(),
1260 span: Span::synthetic(),
1261 };
1262 assert!(matches!(
1263 select_item_to_projection(&SelectItem::Expr {
1264 expr: window,
1265 alias: Some("running_sum".to_string()),
1266 }),
1267 Some(Projection::Window { name, alias, .. })
1268 if name == "SUM" && alias.as_deref() == Some("running_sum")
1269 ));
1270 }
1271
1272 #[test]
1273 fn projections_raise_back_to_select_items_and_expression_nodes() {
1274 assert!(matches!(
1275 projection_to_select_item(&Projection::All),
1276 Some(SelectItem::Wildcard)
1277 ));
1278
1279 let literal = projection_to_expr(&Projection::Column("LIT:42".to_string())).unwrap();
1280 assert!(matches!(
1281 literal,
1282 (
1283 Expr::Literal {
1284 value: Value::Integer(42),
1285 ..
1286 },
1287 None
1288 )
1289 ));
1290
1291 let float_literal = projection_to_expr(&Projection::Column("LIT:3.5".to_string())).unwrap();
1292 assert!(matches!(
1293 float_literal,
1294 (Expr::Literal { value: Value::Float(v), .. }, None) if (v - 3.5).abs() < f64::EPSILON
1295 ));
1296
1297 let null_literal = projection_to_expr(&Projection::Column("LIT:".to_string())).unwrap();
1298 assert!(matches!(
1299 null_literal,
1300 (
1301 Expr::Literal {
1302 value: Value::Null,
1303 ..
1304 },
1305 None
1306 )
1307 ));
1308
1309 let function = Projection::Function(
1310 "LOWER:lower_name".to_string(),
1311 vec![Projection::Field(field("name"), None)],
1312 );
1313 let (expr, alias) = projection_to_expr(&function).unwrap();
1314 assert_eq!(alias.as_deref(), Some("lower_name"));
1315 assert!(
1316 matches!(expr, Expr::FunctionCall { name, args, .. } if name == "LOWER" && args.len() == 1)
1317 );
1318
1319 let window = Projection::Window {
1320 name: "ROW_NUMBER".to_string(),
1321 args: Vec::new(),
1322 window: Box::new(WindowSpec::default()),
1323 alias: Some("rn".to_string()),
1324 };
1325 let (expr, alias) = projection_to_expr(&window).unwrap();
1326 assert_eq!(alias.as_deref(), Some("rn"));
1327 assert!(matches!(expr, Expr::WindowFunctionCall { name, .. } if name == "ROW_NUMBER"));
1328 }
1329
1330 #[test]
1331 fn filters_round_trip_through_expression_forms() {
1332 let filters = vec![
1333 Filter::Compare {
1334 field: field("age"),
1335 op: CompareOp::Ge,
1336 value: Value::Integer(18),
1337 },
1338 Filter::CompareFields {
1339 left: field("updated_at"),
1340 op: CompareOp::Gt,
1341 right: field("created_at"),
1342 },
1343 Filter::And(
1344 Box::new(Filter::IsNotNull(field("email"))),
1345 Box::new(Filter::Like {
1346 field: field("email"),
1347 pattern: "%@example.com".to_string(),
1348 }),
1349 ),
1350 Filter::Or(
1351 Box::new(Filter::StartsWith {
1352 field: field("path"),
1353 prefix: "infra/".to_string(),
1354 }),
1355 Box::new(Filter::EndsWith {
1356 field: field("path"),
1357 suffix: ".log".to_string(),
1358 }),
1359 ),
1360 Filter::Not(Box::new(Filter::Contains {
1361 field: field("body"),
1362 substring: "secret".to_string(),
1363 })),
1364 Filter::IsNull(field("deleted_at")),
1365 Filter::In {
1366 field: field("status"),
1367 values: vec![Value::text("open"), Value::text("pending")],
1368 },
1369 Filter::Between {
1370 field: field("score"),
1371 low: Value::Integer(10),
1372 high: Value::Integer(20),
1373 },
1374 ];
1375
1376 for filter in filters {
1377 let expr = filter_to_expr(&filter);
1378 assert_eq!(expr_to_filter(&expr), filter);
1379 }
1380 }
1381
1382 #[test]
1383 fn expression_filters_specialize_common_predicates_and_fallbacks() {
1384 let flipped = expr_to_filter(&bin(BinOp::Lt, lit(Value::Integer(10)), col("age")));
1385 assert_eq!(
1386 flipped,
1387 Filter::Compare {
1388 field: field("age"),
1389 op: CompareOp::Gt,
1390 value: Value::Integer(10),
1391 }
1392 );
1393
1394 let field_to_field = expr_to_filter(&bin(BinOp::Eq, col("lhs"), col("rhs")));
1395 assert_eq!(
1396 field_to_field,
1397 Filter::CompareFields {
1398 left: field("lhs"),
1399 op: CompareOp::Eq,
1400 right: field("rhs"),
1401 }
1402 );
1403
1404 let arithmetic = expr_to_filter(&bin(BinOp::Add, col("age"), lit(Value::Integer(1))));
1405 assert!(matches!(
1406 arithmetic,
1407 Filter::CompareExpr {
1408 op: CompareOp::Eq,
1409 rhs: Expr::Literal {
1410 value: Value::Boolean(true),
1411 ..
1412 },
1413 ..
1414 }
1415 ));
1416
1417 let negated_in = Expr::InList {
1418 target: Box::new(col("status")),
1419 values: vec![lit(Value::text("closed"))],
1420 negated: true,
1421 span: Span::synthetic(),
1422 };
1423 assert!(matches!(
1424 expr_to_filter(&negated_in),
1425 Filter::CompareExpr {
1426 op: CompareOp::Eq,
1427 ..
1428 }
1429 ));
1430 }
1431
1432 #[test]
1433 fn table_effective_helpers_prefer_canonical_expr_fields() {
1434 let mut query = TableQuery::new("users");
1435 query.select_items = vec![
1436 SelectItem::Expr {
1437 expr: col("name"),
1438 alias: Some("display_name".to_string()),
1439 },
1440 SelectItem::Expr {
1441 expr: bin(BinOp::Add, col("age"), lit(Value::Integer(1))),
1442 alias: Some("next_age".to_string()),
1443 },
1444 ];
1445 query.where_expr = Some(bin(BinOp::Eq, col("active"), lit(Value::Boolean(true))));
1446 query.group_by_exprs = vec![col("name")];
1447 query.group_by = vec!["legacy_group".to_string()];
1448 query.having_expr = Some(bin(BinOp::Gt, col("age"), lit(Value::Integer(18))));
1449
1450 let projections = effective_table_projections(&query);
1451 assert_eq!(projections.len(), 2);
1452 assert!(matches!(
1453 &projections[0],
1454 Projection::Field(FieldRef::TableColumn { column, .. }, Some(alias))
1455 if column == "name" && alias == "display_name"
1456 ));
1457
1458 assert!(matches!(
1459 effective_table_filter(&query),
1460 Some(Filter::Compare {
1461 field: FieldRef::TableColumn { column, .. },
1462 op: CompareOp::Eq,
1463 value: Value::Boolean(true)
1464 }) if column == "active"
1465 ));
1466 assert_eq!(effective_table_group_by_exprs(&query), vec![col("name")]);
1467 assert!(matches!(
1468 effective_table_having_filter(&query),
1469 Some(Filter::Compare {
1470 field: FieldRef::TableColumn { column, .. },
1471 op: CompareOp::Gt,
1472 value: Value::Integer(18)
1473 }) if column == "age"
1474 ));
1475
1476 let mut legacy = TableQuery::new("users");
1477 legacy.columns = vec![Projection::column("id")];
1478 legacy.group_by = vec!["tenant_id".to_string()];
1479 assert!(matches!(
1480 effective_table_projections(&legacy).as_slice(),
1481 [Projection::Column(column)] if column == "id"
1482 ));
1483 assert_eq!(
1484 effective_table_group_by_exprs(&legacy),
1485 vec![Expr::Column {
1486 field: field("tenant_id"),
1487 span: Span::synthetic(),
1488 }]
1489 );
1490
1491 let default_projection = TableQuery::new("users");
1492 assert!(matches!(
1493 effective_table_projections(&default_projection).as_slice(),
1494 [Projection::All]
1495 ));
1496 }
1497
1498 #[test]
1499 fn non_table_effective_helpers_preserve_existing_query_fields() {
1500 let mut join = JoinQuery::new(
1501 QueryExpr::Table(TableQuery::new("users")),
1502 QueryExpr::Graph(GraphQuery::new(GraphPattern::new())),
1503 JoinCondition::new(field("id"), FieldRef::node_id("n")),
1504 );
1505 join.filter = Some(Filter::IsNotNull(field("id")));
1506 join.return_items = vec![SelectItem::Expr {
1507 expr: col("name"),
1508 alias: Some("display_name".to_string()),
1509 }];
1510 join.return_ = vec![Projection::Column("legacy_name".to_string())];
1511
1512 assert_eq!(
1513 effective_join_filter(&join),
1514 Some(Filter::IsNotNull(field("id")))
1515 );
1516 assert!(matches!(
1517 effective_join_projections(&join).as_slice(),
1518 [Projection::Field(FieldRef::TableColumn { column, .. }, Some(alias))]
1519 if column == "name" && alias == "display_name"
1520 ));
1521
1522 join.return_items.clear();
1523 assert_eq!(
1524 effective_join_projections(&join),
1525 vec![Projection::Column("legacy_name".to_string())]
1526 );
1527
1528 let graph_filter = Filter::StartsWith {
1529 field: FieldRef::node_prop("n", "path"),
1530 prefix: "infra/".to_string(),
1531 };
1532 let graph_return = vec![Projection::Field(FieldRef::node_prop("n", "name"), None)];
1533 let mut graph = GraphQuery::new(GraphPattern::new());
1534 graph.filter = Some(graph_filter.clone());
1535 graph.return_ = graph_return.clone();
1536 assert_eq!(effective_graph_filter(&graph), Some(graph_filter));
1537 assert_eq!(effective_graph_projections(&graph), graph_return);
1538
1539 let path_filter = Filter::Contains {
1540 field: FieldRef::edge_prop("e", "label"),
1541 substring: "depends".to_string(),
1542 };
1543 let path_return = vec![Projection::Column("path".to_string())];
1544 let mut path = PathQuery::new(NodeSelector::by_id("start"), NodeSelector::by_id("end"));
1545 path.filter = Some(path_filter.clone());
1546 path.return_ = path_return.clone();
1547 assert_eq!(effective_path_filter(&path), Some(path_filter));
1548 assert_eq!(effective_path_projections(&path), path_return);
1549
1550 let mut vector = VectorQuery::new("embeddings", VectorSource::literal(vec![0.1, 0.2]));
1551 assert!(effective_vector_filter(&vector).is_none());
1552 vector.filter = Some(MetadataFilter::eq("source", "nmap"));
1553 assert!(matches!(
1554 effective_vector_filter(&vector),
1555 Some(MetadataFilter::Eq(key, reddb_types::vector_metadata::MetadataValue::String(value)))
1556 if key == "source" && value == "nmap"
1557 ));
1558 }
1559
1560 #[test]
1561 fn insert_update_delete_helpers_fold_canonical_expressions() {
1562 let insert = InsertQuery {
1563 table: "users".to_string(),
1564 entity_type: crate::ast::InsertEntityType::Row,
1565 columns: vec!["name".to_string(), "password".to_string()],
1566 value_exprs: vec![vec![
1567 lit(Value::text("ada")),
1568 Expr::FunctionCall {
1569 name: "PASSWORD".to_string(),
1570 args: vec![lit(Value::text("pw"))],
1571 span: Span::synthetic(),
1572 },
1573 ]],
1574 values: Vec::new(),
1575 returning: None,
1576 ttl_ms: None,
1577 expires_at_ms: None,
1578 with_metadata: Vec::new(),
1579 auto_embed: None,
1580 suppress_events: false,
1581 };
1582 let rows = effective_insert_rows(&insert).unwrap();
1583 assert!(matches!(
1584 rows.as_slice(),
1585 [row] if row[0] == Value::text("ada")
1586 && matches!(&row[1], Value::Password(value) if value == "@@plain@@pw")
1587 ));
1588
1589 let update = UpdateQuery {
1590 table: "users".to_string(),
1591 target: crate::ast::UpdateTarget::Rows,
1592 assignment_exprs: Vec::new(),
1593 compound_assignment_ops: Vec::new(),
1594 assignments: Vec::new(),
1595 where_expr: Some(bin(BinOp::Eq, col("id"), lit(Value::Integer(1)))),
1596 filter: None,
1597 ttl_ms: None,
1598 expires_at_ms: None,
1599 with_metadata: Vec::new(),
1600 returning: None,
1601 order_by: vec![OrderByClause::asc(field("id"))],
1602 limit: Some(1),
1603 claim_limit: None,
1604 claim_exact: false,
1605 suppress_events: false,
1606 };
1607 assert!(matches!(
1608 effective_update_filter(&update),
1609 Some(Filter::Compare {
1610 field: FieldRef::TableColumn { column, .. },
1611 value: Value::Integer(1),
1612 ..
1613 }) if column == "id"
1614 ));
1615
1616 let delete = DeleteQuery {
1617 table: "users".to_string(),
1618 where_expr: Some(Expr::IsNull {
1619 operand: Box::new(col("deleted_at")),
1620 negated: false,
1621 span: Span::synthetic(),
1622 }),
1623 filter: None,
1624 returning: None,
1625 suppress_events: false,
1626 };
1627 assert!(matches!(
1628 effective_delete_filter(&delete),
1629 Some(Filter::IsNull(FieldRef::TableColumn { column, .. })) if column == "deleted_at"
1630 ));
1631 }
1632
1633 #[test]
1634 fn fold_expr_to_value_handles_secret_constructors_unary_and_errors() {
1635 assert_eq!(
1636 fold_expr_to_value(Expr::UnaryOp {
1637 op: UnaryOp::Neg,
1638 operand: Box::new(lit(Value::UnsignedInteger(7))),
1639 span: Span::synthetic(),
1640 })
1641 .unwrap(),
1642 Value::Integer(-7)
1643 );
1644 assert_eq!(
1645 fold_expr_to_value(Expr::UnaryOp {
1646 op: UnaryOp::Not,
1647 operand: Box::new(lit(Value::Boolean(false))),
1648 span: Span::synthetic(),
1649 })
1650 .unwrap(),
1651 Value::Boolean(true)
1652 );
1653
1654 let secret = fold_expr_to_value(Expr::FunctionCall {
1655 name: "SECRET".to_string(),
1656 args: vec![lit(Value::text("token"))],
1657 span: Span::synthetic(),
1658 })
1659 .unwrap();
1660 assert!(matches!(secret, Value::Secret(bytes) if bytes == b"@@plain@@token"));
1661
1662 let casted = fold_expr_to_value(Expr::Cast {
1663 inner: Box::new(lit(Value::Integer(5))),
1664 target: reddb_types::types::DataType::Text,
1665 span: Span::synthetic(),
1666 })
1667 .unwrap();
1668 assert_eq!(casted, Value::Integer(5));
1669
1670 assert!(fold_expr_to_value(bin(BinOp::Add, col("age"), lit(Value::Integer(1)))).is_err());
1671 assert!(fold_expr_to_value(Expr::FunctionCall {
1672 name: "PASSWORD".to_string(),
1673 args: vec![lit(Value::Integer(1))],
1674 span: Span::synthetic(),
1675 })
1676 .is_err());
1677 }
1678
1679 #[test]
1680 fn render_label_and_literal_helpers_cover_private_round_trip_paths() {
1681 assert_eq!(
1682 render_expr_label(&lit(Value::text("O'Reilly"))),
1683 "'O''Reilly'"
1684 );
1685 assert_eq!(render_expr_label(¶meter(4)), "$4");
1686 assert_eq!(
1687 render_expr_label(&bin(
1688 BinOp::Mul,
1689 bin(BinOp::Add, col("a"), col("b")),
1690 col("c")
1691 )),
1692 "(a + b) * c"
1693 );
1694 assert_eq!(
1695 render_expr_label(&Expr::UnaryOp {
1696 op: UnaryOp::Not,
1697 operand: Box::new(col("active")),
1698 span: Span::synthetic(),
1699 }),
1700 "NOT active"
1701 );
1702 assert_eq!(
1703 render_expr_label(&Expr::Cast {
1704 inner: Box::new(col("age")),
1705 target: reddb_types::types::DataType::Text,
1706 span: Span::synthetic(),
1707 }),
1708 "CAST(age AS TEXT)"
1709 );
1710 assert_eq!(
1711 render_expr_label(&Expr::FunctionCall {
1712 name: "lower".to_string(),
1713 args: vec![col("name")],
1714 span: Span::synthetic(),
1715 }),
1716 "lower(name)"
1717 );
1718 assert_eq!(
1719 render_expr_label(&Expr::Case {
1720 branches: vec![(col("active"), lit(Value::text("yes")))],
1721 else_: Some(Box::new(lit(Value::text("no")))),
1722 span: Span::synthetic(),
1723 }),
1724 "CASE WHEN active THEN 'yes' ELSE 'no' END"
1725 );
1726 assert_eq!(
1727 render_expr_label(&Expr::IsNull {
1728 operand: Box::new(col("deleted_at")),
1729 negated: true,
1730 span: Span::synthetic(),
1731 }),
1732 "deleted_at IS NOT NULL"
1733 );
1734 assert_eq!(
1735 render_expr_label(&Expr::InList {
1736 target: Box::new(col("status")),
1737 values: vec![lit(Value::text("closed"))],
1738 negated: true,
1739 span: Span::synthetic(),
1740 }),
1741 "status NOT IN ('closed')"
1742 );
1743 assert_eq!(
1744 render_expr_label(&Expr::Between {
1745 target: Box::new(col("age")),
1746 low: Box::new(lit(Value::Integer(18))),
1747 high: Box::new(lit(Value::Integer(65))),
1748 negated: false,
1749 span: Span::synthetic(),
1750 }),
1751 "age BETWEEN 18 AND 65"
1752 );
1753 assert_eq!(
1754 render_expr_label(&Expr::Subquery {
1755 query: crate::ast::ExprSubquery {
1756 query: Box::new(QueryExpr::Table(TableQuery::new("users"))),
1757 },
1758 span: Span::synthetic(),
1759 }),
1760 "subquery"
1761 );
1762 assert_eq!(
1763 render_expr_label(&Expr::WindowFunctionCall {
1764 name: "sum".to_string(),
1765 args: vec![col("amount")],
1766 window: WindowSpec::default(),
1767 span: Span::synthetic(),
1768 }),
1769 "sum(amount) OVER (...)"
1770 );
1771
1772 assert_eq!(render_field_label(&FieldRef::node_id("n")), "n.id");
1773 assert_eq!(
1774 render_field_label(&FieldRef::edge_prop("e", "weight")),
1775 "e.weight"
1776 );
1777 assert_eq!(render_binop_label(BinOp::Concat), "||");
1778 assert_eq!(render_sql_literal_label(&Value::Float(3.0)), "3");
1779 assert_eq!(
1780 render_projection_literal(&Value::Array(vec![Value::Integer(1), Value::text("x"),])),
1781 "@RL:[1,\"x\"]"
1782 );
1783 assert_eq!(
1784 render_projection_literal(&Value::Vector(vec![1.0, 2.5])),
1785 "@RL:V[1,2.5]"
1786 );
1787 assert_eq!(
1788 render_projection_literal(&Value::Json(br#"{"a":1}"#.to_vec())),
1789 "@RL:\"<json 7 bytes>\""
1790 );
1791 assert!(matches!(
1792 projection_from_literal(&Value::Boolean(true)),
1793 Some(Projection::Expression(_, None))
1794 ));
1795 assert_eq!(
1796 split_projection_function_alias("LOWER:name").1.as_deref(),
1797 Some("name")
1798 );
1799 assert_eq!(split_projection_function_alias(":bad").1, None);
1800 }
1801
1802 #[test]
1803 fn lowering_fallbacks_cover_alias_legacy_and_non_specialized_paths() {
1804 for (op, name) in [
1805 (BinOp::Sub, "SUB"),
1806 (BinOp::Div, "DIV"),
1807 (BinOp::Mod, "MOD"),
1808 (BinOp::Concat, "CONCAT"),
1809 ] {
1810 assert!(matches!(
1811 expr_to_projection(&bin(op, col("lhs"), col("rhs"))),
1812 Some(Projection::Function(function, args))
1813 if function == name && args.len() == 2
1814 ));
1815 }
1816
1817 assert!(matches!(
1818 select_item_to_projection(&SelectItem::Expr {
1819 expr: lit(Value::Integer(1)),
1820 alias: Some("one".to_string()),
1821 }),
1822 Some(Projection::Alias(column, alias)) if column == "LIT:1" && alias == "one"
1823 ));
1824 assert!(matches!(
1825 select_item_to_projection(&SelectItem::Expr {
1826 expr: Expr::UnaryOp {
1827 op: UnaryOp::Not,
1828 operand: Box::new(col("active")),
1829 span: Span::synthetic(),
1830 },
1831 alias: Some("inactive".to_string()),
1832 }),
1833 Some(Projection::Expression(_, Some(alias))) if alias == "inactive"
1834 ));
1835
1836 let legacy_insert = InsertQuery {
1837 table: "users".to_string(),
1838 entity_type: crate::ast::InsertEntityType::Row,
1839 columns: vec!["id".to_string()],
1840 value_exprs: Vec::new(),
1841 values: vec![vec![Value::Integer(1)]],
1842 returning: None,
1843 ttl_ms: None,
1844 expires_at_ms: None,
1845 with_metadata: Vec::new(),
1846 auto_embed: None,
1847 suppress_events: false,
1848 };
1849 assert_eq!(
1850 effective_insert_rows(&legacy_insert).unwrap(),
1851 vec![vec![Value::Integer(1)]]
1852 );
1853
1854 assert!(matches!(
1855 expr_to_filter(&Expr::IsNull {
1856 operand: Box::new(lit(Value::Null)),
1857 negated: false,
1858 span: Span::synthetic(),
1859 }),
1860 Filter::CompareExpr { .. }
1861 ));
1862 assert!(matches!(
1863 expr_to_filter(&Expr::InList {
1864 target: Box::new(col("status")),
1865 values: vec![col("other_status")],
1866 negated: false,
1867 span: Span::synthetic(),
1868 }),
1869 Filter::CompareExpr { .. }
1870 ));
1871 assert!(matches!(
1872 expr_to_filter(&Expr::Between {
1873 target: Box::new(col("age")),
1874 low: Box::new(lit(Value::Integer(18))),
1875 high: Box::new(lit(Value::Integer(65))),
1876 negated: true,
1877 span: Span::synthetic(),
1878 }),
1879 Filter::CompareExpr { .. }
1880 ));
1881 assert!(matches!(
1882 expr_to_filter(&Expr::FunctionCall {
1883 name: "UNKNOWN".to_string(),
1884 args: vec![col("name"), lit(Value::text("a"))],
1885 span: Span::synthetic(),
1886 }),
1887 Filter::CompareExpr { .. }
1888 ));
1889 assert!(matches!(
1890 expr_to_filter(&Expr::FunctionCall {
1891 name: "LIKE".to_string(),
1892 args: vec![lit(Value::text("not_field")), lit(Value::text("a"))],
1893 span: Span::synthetic(),
1894 }),
1895 Filter::CompareExpr { .. }
1896 ));
1897
1898 assert_eq!(
1899 fold_expr_to_value(Expr::UnaryOp {
1900 op: UnaryOp::Neg,
1901 operand: Box::new(lit(Value::Float(1.5))),
1902 span: Span::synthetic(),
1903 })
1904 .unwrap(),
1905 Value::Float(-1.5)
1906 );
1907 assert!(fold_expr_to_value(Expr::UnaryOp {
1908 op: UnaryOp::Not,
1909 operand: Box::new(lit(Value::Integer(1))),
1910 span: Span::synthetic(),
1911 })
1912 .is_err());
1913 assert!(fold_expr_to_value(Expr::FunctionCall {
1914 name: "LOWER".to_string(),
1915 args: vec![lit(Value::text("Ada"))],
1916 span: Span::synthetic(),
1917 })
1918 .is_err());
1919
1920 assert_eq!(render_projection_literal(&Value::Null), "");
1921 assert_eq!(render_projection_literal(&Value::UnsignedInteger(7)), "7");
1922 assert_eq!(render_projection_literal(&Value::Boolean(false)), "false");
1923 assert_eq!(
1924 render_projection_literal(&Value::Blob(vec![1, 2, 3])),
1925 "@RL:\"<blob 3 bytes>\""
1926 );
1927 assert_eq!(serialize_value_json(&Value::Null), "null");
1928 }
1929}