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()) }
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 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 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}