vortex_array/aggregate_fn/
proto.rs1use 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 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 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 #[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 #[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}