Skip to main content

keelson_core/expr/
case.rs

1use super::convert::IntoExpr;
2use super::node::Expr;
3
4/// A `CASE` expression under construction — bob's `CaseChain`.
5///
6/// ```
7/// use keelson_core::expr::{case, literal, quote, Chain, arg};
8///
9/// // (CASE WHEN ("id" = $1) THEN 'A' ELSE 'B' END)
10/// let e = case()
11///     .when(quote("id").eq(arg(1i32)), literal("A"))
12///     .else_(literal("B"));
13/// ```
14///
15/// [`end`](Self::end) and [`else_`](Self::else_) both apply the parenthesisation
16/// rule, so the result is `(CASE .. END)` — self-delimiting, and safe to drop into
17/// an operand slot or alias with
18/// [`Chain::as_`](crate::expr::Chain::as_).
19#[derive(Debug, Clone, Default)]
20pub struct CaseBuilder {
21    whens: Vec<(Expr, Expr)>,
22}
23
24impl CaseBuilder {
25    /// An empty `CASE`. At least one [`when`](Self::when) is required before it
26    /// can render.
27    pub fn new() -> CaseBuilder {
28        CaseBuilder::default()
29    }
30
31    /// Add a `WHEN condition THEN result` branch.
32    #[must_use]
33    pub fn when(mut self, condition: impl IntoExpr, then: impl IntoExpr) -> CaseBuilder {
34        self.whens.push((condition.into_expr(), then.into_expr()));
35        self
36    }
37
38    /// Finish with an `ELSE` branch.
39    #[must_use]
40    pub fn else_(self, then: impl IntoExpr) -> Expr {
41        Expr::Case {
42            whens: self.whens,
43            else_: Some(Box::new(then.into_expr())),
44        }
45        .grouped()
46    }
47
48    /// Finish without an `ELSE` branch.
49    #[must_use]
50    pub fn end(self) -> Expr {
51        Expr::Case {
52            whens: self.whens,
53            else_: None,
54        }
55        .grouped()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use keelson_sqlcheck::testing::assert_frag_sql;
62
63    use super::super::{arg, case, literal, quote};
64    use super::*;
65    use crate::dialect::testing::Numbered;
66    use crate::expr::Chain;
67    use crate::writer::build;
68
69    const VALUE: &str = "SELECT {} FROM users";
70
71    fn sql(e: Expr) -> String {
72        build(&Numbered, &e).expect("render").0
73    }
74
75    /// The `case with else` fixture, in all three dialects, is this shape aliased
76    /// into a select list.
77    #[test]
78    fn a_case_with_an_else_branch() {
79        let e = case()
80            .when(quote("id").eq(literal("1")), literal("A"))
81            .else_(literal("B"))
82            .as_("C");
83        assert_frag_sql(
84            VALUE,
85            &sql(e),
86            r#"(CASE WHEN ("id" = '1') THEN 'A' ELSE 'B' END) AS "C""#,
87        );
88    }
89
90    /// The `case without else` fixture.
91    #[test]
92    fn a_case_without_an_else_branch() {
93        let e = case()
94            .when(quote("id").eq(literal("1")), literal("A"))
95            .end()
96            .as_("C");
97        assert_frag_sql(
98            VALUE,
99            &sql(e),
100            r#"(CASE WHEN ("id" = '1') THEN 'A' END) AS "C""#,
101        );
102    }
103
104    #[test]
105    fn branches_render_in_the_order_they_were_added() {
106        let e = case()
107            .when(quote("is_active"), arg(1i32))
108            .when(Expr::raw("age > 1"), arg(2i32))
109            .else_(arg(3i32));
110        let (s, args) = build(&Numbered, &e).unwrap();
111        // The cast is the frame's: every result is a placeholder, so the CASE has
112        // no branch of known type and PostgreSQL cannot infer one — comparing it
113        // to an integer column does not help, since the comparison is what it
114        // would need the type for.
115        assert_frag_sql(
116            r#"SELECT "id" FROM users WHERE "age" = CAST({} AS integer)"#,
117            &s,
118            r#"(CASE WHEN "is_active" THEN $1 WHEN age > 1 THEN $2 ELSE $3 END)"#,
119        );
120        assert_eq!(args.len(), 3);
121    }
122
123    #[test]
124    fn a_case_with_no_branches_refuses_to_build() {
125        assert!(build(&Numbered, &CaseBuilder::new().end()).is_err());
126    }
127}