drizzle-core 0.1.9

A type-safe SQL query builder for Rust
Documentation
//! NULL propagation and handling.
//!
//! This module provides traits and functions for handling SQL NULL values
//! in a type-safe manner.
//!
//! # Type Safety
//!
//! - `coalesce`, `ifnull`: Require compatible types between expression and default
//! - `nullif`: Requires compatible types between the two arguments

use crate::PostgresDialect;
use crate::sql::{SQL, Token};
use crate::traits::SQLParam;
use crate::types::Compatible;

use super::{AggOr, AggregateKind, Expr, NonNull, Null, Nullability, SQLExpr};

// =============================================================================
// Nullability Combination
// =============================================================================

/// Combine nullability: if either input is nullable, output is nullable.
///
/// This follows SQL's NULL propagation semantics where operations on
/// NULL values produce NULL results.
///
/// # Truth Table
///
/// | Left | Right | Output |
/// |------|-------|--------|
/// | NonNull | NonNull | NonNull |
/// | NonNull | Null | Null |
/// | Null | NonNull | Null |
/// | Null | Null | Null |
pub trait NullOr<Rhs: Nullability>: Nullability {
    /// The resulting nullability.
    type Output: Nullability;
}

impl NullOr<Self> for NonNull {
    type Output = Self;
}
impl NullOr<Null> for NonNull {
    type Output = Null;
}
impl NullOr<NonNull> for Null {
    type Output = Self;
}
impl NullOr<Self> for Null {
    type Output = Self;
}

/// Combine nullability for COALESCE-style fallback behavior.
///
/// Result is nullable only when both inputs are nullable.
pub trait NullAnd<Rhs: Nullability>: Nullability {
    /// The resulting nullability.
    type Output: Nullability;
}

impl NullAnd<Self> for NonNull {
    type Output = Self;
}
impl NullAnd<Null> for NonNull {
    type Output = Self;
}
impl NullAnd<NonNull> for Null {
    type Output = NonNull;
}
impl NullAnd<Self> for Null {
    type Output = Self;
}

// =============================================================================
// COALESCE Function
// =============================================================================

/// COALESCE - returns first non-null value.
///
/// Requires compatible types between the expression and default.
///
/// # Type Safety
///
/// ```rust
/// # let _ = r####"
/// // ✅ OK: Both are Text
/// coalesce(users.nickname, users.name);
///
/// // ✅ OK: Int with i32 literal
/// coalesce(users.age, 0);
///
/// // ❌ Compile error: Int not compatible with Text
/// coalesce(users.age, "unknown");
/// # "####;
/// ```
#[allow(clippy::type_complexity)]
pub fn coalesce<'a, V, E, D, N>(
    expr: E,
    default: D,
) -> SQLExpr<'a, V, E::SQLType, N, <E::Aggregate as AggOr<D::Aggregate>>::Output>
where
    V: SQLParam + 'a,
    E: Expr<'a, V>,
    D: Expr<'a, V>,
    N: Nullability,
    E::SQLType: Compatible<D::SQLType>,
    E::Nullable: NullAnd<D::Nullable, Output = N>,
    D::Nullable: Nullability,
    E::Aggregate: AggOr<D::Aggregate>,
    D::Aggregate: AggregateKind,
{
    SQLExpr::new(SQL::func(
        "COALESCE",
        expr.into_expr_sql()
            .push(Token::COMMA)
            .append(default.into_expr_sql()),
    ))
}

/// COALESCE with multiple values.
///
/// Returns the first non-null value from the provided expressions.
/// Takes an explicit first argument to guarantee at least one value at compile time.
///
/// # Example
///
/// ```rust
/// # let _ = r####"
/// use drizzle_core::expr::coalesce_many;
///
/// // COALESCE(users.nickname, users.username, 'Anonymous')
/// let name = coalesce_many(users.nickname, [users.username, "Anonymous"]);
/// # "####;
/// ```
#[allow(clippy::type_complexity)]
pub fn coalesce_many<'a, V, E, I, N>(
    first: E,
    rest: I,
) -> SQLExpr<
    'a,
    V,
    E::SQLType,
    N,
    <E::Aggregate as AggOr<<I::Item as Expr<'a, V>>::Aggregate>>::Output,
>
where
    V: SQLParam + 'a,
    E: Expr<'a, V>,
    N: Nullability,
    I: IntoIterator,
    I::Item: Expr<'a, V>,
    E::SQLType: Compatible<<I::Item as Expr<'a, V>>::SQLType>,
    E::Nullable: NullAnd<<I::Item as Expr<'a, V>>::Nullable, Output = N>,
    <I::Item as Expr<'a, V>>::Nullable: Nullability,
    E::Aggregate: AggOr<<I::Item as Expr<'a, V>>::Aggregate>,
    <I::Item as Expr<'a, V>>::Aggregate: AggregateKind,
{
    let mut sql = first.into_expr_sql();
    for value in rest {
        sql = sql.push(Token::COMMA).append(value.into_expr_sql());
    }
    SQLExpr::new(SQL::func("COALESCE", sql))
}

// =============================================================================
// NULLIF Function
// =============================================================================

