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