gluesql_core/ast/
expr.rs

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}