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