diff --git a/core/util.rs b/core/util.rs index 77062fd7d..426e5f8f6 100644 --- a/core/util.rs +++ b/core/util.rs @@ -855,11 +855,19 @@ pub fn cast_text_to_real(text: &str) -> Value { /// IEEE 754 64-bit float and thus provides a 1-bit of margin for the text-to-float conversion operation.) /// Any text input that describes a value outside the range of a 64-bit signed integer yields a REAL result. /// Casting a REAL or INTEGER value to NUMERIC is a no-op, even if a real value could be losslessly converted to an integer. -pub fn checked_cast_text_to_numeric(text: &str) -> std::result::Result { +/// +/// `lossless`: If `true`, rejects the input if any characters remain after the numeric prefix (strict / exact conversion). +pub fn checked_cast_text_to_numeric(text: &str, lossless: bool) -> std::result::Result { // sqlite will parse the first N digits of a string to numeric value, then determine // whether _that_ value is more likely a real or integer value. e.g. // '-100234-2344.23e14' evaluates to -100234 instead of -100234.0 + let original_len = text.trim().len(); let (kind, text) = parse_numeric_str(text)?; + + if original_len != text.len() && lossless { + return Err(()); + } + match kind { ValueType::Integer => match text.parse::() { Ok(i) => Ok(Value::Integer(i)), @@ -940,7 +948,7 @@ fn parse_numeric_str(text: &str) -> Result<(ValueType, &str), ()> { } pub fn cast_text_to_numeric(txt: &str) -> Value { - checked_cast_text_to_numeric(txt).unwrap_or(Value::Integer(0)) + checked_cast_text_to_numeric(txt, false).unwrap_or(Value::Integer(0)) } // Check if float can be losslessly converted to 51-bit integer @@ -1368,7 +1376,7 @@ pub fn rewrite_column_references_if_needed( #[cfg(test)] pub mod tests { use super::*; - use crate::schema::Type as SchemaValueType; + use crate::{schema::Type as SchemaValueType, types::Text}; use turso_parser::ast::{self, Expr, Literal, Name, Operator::*, Type}; #[test] @@ -2379,4 +2387,44 @@ pub mod tests { assert_eq!(result, expected, "Failed for input: {input}"); } } + + #[test] + fn test_checked_cast_text_to_numeric_lossless_property() { + use Value::*; + assert_eq!(checked_cast_text_to_numeric("1.xx", true), Err(())); + assert_eq!(checked_cast_text_to_numeric("abc", true), Err(())); + assert_eq!(checked_cast_text_to_numeric("--5", true), Err(())); + assert_eq!(checked_cast_text_to_numeric("12.34.56", true), Err(())); + assert_eq!(checked_cast_text_to_numeric("", true), Err(())); + assert_eq!(checked_cast_text_to_numeric(" ", true), Err(())); + assert_eq!(checked_cast_text_to_numeric("0", true), Ok(Integer(0))); + assert_eq!(checked_cast_text_to_numeric("42", true), Ok(Integer(42))); + assert_eq!(checked_cast_text_to_numeric("-42", true), Ok(Integer(-42))); + assert_eq!( + checked_cast_text_to_numeric("999999999999", true), + Ok(Integer(999_999_999_999)) + ); + assert_eq!(checked_cast_text_to_numeric("1.0", true), Ok(Float(1.0))); + assert_eq!( + checked_cast_text_to_numeric("-3.22", true), + Ok(Float(-3.22)) + ); + assert_eq!( + checked_cast_text_to_numeric("0.001", true), + Ok(Float(0.001)) + ); + assert_eq!(checked_cast_text_to_numeric("2e3", true), Ok(Float(2000.0))); + assert_eq!( + checked_cast_text_to_numeric("-5.5e-2", true), + Ok(Float(-0.055)) + ); + assert_eq!( + checked_cast_text_to_numeric(" 123 ", true), + Ok(Integer(123)) + ); + assert_eq!( + checked_cast_text_to_numeric("\t-3.22\n", true), + Ok(Float(-3.22)) + ); + } } diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index da7518def..ce1c8a7d2 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -6699,11 +6699,12 @@ pub fn op_must_be_int( Ok(i) => state.registers[*reg] = Register::Value(Value::Integer(i)), Err(_) => crate::bail_parse_error!("datatype mismatch"), }, - Value::Text(text) => match checked_cast_text_to_numeric(text.as_str()) { + Value::Text(text) => match checked_cast_text_to_numeric(text.as_str(), true) { Ok(Value::Integer(i)) => state.registers[*reg] = Register::Value(Value::Integer(i)), - Ok(Value::Float(f)) => { - state.registers[*reg] = Register::Value(Value::Integer(f as i64)) - } + Ok(Value::Float(f)) => match cast_real_to_integer(f) { + Ok(i) => state.registers[*reg] = Register::Value(Value::Integer(i)), + Err(_) => crate::bail_parse_error!("datatype mismatch"), + }, _ => crate::bail_parse_error!("datatype mismatch"), }, _ => { @@ -9709,7 +9710,7 @@ fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { if s.starts_with("0x") { return false; } - if let Ok(num) = checked_cast_text_to_numeric(s) { + if let Ok(num) = checked_cast_text_to_numeric(s, false) { *value = num; return true; } else { diff --git a/testing/select.test b/testing/select.test index c42e38e42..1267f131d 100755 --- a/testing/select.test +++ b/testing/select.test @@ -1008,6 +1008,11 @@ do_execsql_test_in_memory_error_content limit-expr-invalid-data-type-4 { SELECT 1 LIMIT 4+NULL; } {"datatype mismatch"} +do_execsql_test_in_memory_error_content limit-expression-invalid-type { + SELECT 1 LIMIT '1.xx'; +} {"datatype mismatch"} + + do_execsql_test_on_specific_db {:memory:} rowid-references { CREATE TABLE test_table (id INTEGER); INSERT INTO test_table VALUES (5),(5); @@ -1093,4 +1098,4 @@ do_execsql_test_on_specific_db {:memory:} unambiguous-self-join { 2|3 3|1 3|2 -3|3} +3|3} \ No newline at end of file