Skip to main content

reddb_server/storage/query/
sql_lowering.rs

1use crate::storage::engine::vector_metadata::MetadataFilter;
2use crate::storage::query::ast::{
3    BinOp, CompareOp, DeleteQuery, Expr, FieldRef, Filter, GraphQuery, InsertQuery, JoinQuery,
4    PathQuery, Projection, SelectItem, Span, TableQuery, UnaryOp, UpdateQuery, VectorQuery,
5};
6use crate::storage::schema::Value;
7
8pub const PARAMETER_PROJECTION_PREFIX: &str = "__user_param_projection__:";
9
10pub fn expr_to_projection(expr: &Expr) -> Option<Projection> {
11    match expr {
12        Expr::Literal { value, .. } => projection_from_literal(value),
13        Expr::Column { field, .. } => {
14            if matches!(
15                field,
16                FieldRef::TableColumn { table, column } if table.is_empty() && column == "*"
17            ) {
18                Some(Projection::All)
19            } else {
20                Some(Projection::Field(field.clone(), None))
21            }
22        }
23        Expr::Parameter { index, .. } => Some(Projection::Column(format!(
24            "{PARAMETER_PROJECTION_PREFIX}{index}"
25        ))),
26        Expr::BinaryOp { op, lhs, rhs, .. } => match op {
27            BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
28                Some(Projection::Function(
29                    projection_binop_name(*op).to_string(),
30                    vec![expr_to_projection(lhs)?, expr_to_projection(rhs)?],
31                ))
32            }
33            _ => Some(boolean_expr_projection(expr.clone())),
34        },
35        Expr::UnaryOp { op, operand, .. } => match op {
36            UnaryOp::Neg => Some(Projection::Function(
37                "SUB".to_string(),
38                vec![
39                    Projection::Column("LIT:0".to_string()),
40                    expr_to_projection(operand)?,
41                ],
42            )),
43            UnaryOp::Not => Some(boolean_expr_projection(expr.clone())),
44        },
45        Expr::Cast { inner, target, .. } => Some(Projection::Function(
46            "CAST".to_string(),
47            vec![
48                expr_to_projection(inner)?,
49                Projection::Column(format!("TYPE:{target}")),
50            ],
51        )),
52        Expr::FunctionCall { name, args, .. } => Some(Projection::Function(
53            name.to_uppercase(),
54            args.iter()
55                .map(expr_to_projection)
56                .collect::<Option<Vec<_>>>()?,
57        )),
58        Expr::Case {
59            branches, else_, ..
60        } => {
61            let mut args = Vec::with_capacity(branches.len() * 2 + usize::from(else_.is_some()));
62            for (cond, value) in branches {
63                args.push(case_condition_projection(cond.clone()));
64                args.push(expr_to_projection(value)?);
65            }
66            if let Some(else_expr) = else_ {
67                args.push(expr_to_projection(else_expr)?);
68            }
69            Some(Projection::Function("CASE".to_string(), args))
70        }
71        Expr::IsNull { .. }
72        | Expr::InList { .. }
73        | Expr::Between { .. }
74        | Expr::Subquery { .. } => Some(boolean_expr_projection(expr.clone())),
75        Expr::WindowFunctionCall {
76            name, args, window, ..
77        } => {
78            let lowered_args = args
79                .iter()
80                .map(expr_to_projection)
81                .collect::<Option<Vec<_>>>()?;
82            Some(crate::storage::query::ast::Projection::Window {
83                name: name.to_uppercase(),
84                args: lowered_args,
85                window: Box::new(window.clone()),
86                alias: None,
87            })
88        }
89    }
90}
91
92pub fn select_item_to_projection(item: &SelectItem) -> Option<Projection> {
93    match item {
94        SelectItem::Wildcard => Some(Projection::All),
95        SelectItem::Expr { expr, alias } => {
96            let projection = expr_to_projection(expr)?;
97            let output_name = alias.clone().or_else(|| Some(render_expr_label(expr)));
98            Some(attach_projection_alias(projection, output_name))
99        }
100    }
101}
102
103pub fn effective_table_projections(query: &TableQuery) -> Vec<Projection> {
104    if !query.select_items.is_empty() {
105        return query
106            .select_items
107            .iter()
108            .filter_map(select_item_to_projection)
109            .collect();
110    }
111    if query.columns.is_empty() {
112        vec![Projection::All]
113    } else {
114        query.columns.clone()
115    }
116}
117
118pub fn effective_table_filter(query: &TableQuery) -> Option<Filter> {
119    query
120        .filter
121        .clone()
122        .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
123        .map(|f| f.optimize()) // OR-of-Eq → In; AND/OR flatten; constant fold
124}
125
126pub fn effective_table_group_by_exprs(query: &TableQuery) -> Vec<Expr> {
127    if !query.group_by_exprs.is_empty() {
128        query.group_by_exprs.clone()
129    } else {
130        query
131            .group_by
132            .iter()
133            .map(|column| Expr::Column {
134                field: FieldRef::TableColumn {
135                    table: String::new(),
136                    column: column.clone(),
137                },
138                span: Span::synthetic(),
139            })
140            .collect()
141    }
142}
143
144pub fn effective_table_having_filter(query: &TableQuery) -> Option<Filter> {
145    query
146        .having
147        .clone()
148        .or_else(|| query.having_expr.as_ref().map(expr_to_filter))
149}
150
151pub fn effective_update_filter(query: &UpdateQuery) -> Option<Filter> {
152    query
153        .filter
154        .clone()
155        .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
156}
157
158pub fn effective_insert_rows(query: &InsertQuery) -> Result<Vec<Vec<Value>>, String> {
159    if !query.value_exprs.is_empty() {
160        return query
161            .value_exprs
162            .iter()
163            .cloned()
164            .map(|row| row.into_iter().map(fold_expr_to_value).collect())
165            .collect();
166    }
167    Ok(query.values.clone())
168}
169
170pub fn effective_delete_filter(query: &DeleteQuery) -> Option<Filter> {
171    query
172        .filter
173        .clone()
174        .or_else(|| query.where_expr.as_ref().map(expr_to_filter))
175}
176
177pub fn effective_join_filter(query: &JoinQuery) -> Option<Filter> {
178    query.filter.clone()
179}
180
181pub fn effective_graph_filter(query: &GraphQuery) -> Option<Filter> {
182    query.filter.clone()
183}
184
185pub fn effective_graph_projections(query: &GraphQuery) -> Vec<Projection> {
186    query.return_.clone()
187}
188
189pub fn effective_path_filter(query: &PathQuery) -> Option<Filter> {
190    query.filter.clone()
191}
192
193pub fn effective_path_projections(query: &PathQuery) -> Vec<Projection> {
194    query.return_.clone()
195}
196
197pub fn effective_vector_filter(query: &VectorQuery) -> Option<MetadataFilter> {
198    query.filter.clone()
199}
200
201pub fn projection_to_expr(projection: &Projection) -> Option<(Expr, Option<String>)> {
202    match projection {
203        Projection::All => Some((
204            Expr::Column {
205                field: FieldRef::TableColumn {
206                    table: String::new(),
207                    column: "*".to_string(),
208                },
209                span: Span::synthetic(),
210            },
211            None,
212        )),
213        Projection::Column(column) => Some((projection_column_to_expr(column), None)),
214        Projection::Alias(column, alias) => {
215            Some((projection_column_to_expr(column), Some(alias.clone())))
216        }
217        Projection::Function(name, args) => {
218            let (name, alias) = split_projection_function_alias(name);
219            let args = args
220                .iter()
221                .map(projection_to_expr)
222                .collect::<Option<Vec<_>>>()?
223                .into_iter()
224                .map(|(expr, _)| expr)
225                .collect();
226            Some((
227                Expr::FunctionCall {
228                    name,
229                    args,
230                    span: Span::synthetic(),
231                },
232                alias,
233            ))
234        }
235        Projection::Expression(filter, alias) => Some((filter_to_expr(filter), alias.clone())),
236        Projection::Field(field, alias) => Some((
237            Expr::Column {
238                field: field.clone(),
239                span: Span::synthetic(),
240            },
241            alias.clone(),
242        )),
243        Projection::Window {
244            name,
245            args,
246            window,
247            alias,
248        } => {
249            let args = args
250                .iter()
251                .map(projection_to_expr)
252                .collect::<Option<Vec<_>>>()?
253                .into_iter()
254                .map(|(expr, _)| expr)
255                .collect();
256            Some((
257                Expr::WindowFunctionCall {
258                    name: name.clone(),
259                    args,
260                    window: (**window).clone(),
261                    span: Span::synthetic(),
262                },
263                alias.clone(),
264            ))
265        }
266    }
267}
268
269fn projection_column_to_expr(column: &str) -> Expr {
270    if let Some(value) = projection_literal_value(column) {
271        return Expr::Literal {
272            value,
273            span: Span::synthetic(),
274        };
275    }
276
277    Expr::Column {
278        field: FieldRef::TableColumn {
279            table: String::new(),
280            column: column.to_string(),
281        },
282        span: Span::synthetic(),
283    }
284}
285
286fn projection_literal_value(column: &str) -> Option<Value> {
287    let literal = column.strip_prefix("LIT:")?;
288    if literal.is_empty() {
289        return Some(Value::Null);
290    }
291    if let Ok(value) = literal.parse::<i64>() {
292        return Some(Value::Integer(value));
293    }
294    if let Ok(value) = literal.parse::<f64>() {
295        return Some(Value::Float(value));
296    }
297    Some(Value::text(literal.to_string()))
298}
299
300pub fn projection_to_select_item(projection: &Projection) -> Option<SelectItem> {
301    match projection {
302        Projection::All => Some(SelectItem::Wildcard),
303        other => {
304            let (expr, alias) = projection_to_expr(other)?;
305            Some(SelectItem::Expr { expr, alias })
306        }
307    }
308}
309
310pub fn effective_join_projections(query: &JoinQuery) -> Vec<Projection> {
311    if !query.return_items.is_empty() {
312        return query
313            .return_items
314            .iter()
315            .filter_map(select_item_to_projection)
316            .collect();
317    }
318    query.return_.clone()
319}
320
321pub fn expr_to_filter(expr: &Expr) -> Filter {
322    match expr {
323        Expr::BinaryOp { op, lhs, rhs, .. } => match op {
324            BinOp::And => Filter::And(Box::new(expr_to_filter(lhs)), Box::new(expr_to_filter(rhs))),
325            BinOp::Or => Filter::Or(Box::new(expr_to_filter(lhs)), Box::new(expr_to_filter(rhs))),
326            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
327                try_specialized_compare_filter(lhs, *op, rhs).unwrap_or_else(|| {
328                    Filter::CompareExpr {
329                        lhs: lhs.as_ref().clone(),
330                        op: binop_to_compare_op(*op),
331                        rhs: rhs.as_ref().clone(),
332                    }
333                })
334            }
335            BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Concat => {
336                Filter::CompareExpr {
337                    lhs: expr.clone(),
338                    op: CompareOp::Eq,
339                    rhs: Expr::lit(Value::Boolean(true)),
340                }
341            }
342        },
343        Expr::UnaryOp {
344            op: UnaryOp::Not,
345            operand,
346            ..
347        } => Filter::Not(Box::new(expr_to_filter(operand))),
348        Expr::IsNull {
349            operand, negated, ..
350        } => match operand.as_ref() {
351            Expr::Column { field, .. } => {
352                if *negated {
353                    Filter::IsNotNull(field.clone())
354                } else {
355                    Filter::IsNull(field.clone())
356                }
357            }
358            _ => Filter::CompareExpr {
359                lhs: expr.clone(),
360                op: CompareOp::Eq,
361                rhs: Expr::lit(Value::Boolean(true)),
362            },
363        },
364        Expr::InList {
365            target,
366            values,
367            negated,
368            ..
369        } => match (target.as_ref(), all_literal_values(values)) {
370            (Expr::Column { field, .. }, Some(values)) if !negated => Filter::In {
371                field: field.clone(),
372                values,
373            },
374            _ => Filter::CompareExpr {
375                lhs: expr.clone(),
376                op: CompareOp::Eq,
377                rhs: Expr::lit(Value::Boolean(true)),
378            },
379        },
380        Expr::Between {
381            target,
382            low,
383            high,
384            negated,
385            ..
386        } => match (
387            target.as_ref(),
388            literal_expr_value(low),
389            literal_expr_value(high),
390        ) {
391            (Expr::Column { field, .. }, Some(low), Some(high)) if !negated => Filter::Between {
392                field: field.clone(),
393                low,
394                high,
395            },
396            _ => Filter::CompareExpr {
397                lhs: expr.clone(),
398                op: CompareOp::Eq,
399                rhs: Expr::lit(Value::Boolean(true)),
400            },
401        },
402        Expr::Subquery { .. } => Filter::CompareExpr {
403            lhs: expr.clone(),
404            op: CompareOp::Eq,
405            rhs: Expr::lit(Value::Boolean(true)),
406        },
407        // Reverse-lower the string-predicate FunctionCall forms emitted by
408        // `filter_to_expr` (`LIKE`, `STARTS_WITH`, `ENDS_WITH`, `CONTAINS`)
409        // back to the typed `Filter` variants. The runtime filter
410        // evaluators (`runtime::join_filter`, virtual `red.*` reads) only
411        // understand the typed variants; without this round-trip step a
412        // `WHERE path STARTS WITH 'infra'` clause survives the parser as
413        // `Filter::StartsWith` but is reduced to a `where_expr`-only
414        // `FunctionCall` after subquery resolution clears `table.filter`,
415        // and `effective_table_filter` would then fall through to a
416        // generic `CompareExpr(FunctionCall, =, true)` that no virtual
417        // table can evaluate. Refs #785.
418        Expr::FunctionCall { name, args, .. } => string_predicate_from_function_call(name, args)
419            .unwrap_or_else(|| Filter::CompareExpr {
420                lhs: expr.clone(),
421                op: CompareOp::Eq,
422                rhs: Expr::lit(Value::Boolean(true)),
423            }),
424        _ => Filter::CompareExpr {
425            lhs: expr.clone(),
426            op: CompareOp::Eq,
427            rhs: Expr::lit(Value::Boolean(true)),
428        },
429    }
430}
431
432fn string_predicate_from_function_call(name: &str, args: &[Expr]) -> Option<Filter> {
433    if args.len() != 2 {
434        return None;
435    }
436    let field = match &args[0] {
437        Expr::Column { field, .. } => field.clone(),
438        _ => return None,
439    };
440    let text = match &args[1] {
441        Expr::Literal {
442            value: Value::Text(value),
443            ..
444        } => value.as_ref().to_string(),
445        _ => return None,
446    };
447    if name.eq_ignore_ascii_case("LIKE") {
448        Some(Filter::Like {
449            field,
450            pattern: text,
451        })
452    } else if name.eq_ignore_ascii_case("STARTS_WITH") {
453        Some(Filter::StartsWith {
454            field,
455            prefix: text,
456        })
457    } else if name.eq_ignore_ascii_case("ENDS_WITH") {
458        Some(Filter::EndsWith {
459            field,
460            suffix: text,
461        })
462    } else if name.eq_ignore_ascii_case("CONTAINS") {
463        Some(Filter::Contains {
464            field,
465            substring: text,
466        })
467    } else {
468        None
469    }
470}
471
472pub fn boolean_expr_projection(expr: Expr) -> Projection {
473    Projection::Expression(
474        Box::new(Filter::CompareExpr {
475            lhs: expr,
476            op: CompareOp::Eq,
477            rhs: Expr::Literal {
478                value: Value::Boolean(true),
479                span: Span::synthetic(),
480            },
481        }),
482        None,
483    )
484}
485
486pub fn filter_to_expr(filter: &Filter) -> Expr {
487    match filter {
488        Filter::Compare { field, op, value } => Expr::BinaryOp {
489            op: compare_op_to_binop(*op),
490            lhs: Box::new(Expr::Column {
491                field: field.clone(),
492                span: Span::synthetic(),
493            }),
494            rhs: Box::new(Expr::Literal {
495                value: value.clone(),
496                span: Span::synthetic(),
497            }),
498            span: Span::synthetic(),
499        },
500        Filter::CompareFields { left, op, right } => Expr::BinaryOp {
501            op: compare_op_to_binop(*op),
502            lhs: Box::new(Expr::Column {
503                field: left.clone(),
504                span: Span::synthetic(),
505            }),
506            rhs: Box::new(Expr::Column {
507                field: right.clone(),
508                span: Span::synthetic(),
509            }),
510            span: Span::synthetic(),
511        },
512        Filter::CompareExpr { lhs, op, rhs } => Expr::BinaryOp {
513            op: compare_op_to_binop(*op),
514            lhs: Box::new(lhs.clone()),
515            rhs: Box::new(rhs.clone()),
516            span: Span::synthetic(),
517        },
518        Filter::And(left, right) => Expr::BinaryOp {
519            op: BinOp::And,
520            lhs: Box::new(filter_to_expr(left)),
521            rhs: Box::new(filter_to_expr(right)),
522            span: Span::synthetic(),
523        },
524        Filter::Or(left, right) => Expr::BinaryOp {
525            op: BinOp::Or,
526            lhs: Box::new(filter_to_expr(left)),
527            rhs: Box::new(filter_to_expr(right)),
528            span: Span::synthetic(),
529        },
530        Filter::Not(inner) => Expr::UnaryOp {
531            op: UnaryOp::Not,
532            operand: Box::new(filter_to_expr(inner)),
533            span: Span::synthetic(),
534        },
535        Filter::IsNull(field) => Expr::IsNull {
536            operand: Box::new(Expr::Column {
537                field: field.clone(),
538                span: Span::synthetic(),
539            }),
540            negated: false,
541            span: Span::synthetic(),
542        },
543        Filter::IsNotNull(field) => Expr::IsNull {
544            operand: Box::new(Expr::Column {
545                field: field.clone(),
546                span: Span::synthetic(),
547            }),
548            negated: true,
549            span: Span::synthetic(),
550        },
551        Filter::In { field, values } => Expr::InList {
552            target: Box::new(Expr::Column {
553                field: field.clone(),
554                span: Span::synthetic(),
555            }),
556            values: values
557                .iter()
558                .cloned()
559                .map(|value| Expr::Literal {
560                    value,
561                    span: Span::synthetic(),
562                })
563                .collect(),
564            negated: false,
565            span: Span::synthetic(),
566        },
567        Filter::Between { field, low, high } => Expr::Between {
568            target: Box::new(Expr::Column {
569                field: field.clone(),
570                span: Span::synthetic(),
571            }),
572            low: Box::new(Expr::Literal {
573                value: low.clone(),
574                span: Span::synthetic(),
575            }),
576            high: Box::new(Expr::Literal {
577                value: high.clone(),
578                span: Span::synthetic(),
579            }),
580            negated: false,
581            span: Span::synthetic(),
582        },
583        Filter::Like { field, pattern } => Expr::FunctionCall {
584            name: "LIKE".to_string(),
585            args: vec![
586                Expr::Column {
587                    field: field.clone(),
588                    span: Span::synthetic(),
589                },
590                Expr::Literal {
591                    value: Value::text(pattern.clone()),
592                    span: Span::synthetic(),
593                },
594            ],
595            span: Span::synthetic(),
596        },
597        Filter::StartsWith { field, prefix } => Expr::FunctionCall {
598            name: "STARTS_WITH".to_string(),
599            args: vec![
600                Expr::Column {
601                    field: field.clone(),
602                    span: Span::synthetic(),
603                },
604                Expr::Literal {
605                    value: Value::text(prefix.clone()),
606                    span: Span::synthetic(),
607                },
608            ],
609            span: Span::synthetic(),
610        },
611        Filter::EndsWith { field, suffix } => Expr::FunctionCall {
612            name: "ENDS_WITH".to_string(),
613            args: vec![
614                Expr::Column {
615                    field: field.clone(),
616                    span: Span::synthetic(),
617                },
618                Expr::Literal {
619                    value: Value::text(suffix.clone()),
620                    span: Span::synthetic(),
621                },
622            ],
623            span: Span::synthetic(),
624        },
625        Filter::Contains { field, substring } => Expr::FunctionCall {
626            name: "CONTAINS".to_string(),
627            args: vec![
628                Expr::Column {
629                    field: field.clone(),
630                    span: Span::synthetic(),
631                },
632                Expr::Literal {
633                    value: Value::text(substring.clone()),
634                    span: Span::synthetic(),
635                },
636            ],
637            span: Span::synthetic(),
638        },
639    }
640}
641
642pub fn projection_from_literal(value: &Value) -> Option<Projection> {
643    match value {
644        Value::Boolean(_) => Some(boolean_expr_projection(Expr::Literal {
645            value: value.clone(),
646            span: Span::synthetic(),
647        })),
648        _ => Some(Projection::Column(format!(
649            "LIT:{}",
650            render_projection_literal(value)
651        ))),
652    }
653}
654
655pub fn case_condition_projection(condition: Expr) -> Projection {
656    Projection::Expression(
657        Box::new(Filter::CompareExpr {
658            lhs: condition,
659            op: CompareOp::Eq,
660            rhs: Expr::Literal {
661                value: Value::Boolean(true),
662                span: Span::synthetic(),
663            },
664        }),
665        None,
666    )
667}
668
669pub fn fold_expr_to_value(expr: Expr) -> Result<Value, String> {
670    match expr {
671        Expr::Literal { value, .. } => Ok(value),
672        Expr::FunctionCall { name, args, .. } => {
673            if (name.eq_ignore_ascii_case("PASSWORD") || name.eq_ignore_ascii_case("SECRET"))
674                && args.len() == 1
675            {
676                let plaintext = match fold_expr_to_value(args.into_iter().next().unwrap())? {
677                    Value::Text(text) => text,
678                    other => {
679                        return Err(format!(
680                            "{name}() expects a string literal argument, got {other:?}"
681                        ))
682                    }
683                };
684                return Ok(if name.eq_ignore_ascii_case("PASSWORD") {
685                    Value::Password(format!("@@plain@@{plaintext}"))
686                } else {
687                    Value::Secret(format!("@@plain@@{plaintext}").into_bytes())
688                });
689            }
690            Err(format!(
691                "expression is not a foldable literal: FunctionCall({name})"
692            ))
693        }
694        Expr::UnaryOp { op, operand, .. } => {
695            let inner = fold_expr_to_value(*operand)?;
696            match (op, inner) {
697                (UnaryOp::Neg, Value::Integer(n)) => Ok(Value::Integer(-n)),
698                (UnaryOp::Neg, Value::UnsignedInteger(n)) => Ok(Value::Integer(-(n as i64))),
699                (UnaryOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
700                (UnaryOp::Not, Value::Boolean(b)) => Ok(Value::Boolean(!b)),
701                (other_op, other) => Err(format!(
702                    "unary `{other_op:?}` cannot fold to literal Value (operand: {other:?})"
703                )),
704            }
705        }
706        Expr::Cast { inner, .. } => fold_expr_to_value(*inner),
707        other => Err(format!("expression is not a foldable literal: {other:?}")),
708    }
709}
710
711fn projection_binop_name(op: BinOp) -> &'static str {
712    match op {
713        BinOp::Add => "ADD",
714        BinOp::Sub => "SUB",
715        BinOp::Mul => "MUL",
716        BinOp::Div => "DIV",
717        BinOp::Mod => "MOD",
718        BinOp::Concat => "CONCAT",
719        BinOp::Eq
720        | BinOp::Ne
721        | BinOp::Lt
722        | BinOp::Le
723        | BinOp::Gt
724        | BinOp::Ge
725        | BinOp::And
726        | BinOp::Or => {
727            unreachable!("boolean operators are lowered through Projection::Expression")
728        }
729    }
730}
731
732fn render_expr_label(expr: &Expr) -> String {
733    render_expr_label_prec(expr, 0)
734}
735
736fn render_expr_label_prec(expr: &Expr, parent_prec: u8) -> String {
737    match expr {
738        Expr::Literal { value, .. } => render_sql_literal_label(value),
739        Expr::Column { field, .. } => render_field_label(field),
740        Expr::Parameter { index, .. } => format!("${index}"),
741        Expr::BinaryOp { op, lhs, rhs, .. } => {
742            let prec = op.precedence();
743            let rendered = format!(
744                "{} {} {}",
745                render_expr_label_prec(lhs, prec),
746                render_binop_label(*op),
747                render_expr_label_prec(rhs, prec + 1)
748            );
749            if prec < parent_prec {
750                format!("({rendered})")
751            } else {
752                rendered
753            }
754        }
755        Expr::UnaryOp { op, operand, .. } => match op {
756            UnaryOp::Neg => format!("-{}", render_expr_label_prec(operand, u8::MAX)),
757            UnaryOp::Not => format!("NOT {}", render_expr_label_prec(operand, u8::MAX)),
758        },
759        Expr::Cast { inner, target, .. } => {
760            format!("CAST({} AS {target})", render_expr_label(inner))
761        }
762        Expr::FunctionCall { name, args, .. } => {
763            let args = args
764                .iter()
765                .map(render_expr_label)
766                .collect::<Vec<_>>()
767                .join(", ");
768            format!("{name}({args})")
769        }
770        Expr::Case {
771            branches, else_, ..
772        } => {
773            let mut out = String::from("CASE");
774            for (condition, value) in branches {
775                out.push_str(" WHEN ");
776                out.push_str(&render_expr_label(condition));
777                out.push_str(" THEN ");
778                out.push_str(&render_expr_label(value));
779            }
780            if let Some(else_expr) = else_ {
781                out.push_str(" ELSE ");
782                out.push_str(&render_expr_label(else_expr));
783            }
784            out.push_str(" END");
785            out
786        }
787        Expr::IsNull {
788            operand, negated, ..
789        } => {
790            let op = if *negated { "IS NOT NULL" } else { "IS NULL" };
791            format!("{} {op}", render_expr_label_prec(operand, u8::MAX))
792        }
793        Expr::InList {
794            target,
795            values,
796            negated,
797            ..
798        } => {
799            let op = if *negated { "NOT IN" } else { "IN" };
800            let values = values
801                .iter()
802                .map(render_expr_label)
803                .collect::<Vec<_>>()
804                .join(", ");
805            format!("{} {op} ({values})", render_expr_label(target))
806        }
807        Expr::Between {
808            target,
809            low,
810            high,
811            negated,
812            ..
813        } => {
814            let op = if *negated { "NOT BETWEEN" } else { "BETWEEN" };
815            format!(
816                "{} {op} {} AND {}",
817                render_expr_label(target),
818                render_expr_label(low),
819                render_expr_label(high)
820            )
821        }
822        Expr::Subquery { .. } => "subquery".to_string(),
823        Expr::WindowFunctionCall { name, args, .. } => {
824            let args = args
825                .iter()
826                .map(render_expr_label)
827                .collect::<Vec<_>>()
828                .join(", ");
829            format!("{name}({args}) OVER (...)")
830        }
831    }
832}
833
834fn render_binop_label(op: BinOp) -> &'static str {
835    match op {
836        BinOp::Add => "+",
837        BinOp::Sub => "-",
838        BinOp::Mul => "*",
839        BinOp::Div => "/",
840        BinOp::Mod => "%",
841        BinOp::Concat => "||",
842        BinOp::Eq => "=",
843        BinOp::Ne => "!=",
844        BinOp::Lt => "<",
845        BinOp::Le => "<=",
846        BinOp::Gt => ">",
847        BinOp::Ge => ">=",
848        BinOp::And => "AND",
849        BinOp::Or => "OR",
850    }
851}
852
853fn render_field_label(field: &FieldRef) -> String {
854    match field {
855        FieldRef::TableColumn { table, column } => {
856            if table.is_empty() {
857                column.clone()
858            } else {
859                format!("{table}.{column}")
860            }
861        }
862        FieldRef::NodeProperty { alias, property } => format!("{alias}.{property}"),
863        FieldRef::EdgeProperty { alias, property } => format!("{alias}.{property}"),
864        FieldRef::NodeId { alias } => format!("{alias}.id"),
865    }
866}
867
868fn render_sql_literal_label(value: &Value) -> String {
869    match value {
870        Value::Null => "NULL".to_string(),
871        Value::Text(value) => format!("'{}'", value.replace('\'', "''")),
872        Value::Boolean(value) => value.to_string(),
873        Value::Integer(value) => value.to_string(),
874        Value::UnsignedInteger(value) => value.to_string(),
875        Value::Float(value) => {
876            if value.fract().abs() < f64::EPSILON {
877                (*value as i64).to_string()
878            } else {
879                value.to_string()
880            }
881        }
882        other => other.to_string(),
883    }
884}
885
886fn binop_to_compare_op(op: BinOp) -> CompareOp {
887    match op {
888        BinOp::Eq => CompareOp::Eq,
889        BinOp::Ne => CompareOp::Ne,
890        BinOp::Lt => CompareOp::Lt,
891        BinOp::Le => CompareOp::Le,
892        BinOp::Gt => CompareOp::Gt,
893        BinOp::Ge => CompareOp::Ge,
894        other => unreachable!("non-compare binop cannot lower to CompareOp: {other:?}"),
895    }
896}
897
898fn compare_op_to_binop(op: CompareOp) -> BinOp {
899    match op {
900        CompareOp::Eq => BinOp::Eq,
901        CompareOp::Ne => BinOp::Ne,
902        CompareOp::Lt => BinOp::Lt,
903        CompareOp::Le => BinOp::Le,
904        CompareOp::Gt => BinOp::Gt,
905        CompareOp::Ge => BinOp::Ge,
906    }
907}
908
909fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
910    let Some(alias) = alias else { return proj };
911    match proj {
912        Projection::Field(f, _) => Projection::Field(f, Some(alias)),
913        Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
914        Projection::Function(name, args) => {
915            if name.contains(':') {
916                Projection::Function(name, args)
917            } else {
918                Projection::Function(format!("{name}:{alias}"), args)
919            }
920        }
921        Projection::Column(c) => Projection::Alias(c, alias),
922        Projection::Window {
923            name, args, window, ..
924        } => Projection::Window {
925            name,
926            args,
927            window,
928            alias: Some(alias),
929        },
930        other => other,
931    }
932}
933
934fn split_projection_function_alias(name: &str) -> (String, Option<String>) {
935    match name.split_once(':') {
936        Some((function, alias)) if !function.is_empty() && !alias.is_empty() => {
937            (function.to_string(), Some(alias.to_string()))
938        }
939        _ => (name.to_string(), None),
940    }
941}
942
943fn render_projection_literal(value: &Value) -> String {
944    match value {
945        Value::Null => String::new(),
946        Value::Integer(v) => v.to_string(),
947        Value::UnsignedInteger(v) => v.to_string(),
948        Value::Float(v) => {
949            if v.fract().abs() < f64::EPSILON {
950                (*v as i64).to_string()
951            } else {
952                v.to_string()
953            }
954        }
955        Value::Text(v) => v.to_string(),
956        Value::Boolean(true) => "true".to_string(),
957        Value::Boolean(false) => "false".to_string(),
958        // Composite values (arrays, vectors, blobs) would lose fidelity
959        // going through `Display` — `Vec<Value>` turns into
960        // "<vector dim=N>". Use a JSON sentinel so the reader in
961        // `eval_projection_value` can round-trip the exact Value.
962        Value::Array(_) | Value::Vector(_) | Value::Json(_) | Value::Blob(_) => {
963            format!("@RL:{}", serialize_value_json(value))
964        }
965        other => other.to_string(),
966    }
967}
968
969fn serialize_value_json(value: &Value) -> String {
970    // Uses `crate::serde_json` which is already a workspace dep.
971    match value {
972        Value::Array(items) => {
973            let mut out = String::from("[");
974            for (i, item) in items.iter().enumerate() {
975                if i > 0 {
976                    out.push(',');
977                }
978                out.push_str(&serialize_value_json(item));
979            }
980            out.push(']');
981            out
982        }
983        Value::Vector(items) => {
984            let mut out = String::from("V[");
985            for (i, f) in items.iter().enumerate() {
986                if i > 0 {
987                    out.push(',');
988                }
989                out.push_str(&f.to_string());
990            }
991            out.push(']');
992            out
993        }
994        Value::Integer(n) | Value::BigInt(n) => n.to_string(),
995        Value::UnsignedInteger(n) => n.to_string(),
996        Value::Float(f) => f.to_string(),
997        Value::Text(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
998        Value::Boolean(b) => b.to_string(),
999        Value::Null => "null".to_string(),
1000        other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
1001    }
1002}
1003
1004fn try_specialized_compare_filter(lhs: &Expr, op: BinOp, rhs: &Expr) -> Option<Filter> {
1005    let op = binop_to_compare_op(op);
1006    match (lhs, rhs) {
1007        (Expr::Column { field, .. }, Expr::Literal { value, .. }) => Some(Filter::Compare {
1008            field: field.clone(),
1009            op,
1010            value: value.clone(),
1011        }),
1012        (Expr::Literal { value, .. }, Expr::Column { field, .. }) => Some(Filter::Compare {
1013            field: field.clone(),
1014            op: flipped_compare_op(op),
1015            value: value.clone(),
1016        }),
1017        (Expr::Column { field: left, .. }, Expr::Column { field: right, .. }) => {
1018            Some(Filter::CompareFields {
1019                left: left.clone(),
1020                op,
1021                right: right.clone(),
1022            })
1023        }
1024        _ => None,
1025    }
1026}
1027
1028fn flipped_compare_op(op: CompareOp) -> CompareOp {
1029    match op {
1030        CompareOp::Eq => CompareOp::Eq,
1031        CompareOp::Ne => CompareOp::Ne,
1032        CompareOp::Lt => CompareOp::Gt,
1033        CompareOp::Le => CompareOp::Ge,
1034        CompareOp::Gt => CompareOp::Lt,
1035        CompareOp::Ge => CompareOp::Le,
1036    }
1037}
1038
1039fn literal_expr_value(expr: &Expr) -> Option<Value> {
1040    match expr {
1041        Expr::Literal { value, .. } => Some(value.clone()),
1042        _ => None,
1043    }
1044}
1045
1046fn all_literal_values(values: &[Expr]) -> Option<Vec<Value>> {
1047    values.iter().map(literal_expr_value).collect()
1048}