Skip to main content

inillucent_sql/bind/
column_affinity.rs

1//! The affinity a derived table's or a scalar subquery's column has when a
2//! comparison reads it.
3//!
4//! Invariant: **a column of a compound `SELECT` has an affinity only when every
5//! arm agrees about what the column holds, and an arm that disagrees takes the
6//! affinity away from all of them.** The rules are measured against 3.53.4 with
7//! a table of every pair of arm kinds, not read from its source:
8//!
9//! - one arm: the affinity of its expression, which is the declared affinity
10//!   for a column, a `CAST` and the rowid, and none for anything else;
11//! - several arms: every arm is classified as text, numeric, unknown or NULL.
12//!   A column of `TEXT` affinity is text, one of `INTEGER`, `REAL` or `NUMERIC`
13//!   affinity is numeric, one with no declared type is unknown. An expression
14//!   with no affinity is classified by what it produces: a string literal and
15//!   `||` are text, a number and arithmetic are numeric, a function call is
16//!   unknown. NULL agrees with everything. When all arms have the same class and
17//!   at least one of them has a real affinity, the column has that class's
18//!   affinity. Otherwise it has none;
19//! - a scalar subquery has the affinity of its first result column.
20//!
21//! So `SELECT a FROM t UNION ALL SELECT b FROM u` with `a TEXT` and `b INTEGER`
22//! has no affinity, and `x = 1` over it is true for the integer row and false
23//! for the text row `'1'`. Taking the first arm's affinity, which this file
24//! replaces, made it true for both.
25
26use inillucent_value::{Affinity, Collation};
27
28use super::{BoundExpr, BoundSelect, SubqueryKind};
29use crate::ast::{BinaryOp, UnaryOp};
30
31/// What an arm's column holds, as far as the compound's affinity is concerned.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33enum Held {
34    /// Text values.
35    Text,
36    /// Integer or real values.
37    Numeric,
38    /// Values of no particular class, which disagree with every other class.
39    Unknown,
40    /// NULL, which agrees with every class.
41    Null,
42}
43
44impl BoundSelect {
45    /// Returns the affinity a reader of result column `index` applies, with
46    /// "none" reported as `Affinity::Blob` the way a column with no declared
47    /// type reports it.
48    ///
49    /// @param index - which result column
50    pub fn column_affinity(&self, index: usize) -> Affinity {
51        self.column_affinity_if_any(index).unwrap_or(Affinity::Blob)
52    }
53
54    /// Returns the affinity of result column `index`, or `None` when no arm of
55    /// the block has an affinity of any kind.
56    ///
57    /// `None` and `Some(Affinity::Blob)` are different answers: the first is an
58    /// expression with nothing declared, the second is a column or a clash.
59    /// A comparison treats both as "apply nothing", but an enclosing compound
60    /// does not.
61    ///
62    /// @param index - which result column
63    pub fn column_affinity_if_any(&self, index: usize) -> Option<Affinity> {
64        // **A `VALUES` list of several rows joined to another arm leaves the
65        // column with no affinity**, whatever the other arm declares:
66        // `SELECT n FROM t UNION VALUES (1), (2)` over an INTEGER `n` compares
67        // as a column of no affinity, where `UNION VALUES (1)` keeps INTEGER.
68        let several_rows = |block: &BoundSelect| block.values.len() > 1;
69        if !self.compounds.is_empty()
70            && (several_rows(self) || self.compounds.iter().any(|(_, arm)| several_rows(arm)))
71        {
72            return Some(Affinity::Blob);
73        }
74        let mut arms: Vec<&BoundExpr> = Vec::new();
75        self.collect_arm_columns(index, &mut arms);
76        compound_affinity(&arms)
77    }
78
79    /// Returns the collation result column `index` of this block, with every
80    /// compound arm, compares with.
81    ///
82    /// SQLite reads it from the leftmost arm whose expression has a collation
83    /// of its own, so a derived table over `SELECT a, b FROM t1 UNION ALL
84    /// SELECT c, d FROM t2` with a NOCASE `t1.b` has a NOCASE `b`. Reading the
85    /// first arm alone gave a `VALUES` first arm or a literal no collation at
86    /// all, where the arm to its right should have supplied one.
87    ///
88    /// @param index - which result column
89    pub fn column_collation(&self, index: usize) -> Collation {
90        let mut arms: Vec<&BoundExpr> = Vec::new();
91        self.collect_arm_columns(index, &mut arms);
92        super::set_rules::compound_collation(arms)
93    }
94
95    /// Returns the affinity a scalar subquery has as an operand.
96    ///
97    /// **The last arm decides, not the combination of all of them.** SQLite
98    /// reads the first result column of the statement it holds for the
99    /// subquery, and for a compound that statement is the last arm. Measured
100    /// with `(SELECT a FROM t UNION ALL SELECT b FROM u LIMIT 1) = 1.0` over a
101    /// TEXT `a` and an INTEGER `b`: the text row `'1'` is equal to `1.0`,
102    /// which only the INTEGER arm's affinity gives, and the same arms written
103    /// in the other order and compared with `'1.0'` answer false.
104    pub fn scalar_affinity(&self) -> Option<Affinity> {
105        let last = self.compounds.last().map_or(self, |(_, arm)| arm);
106        if last.values.is_empty() {
107            last.columns
108                .first()
109                .and_then(|column| column.expr.affinity())
110        } else {
111            None
112        }
113    }
114
115    /// Adds result column `index` of this block and of each later arm.
116    ///
117    /// A `VALUES` list contributes one expression per row, because SQLite reads
118    /// each row as an arm of a compound.
119    ///
120    /// @param index - which result column
121    /// @param into - the expressions found, first arm first
122    fn collect_arm_columns<'a>(&'a self, index: usize, into: &mut Vec<&'a BoundExpr>) {
123        if self.values.is_empty() {
124            into.extend(self.columns.get(index).map(|column| &column.expr));
125        } else {
126            into.extend(self.values.iter().filter_map(|row| row.get(index)));
127        }
128        for (_, arm) in &self.compounds {
129            arm.collect_arm_columns(index, into);
130        }
131    }
132}
133
134/// Combines the arms of a compound into the affinity of their column.
135///
136/// @param arms - the expression each arm puts in the column, first arm first
137pub(super) fn compound_affinity(arms: &[&BoundExpr]) -> Option<Affinity> {
138    if let [only] = arms {
139        return only.affinity();
140    }
141    let bearing: Vec<Affinity> = arms.iter().filter_map(|arm| arm.affinity()).collect();
142    let first = *bearing.first()?;
143    let mut classes = arms
144        .iter()
145        .map(|arm| held_by(arm))
146        .filter(|held| *held != Held::Null);
147    let agreed = classes.next()?;
148    if agreed == Held::Unknown || classes.any(|held| held != agreed) {
149        return Some(Affinity::Blob);
150    }
151    Some(match agreed {
152        Held::Text => Affinity::Text,
153        _ if bearing.iter().all(|affinity| *affinity == first) => first,
154        _ => Affinity::Numeric,
155    })
156}
157
158/// Classifies what one arm's expression puts in the column.
159///
160/// @param expr - the arm's result expression
161fn held_by(expr: &BoundExpr) -> Held {
162    if let Some(affinity) = expr.affinity() {
163        return match affinity {
164            Affinity::Text => Held::Text,
165            Affinity::Blob => Held::Unknown,
166            numeric => {
167                debug_assert!(numeric.is_numeric());
168                Held::Numeric
169            }
170        };
171    }
172    match expr {
173        BoundExpr::Null => Held::Null,
174        BoundExpr::Integer(_) | BoundExpr::Real(_) => Held::Numeric,
175        BoundExpr::Text(_) => Held::Text,
176        BoundExpr::Arithmetic { op, .. } => held_by_operator(*op),
177        BoundExpr::Compare { .. }
178        | BoundExpr::Is { .. }
179        | BoundExpr::IsNull { .. }
180        | BoundExpr::And(_, _)
181        | BoundExpr::Or(_, _)
182        | BoundExpr::Not(_)
183        | BoundExpr::Between { .. }
184        | BoundExpr::InList { .. }
185        | BoundExpr::Pattern { .. } => Held::Numeric,
186        BoundExpr::Subquery {
187            kind: SubqueryKind::Exists | SubqueryKind::In,
188            ..
189        } => Held::Numeric,
190        BoundExpr::Unary {
191            op: UnaryOp::Identity,
192            operand,
193        } => held_by(operand),
194        BoundExpr::Unary { .. } => Held::Numeric,
195        BoundExpr::Collate { operand, .. } => held_by(operand),
196        BoundExpr::Case {
197            branches,
198            otherwise,
199            ..
200        } => held_by_case(branches, otherwise.as_deref()),
201        _ => Held::Unknown,
202    }
203}
204
205/// Classifies the result of a binary operator.
206///
207/// @param op - the operator
208fn held_by_operator(op: BinaryOp) -> Held {
209    match op {
210        BinaryOp::Concat => Held::Text,
211        BinaryOp::Add
212        | BinaryOp::Subtract
213        | BinaryOp::Multiply
214        | BinaryOp::Divide
215        | BinaryOp::Modulo
216        | BinaryOp::BitAnd
217        | BinaryOp::BitOr
218        | BinaryOp::ShiftLeft
219        | BinaryOp::ShiftRight => Held::Numeric,
220        _ => Held::Unknown,
221    }
222}
223
224/// Classifies a `CASE` by the values its branches and its `ELSE` produce.
225///
226/// @param branches - the `WHEN` and `THEN` pairs
227/// @param otherwise - the `ELSE` arm, when written
228fn held_by_case(branches: &[(BoundExpr, BoundExpr)], otherwise: Option<&BoundExpr>) -> Held {
229    let mut classes = branches
230        .iter()
231        .map(|(_, then)| held_by(then))
232        .chain(otherwise.map(held_by))
233        .filter(|held| *held != Held::Null);
234    let Some(first) = classes.next() else {
235        return Held::Null;
236    };
237    if classes.all(|held| held == first) {
238        first
239    } else {
240        Held::Unknown
241    }
242}