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}