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