vgi/aggregate.rs
1// Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
3//! Aggregate function model (update / combine / finalize).
4//!
5//! DuckDB drives aggregates with per-group state. The worker keeps state in
6//! the cross-process [`FunctionStorage`](crate::storage::FunctionStorage) (KV
7//! mode), keyed by `(execution_id, group_id)`, because update / combine /
8//! finalize can run in different pooled worker processes. States are opaque to
9//! the C++ extension — the worker chooses any encoding.
10
11use std::collections::HashMap;
12
13use arrow_array::{ArrayRef, Int64Array, RecordBatch};
14use arrow_schema::SchemaRef;
15use vgi_rpc::{Result, RpcError};
16
17use crate::arguments::Arguments;
18use crate::function::{ArgSpec, BindResponse, FunctionMetadata};
19use crate::settings::Settings;
20
21/// The reserved group-id column prepended to UPDATE input batches.
22pub const GROUP_COLUMN_NAME: &str = "__vgi_group_id";
23
24/// Parameters for `aggregate_bind`.
25pub struct AggregateBindParams {
26 pub arguments: Arguments,
27 pub input_schema: Option<SchemaRef>,
28 pub settings: Settings,
29 /// Statically pre-resolved secrets, delivered on `AggregateBindRequest.secrets`
30 /// when the aggregate advertises a required secret (see
31 /// [`FunctionMetadata::required_secrets`](crate::function::FunctionMetadata::required_secrets)).
32 /// Aggregates read a secret *value* here at bind time only — two-phase
33 /// `.get()`-style secret resolution is not supported for aggregates. Empty
34 /// when nothing was requested / resolved.
35 pub secrets: crate::secrets::Secrets,
36}
37
38/// An aggregate VGI function.
39pub trait AggregateFunction: Send + Sync {
40 fn name(&self) -> &str;
41 fn metadata(&self) -> FunctionMetadata;
42 fn argument_specs(&self) -> Vec<ArgSpec>;
43 /// Resolve the (single-column `result`) output schema.
44 fn on_bind(&self, params: &AggregateBindParams) -> Result<BindResponse>;
45 /// The serialized initial state for a fresh group.
46 fn initial_state(&self) -> Vec<u8>;
47 /// Fold the batch's rows into the per-group `states` map. `states` is
48 /// pre-loaded (initial state for new groups) for every group id present in
49 /// `group_ids`. `columns` are the input columns with the group-id column
50 /// already stripped.
51 fn update(
52 &self,
53 states: &mut HashMap<i64, Vec<u8>>,
54 group_ids: &Int64Array,
55 columns: &[ArrayRef],
56 ) -> Result<()>;
57 /// Merge `source` state into `target` state, returning the new target.
58 fn combine(&self, target: Vec<u8>, source: Vec<u8>) -> Result<Vec<u8>>;
59 /// Build the single-column output batch: one row per `group_ids` entry.
60 /// `states[i]` is the loaded state for `group_ids[i]` (`None` if unseen).
61 fn finalize(
62 &self,
63 output_schema: &SchemaRef,
64 group_ids: &Int64Array,
65 states: &[Option<Vec<u8>>],
66 ) -> Result<RecordBatch>;
67 /// Evaluate the windowed aggregate for each output row. `frames[i]` is the
68 /// list of `(begin, end)` sub-frames for output row `i` over the
69 /// `partition`'s input columns; returns the output column (one element per
70 /// row), matching `output_schema`. Only `supports_window` functions
71 /// override this; the default errors.
72 fn window(
73 &self,
74 _partition: &RecordBatch,
75 _output_schema: &SchemaRef,
76 _frames: &[Vec<(i64, i64)>],
77 _filter_mask: Option<&[bool]>,
78 ) -> Result<arrow_array::ArrayRef> {
79 Err(RpcError::runtime_error(
80 "window() not supported by this aggregate",
81 ))
82 }
83
84 /// Process one chunk of a streaming-partitioned session. The chunk's
85 /// columns are `[partition_key_cols.., order_key_cols.., value_cols..]`.
86 /// `states` is the cross-chunk per-partition state map (partition-key bytes
87 /// → opaque state bytes), loaded and persisted by the framework. Returns a
88 /// same-length output column. Only `streaming_partitioned` functions
89 /// override this; the default errors.
90 fn streaming_chunk(
91 &self,
92 _chunk: &RecordBatch,
93 _partition_key_count: usize,
94 _order_key_count: usize,
95 _states: &mut HashMap<Vec<u8>, Vec<u8>>,
96 ) -> Result<ArrayRef> {
97 Err(RpcError::runtime_error(
98 "streaming_chunk() not supported by this aggregate",
99 ))
100 }
101
102 /// Like [`Self::finalize`], but with access to the bind-time arguments (stashed
103 /// at `aggregate_bind`, reloaded here). Override for `ConstParam(phase=
104 /// "finalize")` aggregates like `vgi_percentile`. The default ignores them.
105 fn finalize_with_args(
106 &self,
107 output_schema: &SchemaRef,
108 group_ids: &Int64Array,
109 states: &[Option<Vec<u8>>],
110 _args: &crate::arguments::Arguments,
111 ) -> Result<RecordBatch> {
112 self.finalize(output_schema, group_ids, states)
113 }
114}