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 _ => Filter::CompareExpr {
408 lhs: expr.clone(),
409 op: CompareOp::Eq,
410 rhs: Expr::lit(Value::Boolean(true)),
411 },
412 }
413}
414
415pub fn boolean_expr_projection(expr: Expr) -> Projection {
416 Projection::Expression(
417 Box::new(Filter::CompareExpr {
418 lhs: expr,
419 op: CompareOp::Eq,
420 rhs: Expr::Literal {
421 value: Value::Boolean(true),
422 span: Span::synthetic(),
423 },
424 }),
425 None,
426 )
427}
428
429pub fn filter_to_expr(filter: &Filter) -> Expr {
430 match filter {
431 Filter::Compare { field, op, value } => Expr::BinaryOp {
432 op: compare_op_to_binop(*op),
433 lhs: Box::new(Expr::Column {
434 field: field.clone(),
435 span: Span::synthetic(),
436 }),
437 rhs: Box::new(Expr::Literal {
438 value: value.clone(),
439 span: Span::synthetic(),
440 }),
441 span: Span::synthetic(),
442 },
443 Filter::CompareFields { left, op, right } => Expr::BinaryOp {
444 op: compare_op_to_binop(*op),
445 lhs: Box::new(Expr::Column {
446 field: left.clone(),
447 span: Span::synthetic(),
448 }),
449 rhs: Box::new(Expr::Column {
450 field: right.clone(),
451 span: Span::synthetic(),
452 }),
453 span: Span::synthetic(),
454 },
455 Filter::CompareExpr { lhs, op, rhs } => Expr::BinaryOp {
456 op: compare_op_to_binop(*op),
457 lhs: Box::new(lhs.clone()),
458 rhs: Box::new(rhs.clone()),
459 span: Span::synthetic(),
460 },
461 Filter::And(left, right) => Expr::BinaryOp {
462 op: BinOp::And,
463 lhs: Box::new(filter_to_expr(left)),
464 rhs: Box::new(filter_to_expr(right)),
465 span: Span::synthetic(),
466 },
467 Filter::Or(left, right) => Expr::BinaryOp {
468 op: BinOp::Or,
469 lhs: Box::new(filter_to_expr(left)),
470 rhs: Box::new(filter_to_expr(right)),
471 span: Span::synthetic(),
472 },
473 Filter::Not(inner) => Expr::UnaryOp {
474 op: UnaryOp::Not,
475 operand: Box::new(filter_to_expr(inner)),
476 span: Span::synthetic(),
477 },
478 Filter::IsNull(field) => Expr::IsNull {
479 operand: Box::new(Expr::Column {
480 field: field.clone(),
481 span: Span::synthetic(),
482 }),
483 negated: false,
484 span: Span::synthetic(),
485 },
486 Filter::IsNotNull(field) => Expr::IsNull {
487 operand: Box::new(Expr::Column {
488 field: field.clone(),
489 span: Span::synthetic(),
490 }),
491 negated: true,
492 span: Span::synthetic(),
493 },
494 Filter::In { field, values } => Expr::InList {
495 target: Box::new(Expr::Column {
496 field: field.clone(),
497 span: Span::synthetic(),
498 }),
499 values: values
500 .iter()
501 .cloned()
502 .map(|value| Expr::Literal {
503 value,
504 span: Span::synthetic(),
505 })
506 .collect(),
507 negated: false,
508 span: Span::synthetic(),
509 },
510 Filter::Between { field, low, high } => Expr::Between {
511 target: Box::new(Expr::Column {
512 field: field.clone(),
513 span: Span::synthetic(),
514 }),
515 low: Box::new(Expr::Literal {
516 value: low.clone(),
517 span: Span::synthetic(),
518 }),
519 high: Box::new(Expr::Literal {
520 value: high.clone(),
521 span: Span::synthetic(),
522 }),
523 negated: false,
524 span: Span::synthetic(),
525 },
526 Filter::Like { field, pattern } => Expr::FunctionCall {
527 name: "LIKE".to_string(),
528 args: vec![
529 Expr::Column {
530 field: field.clone(),
531 span: Span::synthetic(),
532 },
533 Expr::Literal {
534 value: Value::text(pattern.clone()),
535 span: Span::synthetic(),
536 },
537 ],
538 span: Span::synthetic(),
539 },
540 Filter::StartsWith { field, prefix } => Expr::FunctionCall {
541 name: "STARTS_WITH".to_string(),
542 args: vec![
543 Expr::Column {
544 field: field.clone(),
545 span: Span::synthetic(),
546 },
547 Expr::Literal {
548 value: Value::text(prefix.clone()),
549 span: Span::synthetic(),
550 },
551 ],
552 span: Span::synthetic(),
553 },
554 Filter::EndsWith { field, suffix } => Expr::FunctionCall {
555 name: "ENDS_WITH".to_string(),
556 args: vec![
557 Expr::Column {
558 field: field.clone(),
559 span: Span::synthetic(),
560 },
561 Expr::Literal {
562 value: Value::text(suffix.clone()),
563 span: Span::synthetic(),
564 },
565 ],
566 span: Span::synthetic(),
567 },
568 Filter::Contains { field, substring } => Expr::FunctionCall {
569 name: "CONTAINS".to_string(),
570 args: vec![
571 Expr::Column {
572 field: field.clone(),
573 span: Span::synthetic(),
574 },
575 Expr::Literal {
576 value: Value::text(substring.clone()),
577 span: Span::synthetic(),
578 },
579 ],
580 span: Span::synthetic(),
581 },
582 }
583}
584
585pub fn projection_from_literal(value: &Value) -> Option<Projection> {
586 match value {
587 Value::Boolean(_) => Some(boolean_expr_projection(Expr::Literal {
588 value: value.clone(),
589 span: Span::synthetic(),
590 })),
591 _ => Some(Projection::Column(format!(
592 "LIT:{}",
593 render_projection_literal(value)
594 ))),
595 }
596}
597
598pub fn case_condition_projection(condition: Expr) -> Projection {
599 Projection::Expression(
600 Box::new(Filter::CompareExpr {
601 lhs: condition,
602 op: CompareOp::Eq,
603 rhs: Expr::Literal {
604 value: Value::Boolean(true),
605 span: Span::synthetic(),
606 },
607 }),
608 None,
609 )
610}
611
612pub fn fold_expr_to_value(expr: Expr) -> Result<Value, String> {
613 match expr {
614 Expr::Literal { value, .. } => Ok(value),
615 Expr::FunctionCall { name, args, .. } => {
616 if (name.eq_ignore_ascii_case("PASSWORD") || name.eq_ignore_ascii_case("SECRET"))
617 && args.len() == 1
618 {
619 let plaintext = match fold_expr_to_value(args.into_iter().next().unwrap())? {
620 Value::Text(text) => text,
621 other => {
622 return Err(format!(
623 "{name}() expects a string literal argument, got {other:?}"
624 ))
625 }
626 };
627 return Ok(if name.eq_ignore_ascii_case("PASSWORD") {
628 Value::Password(format!("@@plain@@{plaintext}"))
629 } else {
630 Value::Secret(format!("@@plain@@{plaintext}").into_bytes())
631 });
632 }
633 Err(format!(
634 "expression is not a foldable literal: FunctionCall({name})"
635 ))
636 }
637 Expr::UnaryOp { op, operand, .. } => {
638 let inner = fold_expr_to_value(*operand)?;
639 match (op, inner) {
640 (UnaryOp::Neg, Value::Integer(n)) => Ok(Value::Integer(-n)),
641 (UnaryOp::Neg, Value::UnsignedInteger(n)) => Ok(Value::Integer(-(n as i64))),
642 (UnaryOp::Neg, Value::Float(f)) => Ok(Value::Float(-f)),
643 (UnaryOp::Not, Value::Boolean(b)) => Ok(Value::Boolean(!b)),
644 (other_op, other) => Err(format!(
645 "unary `{other_op:?}` cannot fold to literal Value (operand: {other:?})"
646 )),
647 }
648 }
649 Expr::Cast { inner, .. } => fold_expr_to_value(*inner),
650 other => Err(format!("expression is not a foldable literal: {other:?}")),
651 }
652}
653
654fn projection_binop_name(op: BinOp) -> &'static str {
655 match op {
656 BinOp::Add => "ADD",
657 BinOp::Sub => "SUB",
658 BinOp::Mul => "MUL",
659 BinOp::Div => "DIV",
660 BinOp::Mod => "MOD",
661 BinOp::Concat => "CONCAT",
662 BinOp::Eq
663 | BinOp::Ne
664 | BinOp::Lt
665 | BinOp::Le
666 | BinOp::Gt
667 | BinOp::Ge
668 | BinOp::And
669 | BinOp::Or => {
670 unreachable!("boolean operators are lowered through Projection::Expression")
671 }
672 }
673}
674
675fn render_expr_label(expr: &Expr) -> String {
676 render_expr_label_prec(expr, 0)
677}
678
679fn render_expr_label_prec(expr: &Expr, parent_prec: u8) -> String {
680 match expr {
681 Expr::Literal { value, .. } => render_sql_literal_label(value),
682 Expr::Column { field, .. } => render_field_label(field),
683 Expr::Parameter { index, .. } => format!("${index}"),
684 Expr::BinaryOp { op, lhs, rhs, .. } => {
685 let prec = op.precedence();
686 let rendered = format!(
687 "{} {} {}",
688 render_expr_label_prec(lhs, prec),
689 render_binop_label(*op),
690 render_expr_label_prec(rhs, prec + 1)
691 );
692 if prec < parent_prec {
693 format!("({rendered})")
694 } else {
695 rendered
696 }
697 }
698 Expr::UnaryOp { op, operand, .. } => match op {
699 UnaryOp::Neg => format!("-{}", render_expr_label_prec(operand, u8::MAX)),
700 UnaryOp::Not => format!("NOT {}", render_expr_label_prec(operand, u8::MAX)),
701 },
702 Expr::Cast { inner, target, .. } => {
703 format!("CAST({} AS {target})", render_expr_label(inner))
704 }
705 Expr::FunctionCall { name, args, .. } => {
706 let args = args
707 .iter()
708 .map(render_expr_label)
709 .collect::<Vec<_>>()
710 .join(", ");
711 format!("{name}({args})")
712 }
713 Expr::Case {
714 branches, else_, ..
715 } => {
716 let mut out = String::from("CASE");
717 for (condition, value) in branches {
718 out.push_str(" WHEN ");
719 out.push_str(&render_expr_label(condition));
720 out.push_str(" THEN ");
721 out.push_str(&render_expr_label(value));
722 }
723 if let Some(else_expr) = else_ {
724 out.push_str(" ELSE ");
725 out.push_str(&render_expr_label(else_expr));
726 }
727 out.push_str(" END");
728 out
729 }
730 Expr::IsNull {
731 operand, negated, ..
732 } => {
733 let op = if *negated { "IS NOT NULL" } else { "IS NULL" };
734 format!("{} {op}", render_expr_label_prec(operand, u8::MAX))
735 }
736 Expr::InList {
737 target,
738 values,
739 negated,
740 ..
741 } => {
742 let op = if *negated { "NOT IN" } else { "IN" };
743 let values = values
744 .iter()
745 .map(render_expr_label)
746 .collect::<Vec<_>>()
747 .join(", ");
748 format!("{} {op} ({values})", render_expr_label(target))
749 }
750 Expr::Between {
751 target,
752 low,
753 high,
754 negated,
755 ..
756 } => {
757 let op = if *negated { "NOT BETWEEN" } else { "BETWEEN" };
758 format!(
759 "{} {op} {} AND {}",
760 render_expr_label(target),
761 render_expr_label(low),
762 render_expr_label(high)
763 )
764 }
765 Expr::Subquery { .. } => "subquery".to_string(),
766 Expr::WindowFunctionCall { name, args, .. } => {
767 let args = args
768 .iter()
769 .map(render_expr_label)
770 .collect::<Vec<_>>()
771 .join(", ");
772 format!("{name}({args}) OVER (...)")
773 }
774 }
775}
776
777fn render_binop_label(op: BinOp) -> &'static str {
778 match op {
779 BinOp::Add => "+",
780 BinOp::Sub => "-",
781 BinOp::Mul => "*",
782 BinOp::Div => "/",
783 BinOp::Mod => "%",
784 BinOp::Concat => "||",
785 BinOp::Eq => "=",
786 BinOp::Ne => "!=",
787 BinOp::Lt => "<",
788 BinOp::Le => "<=",
789 BinOp::Gt => ">",
790 BinOp::Ge => ">=",
791 BinOp::And => "AND",
792 BinOp::Or => "OR",
793 }
794}
795
796fn render_field_label(field: &FieldRef) -> String {
797 match field {
798 FieldRef::TableColumn { table, column } => {
799 if table.is_empty() {
800 column.clone()
801 } else {
802 format!("{table}.{column}")
803 }
804 }
805 FieldRef::NodeProperty { alias, property } => format!("{alias}.{property}"),
806 FieldRef::EdgeProperty { alias, property } => format!("{alias}.{property}"),
807 FieldRef::NodeId { alias } => format!("{alias}.id"),
808 }
809}
810
811fn render_sql_literal_label(value: &Value) -> String {
812 match value {
813 Value::Null => "NULL".to_string(),
814 Value::Text(value) => format!("'{}'", value.replace('\'', "''")),
815 Value::Boolean(value) => value.to_string(),
816 Value::Integer(value) => value.to_string(),
817 Value::UnsignedInteger(value) => value.to_string(),
818 Value::Float(value) => {
819 if value.fract().abs() < f64::EPSILON {
820 (*value as i64).to_string()
821 } else {
822 value.to_string()
823 }
824 }
825 other => other.to_string(),
826 }
827}
828
829fn binop_to_compare_op(op: BinOp) -> CompareOp {
830 match op {
831 BinOp::Eq => CompareOp::Eq,
832 BinOp::Ne => CompareOp::Ne,
833 BinOp::Lt => CompareOp::Lt,
834 BinOp::Le => CompareOp::Le,
835 BinOp::Gt => CompareOp::Gt,
836 BinOp::Ge => CompareOp::Ge,
837 other => unreachable!("non-compare binop cannot lower to CompareOp: {other:?}"),
838 }
839}
840
841fn compare_op_to_binop(op: CompareOp) -> BinOp {
842 match op {
843 CompareOp::Eq => BinOp::Eq,
844 CompareOp::Ne => BinOp::Ne,
845 CompareOp::Lt => BinOp::Lt,
846 CompareOp::Le => BinOp::Le,
847 CompareOp::Gt => BinOp::Gt,
848 CompareOp::Ge => BinOp::Ge,
849 }
850}
851
852fn attach_projection_alias(proj: Projection, alias: Option<String>) -> Projection {
853 let Some(alias) = alias else { return proj };
854 match proj {
855 Projection::Field(f, _) => Projection::Field(f, Some(alias)),
856 Projection::Expression(filter, _) => Projection::Expression(filter, Some(alias)),
857 Projection::Function(name, args) => {
858 if name.contains(':') {
859 Projection::Function(name, args)
860 } else {
861 Projection::Function(format!("{name}:{alias}"), args)
862 }
863 }
864 Projection::Column(c) => Projection::Alias(c, alias),
865 Projection::Window {
866 name, args, window, ..
867 } => Projection::Window {
868 name,
869 args,
870 window,
871 alias: Some(alias),
872 },
873 other => other,
874 }
875}
876
877fn split_projection_function_alias(name: &str) -> (String, Option<String>) {
878 match name.split_once(':') {
879 Some((function, alias)) if !function.is_empty() && !alias.is_empty() => {
880 (function.to_string(), Some(alias.to_string()))
881 }
882 _ => (name.to_string(), None),
883 }
884}
885
886fn render_projection_literal(value: &Value) -> String {
887 match value {
888 Value::Null => String::new(),
889 Value::Integer(v) => v.to_string(),
890 Value::UnsignedInteger(v) => v.to_string(),
891 Value::Float(v) => {
892 if v.fract().abs() < f64::EPSILON {
893 (*v as i64).to_string()
894 } else {
895 v.to_string()
896 }
897 }
898 Value::Text(v) => v.to_string(),
899 Value::Boolean(true) => "true".to_string(),
900 Value::Boolean(false) => "false".to_string(),
901 Value::Array(_) | Value::Vector(_) | Value::Json(_) | Value::Blob(_) => {
906 format!("@RL:{}", serialize_value_json(value))
907 }
908 other => other.to_string(),
909 }
910}
911
912fn serialize_value_json(value: &Value) -> String {
913 match value {
915 Value::Array(items) => {
916 let mut out = String::from("[");
917 for (i, item) in items.iter().enumerate() {
918 if i > 0 {
919 out.push(',');
920 }
921 out.push_str(&serialize_value_json(item));
922 }
923 out.push(']');
924 out
925 }
926 Value::Vector(items) => {
927 let mut out = String::from("V[");
928 for (i, f) in items.iter().enumerate() {
929 if i > 0 {
930 out.push(',');
931 }
932 out.push_str(&f.to_string());
933 }
934 out.push(']');
935 out
936 }
937 Value::Integer(n) | Value::BigInt(n) => n.to_string(),
938 Value::UnsignedInteger(n) => n.to_string(),
939 Value::Float(f) => f.to_string(),
940 Value::Text(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
941 Value::Boolean(b) => b.to_string(),
942 Value::Null => "null".to_string(),
943 other => format!("\"{}\"", other.to_string().replace('"', "\\\"")),
944 }
945}
946
947fn try_specialized_compare_filter(lhs: &Expr, op: BinOp, rhs: &Expr) -> Option<Filter> {
948 let op = binop_to_compare_op(op);
949 match (lhs, rhs) {
950 (Expr::Column { field, .. }, Expr::Literal { value, .. }) => Some(Filter::Compare {
951 field: field.clone(),
952 op,
953 value: value.clone(),
954 }),
955 (Expr::Literal { value, .. }, Expr::Column { field, .. }) => Some(Filter::Compare {
956 field: field.clone(),
957 op: flipped_compare_op(op),
958 value: value.clone(),
959 }),
960 (Expr::Column { field: left, .. }, Expr::Column { field: right, .. }) => {
961 Some(Filter::CompareFields {
962 left: left.clone(),
963 op,
964 right: right.clone(),
965 })
966 }
967 _ => None,
968 }
969}
970
971fn flipped_compare_op(op: CompareOp) -> CompareOp {
972 match op {
973 CompareOp::Eq => CompareOp::Eq,
974 CompareOp::Ne => CompareOp::Ne,
975 CompareOp::Lt => CompareOp::Gt,
976 CompareOp::Le => CompareOp::Ge,
977 CompareOp::Gt => CompareOp::Lt,
978 CompareOp::Ge => CompareOp::Le,
979 }
980}
981
982fn literal_expr_value(expr: &Expr) -> Option<Value> {
983 match expr {
984 Expr::Literal { value, .. } => Some(value.clone()),
985 _ => None,
986 }
987}
988
989fn all_literal_values(values: &[Expr]) -> Option<Vec<Value>> {
990 values.iter().map(literal_expr_value).collect()
991}