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