Skip to main content

drizzle_core/expr/
case.rs

1//! Type-safe CASE/WHEN expressions.
2//!
3//! Provides a typestate builder for SQL CASE expressions that tracks the
4//! result type and nullability through each WHEN branch and the optional
5//! ELSE clause.
6//!
7//! # Example
8//!
9//! ```rust
10//! # let _ = r####"
11//! use drizzle_core::expr::*;
12//!
13//! // Searched CASE with ELSE — result is NonNull Text
14//! case()
15//!     .when(gt(users.age, 65), "Senior")
16//!     .when(gt(users.age, 18), "Adult")
17//!     .r#else("Minor")
18//!
19//! // Without ELSE — result is always Null
20//! case()
21//!     .when(gt(users.age, 18), "Adult")
22//!     .end()
23//! # "####;
24//! ```
25
26use core::marker::PhantomData;
27
28use crate::sql::{SQL, Token};
29use crate::traits::SQLParam;
30use crate::types::{BooleanLike, Compatible, DataType};
31
32use super::null::NullOr;
33use super::{AggOr, AggregateKind, Expr, Null, Nullability, SQLExpr};
34
35// =============================================================================
36// Entry Point
37// =============================================================================
38
39/// Start building a searched CASE expression.
40///
41/// Returns a `CaseInit` which requires at least one `.when()` call before
42/// it can be finished with `.end()` or `.r#else()`.
43#[must_use]
44pub fn case<'a, V: SQLParam>() -> CaseInit<'a, V> {
45    CaseInit {
46        sql: SQL::from(Token::CASE),
47        _marker: PhantomData,
48    }
49}
50
51// =============================================================================
52// CaseInit — before the first WHEN (no type established yet)
53// =============================================================================
54
55/// Builder state before the first WHEN branch.
56///
57/// The result type is not yet known — it will be set by the first `.when()`.
58pub struct CaseInit<'a, V: SQLParam> {
59    sql: SQL<'a, V>,
60    _marker: PhantomData<V>,
61}
62
63impl<'a, V: SQLParam + 'a> CaseInit<'a, V> {
64    /// Add the first WHEN branch. This establishes the result type.
65    ///
66    /// ```rust
67    /// # let _ = r####"
68    /// case().when(gt(users.age, 65), "Senior")
69    /// // Type T = Text, Nullability N = NonNull (from &str literal)
70    /// # "####;
71    /// ```
72    #[allow(clippy::type_complexity)]
73    pub fn when<C, R>(
74        self,
75        condition: C,
76        result: R,
77    ) -> CaseBuilder<'a, V, R::SQLType, R::Nullable, <C::Aggregate as AggOr<R::Aggregate>>::Output>
78    where
79        C: Expr<'a, V>,
80        R: Expr<'a, V>,
81        C::SQLType: BooleanLike,
82        C::Aggregate: AggOr<R::Aggregate>,
83    {
84        let sql = self
85            .sql
86            .push(Token::WHEN)
87            .append(condition.into_expr_sql())
88            .push(Token::THEN)
89            .append(result.into_expr_sql());
90
91        CaseBuilder {
92            sql,
93            _marker: PhantomData,
94        }
95    }
96}
97
98// =============================================================================
99// CaseBuilder — after at least one WHEN (type T established)
100// =============================================================================
101
102/// Builder state after at least one WHEN branch has been added.
103///
104/// The result type `T` and accumulated nullability `N` are tracked.
105pub struct CaseBuilder<'a, V: SQLParam, T: DataType, N: Nullability, A: AggregateKind> {
106    sql: SQL<'a, V>,
107    _marker: PhantomData<(V, T, N, A)>,
108}
109
110impl<'a, V, T, N, A> CaseBuilder<'a, V, T, N, A>
111where
112    V: SQLParam + 'a,
113    T: DataType,
114    N: Nullability,
115    A: AggregateKind,
116{
117    /// Add another WHEN branch.
118    ///
119    /// The result type must be compatible with the type established by the
120    /// first branch. Nullability is accumulated via `NullOr`.
121    #[allow(clippy::type_complexity)]
122    pub fn when<C, R>(
123        self,
124        condition: C,
125        result: R,
126    ) -> CaseBuilder<
127        'a,
128        V,
129        T,
130        <N as NullOr<R::Nullable>>::Output,
131        <<A as AggOr<C::Aggregate>>::Output as AggOr<R::Aggregate>>::Output,
132    >
133    where
134        C: Expr<'a, V>,
135        R: Expr<'a, V>,
136        C::SQLType: BooleanLike,
137        T: Compatible<R::SQLType>,
138        N: NullOr<R::Nullable>,
139        R::Nullable: Nullability,
140        A: AggOr<C::Aggregate>,
141        <A as AggOr<C::Aggregate>>::Output: AggOr<R::Aggregate>,
142        C::Aggregate: AggregateKind,
143        R::Aggregate: AggregateKind,
144    {
145        let sql = self
146            .sql
147            .push(Token::WHEN)
148            .append(condition.into_expr_sql())
149            .push(Token::THEN)
150            .append(result.into_expr_sql());
151
152        CaseBuilder {
153            sql,
154            _marker: PhantomData,
155        }
156    }
157
158    /// Finish the CASE expression without an ELSE clause.
159    ///
160    /// Without ELSE, unmatched rows produce NULL, so the result is always
161    /// `Null` regardless of branch nullability.
162    pub fn end(self) -> SQLExpr<'a, V, T, Null, A> {
163        let sql = self.sql.push(Token::END);
164        SQLExpr::new(sql)
165    }
166
167    /// Finish the CASE expression with an ELSE clause.
168    ///
169    /// The ELSE value must have a compatible type. Nullability is the
170    /// combination of all branch nullabilities and the default's nullability.
171    #[allow(clippy::type_complexity)]
172    pub fn r#else<D>(
173        self,
174        default: D,
175    ) -> SQLExpr<'a, V, T, <N as NullOr<D::Nullable>>::Output, <A as AggOr<D::Aggregate>>::Output>
176    where
177        D: Expr<'a, V>,
178        T: Compatible<D::SQLType>,
179        N: NullOr<D::Nullable>,
180        D::Nullable: Nullability,
181        A: AggOr<D::Aggregate>,
182        D::Aggregate: AggregateKind,
183    {
184        let sql = self
185            .sql
186            .push(Token::ELSE)
187            .append(default.into_expr_sql())
188            .push(Token::END);
189        SQLExpr::new(sql)
190    }
191}