Skip to main content

vortex_array/aggregate_fn/
proto.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_error::VortexResult;
5use vortex_error::vortex_bail;
6use vortex_error::vortex_err;
7use vortex_session::VortexSession;
8
9use crate::aggregate_fn::AggregateFnId;
10use crate::aggregate_fn::AggregateFnRef;
11use crate::aggregate_fn::new_foreign_aggregate_fn;
12use crate::aggregate_fn::session::AggregateFnSessionExt;
13use crate::proto::expr as pb;
14
15impl AggregateFnRef {
16    /// Serialize this aggregate function to its protobuf representation.
17    ///
18    /// Note: the serialization format is not stable and may change between versions.
19    pub fn serialize_proto(&self) -> VortexResult<pb::AggregateFn> {
20        let metadata = self
21            .options()
22            .serialize()?
23            .ok_or_else(|| vortex_err!("Aggregate function '{}' is not serializable", self.id()))?;
24
25        Ok(pb::AggregateFn {
26            id: self.id().to_string(),
27            metadata: Some(metadata),
28        })
29    }
30
31    /// Deserialize an aggregate function from its protobuf representation.
32    ///
33    /// Looks up the aggregate function plugin by ID in the session's registry
34    /// and delegates deserialization to it.
35    ///
36    /// Note: the serialization format is not stable and may change between versions.
37    pub fn from_proto(proto: &pb::AggregateFn, session: &VortexSession) -> VortexResult<Self> {
38        #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
39        let agg_fn_id: AggregateFnId = AggregateFnId::new(proto.id.as_str());
40        let agg_fn = if let Some(plugin) = session.aggregate_fns().find_plugin(&agg_fn_id) {
41            plugin.deserialize(proto.metadata(), session)?
42        } else if session.allows_unknown() {
43            new_foreign_aggregate_fn(agg_fn_id, proto.metadata().to_vec())
44        } else {
45            return Err(vortex_err!("unknown aggregate function id: {}", proto.id));
46        };
47
48        if agg_fn.id() != agg_fn_id {
49            vortex_bail!(
50                "Aggregate function ID mismatch: expected {}, got {}",
51                agg_fn_id,
52                agg_fn.id()
53            );
54        }
55
56        Ok(agg_fn)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use prost::Message;
63    use rstest::rstest;
64    use vortex_error::VortexResult;
65    use vortex_error::vortex_panic;
66    use vortex_session::VortexSession;
67
68    use crate::ArrayRef;
69    use crate::Columnar;
70    use crate::ExecutionCtx;
71    use crate::aggregate_fn::AggregateArgs;
72    use crate::aggregate_fn::AggregateFnId;
73    use crate::aggregate_fn::AggregateFnRef;
74    use crate::aggregate_fn::AggregateFnVTable;
75    use crate::aggregate_fn::AggregateFnVTableExt;
76    use crate::aggregate_fn::EmptyOptions;
77    use crate::aggregate_fn::NumericalAggregateOpts;
78    use crate::aggregate_fn::fns::sum::Sum;
79    use crate::aggregate_fn::session::AggregateFnSession;
80    use crate::aggregate_fn::session::AggregateFnSessionExt;
81    use crate::dtype::DType;
82    use crate::proto::expr as pb;
83    use crate::scalar::Scalar;
84
85    /// A minimal serializable aggregate function used solely to exercise the serde round-trip.
86    #[derive(Clone, Debug)]
87    struct TestAgg;
88
89    impl AggregateFnVTable for TestAgg {
90        type Options = EmptyOptions;
91        type Partial = ();
92
93        #[expect(clippy::disallowed_methods, reason = "test-only id")]
94        fn id(&self) -> AggregateFnId {
95            AggregateFnId::new("vortex.test.proto")
96        }
97
98        fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
99            Ok(Some(vec![]))
100        }
101
102        fn deserialize(
103            &self,
104            _metadata: &[u8],
105            _session: &VortexSession,
106        ) -> VortexResult<Self::Options> {
107            Ok(EmptyOptions)
108        }
109
110        fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
111            Some(input_dtype.clone())
112        }
113
114        fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
115            self.return_dtype(options, input_dtype)
116        }
117
118        fn empty_partial(
119            &self,
120            _args: AggregateArgs<'_, Self::Options>,
121        ) -> VortexResult<Self::Partial> {
122            Ok(())
123        }
124
125        fn partial_from_scalar(
126            &self,
127            _args: AggregateArgs<'_, Self::Options>,
128            _scalar: Scalar,
129        ) -> VortexResult<Self::Partial> {
130            Ok(())
131        }
132
133        fn merge_partials(
134            &self,
135            _args: AggregateArgs<'_, Self::Options>,
136            _first: Self::Partial,
137            _second: Self::Partial,
138        ) -> VortexResult<Self::Partial> {
139            Ok(())
140        }
141
142        fn to_scalar(
143            &self,
144            _args: AggregateArgs<'_, Self::Options>,
145            _partial: &Self::Partial,
146        ) -> VortexResult<Scalar> {
147            vortex_panic!("TestAgg is for serde tests only");
148        }
149
150        fn is_saturated(
151            &self,
152            _args: AggregateArgs<'_, Self::Options>,
153            _partial: &Self::Partial,
154        ) -> bool {
155            true
156        }
157
158        fn accumulate(
159            &self,
160            _args: AggregateArgs<'_, Self::Options>,
161            _state: &mut Self::Partial,
162            _batch: &Columnar,
163            _ctx: &mut ExecutionCtx,
164        ) -> VortexResult<()> {
165            Ok(())
166        }
167
168        fn finalize(
169            &self,
170            _args: AggregateArgs<'_, Self::Options>,
171            partials: ArrayRef,
172        ) -> VortexResult<ArrayRef> {
173            Ok(partials)
174        }
175
176        fn finalize_scalar(
177            &self,
178            _args: AggregateArgs<'_, Self::Options>,
179            _partial: &Self::Partial,
180        ) -> VortexResult<Scalar> {
181            vortex_panic!("TestAgg is for serde tests only");
182        }
183    }
184
185    #[test]
186    fn aggregate_fn_serde() {
187        let session = crate::array_session();
188        session.aggregate_fns().register(TestAgg);
189
190        let agg_fn = TestAgg.bind(EmptyOptions);
191
192        let serialized = agg_fn.serialize_proto().unwrap();
193        let buf = serialized.encode_to_vec();
194        let deserialized_proto = pb::AggregateFn::decode(buf.as_slice()).unwrap();
195        let deserialized = AggregateFnRef::from_proto(&deserialized_proto, &session).unwrap();
196
197        assert_eq!(deserialized, agg_fn);
198    }
199
200    /// The `skip_nans` option must survive a protobuf serialize/deserialize round-trip for the
201    /// numeric aggregates, including the non-default NaN-including configuration.
202    #[rstest]
203    #[case(NumericalAggregateOpts::skip_nans())]
204    #[case(NumericalAggregateOpts::include_nans())]
205    fn numeric_aggregate_options_round_trip(
206        #[case] options: NumericalAggregateOpts,
207    ) -> VortexResult<()> {
208        let session = crate::array_session();
209        let agg_fn = Sum.bind(options);
210
211        let proto = agg_fn.serialize_proto()?;
212        let buf = proto.encode_to_vec();
213        let decoded = pb::AggregateFn::decode(buf.as_slice())?;
214        let round_tripped = AggregateFnRef::from_proto(&decoded, &session)?;
215
216        assert_eq!(round_tripped, agg_fn);
217        Ok(())
218    }
219
220    #[test]
221    fn unknown_aggregate_fn_id_allow_unknown() {
222        let session = VortexSession::empty().with::<AggregateFnSession>();
223        session.allow_unknown();
224
225        let proto = pb::AggregateFn {
226            id: "vortex.test.foreign_aggregate".to_string(),
227            metadata: Some(vec![7, 8, 9]),
228        };
229
230        let agg_fn = AggregateFnRef::from_proto(&proto, &session).unwrap();
231        assert_eq!(agg_fn.id().as_ref(), "vortex.test.foreign_aggregate");
232
233        let roundtrip = agg_fn.serialize_proto().unwrap();
234        assert_eq!(roundtrip.id, proto.id);
235        assert_eq!(roundtrip.metadata(), proto.metadata());
236    }
237}