vortex_array/stats/
bind.rs1use vortex_error::VortexResult;
17
18use crate::aggregate_fn::AggregateFnRef;
19use crate::dtype::DType;
20use crate::expr::BoundExpression;
21use crate::expr::bound::lit;
22use crate::expr::traversal::NodeExt;
23use crate::expr::traversal::Transformed;
24use crate::scalar::Scalar;
25use crate::scalar_fn::fns::stat::StatFn;
26
27pub trait StatBinder {
34 fn bind_aggregate(
39 &self,
40 input: &BoundExpression,
41 aggregate_fn: &AggregateFnRef,
42 stat_dtype: &DType,
43 ) -> VortexResult<Option<BoundExpression>>;
44
45 fn missing_stat(&self, dtype: DType) -> VortexResult<BoundExpression> {
50 null_expr(dtype)
51 }
52}
53
54pub fn bind_stats<B: StatBinder + ?Sized>(
60 predicate: BoundExpression,
61 binder: &B,
62) -> VortexResult<BoundExpression> {
63 Ok(predicate
64 .transform_down(|expr| {
65 if !expr.is::<StatFn>() {
66 return Ok(Transformed::no(expr));
67 }
68
69 match bind_stat_fn(&expr, binder)? {
70 Some(bound) => Ok(Transformed::yes(bound)),
71 None => Ok(Transformed::yes(binder.missing_stat(expr.dtype().clone())?)),
72 }
73 })?
74 .into_inner())
75}
76
77fn bind_stat_fn(
78 expr: &BoundExpression,
79 binder: &(impl StatBinder + ?Sized),
80) -> VortexResult<Option<BoundExpression>> {
81 let options = expr.as_::<StatFn>();
82 let aggregate_fn = options.aggregate_fn();
83 let input = expr.child(0);
85
86 binder.bind_aggregate(input, aggregate_fn, expr.dtype())
87}
88
89fn null_expr(dtype: DType) -> VortexResult<BoundExpression> {
90 Ok(lit(Scalar::null(dtype.as_nullable())))
91}
92
93#[cfg(test)]
94mod tests {
95 use vortex_error::VortexResult;
96
97 use super::*;
98 use crate::dtype::Nullability;
99 use crate::dtype::PType;
100 use crate::dtype::StructFields;
101 use crate::expr::and;
102 use crate::expr::col;
103 use crate::expr::get_item;
104 use crate::expr::is_null;
105 use crate::expr::lit;
106 use crate::expr::or;
107 use crate::expr::root;
108 use crate::expr::stats::Stat;
109 use crate::stats::all_non_nan;
110 use crate::stats::nan_count;
111
112 struct TestBinder {
113 input_scope: DType,
114 stats_scope: DType,
115 bind_nan_count: bool,
116 }
117
118 impl TestBinder {
119 fn new(bind_nan_count: bool) -> Self {
120 Self {
121 input_scope: DType::Struct(
122 StructFields::from_iter([(
123 "f",
124 DType::Primitive(PType::F32, Nullability::NonNullable),
125 )]),
126 Nullability::NonNullable,
127 ),
128 stats_scope: DType::Struct(
129 StructFields::from_iter([(
130 "f_nan_count",
131 DType::Primitive(PType::U64, Nullability::NonNullable),
132 )]),
133 Nullability::NonNullable,
134 ),
135 bind_nan_count,
136 }
137 }
138 }
139
140 impl StatBinder for TestBinder {
141 fn bind_aggregate(
142 &self,
143 _input: &BoundExpression,
144 aggregate_fn: &AggregateFnRef,
145 _stat_dtype: &DType,
146 ) -> VortexResult<Option<BoundExpression>> {
147 let Some(stat) = Stat::from_aggregate_fn(aggregate_fn) else {
148 return Ok(None);
149 };
150
151 if stat == Stat::NaNCount && self.bind_nan_count {
152 Ok(Some(
153 get_item("f_nan_count", root()).bind(&self.stats_scope)?,
154 ))
155 } else {
156 Ok(None)
157 }
158 }
159 }
160
161 #[test]
162 fn nan_count_binds_to_direct_stat_slot() -> VortexResult<()> {
163 let binder = TestBinder::new(true);
164
165 let bound = bind_stats(nan_count(col("f")).bind(&binder.input_scope)?, &binder)?;
166
167 assert_eq!(bound, col("f_nan_count").bind(&binder.stats_scope)?);
168 Ok(())
169 }
170
171 #[test]
172 fn all_non_nan_does_not_derive_from_nan_count() -> VortexResult<()> {
173 let binder = TestBinder::new(true);
174
175 let bound = bind_stats(all_non_nan(col("f")).bind(&binder.input_scope)?, &binder)?;
176
177 assert_eq!(
178 bound,
179 lit(Scalar::null(DType::Bool(Nullability::Nullable))).bind(&binder.stats_scope)?
180 );
181 Ok(())
182 }
183
184 #[test]
185 fn missing_stats_bind_to_null_without_reducing() -> VortexResult<()> {
186 let binder = TestBinder::new(false);
187 let null_bool = lit(Scalar::null(DType::Bool(Nullability::Nullable)));
188
189 let bound = bind_stats(
190 and(lit(false), all_non_nan(col("f"))).bind(&binder.input_scope)?,
191 &binder,
192 )?;
193
194 assert_eq!(
195 bound,
196 and(lit(false), null_bool.clone()).bind(&binder.stats_scope)?
197 );
198
199 let bound = bind_stats(
200 or(lit(true), all_non_nan(col("f"))).bind(&binder.input_scope)?,
201 &binder,
202 )?;
203
204 assert_eq!(bound, or(lit(true), null_bool).bind(&binder.stats_scope)?);
205 Ok(())
206 }
207
208 #[test]
209 fn unrelated_expressions_do_not_request_nan_count() -> VortexResult<()> {
210 let binder = TestBinder::new(false);
211
212 let bound = bind_stats(is_null(col("f")).bind(&binder.input_scope)?, &binder)?;
213
214 assert_eq!(bound, is_null(col("f")).bind(&binder.input_scope)?);
215 Ok(())
216 }
217}