From 9a3af4bd068bf2657841e84d95c89d93062bab14 Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:20:13 +0400 Subject: [PATCH 1/6] add CAST expression in the fuzz test --- tests/integration/fuzz/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index 13cd8140b..3cbc53e3a 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -165,6 +165,7 @@ mod tests { "SELECT like('a%', 'a') = 1", "SELECT CASE ( NULL < NULL ) WHEN ( 0 ) THEN ( NULL ) ELSE ( 2.0 ) END;", "SELECT (COALESCE(0, COALESCE(0, 0)));", + "SELECT CAST((1 > 0) AS INTEGER);", ] { let limbo = limbo_exec_row(&limbo_conn, query); let sqlite = sqlite_exec_row(&sqlite_conn, query); @@ -238,6 +239,21 @@ mod tests { .push_str(")") .build(); + let (cast_expr, cast_expr_builder) = g.create_handle(); + cast_expr_builder + .concat(" ") + .push_str("CAST ( (") + .push(expr) + .push_str(") AS ") + .push( + g.create() + .choice() + .options_str(["NUMERIC", "REAL", "INTEGER"]) + .build(), + ) + .push_str(")") + .build(); + let (case_expr, case_expr_builder) = g.create_handle(); case_expr_builder .concat(" ") @@ -309,6 +325,7 @@ mod tests { expr_builder .choice() + .option_w(cast_expr, 1.0) .option_w(case_expr, 1.0) .option_w(unary_infix_op, 2.0) .option_w(bin_op, 3.0) From dff1fdc853ca2a7a1b3a1b53055edacc9e37f51f Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:20:28 +0400 Subject: [PATCH 2/6] fix codegen of CAST --- core/translate/expr.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index ec895a4db..409df5155 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -835,15 +835,14 @@ pub fn translate_expr( } ast::Expr::Cast { expr, type_name } => { let type_name = type_name.as_ref().unwrap(); // TODO: why is this optional? - let reg_expr = program.alloc_register(); + let reg_expr = program.alloc_registers(2); translate_expr(program, referenced_tables, expr, reg_expr, resolver)?; - let reg_type = program.alloc_register(); program.emit_insn(Insn::String8 { // we make a comparison against uppercase static strs in the affinity() function, // so we need to make sure we're comparing against the uppercase version, // and it's better to do this once instead of every time we check affinity value: type_name.name.to_uppercase(), - dest: reg_type, + dest: reg_expr + 1, }); program.mark_last_insn_constant(); program.emit_insn(Insn::Function { From e3e399f9b1c781c8589f3203d063308566a575e7 Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:31:13 +0400 Subject: [PATCH 3/6] fixup fuzz --- tests/integration/fuzz/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index 3cbc53e3a..c91281423 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -248,7 +248,7 @@ mod tests { .push( g.create() .choice() - .options_str(["NUMERIC", "REAL", "INTEGER"]) + .options_str(["NUMERIC", "INTEGER"]) .build(), ) .push_str(")") From 3c4d9a93af5a16bb89e9ee9383f6c1d518bfd1c4 Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:32:54 +0400 Subject: [PATCH 4/6] fix rounding of REAL to INTEGER - SQLite rounds (x: f64) to the nearest number between x and 0 - so basically truncates the x --- core/vdbe/mod.rs | 2 +- testing/scalar-functions.test | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 3ccfdc1e3..989a8331d 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -3484,7 +3484,7 @@ fn exec_cast(value: &OwnedValue, datatype: &str) -> OwnedValue { // then the result is the greatest possible signed integer and if the REAL is less than the least possible signed integer (-9223372036854775808) // then the result is the least possible signed integer. OwnedValue::Float(f) => { - let i = f.floor() as i128; + let i = f.trunc().floor() as i128; if i > i64::MAX as i128 { OwnedValue::Integer(i64::MAX) } else if i < i64::MIN as i128 { diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index f04fa1765..5ce9e02f6 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -740,6 +740,21 @@ do_execsql_test cast-float-to-integer { SELECT CAST(123.45 AS INTEGER); } {123} +do_execsql_test cast-float-to-integer-rounding { + SELECT CAST(0.6 AS INTEGER); + SELECT CAST(1.0 AS INTEGER); + SELECT CAST(1.6 AS INTEGER); + SELECT CAST(-0.6 AS INTEGER); + SELECT CAST(-1.0 AS INTEGER); + SELECT CAST(-1.6 AS INTEGER); +} {0 +1 +1 +0 +-1 +-1 +} + do_execsql_test cast-large-float-to-integer { SELECT CAST(9223372036854775808.0 AS INTEGER); } {9223372036854775807} From a37b74666f3d762f5079b990ba5e5a526b7165c2 Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:41:08 +0400 Subject: [PATCH 5/6] remove unnecessary floor() --- core/vdbe/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 989a8331d..c4ce0a5b6 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -3484,7 +3484,7 @@ fn exec_cast(value: &OwnedValue, datatype: &str) -> OwnedValue { // then the result is the greatest possible signed integer and if the REAL is less than the least possible signed integer (-9223372036854775808) // then the result is the least possible signed integer. OwnedValue::Float(f) => { - let i = f.trunc().floor() as i128; + let i = f.trunc() as i128; if i > i64::MAX as i128 { OwnedValue::Integer(i64::MAX) } else if i < i64::MIN as i128 { From d2fb772936f2268756eb3f6339d95b39d2c37789 Mon Sep 17 00:00:00 2001 From: Nikita Sivukhin Date: Sun, 9 Feb 2025 22:47:06 +0400 Subject: [PATCH 6/6] adjust CAST fuzzing options --- tests/integration/fuzz/mod.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index c91281423..cf9c170f2 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -245,12 +245,8 @@ mod tests { .push_str("CAST ( (") .push(expr) .push_str(") AS ") - .push( - g.create() - .choice() - .options_str(["NUMERIC", "INTEGER"]) - .build(), - ) + // cast to INTEGER/REAL/TEXT types can be added when Limbo will use proper equality semantic between values (e.g. 1 = 1.0) + .push(g.create().choice().options_str(["NUMERIC"]).build()) .push_str(")") .build();