1use {
2 super::{
3 Aggregate, AstLiteral, BinaryOperator, DataType, DateTimeField, Function, Query, ToSql,
4 ToSqlUnquoted, UnaryOperator,
5 },
6 serde::{Deserialize, Serialize},
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Expr {
11 Identifier(String),
12 CompoundIdentifier {
13 alias: String,
14 ident: String,
15 },
16 IsNull(Box<Expr>),
17 IsNotNull(Box<Expr>),
18 InList {
19 expr: Box<Expr>,
20 list: Vec<Expr>,
21 negated: bool,
22 },
23 InSubquery {
24 expr: Box<Expr>,
25 subquery: Box<Query>,
26 negated: bool,
27 },
28 Between {
29 expr: Box<Expr>,
30 negated: bool,
31 low: Box<Expr>,
32 high: Box<Expr>,
33 },
34 Like {
35 expr: Box<Expr>,
36 negated: bool,
37 pattern: Box<Expr>,
38 },
39 ILike {
40 expr: Box<Expr>,
41 negated: bool,
42 pattern: Box<Expr>,
43 },
44 BinaryOp {
45 left: Box<Expr>,
46 op: BinaryOperator,
47 right: Box<Expr>,
48 },
49 UnaryOp {
50 op: UnaryOperator,
51 expr: Box<Expr>,
52 },
53 Nested(Box<Expr>),
54 Literal(AstLiteral),
55 TypedString {
56 data_type: DataType,
57 value: String,
58 },
59 Function(Box<Function>),
60 Aggregate(Box<Aggregate>),
61 Exists {
62 subquery: Box<Query>,
63 negated: bool,
64 },
65 Subquery(Box<Query>),
66 Case {
67 operand: Option<Box<Expr>>,
68 when_then: Vec<(Expr, Expr)>,
69 else_result: Option<Box<Expr>>,
70 },
71 ArrayIndex {
72 obj: Box<Expr>,
73 indexes: Vec<Expr>,
74 },
75 Interval {
76 expr: Box<Expr>,
77 leading_field: Option<DateTimeField>,
78 last_field: Option<DateTimeField>,
79 },
80 Array {
81 elem: Vec<Expr>,
82 },
83}
84
85impl ToSql for Expr {
86 fn to_sql(&self) -> String {
87 self.to_sql_with(true)
88 }
89}
90
91impl ToSqlUnquoted for Expr {
92 fn to_sql_unquoted(&self) -> String {
93 self.to_sql_with(false)
94 }
95}
96
97impl Expr {
98 fn to_sql_with(&self, quoted: bool) -> String {
99 match self {
100 Expr::Identifier(s) => match quoted {
101 true => format! {r#""{s}""#},
102 false => s.to_owned(),
103 },
104 Expr::BinaryOp { left, op, right } => {
105 format!(
106 "{} {} {}",
107 left.to_sql_with(quoted),
108 op.to_sql(),
109 right.to_sql_with(quoted),
110 )
111 }
112 Expr::CompoundIdentifier { alias, ident } => match quoted {
113 true => format!(r#""{alias}"."{ident}""#),
114 false => format!("{alias}.{ident}"),
115 },
116 Expr::IsNull(s) => format!("{} IS NULL", s.to_sql_with(quoted)),
117 Expr::IsNotNull(s) => format!("{} IS NOT NULL", s.to_sql_with(quoted)),
118 Expr::InList {
119 expr,
120 list,
121 negated,
122 } => {
123 let expr = expr.to_sql_with(quoted);
124 let list = list
125 .iter()
126 .map(|expr| expr.to_sql_with(quoted))
127 .collect::<Vec<_>>()
128 .join(", ");
129
130 match negated {
131 true => format!("{expr} NOT IN ({list})"),
132 false => format!("{expr} IN ({list})"),
133 }
134 }
135 Expr::Between {
136 expr,
137 negated,
138 low,
139 high,
140 } => {
141 let expr = expr.to_sql_with(quoted);
142 let low = low.to_sql_with(quoted);
143 let high = high.to_sql_with(quoted);
144
145 match negated {
146 true => format!("{expr} NOT BETWEEN {low} AND {high}"),
147 false => format!("{expr} BETWEEN {low} AND {high}"),
148 }
149 }
150 Expr::Like {
151 expr,
152 negated,
153 pattern,
154 } => {
155 let expr = expr.to_sql_with(quoted);
156 let pattern = pattern.to_sql_with(quoted);
157
158 match negated {
159 true => format!("{expr} NOT LIKE {pattern}"),
160 false => format!("{expr} LIKE {pattern}"),
161 }
162 }
163 Expr::ILike {
164 expr,
165 negated,
166 pattern,
167 } => {
168 let expr = expr.to_sql_with(quoted);
169 let pattern = pattern.to_sql_with(quoted);
170
171 match negated {
172 true => format!("{expr} NOT ILIKE {pattern}"),
173 false => format!("{expr} ILIKE {pattern}"),
174 }
175 }
176 Expr::UnaryOp { op, expr } => match op {
177 UnaryOperator::Factorial => {
178 format!("{}{}", expr.to_sql_with(quoted), op.to_sql())
179 }
180 _ => format!("{}{}", op.to_sql(), expr.to_sql_with(quoted)),
181 },
182 Expr::Nested(expr) => format!("({})", expr.to_sql_with(quoted)),
183 Expr::Literal(s) => s.to_sql(),
184 Expr::TypedString { data_type, value } => format!("{data_type} '{value}'"),
185 Expr::Case {
186 operand,
187 when_then,
188 else_result,
189 } => {
190 let operand = match operand {
191 Some(operand) => format!("CASE {}", operand.to_sql_with(quoted)),
192 None => "CASE".to_owned(),
193 };
194
195 let when_then = when_then
196 .iter()
197 .map(|(when, then)| {
198 format!(
199 "WHEN {} THEN {}",
200 when.to_sql_with(quoted),
201 then.to_sql_with(quoted)
202 )
203 })
204 .collect::<Vec<_>>()
205 .join("\n");
206
207 let else_result = else_result
208 .as_ref()
209 .map(|else_result| format!("ELSE {}", else_result.to_sql_with(quoted)));
210
211 match else_result {
212 Some(else_result) => {
213 [operand, when_then, else_result, "END".to_owned()].join("\n")
214 }
215 None => [operand, when_then, "END".to_owned()].join("\n"),
216 }
217 }
218 Expr::Aggregate(a) => a.to_sql(),
219 Expr::Function(func) => func.to_sql(),
220 Expr::InSubquery {
221 expr,
222 subquery,
223 negated,
224 } => match negated {
225 true => format!(
226 "{} NOT IN ({})",
227 expr.to_sql_with(quoted),
228 subquery.to_sql()
229 ),
230 false => format!("{} IN ({})", expr.to_sql_with(quoted), subquery.to_sql()),
231 },
232 Expr::Exists { subquery, negated } => match negated {
233 true => format!("NOT EXISTS({})", subquery.to_sql()),
234 false => format!("EXISTS({})", subquery.to_sql()),
235 },
236 Expr::ArrayIndex { obj, indexes } => {
237 let obj = obj.to_sql_with(quoted);
238 let indexes = indexes
239 .iter()
240 .map(|index| format!("[{}]", index.to_sql_with(quoted)))
241 .collect::<Vec<_>>()
242 .join("");
243 format!("{obj}{indexes}")
244 }
245 Expr::Array { elem } => {
246 let elem = elem
247 .iter()
248 .map(|e| e.to_sql_with(quoted))
249 .collect::<Vec<_>>()
250 .join(", ");
251 format!("[{elem}]")
252 }
253 Expr::Subquery(query) => format!("({})", query.to_sql()),
254 Expr::Interval {
255 expr,
256 leading_field,
257 last_field,
258 } => {
259 let expr = expr.to_sql_with(quoted);
260 let leading_field = leading_field
261 .as_ref()
262 .map(|field| field.to_string())
263 .unwrap_or_else(|| "".to_owned());
264
265 match last_field {
266 Some(last_field) => format!("INTERVAL {expr} {leading_field} TO {last_field}"),
267 None => format!("INTERVAL {expr} {leading_field}"),
268 }
269 }
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276
277 use {
278 crate::ast::{
279 AstLiteral, BinaryOperator, DataType, DateTimeField, Expr, Query, Select, SelectItem,
280 SetExpr, TableFactor, TableWithJoins, ToSql, ToSqlUnquoted, UnaryOperator,
281 },
282 bigdecimal::BigDecimal,
283 regex::Regex,
284 std::str::FromStr,
285 };
286
287 #[test]
288 fn to_sql() {
289 let re = Regex::new(r"\n\s+").unwrap();
290 let trim = |s: &str| re.replace_all(s.trim(), "\n").into_owned();
291
292 assert_eq!(r#""id""#, Expr::Identifier("id".to_owned()).to_sql());
293
294 assert_eq!(
295 r#""id" + "num""#,
296 Expr::BinaryOp {
297 left: Box::new(Expr::Identifier("id".to_owned())),
298 op: BinaryOperator::Plus,
299 right: Box::new(Expr::Identifier("num".to_owned()))
300 }
301 .to_sql()
302 );
303 assert_eq!(
304 r#"-"id""#,
305 Expr::UnaryOp {
306 op: UnaryOperator::Minus,
307 expr: Box::new(Expr::Identifier("id".to_owned())),
308 }
309 .to_sql(),
310 );
311
312 assert_eq!(
313 r#""alias"."column""#,
314 Expr::CompoundIdentifier {
315 alias: "alias".into(),
316 ident: "column".into()
317 }
318 .to_sql()
319 );
320
321 assert_eq!(
322 "alias.column",
323 Expr::CompoundIdentifier {
324 alias: "alias".into(),
325 ident: "column".into()
326 }
327 .to_sql_unquoted()
328 );
329
330 let id_expr: Box<Expr> = Box::new(Expr::Identifier("id".to_owned()));
331 assert_eq!(r#""id" IS NULL"#, Expr::IsNull(id_expr).to_sql());
332
333 let id_expr: Box<Expr> = Box::new(Expr::Identifier("id".to_owned()));
334 assert_eq!(r#""id" IS NOT NULL"#, Expr::IsNotNull(id_expr).to_sql());
335
336 assert_eq!(
337 "INT '1'",
338 Expr::TypedString {
339 data_type: DataType::Int,
340 value: "1".to_owned()
341 }
342 .to_sql()
343 );
344
345 assert_eq!(
346 r#"("id")"#,
347 Expr::Nested(Box::new(Expr::Identifier("id".to_owned()))).to_sql(),
348 );
349
350 assert_eq!(
351 r#""id" BETWEEN "low" AND "high""#,
352 Expr::Between {
353 expr: Box::new(Expr::Identifier("id".to_owned())),
354 negated: false,
355 low: Box::new(Expr::Identifier("low".to_owned())),
356 high: Box::new(Expr::Identifier("high".to_owned()))
357 }
358 .to_sql()
359 );
360
361 assert_eq!(
362 r#""id" NOT BETWEEN "low" AND "high""#,
363 Expr::Between {
364 expr: Box::new(Expr::Identifier("id".to_owned())),
365 negated: true,
366 low: Box::new(Expr::Identifier("low".to_owned())),
367 high: Box::new(Expr::Identifier("high".to_owned()))
368 }
369 .to_sql()
370 );
371
372 assert_eq!(
373 r#""id" LIKE '%abc'"#,
374 Expr::Like {
375 expr: Box::new(Expr::Identifier("id".to_owned())),
376 negated: false,
377 pattern: Box::new(Expr::Literal(AstLiteral::QuotedString("%abc".to_owned()))),
378 }
379 .to_sql()
380 );
381
382 assert_eq!(
383 r#""id" NOT LIKE '%abc'"#,
384 Expr::Like {
385 expr: Box::new(Expr::Identifier("id".to_owned())),
386 negated: true,
387 pattern: Box::new(Expr::Literal(AstLiteral::QuotedString("%abc".to_owned()))),
388 }
389 .to_sql()
390 );
391
392 assert_eq!(
393 r#""id" ILIKE '%abc_'"#,
394 Expr::ILike {
395 expr: Box::new(Expr::Identifier("id".to_owned())),
396 negated: false,
397 pattern: Box::new(Expr::Literal(AstLiteral::QuotedString("%abc_".to_owned()))),
398 }
399 .to_sql()
400 );
401
402 assert_eq!(
403 r#""id" NOT ILIKE '%abc_'"#,
404 Expr::ILike {
405 expr: Box::new(Expr::Identifier("id".to_owned())),
406 negated: true,
407 pattern: Box::new(Expr::Literal(AstLiteral::QuotedString("%abc_".to_owned()))),
408 }
409 .to_sql()
410 );
411
412 assert_eq!(
413 r#""id" IN ('a', 'b', 'c')"#,
414 Expr::InList {
415 expr: Box::new(Expr::Identifier("id".to_owned())),
416 list: vec![
417 Expr::Literal(AstLiteral::QuotedString("a".to_owned())),
418 Expr::Literal(AstLiteral::QuotedString("b".to_owned())),
419 Expr::Literal(AstLiteral::QuotedString("c".to_owned()))
420 ],
421 negated: false
422 }
423 .to_sql()
424 );
425
426 assert_eq!(
427 r#""id" NOT IN ('a', 'b', 'c')"#,
428 Expr::InList {
429 expr: Box::new(Expr::Identifier("id".to_owned())),
430 list: vec![
431 Expr::Literal(AstLiteral::QuotedString("a".to_owned())),
432 Expr::Literal(AstLiteral::QuotedString("b".to_owned())),
433 Expr::Literal(AstLiteral::QuotedString("c".to_owned()))
434 ],
435 negated: true
436 }
437 .to_sql()
438 );
439
440 assert_eq!(
441 r#""id" IN (SELECT * FROM "FOO")"#,
442 Expr::InSubquery {
443 expr: Box::new(Expr::Identifier("id".to_owned())),
444 subquery: Box::new(Query {
445 body: SetExpr::Select(Box::new(Select {
446 distinct: false,
447 projection: vec![SelectItem::Wildcard],
448 from: TableWithJoins {
449 relation: TableFactor::Table {
450 name: "FOO".to_owned(),
451 alias: None,
452 index: None,
453 },
454 joins: Vec::new(),
455 },
456 selection: None,
457 group_by: Vec::new(),
458 having: None,
459 })),
460 order_by: Vec::new(),
461 limit: None,
462 offset: None,
463 }),
464 negated: false
465 }
466 .to_sql()
467 );
468
469 assert_eq!(
470 r#""id" NOT IN (SELECT * FROM "FOO")"#,
471 Expr::InSubquery {
472 expr: Box::new(Expr::Identifier("id".to_owned())),
473 subquery: Box::new(Query {
474 body: SetExpr::Select(Box::new(Select {
475 distinct: false,
476 projection: vec![SelectItem::Wildcard],
477 from: TableWithJoins {
478 relation: TableFactor::Table {
479 name: "FOO".to_owned(),
480 alias: None,
481 index: None,
482 },
483 joins: Vec::new(),
484 },
485 selection: None,
486 group_by: Vec::new(),
487 having: None,
488 })),
489 order_by: Vec::new(),
490 limit: None,
491 offset: None,
492 }),
493 negated: true
494 }
495 .to_sql()
496 );
497
498 assert_eq!(
499 r#"EXISTS(SELECT * FROM "FOO")"#,
500 Expr::Exists {
501 subquery: Box::new(Query {
502 body: SetExpr::Select(Box::new(Select {
503 distinct: false,
504 projection: vec![SelectItem::Wildcard],
505 from: TableWithJoins {
506 relation: TableFactor::Table {
507 name: "FOO".to_owned(),
508 alias: None,
509 index: None,
510 },
511 joins: Vec::new(),
512 },
513 selection: None,
514 group_by: Vec::new(),
515 having: None,
516 })),
517 order_by: Vec::new(),
518 limit: None,
519 offset: None,
520 }),
521 negated: false,
522 }
523 .to_sql(),
524 );
525
526 assert_eq!(
527 r#"NOT EXISTS(SELECT * FROM "FOO")"#,
528 Expr::Exists {
529 subquery: Box::new(Query {
530 body: SetExpr::Select(Box::new(Select {
531 distinct: false,
532 projection: vec![SelectItem::Wildcard],
533 from: TableWithJoins {
534 relation: TableFactor::Table {
535 name: "FOO".to_owned(),
536 alias: None,
537 index: None,
538 },
539 joins: Vec::new(),
540 },
541 selection: None,
542 group_by: Vec::new(),
543 having: None,
544 })),
545 order_by: Vec::new(),
546 limit: None,
547 offset: None,
548 }),
549 negated: true,
550 }
551 .to_sql(),
552 );
553
554 assert_eq!(
555 r#"(SELECT * FROM "FOO")"#,
556 Expr::Subquery(Box::new(Query {
557 body: SetExpr::Select(Box::new(Select {
558 distinct: false,
559 projection: vec![SelectItem::Wildcard],
560 from: TableWithJoins {
561 relation: TableFactor::Table {
562 name: "FOO".to_owned(),
563 alias: None,
564 index: None,
565 },
566 joins: Vec::new(),
567 },
568 selection: None,
569 group_by: Vec::new(),
570 having: None,
571 })),
572 order_by: Vec::new(),
573 limit: None,
574 offset: None,
575 }))
576 .to_sql()
577 );
578
579 assert_eq!(
580 trim(
581 r#"CASE "id"
582 WHEN 1 THEN 'a'
583 WHEN 2 THEN 'b'
584 ELSE 'c'
585 END"#,
586 ),
587 Expr::Case {
588 operand: Some(Box::new(Expr::Identifier("id".to_owned()))),
589 when_then: vec![
590 (
591 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("1").unwrap())),
592 Expr::Literal(AstLiteral::QuotedString("a".to_owned()))
593 ),
594 (
595 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap())),
596 Expr::Literal(AstLiteral::QuotedString("b".to_owned()))
597 )
598 ],
599 else_result: Some(Box::new(Expr::Literal(AstLiteral::QuotedString(
600 "c".to_owned()
601 ))))
602 }
603 .to_sql()
604 );
605
606 assert_eq!(
607 trim(
608 r#"CASE
609 WHEN "id" = 1 THEN 'a'
610 WHEN "id" = 2 THEN 'b'
611 END"#,
612 ),
613 Expr::Case {
614 operand: None,
615 when_then: vec![
616 (
617 Expr::BinaryOp {
618 left: Box::new(Expr::Identifier("id".to_owned())),
619 op: BinaryOperator::Eq,
620 right: Box::new(Expr::Literal(AstLiteral::Number(
621 BigDecimal::from_str("1").unwrap()
622 )))
623 },
624 Expr::Literal(AstLiteral::QuotedString("a".to_owned()))
625 ),
626 (
627 Expr::BinaryOp {
628 left: Box::new(Expr::Identifier("id".to_owned())),
629 op: BinaryOperator::Eq,
630 right: Box::new(Expr::Literal(AstLiteral::Number(
631 BigDecimal::from_str("2").unwrap()
632 )))
633 },
634 Expr::Literal(AstLiteral::QuotedString("b".to_owned()))
635 )
636 ],
637 else_result: None,
638 }
639 .to_sql()
640 );
641
642 assert_eq!(
643 trim(
644 r#"CASE "id"
645 WHEN 1 THEN 'a'
646 WHEN 2 THEN 'b'
647 END"#,
648 ),
649 Expr::Case {
650 operand: Some(Box::new(Expr::Identifier("id".to_owned()))),
651 when_then: vec![
652 (
653 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("1").unwrap())),
654 Expr::Literal(AstLiteral::QuotedString("a".to_owned()))
655 ),
656 (
657 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap())),
658 Expr::Literal(AstLiteral::QuotedString("b".to_owned()))
659 )
660 ],
661 else_result: None,
662 }
663 .to_sql()
664 );
665
666 assert_eq!(
667 r#""choco"[1][2]"#,
668 Expr::ArrayIndex {
669 obj: Box::new(Expr::Identifier("choco".to_owned())),
670 indexes: vec![
671 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("1").unwrap())),
672 Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap()))
673 ]
674 }
675 .to_sql()
676 );
677
678 assert_eq!(
679 r#"['GlueSQL', 'Rust']"#,
680 Expr::Array {
681 elem: vec![
682 Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())),
683 Expr::Literal(AstLiteral::QuotedString("Rust".to_owned()))
684 ]
685 }
686 .to_sql()
687 );
688
689 assert_eq!(
690 r#"INTERVAL "col1" + 3 DAY"#,
691 &Expr::Interval {
692 expr: Box::new(Expr::BinaryOp {
693 left: Box::new(Expr::Identifier("col1".to_owned())),
694 op: BinaryOperator::Plus,
695 right: Box::new(Expr::Literal(AstLiteral::Number(3.into()))),
696 }),
697 leading_field: Some(DateTimeField::Day),
698 last_field: None,
699 }
700 .to_sql()
701 );
702
703 assert_eq!(
704 "INTERVAL '3-5' HOUR TO MINUTE",
705 &Expr::Interval {
706 expr: Box::new(Expr::Literal(AstLiteral::QuotedString("3-5".to_owned()))),
707 leading_field: Some(DateTimeField::Hour),
708 last_field: Some(DateTimeField::Minute),
709 }
710 .to_sql()
711 );
712 }
713}