Merge 'Fix math functions compatibility issues' from Levy A.

Adds `round`, `hex`, `unhex`, `abs`, `lower`, `upper`, `sign` and `log`
(with base) to the expression fuzzer.
Rounding with the precision argument still has some incompatibilities.

Closes #3160
This commit is contained in:
Jussi Saurio
2025-09-19 09:15:11 +03:00
committed by GitHub
4 changed files with 141 additions and 166 deletions

View File

@@ -103,17 +103,6 @@ impl<I: ?Sized + IO> IOExt for I {
}
}
pub trait RoundToPrecision {
fn round_to_precision(self, precision: i32) -> f64;
}
impl RoundToPrecision for f64 {
fn round_to_precision(self, precision: i32) -> f64 {
let factor = 10f64.powi(precision);
(self * factor).round() / factor
}
}
// https://sqlite.org/lang_keywords.html
const QUOTE_PAIRS: &[(char, char)] = &[
('"', '"'),

View File

@@ -56,7 +56,7 @@ use crate::{
},
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, RoundToPrecision,
checked_cast_text_to_numeric, parse_schema_rows,
},
vdbe::{
builder::CursorType,
@@ -4599,7 +4599,11 @@ pub fn op_function(
}
ScalarFunc::Unhex => {
let reg_value = &state.registers[*start_reg];
let ignored_chars = state.registers.get(*start_reg + 1);
let ignored_chars = if func.arg_count == 2 {
state.registers.get(*start_reg + 1)
} else {
None
};
let result = reg_value
.get_value()
.exec_unhex(ignored_chars.map(|x| x.get_value()));
@@ -7908,10 +7912,8 @@ mod cmath {
impl Value {
pub fn exec_lower(&self) -> Option<Self> {
match self {
Value::Text(t) => Some(Value::build_text(t.as_str().to_lowercase())),
t => Some(t.to_owned()),
}
self.cast_text()
.map(|s| Value::build_text(s.to_ascii_lowercase()))
}
pub fn exec_length(&self) -> Self {
@@ -7940,49 +7942,20 @@ impl Value {
}
pub fn exec_upper(&self) -> Option<Self> {
match self {
Value::Text(t) => Some(Value::build_text(t.as_str().to_uppercase())),
t => Some(t.to_owned()),
}
self.cast_text()
.map(|s| Value::build_text(s.to_ascii_uppercase()))
}
pub fn exec_sign(&self) -> Option<Value> {
let num = match self {
Value::Integer(i) => *i as f64,
Value::Float(f) => *f,
Value::Text(s) => {
if let Ok(i) = s.as_str().parse::<i64>() {
i as f64
} else if let Ok(f) = s.as_str().parse::<f64>() {
f
} else {
return Some(Value::Null);
}
}
Value::Blob(b) => match std::str::from_utf8(b) {
Ok(s) => {
if let Ok(i) = s.parse::<i64>() {
i as f64
} else if let Ok(f) = s.parse::<f64>() {
f
} else {
return Some(Value::Null);
}
}
Err(_) => return Some(Value::Null),
},
_ => return Some(Value::Null),
};
let v = Numeric::from_value_strict(self).try_into_f64()?;
let sign = if num > 0.0 {
Some(Value::Integer(if v > 0.0 {
1
} else if num < 0.0 {
} else if v < 0.0 {
-1
} else {
0
};
Some(Value::Integer(sign))
}))
}
/// Generates the Soundex code for a given word
@@ -8077,25 +8050,24 @@ impl Value {
}
pub fn exec_abs(&self) -> Result<Self> {
match self {
Value::Integer(x) => {
match i64::checked_abs(*x) {
Some(y) => Ok(Value::Integer(y)),
// Special case: if we do the abs of "-9223372036854775808", it causes overflow.
// return IntegerOverflow error
None => Err(LimboError::IntegerOverflow),
}
Ok(match self {
Value::Null => Value::Null,
Value::Integer(v) => {
Value::Integer(v.checked_abs().ok_or(LimboError::IntegerOverflow)?)
}
Value::Float(x) => {
if x < &0.0 {
Ok(Value::Float(-x))
} else {
Ok(Value::Float(*x))
}
Value::Float(non_nan) => Value::Float(non_nan.abs()),
_ => {
let s = match self {
Value::Text(text) => text.to_string(),
Value::Blob(blob) => String::from_utf8_lossy(blob).to_string(),
_ => unreachable!(),
};
crate::numeric::str_to_f64(s)
.map(|v| Value::Float(f64::from(v).abs()))
.unwrap_or(Value::Float(0.0))
}
Value::Null => Ok(Value::Null),
_ => Ok(Value::Float(0.0)),
}
})
}
pub fn exec_random() -> Self {
@@ -8238,7 +8210,7 @@ impl Value {
Value::build_text(hex::encode_upper(text))
}
Value::Blob(blob_bytes) => Value::build_text(hex::encode_upper(blob_bytes)),
_ => Value::Null,
Value::Null => Value::build_text(""),
}
}
@@ -8246,9 +8218,12 @@ impl Value {
match self {
Value::Null => Value::Null,
_ => match ignored_chars {
None => match hex::decode(self.to_string()) {
Ok(bytes) => Value::Blob(bytes),
Err(_) => Value::Null,
None => match self
.cast_text()
.map(|s| hex::decode(&s[0..s.find('\0').unwrap_or(s.len())]))
{
Some(Ok(bytes)) => Value::Blob(bytes),
_ => Value::Null,
},
Some(ignore) => match ignore {
Value::Text(_) => {
@@ -8260,7 +8235,7 @@ impl Value {
.to_string();
match hex::decode(trimmed) {
Ok(bytes) => Value::Blob(bytes),
Err(_) => Value::Null,
_ => Value::Null,
}
}
_ => Value::Null,
@@ -8283,36 +8258,33 @@ impl Value {
}
}
fn _to_float(&self) -> f64 {
match self {
Value::Text(x) => match cast_text_to_numeric(x.as_str()) {
Value::Integer(i) => i as f64,
Value::Float(f) => f,
_ => unreachable!(),
},
Value::Integer(x) => *x as f64,
Value::Float(x) => *x,
_ => 0.0,
}
}
pub fn exec_round(&self, precision: Option<&Value>) -> Value {
let reg = self._to_float();
let round = |reg: f64, f: f64| {
let precision = if f < 1.0 { 0.0 } else { f };
Value::Float(reg.round_to_precision(precision as i32))
let Some(f) = Numeric::from(self).try_into_f64() else {
return Value::Null;
};
match precision {
Some(Value::Text(x)) => match cast_text_to_numeric(x.as_str()) {
Value::Integer(i) => round(reg, i as f64),
Value::Float(f) => round(reg, f),
_ => unreachable!(),
},
Some(Value::Integer(i)) => round(reg, *i as f64),
Some(Value::Float(f)) => round(reg, *f),
None => round(reg, 0.0),
_ => Value::Null,
let precision = match precision.map(|v| Numeric::from(v).try_into_f64()) {
None => 0.0,
Some(Some(v)) => v,
Some(None) => return Value::Null,
};
if !(-4503599627370496.0..=4503599627370496.0).contains(&f) {
return Value::Float(f);
}
let precision = if precision < 1.0 { 0.0 } else { precision };
let precision = precision.clamp(0.0, 30.0) as usize;
if precision == 0 {
return Value::Float(((f + if f < 0.0 { -0.5 } else { 0.5 }) as i64) as f64);
}
let f: f64 = crate::numeric::str_to_f64(format!("{f:.precision$}"))
.unwrap()
.into();
Value::Float(f)
}
// Implements TRIM pattern matching.
@@ -8576,6 +8548,11 @@ impl Value {
let log_x = libm::log(f);
let log_base = libm::log(base);
if log_base <= 0.0 {
return Value::Null;
}
let result = log_x / log_base;
Value::Float(result)
}
@@ -10008,8 +9985,8 @@ mod tests {
assert_eq!(input_str.exec_upper().unwrap(), expected_str);
let input_int = Value::Integer(10);
assert_eq!(input_int.exec_upper().unwrap(), input_int);
assert_eq!(Value::Null.exec_upper().unwrap(), Value::Null)
assert_eq!(input_int.exec_upper().unwrap(), Value::build_text("10"));
assert_eq!(Value::Null.exec_upper(), None)
}
#[test]
@@ -10019,8 +9996,8 @@ mod tests {
assert_eq!(input_str.exec_lower().unwrap(), expected_str);
let input_int = Value::Integer(10);
assert_eq!(input_int.exec_lower().unwrap(), input_int);
assert_eq!(Value::Null.exec_lower().unwrap(), Value::Null)
assert_eq!(input_int.exec_lower().unwrap(), Value::build_text("10"));
assert_eq!(Value::Null.exec_lower(), None)
}
#[test]
@@ -10487,7 +10464,7 @@ mod tests {
assert_eq!(input.exec_sign(), expected);
let input = Value::build_text("abc");
let expected = Some(Value::Null);
let expected = None;
assert_eq!(input.exec_sign(), expected);
let input = Value::build_text("42");
@@ -10503,23 +10480,23 @@ mod tests {
assert_eq!(input.exec_sign(), expected);
let input = Value::Blob(b"abc".to_vec());
let expected = Some(Value::Null);
let expected = None;
assert_eq!(input.exec_sign(), expected);
let input = Value::Blob(b"42".to_vec());
let expected = Some(Value::Integer(1));
let expected = None;
assert_eq!(input.exec_sign(), expected);
let input = Value::Blob(b"-42".to_vec());
let expected = Some(Value::Integer(-1));
let expected = None;
assert_eq!(input.exec_sign(), expected);
let input = Value::Blob(b"0".to_vec());
let expected = Some(Value::Integer(0));
let expected = None;
assert_eq!(input.exec_sign(), expected);
let input = Value::Null;
let expected = Some(Value::Null);
let expected = None;
assert_eq!(input.exec_sign(), expected);
}

109
fuzz/Cargo.lock generated
View File

@@ -56,12 +56,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -88,9 +82,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.0"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "built"
@@ -153,11 +147,10 @@ checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487"
[[package]]
name = "chrono"
version = "0.4.40"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
@@ -231,7 +224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"typenum",
]
@@ -266,12 +259,6 @@ dependencies = [
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.10"
@@ -569,16 +556,6 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "indexmap"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.4"
@@ -893,8 +870,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
@@ -904,7 +891,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
@@ -916,6 +913,15 @@ dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
@@ -1121,18 +1127,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.69"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@@ -1182,7 +1188,7 @@ dependencies = [
[[package]]
name = "turso_core"
version = "0.2.0-pre.1"
version = "0.2.0-pre.3"
dependencies = [
"aegis",
"aes",
@@ -1205,7 +1211,7 @@ dependencies = [
"parking_lot",
"paste",
"polling",
"rand",
"rand 0.8.5",
"regex",
"regex-syntax",
"rustix 1.0.7",
@@ -1218,14 +1224,14 @@ dependencies = [
"turso_ext",
"turso_macros",
"turso_parser",
"turso_sqlite3_parser",
"twox-hash",
"uncased",
"uuid",
]
[[package]]
name = "turso_ext"
version = "0.2.0-pre.1"
version = "0.2.0-pre.3"
dependencies = [
"chrono",
"getrandom 0.3.1",
@@ -1234,7 +1240,7 @@ dependencies = [
[[package]]
name = "turso_macros"
version = "0.2.0-pre.1"
version = "0.2.0-pre.3"
dependencies = [
"proc-macro2",
"quote",
@@ -1243,7 +1249,7 @@ dependencies = [
[[package]]
name = "turso_parser"
version = "0.2.0-pre.1"
version = "0.2.0-pre.3"
dependencies = [
"bitflags",
"miette",
@@ -1254,19 +1260,12 @@ dependencies = [
]
[[package]]
name = "turso_sqlite3_parser"
version = "0.2.0-pre.1"
name = "twox-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
dependencies = [
"bitflags",
"cc",
"fallible-iterator",
"indexmap",
"log",
"memchr",
"miette",
"smallvec",
"strum",
"strum_macros",
"rand 0.9.2",
]
[[package]]
@@ -1434,9 +1433,9 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"

View File

@@ -118,6 +118,14 @@ impl rusqlite::types::FromSql for Value {
str_enum! {
enum UnaryFunc {
Round => "round",
Hex => "hex",
Unhex => "unhex",
Abs => "abs",
Lower => "lower",
Upper => "upper",
Sign => "sign",
Ceil => "ceil",
Floor => "floor",
Trunc => "trunc",
@@ -149,9 +157,11 @@ str_enum! {
str_enum! {
enum BinaryFunc {
Round => "round",
Power => "pow",
Mod => "mod",
Atan2 => "atan2",
Log => "log",
}
}