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.to_sql().push(Token::COMMA).append(default.to_sql()),
82 ))
83}
84
85/// COALESCE with multiple values.
86///
87/// Returns the first non-null value from the provided expressions.
88/// Takes an explicit first argument to guarantee at least one value at compile time.
89///
90/// # Example
91///
92/// ```ignore
93/// use drizzle_core::expr::coalesce_many;
94///
95/// // COALESCE(users.nickname, users.username, 'Anonymous')
96/// let name = coalesce_many(users.nickname, [users.username, "Anonymous"]);
97/// ```
98pub fn coalesce_many<'a, V, E, I>(first: E, rest: I) -> SQLExpr<'a, V, E::SQLType, Null, Scalar>
99where
100 V: SQLParam + 'a,
101 E: Expr<'a, V>,
102 I: IntoIterator,
103 I::Item: Expr<'a, V>,
104{
105 let mut sql = first.to_sql();
106 for value in rest {
107 sql = sql.push(Token::COMMA).append(value.to_sql());
108 }
109 SQLExpr::new(SQL::func("COALESCE", sql))
110}
111
112// =============================================================================
113// NULLIF Function
114// =============================================================================
115
116/// NULLIF - returns NULL if arguments are equal, else first argument.
117///
118/// Requires compatible types between the two arguments.
119/// The result is always nullable since it can return NULL.
120///
121/// # Example
122///
123/// ```ignore
124/// use drizzle_core::expr::nullif;
125///
126/// // Returns NULL if status is 'unknown', otherwise returns status
127/// let status = nullif(item.status, "unknown");
128/// ```
129pub fn nullif<'a, V, E1, E2>(expr1: E1, expr2: E2) -> SQLExpr<'a, V, E1::SQLType, Null, Scalar>
130where
131 V: SQLParam + 'a,
132 E1: Expr<'a, V>,
133 E2: Expr<'a, V>,
134 E1::SQLType: Compatible<E2::SQLType>,
135{
136 SQLExpr::new(SQL::func(
137 "NULLIF",
138 expr1.to_sql().push(Token::COMMA).append(expr2.to_sql()),
139 ))
140}
141
142// =============================================================================
143// IFNULL / NVL Function
144// =============================================================================
145
146/// IFNULL - SQLite/MySQL equivalent of COALESCE with two arguments.
147///
148/// Requires compatible types between the expression and default.
149/// Returns the first argument if not NULL, otherwise returns the second.
150pub fn ifnull<'a, V, E, D>(expr: E, default: D) -> SQLExpr<'a, V, E::SQLType, Null, Scalar>
151where
152 V: SQLParam + 'a,
153 E: Expr<'a, V>,
154 D: Expr<'a, V>,
155 E::SQLType: Compatible<D::SQLType>,
156{
157 SQLExpr::new(SQL::func(
158 "IFNULL",
159 expr.to_sql().push(Token::COMMA).append(default.to_sql()),
160 ))
161}