diff --git a/core/numeric/mod.rs b/core/numeric/mod.rs index c0abc0e85..ba5ca86b9 100644 --- a/core/numeric/mod.rs +++ b/core/numeric/mod.rs @@ -500,7 +500,7 @@ pub fn str_to_i64(input: impl AsRef) -> Option { ) } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum StrToF64 { Fractional(NonNan), Decimal(NonNan), diff --git a/core/schema.rs b/core/schema.rs index cb2817d77..f2964ad35 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -1318,7 +1318,8 @@ impl From<&ColumnDefinition> for Column { /// /// Note that the order of the rules for determining column affinity is important. A column whose declared type is "CHARINT" will match both rules 1 and 2 but the first rule takes precedence and so the column affinity will be INTEGER. pub fn affinity(datatype: &str) -> Affinity { - // Note: callers of this function must ensure that the datatype is uppercase. + let datatype = datatype.to_ascii_uppercase(); + // Rule 1: INT -> INTEGER affinity if datatype.contains("INT") { return Affinity::Integer; diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 2cf3b7f96..fffad67b1 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -662,7 +662,7 @@ pub fn translate_expr( ast::Expr::Cast { expr, type_name } => { let type_name = type_name.as_ref().unwrap(); // TODO: why is this optional? translate_expr(program, referenced_tables, expr, target_register, resolver)?; - let type_affinity = affinity(&type_name.name.to_uppercase()); + let type_affinity = affinity(&type_name.name); program.emit_insn(Insn::Cast { reg: target_register, affinity: type_affinity, @@ -3433,6 +3433,9 @@ pub fn get_expr_affinity( Affinity::Blob } } + ast::Expr::Parenthesized(exprs) if exprs.len() == 1 => { + get_expr_affinity(exprs.first().unwrap(), referenced_tables) + } ast::Expr::Collate(expr, _) => get_expr_affinity(expr, referenced_tables), // Literals have NO affinity in SQLite! ast::Expr::Literal(_) => Affinity::Blob, // No affinity! diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 3d43c5bd2..61f7a0654 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -55,10 +55,7 @@ use crate::{ AggContext, Cursor, ExternalAggState, IOResult, SeekKey, SeekOp, SumAggState, Value, ValueType, }, - util::{ - cast_real_to_integer, cast_text_to_integer, cast_text_to_numeric, cast_text_to_real, - checked_cast_text_to_numeric, parse_schema_rows, - }, + util::{cast_real_to_integer, checked_cast_text_to_numeric, parse_schema_rows}, vdbe::{ builder::CursorType, insn::{IdxInsertFlags, Insn}, @@ -8180,11 +8177,16 @@ impl Value { } Affinity::Real => match self { Value::Blob(b) => { - // Convert BLOB to TEXT first let text = String::from_utf8_lossy(b); - cast_text_to_real(&text) + Value::Float( + crate::numeric::str_to_f64(&text) + .map(f64::from) + .unwrap_or(0.0), + ) + } + Value::Text(t) => { + Value::Float(crate::numeric::str_to_f64(t).map(f64::from).unwrap_or(0.0)) } - Value::Text(t) => cast_text_to_real(t.as_str()), Value::Integer(i) => Value::Float(*i as f64), Value::Float(f) => Value::Float(*f), _ => Value::Float(0.0), @@ -8193,9 +8195,9 @@ impl Value { Value::Blob(b) => { // Convert BLOB to TEXT first let text = String::from_utf8_lossy(b); - cast_text_to_integer(&text) + Value::Integer(crate::numeric::str_to_i64(&text).unwrap_or(0)) } - Value::Text(t) => cast_text_to_integer(t.as_str()), + Value::Text(t) => Value::Integer(crate::numeric::str_to_i64(t).unwrap_or(0)), Value::Integer(i) => Value::Integer(*i), // A cast of a REAL value into an INTEGER results in the integer between the REAL value and zero // that is closest to the REAL value. If a REAL is greater than the greatest possible signed integer (+9223372036854775807) @@ -8214,14 +8216,31 @@ impl Value { _ => Value::Integer(0), }, Affinity::Numeric => match self { - Value::Blob(b) => { - let text = String::from_utf8_lossy(b); - cast_text_to_numeric(&text) + Value::Null => Value::Null, + Value::Integer(v) => Value::Integer(*v), + Value::Float(v) => Self::Float(*v), + _ => { + let s = match self { + Value::Text(text) => text.to_string(), + Value::Blob(blob) => String::from_utf8_lossy(blob.as_slice()).to_string(), + _ => unreachable!(), + }; + + match crate::numeric::str_to_f64(&s) { + Some(parsed) => { + let Some(int) = crate::numeric::str_to_i64(&s) else { + return Value::Integer(0); + }; + + if f64::from(parsed) == int as f64 { + return Value::Integer(int); + } + + Value::Float(parsed.into()) + } + None => Value::Integer(0), + } } - Value::Text(t) => cast_text_to_numeric(t.as_str()), - Value::Integer(i) => Value::Integer(*i), - Value::Float(f) => Value::Float(*f), - _ => self.clone(), // TODO probably wrong }, } } diff --git a/fuzz/fuzz_targets/expression.rs b/fuzz/fuzz_targets/expression.rs index c9b4aa92f..779864f9c 100644 --- a/fuzz/fuzz_targets/expression.rs +++ b/fuzz/fuzz_targets/expression.rs @@ -165,11 +165,21 @@ str_enum! { } } +str_enum! { + enum CastType { + Text => "text", + Real => "real", + Integer => "integer", + Numeric => "numeric", + } +} + #[derive(Debug, Arbitrary)] enum Expr { Value(Value), Binary(Binary, Box, Box), Unary(Unary, Box), + Cast(Box, CastType), UnaryFunc(UnaryFunc, Box), BinaryFunc(BinaryFunc, Box, Box), } @@ -229,6 +239,14 @@ impl Expr { depth: expr.depth + 1, } } + Expr::Cast(expr, cast_type) => { + let expr = expr.lower(); + Output { + query: format!("cast({} as {cast_type})", expr.query), + parameters: expr.parameters, + depth: expr.depth + 1, + } + } } } }