use either::Either;
use turso_parser::ast::{Expr, Literal};
use crate::{
numeric::{format_float, DoubleDouble, Numeric},
types::AsValueRef,
Value, ValueRef,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Affinity {
Blob = 0,
Text = 1,
Numeric = 2,
Integer = 3,
Real = 4,
}
pub const SQLITE_AFF_NONE: char = 'A'; pub const SQLITE_AFF_TEXT: char = 'B';
pub const SQLITE_AFF_NUMERIC: char = 'C';
pub const SQLITE_AFF_INTEGER: char = 'D';
pub const SQLITE_AFF_REAL: char = 'E';
impl Affinity {
pub fn aff_mask(&self) -> char {
match self {
Affinity::Integer => SQLITE_AFF_INTEGER,
Affinity::Text => SQLITE_AFF_TEXT,
Affinity::Blob => SQLITE_AFF_NONE,
Affinity::Real => SQLITE_AFF_REAL,
Affinity::Numeric => SQLITE_AFF_NUMERIC,
}
}
pub fn from_char(char: char) -> Self {
match char {
SQLITE_AFF_INTEGER => Affinity::Integer,
SQLITE_AFF_TEXT => Affinity::Text,
SQLITE_AFF_NONE => Affinity::Blob,
SQLITE_AFF_REAL => Affinity::Real,
SQLITE_AFF_NUMERIC => Affinity::Numeric,
_ => Affinity::Blob,
}
}
pub fn as_char_code(&self) -> u8 {
self.aff_mask() as u8
}
pub fn from_char_code(code: u8) -> Self {
Self::from_char(code as char)
}
pub fn is_numeric(&self) -> bool {
matches!(self, Affinity::Integer | Affinity::Real | Affinity::Numeric)
}
pub fn has_affinity(&self) -> bool {
!matches!(self, Affinity::Blob)
}
pub fn short_type_name(&self) -> &'static str {
match self {
Affinity::Blob => "",
Affinity::Text => "TEXT",
Affinity::Numeric => "NUM",
Affinity::Integer => "INT",
Affinity::Real => "REAL",
}
}
#[expect(clippy::self_named_constructors)]
pub fn affinity(datatype: &str) -> Self {
let datatype = datatype.to_ascii_uppercase();
if datatype.contains("INT") {
return Affinity::Integer;
}
if datatype.contains("CHAR") || datatype.contains("CLOB") || datatype.contains("TEXT") {
return Affinity::Text;
}
if datatype.contains("BLOB") || datatype.is_empty() {
return Affinity::Blob;
}
if datatype.contains("REAL") || datatype.contains("FLOA") || datatype.contains("DOUB") {
return Affinity::Real;
}
Affinity::Numeric
}
pub fn convert<'a>(&self, val: &'a impl AsValueRef) -> Option<Either<ValueRef<'a>, Value>> {
let val = val.as_value_ref();
let is_text = matches!(val, ValueRef::Text(_));
match self {
Affinity::Numeric | Affinity::Integer => is_text
.then(|| apply_numeric_affinity(val, false))
.flatten()
.map(Either::Left),
Affinity::Text => {
match val {
ValueRef::Numeric(Numeric::Integer(i)) => {
Some(Either::Right(Value::Text(i.to_string().into())))
}
ValueRef::Numeric(Numeric::Float(f)) => Some(Either::Right(Value::Text(
format_float(f64::from(f)).into(),
))),
ValueRef::Text(_) => {
if is_numeric_value(val) {
stringify_register(val).map(Either::Right)
} else {
None }
}
_ => None, }
}
Affinity::Real => {
let mut left = is_text
.then(|| apply_numeric_affinity(val, false))
.flatten();
if let ValueRef::Numeric(Numeric::Integer(i)) = left.unwrap_or(val) {
left = Some(ValueRef::from_f64(i as f64));
}
left.map(Either::Left)
}
Affinity::Blob => None, }
}
pub fn convert_for_compare<'a>(
&self,
val: &'a impl AsValueRef,
) -> Option<Either<ValueRef<'a>, Value>> {
let val_ref = val.as_value_ref();
let is_text = matches!(val_ref, ValueRef::Text(_));
match self {
Affinity::Numeric | Affinity::Integer | Affinity::Real => is_text
.then(|| apply_numeric_affinity(val_ref, false))
.flatten()
.map(Either::Left),
Affinity::Text => self.convert(val),
Affinity::Blob => None,
}
}
pub fn expr_needs_no_affinity_change(&self, expr: &Expr) -> bool {
if !self.has_affinity() {
return true;
}
match expr {
Expr::Literal(literal) => match literal {
Literal::Numeric(_) => self.is_numeric(),
Literal::String(_) => matches!(self, Affinity::Text),
Literal::Blob(_) => true,
_ => false,
},
Expr::Column {
is_rowid_alias: true,
..
} => self.is_numeric(),
_ => false,
}
}
}
#[derive(Debug, PartialEq)]
pub enum NumericParseResult {
NotNumeric, PureInteger, HasDecimalOrExp, ValidPrefixOnly, }
#[derive(Debug)]
pub enum ParsedNumber {
None,
Integer(i64),
Float(f64),
}
impl ParsedNumber {
fn as_integer(&self) -> Option<i64> {
match self {
ParsedNumber::Integer(i) => Some(*i),
_ => None,
}
}
fn as_float(&self) -> Option<f64> {
match self {
ParsedNumber::Float(f) => Some(*f),
_ => None,
}
}
}
pub fn try_for_float(bytes: &[u8]) -> (NumericParseResult, ParsedNumber) {
if bytes.is_empty() {
return (NumericParseResult::NotNumeric, ParsedNumber::None);
}
let mut pos = 0;
let len = bytes.len();
while pos < len && is_space(bytes[pos]) {
pos += 1;
}
if pos >= len {
return (NumericParseResult::NotNumeric, ParsedNumber::None);
}
let mut sign = 1i64;
if bytes[pos] == b'-' {
sign = -1;
pos += 1;
} else if bytes[pos] == b'+' {
pos += 1;
}
if pos >= len {
return (NumericParseResult::NotNumeric, ParsedNumber::None);
}
let mut significand = 0u64;
let mut decimal_adjust = 0i32;
let mut has_digits = false;
while pos < len && bytes[pos].is_ascii_digit() {
has_digits = true;
let digit = (bytes[pos] - b'0') as u64;
if significand <= (u64::MAX - 9) / 10 {
significand = significand * 10 + digit;
} else {
decimal_adjust += 1;
}
pos += 1;
}
let mut has_decimal = false;
let mut has_exponent = false;
if pos < len && bytes[pos] == b'.' {
has_decimal = true;
pos += 1;
while pos < len && bytes[pos].is_ascii_digit() {
has_digits = true;
let digit = (bytes[pos] - b'0') as u64;
if significand <= (u64::MAX - 9) / 10 {
significand = significand * 10 + digit;
decimal_adjust -= 1;
}
pos += 1;
}
}
if !has_digits {
return (NumericParseResult::NotNumeric, ParsedNumber::None);
}
let mut exponent = 0i32;
if pos < len && (bytes[pos] == b'e' || bytes[pos] == b'E') {
has_exponent = true;
pos += 1;
if pos >= len {
return create_result_from_significand(
significand,
sign,
decimal_adjust,
has_decimal,
has_exponent,
NumericParseResult::ValidPrefixOnly,
);
}
let mut exp_sign = 1i32;
if bytes[pos] == b'-' {
exp_sign = -1;
pos += 1;
} else if bytes[pos] == b'+' {
pos += 1;
}
if pos >= len || !bytes[pos].is_ascii_digit() {
return create_result_from_significand(
significand,
sign,
decimal_adjust,
has_decimal,
false,
NumericParseResult::ValidPrefixOnly,
);
}
while pos < len && bytes[pos].is_ascii_digit() {
let digit = (bytes[pos] - b'0') as i32;
if exponent < 10000 {
exponent = exponent * 10 + digit;
} else {
exponent = 10000; }
pos += 1;
}
exponent *= exp_sign;
}
while pos < len && is_space(bytes[pos]) {
pos += 1;
}
let consumed_all = pos >= len;
let final_exponent = decimal_adjust + exponent;
let parse_result = if !consumed_all {
NumericParseResult::ValidPrefixOnly
} else if has_decimal || has_exponent {
NumericParseResult::HasDecimalOrExp
} else {
NumericParseResult::PureInteger
};
create_result_from_significand(
significand,
sign,
final_exponent,
has_decimal,
has_exponent,
parse_result,
)
}
fn create_result_from_significand(
significand: u64,
sign: i64,
exponent: i32,
has_decimal: bool,
has_exponent: bool,
parse_result: NumericParseResult,
) -> (NumericParseResult, ParsedNumber) {
if significand == 0 {
match parse_result {
NumericParseResult::PureInteger => {
return (parse_result, ParsedNumber::Integer(0));
}
_ => {
return (parse_result, ParsedNumber::Float(0.0));
}
}
}
if !has_decimal && !has_exponent && exponent == 0 && significand <= i64::MAX as u64 {
let signed_val = (significand as i64).wrapping_mul(sign);
return (parse_result, ParsedNumber::Integer(signed_val));
}
let mut result = DoubleDouble::from(significand);
let mut exp = exponent;
match exp.cmp(&0) {
std::cmp::Ordering::Greater => {
while exp >= 100 {
result *= DoubleDouble::E100;
exp -= 100;
}
while exp >= 10 {
result *= DoubleDouble::E10;
exp -= 10;
}
while exp >= 1 {
result *= DoubleDouble::E1;
exp -= 1;
}
}
std::cmp::Ordering::Less => {
while exp <= -100 {
result *= DoubleDouble::NEG_E100;
exp += 100;
}
while exp <= -10 {
result *= DoubleDouble::NEG_E10;
exp += 10;
}
while exp <= -1 {
result *= DoubleDouble::NEG_E1;
exp += 1;
}
}
std::cmp::Ordering::Equal => {}
}
let mut final_result: f64 = result.into();
if final_result.is_nan() {
final_result = f64::INFINITY;
}
if sign < 0 {
final_result = -final_result;
}
(parse_result, ParsedNumber::Float(final_result))
}
pub fn is_space(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c')
}
pub(crate) fn real_to_i64(r: f64) -> i64 {
if r < -9223372036854774784.0 {
i64::MIN
} else if r > 9223372036854774784.0 {
i64::MAX
} else {
r as i64
}
}
fn apply_integer_affinity(val: ValueRef) -> Option<ValueRef> {
let ValueRef::Numeric(Numeric::Float(nn)) = val else {
return None;
};
let f: f64 = nn.into();
let ix = real_to_i64(f);
if f == (ix as f64) && ix > i64::MIN && ix < i64::MAX {
Some(ValueRef::Numeric(Numeric::Integer(ix)))
} else {
None
}
}
pub fn apply_numeric_affinity(val: ValueRef, try_for_int: bool) -> Option<ValueRef> {
let ValueRef::Text(text) = val else {
return None; };
let text_str = text.as_str();
let (parse_result, parsed_value) = try_for_float(text_str.as_bytes());
match parse_result {
NumericParseResult::NotNumeric | NumericParseResult::ValidPrefixOnly => {
None }
NumericParseResult::PureInteger => {
if let Some(int_val) = parsed_value.as_integer() {
Some(ValueRef::Numeric(Numeric::Integer(int_val)))
} else if let Some(float_val) = parsed_value.as_float() {
let res = ValueRef::from_f64(float_val);
if try_for_int {
apply_integer_affinity(res)
} else {
Some(res)
}
} else {
None
}
}
NumericParseResult::HasDecimalOrExp => {
if let Some(float_val) = parsed_value.as_float() {
if float_val.is_nan() {
return None;
}
let res = ValueRef::from_f64(float_val);
if try_for_int {
apply_integer_affinity(res)
} else {
Some(res)
}
} else {
None
}
}
}
}
fn is_numeric_value(val: ValueRef) -> bool {
matches!(val, ValueRef::Numeric(_))
}
fn stringify_register(val: ValueRef) -> Option<Value> {
match val {
ValueRef::Numeric(Numeric::Integer(i)) => Some(Value::build_text(i.to_string())),
ValueRef::Numeric(Numeric::Float(f)) => Some(Value::build_text(f64::from(f).to_string())),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_numeric_affinity_partial_numbers() {
let val = Value::Text("123abc".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert!(res.is_none());
let val = Value::Text("-53093015420544-15062897".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert!(res.is_none());
let val = Value::Text("123.45xyz".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert!(res.is_none());
}
#[test]
fn test_apply_numeric_affinity_complete_numbers() {
let val = Value::Text("123".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(123))));
let val = Value::Text("123.45".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert_eq!(res, Some(ValueRef::from_f64(123.45)));
let val = Value::Text(" -456 ".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(-456))));
let val = Value::Text("0".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(0))));
}
#[test]
fn test_apply_numeric_affinity_extreme_exponent_gives_infinity() {
let val = Value::Text("3139353734372E383932303939343135".into());
let res = apply_numeric_affinity(val.as_value_ref(), false);
assert!(res.is_some());
match res.unwrap() {
ValueRef::Numeric(Numeric::Float(f)) => assert!(f64::from(f).is_infinite()),
other => panic!("expected Float, got {other:?}"),
}
}
#[test]
fn test_try_for_float_precision() {
let (_, parsed) = try_for_float(b"12345678901234567e-5");
let expected: f64 = "12345678901234567e-5".parse().unwrap();
assert_eq!(
parsed.as_float().unwrap().to_bits(),
expected.to_bits(),
"try_for_float precision mismatch: got {}, expected {expected}",
parsed.as_float().unwrap(),
);
}
}