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    /// Process one row with per-argument subtype tags available.
46    ///
47    /// `arg_subtypes[i]` is the subtype of `args[i]` (0 when untagged), the same
48    /// channel the scalar path exposes via `ScalarFunction::invoke_with_arg_subtypes`.
49    /// The default ignores the tags and forwards to [`Self::step`]; aggregates
50    /// that must preserve them (e.g. `json_group_array` embedding a nested
51    /// `json_object(...)` rather than quoting it) override this.
52    fn step_with_arg_subtypes(
53        &self,
54        state: &mut Self::State,
55        args: &[SqliteValue],
56        _arg_subtypes: &[u32],
57    ) -> Result<()> {
58        self.step(state, args)
59    }
60
61    /// Consume the accumulator and produce the final result.
62    fn finalize(&self, state: Self::State) -> Result<SqliteValue>;
63
64    /// The number of arguments this function accepts (`-1` = variadic).
65    fn num_args(&self) -> i32;
66
67    /// Minimum accepted SQL argument count for a variadic function.
68    ///
69    /// The default is zero. Fixed-arity functions are matched directly from
70    /// [`Self::num_args`] and do not consult this method.
71    fn min_args(&self) -> i32 {
72        0
73    }
74
75    /// Maximum accepted SQL argument count for a variadic function.
76    ///
77    /// The default is unbounded. Fixed-arity functions are matched directly
78    /// from [`Self::num_args`] and do not consult this method.
79    fn max_args(&self) -> Option<i32> {
80        None
81    }
82
83    /// Return the complete SQL-visible arity contract in one metadata call.
84    ///
85    /// Registries use this method exactly once before publication, preventing
86    /// a reentrant or stateful [`Self::num_args`] implementation from producing
87    /// a key and bounds from different observations.
88    fn arity(&self) -> FunctionArity {
89        let declared = self.num_args();
90        FunctionArity::from_declared_args(declared, || (self.min_args(), self.max_args()))
91    }
92
93    /// The function name, used in error messages and EXPLAIN output.
94    fn name(&self) -> &str;
95}
96
97/// Type-erased adapter that wraps a concrete [`AggregateFunction`] so the
98/// registry can store heterogeneous aggregates behind a single trait object.
99///
100/// The adapter implements `AggregateFunction<State = Box<dyn Any + Send>>`,
101/// boxing the concrete state on creation and downcasting on step/finalize.
102pub struct AggregateAdapter<F> {
103    inner: F,
104}
105
106impl<F> AggregateAdapter<F> {
107    /// Wrap a concrete aggregate function for type-erased storage.
108    pub const fn new(inner: F) -> Self {
109        Self { inner }
110    }
111}
112
113impl<F> AggregateFunction for AggregateAdapter<F>
114where
115    F: AggregateFunction,
116    F::State: 'static,
117{
118    type State = Box<dyn Any + Send>;
119
120    fn initial_state(&self) -> Self::State {
121        Box::new(self.inner.initial_state())
122    }
123
124    fn step(&self, state: &mut Self::State, args: &[SqliteValue]) -> Result<()> {
125        let concrete = state
126            .downcast_mut::<F::State>()
127            .expect("aggregate state type mismatch");
128        self.inner.step(concrete, args)
129    }
130
131    fn step_with_arg_subtypes(
132        &self,
133        state: &mut Self::State,
134        args: &[SqliteValue],
135        arg_subtypes: &[u32],
136    ) -> Result<()> {
137        let concrete = state
138            .downcast_mut::<F::State>()
139            .expect("aggregate state type mismatch");
140        self.inner
141            .step_with_arg_subtypes(concrete, args, arg_subtypes)
142    }
143
144    fn finalize(&self, state: Self::State) -> Result<SqliteValue> {
145        let concrete = *state
146            .downcast::<F::State>()
147            .expect("aggregate state type mismatch");
148        self.inner.finalize(concrete)
149    }
150
151    fn num_args(&self) -> i32 {
152        self.inner.num_args()
153    }
154
155    fn min_args(&self) -> i32 {
156        self.inner.min_args()
157    }
158
159    fn max_args(&self) -> Option<i32> {
160        self.inner.max_args()
161    }
162
163    fn arity(&self) -> FunctionArity {
164        self.inner.arity()
165    }
166
167    fn name(&self) -> &str {
168        self.inner.name()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use std::sync::Arc;
175
176    use super::*;
177
178    // -- Mock: Sum aggregate --
179
180    struct SumAgg;
181
182    impl AggregateFunction for SumAgg {
183        type State = i64;
184
185        fn initial_state(&self) -> i64 {
186            0
187        }
188
189        fn step(&self, state: &mut i64, args: &[SqliteValue]) -> Result<()> {
190            *state += args[0].to_integer();
191            Ok(())
192        }
193
194        fn finalize(&self, state: i64) -> Result<SqliteValue> {
195            Ok(SqliteValue::Integer(state))
196        }
197
198        fn num_args(&self) -> i32 {
199            1
200        }
201
202        fn name(&self) -> &str {
203            "sum"
204        }
205    }
206
207    #[test]
208    fn test_aggregate_initial_state() {
209        let agg = SumAgg;
210        assert_eq!(agg.initial_state(), 0);
211    }
212
213    #[test]
214    fn test_aggregate_step_and_finalize() {
215        let agg = SumAgg;
216        let mut state = agg.initial_state();
217
218        agg.step(&mut state, &[SqliteValue::Integer(10)]).unwrap();
219        agg.step(&mut state, &[SqliteValue::Integer(20)]).unwrap();
220        agg.step(&mut state, &[SqliteValue::Integer(12)]).unwrap();
221
222        let result = agg.finalize(state).unwrap();
223        assert_eq!(result, SqliteValue::Integer(42));
224    }
225
226    #[test]
227    fn test_aggregate_type_erasure_adapter() {
228        let adapted: AggregateAdapter<SumAgg> = AggregateAdapter::new(SumAgg);
229        let erased: Arc<dyn AggregateFunction<State = Box<dyn Any + Send>>> = Arc::new(adapted);
230
231        let mut state = erased.initial_state();
232        erased
233            .step(&mut state, &[SqliteValue::Integer(10)])
234            .unwrap();
235        erased
236            .step(&mut state, &[SqliteValue::Integer(32)])
237            .unwrap();
238
239        let result = erased.finalize(state).unwrap();
240        assert_eq!(result, SqliteValue::Integer(42));
241
242        // Verify we can clone the Arc (shared across threads).
243        let e2 = Arc::clone(&erased);
244        assert_eq!(e2.name(), "sum");
245    }
246}