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;
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 affinity a scalar subquery has as an operand.
80    ///
81    /// **The last arm decides, not the combination of all of them.** SQLite
82    /// reads the first result column of the statement it holds for the
83    /// subquery, and for a compound that statement is the last arm. Measured
84    /// with `(SELECT a FROM t UNION ALL SELECT b FROM u LIMIT 1) = 1.0` over a
85    /// TEXT `a` and an INTEGER `b`: the text row `'1'` is equal to `1.0`,
86    /// which only the INTEGER arm's affinity gives, and the same arms written
87    /// in the other order and compared with `'1.0'` answer false.
88    pub fn scalar_affinity(&self) -> Option<Affinity> {
89        let last = self.compounds.last().map_or(self, |(_, arm)| arm);
90        if last.values.is_empty() {
91            last.columns
92                .first()
93                .and_then(|column| column.expr.affinity())
94        } else {
95            None
96        }
97    }
98
99    /// Adds result column `index` of this block and of each later arm.
100    ///
101    /// A `VALUES` list contributes one expression per row, because SQLite reads
102    /// each row as an arm of a compound.
103    ///
104    /// @param index - which result column
105    /// @param into - the expressions found, first arm first
106    fn collect_arm_columns<'a>(&'a self, index: usize, into: &mut Vec<&'a BoundExpr>) {
107        if self.values.is_empty() {
108            into.extend(self.columns.get(index).map(|column| &column.expr));
109        } else {
110            into.extend(self.values.iter().filter_map(|row| row.get(index)));
111        }
112        for (_, arm) in &self.compounds {
113            arm.collect_arm_columns(index, into);
114        }
115    }
116}
117
118/// Combines the arms of a compound into the affinity of their column.
119///
120/// @param arms - the expression each arm puts in the column, first arm first
121pub(super) fn compound_affinity(arms: &[&BoundExpr]) -> Option<Affinity> {
122    if let [only] = arms {
123        return only.affinity();
124    }
125    let bearing: Vec<Affinity> = arms.iter().filter_map(|arm| arm.affinity()).collect();
126    let first = *bearing.first()?;
127    let mut classes = arms
128        .iter()
129        .map(|arm| held_by(arm))
130        .filter(|held| *held != Held::Null);
131    let agreed = classes.next()?;
132    if agreed == Held::Unknown || classes.any(|held| held != agreed) {
133        return Some(Affinity::Blob);
134    }
135    Some(match agreed {
136        Held::Text => Affinity::Text,
137        _ if bearing.iter().all(|affinity| *affinity == first) => first,
138        _ => Affinity::Numeric,
139    })
140}
141
142/// Classifies what one arm's expression puts in the column.
143///
144/// @param expr - the arm's result expression
145fn held_by(expr: &BoundExpr) -> Held {
146    if let Some(affinity) = expr.affinity() {
147        return match affinity {
148            Affinity::Text => Held::Text,
149            Affinity::Blob => Held::Unknown,
150            numeric => {
151                debug_assert!(numeric.is_numeric());
152                Held::Numeric
153            }
154        };
155    }
156    match expr {
157        BoundExpr::Null => Held::Null,
158        BoundExpr::Integer(_) | BoundExpr::Real(_) => Held::Numeric,
159        BoundExpr::Text(_) => Held::Text,
160        BoundExpr::Arithmetic { op, .. } => held_by_operator(*op),
161        BoundExpr::Compare { .. }
162        | BoundExpr::Is { .. }
163        | BoundExpr::IsNull { .. }
164        | BoundExpr::And(_, _)
165        | BoundExpr::Or(_, _)
166        | BoundExpr::Not(_)
167        | BoundExpr::Between { .. }
168        | BoundExpr::InList { .. }
169        | BoundExpr::Pattern { .. } => Held::Numeric,
170        BoundExpr::Subquery {
171            kind: SubqueryKind::Exists | SubqueryKind::In,
172            ..
173        } => Held::Numeric,
174        BoundExpr::Unary {
175            op: UnaryOp::Identity,
176            operand,
177        } => held_by(operand),
178        BoundExpr::Unary { .. } => Held::Numeric,
179        BoundExpr::Collate { operand, .. } => held_by(operand),
180        BoundExpr::Case {
181            branches,
182            otherwise,
183            ..
184        } => held_by_case(branches, otherwise.as_deref()),
185        _ => Held::Unknown,
186    }
187}
188
189/// Classifies the result of a binary operator.
190///
191/// @param op - the operator
192fn held_by_operator(op: BinaryOp) -> Held {
193    match op {
194        BinaryOp::Concat => Held::Text,
195        BinaryOp::Add
196        | BinaryOp::Subtract
197        | BinaryOp::Multiply
198        | BinaryOp::Divide
199        | BinaryOp::Modulo
200        | BinaryOp::BitAnd
201        | BinaryOp::BitOr
202        | BinaryOp::ShiftLeft
203        | BinaryOp::ShiftRight => Held::Numeric,
204        _ => Held::Unknown,
205    }
206}
207
208/// Classifies a `CASE` by the values its branches and its `ELSE` produce.
209///
210/// @param branches - the `WHEN` and `THEN` pairs
211/// @param otherwise - the `ELSE` arm, when written
212fn held_by_case(branches: &[(BoundExpr, BoundExpr)], otherwise: Option<&BoundExpr>) -> Held {
213    let mut classes = branches
214        .iter()
215        .map(|(_, then)| held_by(then))
216        .chain(otherwise.map(held_by))
217        .filter(|held| *held != Held::Null);
218    let Some(first) = classes.next() else {
219        return Held::Null;
220    };
221    if classes.all(|held| held == first) {
222        first
223    } else {
224        Held::Unknown
225    }
226}