Skip to main content

fsqlite_func/
math.rs

1//! SQLite 3.35+ math functions (§13.2).
2//!
3//! All 30 math functions: trig, hyperbolic, rounding, log/exp, and misc.
4//! Always included in FrankenSQLite (no compile flag needed).
5//!
6//! # NaN / Inf semantics
7//! - NaN results are normalized to NULL.
8//! - +Inf / -Inf are valid REAL values and propagate.
9//! - Division by zero (mod) returns NULL.
10#![allow(
11    clippy::unnecessary_literal_bound,
12    clippy::too_many_lines,
13    clippy::cast_possible_truncation,
14    clippy::cast_possible_wrap,
15    clippy::suboptimal_flops,
16    clippy::unnecessary_wraps,
17    clippy::match_same_arms,
18    clippy::items_after_statements,
19    clippy::float_cmp
20)]
21
22use fsqlite_error::Result;
23use fsqlite_types::SqliteValue;
24
25use crate::{FunctionRegistry, ScalarFunction};
26
27// ── Helpers ───────────────────────────────────────────────────────────────
28
29/// Coerce a `SqliteValue` to `f64`. Returns `None` for `Null`, `Blob`, and for
30/// non-numeric text (SQLite math functions return NULL for these cases,
31/// per `sqlite3_value_numeric_type()` semantics in C SQLite).
32fn to_f64(v: &SqliteValue) -> Option<f64> {
33    match v {
34        SqliteValue::Null => None,
35        SqliteValue::Integer(i) => Some(*i as f64),
36        SqliteValue::Float(f) => Some(*f),
37        // SQLite trims leading/trailing whitespace via sqlite3AtoF before
38        // numeric conversion.  Non-numeric text produces NULL (not 0.0).
39        SqliteValue::Text(s) => {
40            let trimmed = s.trim();
41            // Reject non-finite (NaN/Inf) — sqlite3AtoF doesn't recognize them.
42            trimmed.parse::<f64>().ok().filter(|f| f.is_finite())
43        }
44        SqliteValue::Blob(_) => None,
45    }
46}
47
48/// Wrap an `f64` result: NaN → NULL, otherwise Float.
49fn wrap(v: f64) -> SqliteValue {
50    if v.is_nan() {
51        SqliteValue::Null
52    } else {
53        SqliteValue::Float(v)
54    }
55}
56
57/// One-arg math function that returns NULL on domain error (NaN).
58fn unary_math(args: &[SqliteValue], f: fn(f64) -> f64) -> Result<SqliteValue> {
59    let Some(x) = to_f64(&args[0]) else {
60        return Ok(SqliteValue::Null);
61    };
62    Ok(wrap(f(x)))
63}
64
65/// One-arg math with explicit domain check.  Returns NULL if `domain` is false.
66fn unary_domain(
67    args: &[SqliteValue],
68    domain: fn(f64) -> bool,
69    f: fn(f64) -> f64,
70) -> Result<SqliteValue> {
71    let Some(x) = to_f64(&args[0]) else {
72        return Ok(SqliteValue::Null);
73    };
74    if !domain(x) {
75        return Ok(SqliteValue::Null);
76    }
77    Ok(wrap(f(x)))
78}
79
80// ── Trigonometric ─────────────────────────────────────────────────────────
81
82pub struct AcosFunc;
83
84impl ScalarFunction for AcosFunc {
85    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
86        unary_domain(args, |x| (-1.0..=1.0).contains(&x), f64::acos)
87    }
88
89    fn num_args(&self) -> i32 {
90        1
91    }
92
93    fn name(&self) -> &str {
94        "acos"
95    }
96}
97
98pub struct AsinFunc;
99
100impl ScalarFunction for AsinFunc {
101    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
102        unary_domain(args, |x| (-1.0..=1.0).contains(&x), f64::asin)
103    }
104
105    fn num_args(&self) -> i32 {
106        1
107    }
108
109    fn name(&self) -> &str {
110        "asin"
111    }
112}
113
114pub struct AtanFunc;
115
116impl ScalarFunction for AtanFunc {
117    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
118        unary_math(args, f64::atan)
119    }
120
121    fn num_args(&self) -> i32 {
122        1
123    }
124
125    fn name(&self) -> &str {
126        "atan"
127    }
128}
129
130pub struct Atan2Func;
131
132impl ScalarFunction for Atan2Func {
133    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
134        let Some(y) = to_f64(&args[0]) else {
135            return Ok(SqliteValue::Null);
136        };
137        let Some(x) = to_f64(&args[1]) else {
138            return Ok(SqliteValue::Null);
139        };
140        Ok(wrap(y.atan2(x)))
141    }
142
143    fn num_args(&self) -> i32 {
144        2
145    }
146
147    fn name(&self) -> &str {
148        "atan2"
149    }
150}
151
152pub struct CosFunc;
153
154impl ScalarFunction for CosFunc {
155    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
156        unary_math(args, f64::cos)
157    }
158
159    fn num_args(&self) -> i32 {
160        1
161    }
162
163    fn name(&self) -> &str {
164        "cos"
165    }
166}
167
168pub struct SinFunc;
169
170impl ScalarFunction for SinFunc {
171    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
172        unary_math(args, f64::sin)
173    }
174
175    fn num_args(&self) -> i32 {
176        1
177    }
178
179    fn name(&self) -> &str {
180        "sin"
181    }
182}
183
184pub struct TanFunc;
185
186impl ScalarFunction for TanFunc {
187    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
188        unary_math(args, f64::tan)
189    }
190
191    fn num_args(&self) -> i32 {
192        1
193    }
194
195    fn name(&self) -> &str {
196        "tan"
197    }
198}
199
200// ── Hyperbolic ────────────────────────────────────────────────────────────
201
202pub struct AcoshFunc;
203
204impl ScalarFunction for AcoshFunc {
205    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
206        unary_domain(args, |x| x >= 1.0, f64::acosh)
207    }
208
209    fn num_args(&self) -> i32 {
210        1
211    }
212
213    fn name(&self) -> &str {
214        "acosh"
215    }
216}
217
218pub struct AsinhFunc;
219
220impl ScalarFunction for AsinhFunc {
221    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
222        unary_math(args, f64::asinh)
223    }
224
225    fn num_args(&self) -> i32 {
226        1
227    }
228
229    fn name(&self) -> &str {
230        "asinh"
231    }
232}
233
234pub struct AtanhFunc;
235
236impl ScalarFunction for AtanhFunc {
237    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
238        // Domain: (-1, 1) — open interval, atanh(1) and atanh(-1) are ±Inf
239        // but C sqlite returns NULL for these edge cases.
240        unary_domain(args, |x| x > -1.0 && x < 1.0, f64::atanh)
241    }
242
243    fn num_args(&self) -> i32 {
244        1
245    }
246
247    fn name(&self) -> &str {
248        "atanh"
249    }
250}
251
252pub struct CoshFunc;
253
254impl ScalarFunction for CoshFunc {
255    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
256        unary_math(args, f64::cosh)
257    }
258
259    fn num_args(&self) -> i32 {
260        1
261    }
262
263    fn name(&self) -> &str {
264        "cosh"
265    }
266}
267
268pub struct SinhFunc;
269
270impl ScalarFunction for SinhFunc {
271    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
272        unary_math(args, f64::sinh)
273    }
274
275    fn num_args(&self) -> i32 {
276        1
277    }
278
279    fn name(&self) -> &str {
280        "sinh"
281    }
282}
283
284pub struct TanhFunc;
285
286impl ScalarFunction for TanhFunc {
287    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
288        unary_math(args, f64::tanh)
289    }
290
291    fn num_args(&self) -> i32 {
292        1
293    }
294
295    fn name(&self) -> &str {
296        "tanh"
297    }
298}
299
300// ── Rounding ──────────────────────────────────────────────────────────────
301//
302// ceil/floor/trunc preserve INTEGER type for INTEGER input.
303
304pub struct CeilFunc;
305
306impl ScalarFunction for CeilFunc {
307    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
308        match &args[0] {
309            SqliteValue::Null => Ok(SqliteValue::Null),
310            SqliteValue::Integer(i) => Ok(SqliteValue::Integer(*i)),
311            other => {
312                let Some(x) = to_f64(other) else {
313                    return Ok(SqliteValue::Null);
314                };
315                Ok(wrap(x.ceil()))
316            }
317        }
318    }
319
320    fn num_args(&self) -> i32 {
321        1
322    }
323
324    fn name(&self) -> &str {
325        "ceil"
326    }
327}
328
329pub struct FloorFunc;
330
331impl ScalarFunction for FloorFunc {
332    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
333        match &args[0] {
334            SqliteValue::Null => Ok(SqliteValue::Null),
335            SqliteValue::Integer(i) => Ok(SqliteValue::Integer(*i)),
336            other => {
337                let Some(x) = to_f64(other) else {
338                    return Ok(SqliteValue::Null);
339                };
340                Ok(wrap(x.floor()))
341            }
342        }
343    }
344
345    fn num_args(&self) -> i32 {
346        1
347    }
348
349    fn name(&self) -> &str {
350        "floor"
351    }
352}
353
354pub struct TruncFunc;
355
356impl ScalarFunction for TruncFunc {
357    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
358        match &args[0] {
359            SqliteValue::Null => Ok(SqliteValue::Null),
360            SqliteValue::Integer(i) => Ok(SqliteValue::Integer(*i)),
361            other => {
362                let Some(x) = to_f64(other) else {
363                    return Ok(SqliteValue::Null);
364                };
365                Ok(wrap(x.trunc()))
366            }
367        }
368    }
369
370    fn num_args(&self) -> i32 {
371        1
372    }
373
374    fn name(&self) -> &str {
375        "trunc"
376    }
377}
378
379// ── Logarithmic / Exponential ─────────────────────────────────────────────
380
381pub struct LnFunc;
382
383impl ScalarFunction for LnFunc {
384    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
385        unary_domain(args, |x| x > 0.0, f64::ln)
386    }
387
388    fn num_args(&self) -> i32 {
389        1
390    }
391
392    fn name(&self) -> &str {
393        "ln"
394    }
395}
396
397/// `log(X)` — base-10 logarithm (single arg).
398pub struct Log10Func;
399
400impl ScalarFunction for Log10Func {
401    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
402        unary_domain(args, |x| x > 0.0, f64::log10)
403    }
404
405    fn num_args(&self) -> i32 {
406        1
407    }
408
409    fn name(&self) -> &str {
410        "log10"
411    }
412}
413
414/// `log(X)` with 1 arg = base-10.  `log(B, X)` with 2 args = base-B.
415pub struct LogFunc;
416
417impl ScalarFunction for LogFunc {
418    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
419        if args.len() == 1 {
420            // log(X) = log10(X)
421            return Log10Func.invoke(args);
422        }
423        // log(B, X) = ln(X) / ln(B)
424        let Some(b) = to_f64(&args[0]) else {
425            return Ok(SqliteValue::Null);
426        };
427        let Some(x) = to_f64(&args[1]) else {
428            return Ok(SqliteValue::Null);
429        };
430        if b <= 0.0 || b == 1.0 || x <= 0.0 {
431            return Ok(SqliteValue::Null);
432        }
433        Ok(wrap(x.ln() / b.ln()))
434    }
435
436    fn num_args(&self) -> i32 {
437        -1 // variadic: 1 or 2 args
438    }
439
440    fn min_args(&self) -> i32 {
441        1
442    }
443
444    fn max_args(&self) -> Option<i32> {
445        Some(2)
446    }
447
448    fn name(&self) -> &str {
449        "log"
450    }
451}
452
453pub struct Log2Func;
454
455impl ScalarFunction for Log2Func {
456    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
457        unary_domain(args, |x| x > 0.0, f64::log2)
458    }
459
460    fn num_args(&self) -> i32 {
461        1
462    }
463
464    fn name(&self) -> &str {
465        "log2"
466    }
467}
468
469pub struct ExpFunc;
470
471impl ScalarFunction for ExpFunc {
472    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
473        // exp overflow produces +Inf (valid REAL), so no domain check needed.
474        unary_math(args, f64::exp)
475    }
476
477    fn num_args(&self) -> i32 {
478        1
479    }
480
481    fn name(&self) -> &str {
482        "exp"
483    }
484}
485
486pub struct PowFunc;
487
488impl ScalarFunction for PowFunc {
489    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
490        let Some(x) = to_f64(&args[0]) else {
491            return Ok(SqliteValue::Null);
492        };
493        let Some(y) = to_f64(&args[1]) else {
494            return Ok(SqliteValue::Null);
495        };
496        Ok(wrap(x.powf(y)))
497    }
498
499    fn num_args(&self) -> i32 {
500        2
501    }
502
503    fn name(&self) -> &str {
504        "pow"
505    }
506}
507
508pub struct SqrtFunc;
509
510impl ScalarFunction for SqrtFunc {
511    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
512        unary_domain(args, |x| x >= 0.0, f64::sqrt)
513    }
514
515    fn num_args(&self) -> i32 {
516        1
517    }
518
519    fn name(&self) -> &str {
520        "sqrt"
521    }
522}
523
524// ── Other ─────────────────────────────────────────────────────────────────
525
526pub struct DegreesFunc;
527
528impl ScalarFunction for DegreesFunc {
529    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
530        unary_math(args, f64::to_degrees)
531    }
532
533    fn num_args(&self) -> i32 {
534        1
535    }
536
537    fn name(&self) -> &str {
538        "degrees"
539    }
540}
541
542pub struct RadiansFunc;
543
544impl ScalarFunction for RadiansFunc {
545    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
546        unary_math(args, f64::to_radians)
547    }
548
549    fn num_args(&self) -> i32 {
550        1
551    }
552
553    fn name(&self) -> &str {
554        "radians"
555    }
556}
557
558pub struct ModFunc;
559
560impl ScalarFunction for ModFunc {
561    fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
562        let Some(x) = to_f64(&args[0]) else {
563            return Ok(SqliteValue::Null);
564        };
565        let Some(y) = to_f64(&args[1]) else {
566            return Ok(SqliteValue::Null);
567        };
568        if y == 0.0 {
569            return Ok(SqliteValue::Null);
570        }
571        Ok(wrap(x % y))
572    }
573
574    fn num_args(&self) -> i32 {
575        2
576    }
577
578    fn name(&self) -> &str {
579        "mod"
580    }
581}
582
583pub struct PiFunc;
584
585impl ScalarFunction for PiFunc {
586    fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
587        Ok(SqliteValue::Float(std::f64::consts::PI))
588    }
589
590    fn num_args(&self) -> i32 {
591        0
592    }
593
594    fn name(&self) -> &str {
595        "pi"
596    }
597}
598
599// ── Registration ──────────────────────────────────────────────────────────
600
601/// Register all §13.2 math functions into the given registry.
602pub fn register_math_builtins(registry: &mut FunctionRegistry) {
603    // Trigonometric
604    registry.register_scalar(AcosFunc);
605    registry.register_scalar(AsinFunc);
606    registry.register_scalar(AtanFunc);
607    registry.register_scalar(Atan2Func);
608    registry.register_scalar(CosFunc);
609    registry.register_scalar(SinFunc);
610    registry.register_scalar(TanFunc);
611
612    // Hyperbolic
613    registry.register_scalar(AcoshFunc);
614    registry.register_scalar(AsinhFunc);
615    registry.register_scalar(AtanhFunc);
616    registry.register_scalar(CoshFunc);
617    registry.register_scalar(SinhFunc);
618    registry.register_scalar(TanhFunc);
619
620    // Rounding
621    registry.register_scalar(CeilFunc);
622    registry.register_scalar(FloorFunc);
623    registry.register_scalar(TruncFunc);
624
625    // Logarithmic / Exponential
626    registry.register_scalar(LnFunc);
627    registry.register_scalar(LogFunc);
628    registry.register_scalar(Log10Func);
629    registry.register_scalar(Log2Func);
630    registry.register_scalar(ExpFunc);
631    registry.register_scalar(PowFunc);
632    registry.register_scalar(SqrtFunc);
633
634    // Other
635    registry.register_scalar(DegreesFunc);
636    registry.register_scalar(RadiansFunc);
637    registry.register_scalar(ModFunc);
638    registry.register_scalar(PiFunc);
639
640    // Aliases
641    // "ceiling" → same as "ceil"
642    struct CeilingFunc;
643    impl ScalarFunction for CeilingFunc {
644        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
645            CeilFunc.invoke(args)
646        }
647
648        fn num_args(&self) -> i32 {
649            1
650        }
651
652        fn name(&self) -> &str {
653            "ceiling"
654        }
655    }
656    registry.register_scalar(CeilingFunc);
657
658    // "power" → same as "pow"
659    struct PowerFunc;
660    impl ScalarFunction for PowerFunc {
661        fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
662            PowFunc.invoke(args)
663        }
664
665        fn num_args(&self) -> i32 {
666            2
667        }
668
669        fn name(&self) -> &str {
670            "power"
671        }
672    }
673    registry.register_scalar(PowerFunc);
674}
675
676// ── Tests ─────────────────────────────────────────────────────────────────
677
678#[cfg(test)]
679mod tests {
680    use fsqlite_error::FrankenError;
681
682    use super::*;
683
684    // Tolerance for floating-point comparisons.
685    const EPS: f64 = 1e-12;
686
687    fn assert_float_eq(result: &SqliteValue, expected: f64) {
688        match result {
689            SqliteValue::Float(v) => {
690                assert!((v - expected).abs() < EPS, "expected {expected}, got {v}");
691            }
692            other => unreachable!("expected Float({expected}), got {other:?}"),
693        }
694    }
695
696    fn assert_null(result: &SqliteValue) {
697        assert_eq!(result, &SqliteValue::Null, "expected NULL");
698    }
699
700    fn float(v: f64) -> SqliteValue {
701        SqliteValue::Float(v)
702    }
703
704    fn int(v: i64) -> SqliteValue {
705        SqliteValue::Integer(v)
706    }
707
708    fn assert_wrong_arg_count(function: &dyn ScalarFunction, args: &[SqliteValue], name: &str) {
709        let err = function
710            .invoke(args)
711            .expect_err("wrong arity should return function error");
712        let expected = format!("wrong number of arguments to function {name}()");
713        assert!(
714            matches!(&err, FrankenError::FunctionError(message) if message == &expected),
715            "expected {expected:?}, got {err:?}"
716        );
717    }
718
719    fn null() -> SqliteValue {
720        SqliteValue::Null
721    }
722
723    // ── Trigonometric ─────────────────────────────────────────────────
724
725    #[test]
726    fn test_acos_valid() {
727        let r = AcosFunc.invoke(&[float(0.5)]).unwrap();
728        assert_float_eq(&r, 0.5_f64.acos());
729    }
730
731    #[test]
732    fn test_acos_domain_error() {
733        assert_null(&AcosFunc.invoke(&[float(2.0)]).unwrap());
734    }
735
736    #[test]
737    fn test_acos_null() {
738        assert_null(&AcosFunc.invoke(&[null()]).unwrap());
739    }
740
741    #[test]
742    fn test_acosh_valid() {
743        let r = AcoshFunc.invoke(&[float(2.0)]).unwrap();
744        assert_float_eq(&r, 2.0_f64.acosh());
745    }
746
747    #[test]
748    fn test_acosh_domain_error() {
749        assert_null(&AcoshFunc.invoke(&[float(0.5)]).unwrap());
750    }
751
752    #[test]
753    fn test_asin_valid() {
754        let r = AsinFunc.invoke(&[float(0.5)]).unwrap();
755        assert_float_eq(&r, 0.5_f64.asin());
756    }
757
758    #[test]
759    fn test_asin_domain_error() {
760        assert_null(&AsinFunc.invoke(&[float(2.0)]).unwrap());
761    }
762
763    #[test]
764    fn test_asinh_all_reals() {
765        let r = AsinhFunc.invoke(&[float(1.0)]).unwrap();
766        assert_float_eq(&r, 1.0_f64.asinh());
767    }
768
769    #[test]
770    fn test_atan_basic() {
771        let r = AtanFunc.invoke(&[float(1.0)]).unwrap();
772        assert_float_eq(&r, std::f64::consts::FRAC_PI_4);
773    }
774
775    #[test]
776    fn test_atan2_quadrants() {
777        // Q1: atan2(1, 1) ≈ π/4
778        let r = Atan2Func.invoke(&[float(1.0), float(1.0)]).unwrap();
779        assert_float_eq(&r, std::f64::consts::FRAC_PI_4);
780
781        // Q2: atan2(1, -1) ≈ 3π/4
782        let r = Atan2Func.invoke(&[float(1.0), float(-1.0)]).unwrap();
783        assert_float_eq(&r, 3.0 * std::f64::consts::FRAC_PI_4);
784
785        // Q3: atan2(-1, -1) ≈ -3π/4
786        let r = Atan2Func.invoke(&[float(-1.0), float(-1.0)]).unwrap();
787        assert_float_eq(&r, -3.0 * std::f64::consts::FRAC_PI_4);
788
789        // Q4: atan2(-1, 1) ≈ -π/4
790        let r = Atan2Func.invoke(&[float(-1.0), float(1.0)]).unwrap();
791        assert_float_eq(&r, -std::f64::consts::FRAC_PI_4);
792    }
793
794    #[test]
795    fn test_atanh_valid() {
796        let r = AtanhFunc.invoke(&[float(0.5)]).unwrap();
797        assert_float_eq(&r, 0.5_f64.atanh());
798    }
799
800    #[test]
801    fn test_atanh_domain_error() {
802        // atanh(1.0) is outside the open interval (-1, 1)
803        assert_null(&AtanhFunc.invoke(&[float(1.0)]).unwrap());
804        assert_null(&AtanhFunc.invoke(&[float(-1.0)]).unwrap());
805    }
806
807    #[test]
808    fn test_cos_zero() {
809        let r = CosFunc.invoke(&[float(0.0)]).unwrap();
810        assert_float_eq(&r, 1.0);
811    }
812
813    #[test]
814    fn test_cosh_zero() {
815        let r = CoshFunc.invoke(&[float(0.0)]).unwrap();
816        assert_float_eq(&r, 1.0);
817    }
818
819    #[test]
820    fn test_sin_zero() {
821        let r = SinFunc.invoke(&[float(0.0)]).unwrap();
822        assert_float_eq(&r, 0.0);
823    }
824
825    #[test]
826    fn test_sinh_zero() {
827        let r = SinhFunc.invoke(&[float(0.0)]).unwrap();
828        assert_float_eq(&r, 0.0);
829    }
830
831    #[test]
832    fn test_tan_zero() {
833        let r = TanFunc.invoke(&[float(0.0)]).unwrap();
834        assert_float_eq(&r, 0.0);
835    }
836
837    #[test]
838    fn test_tanh_zero() {
839        let r = TanhFunc.invoke(&[float(0.0)]).unwrap();
840        assert_float_eq(&r, 0.0);
841    }
842
843    // ── Rounding ──────────────────────────────────────────────────────
844
845    #[test]
846    fn test_ceil_real() {
847        let r = CeilFunc.invoke(&[float(1.2)]).unwrap();
848        assert_float_eq(&r, 2.0);
849    }
850
851    #[test]
852    fn test_ceil_integer_type() {
853        let r = CeilFunc.invoke(&[int(5)]).unwrap();
854        assert_eq!(r, SqliteValue::Integer(5));
855    }
856
857    #[test]
858    fn test_ceil_negative() {
859        let r = CeilFunc.invoke(&[float(-1.2)]).unwrap();
860        assert_float_eq(&r, -1.0);
861    }
862
863    #[test]
864    fn test_floor_real() {
865        let r = FloorFunc.invoke(&[float(1.7)]).unwrap();
866        assert_float_eq(&r, 1.0);
867    }
868
869    #[test]
870    fn test_floor_integer_type() {
871        let r = FloorFunc.invoke(&[int(5)]).unwrap();
872        assert_eq!(r, SqliteValue::Integer(5));
873    }
874
875    #[test]
876    fn test_floor_negative() {
877        let r = FloorFunc.invoke(&[float(-1.2)]).unwrap();
878        assert_float_eq(&r, -2.0);
879    }
880
881    #[test]
882    fn test_trunc_positive() {
883        let r = TruncFunc.invoke(&[float(2.9)]).unwrap();
884        assert_float_eq(&r, 2.0);
885    }
886
887    #[test]
888    fn test_trunc_negative() {
889        let r = TruncFunc.invoke(&[float(-2.9)]).unwrap();
890        assert_float_eq(&r, -2.0);
891    }
892
893    #[test]
894    fn test_trunc_integer_type() {
895        let r = TruncFunc.invoke(&[int(5)]).unwrap();
896        assert_eq!(r, SqliteValue::Integer(5));
897    }
898
899    // ── Logarithmic / Exponential ─────────────────────────────────────
900
901    #[test]
902    fn test_ln_positive() {
903        let r = LnFunc.invoke(&[float(std::f64::consts::E)]).unwrap();
904        assert_float_eq(&r, 1.0);
905    }
906
907    #[test]
908    fn test_ln_zero() {
909        assert_null(&LnFunc.invoke(&[float(0.0)]).unwrap());
910    }
911
912    #[test]
913    fn test_ln_negative() {
914        assert_null(&LnFunc.invoke(&[float(-1.0)]).unwrap());
915    }
916
917    #[test]
918    fn test_log_single_arg_base10() {
919        let r = LogFunc.invoke(&[float(100.0)]).unwrap();
920        assert_float_eq(&r, 2.0);
921    }
922
923    #[test]
924    fn test_log_two_arg_base() {
925        let r = LogFunc.invoke(&[float(2.0), float(8.0)]).unwrap();
926        assert_float_eq(&r, 3.0);
927    }
928
929    #[test]
930    fn test_log10_alias() {
931        let r = Log10Func.invoke(&[float(1000.0)]).unwrap();
932        assert_float_eq(&r, 3.0);
933    }
934
935    #[test]
936    fn test_log2_basic() {
937        let r = Log2Func.invoke(&[float(8.0)]).unwrap();
938        assert_float_eq(&r, 3.0);
939    }
940
941    #[test]
942    fn test_exp_one() {
943        let r = ExpFunc.invoke(&[float(1.0)]).unwrap();
944        assert_float_eq(&r, std::f64::consts::E);
945    }
946
947    #[test]
948    fn test_exp_overflow() {
949        let r = ExpFunc.invoke(&[float(1000.0)]).unwrap();
950        match r {
951            SqliteValue::Float(v) => assert!(v.is_infinite() && v > 0.0, "+Inf expected"),
952            other => unreachable!("expected +Inf Float, got {other:?}"),
953        }
954    }
955
956    #[test]
957    fn test_pow_basic() {
958        let r = PowFunc.invoke(&[float(2.0), float(10.0)]).unwrap();
959        assert_float_eq(&r, 1024.0);
960    }
961
962    #[test]
963    fn test_pow_zero_zero() {
964        let r = PowFunc.invoke(&[float(0.0), float(0.0)]).unwrap();
965        assert_float_eq(&r, 1.0);
966    }
967
968    #[test]
969    fn test_power_alias() {
970        // power is registered via register_math_builtins as an alias.
971        // Test the underlying PowFunc directly.
972        let r = PowFunc.invoke(&[float(3.0), float(2.0)]).unwrap();
973        assert_float_eq(&r, 9.0);
974    }
975
976    #[test]
977    fn test_sqrt_positive() {
978        let r = SqrtFunc.invoke(&[float(144.0)]).unwrap();
979        assert_float_eq(&r, 12.0);
980    }
981
982    #[test]
983    fn test_sqrt_negative() {
984        assert_null(&SqrtFunc.invoke(&[float(-1.0)]).unwrap());
985    }
986
987    // ── Other ─────────────────────────────────────────────────────────
988
989    #[test]
990    #[allow(clippy::approx_constant)]
991    fn test_degrees_pi() {
992        let r = DegreesFunc.invoke(&[float(std::f64::consts::PI)]).unwrap();
993        assert_float_eq(&r, 180.0);
994    }
995
996    #[test]
997    #[allow(clippy::approx_constant)]
998    fn test_radians_180() {
999        let r = RadiansFunc.invoke(&[float(180.0)]).unwrap();
1000        assert_float_eq(&r, std::f64::consts::PI);
1001    }
1002
1003    #[test]
1004    fn test_mod_basic() {
1005        let r = ModFunc.invoke(&[float(10.0), float(3.0)]).unwrap();
1006        assert_float_eq(&r, 1.0);
1007    }
1008
1009    #[test]
1010    fn test_mod_zero_divisor() {
1011        assert_null(&ModFunc.invoke(&[float(10.0), float(0.0)]).unwrap());
1012    }
1013
1014    #[test]
1015    fn test_pi_precision() {
1016        let r = PiFunc.invoke(&[]).unwrap();
1017        assert_float_eq(&r, std::f64::consts::PI);
1018    }
1019
1020    // ── NaN / Inf ─────────────────────────────────────────────────────
1021
1022    #[test]
1023    fn test_nan_normalized_to_null() {
1024        // Any operation producing NaN must return NULL.
1025        // sqrt(-1) → NULL via domain check.
1026        assert_null(&SqrtFunc.invoke(&[float(-1.0)]).unwrap());
1027        // acos(2) → NULL via domain check.
1028        assert_null(&AcosFunc.invoke(&[float(2.0)]).unwrap());
1029    }
1030
1031    #[test]
1032    fn test_inf_propagation() {
1033        // exp(1000) = +Inf (valid REAL)
1034        let r = ExpFunc.invoke(&[float(1000.0)]).unwrap();
1035        match r {
1036            SqliteValue::Float(v) => assert!(v.is_infinite()),
1037            other => unreachable!("expected Inf, got {other:?}"),
1038        }
1039    }
1040
1041    #[test]
1042    fn test_neg_inf_propagation() {
1043        // exp(-1000) is tiny but not -Inf.
1044        // -exp(1000) would be -Inf, but we test sinh(large) which overflows.
1045        let r = SinhFunc.invoke(&[float(1000.0)]).unwrap();
1046        match r {
1047            SqliteValue::Float(v) => assert!(v.is_infinite() && v > 0.0),
1048            other => unreachable!("expected +Inf, got {other:?}"),
1049        }
1050        let r = SinhFunc.invoke(&[float(-1000.0)]).unwrap();
1051        match r {
1052            SqliteValue::Float(v) => assert!(v.is_infinite() && v < 0.0),
1053            other => unreachable!("expected -Inf, got {other:?}"),
1054        }
1055    }
1056
1057    #[test]
1058    fn test_ceiling_alias() {
1059        // "ceiling" is registered as alias for "ceil" via register_math_builtins.
1060        // Here we test functionally: same result.
1061        let r1 = CeilFunc.invoke(&[float(1.2)]).unwrap();
1062        // CeilingFunc is defined inside register_math_builtins, so we test
1063        // via the registry instead.
1064        let mut reg = FunctionRegistry::new();
1065        register_math_builtins(&mut reg);
1066        let ceiling = reg.find_scalar("ceiling", 1).expect("ceiling registered");
1067        let r2 = ceiling.invoke(&[float(1.2)]).unwrap();
1068        assert_eq!(r1, r2);
1069    }
1070
1071    #[test]
1072    fn test_all_null_input() {
1073        // All math functions return NULL for NULL input.
1074        let n = &[null()];
1075        assert_null(&AcosFunc.invoke(n).unwrap());
1076        assert_null(&AsinFunc.invoke(n).unwrap());
1077        assert_null(&AtanFunc.invoke(n).unwrap());
1078        assert_null(&CosFunc.invoke(n).unwrap());
1079        assert_null(&SinFunc.invoke(n).unwrap());
1080        assert_null(&TanFunc.invoke(n).unwrap());
1081        assert_null(&AcoshFunc.invoke(n).unwrap());
1082        assert_null(&AsinhFunc.invoke(n).unwrap());
1083        assert_null(&AtanhFunc.invoke(n).unwrap());
1084        assert_null(&CoshFunc.invoke(n).unwrap());
1085        assert_null(&SinhFunc.invoke(n).unwrap());
1086        assert_null(&TanhFunc.invoke(n).unwrap());
1087        assert_null(&CeilFunc.invoke(n).unwrap());
1088        assert_null(&FloorFunc.invoke(n).unwrap());
1089        assert_null(&TruncFunc.invoke(n).unwrap());
1090        assert_null(&LnFunc.invoke(n).unwrap());
1091        assert_null(&Log10Func.invoke(n).unwrap());
1092        assert_null(&Log2Func.invoke(n).unwrap());
1093        assert_null(&ExpFunc.invoke(n).unwrap());
1094        assert_null(&SqrtFunc.invoke(n).unwrap());
1095        assert_null(&DegreesFunc.invoke(n).unwrap());
1096        assert_null(&RadiansFunc.invoke(n).unwrap());
1097        // Two-arg functions with NULL
1098        assert_null(&Atan2Func.invoke(&[null(), float(1.0)]).unwrap());
1099        assert_null(&Atan2Func.invoke(&[float(1.0), null()]).unwrap());
1100        assert_null(&PowFunc.invoke(&[null(), float(1.0)]).unwrap());
1101        assert_null(&ModFunc.invoke(&[null(), float(1.0)]).unwrap());
1102        // log with NULL
1103        assert_null(&LogFunc.invoke(&[null()]).unwrap());
1104        assert_null(&LogFunc.invoke(&[null(), float(8.0)]).unwrap());
1105        assert_null(&LogFunc.invoke(&[float(2.0), null()]).unwrap());
1106    }
1107
1108    #[test]
1109    fn test_blob_input_returns_null() {
1110        let blob = SqliteValue::Blob(vec![b'1', b'2'].into());
1111        assert_null(&SqrtFunc.invoke(std::slice::from_ref(&blob)).unwrap());
1112        assert_null(&CeilFunc.invoke(std::slice::from_ref(&blob)).unwrap());
1113        assert_null(&Atan2Func.invoke(&[blob.clone(), float(1.0)]).unwrap());
1114        assert_null(&PowFunc.invoke(&[float(2.0), blob.clone()]).unwrap());
1115        assert_null(&LogFunc.invoke(&[blob.clone()]).unwrap());
1116        assert_null(&ModFunc.invoke(&[float(4.0), blob]).unwrap());
1117    }
1118
1119    #[test]
1120    fn test_register_math_builtins_all_present() {
1121        let mut reg = FunctionRegistry::new();
1122        register_math_builtins(&mut reg);
1123
1124        let expected = [
1125            ("acos", 1),
1126            ("asin", 1),
1127            ("atan", 1),
1128            ("atan2", 2),
1129            ("cos", 1),
1130            ("sin", 1),
1131            ("tan", 1),
1132            ("acosh", 1),
1133            ("asinh", 1),
1134            ("atanh", 1),
1135            ("cosh", 1),
1136            ("sinh", 1),
1137            ("tanh", 1),
1138            ("ceil", 1),
1139            ("ceiling", 1),
1140            ("floor", 1),
1141            ("trunc", 1),
1142            ("ln", 1),
1143            ("log10", 1),
1144            ("log2", 1),
1145            ("exp", 1),
1146            ("pow", 2),
1147            ("power", 2),
1148            ("sqrt", 1),
1149            ("degrees", 1),
1150            ("radians", 1),
1151            ("mod", 2),
1152            ("pi", 0),
1153        ];
1154
1155        for (name, arity) in expected {
1156            assert!(
1157                reg.find_scalar(name, arity).is_some(),
1158                "math function '{name}/{arity}' not registered"
1159            );
1160        }
1161
1162        // log is variadic (-1), test lookup with 1 and 2 args
1163        let log0 = reg
1164            .find_scalar("log", 0)
1165            .expect("log/0 returns erroring scalar");
1166        assert_wrong_arg_count(log0.as_ref(), &[], "log");
1167        assert!(reg.find_scalar("log", 1).is_some(), "log/1 via variadic");
1168        assert!(reg.find_scalar("log", 2).is_some(), "log/2 via variadic");
1169        let log3 = reg
1170            .find_scalar("log", 3)
1171            .expect("log/3 returns erroring scalar");
1172        assert_wrong_arg_count(
1173            log3.as_ref(),
1174            &[SqliteValue::Null, SqliteValue::Null, SqliteValue::Null],
1175            "log",
1176        );
1177    }
1178
1179    #[test]
1180    fn test_e2e_registry_invoke_math() {
1181        let mut reg = FunctionRegistry::new();
1182        register_math_builtins(&mut reg);
1183
1184        // pi() through registry
1185        let pi = reg.find_scalar("pi", 0).unwrap();
1186        let r = pi.invoke(&[]).unwrap();
1187        assert_float_eq(&r, std::f64::consts::PI);
1188
1189        // sqrt(144) through registry
1190        let sqrt = reg.find_scalar("sqrt", 1).unwrap();
1191        let r = sqrt.invoke(&[float(144.0)]).unwrap();
1192        assert_float_eq(&r, 12.0);
1193
1194        // log(2, 8) = 3.0 through registry
1195        let log = reg.find_scalar("log", 2).unwrap();
1196        let r = log.invoke(&[float(2.0), float(8.0)]).unwrap();
1197        assert_float_eq(&r, 3.0);
1198    }
1199}