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