Skip to main content

reddb_rql/
sql_lowering.rs

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
10/// Recursively check whether `expr` contains any `Expr::Parameter` node.
11/// Used by the INSERT parser to know when to defer literal folding to
12/// the user_params binder.
13pub 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            // Attach ONLY an explicit alias here. The previous
142            // `.or_else(|| Some(render_expr_label(expr)))` synthesized an implicit
143            // output label from the expression text and baked it into the legacy
144            // Projection — mangling function names (`CAST` → `CAST:CAST(.. AS ..)`),
145            // wrapping bare columns in a redundant `Alias(name, name)`, and thereby
146            // breaking render→parse→render idempotency. The default output-column
147            // label for an un-aliased projection is derived at render time from the
148            // SelectItem (which keeps `alias: None`), not from this lowering.
149            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()) // OR-of-Eq → In; AND/OR flatten; constant fold
175}
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        // Reverse-lower the string-predicate FunctionCall forms emitted by
518        // `filter_to_expr` (`LIKE`, `STARTS_WITH`, `ENDS_WITH`, `CONTAINS`)
519        // back to the typed `Filter` variants. The runtime filter
520        // evaluators (`runtime::join_filter`, virtual `red.*` reads) only
521        // understand the typed variants; without this round-trip step a
522        // `WHERE path STARTS WITH 'infra'` clause survives the parser as
523        // `Filter::StartsWith` but is reduced to a `where_expr`-only
524        // `FunctionCall` after subquery resolution clears `table.filter`,
525        // and `effective_table_filter` would then fall through to a
526        // generic `CompareExpr(FunctionCall, =, true)` that no virtual
527        // table can evaluate. Refs #785.
528        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        // Scalar subqueries return typed encrypted values. Carry the literal
755        // in the existing expression payload instead of rendering it as text.
756        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            // ADR 0067 (#1721): `JSON_PARSE('{…}')` is the sanctioned escape
807            // hatch for writing JSON from a runtime string. Fold a literal
808            // string argument to a `Value::Json` here so it is accepted in
809            // INSERT VALUES positions, using the same parse+canonicalize
810            // pipeline as an inline JSON literal.
811            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// SQL-text label rendering for projection expressions. Retained verbatim from
870// the pre-move module (no live caller today); kept for the upcoming Fase 2
871// projection-aliasing work rather than dropped during the byte-faithful move.
872#[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        // Composite values (arrays, vectors, blobs) would lose fidelity
1104        // going through `Display` — `Vec<Value>` turns into
1105        // "<vector dim=N>". Use a JSON sentinel so the reader in
1106        // `eval_projection_value` can round-trip the exact Value.
1107        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    // Uses `crate::serde_json` which is already a workspace dep.
1116    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(&parameter(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(&parameter(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}