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