Skip to main content

fsqlite_func/
agg_builtins.rs

1//! Built-in aggregate functions (§13.4).
2//!
3//! Implements: avg, count, group_concat, string_agg, max, min, sum, total,
4//! median, percentile, percentile_cont, percentile_disc.
5//!
6//! # NULL handling
7//! All aggregate functions skip NULL values (except `count(*)` which counts
8//! all rows). Empty-set behavior:
9//! - avg / sum / max / min / median → NULL
10//! - total → 0.0
11//! - count → 0
12#![allow(
13    clippy::unnecessary_literal_bound,
14    clippy::too_many_lines,
15    clippy::cast_possible_truncation,
16    clippy::cast_possible_wrap,
17    clippy::cast_precision_loss,
18    clippy::match_same_arms,
19    clippy::items_after_statements,
20    clippy::float_cmp,
21    clippy::cast_sign_loss,
22    clippy::suboptimal_flops
23)]
24
25use fsqlite_error::{FrankenError, Result};
26use fsqlite_types::SqliteValue;
27
28use crate::{AggregateFunction, FunctionRegistry};
29
30// ─── Kahan compensated summation ──────────────────────────────────────────
31
32/// Kahan-Babuska-Neumaier compensated summation step.  Uses magnitude-aware
33/// error term selection to match the precision behavior of C SQLite's
34/// `kahanBabuskaNeumaierStep` aggregate helper.
35#[inline]
36fn kahan_add(sum: &mut f64, compensation: &mut f64, value: f64) {
37    let s = *sum;
38    let t = s + value;
39    if s.abs() > value.abs() {
40        *compensation += (s - t) + value;
41    } else {
42        *compensation += (value - t) + s;
43    }
44    *sum = t;
45}
46
47// ═══════════════════════════════════════════════════════════════════════════
48// avg(X)
49// ═══════════════════════════════════════════════════════════════════════════
50
51pub struct AvgState {
52    sum: f64,
53    compensation: f64,
54    count: i64,
55}
56
57pub struct AvgFunc;
58
59impl AggregateFunction for AvgFunc {
60    type State = AvgState;
61
62    fn initial_state(&self) -> Self::State {
63        AvgState {
64            sum: 0.0,
65            compensation: 0.0,
66            count: 0,
67        }
68    }
69
70    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
71        if !args[0].is_null() {
72            kahan_add(&mut state.sum, &mut state.compensation, args[0].to_float());
73            state.count += 1;
74        }
75        Ok(())
76    }
77
78    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
79        if state.count == 0 {
80            Ok(SqliteValue::Null)
81        } else {
82            Ok(SqliteValue::Float(
83                (state.sum + state.compensation) / state.count as f64,
84            ))
85        }
86    }
87
88    fn num_args(&self) -> i32 {
89        1
90    }
91
92    fn name(&self) -> &str {
93        "avg"
94    }
95}
96
97// ═══════════════════════════════════════════════════════════════════════════
98// count(*) and count(X)
99// ═══════════════════════════════════════════════════════════════════════════
100
101/// `count(*)` — counts all rows including those with NULL values.
102pub struct CountStarFunc;
103
104impl AggregateFunction for CountStarFunc {
105    type State = i64;
106
107    fn initial_state(&self) -> Self::State {
108        0
109    }
110
111    fn step(&self, state: &mut Self::State, _args: &[SqliteValue]) -> Result<()> {
112        *state += 1;
113        Ok(())
114    }
115
116    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
117        Ok(SqliteValue::Integer(state))
118    }
119
120    fn num_args(&self) -> i32 {
121        0 // count(*) takes no column argument
122    }
123
124    fn name(&self) -> &str {
125        "count"
126    }
127}
128
129/// `count(X)` — counts non-NULL values of X.
130pub struct CountFunc;
131
132impl AggregateFunction for CountFunc {
133    type State = i64;
134
135    fn initial_state(&self) -> Self::State {
136        0
137    }
138
139    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
140        if !args[0].is_null() {
141            *state += 1;
142        }
143        Ok(())
144    }
145
146    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
147        Ok(SqliteValue::Integer(state))
148    }
149
150    fn num_args(&self) -> i32 {
151        1
152    }
153
154    fn name(&self) -> &str {
155        "count"
156    }
157}
158
159// ═══════════════════════════════════════════════════════════════════════════
160// group_concat(X [, SEP])
161// ═══════════════════════════════════════════════════════════════════════════
162
163pub struct GroupConcatState {
164    /// Incrementally built result string.  C SQLite appends
165    /// `separator + value` at each step (separator only before 2nd+ value),
166    /// using the separator from *that row's* argument, not a single global one.
167    result: String,
168    has_value: bool,
169}
170
171pub struct GroupConcatFunc;
172
173#[inline]
174fn push_group_concat_text(result: &mut String, value: &SqliteValue) {
175    if let Some(text) = value.as_text_str() {
176        result.push_str(text);
177    } else {
178        result.push_str(&value.to_text());
179    }
180}
181
182impl AggregateFunction for GroupConcatFunc {
183    type State = GroupConcatState;
184
185    fn initial_state(&self) -> Self::State {
186        GroupConcatState {
187            result: String::new(),
188            has_value: false,
189        }
190    }
191
192    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
193        if args[0].is_null() {
194            return Ok(());
195        }
196        if state.has_value {
197            match args.get(1) {
198                Some(separator) if !separator.is_null() => {
199                    push_group_concat_text(&mut state.result, separator);
200                }
201                Some(_) => {}
202                None => state.result.push(','),
203            }
204        }
205        push_group_concat_text(&mut state.result, &args[0]);
206        state.has_value = true;
207        Ok(())
208    }
209
210    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
211        if state.has_value {
212            Ok(SqliteValue::Text(state.result.into()))
213        } else {
214            Ok(SqliteValue::Null)
215        }
216    }
217
218    fn num_args(&self) -> i32 {
219        -1 // 1 or 2 args
220    }
221
222    fn min_args(&self) -> i32 {
223        1
224    }
225
226    fn max_args(&self) -> Option<i32> {
227        Some(2)
228    }
229
230    fn name(&self) -> &str {
231        "group_concat"
232    }
233}
234
235// ═══════════════════════════════════════════════════════════════════════════
236// max(X) — aggregate, single arg
237// ═══════════════════════════════════════════════════════════════════════════
238
239pub struct AggMaxFunc;
240
241impl AggregateFunction for AggMaxFunc {
242    type State = Option<SqliteValue>;
243
244    fn initial_state(&self) -> Self::State {
245        None
246    }
247
248    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
249        if args[0].is_null() {
250            return Ok(());
251        }
252        let candidate = &args[0];
253        match state {
254            None => *state = Some(candidate.clone()),
255            Some(current) => {
256                if candidate > current {
257                    *state = Some(candidate.clone());
258                }
259            }
260        }
261        Ok(())
262    }
263
264    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
265        Ok(state.unwrap_or(SqliteValue::Null))
266    }
267
268    fn num_args(&self) -> i32 {
269        1
270    }
271
272    fn name(&self) -> &str {
273        "max"
274    }
275}
276
277// ═══════════════════════════════════════════════════════════════════════════
278// min(X) — aggregate, single arg
279// ═══════════════════════════════════════════════════════════════════════════
280
281pub struct AggMinFunc;
282
283impl AggregateFunction for AggMinFunc {
284    type State = Option<SqliteValue>;
285
286    fn initial_state(&self) -> Self::State {
287        None
288    }
289
290    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
291        if args[0].is_null() {
292            return Ok(());
293        }
294        let candidate = &args[0];
295        match state {
296            None => *state = Some(candidate.clone()),
297            Some(current) => {
298                if candidate < current {
299                    *state = Some(candidate.clone());
300                }
301            }
302        }
303        Ok(())
304    }
305
306    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
307        Ok(state.unwrap_or(SqliteValue::Null))
308    }
309
310    fn num_args(&self) -> i32 {
311        1
312    }
313
314    fn name(&self) -> &str {
315        "min"
316    }
317}
318
319// ═══════════════════════════════════════════════════════════════════════════
320// sum(X)
321// ═══════════════════════════════════════════════════════════════════════════
322
323/// State for `sum()`: tracks whether all values are integers, the running
324/// integer sum, and the float sum as fallback.  Uses Kahan compensated
325/// summation for the float path to match C SQLite's precision.
326pub struct SumState {
327    int_sum: i64,
328    float_sum: f64,
329    float_compensation: f64,
330    all_integer: bool,
331    has_values: bool,
332    overflowed: bool,
333}
334
335pub struct SumFunc;
336
337impl AggregateFunction for SumFunc {
338    type State = SumState;
339
340    fn initial_state(&self) -> Self::State {
341        SumState {
342            int_sum: 0,
343            float_sum: 0.0,
344            float_compensation: 0.0,
345            all_integer: true,
346            has_values: false,
347            overflowed: false,
348        }
349    }
350
351    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
352        let value = args[0].to_sum_numeric_value();
353        if value.is_null() {
354            return Ok(());
355        }
356        state.has_values = true;
357        match value {
358            SqliteValue::Integer(i) => {
359                if state.all_integer && !state.overflowed {
360                    match state.int_sum.checked_add(i) {
361                        Some(s) => state.int_sum = s,
362                        None => state.overflowed = true,
363                    }
364                }
365                kahan_add(
366                    &mut state.float_sum,
367                    &mut state.float_compensation,
368                    i as f64,
369                );
370            }
371            SqliteValue::Float(f) => {
372                state.all_integer = false;
373                kahan_add(&mut state.float_sum, &mut state.float_compensation, f);
374            }
375            SqliteValue::Null | SqliteValue::Text(_) | SqliteValue::Blob(_) => {}
376        }
377        Ok(())
378    }
379
380    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
381        if !state.has_values {
382            return Ok(SqliteValue::Null);
383        }
384        if state.all_integer && state.overflowed {
385            return Err(FrankenError::IntegerOverflow);
386        }
387        if state.all_integer {
388            Ok(SqliteValue::Integer(state.int_sum))
389        } else {
390            Ok(SqliteValue::Float(
391                state.float_sum + state.float_compensation,
392            ))
393        }
394    }
395
396    fn num_args(&self) -> i32 {
397        1
398    }
399
400    fn name(&self) -> &str {
401        "sum"
402    }
403}
404
405// ═══════════════════════════════════════════════════════════════════════════
406// total(X) — always returns float, 0.0 for empty set, never overflows.
407// ═══════════════════════════════════════════════════════════════════════════
408
409pub struct TotalFunc;
410
411/// State for `total()`: Kahan compensated accumulator.
412pub struct TotalState {
413    sum: f64,
414    compensation: f64,
415}
416
417impl AggregateFunction for TotalFunc {
418    type State = TotalState;
419
420    fn initial_state(&self) -> Self::State {
421        TotalState {
422            sum: 0.0,
423            compensation: 0.0,
424        }
425    }
426
427    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
428        if !args[0].is_null() {
429            kahan_add(&mut state.sum, &mut state.compensation, args[0].to_float());
430        }
431        Ok(())
432    }
433
434    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
435        Ok(SqliteValue::Float(state.sum + state.compensation))
436    }
437
438    fn num_args(&self) -> i32 {
439        1
440    }
441
442    fn name(&self) -> &str {
443        "total"
444    }
445}
446
447// ═══════════════════════════════════════════════════════════════════════════
448// median(X) — equivalent to percentile_cont(X, 0.5)
449// ═══════════════════════════════════════════════════════════════════════════
450
451pub struct MedianFunc;
452
453impl AggregateFunction for MedianFunc {
454    type State = Vec<f64>;
455
456    fn initial_state(&self) -> Self::State {
457        Vec::new()
458    }
459
460    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
461        if !args[0].is_null() {
462            state.push(args[0].to_float());
463        }
464        Ok(())
465    }
466
467    fn finalize(&self, mut state: Self::State) -> Result<SqliteValue> {
468        if state.is_empty() {
469            return Ok(SqliteValue::Null);
470        }
471        state.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
472        let result = percentile_cont_impl(&state, 0.5);
473        Ok(SqliteValue::Float(result))
474    }
475
476    fn num_args(&self) -> i32 {
477        1
478    }
479
480    fn name(&self) -> &str {
481        "median"
482    }
483}
484
485// ═══════════════════════════════════════════════════════════════════════════
486// percentile(Y, P) — P in 0..100
487// ═══════════════════════════════════════════════════════════════════════════
488
489pub struct PercentileState {
490    values: Vec<f64>,
491    p: Option<f64>,
492}
493
494pub struct PercentileFunc;
495
496impl AggregateFunction for PercentileFunc {
497    type State = PercentileState;
498
499    fn initial_state(&self) -> Self::State {
500        PercentileState {
501            values: Vec::new(),
502            p: None,
503        }
504    }
505
506    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
507        if !args[0].is_null() {
508            state.values.push(args[0].to_float());
509        }
510        // Capture P from the second argument (constant expression).
511        if state.p.is_none() && args.len() > 1 && !args[1].is_null() {
512            state.p = Some(args[1].to_float());
513        }
514        Ok(())
515    }
516
517    fn finalize(&self, mut state: Self::State) -> Result<SqliteValue> {
518        if state.values.is_empty() {
519            return Ok(SqliteValue::Null);
520        }
521        let p = state.p.unwrap_or(50.0);
522        state
523            .values
524            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
525        // Convert P from 0-100 to 0-1 for the shared implementation.
526        let result = percentile_cont_impl(&state.values, p / 100.0);
527        Ok(SqliteValue::Float(result))
528    }
529
530    fn num_args(&self) -> i32 {
531        2
532    }
533
534    fn name(&self) -> &str {
535        "percentile"
536    }
537}
538
539// ═══════════════════════════════════════════════════════════════════════════
540// percentile_cont(Y, P) — P in 0..1, continuous interpolation
541// ═══════════════════════════════════════════════════════════════════════════
542
543pub struct PercentileContFunc;
544
545impl AggregateFunction for PercentileContFunc {
546    type State = PercentileState;
547
548    fn initial_state(&self) -> Self::State {
549        PercentileState {
550            values: Vec::new(),
551            p: None,
552        }
553    }
554
555    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
556        if !args[0].is_null() {
557            state.values.push(args[0].to_float());
558        }
559        if state.p.is_none() && args.len() > 1 && !args[1].is_null() {
560            state.p = Some(args[1].to_float());
561        }
562        Ok(())
563    }
564
565    fn finalize(&self, mut state: Self::State) -> Result<SqliteValue> {
566        if state.values.is_empty() {
567            return Ok(SqliteValue::Null);
568        }
569        let p = state.p.unwrap_or(0.5);
570        state
571            .values
572            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
573        let result = percentile_cont_impl(&state.values, p);
574        Ok(SqliteValue::Float(result))
575    }
576
577    fn num_args(&self) -> i32 {
578        2
579    }
580
581    fn name(&self) -> &str {
582        "percentile_cont"
583    }
584}
585
586// ═══════════════════════════════════════════════════════════════════════════
587// percentile_disc(Y, P) — P in 0..1, discrete (returns actual value)
588// ═══════════════════════════════════════════════════════════════════════════
589
590pub struct PercentileDiscFunc;
591
592impl AggregateFunction for PercentileDiscFunc {
593    type State = PercentileState;
594
595    fn initial_state(&self) -> Self::State {
596        PercentileState {
597            values: Vec::new(),
598            p: None,
599        }
600    }
601
602    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
603        if !args[0].is_null() {
604            state.values.push(args[0].to_float());
605        }
606        if state.p.is_none() && args.len() > 1 && !args[1].is_null() {
607            state.p = Some(args[1].to_float());
608        }
609        Ok(())
610    }
611
612    fn finalize(&self, mut state: Self::State) -> Result<SqliteValue> {
613        if state.values.is_empty() {
614            return Ok(SqliteValue::Null);
615        }
616        let p = state.p.unwrap_or(0.5);
617        let p = if p.is_nan() { 0.5 } else { p.clamp(0.0, 1.0) };
618        state
619            .values
620            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
621        // Discrete: pick the value at the ceiling index.
622        let n = state.values.len();
623        let idx = ((p * n as f64).ceil() as usize)
624            .saturating_sub(1)
625            .min(n - 1);
626        Ok(SqliteValue::Float(state.values[idx]))
627    }
628
629    fn num_args(&self) -> i32 {
630        2
631    }
632
633    fn name(&self) -> &str {
634        "percentile_disc"
635    }
636}
637
638// ── Shared percentile helper ──────────────────────────────────────────────
639
640/// Continuous percentile with linear interpolation.
641/// `sorted` must be sorted ascending. `p` is in [0, 1].
642fn percentile_cont_impl(sorted: &[f64], p: f64) -> f64 {
643    let n = sorted.len();
644    if n == 1 {
645        return sorted[0];
646    }
647    let p = if p.is_nan() { 0.5 } else { p.clamp(0.0, 1.0) };
648    let rank = p * (n - 1) as f64;
649    let lower = rank.floor() as usize;
650    let upper = rank.ceil() as usize;
651    if lower == upper {
652        sorted[lower]
653    } else {
654        let frac = rank - lower as f64;
655        sorted[lower] * (1.0 - frac) + sorted[upper] * frac
656    }
657}
658
659// ── Registration ──────────────────────────────────────────────────────────
660
661/// Register all §13.4 aggregate functions into the given registry.
662pub fn register_aggregate_builtins(registry: &mut FunctionRegistry) {
663    registry.register_aggregate(AvgFunc);
664    registry.register_aggregate(CountStarFunc);
665    registry.register_aggregate(CountFunc);
666    registry.register_aggregate(GroupConcatFunc);
667    registry.register_aggregate(AggMaxFunc);
668    registry.register_aggregate(AggMinFunc);
669    registry.register_aggregate(SumFunc);
670    registry.register_aggregate(TotalFunc);
671    registry.register_aggregate(MedianFunc);
672    registry.register_aggregate(PercentileFunc);
673    registry.register_aggregate(PercentileContFunc);
674    registry.register_aggregate(PercentileDiscFunc);
675
676    // string_agg is an alias for group_concat with mandatory separator.
677    struct StringAggFunc;
678    impl AggregateFunction for StringAggFunc {
679        type State = GroupConcatState;
680
681        fn initial_state(&self) -> Self::State {
682            GroupConcatState {
683                result: String::new(),
684                has_value: false,
685            }
686        }
687
688        fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
689            GroupConcatFunc.step(state, args)
690        }
691
692        fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
693            GroupConcatFunc.finalize(state)
694        }
695
696        fn num_args(&self) -> i32 {
697            2 // string_agg requires separator
698        }
699
700        fn name(&self) -> &str {
701            "string_agg"
702        }
703    }
704    registry.register_aggregate(StringAggFunc);
705}
706
707// ── Tests ─────────────────────────────────────────────────────────────────
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    const EPS: f64 = 1e-12;
714
715    fn int(v: i64) -> SqliteValue {
716        SqliteValue::Integer(v)
717    }
718
719    fn float(v: f64) -> SqliteValue {
720        SqliteValue::Float(v)
721    }
722
723    fn null() -> SqliteValue {
724        SqliteValue::Null
725    }
726
727    fn text(s: &str) -> SqliteValue {
728        SqliteValue::Text(s.into())
729    }
730
731    fn assert_float_eq(result: &SqliteValue, expected: f64) {
732        match result {
733            SqliteValue::Float(v) => {
734                assert!((v - expected).abs() < EPS, "expected {expected}, got {v}");
735            }
736            other => {
737                assert!(
738                    matches!(other, SqliteValue::Float(_)),
739                    "expected Float({expected}), got {other:?}"
740                );
741            }
742        }
743    }
744
745    /// Helper: run an aggregate over a list of single-arg row values.
746    fn run_agg<F: AggregateFunction>(func: &F, rows: &[SqliteValue]) -> SqliteValue {
747        let mut state = func.initial_state();
748        for row in rows {
749            func.step(&mut state, std::slice::from_ref(row)).unwrap();
750        }
751        func.finalize(state).unwrap()
752    }
753
754    /// Helper: run an aggregate over a list of two-arg row values.
755    fn run_agg2<F: AggregateFunction>(
756        func: &F,
757        rows: &[(SqliteValue, SqliteValue)],
758    ) -> SqliteValue {
759        let mut state = func.initial_state();
760        for (a, b) in rows {
761            func.step(&mut state, &[a.clone(), b.clone()]).unwrap();
762        }
763        func.finalize(state).unwrap()
764    }
765
766    #[test]
767    fn test_aggregate_oracle_edges_2026_08() {
768        // Oracle: sqlite3 3.46.1. sum() of integers stays integer and ERRORS on
769        // i64 overflow (does NOT promote to real); total() is always real and 0.0
770        // for an empty/all-NULL input; sum() of all-NULL -> NULL; count() skips
771        // NULLs; max()/min() use SQLite's storage-class ordering (int sorts before
772        // text) and ignore NULLs; group_concat default separator is ",".
773        assert_eq!(run_agg(&SumFunc, &[int(1), int(2)]), int(3));
774        assert_eq!(run_agg(&SumFunc, &[null(), null()]), null());
775        assert_eq!(
776            run_agg(&TotalFunc, &[int(1), int(2)]),
777            SqliteValue::Float(3.0)
778        );
779        assert_eq!(
780            run_agg(&TotalFunc, &[null(), null()]),
781            SqliteValue::Float(0.0)
782        );
783        assert_float_eq(&run_agg(&AvgFunc, &[int(1), int(2)]), 1.5);
784        assert_eq!(run_agg(&CountFunc, &[int(1), null(), int(3)]), int(2));
785        assert_eq!(
786            run_agg(&AggMaxFunc, &[int(1), text("a"), null()]),
787            text("a")
788        );
789        assert_eq!(run_agg(&AggMinFunc, &[int(1), text("a"), null()]), int(1));
790        assert_eq!(
791            run_agg(&GroupConcatFunc, &[int(1), int(2), int(3)]),
792            text("1,2,3")
793        );
794
795        // sum() of all-integer input ERRORS on i64 overflow rather than wrapping
796        // or promoting to real (C SQLite raises "integer overflow" while stepping;
797        // frank raises it at finalize — observationally identical for a query).
798        let sum = SumFunc;
799        let mut st = sum.initial_state();
800        sum.step(&mut st, &[int(i64::MAX)]).unwrap();
801        sum.step(&mut st, &[int(1)]).unwrap();
802        assert!(
803            sum.finalize(st).is_err(),
804            "sum() must error on i64 overflow"
805        );
806        // But a real value in the mix switches to float accumulation (no error),
807        // matching C SQLite's `approx` flag.
808        let mut st2 = sum.initial_state();
809        sum.step(&mut st2, &[int(i64::MAX)]).unwrap();
810        sum.step(&mut st2, &[int(1)]).unwrap();
811        sum.step(&mut st2, &[float(0.5)]).unwrap();
812        assert!(
813            matches!(sum.finalize(st2), Ok(SqliteValue::Float(_))),
814            "sum() with a real present returns the float sum, not an overflow error"
815        );
816    }
817
818    // ── avg ───────────────────────────────────────────────────────────
819
820    #[test]
821    fn test_avg_basic() {
822        let r = run_agg(&AvgFunc, &[int(1), int(2), int(3), int(4), int(5)]);
823        assert_float_eq(&r, 3.0);
824    }
825
826    #[test]
827    fn test_avg_with_nulls() {
828        let r = run_agg(&AvgFunc, &[int(1), null(), int(3)]);
829        assert_float_eq(&r, 2.0);
830    }
831
832    #[test]
833    fn test_avg_empty() {
834        let r = run_agg(&AvgFunc, &[]);
835        assert_eq!(r, SqliteValue::Null);
836    }
837
838    #[test]
839    fn test_avg_returns_real() {
840        let r = run_agg(&AvgFunc, &[int(2), int(4)]);
841        assert!(matches!(r, SqliteValue::Float(_)));
842    }
843
844    // ── count ─────────────────────────────────────────────────────────
845
846    #[test]
847    fn test_count_star() {
848        // count(*) counts all rows including NULLs.
849        let mut state = CountStarFunc.initial_state();
850        CountStarFunc.step(&mut state, &[]).unwrap(); // row 1
851        CountStarFunc.step(&mut state, &[]).unwrap(); // row 2
852        CountStarFunc.step(&mut state, &[]).unwrap(); // row 3
853        let r = CountStarFunc.finalize(state).unwrap();
854        assert_eq!(r, int(3));
855    }
856
857    #[test]
858    fn test_count_column() {
859        let r = run_agg(&CountFunc, &[int(1), null(), int(3), null(), int(5)]);
860        assert_eq!(r, int(3));
861    }
862
863    #[test]
864    fn test_count_empty() {
865        let r = run_agg(&CountFunc, &[]);
866        assert_eq!(r, int(0));
867    }
868
869    // ── group_concat ──────────────────────────────────────────────────
870
871    #[test]
872    fn test_group_concat_basic() {
873        let r = run_agg(&GroupConcatFunc, &[text("a"), text("b"), text("c")]);
874        assert_eq!(r, SqliteValue::Text("a,b,c".into()));
875    }
876
877    #[test]
878    fn test_group_concat_custom_sep() {
879        let rows = vec![
880            (text("a"), text("; ")),
881            (text("b"), text("; ")),
882            (text("c"), text("; ")),
883        ];
884        let r = run_agg2(&GroupConcatFunc, &rows);
885        assert_eq!(r, SqliteValue::Text("a; b; c".into()));
886    }
887
888    #[test]
889    fn test_group_concat_null_skipped() {
890        let r = run_agg(&GroupConcatFunc, &[text("a"), null(), text("c")]);
891        assert_eq!(r, SqliteValue::Text("a,c".into()));
892    }
893
894    #[test]
895    fn test_group_concat_empty() {
896        let r = run_agg(&GroupConcatFunc, &[]);
897        assert_eq!(r, SqliteValue::Null);
898    }
899
900    #[test]
901    fn test_group_concat_varying_separator() {
902        // C SQLite uses the separator from each row's argument, not a single
903        // global separator. SELECT group_concat(val, sep) with varying sep
904        // produces a+b*c, not a*b*c (the old bug used the last-seen sep).
905        let rows = vec![
906            (text("a"), text("-")),
907            (text("b"), text("+")),
908            (text("c"), text("*")),
909        ];
910        let r = run_agg2(&GroupConcatFunc, &rows);
911        assert_eq!(r, SqliteValue::Text("a+b*c".into()));
912    }
913
914    #[test]
915    fn test_group_concat_single_value() {
916        let r = run_agg(&GroupConcatFunc, &[text("only")]);
917        assert_eq!(r, SqliteValue::Text("only".into()));
918    }
919
920    #[test]
921    fn test_group_concat_integer_values_coerced_to_text() {
922        let r = run_agg(&GroupConcatFunc, &[int(1), int(2), int(3)]);
923        assert_eq!(r, SqliteValue::Text("1,2,3".into()));
924    }
925
926    #[test]
927    #[ignore = "perf-only benchmark"]
928    fn perf_group_concat_text_rows() {
929        use std::hint::black_box;
930        use std::time::Instant;
931
932        const ROWS: usize = 200_000;
933        const REPEATS: usize = 5;
934
935        let rows: Vec<SqliteValue> = (0..ROWS).map(|_| text("payload")).collect();
936        let mut best_ns = u128::MAX;
937        let mut result_len = 0usize;
938
939        for _ in 0..REPEATS {
940            let started = Instant::now();
941            let result = black_box(run_agg(&GroupConcatFunc, black_box(rows.as_slice())));
942            let elapsed_ns = started.elapsed().as_nanos();
943            if elapsed_ns < best_ns {
944                best_ns = elapsed_ns;
945            }
946            result_len = match result {
947                SqliteValue::Text(text) => text.len(),
948                SqliteValue::Null
949                | SqliteValue::Integer(_)
950                | SqliteValue::Float(_)
951                | SqliteValue::Blob(_) => 0,
952            };
953        }
954
955        println!(
956            "group_concat_text_rows rows={ROWS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
957        );
958    }
959
960    // ── max (aggregate) ───────────────────────────────────────────────
961
962    #[test]
963    fn test_max_aggregate() {
964        let r = run_agg(&AggMaxFunc, &[int(3), int(7), int(1), int(5)]);
965        assert_eq!(r, int(7));
966    }
967
968    #[test]
969    fn test_max_aggregate_null_skipped() {
970        let r = run_agg(&AggMaxFunc, &[int(3), null(), int(7), null()]);
971        assert_eq!(r, int(7));
972    }
973
974    #[test]
975    fn test_max_aggregate_empty() {
976        let r = run_agg(&AggMaxFunc, &[]);
977        assert_eq!(r, SqliteValue::Null);
978    }
979
980    // ── min (aggregate) ───────────────────────────────────────────────
981
982    #[test]
983    fn test_min_aggregate() {
984        let r = run_agg(&AggMinFunc, &[int(3), int(7), int(1), int(5)]);
985        assert_eq!(r, int(1));
986    }
987
988    #[test]
989    fn test_min_aggregate_null_skipped() {
990        let r = run_agg(&AggMinFunc, &[int(3), null(), int(1), null()]);
991        assert_eq!(r, int(1));
992    }
993
994    #[test]
995    fn test_min_aggregate_empty() {
996        let r = run_agg(&AggMinFunc, &[]);
997        assert_eq!(r, SqliteValue::Null);
998    }
999
1000    // ── sum ───────────────────────────────────────────────────────────
1001
1002    #[test]
1003    fn test_sum_integers() {
1004        let r = run_agg(&SumFunc, &[int(1), int(2), int(3)]);
1005        assert_eq!(r, int(6));
1006    }
1007
1008    #[test]
1009    fn test_sum_reals() {
1010        let r = run_agg(&SumFunc, &[float(1.5), float(2.5)]);
1011        assert_float_eq(&r, 4.0);
1012    }
1013
1014    #[test]
1015    fn test_sum_empty_null() {
1016        let r = run_agg(&SumFunc, &[]);
1017        assert_eq!(r, SqliteValue::Null);
1018    }
1019
1020    #[test]
1021    fn test_sum_overflow_error() {
1022        let mut state = SumFunc.initial_state();
1023        SumFunc.step(&mut state, &[int(i64::MAX)]).unwrap();
1024        SumFunc.step(&mut state, &[int(1)]).unwrap();
1025        let err = SumFunc.finalize(state);
1026        assert!(err.is_err(), "sum should raise overflow error");
1027    }
1028
1029    #[test]
1030    fn test_sum_later_real_value_clears_integer_overflow_error() {
1031        let r = run_agg(&SumFunc, &[int(i64::MAX), int(1), float(0.5)]);
1032        assert_float_eq(&r, 9_223_372_036_854_776_000.0);
1033    }
1034
1035    #[test]
1036    fn test_sum_integer_text_preserves_overflow_error() -> Result<()> {
1037        let mut state = SumFunc.initial_state();
1038        SumFunc.step(&mut state, &[text("9223372036854775807")])?;
1039        SumFunc.step(&mut state, &[text("1")])?;
1040        let err = SumFunc.finalize(state);
1041        assert!(err.is_err(), "integer-text sum should raise overflow");
1042        Ok(())
1043    }
1044
1045    #[test]
1046    fn test_sum_integer_text_later_real_clears_overflow_error() {
1047        let r = run_agg(
1048            &SumFunc,
1049            &[text("9223372036854775807"), text("1"), text("0.5")],
1050        );
1051        assert_float_eq(&r, 9_223_372_036_854_776_000.0);
1052    }
1053
1054    #[test]
1055    fn test_sum_prefix_text_uses_real_accumulator() {
1056        let r = run_agg(&SumFunc, &[text("123abc"), int(1)]);
1057        assert_float_eq(&r, 124.0);
1058    }
1059
1060    #[test]
1061    fn test_sum_unicode_whitespace_text_uses_sqlite_ascii_space_rules() {
1062        let leading = run_agg(&SumFunc, &[text("\u{00a0}123"), int(1)]);
1063        assert_float_eq(&leading, 1.0);
1064
1065        let trailing = run_agg(&SumFunc, &[text("123\u{00a0}"), int(1)]);
1066        assert_float_eq(&trailing, 124.0);
1067    }
1068
1069    #[test]
1070    fn test_sum_null_skipped() {
1071        let r = run_agg(&SumFunc, &[int(1), null(), int(3)]);
1072        assert_eq!(r, int(4));
1073    }
1074
1075    // ── total ─────────────────────────────────────────────────────────
1076
1077    #[test]
1078    fn test_total_basic() {
1079        let r = run_agg(&TotalFunc, &[int(1), int(2), int(3)]);
1080        assert_float_eq(&r, 6.0);
1081    }
1082
1083    #[test]
1084    fn test_total_empty_zero() {
1085        let r = run_agg(&TotalFunc, &[]);
1086        assert_float_eq(&r, 0.0);
1087    }
1088
1089    #[test]
1090    fn test_total_no_overflow() {
1091        // total uses f64 and never overflows.
1092        let r = run_agg(&TotalFunc, &[int(i64::MAX), int(i64::MAX)]);
1093        assert!(matches!(r, SqliteValue::Float(_)));
1094    }
1095
1096    // ── median ────────────────────────────────────────────────────────
1097
1098    #[test]
1099    fn test_median_basic() {
1100        let r = run_agg(&MedianFunc, &[int(1), int(2), int(3), int(4), int(5)]);
1101        assert_float_eq(&r, 3.0);
1102    }
1103
1104    #[test]
1105    fn test_median_even() {
1106        let r = run_agg(&MedianFunc, &[int(1), int(2), int(3), int(4)]);
1107        assert_float_eq(&r, 2.5);
1108    }
1109
1110    #[test]
1111    fn test_median_null_skipped() {
1112        let r = run_agg(&MedianFunc, &[int(1), null(), int(3)]);
1113        assert_float_eq(&r, 2.0);
1114    }
1115
1116    #[test]
1117    fn test_median_empty() {
1118        let r = run_agg(&MedianFunc, &[]);
1119        assert_eq!(r, SqliteValue::Null);
1120    }
1121
1122    // ── percentile ────────────────────────────────────────────────────
1123
1124    #[test]
1125    fn test_percentile_50() {
1126        // percentile(col, 50) = median
1127        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1128            (int(1), float(50.0)),
1129            (int(2), float(50.0)),
1130            (int(3), float(50.0)),
1131            (int(4), float(50.0)),
1132            (int(5), float(50.0)),
1133        ];
1134        let r = run_agg2(&PercentileFunc, &rows);
1135        assert_float_eq(&r, 3.0);
1136    }
1137
1138    #[test]
1139    fn test_percentile_0() {
1140        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1141            (int(10), float(0.0)),
1142            (int(20), float(0.0)),
1143            (int(30), float(0.0)),
1144        ];
1145        let r = run_agg2(&PercentileFunc, &rows);
1146        assert_float_eq(&r, 10.0);
1147    }
1148
1149    #[test]
1150    fn test_percentile_100() {
1151        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1152            (int(10), float(100.0)),
1153            (int(20), float(100.0)),
1154            (int(30), float(100.0)),
1155        ];
1156        let r = run_agg2(&PercentileFunc, &rows);
1157        assert_float_eq(&r, 30.0);
1158    }
1159
1160    // ── percentile_cont ───────────────────────────────────────────────
1161
1162    #[test]
1163    fn test_percentile_cont_basic() {
1164        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1165            (int(1), float(0.5)),
1166            (int(2), float(0.5)),
1167            (int(3), float(0.5)),
1168            (int(4), float(0.5)),
1169            (int(5), float(0.5)),
1170        ];
1171        let r = run_agg2(&PercentileContFunc, &rows);
1172        assert_float_eq(&r, 3.0);
1173    }
1174
1175    // ── percentile_disc ───────────────────────────────────────────────
1176
1177    #[test]
1178    fn test_percentile_disc_basic() {
1179        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1180            (int(1), float(0.5)),
1181            (int(2), float(0.5)),
1182            (int(3), float(0.5)),
1183            (int(4), float(0.5)),
1184            (int(5), float(0.5)),
1185        ];
1186        let r = run_agg2(&PercentileDiscFunc, &rows);
1187        // Discrete: returns an actual input value.
1188        match r {
1189            SqliteValue::Float(v) => {
1190                // Should be one of the actual input values (3.0 for 0.5 in 5 items).
1191                assert!(
1192                    [1.0, 2.0, 3.0, 4.0, 5.0].contains(&v),
1193                    "expected actual value, got {v}"
1194                );
1195            }
1196            other => {
1197                assert!(
1198                    matches!(other, SqliteValue::Float(_)),
1199                    "expected Float, got {other:?}"
1200                );
1201            }
1202        }
1203    }
1204
1205    #[test]
1206    fn test_percentile_disc_no_interpolation() {
1207        // With 4 items at p=0.5, cont would interpolate, disc should not.
1208        let rows: Vec<(SqliteValue, SqliteValue)> = vec![
1209            (int(10), float(0.5)),
1210            (int(20), float(0.5)),
1211            (int(30), float(0.5)),
1212            (int(40), float(0.5)),
1213        ];
1214        let r = run_agg2(&PercentileDiscFunc, &rows);
1215        match r {
1216            SqliteValue::Float(v) => {
1217                // Must be one of {10, 20, 30, 40}, not 25.0.
1218                assert!(
1219                    [10.0, 20.0, 30.0, 40.0].contains(&v),
1220                    "disc must not interpolate: got {v}"
1221                );
1222            }
1223            other => {
1224                assert!(
1225                    matches!(other, SqliteValue::Float(_)),
1226                    "expected Float, got {other:?}"
1227                );
1228            }
1229        }
1230    }
1231
1232    // ── string_agg (alias) ────────────────────────────────────────────
1233
1234    #[test]
1235    fn test_string_agg_alias() {
1236        let mut reg = FunctionRegistry::new();
1237        register_aggregate_builtins(&mut reg);
1238        let sa = reg
1239            .find_aggregate("string_agg", 2)
1240            .expect("string_agg registered");
1241        let mut state = sa.initial_state();
1242        sa.step(&mut state, &[text("a"), text(",")]).unwrap();
1243        sa.step(&mut state, &[text("b"), text(",")]).unwrap();
1244        let r = sa.finalize(state).unwrap();
1245        assert_eq!(r, SqliteValue::Text("a,b".into()));
1246    }
1247
1248    // ── registration ──────────────────────────────────────────────────
1249
1250    #[test]
1251    fn test_register_aggregate_builtins_all_present() {
1252        let mut reg = FunctionRegistry::new();
1253        register_aggregate_builtins(&mut reg);
1254
1255        let expected = [
1256            ("avg", 1),
1257            ("count", 0), // count(*)
1258            ("count", 1), // count(X)
1259            ("max", 1),
1260            ("min", 1),
1261            ("sum", 1),
1262            ("total", 1),
1263            ("median", 1),
1264            ("percentile", 2),
1265            ("percentile_cont", 2),
1266            ("percentile_disc", 2),
1267            ("string_agg", 2),
1268        ];
1269
1270        for (name, arity) in expected {
1271            assert!(
1272                reg.find_aggregate(name, arity).is_some(),
1273                "aggregate '{name}/{arity}' not registered"
1274            );
1275        }
1276
1277        // group_concat is variadic
1278        assert!(reg.find_aggregate("group_concat", 1).is_some());
1279        assert!(reg.find_aggregate("group_concat", 2).is_some());
1280
1281        let group_concat_zero = reg.find_aggregate("group_concat", 0).unwrap();
1282        let err = group_concat_zero
1283            .finalize(group_concat_zero.initial_state())
1284            .expect_err("group_concat() should reject zero arguments");
1285        assert!(
1286            matches!(&err, FrankenError::FunctionError(message)
1287                if message == "wrong number of arguments to function group_concat()"),
1288            "unexpected error: {err:?}"
1289        );
1290    }
1291
1292    // ── E2E: full lifecycle through registry ──────────────────────────
1293
1294    #[test]
1295    fn test_e2e_registry_invoke_aggregates() {
1296        let mut reg = FunctionRegistry::new();
1297        register_aggregate_builtins(&mut reg);
1298
1299        // avg through registry
1300        let avg = reg.find_aggregate("avg", 1).unwrap();
1301        let mut state = avg.initial_state();
1302        avg.step(&mut state, &[int(10)]).unwrap();
1303        avg.step(&mut state, &[int(20)]).unwrap();
1304        avg.step(&mut state, &[int(30)]).unwrap();
1305        let r = avg.finalize(state).unwrap();
1306        assert_float_eq(&r, 20.0);
1307
1308        // sum through registry
1309        let sum = reg.find_aggregate("sum", 1).unwrap();
1310        let mut state = sum.initial_state();
1311        sum.step(&mut state, &[int(1)]).unwrap();
1312        sum.step(&mut state, &[int(2)]).unwrap();
1313        sum.step(&mut state, &[int(3)]).unwrap();
1314        let r = sum.finalize(state).unwrap();
1315        assert_eq!(r, int(6));
1316    }
1317}