drizzle_core/expr/null.rs
1//! NULL propagation and handling.
2//!
3//! This module provides traits and functions for handling SQL NULL values
4//! in a type-safe manner.
5//!
6//! # Type Safety
7//!
8//! - `coalesce`, `ifnull`: Require compatible types between expression and default
9//! - `nullif`: Requires compatible types between the two arguments
10
11use crate::PostgresDialect;
12use crate::sql::{SQL, Token};
13use crate::traits::{SQLParam, ToSQL};
14use crate::types::Compatible;
15
16use super::{AggOr, AggregateKind, Expr, NonNull, Null, Nullability, SQLExpr};
17
18// =============================================================================
19// Nullability Combination
20// =============================================================================
21
22/// Combine nullability: if either input is nullable, output is nullable.
23///
24/// This follows SQL's NULL propagation semantics where operations on
25/// NULL values produce NULL results.
26///
27/// # Truth Table
28///
29/// | Left | Right | Output |
30/// |------|-------|--------|
31/// | NonNull | NonNull | NonNull |
32/// | NonNull | Null | Null |
33/// | Null | NonNull | Null |
34/// | Null | Null | Null |
35pub trait NullOr<Rhs: Nullability>: Nullability {
36 /// The resulting nullability.
37 type Output: Nullability;
38}
39
40impl NullOr<Self> for NonNull {
41 type Output = Self;
42}
43impl NullOr<Null> for NonNull {
44 type Output = Null;
45}
46impl NullOr<NonNull> for Null {
47 type Output = Self;
48}
49impl NullOr<Self> for Null {
50 type Output = Self;
51}
52
53/// Combine nullability for COALESCE-style fallback behavior.
54///
55/// Result is nullable only when both inputs are nullable.
56pub trait NullAnd<Rhs: Nullability>: Nullability {
57 /// The resulting nullability.
58 type Output: Nullability;
59}
60
61impl NullAnd<Self> for NonNull {
62 type Output = Self;
63}
64impl NullAnd<Null> for NonNull {
65 type Output = Self;
66}
67impl NullAnd<NonNull> for Null {
68 type Output = NonNull;
69}
70impl NullAnd<Self> for Null {
71 type Output = Self;
72}
73
74// =============================================================================
75// COALESCE Function
76// =============================================================================
77
78/// COALESCE - returns first non-null value.
79///
80/// Requires compatible types between the expression and default.
81///
82/// # Type Safety
83///
84/// ```rust
85/// # let _ = r####"
86/// // ✅ OK: Both are Text
87/// coalesce(users.nickname, users.name);
88///
89/// // ✅ OK: Int with i32 literal
90/// coalesce(users.age, 0);
91///
92/// // ❌ Compile error: Int not compatible with Text
93/// coalesce(users.age, "unknown");
94/// # "####;
95/// ```
96#[allow(clippy::type_complexity)]
97pub fn coalesce<'a, V, E, D, N>(
98 expr: E,
99 default: D,
100) -> SQLExpr<'a, V, E::SQLType, N, <E::Aggregate as AggOr<D::Aggregate>>::Output>
101where
102 V: SQLParam + 'a,
103 E: Expr<'a, V>,
104 D: Expr<'a, V>,
105 N: Nullability,
106 E::SQLType: Compatible<D::SQLType>,
107 E::Nullable: NullAnd<D::Nullable, Output = N>,
108 D::Nullable: Nullability,
109 E::Aggregate: AggOr<D::Aggregate>,
110 D::Aggregate: AggregateKind,
111{
112 SQLExpr::new(SQL::func(
113 "COALESCE",
114 expr.into_sql()
115 .push(Token::COMMA)
116 .append(default.into_sql()),
117 ))
118}
119
120/// COALESCE with multiple values.
121///
122/// Returns the first non-null value from the provided expressions.
123/// Takes an explicit first argument to guarantee at least one value at compile time.
124///
125/// # Example
126///
127/// ```rust
128/// # let _ = r####"
129/// use drizzle_core::expr::coalesce_many;
130///
131/// // COALESCE(users.nickname, users.username, 'Anonymous')
132/// let name = coalesce_many(users.nickname, [users.username, "Anonymous"]);
133/// # "####;
134/// ```
135#[allow(clippy::type_complexity)]
136pub fn coalesce_many<'a, V, E, I, N>(
137 first: E,
138 rest: I,
139) -> SQLExpr<
140 'a,
141 V,
142 E::SQLType,
143 N,
144 <E::Aggregate as AggOr<<I::Item as Expr<'a, V>>::Aggregate>>::Output,
145>
146where
147 V: SQLParam + 'a,
148 E: Expr<'a, V>,
149 N: Nullability,
150 I: IntoIterator,
151 I::Item: Expr<'a, V>,
152 E::SQLType: Compatible<<I::Item as Expr<'a, V>>::SQLType>,
153 E::Nullable: NullAnd<<I::Item as Expr<'a, V>>::Nullable, Output = N>,
154 <I::Item as Expr<'a, V>>::Nullable: Nullability,
155 E::Aggregate: AggOr<<I::Item as Expr<'a, V>>::Aggregate>,
156 <I::Item as Expr<'a, V>>::Aggregate: AggregateKind,
157{
158 let mut sql = first.into_sql();
159 for value in rest {
160 sql = sql.push(Token::COMMA).append(value.into_sql());
161 }
162 SQLExpr::new(SQL::func("COALESCE", sql))
163}
164
165// =============================================================================
166// NULLIF Function
167// =============================================================================
168
169/// NULLIF - returns NULL if arguments are equal, else first argument.
170///
171/// Requires compatible types between the two arguments.
172/// The result is always nullable since it can return NULL.
173///
174/// # Example
175///
176/// ```rust
177/// # let _ = r####"
178/// use drizzle_core::expr::nullif;
179///
180/// // Returns NULL if status is 'unknown', otherwise returns status
181/// let status = nullif(item.status, "unknown");
182/// # "####;
183/// ```
184#[allow(clippy::type_complexity)]
185pub fn nullif<'a, V, E1, E2>(
186 expr1: E1,
187 expr2: E2,
188) -> SQLExpr<'a, V, E1::SQLType, Null, <E1::Aggregate as AggOr<E2::Aggregate>>::Output>
189where
190 V: SQLParam + 'a,
191 E1: Expr<'a, V>,
192 E2: Expr<'a, V>,
193 E1::SQLType: Compatible<E2::SQLType>,
194 E1::Aggregate: AggOr<E2::Aggregate>,
195 E2::Aggregate: AggregateKind,
196{
197 SQLExpr::new(SQL::func(
198 "NULLIF",
199 expr1.into_sql().push(Token::COMMA).append(expr2.into_sql()),
200 ))
201}
202
203// =============================================================================
204// IFNULL / NVL Function
205// =============================================================================
206
207/// IFNULL - SQLite/MySQL equivalent of COALESCE with two arguments.
208///
209/// Requires compatible types between the expression and default.
210/// Returns the first argument if not NULL, otherwise returns the second.
211#[allow(clippy::type_complexity)]
212pub fn ifnull<'a, V, E, D, N>(
213 expr: E,
214 default: D,
215) -> SQLExpr<'a, V, E::SQLType, N, <E::Aggregate as AggOr<D::Aggregate>>::Output>
216where
217 V: SQLParam + 'a,
218 E: Expr<'a, V>,
219 D: Expr<'a, V>,
220 N: Nullability,
221 E::SQLType: Compatible<D::SQLType>,
222 E::Nullable: NullAnd<D::Nullable, Output = N>,
223 D::Nullable: Nullability,
224 E::Aggregate: AggOr<D::Aggregate>,
225 D::Aggregate: AggregateKind,
226{
227 SQLExpr::new(SQL::func(
228 "IFNULL",
229 expr.into_sql()
230 .push(Token::COMMA)
231 .append(default.into_sql()),
232 ))
233}
234
235// =============================================================================
236// GREATEST / LEAST
237// =============================================================================
238
239/// Marker trait for dialects that support GREATEST/LEAST (`PostgreSQL`).
240///
241/// `SQLite` does not have `GREATEST`/`LEAST`. While its multi-argument
242/// `MAX`/`MIN` are superficially similar, they have different NULL semantics:
243/// `PostgreSQL` ignores NULLs (`GREATEST(1, NULL)` = `1`) while `SQLite`
244/// propagates them (`MAX(1, NULL)` = `NULL`).
245pub trait PostgresNullSupport {}
246impl PostgresNullSupport for PostgresDialect {}
247
248/// GREATEST - returns the largest of the given values (`PostgreSQL`).
249///
250/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
251/// so `GREATEST(1, NULL)` returns `1`. The result is only NULL when all
252/// inputs are NULL.
253///
254/// # Example
255///
256/// ```rust
257/// # let _ = r####"
258/// use drizzle_core::expr::greatest;
259///
260/// // Clamp to minimum of 0
261/// let score = greatest(users.score, 0);
262/// # "####;
263/// ```
264#[allow(clippy::type_complexity)]
265pub fn greatest<'a, V, L, R>(
266 left: L,
267 right: R,
268) -> SQLExpr<
269 'a,
270 V,
271 L::SQLType,
272 <L::Nullable as NullAnd<R::Nullable>>::Output,
273 <L::Aggregate as AggOr<R::Aggregate>>::Output,
274>
275where
276 V: SQLParam + 'a,
277 V::DialectMarker: PostgresNullSupport,
278 L: Expr<'a, V>,
279 R: Expr<'a, V>,
280 L::SQLType: Compatible<R::SQLType>,
281 L::Nullable: NullAnd<R::Nullable>,
282 R::Nullable: Nullability,
283 L::Aggregate: AggOr<R::Aggregate>,
284 R::Aggregate: AggregateKind,
285{
286 SQLExpr::new(SQL::func(
287 "GREATEST",
288 left.into_sql().push(Token::COMMA).append(right.into_sql()),
289 ))
290}
291
292/// LEAST - returns the smallest of the given values (`PostgreSQL`).
293///
294/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
295/// so `LEAST(1, NULL)` returns `1`. The result is only NULL when all
296/// inputs are NULL.
297///
298/// # Example
299///
300/// ```rust
301/// # let _ = r####"
302/// use drizzle_core::expr::least;
303///
304/// // Cap at maximum of 100
305/// let score = least(users.score, 100);
306/// # "####;
307/// ```
308#[allow(clippy::type_complexity)]
309pub fn least<'a, V, L, R>(
310 left: L,
311 right: R,
312) -> SQLExpr<
313 'a,
314 V,
315 L::SQLType,
316 <L::Nullable as NullAnd<R::Nullable>>::Output,
317 <L::Aggregate as AggOr<R::Aggregate>>::Output,
318>
319where
320 V: SQLParam + 'a,
321 V::DialectMarker: PostgresNullSupport,
322 L: Expr<'a, V>,
323 R: Expr<'a, V>,
324 L::SQLType: Compatible<R::SQLType>,
325 L::Nullable: NullAnd<R::Nullable>,
326 R::Nullable: Nullability,
327 L::Aggregate: AggOr<R::Aggregate>,
328 R::Aggregate: AggregateKind,
329{
330 SQLExpr::new(SQL::func(
331 "LEAST",
332 left.into_sql().push(Token::COMMA).append(right.into_sql()),
333 ))
334}