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