Skip to main content

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::sql::{SQL, Token};
12use crate::traits::SQLParam;
13use crate::types::Compatible;
14use crate::{MySQLDialect, PostgresDialect};
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_expr_sql()
115            .push(Token::COMMA)
116            .append(default.into_expr_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_expr_sql();
159    for value in rest {
160        sql = sql.push(Token::COMMA).append(value.into_expr_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
200            .into_expr_sql()
201            .push(Token::COMMA)
202            .append(expr2.into_expr_sql()),
203    ))
204}
205
206// =============================================================================
207// IFNULL / NVL Function
208// =============================================================================
209
210/// IFNULL - SQLite/MySQL equivalent of COALESCE with two arguments.
211///
212/// Requires compatible types between the expression and default.
213/// Returns the first argument if not NULL, otherwise returns the second.
214#[allow(clippy::type_complexity)]
215pub fn ifnull<'a, V, E, D, N>(
216    expr: E,
217    default: D,
218) -> SQLExpr<'a, V, E::SQLType, N, <E::Aggregate as AggOr<D::Aggregate>>::Output>
219where
220    V: SQLParam + 'a,
221    E: Expr<'a, V>,
222    D: Expr<'a, V>,
223    N: Nullability,
224    E::SQLType: Compatible<D::SQLType>,
225    E::Nullable: NullAnd<D::Nullable, Output = N>,
226    D::Nullable: Nullability,
227    E::Aggregate: AggOr<D::Aggregate>,
228    D::Aggregate: AggregateKind,
229{
230    SQLExpr::new(SQL::func(
231        "IFNULL",
232        expr.into_expr_sql()
233            .push(Token::COMMA)
234            .append(default.into_expr_sql()),
235    ))
236}
237
238// =============================================================================
239// GREATEST / LEAST
240// =============================================================================
241
242/// Dialect-specific NULL propagation for `GREATEST` and `LEAST`.
243///
244/// PostgreSQL ignores NULL arguments, while MySQL returns NULL when either
245/// argument is NULL. SQLite does not provide these functions.
246#[diagnostic::on_unimplemented(
247    message = "GREATEST/LEAST are not available for this dialect",
248    label = "use a dialect-specific extrema expression"
249)]
250pub trait GreatestLeastPolicy<L: Nullability, R: Nullability> {
251    type Nullable: Nullability;
252}
253
254impl<L, R> GreatestLeastPolicy<L, R> for PostgresDialect
255where
256    L: NullAnd<R>,
257    R: Nullability,
258{
259    type Nullable = <L as NullAnd<R>>::Output;
260}
261
262impl<L, R> GreatestLeastPolicy<L, R> for MySQLDialect
263where
264    L: NullOr<R>,
265    R: Nullability,
266{
267    type Nullable = <L as NullOr<R>>::Output;
268}
269
270/// GREATEST - returns the largest of the given values (`PostgreSQL` and `MySQL`).
271///
272/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
273/// so `GREATEST(1, NULL)` returns `1`. The result is only NULL when all
274/// inputs are NULL. MySQL returns NULL if either input is NULL.
275///
276/// # Example
277///
278/// ```rust
279/// # let _ = r####"
280/// use drizzle_core::expr::greatest;
281///
282/// // Clamp to minimum of 0
283/// let score = greatest(users.score, 0);
284/// # "####;
285/// ```
286#[allow(clippy::type_complexity)]
287pub fn greatest<'a, V, L, R>(
288    left: L,
289    right: R,
290) -> SQLExpr<
291    'a,
292    V,
293    L::SQLType,
294    <V::DialectMarker as GreatestLeastPolicy<L::Nullable, R::Nullable>>::Nullable,
295    <L::Aggregate as AggOr<R::Aggregate>>::Output,
296>
297where
298    V: SQLParam + 'a,
299    V::DialectMarker: GreatestLeastPolicy<L::Nullable, R::Nullable>,
300    L: Expr<'a, V>,
301    R: Expr<'a, V>,
302    L::SQLType: Compatible<R::SQLType>,
303    R::Nullable: Nullability,
304    L::Aggregate: AggOr<R::Aggregate>,
305    R::Aggregate: AggregateKind,
306{
307    SQLExpr::new(SQL::func(
308        "GREATEST",
309        left.into_expr_sql()
310            .push(Token::COMMA)
311            .append(right.into_expr_sql()),
312    ))
313}
314
315/// LEAST - returns the smallest of the given values (`PostgreSQL` and `MySQL`).
316///
317/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
318/// so `LEAST(1, NULL)` returns `1`. The result is only NULL when all
319/// inputs are NULL. MySQL returns NULL if either input is NULL.
320///
321/// # Example
322///
323/// ```rust
324/// # let _ = r####"
325/// use drizzle_core::expr::least;
326///
327/// // Cap at maximum of 100
328/// let score = least(users.score, 100);
329/// # "####;
330/// ```
331#[allow(clippy::type_complexity)]
332pub fn least<'a, V, L, R>(
333    left: L,
334    right: R,
335) -> SQLExpr<
336    'a,
337    V,
338    L::SQLType,
339    <V::DialectMarker as GreatestLeastPolicy<L::Nullable, R::Nullable>>::Nullable,
340    <L::Aggregate as AggOr<R::Aggregate>>::Output,
341>
342where
343    V: SQLParam + 'a,
344    V::DialectMarker: GreatestLeastPolicy<L::Nullable, R::Nullable>,
345    L: Expr<'a, V>,
346    R: Expr<'a, V>,
347    L::SQLType: Compatible<R::SQLType>,
348    R::Nullable: Nullability,
349    L::Aggregate: AggOr<R::Aggregate>,
350    R::Aggregate: AggregateKind,
351{
352    SQLExpr::new(SQL::func(
353        "LEAST",
354        left.into_expr_sql()
355            .push(Token::COMMA)
356            .append(right.into_expr_sql()),
357    ))
358}