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        _ => Filter::CompareExpr {
408            lhs: expr.clone(),
409            op: CompareOp::Eq,
410            rhs: Expr::lit(Value::Boolean(true)),
411        },
412    }
413}
414
415pub fn boolean_expr_projection(expr: Expr) -> Projection {
416    Projection::Expression(
417        Box::new(Filter::CompareExpr {
418            lhs: expr,
419            op: CompareOp::Eq,
420            rhs: Expr::Literal {
421                value: Value::Boolean(true),
422                span: Span::synthetic(),
423            },
424        }),
425        None,
426    )
427}
428
429pub fn filter_to_expr(filter: &Filter) -> Expr {
430    match filter {
431        Filter::Compare { field, op, value } => Expr::BinaryOp {
432            op: compare_op_to_binop(*op),
433            lhs: Box::new(Expr::Column {
434                field: field.clone(),
435                span: Span::synthetic(),
436            }),
437            rhs: Box::new(Expr::Literal {
438                value: value.clone(),
439                span: Span::synthetic(),
440            }),
441            span: Span::synthetic(),
442        },
443        Filter::CompareFields { left, op, right } => Expr::BinaryOp {
444            op: compare_op_to_binop(*op),
445            lhs: Box::new(Expr::Column {
446                field: left.clone(),
447                span: Span::synthetic(),
448            }),
449            rhs: Box::new(Expr::Column {
450                field: right.clone(),
451                span: Span::synthetic(),
452            }),
453            span: Span::synthetic(),
454        },
455        Filter::CompareExpr { lhs, op, rhs } => Expr::BinaryOp {
456            op: compare_op_to_binop(*op),
457            lhs: Box::new(lhs.clone()),
458            rhs: Box::new(rhs.clone()),
459            span: Span::synthetic(),
460        },
461        Filter::And(left, right) => Expr::BinaryOp {
462            op: BinOp::And,
463            lhs: Box::new(filter_to_expr(left)),
464            rhs: Box::new(filter_to_expr(right)),
465            span: Span::synthetic(),
466        },
467        Filter::Or(left, right) => Expr::BinaryOp {
468            op: BinOp::Or,
469            lhs: Box::new(filter_to_expr(left)),
470            rhs: Box::new(filter_to_expr(right)),
471            span: Span::synthetic(),
472        },
473        Filter::Not(inner) => Expr::UnaryOp {
474            op: UnaryOp::Not,
475            operand: Box::new(filter_to_expr(inner)),
476            span: Span::synthetic(),
477        },
478        Filter::IsNull(field) => Expr::IsNull {
479            operand: Box::new(Expr::Column {
480                field: field.clone(),
481                span: Span::synthetic(),
482            }),
483            negated: false,
484            span: Span::synthetic(),
485        },
486        Filter::IsNotNull(field) => Expr::IsNull {
487            operand: Box::new(Expr::Column {
488                field: field.clone(),
489                span: Span::synthetic(),
490            }),
491            negated: true,
492            span: Span::synthetic(),
493        },
494        Filter::In { field, values } => Expr::InList {
495            target: Box::new(Expr::Column {
496                field: field.clone(),
497                span: Span::synthetic(),
498            }),
499            values: values
500                .iter()
501                .cloned()
502                .map(|value| Expr::Literal {
503                    value,
504                    span: Span::synthetic(),
505                })
506                .collect(),
507            negated: false,
508            span: Span::synthetic(),
509        },
510        Filter::Between { field, low, high } => Expr::Between {
511            target: Box::new(Expr::Column {
512                field: field.clone(),
513                span: Span::synthetic(),
514            }),
515            low: Box::new(Expr::Literal {
516                value: low.clone(),
517                span: Span::synthetic(),
518            }),
519            high: Box::new(Expr::Literal {
520                value: high.clone(),
521                span: Span::synthetic(),
522            }),
523            negated: false,
524            span: Span::synthetic(),
525        },
526        Filter::Like { field, pattern } => Expr::FunctionCall {
527            name: "LIKE".to_string(),
528            args: vec![
529                Expr::Column {
530                    field: field.clone(),
531                    span: Span::synthetic(),
532                },
533                Expr::Literal {
534                    value: Value::text(pattern.clone()),
535                    span: Span::synthetic(),
536                },
537            ],
538            span: Span::synthetic(),
539        },
540        Filter::StartsWith { field, prefix } => Expr::FunctionCall {
541            name: "STARTS_WITH".to_string(),
542            args: vec![
543                Expr::Column {
544                    field: field.clone(),
545                    span: Span::synthetic(),
546                },
547                Expr::Literal {
548                    value: Value::text(prefix.clone()),
549                    span: Span::synthetic(),
550                },
551            ],
552            span: Span::synthetic(),
553        },
554        Filter::EndsWith { field, suffix } => Expr::FunctionCall {
555            name: "ENDS_WITH".to_string(),
556            args: vec![
557                Expr::Column {
558                    field: field.clone(),
559                    span: Span::synthetic(),
560                },
561                Expr::Literal {
562                    value: Value::text(suffix.clone()),
563                    span: Span::synthetic(),
564                },
565            ],
566            span: Span::synthetic(),
567        },
568        Filter::Contains { field, substring } => Expr::FunctionCall {
569            name: "CONTAINS".to_string(),
570            args: vec![
571                Expr::Column {
572                    field: field.clone(),
573                    span: Span::synthetic(),
574                },
575                Expr::Literal {
576                    value: Value::text(substring.clone()),
577                    span: Span::synthetic(),
578                },
579            ],
580            span: Span::synthetic(),
581        },
582    }
583}
584
585pub fn projection_from_literal(value: &Value) -> Option<Projection> {
586    match value {
587        Value::Boolean(_) => Some(boolean_expr_projection(Expr::Literal {
588            value: value.clone(),
589            span: Span::synthetic(),
590        })),
591        _ => Some(Projection::Column(format!(
592            "LIT:{}",
593            render_projection_literal(value)
594        ))),
595    }
596}
597
598pub fn case_condition_projection(condition: Expr) -> Projection {
599    Projection::Expression(
600        Box::new(Filter::CompareExpr {
601            lhs: condition,
602            op: CompareOp::Eq,
603            rhs: Expr::Literal {
604                value: Value::Boolean(true),
605                span: Span::synthetic(),
606            },
607        }),
608        None,
609    )
610}
611
612pub fn fold_expr_to_value(expr: Expr) -> Result<Value, String> {
613    match expr {
614        Expr::Literal { value, .. } => Ok(value),
615        Expr::FunctionCall { name, args, .. } => {
616            if (name.eq_ignore_ascii_case("PASSWORD") || name.eq_ignore_ascii_case("SECRET"))
617                && args.len() == 1
618            {
619                let plaintext = match fold_expr_to_value(args.into_iter().next().unwrap())? {
620                    Value::Text(text) => text,
621                    other => {
622                        return Err(format!(
623                            "{name}() expects a string literal argument, got {other:?}"
624                        ))
625                    }
626                };
627                return Ok(if name.eq_ignore_ascii_case("PASSWORD") {
628                    Value::Password(format!("@@plain@@{plaintext}"))
629                } else {
630                    Value::Secret(format!("@@plain@@{plaintext}").into_bytes())
631                });
632            }
633            Err(format!(
634                "expression is not a foldable literal: FunctionCall({name})"
635            ))
636        }
637        Expr::UnaryOp { op, operand, .. } => {
638            let inner = fold_expr_to_value(*operand)?;
639            match (op, inner) {
640                (UnaryOp::Neg, Value::Integer(n)) => Ok(Value::Integer(-n)),
641                (UnaryOp::Neg, Value::UnsignedInteger(n)) => Ok(Value::Integer(-(n as i64))),
642                (UnaryOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
643                (UnaryOp::Not, Value::Boolean(b)) => Ok(Value::Boolean(!b)),
644                (other_op, other) => Err(format!(
645                    "unary `{other_op:?}` cannot fold to literal Value (operand: {other:?})"
646                )),
647            }
648        }
649        Expr::Cast { inner, .. } => fold_expr_to_value(*inner),
650        other => Err(format!("expression is not a foldable literal: {other:?}")),
651    }
652}
653
654fn projection_binop_name(op: BinOp) -> &'static str {
655    match op {
656        BinOp::Add => "ADD",
657        BinOp::Sub => "SUB",
658        BinOp::Mul => "MUL",
659        BinOp::Div => "DIV",
660        BinOp::Mod => "MOD",
661        BinOp::Concat => "CONCAT",
662        BinOp::Eq
663        | BinOp::Ne
664        | BinOp::Lt
665        | BinOp::Le
666        | BinOp::Gt
667        | BinOp::Ge
668        | BinOp::And
669        | BinOp::Or => {
670            unreachable!("boolean operators are lowered through Projection::Expression")
671        }
672    }
673}
674
675fn render_expr_label(expr: &Expr) -> String {
676    render_expr_label_prec(expr, 0)
677}
678
679fn render_expr_label_prec(expr: &Expr, parent_prec: u8) -> String {
680    match expr {
681        Expr::Literal { value, .. } => render_sql_literal_label(value),
682        Expr::Column { field, .. } => render_field_label(field),
683        Expr::Parameter { index, .. } => format!("${index}"),
684        Expr::BinaryOp { op, lhs, rhs, .. } => {
685            let prec = op.precedence();
686            let rendered = format!(
687                "{} {} {}",
688                render_expr_label_prec(lhs, prec),
689                render_binop_label(*op),
690                render_expr_label_prec(rhs, prec + 1)
691            );
692            if prec < parent_prec {
693                format!("({rendered})")
694            } else {
695                rendered
696            }
697        }
698        Expr::UnaryOp { op, operand, .. } => match op {
699            UnaryOp::Neg => format!("-{}", render_expr_label_prec(operand, u8::MAX)),
700            UnaryOp::Not => format!("NOT {}", render_expr_label_prec(operand, u8::MAX)),
701        },
702        Expr::Cast { inner, target, .. } => {
703            format!("CAST({} AS {target})", render_expr_label(inner))
704        }
705        Expr::FunctionCall { name, args, .. } => {
706            let args = args
707                .iter()
708                .map(render_expr_label)
709                .collect::<Vec<_>>()
710                .join(", ");
711            format!("{name}({args})")
712        }
713        Expr::Case {
714            branches, else_, ..
715        } => {
716            let mut out = String::from("CASE");
717            for (condition, value) in branches {
718                out.push_str(" WHEN ");
719                out.push_str(&render_expr_label(condition));
720                out.push_str(" THEN ");
721                out.push_str(&render_expr_label(value));
722            }
723            if let Some(else_expr) = else_ {
724                out.push_str(" ELSE ");
725                out.push_str(&render_expr_label(else_expr));
726            }
727            out.push_str(" END");
728            out
729        }
730        Expr::IsNull {
731            operand, negated, ..
732        } => {
733            let op = if *negated { "IS NOT NULL" } else { "IS NULL" };
734            format!("{} {op}", render_expr_label_prec(operand, u8::MAX))
735        }
736        Expr::InList {
737            target,
738            values,
739            negated,
740            ..
741        } => {
742            let op = if *negated { "NOT IN" } else { "IN" };
743            let values = values
744                .iter()
745                .map(render_expr_label)
746                .collect::<Vec<_>>()
747                .join(", ");
748            format!("{} {op} ({values})", render_expr_label(target))
749        }
750        Expr::Between {
751            target,
752            low,
753            high,
754            negated,
755            ..
756        } => {
757            let op = if *negated { "NOT BETWEEN" } else { "BETWEEN" };
758            format!(
759                "{} {op} {} AND {}",
760                render_expr_label(target),
761                render_expr_label(low),
762                render_expr_label(high)
763            )
764        }
765        Expr::Subquery { .. } => "subquery".to_string(),
766        Expr::WindowFunctionCall { name, args, .. } => {
767            let args = args
768                .iter()
769                .map(render_expr_label)
770                .collect::<Vec<_>>()
771                .join(", ");
772            format!("{name}({args}) OVER (...)")
773        }
774    }
775}
776
777fn render_binop_label(op: BinOp) -> &'static str {
778    match op {
779        BinOp::Add => "+",
780        BinOp::Sub => "-",
781        BinOp::Mul => "*",
782        BinOp::Div => "/",
783        BinOp::Mod => "%",
784        BinOp::Concat => "||",
785        BinOp::Eq => "=",
786        BinOp::Ne => "!=",
787        BinOp::Lt => "<",
788        BinOp::Le => "<=",
789        BinOp::Gt => ">",
790        BinOp::Ge => ">=",
791        BinOp::And => "AND",
792        BinOp::Or => "OR",
793    }
794}
795
796fn render_field_label(field: &FieldRef) -> String {
797    match field {
798        FieldRef::TableColumn { table, column } => {
799            if table.is_empty() {
800                column.clone()
801            } else {
802                format!("{table}.{column}")
803            }
804        }
805        FieldRef::NodeProperty { alias, property } => format!("{alias}.{property}"),
806        FieldRef::EdgeProperty { alias, property } => format!("{alias}.{property}"),
807        FieldRef::NodeId { alias } => format!("{alias}.id"),
808    }
809}
810
811fn render_sql_literal_label(value: &Value) -> String {
812    match value {
813        Value::Null => "NULL".to_string(),
814        Value::Text(value) => format!("'{}'", value.replace('\'', "''")),
815        Value::Boolean(value) => value.to_string(),
816        Value::Integer(value) => value.to_string(),
817        Value::UnsignedInteger(value) => value.to_string(),
818        Value::Float(value) => {
819            if value.fract().abs() < f64::EPSILON {
820                (*value as i64).to_string()
821            } else {
822                value.to_string()
823            }
824        }
825        other => other.to_string(),
826    }
827}
828
829fn binop_to_compare_op(op: BinOp) -> CompareOp {
830    match op {
831        BinOp::Eq => CompareOp::Eq,
832        BinOp::Ne => CompareOp::Ne,
833        BinOp::Lt => CompareOp::Lt,
834        BinOp::Le => CompareOp::Le,
835        BinOp::Gt => CompareOp::Gt,
836        BinOp::Ge => CompareOp::Ge,
837        other => unreachable!("non-compare binop cannot lower to CompareOp: {other:?}"),
838    }
839}
840
841fn compare_op_to_binop(op: CompareOp) -> BinOp {
842    match op {
843        CompareOp::Eq => BinOp::Eq,
844        CompareOp::Ne => BinOp::Ne,
845        CompareOp::Lt => BinOp::Lt,
846        CompareOp::Le => BinOp::Le,
847        CompareOp::Gt => BinOp::Gt,
848        CompareOp::Ge => BinOp::Ge,
849    }
850}
851
852fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
853    let Some(alias) = alias else { return proj };
854    match proj {
855        Projection::Field(f, _) => Projection::Field(f, Some(alias)),
856        Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
857        Projection::Function(name, args) => {
858            if name.contains(':') {
859                Projection::Function(name, args)
860            } else {
861                Projection::Function(format!("{name}:{alias}"), args)
862            }
863        }
864        Projection::Column(c) => Projection::Alias(c, alias),
865        Projection::Window {
866            name, args, window, ..
867        } => Projection::Window {
868            name,
869            args,
870            window,
871            alias: Some(alias),
872        },
873        other => other,
874    }
875}
876
877fn split_projection_function_alias(name: &str) -> (String, Option<String>) {
878    match name.split_once(':') {
879        Some((function, alias)) if !function.is_empty() && !alias.is_empty() => {
880            (function.to_string(), Some(alias.to_string()))
881        }
882        _ => (name.to_string(), None),
883    }
884}
885
886fn render_projection_literal(value: &Value) -> String {
887    match value {
888        Value::Null => String::new(),
889        Value::Integer(v) => v.to_string(),
890        Value::UnsignedInteger(v) => v.to_string(),
891        Value::Float(v) => {
892            if v.fract().abs() < f64::EPSILON {
893                (*v as i64).to_string()
894            } else {
895                v.to_string()
896            }
897        }
898        Value::Text(v) => v.to_string(),
899        Value::Boolean(true) => "true".to_string(),
900        Value::Boolean(false) => "false".to_string(),
901        // Composite values (arrays, vectors, blobs) would lose fidelity
902        // going through `Display` — `Vec<Value>` turns into
903        // "<vector dim=N>". Use a JSON sentinel so the reader in
904        // `eval_projection_value` can round-trip the exact Value.
905        Value::Array(_) | Value::Vector(_) | Value::Json(_) | Value::Blob(_) => {
906            format!("@RL:{}", serialize_value_json(value))
907        }
908        other => other.to_string(),
909    }
910}
911
912fn serialize_value_json(value: &Value) -> String {
913    // Uses `crate::serde_json` which is already a workspace dep.
914    match value {
915        Value::Array(items) => {
916            let mut out = String::from("[");
917            for (i, item) in items.iter().enumerate() {
918                if i > 0 {
919                    out.push(',');
920                }
921                out.push_str(&serialize_value_json(item));
922            }
923            out.push(']');
924            out
925        }
926        Value::Vector(items) => {
927            let mut out = String::from("V[");
928            for (i, f) in items.iter().enumerate() {
929                if i > 0 {
930                    out.push(',');
931                }
932                out.push_str(&f.to_string());
933            }
934            out.push(']');
935            out
936        }
937        Value::Integer(n) | Value::BigInt(n) => n.to_string(),
938        Value::UnsignedInteger(n) => n.to_string(),
939        Value::Float(f) => f.to_string(),
940        Value::Text(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
941        Value::Boolean(b) => b.to_string(),
942        Value::Null => "null".to_string(),
943        other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
944    }
945}
946
947fn try_specialized_compare_filter(lhs: &Expr, op: BinOp, rhs: &Expr) -> Option<Filter> {
948    let op = binop_to_compare_op(op);
949    match (lhs, rhs) {
950        (Expr::Column { field, .. }, Expr::Literal { value, .. }) => Some(Filter::Compare {
951            field: field.clone(),
952            op,
953            value: value.clone(),
954        }),
955        (Expr::Literal { value, .. }, Expr::Column { field, .. }) => Some(Filter::Compare {
956            field: field.clone(),
957            op: flipped_compare_op(op),
958            value: value.clone(),
959        }),
960        (Expr::Column { field: left, .. }, Expr::Column { field: right, .. }) => {
961            Some(Filter::CompareFields {
962                left: left.clone(),
963                op,
964                right: right.clone(),
965            })
966        }
967        _ => None,
968    }
969}
970
971fn flipped_compare_op(op: CompareOp) -> CompareOp {
972    match op {
973        CompareOp::Eq => CompareOp::Eq,
974        CompareOp::Ne => CompareOp::Ne,
975        CompareOp::Lt => CompareOp::Gt,
976        CompareOp::Le => CompareOp::Ge,
977        CompareOp::Gt => CompareOp::Lt,
978        CompareOp::Ge => CompareOp::Le,
979    }
980}
981
982fn literal_expr_value(expr: &Expr) -> Option<Value> {
983    match expr {
984        Expr::Literal { value, .. } => Some(value.clone()),
985        _ => None,
986    }
987}
988
989fn all_literal_values(values: &[Expr]) -> Option<Vec<Value>> {
990    values.iter().map(literal_expr_value).collect()
991}