use inillucent_value::Affinity;
use super::{BoundExpr, BoundSelect, SubqueryKind};
use crate::ast::{BinaryOp, UnaryOp};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Held {
Text,
Numeric,
Unknown,
Null,
}
impl BoundSelect {
pub fn column_affinity(&self, index: usize) -> Affinity {
self.column_affinity_if_any(index).unwrap_or(Affinity::Blob)
}
pub fn column_affinity_if_any(&self, index: usize) -> Option<Affinity> {
let several_rows = |block: &BoundSelect| block.values.len() > 1;
if !self.compounds.is_empty()
&& (several_rows(self) || self.compounds.iter().any(|(_, arm)| several_rows(arm)))
{
return Some(Affinity::Blob);
}
let mut arms: Vec<&BoundExpr> = Vec::new();
self.collect_arm_columns(index, &mut arms);
compound_affinity(&arms)
}
pub fn scalar_affinity(&self) -> Option<Affinity> {
let last = self.compounds.last().map_or(self, |(_, arm)| arm);
if last.values.is_empty() {
last.columns
.first()
.and_then(|column| column.expr.affinity())
} else {
None
}
}
fn collect_arm_columns<'a>(&'a self, index: usize, into: &mut Vec<&'a BoundExpr>) {
if self.values.is_empty() {
into.extend(self.columns.get(index).map(|column| &column.expr));
} else {
into.extend(self.values.iter().filter_map(|row| row.get(index)));
}
for (_, arm) in &self.compounds {
arm.collect_arm_columns(index, into);
}
}
}
pub(super) fn compound_affinity(arms: &[&BoundExpr]) -> Option<Affinity> {
if let [only] = arms {
return only.affinity();
}
let bearing: Vec<Affinity> = arms.iter().filter_map(|arm| arm.affinity()).collect();
let first = *bearing.first()?;
let mut classes = arms
.iter()
.map(|arm| held_by(arm))
.filter(|held| *held != Held::Null);
let agreed = classes.next()?;
if agreed == Held::Unknown || classes.any(|held| held != agreed) {
return Some(Affinity::Blob);
}
Some(match agreed {
Held::Text => Affinity::Text,
_ if bearing.iter().all(|affinity| *affinity == first) => first,
_ => Affinity::Numeric,
})
}
fn held_by(expr: &BoundExpr) -> Held {
if let Some(affinity) = expr.affinity() {
return match affinity {
Affinity::Text => Held::Text,
Affinity::Blob => Held::Unknown,
numeric => {
debug_assert!(numeric.is_numeric());
Held::Numeric
}
};
}
match expr {
BoundExpr::Null => Held::Null,
BoundExpr::Integer(_) | BoundExpr::Real(_) => Held::Numeric,
BoundExpr::Text(_) => Held::Text,
BoundExpr::Arithmetic { op, .. } => held_by_operator(*op),
BoundExpr::Compare { .. }
| BoundExpr::Is { .. }
| BoundExpr::IsNull { .. }
| BoundExpr::And(_, _)
| BoundExpr::Or(_, _)
| BoundExpr::Not(_)
| BoundExpr::Between { .. }
| BoundExpr::InList { .. }
| BoundExpr::Pattern { .. } => Held::Numeric,
BoundExpr::Subquery {
kind: SubqueryKind::Exists | SubqueryKind::In,
..
} => Held::Numeric,
BoundExpr::Unary {
op: UnaryOp::Identity,
operand,
} => held_by(operand),
BoundExpr::Unary { .. } => Held::Numeric,
BoundExpr::Collate { operand, .. } => held_by(operand),
BoundExpr::Case {
branches,
otherwise,
..
} => held_by_case(branches, otherwise.as_deref()),
_ => Held::Unknown,
}
}
fn held_by_operator(op: BinaryOp) -> Held {
match op {
BinaryOp::Concat => Held::Text,
BinaryOp::Add
| BinaryOp::Subtract
| BinaryOp::Multiply
| BinaryOp::Divide
| BinaryOp::Modulo
| BinaryOp::BitAnd
| BinaryOp::BitOr
| BinaryOp::ShiftLeft
| BinaryOp::ShiftRight => Held::Numeric,
_ => Held::Unknown,
}
}
fn held_by_case(branches: &[(BoundExpr, BoundExpr)], otherwise: Option<&BoundExpr>) -> Held {
let mut classes = branches
.iter()
.map(|(_, then)| held_by(then))
.chain(otherwise.map(held_by))
.filter(|held| *held != Held::Null);
let Some(first) = classes.next() else {
return Held::Null;
};
if classes.all(|held| held == first) {
first
} else {
Held::Unknown
}
}