#![cfg(feature = "std")]
use graphitesql::{Connection, Value};
use std::process::Command;
fn sqlite3_available() -> bool {
Command::new("sqlite3").arg("--version").output().is_ok()
}
fn one(c: &Connection, sql: &str) -> Value {
c.query(sql).unwrap().rows[0][0].clone()
}
fn as_f64(v: &Value) -> Option<f64> {
match v {
Value::Real(r) => Some(*r),
Value::Integer(i) => Some(*i as f64),
_ => None,
}
}
#[test]
fn scalar_values() {
let c = Connection::open_memory().unwrap();
let approx = |sql: &str, want: f64| {
let got = as_f64(&one(&c, sql)).expect("numeric");
assert!(
(got - want).abs() <= 1e-12 * (1.0 + want.abs()),
"{sql}: got {got}, want {want}"
);
};
approx("SELECT pi()", std::f64::consts::PI);
approx("SELECT sqrt(2)", std::f64::consts::SQRT_2);
approx("SELECT sqrt(16)", 4.0);
approx("SELECT exp(1)", std::f64::consts::E);
approx("SELECT ln(10)", 10f64.ln());
approx("SELECT log(100)", 2.0); approx("SELECT log(2, 8)", 3.0); approx("SELECT log10(1000)", 3.0);
approx("SELECT log2(1024)", 10.0);
approx("SELECT pow(2, 10)", 1024.0);
approx("SELECT power(2, 0.5)", std::f64::consts::SQRT_2);
approx("SELECT sin(1)", 1f64.sin());
approx("SELECT cos(1)", 1f64.cos());
approx("SELECT tan(1)", 1f64.tan());
approx("SELECT asin(0.5)", 0.5f64.asin());
approx("SELECT acos(0.5)", 0.5f64.acos());
approx("SELECT atan(2)", 2f64.atan());
approx("SELECT atan2(1, 3)", 1f64.atan2(3.0));
approx("SELECT sinh(1)", 1f64.sinh());
approx("SELECT cosh(1)", 1f64.cosh());
approx("SELECT tanh(0.5)", 0.5f64.tanh());
approx("SELECT asinh(1)", 1f64.asinh());
approx("SELECT acosh(2)", 2f64.acosh());
approx("SELECT atanh(0.5)", 0.5f64.atanh());
approx("SELECT ceil(2.1)", 3.0);
approx("SELECT floor(-2.1)", -3.0);
approx("SELECT trunc(2.7)", 2.0);
approx("SELECT degrees(pi())", 180.0);
approx("SELECT radians(180)", std::f64::consts::PI);
approx("SELECT mod(7, 3)", 1.0);
}
#[test]
fn null_and_domain() {
let c = Connection::open_memory().unwrap();
assert_eq!(one(&c, "SELECT sqrt(NULL)"), Value::Null);
assert_eq!(one(&c, "SELECT sin(NULL)"), Value::Null);
assert_eq!(one(&c, "SELECT sqrt(-1)"), Value::Null);
assert_eq!(one(&c, "SELECT ln(0)"), Value::Null);
assert_eq!(one(&c, "SELECT ln(-2)"), Value::Null);
assert_eq!(one(&c, "SELECT acos(2)"), Value::Null);
}
#[test]
fn against_sqlite3() {
if !sqlite3_available() {
eprintln!("sqlite3 not found; skipping");
return;
}
let exprs = [
"sqrt(2)",
"sqrt(150.25)",
"exp(2.5)",
"ln(7)",
"log(1000)",
"log(2, 100)",
"log2(40)",
"pow(3, 4)",
"pow(2.0, 0.3)",
"sin(0.7)",
"cos(2.3)",
"tan(1.1)",
"asin(0.3)",
"acos(0.9)",
"atan(5)",
"atan2(2, 7)",
"sinh(2)",
"cosh(1.5)",
"tanh(0.9)",
"ceil(9.0001)",
"floor(9.999)",
"degrees(1)",
"radians(57)",
"mod(17.5, 4)",
];
let select = format!("SELECT {}", exprs.join(", "));
let out = Command::new("sqlite3")
.arg(":memory:")
.arg(&select)
.output()
.unwrap();
let want_line = String::from_utf8_lossy(&out.stdout).trim().to_string();
let want: Vec<f64> = want_line
.split('|')
.map(|s| s.parse::<f64>().unwrap())
.collect();
let c = Connection::open_memory().unwrap();
let r = c.query(&select).unwrap();
let mut failures = Vec::new();
for (i, e) in exprs.iter().enumerate() {
let got = as_f64(&r.rows[0][i]).unwrap();
let w = want[i];
if (got - w).abs() > 1e-11 * (1.0 + w.abs()) {
failures.push(format!(" {e}: sqlite={w}, graphite={got}"));
}
}
assert!(
failures.is_empty(),
"{} math exprs diverged:\n{}",
failures.len(),
failures.join("\n")
);
}
fn text(c: &Connection, sql: &str) -> String {
match c
.query(&format!("SELECT CAST(({sql}) AS TEXT)"))
.unwrap()
.rows[0][0]
.clone()
{
Value::Null => String::new(),
Value::Text(t) => t,
other => format!("{other:?}"),
}
}
#[test]
fn overflow_is_infinity() {
let c = Connection::open_memory().unwrap();
assert_eq!(one(&c, "SELECT exp(710)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT exp(1000)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT pow(2, 2000)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT pow(-2, 2000)"), Value::Real(f64::INFINITY));
assert_eq!(
one(&c, "SELECT pow(-2, 1025)"),
Value::Real(f64::NEG_INFINITY)
);
assert_eq!(one(&c, "SELECT pow(0, -1)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT pow(0, -0.5)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT sinh(800)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT cosh(800)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT sinh(-800)"), Value::Real(f64::NEG_INFINITY));
assert_eq!(one(&c, "SELECT exp(1e308)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT sinh(1e308)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT atanh(1)"), Value::Real(f64::INFINITY));
assert_eq!(one(&c, "SELECT atanh(-1)"), Value::Real(f64::NEG_INFINITY));
assert_eq!(one(&c, "SELECT exp(-745)"), Value::Real(f64::from_bits(1)));
assert_eq!(one(&c, "SELECT exp(-746)"), Value::Real(0.0));
assert_eq!(one(&c, "SELECT exp(-1000)"), Value::Real(0.0));
}
#[test]
fn domain_errors_are_null() {
let c = Connection::open_memory().unwrap();
for sql in [
"SELECT sqrt(-1)",
"SELECT ln(0)",
"SELECT ln(-1)",
"SELECT log10(0)",
"SELECT log2(0)",
"SELECT log(0)",
"SELECT log(0, 5)", "SELECT log(1, 5)", "SELECT log(5, 0)", "SELECT log(5, -1)",
"SELECT acos(2)",
"SELECT asin(-2)",
"SELECT acosh(0.5)",
"SELECT atanh(2)",
"SELECT pow(-8, 1.0/3)", ] {
assert_eq!(one(&c, sql), Value::Null, "{sql} should be NULL");
}
}
#[test]
fn rendering_matches_sqlite() {
let c = Connection::open_memory().unwrap();
assert_eq!(text(&c, "SELECT exp(1)"), "2.71828182845905");
assert_eq!(text(&c, "SELECT exp(709)"), "8.21840746155497e+307");
assert_eq!(text(&c, "SELECT pow(2, 0.5)"), "1.4142135623731");
assert_eq!(text(&c, "SELECT pow(0.5, -0.5)"), "1.4142135623731");
assert_eq!(text(&c, "SELECT exp(710)"), "Inf");
assert_eq!(text(&c, "SELECT sinh(-800)"), "-Inf");
}
#[test]
fn atan2_signed_zero_matches_sqlite() {
let c = Connection::open_memory().unwrap();
assert_eq!(text(&c, "SELECT atan2(-0.0, -1)"), "-3.14159265358979");
assert_eq!(text(&c, "SELECT atan2(0.0, -1)"), "3.14159265358979");
assert_eq!(text(&c, "SELECT atan2(0.0, -0.0)"), "3.14159265358979");
assert_eq!(text(&c, "SELECT atan2(-0.0, -0.0)"), "-3.14159265358979");
}
#[test]
fn modulo_operator_truncates_to_integer() {
let c = Connection::open_memory().unwrap();
let cell = |sql: &str| c.query(sql).unwrap().rows[0][0].clone();
assert_eq!(cell("SELECT 10 % 3"), Value::Integer(1));
assert_eq!(cell("SELECT 10.5 % 3"), Value::Real(1.0));
assert_eq!(cell("SELECT 10.9 % 3.9"), Value::Real(1.0));
assert_eq!(cell("SELECT -10.5 % 3"), Value::Real(-1.0));
assert_eq!(cell("SELECT 7 % 2.5"), Value::Real(1.0));
assert_eq!(cell("SELECT 5 % 1.5"), Value::Real(0.0));
assert_eq!(cell("SELECT 5 % 0"), Value::Null);
assert_eq!(cell("SELECT 5 % 0.5"), Value::Null);
assert_eq!(cell("SELECT mod(10.5, 3)"), Value::Real(1.5));
}