diff --git a/COMPAT.md b/COMPAT.md index ec9473d48..f02c25b66 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -160,6 +160,41 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | upper(X) | Yes | | | zeroblob(N) | Yes | | +### Mathematical functions + +| Function | Status | Comment | +| ---------- | ------ | ------- | +| acos(X) | Yes | | +| acosh(X) | Yes | | +| asin(X) | Yes | | +| asinh(X) | Yes | | +| atan(X) | Yes | | +| atan2(Y,X) | Yes | | +| atanh(X) | Yes | | +| ceil(X) | Yes | | +| ceiling(X) | Yes | | +| cos(X) | Yes | | +| cosh(X) | Yes | | +| degrees(X) | Yes | | +| exp(X) | Yes | | +| floor(X) | Yes | | +| ln(X) | Yes | | +| log(B,X) | Yes | | +| log(X) | Yes | | +| log10(X) | Yes | | +| log2(X) | Yes | | +| mod(X,Y) | Yes | | +| pi() | Yes | | +| pow(X,Y) | Yes | | +| power(X,Y) | Yes | | +| radians(X) | Yes | | +| sin(X) | Yes | | +| sinh(X) | Yes | | +| sqrt(X) | Yes | | +| tan(X) | Yes | | +| tanh(X) | Yes | | +| trunc(X) | Yes | | + ### Aggregate functions | Function | Status | Comment | diff --git a/core/function.rs b/core/function.rs index 9e2dca0f3..c4885925f 100644 --- a/core/function.rs +++ b/core/function.rs @@ -139,10 +139,126 @@ impl Display for ScalarFunc { } } +#[derive(Debug, Clone, PartialEq)] +pub enum MathFunc { + Acos, + Acosh, + Asin, + Asinh, + Atan, + Atan2, + Atanh, + Ceil, + Ceiling, + Cos, + Cosh, + Degrees, + Exp, + Floor, + Ln, + Log, + Log10, + Log2, + Mod, + Pi, + Pow, + Power, + Radians, + Sin, + Sinh, + Sqrt, + Tan, + Tanh, + Trunc, +} + +pub enum MathFuncArity { + Nullary, + Unary, + Binary, + UnaryOrBinary, +} + +impl MathFunc { + pub fn arity(&self) -> MathFuncArity { + match self { + MathFunc::Pi => MathFuncArity::Nullary, + + MathFunc::Acos + | MathFunc::Acosh + | MathFunc::Asin + | MathFunc::Asinh + | MathFunc::Atan + | MathFunc::Atanh + | MathFunc::Ceil + | MathFunc::Ceiling + | MathFunc::Cos + | MathFunc::Cosh + | MathFunc::Degrees + | MathFunc::Exp + | MathFunc::Floor + | MathFunc::Ln + | MathFunc::Log10 + | MathFunc::Log2 + | MathFunc::Radians + | MathFunc::Sin + | MathFunc::Sinh + | MathFunc::Sqrt + | MathFunc::Tan + | MathFunc::Tanh + | MathFunc::Trunc => MathFuncArity::Unary, + + MathFunc::Atan2 | MathFunc::Mod | MathFunc::Pow | MathFunc::Power => { + MathFuncArity::Binary + } + + MathFunc::Log => MathFuncArity::UnaryOrBinary, + } + } +} + +impl Display for MathFunc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + MathFunc::Acos => "acos".to_string(), + MathFunc::Acosh => "acosh".to_string(), + MathFunc::Asin => "asin".to_string(), + MathFunc::Asinh => "asinh".to_string(), + MathFunc::Atan => "atan".to_string(), + MathFunc::Atan2 => "atan2".to_string(), + MathFunc::Atanh => "atanh".to_string(), + MathFunc::Ceil => "ceil".to_string(), + MathFunc::Ceiling => "ceiling".to_string(), + MathFunc::Cos => "cos".to_string(), + MathFunc::Cosh => "cosh".to_string(), + MathFunc::Degrees => "degrees".to_string(), + MathFunc::Exp => "exp".to_string(), + MathFunc::Floor => "floor".to_string(), + MathFunc::Ln => "ln".to_string(), + MathFunc::Log => "log".to_string(), + MathFunc::Log10 => "log10".to_string(), + MathFunc::Log2 => "log2".to_string(), + MathFunc::Mod => "mod".to_string(), + MathFunc::Pi => "pi".to_string(), + MathFunc::Pow => "pow".to_string(), + MathFunc::Power => "power".to_string(), + MathFunc::Radians => "radians".to_string(), + MathFunc::Sin => "sin".to_string(), + MathFunc::Sinh => "sinh".to_string(), + MathFunc::Sqrt => "sqrt".to_string(), + MathFunc::Tan => "tan".to_string(), + MathFunc::Tanh => "tanh".to_string(), + MathFunc::Trunc => "trunc".to_string(), + }; + write!(f, "{}", str) + } +} + #[derive(Debug)] pub enum Func { Agg(AggFunc), Scalar(ScalarFunc), + Math(MathFunc), #[cfg(feature = "json")] Json(JsonFunc), } @@ -152,6 +268,7 @@ impl Display for Func { match self { Func::Agg(agg_func) => write!(f, "{}", agg_func.to_string()), Func::Scalar(scalar_func) => write!(f, "{}", scalar_func), + Func::Math(math_func) => write!(f, "{}", math_func), #[cfg(feature = "json")] Func::Json(json_func) => write!(f, "{}", json_func), } @@ -216,6 +333,35 @@ impl Func { "unhex" => Ok(Func::Scalar(ScalarFunc::Unhex)), "zeroblob" => Ok(Func::Scalar(ScalarFunc::ZeroBlob)), "soundex" => Ok(Func::Scalar(ScalarFunc::Soundex)), + "acos" => Ok(Func::Math(MathFunc::Acos)), + "acosh" => Ok(Func::Math(MathFunc::Acosh)), + "asin" => Ok(Func::Math(MathFunc::Asin)), + "asinh" => Ok(Func::Math(MathFunc::Asinh)), + "atan" => Ok(Func::Math(MathFunc::Atan)), + "atan2" => Ok(Func::Math(MathFunc::Atan2)), + "atanh" => Ok(Func::Math(MathFunc::Atanh)), + "ceil" => Ok(Func::Math(MathFunc::Ceil)), + "ceiling" => Ok(Func::Math(MathFunc::Ceiling)), + "cos" => Ok(Func::Math(MathFunc::Cos)), + "cosh" => Ok(Func::Math(MathFunc::Cosh)), + "degrees" => Ok(Func::Math(MathFunc::Degrees)), + "exp" => Ok(Func::Math(MathFunc::Exp)), + "floor" => Ok(Func::Math(MathFunc::Floor)), + "ln" => Ok(Func::Math(MathFunc::Ln)), + "log" => Ok(Func::Math(MathFunc::Log)), + "log10" => Ok(Func::Math(MathFunc::Log10)), + "log2" => Ok(Func::Math(MathFunc::Log2)), + "mod" => Ok(Func::Math(MathFunc::Mod)), + "pi" => Ok(Func::Math(MathFunc::Pi)), + "pow" => Ok(Func::Math(MathFunc::Pow)), + "power" => Ok(Func::Math(MathFunc::Power)), + "radians" => Ok(Func::Math(MathFunc::Radians)), + "sin" => Ok(Func::Math(MathFunc::Sin)), + "sinh" => Ok(Func::Math(MathFunc::Sinh)), + "sqrt" => Ok(Func::Math(MathFunc::Sqrt)), + "tan" => Ok(Func::Math(MathFunc::Tan)), + "tanh" => Ok(Func::Math(MathFunc::Tanh)), + "trunc" => Ok(Func::Math(MathFunc::Trunc)), _ => Err(()), } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 04ea1fc98..b1db3ac92 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -2,7 +2,7 @@ use sqlite3_parser::ast::{self, UnaryOperator}; #[cfg(feature = "json")] use crate::function::JsonFunc; -use crate::function::{AggFunc, Func, FuncCtx, ScalarFunc}; +use crate::function::{AggFunc, Func, FuncCtx, MathFuncArity, ScalarFunc}; use crate::schema::Type; use crate::util::normalize_ident; use crate::vdbe::{builder::ProgramBuilder, BranchOffset, Insn}; @@ -1603,6 +1603,134 @@ pub fn translate_expr( } } } + Func::Math(math_func) => match math_func.arity() { + MathFuncArity::Nullary => { + if args.is_some() { + crate::bail_parse_error!("{} function with arguments", math_func); + } + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: 0, + dest: target_register, + func: func_ctx, + }); + Ok(target_register) + } + + MathFuncArity::Unary => { + let args = if let Some(args) = args { + if args.len() != 1 { + crate::bail_parse_error!( + "{} function with not exactly 1 argument", + math_func + ); + } + args + } else { + crate::bail_parse_error!("{} function with no arguments", math_func); + }; + + let reg = program.alloc_register(); + + translate_expr( + program, + referenced_tables, + &args[0], + reg, + precomputed_exprs_to_registers, + )?; + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: reg, + dest: target_register, + func: func_ctx, + }); + Ok(target_register) + } + + MathFuncArity::Binary => { + let args = if let Some(args) = args { + if args.len() != 2 { + crate::bail_parse_error!( + "{} function with not exactly 2 arguments", + math_func + ); + } + args + } else { + crate::bail_parse_error!("{} function with no arguments", math_func); + }; + + let reg1 = program.alloc_register(); + let reg2 = program.alloc_register(); + + translate_expr( + program, + referenced_tables, + &args[0], + reg1, + precomputed_exprs_to_registers, + )?; + if let ast::Expr::Literal(_) = &args[0] { + program.mark_last_insn_constant(); + } + + translate_expr( + program, + referenced_tables, + &args[1], + reg2, + precomputed_exprs_to_registers, + )?; + if let ast::Expr::Literal(_) = &args[1] { + program.mark_last_insn_constant(); + } + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: target_register + 1, + dest: target_register, + func: func_ctx, + }); + Ok(target_register) + } + + MathFuncArity::UnaryOrBinary => { + let args = if let Some(args) = args { + if args.len() > 2 { + crate::bail_parse_error!( + "{} function with more than 2 arguments", + math_func + ); + } + args + } else { + crate::bail_parse_error!("{} function with no arguments", math_func); + }; + + let regs = program.alloc_registers(args.len()); + + for (i, arg) in args.iter().enumerate() { + translate_expr( + program, + referenced_tables, + arg, + regs + i, + precomputed_exprs_to_registers, + )?; + } + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: regs, + dest: target_register, + func: func_ctx, + }); + Ok(target_register) + } + }, } } ast::Expr::FunctionCallStar { .. } => todo!(), diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index fa07fa69e..069782c9a 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -24,7 +24,7 @@ pub mod sorter; mod datetime; use crate::error::{LimboError, SQLITE_CONSTRAINT_PRIMARYKEY}; -use crate::function::{AggFunc, FuncCtx, ScalarFunc}; +use crate::function::{AggFunc, FuncCtx, MathFunc, MathFuncArity, ScalarFunc}; use crate::pseudo::PseudoCursor; use crate::schema::Table; use crate::storage::sqlite3_ondisk::DatabaseHeader; @@ -2491,6 +2491,58 @@ impl Program { state.registers[*dest] = exec_replace(source, pattern, replacement); } }, + crate::function::Func::Math(math_func) => match math_func.arity() { + MathFuncArity::Nullary => match math_func { + MathFunc::Pi => { + state.registers[*dest] = + OwnedValue::Float(std::f64::consts::PI); + } + _ => { + unreachable!( + "Unexpected mathematical Nullary function {:?}", + math_func + ); + } + }, + + MathFuncArity::Unary => { + let reg_value = &state.registers[*start_reg]; + let result = exec_math_unary(reg_value, math_func); + state.registers[*dest] = result; + } + + MathFuncArity::Binary => { + let lhs = &state.registers[*start_reg]; + let rhs = &state.registers[*start_reg + 1]; + let result = exec_math_binary(lhs, rhs, math_func); + state.registers[*dest] = result; + } + + MathFuncArity::UnaryOrBinary => match math_func { + MathFunc::Log => { + let result = match arg_count { + 1 => { + let arg = &state.registers[*start_reg]; + exec_math_log(arg, None) + } + 2 => { + let base = &state.registers[*start_reg]; + let arg = &state.registers[*start_reg + 1]; + exec_math_log(arg, Some(base)) + } + _ => unreachable!( + "{:?} function with unexpected number of arguments", + math_func + ), + }; + state.registers[*dest] = result; + } + _ => unreachable!( + "Unexpected mathematical UnaryOrBinary function {:?}", + math_func + ), + }, + }, crate::function::Func::Agg(_) => { unreachable!("Aggregate functions should not be handled here") } @@ -3597,6 +3649,109 @@ fn execute_sqlite_version(version_integer: i64) -> String { format!("{}.{}.{}", major, minor, release) } +fn to_f64(reg: &OwnedValue) -> Option { + match reg { + OwnedValue::Integer(i) => Some(*i as f64), + OwnedValue::Float(f) => Some(*f), + OwnedValue::Text(t) => t.parse::().ok(), + OwnedValue::Agg(ctx) => to_f64(ctx.final_value()), + _ => None, + } +} + +fn exec_math_unary(reg: &OwnedValue, function: &MathFunc) -> OwnedValue { + // In case of some functions and integer input, return the input as is + if let OwnedValue::Integer(_) = reg { + if matches! { function, MathFunc::Ceil | MathFunc::Ceiling | MathFunc::Floor | MathFunc::Trunc } + { + return reg.clone(); + } + } + + let f = match to_f64(reg) { + Some(f) => f, + None => return OwnedValue::Null, + }; + + let result = match function { + MathFunc::Acos => f.acos(), + MathFunc::Acosh => f.acosh(), + MathFunc::Asin => f.asin(), + MathFunc::Asinh => f.asinh(), + MathFunc::Atan => f.atan(), + MathFunc::Atanh => f.atanh(), + MathFunc::Ceil | MathFunc::Ceiling => f.ceil(), + MathFunc::Cos => f.cos(), + MathFunc::Cosh => f.cosh(), + MathFunc::Degrees => f.to_degrees(), + MathFunc::Exp => f.exp(), + MathFunc::Floor => f.floor(), + MathFunc::Ln => f.ln(), + MathFunc::Log10 => f.log10(), + MathFunc::Log2 => f.log2(), + MathFunc::Radians => f.to_radians(), + MathFunc::Sin => f.sin(), + MathFunc::Sinh => f.sinh(), + MathFunc::Sqrt => f.sqrt(), + MathFunc::Tan => f.tan(), + MathFunc::Tanh => f.tanh(), + MathFunc::Trunc => f.trunc(), + _ => unreachable!("Unexpected mathematical unary function {:?}", function), + }; + + if result.is_nan() { + OwnedValue::Null + } else { + OwnedValue::Float(result) + } +} + +fn exec_math_binary(lhs: &OwnedValue, rhs: &OwnedValue, function: &MathFunc) -> OwnedValue { + let lhs = match to_f64(lhs) { + Some(f) => f, + None => return OwnedValue::Null, + }; + + let rhs = match to_f64(rhs) { + Some(f) => f, + None => return OwnedValue::Null, + }; + + let result = match function { + MathFunc::Atan2 => lhs.atan2(rhs), + MathFunc::Mod => lhs % rhs, + MathFunc::Pow | MathFunc::Power => lhs.powf(rhs), + _ => unreachable!("Unexpected mathematical binary function {:?}", function), + }; + + if result.is_nan() { + OwnedValue::Null + } else { + OwnedValue::Float(result) + } +} + +fn exec_math_log(arg: &OwnedValue, base: Option<&OwnedValue>) -> OwnedValue { + let f = match to_f64(arg) { + Some(f) => f, + None => return OwnedValue::Null, + }; + + let base = match base { + Some(base) => match to_f64(base) { + Some(f) => f, + None => return OwnedValue::Null, + }, + None => 10.0, + }; + + if f <= 0.0 || base <= 0.0 || base == 1.0 { + return OwnedValue::Null; + } + + OwnedValue::Float(f.log(base)) +} + #[cfg(test)] mod tests { diff --git a/testing/math.test b/testing/math.test index af477fca4..7b27495e3 100644 --- a/testing/math.test +++ b/testing/math.test @@ -3,6 +3,10 @@ set testdir [file dirname $argv0] source $testdir/tester.tcl +# Tolerance for floating point comparisons +# FIXME: When Limbo's floating point presentation matches to SQLite, this could/should be removed +set tolerance 1e-13 + do_execsql_test add-int { SELECT 10 + 1 } {11} @@ -361,3 +365,663 @@ do_execsql_test bitwise-not-zero { SELECT ~0 } {-1} + +do_execsql_test_tolerance pi { + SELECT pi() +} {3.14159265358979} $tolerance + + +do_execsql_test_tolerance acos-int { + SELECT acos(1) +} {0.0} $tolerance + +do_execsql_test_tolerance acos-float { + SELECT acos(-0.5) +} {2.0943951023931957} $tolerance + +do_execsql_test_tolerance acos-str { + SELECT acos('-0.5') +} {2.0943951023931957} $tolerance + +do_execsql_test_tolerance acos-null { + SELECT acos(null) +} {} $tolerance + + +do_execsql_test_tolerance acosh-int { + SELECT acosh(1) +} {0.0} $tolerance + +do_execsql_test_tolerance acosh-float { + SELECT acosh(1.5) +} {0.962423650119207} $tolerance + +do_execsql_test_tolerance acosh-str { + SELECT acosh('1.5') +} {0.962423650119207} $tolerance + +do_execsql_test_tolerance acosh-invalid { + SELECT acosh(0.99) +} {} $tolerance + +do_execsql_test_tolerance acosh-null { + SELECT acosh(null) +} {} $tolerance + + +do_execsql_test_tolerance asin-int { + SELECT asin(1) +} {1.5707963267948966} $tolerance + +do_execsql_test_tolerance asin-float { + SELECT asin(-0.5) +} {-0.5235987755982989} $tolerance + +do_execsql_test_tolerance asin-str { + SELECT asin('-0.5') +} {-0.5235987755982989} $tolerance + +do_execsql_test_tolerance asin-null { + SELECT asin(null) +} {} $tolerance + + +do_execsql_test_tolerance sin-int { + SELECT sin(1) +} {0.841470984807897} $tolerance + +do_execsql_test_tolerance sin-float { + SELECT sin(-0.5) +} {-0.479425538604203} $tolerance + +do_execsql_test_tolerance sin-str { + SELECT sin('-0.5') +} {-0.479425538604203} $tolerance + +do_execsql_test_tolerance sin-null { + SELECT sin(null) +} {} $tolerance + +do_execsql_test_tolerance sin-products-id { + SELECT sin(id) from products limit 5 +} {0.8414709848078965 +0.9092974268256817 +0.1411200080598672 +-0.7568024953079282 +-0.9589242746631385} $tolerance + + +do_execsql_test_tolerance asinh-int { + SELECT asinh(1) +} {0.881373587019543} $tolerance + +do_execsql_test_tolerance asinh-float { + SELECT asinh(-0.5) +} {-0.48121182505960347} $tolerance + +do_execsql_test_tolerance asinh-str { + SELECT asinh('-0.5') +} {-0.48121182505960347} $tolerance + +do_execsql_test_tolerance asinh-null { + SELECT asinh(null) +} {} $tolerance + + +do_execsql_test_tolerance atan-int { + SELECT atan(1) +} {0.7853981633974483} $tolerance + +do_execsql_test_tolerance atan-float { + SELECT atan(-0.5) +} {-0.4636476090008061} $tolerance + +do_execsql_test_tolerance atan-str { + SELECT atan('-0.5') +} {-0.4636476090008061} $tolerance + +do_execsql_test_tolerance atan-null { + SELECT atan(null) +} {} $tolerance + + +do_execsql_test_tolerance tan-int { + SELECT tan(1) +} {1.5574077246549} $tolerance + +do_execsql_test_tolerance tan-float { + SELECT tan(-0.5) +} {-0.54630248984379} $tolerance + +do_execsql_test_tolerance tan-str { + SELECT tan('-0.5') +} {-0.54630248984379} $tolerance + +do_execsql_test_tolerance tan-null { + SELECT tan(null) +} {} $tolerance + + +do_execsql_test_tolerance atanh-int { + SELECT atanh(0) +} {0.0} $tolerance + +do_execsql_test_tolerance atanh-float { + SELECT atanh(-0.5) +} {-0.5493061443340548} $tolerance + +do_execsql_test_tolerance atanh-str { + SELECT atanh('-0.5') +} {-0.5493061443340548} $tolerance + +do_execsql_test_tolerance atanh-null { + SELECT atanh(null) +} {} $tolerance + + +do_execsql_test ceil-int { + SELECT ceil(1) +} {1} + +do_execsql_test ceil-float { + SELECT ceil(-1.5) +} {-1.0} + +do_execsql_test ceil-str { + SELECT ceil('1.5') +} {2.0} + +do_execsql_test ceil-null { + SELECT ceil(null) +} {} + + +do_execsql_test ceiling-int { + SELECT ceiling(1) +} {1} + +do_execsql_test ceiling-float { + SELECT ceiling(-1.5) +} {-1.0} + +do_execsql_test ceiling-str { + SELECT ceiling('1.5') +} {2.0} + +do_execsql_test ceiling-null { + SELECT ceiling(null) +} {} + + +do_execsql_test_tolerance cos-int { + SELECT cos(1) +} {0.54030230586814} $tolerance + +do_execsql_test_tolerance cos-float { + SELECT cos(-0.5) +} {0.877582561890373} $tolerance + +do_execsql_test_tolerance cos-str { + SELECT cos('-0.5') +} {0.877582561890373} $tolerance + +do_execsql_test_tolerance cos-null { + SELECT cos(null) +} {} $tolerance + + +do_execsql_test_tolerance cosh-int { + SELECT cosh(1) +} {1.54308063481524} $tolerance + +do_execsql_test_tolerance cosh-float { + SELECT cosh(-0.5) +} {1.12762596520638} $tolerance + +do_execsql_test_tolerance cosh-str { + SELECT cosh('-0.5') +} {1.12762596520638} $tolerance + +do_execsql_test_tolerance cosh-null { + SELECT cosh(null) +} {} $tolerance + + +do_execsql_test_tolerance degrees-int { + SELECT degrees(1) +} {57.2957795130823} $tolerance + +do_execsql_test_tolerance degrees-float { + SELECT degrees(-0.5) +} {-28.6478897565412} $tolerance + +do_execsql_test_tolerance degrees-str { + SELECT degrees('-0.5') +} {-28.6478897565412} $tolerance + +do_execsql_test_tolerance degrees-null { + SELECT degrees(null) +} {} $tolerance + + +do_execsql_test_tolerance exp-int { + SELECT exp(1) +} {2.71828182845905} $tolerance + +do_execsql_test_tolerance exp-float { + SELECT exp(-0.5) +} {0.606530659712633} $tolerance + +do_execsql_test_tolerance exp-str { + SELECT exp('-0.5') +} {0.606530659712633} $tolerance + +do_execsql_test_tolerance exp-null { + SELECT exp(null) +} {} $tolerance + + +do_execsql_test floor-int { + SELECT floor(1) +} {1} + +do_execsql_test floor-float { + SELECT floor(-1.5) +} {-2.0} + +do_execsql_test floor-str { + SELECT floor('1.5') +} {1.0} + +do_execsql_test floor-null { + SELECT floor(null) +} {} + + +do_execsql_test_tolerance ln-int { + SELECT ln(1) +} {0.0} $tolerance + +do_execsql_test_tolerance ln-float { + SELECT ln(0.5) +} {-0.693147180559945} $tolerance + +do_execsql_test_tolerance ln-str { + SELECT ln('0.5') +} {-0.693147180559945} $tolerance + +do_execsql_test_tolerance ln-negative { + SELECT ln(-0.5) +} {} $tolerance + +do_execsql_test_tolerance ln-null { + SELECT ln(null) +} {} $tolerance + + +do_execsql_test_tolerance log10-int { + SELECT log10(1) +} {0.0} $tolerance + +do_execsql_test_tolerance log10-float { + SELECT log10(0.5) +} {-0.301029995663981} $tolerance + +do_execsql_test_tolerance log10-str { + SELECT log10('0.5') +} {-0.301029995663981} $tolerance + +do_execsql_test_tolerance log10-negative { + SELECT log10(-0.5) +} {} $tolerance + +do_execsql_test_tolerance log10-null { + SELECT log10(null) +} {} $tolerance + + +do_execsql_test_tolerance log2-int { + SELECT log2(1) +} {0.0} $tolerance + +do_execsql_test_tolerance log2-float { + SELECT log2(0.5) +} {-1.0} $tolerance + +do_execsql_test_tolerance log2-str { + SELECT log2('0.5') +} {-1.0} $tolerance + +do_execsql_test_tolerance log2-negative { + SELECT log2(-0.5) +} {} $tolerance + +do_execsql_test_tolerance log2-null { + SELECT log2(null) +} {} $tolerance + + +do_execsql_test_tolerance radians-int { + SELECT radians(1) +} {0.0174532925199433} $tolerance + +do_execsql_test_tolerance radians-float { + SELECT radians(-0.5) +} {-0.00872664625997165} $tolerance + +do_execsql_test_tolerance radians-str { + SELECT radians('-0.5') +} {-0.00872664625997165} $tolerance + +do_execsql_test_tolerance radians-null { + SELECT radians(null) +} {} $tolerance + + +do_execsql_test_tolerance sinh-int { + SELECT sinh(1) +} {1.1752011936438} $tolerance + +do_execsql_test_tolerance sinh-float { + SELECT sinh(-0.5) +} {-0.521095305493747} $tolerance + +do_execsql_test_tolerance sinh-str { + SELECT sinh('-0.5') +} {-0.521095305493747} $tolerance + +do_execsql_test_tolerance sinh-null { + SELECT sinh(null) +} {} $tolerance + + +do_execsql_test_tolerance sqrt-int { + SELECT sqrt(1) +} {1.0} $tolerance + +do_execsql_test_tolerance sqrt-float { + SELECT sqrt(0.5) +} {0.707106781186548} $tolerance + +do_execsql_test_tolerance sqrt-str { + SELECT sqrt('0.5') +} {0.707106781186548} $tolerance + +do_execsql_test_tolerance sqrt-negative { + SELECT sqrt(-0.5) +} {} $tolerance + +do_execsql_test_tolerance sqrt-null { + SELECT sqrt(null) +} {} $tolerance + + +do_execsql_test_tolerance tanh-int { + SELECT tanh(1) +} {0.761594155955765} $tolerance + +do_execsql_test_tolerance tanh-float { + SELECT tanh(-0.5) +} {-0.46211715726001} $tolerance + +do_execsql_test_tolerance tanh-str { + SELECT tanh('-0.5') +} {-0.46211715726001} $tolerance + +do_execsql_test_tolerance tanh-null { + SELECT tanh(null) +} {} $tolerance + + +do_execsql_test trunc-int { + SELECT trunc(1) +} {1} + +do_execsql_test trunc-float { + SELECT trunc(2.5) +} {2.0} + +do_execsql_test trunc-float-negative { + SELECT trunc(-2.5) +} {-2.0} + +do_execsql_test trunc-str { + SELECT trunc('2.5') +} {2.0} + +do_execsql_test trunc-null { + SELECT trunc(null) +} {} + + +do_execsql_test_tolerance atan2-int-int { + SELECT atan2(5, -1) +} {1.76819188664478} $tolerance + +do_execsql_test_tolerance atan2-int-float { + SELECT atan2(5, -1.5) +} {1.86225312127276} $tolerance + +do_execsql_test_tolerance atan2-int-str { + SELECT atan2(5, '-1.5') +} {1.86225312127276} $tolerance + +do_execsql_test_tolerance atan2-float-int { + SELECT atan2(5.5, 10) +} {0.502843210927861} $tolerance + +do_execsql_test_tolerance atan2-float-float { + SELECT atan2(5.5, -1.5) +} {1.83704837594582} $tolerance + +do_execsql_test_tolerance atan2-float-str { + SELECT atan2(5.5, '-1.5') +} {1.83704837594582} $tolerance + +do_execsql_test_tolerance atan2-str-str { + SELECT atan2('5.5', '-1.5') +} {1.83704837594582} $tolerance + +do_execsql_test atan2-null-int { + SELECT atan2(null, 5) +} {} + +do_execsql_test atan2-int-null { + SELECT atan2(5, null) +} {} + + +do_execsql_test_tolerance mod-int-int { + SELECT mod(10, -3) +} {1.0} $tolerance + +do_execsql_test_tolerance mod-int-float { + SELECT mod(5, -1.5) +} {0.5} $tolerance + +do_execsql_test_tolerance mod-int-str { + SELECT mod(5, '-1.5') +} {0.5} $tolerance + +do_execsql_test_tolerance mod-float-int { + SELECT mod(5.5, 2) +} {1.5} $tolerance + +do_execsql_test_tolerance mod-float-float { + SELECT mod(5.5, -1.5) +} {1.0} $tolerance + +do_execsql_test_tolerance mod-float-str { + SELECT mod(5.5, '-1.5') +} {1.0} $tolerance + +do_execsql_test_tolerance mod-str-str { + SELECT mod('5.5', '-1.5') +} {1.0} $tolerance + +do_execsql_test mod-null-int { + SELECT mod(null, 5) +} {} + +do_execsql_test mod-int-null { + SELECT mod(5, null) +} {} + +do_execsql_test mod-float-zero { + SELECT mod(1.5, 0) +} {} + +do_execsql_test mod-products-id { + SELECT mod(products.id, 3) from products limit 5 +} {1.0 +2.0 +0.0 +1.0 +2.0} + +do_execsql_test mod-products-price-id { + SELECT mod(products.price, products.id) from products limit 5 +} {0.0 +0.0 +0.0 +1.0 +4.0} + + +do_execsql_test_tolerance pow-int-int { + SELECT pow(5, -1) +} {0.2} $tolerance + +do_execsql_test_tolerance pow-int-float { + SELECT pow(5, -1.5) +} {0.0894427190999916} $tolerance + +do_execsql_test_tolerance pow-int-str { + SELECT pow(5, '-1.5') +} {0.0894427190999916} $tolerance + +do_execsql_test_tolerance pow-float-int { + SELECT pow(5.5, 2) +} {30.25} $tolerance + +do_execsql_test_tolerance pow-float-float { + SELECT pow(5.5, -1.5) +} {0.077527533220222} $tolerance + +do_execsql_test_tolerance pow-float-str { + SELECT pow(5.5, '-1.5') +} {0.077527533220222} $tolerance + +do_execsql_test_tolerance pow-str-str { + SELECT pow('5.5', '-1.5') +} {0.077527533220222} $tolerance + +do_execsql_test pow-null-int { + SELECT pow(null, 5) +} {} + +do_execsql_test pow-int-null { + SELECT pow(5, null) +} {} + + +do_execsql_test_tolerance power-int-int { + SELECT power(5, -1) +} {0.2} $tolerance + +do_execsql_test_tolerance power-int-float { + SELECT power(5, -1.5) +} {0.0894427190999916} $tolerance + +do_execsql_test_tolerance power-int-str { + SELECT power(5, '-1.5') +} {0.0894427190999916} $tolerance + +do_execsql_test_tolerance power-float-int { + SELECT power(5.5, 2) +} {30.25} $tolerance + +do_execsql_test_tolerance power-float-float { + SELECT power(5.5, -1.5) +} {0.077527533220222} $tolerance + +do_execsql_test_tolerance power-float-str { + SELECT power(5.5, '-1.5') +} {0.077527533220222} $tolerance + +do_execsql_test_tolerance power-str-str { + SELECT power('5.5', '-1.5') +} {0.077527533220222} $tolerance + +do_execsql_test power-null-int { + SELECT power(null, 5) +} {} + +do_execsql_test power-int-null { + SELECT power(5, null) +} {} + + +do_execsql_test_tolerance log-int { + SELECT log(1) +} {0.0} $tolerance + +do_execsql_test_tolerance log-float { + SELECT log(1.5) +} {0.176091259055681} $tolerance + +do_execsql_test_tolerance log-str { + SELECT log('1.5') +} {0.176091259055681} $tolerance + +do_execsql_test log-negative { + SELECT log(-1.5) +} {} + +do_execsql_test log-null { + SELECT log(null) +} {} + +do_execsql_test_tolerance log-int-int { + SELECT log(5, 1) +} {0.0} $tolerance + +do_execsql_test_tolerance log-int-float { + SELECT log(5, 1.5) +} {0.251929636412592} $tolerance + +do_execsql_test_tolerance log-int-str { + SELECT log(5, '1.5') +} {0.251929636412592} $tolerance + +do_execsql_test_tolerance log-float-int { + SELECT log(5.5, 10) +} {1.35068935021985} $tolerance + +do_execsql_test_tolerance log-float-float { + SELECT log(5.5, 1.5) +} {0.237844588273313} $tolerance + +do_execsql_test_tolerance log-float-str { + SELECT log(5.5, '1.5') +} {0.237844588273313} $tolerance + +do_execsql_test_tolerance log-str-str { + SELECT log('5.5', '1.5') +} {0.237844588273313} $tolerance + +do_execsql_test log-negative-negative { + SELECT log(-1.5, -1.5) +} {} + +do_execsql_test log-float-negative { + SELECT log(1.5, -1.5) +} {} + +do_execsql_test log-null-int { + SELECT log(null, 5) +} {} + +do_execsql_test log-int-null { + SELECT log(5, null) +} {} diff --git a/testing/tester.tcl b/testing/tester.tcl index 5ccb2165b..04a43c3eb 100644 --- a/testing/tester.tcl +++ b/testing/tester.tcl @@ -32,3 +32,40 @@ proc do_execsql_test_on_specific_db {db_name test_name sql_statements expected_o set combined_expected_output [join $expected_outputs "\n"] run_test $::sqlite_exec $db_name $combined_sql $combined_expected_output } + +proc within_tolerance {actual expected tolerance} { + expr {abs($actual - $expected) <= $tolerance} +} + +# This function is used to test floating point values within a tolerance +# FIXME: When Limbo's floating point presentation matches to SQLite, this could/should be removed +proc do_execsql_test_tolerance {test_name sql_statements expected_outputs tolerance} { + foreach db $::test_dbs { + puts [format "(%s) %s Running test: %s" $db [string repeat " " [expr {40 - [string length $db]}]] $test_name] + set combined_sql [string trim $sql_statements] + set actual_output [evaluate_sql $::sqlite_exec $db $combined_sql] + set actual_values [split $actual_output "\n"] + set expected_values [split $expected_outputs "\n"] + + if {[llength $actual_values] != [llength $expected_values]} { + puts "Test FAILED: '$sql_statements'" + puts "returned '$actual_output'" + puts "expected '$expected_outputs'" + exit 1 + } + + for {set i 0} {$i < [llength $actual_values]} {incr i} { + set actual [lindex $actual_values $i] + set expected [lindex $expected_values $i] + + if {![within_tolerance $actual $expected $tolerance]} { + set lower_bound [expr {$expected - $tolerance}] + set upper_bound [expr {$expected + $tolerance}] + puts "Test FAILED: '$sql_statements'" + puts "returned '$actual'" + puts "expected a value within the range \[$lower_bound, $upper_bound\]" + exit 1 + } + } + } +}