vortex_array/aggregate_fn/fns/null_count/
mod.rs1use vortex_error::VortexExpect;
5use vortex_error::VortexResult;
6use vortex_error::vortex_err;
7use vortex_session::VortexSession;
8use vortex_session::registry::CachedId;
9
10use crate::ArrayRef;
11use crate::Columnar;
12use crate::ExecutionCtx;
13use crate::IntoArray;
14use crate::aggregate_fn::Accumulator;
15use crate::aggregate_fn::AggregateArgs;
16use crate::aggregate_fn::AggregateFnId;
17use crate::aggregate_fn::AggregateFnVTable;
18use crate::aggregate_fn::DynAccumulator;
19use crate::aggregate_fn::EmptyOptions;
20use crate::dtype::DType;
21use crate::dtype::Nullability::NonNullable;
22use crate::dtype::PType;
23use crate::expr::stats::Precision;
24use crate::expr::stats::Stat;
25use crate::expr::stats::StatsProvider;
26use crate::scalar::Scalar;
27use crate::scalar::ScalarValue;
28
29pub fn null_count(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
31 if let Precision::Exact(null_count_scalar) = array.statistics().get(Stat::NullCount) {
32 return usize::try_from(&null_count_scalar)
33 .map_err(|e| vortex_err!("Failed to convert null count stat to usize: {e}"));
34 }
35
36 let mut acc = Accumulator::try_new(NullCount, EmptyOptions, array.dtype().clone())?;
37 acc.accumulate(array, ctx)?;
38 let result = acc.finish()?;
39
40 let count = result
41 .as_primitive()
42 .typed_value::<u64>()
43 .vortex_expect("null_count result should not be null");
44 let count_usize = usize::try_from(count).vortex_expect("Cannot be more nulls than usize::MAX");
45
46 array
47 .statistics()
48 .set(Stat::NullCount, Precision::Exact(ScalarValue::from(count)));
49
50 Ok(count_usize)
51}
52
53#[derive(Clone, Debug)]
57pub struct NullCount;
58
59impl AggregateFnVTable for NullCount {
60 type Options = EmptyOptions;
61 type Partial = u64;
62
63 fn id(&self) -> AggregateFnId {
64 static ID: CachedId = CachedId::new("vortex.null_count");
65 *ID
66 }
67
68 fn serialize(&self, _options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
69 Ok(Some(vec![]))
70 }
71
72 fn deserialize(
73 &self,
74 _metadata: &[u8],
75 _session: &VortexSession,
76 ) -> VortexResult<Self::Options> {
77 Ok(EmptyOptions)
78 }
79
80 fn return_dtype(&self, _options: &Self::Options, _input_dtype: &DType) -> Option<DType> {
81 Some(DType::Primitive(PType::U64, NonNullable))
82 }
83
84 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
85 self.return_dtype(options, input_dtype)
86 }
87
88 fn empty_partial(
89 &self,
90 _args: AggregateArgs<'_, Self::Options>,
91 ) -> VortexResult<Self::Partial> {
92 Ok(0)
93 }
94
95 fn partial_from_scalar(
96 &self,
97 _args: AggregateArgs<'_, Self::Options>,
98 scalar: Scalar,
99 ) -> VortexResult<Self::Partial> {
100 Ok(scalar
101 .as_primitive()
102 .typed_value::<u64>()
103 .vortex_expect("null_count partial should not be null"))
104 }
105
106 fn merge_partials(
107 &self,
108 _args: AggregateArgs<'_, Self::Options>,
109 first: Self::Partial,
110 second: Self::Partial,
111 ) -> VortexResult<Self::Partial> {
112 Ok(first + second)
113 }
114
115 fn to_scalar(
116 &self,
117 _args: AggregateArgs<'_, Self::Options>,
118 partial: &Self::Partial,
119 ) -> VortexResult<Scalar> {
120 Ok(Scalar::primitive(*partial, NonNullable))
121 }
122
123 #[inline]
124 fn is_saturated(
125 &self,
126 _args: AggregateArgs<'_, Self::Options>,
127 _partial: &Self::Partial,
128 ) -> bool {
129 false
130 }
131
132 fn try_accumulate(
133 &self,
134 _args: AggregateArgs<'_, Self::Options>,
135 state: &mut Self::Partial,
136 batch: &ArrayRef,
137 ctx: &mut ExecutionCtx,
138 ) -> VortexResult<bool> {
139 *state += batch.invalid_count(ctx)? as u64;
140 Ok(true)
141 }
142
143 fn accumulate(
144 &self,
145 _args: AggregateArgs<'_, Self::Options>,
146 partial: &mut Self::Partial,
147 batch: &Columnar,
148 ctx: &mut ExecutionCtx,
149 ) -> VortexResult<()> {
150 *partial += match batch {
151 Columnar::Constant(c) => {
152 if c.scalar().is_null() {
153 c.len() as u64
154 } else {
155 0
156 }
157 }
158 Columnar::Canonical(c) => c.clone().into_array().invalid_count(ctx)? as u64,
159 };
160 Ok(())
161 }
162
163 fn finalize(
164 &self,
165 _args: AggregateArgs<'_, Self::Options>,
166 partials: ArrayRef,
167 ) -> VortexResult<ArrayRef> {
168 Ok(partials)
169 }
170
171 fn finalize_scalar(
172 &self,
173 args: AggregateArgs<'_, Self::Options>,
174 partial: &Self::Partial,
175 ) -> VortexResult<Scalar> {
176 self.to_scalar(args, partial)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use vortex_error::VortexResult;
183
184 use crate::IntoArray;
185 use crate::VortexSessionExecute;
186 use crate::aggregate_fn::Accumulator;
187 use crate::aggregate_fn::DynAccumulator;
188 use crate::aggregate_fn::EmptyOptions;
189 use crate::aggregate_fn::fns::null_count::NullCount;
190 use crate::aggregate_fn::fns::null_count::null_count;
191 use crate::array_session;
192 use crate::arrays::PrimitiveArray;
193 use crate::dtype::DType;
194 use crate::dtype::Nullability;
195 use crate::dtype::PType;
196 use crate::expr::stats::Precision;
197 use crate::expr::stats::Stat;
198 use crate::expr::stats::StatsProviderExt;
199
200 #[test]
201 fn null_count_with_nulls() -> VortexResult<()> {
202 let array =
203 PrimitiveArray::from_option_iter([Some(1i32), None, Some(3), None]).into_array();
204 let mut ctx = array_session().create_execution_ctx();
205
206 assert_eq!(null_count(&array, &mut ctx)?, 2);
207 assert_eq!(
208 array.statistics().get_as::<u64>(Stat::NullCount),
209 Precision::exact(2u64)
210 );
211 Ok(())
212 }
213
214 #[test]
215 fn null_count_multi_batch() -> VortexResult<()> {
216 let mut ctx = array_session().create_execution_ctx();
217 let dtype = DType::Primitive(PType::I32, Nullability::Nullable);
218 let mut acc = Accumulator::try_new(NullCount, EmptyOptions, dtype)?;
219
220 let batch1 = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]).into_array();
221 acc.accumulate(&batch1, &mut ctx)?;
222
223 let batch2 = PrimitiveArray::from_option_iter([None, Some(5i32), None]).into_array();
224 acc.accumulate(&batch2, &mut ctx)?;
225
226 let result = acc.finish()?;
227 assert_eq!(result.as_primitive().typed_value::<u64>(), Some(3));
228 Ok(())
229 }
230}