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}