Skip to main content

fsqlite_func/
aggregate.rs

1//! Aggregate function trait with type-erased state adapter.
2//!
3//! Aggregate functions accumulate a result across multiple rows (e.g.
4//! `SUM`, `COUNT`, `AVG`). Each GROUP BY group gets its own state.
5//!
6//! # Type Erasure
7//!
8//! The [`FunctionRegistry`](crate::FunctionRegistry) stores aggregates as
9//! `Arc<dyn AggregateFunction<State = Box<dyn Any + Send>>>`. Concrete
10//! implementations use [`AggregateAdapter`] to wrap their typed state.
11#![allow(clippy::unnecessary_literal_bound)]
12
13use std::any::Any;
14
15use fsqlite_error::Result;
16use fsqlite_types::SqliteValue;
17
18use crate::FunctionArity;
19
20/// An aggregate SQL function (e.g. `SUM`, `COUNT`, `AVG`).
21///
22/// This trait is **open** (user-implementable). Extension authors implement
23/// this trait to register custom aggregate functions.
24///
25/// # State Lifecycle
26///
27/// 1. [`initial_state`](Self::initial_state) creates a fresh accumulator.
28/// 2. [`step`](Self::step) is called once per row.
29/// 3. [`finalize`](Self::finalize) consumes the state and returns the result.
30///
31/// # Send + Sync
32///
33/// The function object itself is shared across threads via `Arc`. The
34/// `State` type must be `Send` so it can be moved between threads.
35pub trait AggregateFunction: Send + Sync {
36    /// The per-group accumulator type.
37    type State: Send;
38
39    /// Create a fresh accumulator (zero/identity state).
40    fn initial_state(&self) -> Self::State;
41
42    /// Process one row, updating the accumulator.
43    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()>;
44
45    /// Consume the accumulator and produce the final result.
46    fn finalize(&self, state: Self::State) -> Result<SqliteValue>;
47
48    /// The number of arguments this function accepts (`-1` = variadic).
49    fn num_args(&self) -> i32;
50
51    /// Minimum accepted SQL argument count for a variadic function.
52    ///
53    /// The default is zero. Fixed-arity functions are matched directly from
54    /// [`Self::num_args`] and do not consult this method.
55    fn min_args(&self) -> i32 {
56        0
57    }
58
59    /// Maximum accepted SQL argument count for a variadic function.
60    ///
61    /// The default is unbounded. Fixed-arity functions are matched directly
62    /// from [`Self::num_args`] and do not consult this method.
63    fn max_args(&self) -> Option<i32> {
64        None
65    }
66
67    /// Return the complete SQL-visible arity contract in one metadata call.
68    ///
69    /// Registries use this method exactly once before publication, preventing
70    /// a reentrant or stateful [`Self::num_args`] implementation from producing
71    /// a key and bounds from different observations.
72    fn arity(&self) -> FunctionArity {
73        let declared = self.num_args();
74        FunctionArity::from_declared_args(declared, || (self.min_args(), self.max_args()))
75    }
76
77    /// The function name, used in error messages and EXPLAIN output.
78    fn name(&self) -> &str;
79}
80
81/// Type-erased adapter that wraps a concrete [`AggregateFunction`] so the
82/// registry can store heterogeneous aggregates behind a single trait object.
83///
84/// The adapter implements `AggregateFunction<State = Box<dyn Any + Send>>`,
85/// boxing the concrete state on creation and downcasting on step/finalize.
86pub struct AggregateAdapter<F> {
87    inner: F,
88}
89
90impl<F> AggregateAdapter<F> {
91    /// Wrap a concrete aggregate function for type-erased storage.
92    pub const fn new(inner: F) -> Self {
93        Self { inner }
94    }
95}
96
97impl<F> AggregateFunction for AggregateAdapter<F>
98where
99    F: AggregateFunction,
100    F::State: 'static,
101{
102    type State = Box<dyn Any + Send>;
103
104    fn initial_state(&self) -> Self::State {
105        Box::new(self.inner.initial_state())
106    }
107
108    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
109        let concrete = state
110            .downcast_mut::<F::State>()
111            .expect("aggregate state type mismatch");
112        self.inner.step(concrete, args)
113    }
114
115    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
116        let concrete = *state
117            .downcast::<F::State>()
118            .expect("aggregate state type mismatch");
119        self.inner.finalize(concrete)
120    }
121
122    fn num_args(&self) -> i32 {
123        self.inner.num_args()
124    }
125
126    fn min_args(&self) -> i32 {
127        self.inner.min_args()
128    }
129
130    fn max_args(&self) -> Option<i32> {
131        self.inner.max_args()
132    }
133
134    fn arity(&self) -> FunctionArity {
135        self.inner.arity()
136    }
137
138    fn name(&self) -> &str {
139        self.inner.name()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use std::sync::Arc;
146
147    use super::*;
148
149    // -- Mock: Sum aggregate --
150
151    struct SumAgg;
152
153    impl AggregateFunction for SumAgg {
154        type State = i64;
155
156        fn initial_state(&self) -> i64 {
157            0
158        }
159
160        fn step(&self, state: &mut i64, args: &[SqliteValue]) -> Result<()> {
161            *state += args[0].to_integer();
162            Ok(())
163        }
164
165        fn finalize(&self, state: i64) -> Result<SqliteValue> {
166            Ok(SqliteValue::Integer(state))
167        }
168
169        fn num_args(&self) -> i32 {
170            1
171        }
172
173        fn name(&self) -> &str {
174            "sum"
175        }
176    }
177
178    #[test]
179    fn test_aggregate_initial_state() {
180        let agg = SumAgg;
181        assert_eq!(agg.initial_state(), 0);
182    }
183
184    #[test]
185    fn test_aggregate_step_and_finalize() {
186        let agg = SumAgg;
187        let mut state = agg.initial_state();
188
189        agg.step(&mut state, &[SqliteValue::Integer(10)]).unwrap();
190        agg.step(&mut state, &[SqliteValue::Integer(20)]).unwrap();
191        agg.step(&mut state, &[SqliteValue::Integer(12)]).unwrap();
192
193        let result = agg.finalize(state).unwrap();
194        assert_eq!(result, SqliteValue::Integer(42));
195    }
196
197    #[test]
198    fn test_aggregate_type_erasure_adapter() {
199        let adapted: AggregateAdapter<SumAgg> = AggregateAdapter::new(SumAgg);
200        let erased: Arc<dyn AggregateFunction<State = Box<dyn Any + Send>>> = Arc::new(adapted);
201
202        let mut state = erased.initial_state();
203        erased
204            .step(&mut state, &[SqliteValue::Integer(10)])
205            .unwrap();
206        erased
207            .step(&mut state, &[SqliteValue::Integer(32)])
208            .unwrap();
209
210        let result = erased.finalize(state).unwrap();
211        assert_eq!(result, SqliteValue::Integer(42));
212
213        // Verify we can clone the Arc (shared across threads).
214        let e2 = Arc::clone(&erased);
215        assert_eq!(e2.name(), "sum");
216    }
217}