/// NULLIF - returns NULL if arguments are equal, else first argument.
///
/// Requires compatible types between the two arguments.
/// The result is always nullable since it can return NULL.
///
/// # Example
///
/// ```rust
/// # let _ = r####"
/// use drizzle_core::expr::nullif;
///
/// // Returns NULL if status is 'unknown', otherwise returns status
/// let status = nullif(item.status, "unknown");
/// # "####;
/// ```
#[allow(clippy::type_complexity)]
pub fn nullif<'a, V, E1, E2>(
    expr1: E1,
    expr2: E2,
) -> SQLExpr<'a, V, E1::SQLType, Null, <E1::Aggregate as AggOr<E2::Aggregate>>::Output>
where
    V: SQLParam + 'a,
    E1: Expr<'a, V>,
    E2: Expr<'a, V>,
    E1::SQLType: Compatible<E2::SQLType>,
    E1::Aggregate: AggOr<E2::Aggregate>,
    E2::Aggregate: AggregateKind,
{
    SQLExpr::new(SQL::func(
        "NULLIF",
        expr1
            .into_expr_sql()
            .push(Token::COMMA)
            .append(expr2.into_expr_sql()),
    ))
}

// =============================================================================
// IFNULL / NVL Function
// =============================================================================

/// IFNULL - SQLite/MySQL equivalent of COALESCE with two arguments.
///
/// Requires compatible types between the expression and default.
/// Returns the first argument if not NULL, otherwise returns the second.
#[allow(clippy::type_complexity)]
pub fn ifnull<'a, V, E, D, N>(
    expr: E,
    default: D,
) -> SQLExpr<'a, V, E::SQLType, N, <E::Aggregate as AggOr<D::Aggregate>>::Output>
where
    V: SQLParam + 'a,
    E: Expr<'a, V>,
    D: Expr<'a, V>,
    N: Nullability,
    E::SQLType: Compatible<D::SQLType>,
    E::Nullable: NullAnd<D::Nullable, Output = N>,
    D::Nullable: Nullability,
    E::Aggregate: AggOr<D::Aggregate>,
    D::Aggregate: AggregateKind,
{
    SQLExpr::new(SQL::func(
        "IFNULL",
        expr.into_expr_sql()
            .push(Token::COMMA)
            .append(default.into_expr_sql()),
    ))
}

// =============================================================================
// GREATEST / LEAST
// =============================================================================

/// Marker trait for dialects that support GREATEST/LEAST (`PostgreSQL`).
///
/// `SQLite` does not have `GREATEST`/`LEAST`. While its multi-argument
/// `MAX`/`MIN` are superficially similar, they have different NULL semantics:
/// `PostgreSQL` ignores NULLs (`GREATEST(1, NULL)` = `1`) while `SQLite`
/// propagates them (`MAX(1, NULL)` = `NULL`).
pub trait PostgresNullSupport {}
impl PostgresNullSupport for PostgresDialect {}

/// GREATEST - returns the largest of the given values (`PostgreSQL`).
///
/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
/// so `GREATEST(1, NULL)` returns `1`. The result is only NULL when all
/// inputs are NULL.
///
/// # Example
///
/// ```rust
/// # let _ = r####"
/// use drizzle_core::expr::greatest;
///
/// // Clamp to minimum of 0
/// let score = greatest(users.score, 0);
/// # "####;
/// ```
#[allow(clippy::type_complexity)]
pub fn greatest<'a, V, L, R>(
    left: L,
    right: R,
) -> SQLExpr<
    'a,
    V,
    L::SQLType,
    <L::Nullable as NullAnd<R::Nullable>>::Output,
    <L::Aggregate as AggOr<R::Aggregate>>::Output,
>
where
    V: SQLParam + 'a,
    V::DialectMarker: PostgresNullSupport,
    L: Expr<'a, V>,
    R: Expr<'a, V>,
    L::SQLType: Compatible<R::SQLType>,
    L::Nullable: NullAnd<R::Nullable>,
    R::Nullable: Nullability,
    L::Aggregate: AggOr<R::Aggregate>,
    R::Aggregate: AggregateKind,
{
    SQLExpr::new(SQL::func(
        "GREATEST",
        left.into_expr_sql()
            .push(Token::COMMA)
            .append(right.into_expr_sql()),
    ))
}

/// LEAST - returns the smallest of the given values (`PostgreSQL`).
///
/// Both arguments must have compatible types. `PostgreSQL` ignores NULL inputs,
/// so `LEAST(1, NULL)` returns `1`. The result is only NULL when all
/// inputs are NULL.
///
/// # Example
///
/// ```rust
/// # let _ = r####"
/// use drizzle_core::expr::least;
///
/// // Cap at maximum of 100
/// let score = least(users.score, 100);
/// # "####;
/// ```
#[allow(clippy::type_complexity)]
pub fn least<'a, V, L, R>(
    left: L,
    right: R,
) -> SQLExpr<
    'a,
    V,
    L::SQLType,
    <L::Nullable as NullAnd<R::Nullable>>::Output,
    <L::Aggregate as AggOr<R::Aggregate>>::Output,
>
where
    V: SQLParam + 'a,
    V::DialectMarker: PostgresNullSupport,
    L: Expr<'a, V>,
    R: Expr<'a, V>,
    L::SQLType: Compatible<R::SQLType>,
    L::Nullable: NullAnd<R::Nullable>,
    R::Nullable: Nullability,
    L::Aggregate: AggOr<R::Aggregate>,
    R::Aggregate: AggregateKind,
{
    SQLExpr::new(SQL::func(
        "LEAST",
        left.into_expr_sql()
            .push(Token::COMMA)
            .append(right.into_expr_sql()),
    ))
}