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, ToSQL};
13use crate::types::Compatible;
14
15use super::{Expr, NonNull, Null, Nullability, SQLExpr, Scalar};
16
17// =============================================================================
18// Nullability Combination
19// =============================================================================
20
21/// Combine nullability: if either input is nullable, output is nullable.
22///
23/// This follows SQL's NULL propagation semantics where operations on
24/// NULL values produce NULL results.
25///
26/// # Truth Table
27///
28/// | Left | Right | Output |
29/// |------|-------|--------|
30/// | NonNull | NonNull | NonNull |
31/// | NonNull | Null | Null |
32/// | Null | NonNull | Null |
33/// | Null | Null | Null |
34pub trait NullOr<Rhs: Nullability>: Nullability {
35    /// The resulting nullability.
36    type Output: Nullability;
37}
38
39impl NullOr<NonNull> for NonNull {
40    type Output = NonNull;
41}
42impl NullOr<Null> for NonNull {
43    type Output = Null;
44}
45impl NullOr<NonNull> for Null {
46    type Output = Null;
47}
48impl NullOr<Null> for Null {
49    type Output = Null;
50}
51
52// =============================================================================
53// COALESCE Function
54// =============================================================================
55
56/// COALESCE - returns first non-null value.
57///
58/// Requires compatible types between the expression and default.
59///
60/// # Type Safety
61///
62/// ```ignore
63/// // ✅ OK: Both are Text
64/// coalesce(users.nickname, users.name);
65///
66/// // ✅ OK: Int with i32 literal
67/// coalesce(users.age, 0);
68///
69/// // ❌ Compile error: Int not compatible with Text
70/// coalesce(users.age, "unknown");
71/// ```
72pub fn coalesce<'a, V, E, D>(expr: E, default: D) -> SQLExpr<'a, V, E::SQLType, Null, Scalar>
73where
74    V: SQLParam + 'a,
75    E: Expr<'a, V>,
76    D: Expr<'a, V>,
77    E::SQLType: Compatible<D::SQLType>,
78{
79    SQLExpr::new(SQL::func(
80        "COALESCE",
81        expr.into_sql()
82            .push(Token::COMMA)
83            .append(default.into_sql()),
84    ))
85}
86
87/// COALESCE with multiple values.
88///
89/// Returns the first non-null value from the provided expressions.
90/// Takes an explicit first argument to guarantee at least one value at compile time.
91///
92/// # Example
93///
94/// ```ignore
95/// use drizzle_core::expr::coalesce_many;
96///
97/// // COALESCE(users.nickname, users.username, 'Anonymous')
98/// let name = coalesce_many(users.nickname, [users.username, "Anonymous"]);
99/// ```
100pub fn coalesce_many<'a, V, E, I>(first: E, rest: I) -> SQLExpr<'a, V, E::SQLType, Null, Scalar>
101where
102    V: SQLParam + 'a,
103    E: Expr<'a, V>,
104    I: IntoIterator,
105    I::Item: Expr<'a, V>,
106{
107    let mut sql = first.into_sql();
108    for value in rest {
109        sql = sql.push(Token::COMMA).append(value.into_sql());
110    }
111    SQLExpr::new(SQL::func("COALESCE", sql))
112}
113
114// =============================================================================
115// NULLIF Function
116// =============================================================================
117
118/// NULLIF - returns NULL if arguments are equal, else first argument.
119///
120/// Requires compatible types between the two arguments.
121/// The result is always nullable since it can return NULL.
122///
123/// # Example
124///
125/// ```ignore
126/// use drizzle_core::expr::nullif;
127///
128/// // Returns NULL if status is 'unknown', otherwise returns status
129/// let status = nullif(item.status, "unknown");
130/// ```
131pub fn nullif<'a, V, E1, E2>(expr1: E1, expr2: E2) -> SQLExpr<'a, V, E1::SQLType, Null, Scalar>
132where
133    V: SQLParam + 'a,
134    E1: Expr<'a, V>,
135    E2: Expr<'a, V>,
136    E1::SQLType: Compatible<E2::SQLType>,
137{
138    SQLExpr::new(SQL::func(
139        "NULLIF",
140        expr1.into_sql().push(Token::COMMA).append(expr2.into_sql()),
141    ))
142}
143
144// =============================================================================
145// IFNULL / NVL Function
146// =============================================================================
147
148/// IFNULL - SQLite/MySQL equivalent of COALESCE with two arguments.
149///
150/// Requires compatible types between the expression and default.
151/// Returns the first argument if not NULL, otherwise returns the second.
152pub fn ifnull<'a, V, E, D>(expr: E, default: D) -> SQLExpr<'a, V, E::SQLType, Null, Scalar>
153where
154    V: SQLParam + 'a,
155    E: Expr<'a, V>,
156    D: Expr<'a, V>,
157    E::SQLType: Compatible<D::SQLType>,
158{
159    SQLExpr::new(SQL::func(
160        "IFNULL",
161        expr.into_sql()
162            .push(Token::COMMA)
163            .append(default.into_sql()),
164    ))
165}