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