inillucent-sql 2.4.2

First-party lexer, parser, AST, binder, semantic rewrites, and logical and physical plans.
Documentation
//! The affinity and collation a comparison between two operands uses.
//!
//! Invariant: **a comparison applies the left operand's affinity first and an
//! explicit collation before an implicit one, as `sqlite3BinaryCompareCollSeq`
//! does, and this is the only place those two answers are combined.** The
//! functions moved here from `collation.rs` to keep that file under its
//! recorded size.

use inillucent_value::{Affinity, Collation};

use super::{BoundExpr, BoundSource, SourceRows};

/// Returns the affinity and collation a comparison between two operands uses.
///
/// SQLite's rule, in order: if either side has a column affinity the comparison
/// applies it, with the left side winning. The collation is an explicit one on
/// the left operand, then an explicit one on the right, then the left
/// operand's implicit one, then the right's, and otherwise BINARY. "On an
/// operand" includes anywhere inside it: see [`BoundExpr::explicit_collation`].
///
/// @param left - the comparison's left operand
/// @param right - the comparison's right operand
pub fn comparison_rules(left: &BoundExpr, right: &BoundExpr) -> (Option<Affinity>, Collation) {
    comparison_rules_over(left, right, left.affinity(), right.affinity())
}

/// The rules of [`comparison_rules`] for operands whose affinities the caller
/// has already decided.
///
/// **A column of a derived table whose query gave it no affinity has none.** The
/// binder stores that as BLOB, which is also what a declared column with no
/// type has, and the two compare differently: a TEXT column meets the first with
/// TEXT affinity applied to it, and meets the second with nothing converted.
///
/// @param left - the comparison's left operand
/// @param right - the comparison's right operand
/// @param left_affinity - the left operand's affinity as the comparison sees it
/// @param right_affinity - the right operand's affinity as the comparison sees it
pub fn comparison_rules_over(
    left: &BoundExpr,
    right: &BoundExpr,
    left_affinity: Option<Affinity>,
    right_affinity: Option<Affinity>,
) -> (Option<Affinity>, Collation) {
    let affinity = match (left_affinity, right_affinity) {
        (Some(left), Some(right)) => inillucent_value::compare::comparison_affinity(left, right),
        (Some(left), None) => Some(left),
        (None, Some(right)) => Some(right),
        (None, None) => None,
    };
    let collation = left
        .explicit_collation()
        .or_else(|| right.explicit_collation())
        .or_else(|| left.collation())
        .or_else(|| right.collation())
        .unwrap_or(Collation::Binary);
    (affinity, collation)
}

/// Returns the affinity an operand has in a comparison, reading a column of a
/// derived table, a view or a CTE that carries no affinity as having none.
///
/// A declared column with no type is stored as BLOB and the comparison applies
/// nothing to it. A derived column no arm gave an affinity is also stored as
/// BLOB, but SQLite treats it as having none, so the other operand's affinity
/// applies to it.
///
/// @param sources - the FROM terms the operand's column number is looked up in
/// @param expr - the operand
pub(super) fn seen_affinity_among(sources: &[BoundSource], expr: &BoundExpr) -> Option<Affinity> {
    if let BoundExpr::Column {
        source,
        column,
        affinity: Affinity::Blob,
        ..
    } = expr
    {
        let nothing =
            sources
                .iter()
                .find(|held| held.id == *source)
                .is_some_and(|held| match &held.rows {
                    SourceRows::Subquery(block) => {
                        block.column_affinity_if_any(usize::from(*column)).is_none()
                    }
                    _ => false,
                });
        if nothing {
            return None;
        }
    }
    expr.affinity()
}