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