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}