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}