@@ -64,41 +64,7 @@ pub trait StatBinder {
6464 aggregate_fn : & AggregateFnRef ,
6565 stat_dtype : & DType ,
6666 ) -> VortexResult < Option < Expression > > {
67- // These aggregate stats are derived from legacy count slots rather than
68- // stored directly. Keep that storage mapping in binding so stats rewrite
69- // rules can continue to ask for the semantic aggregate they need.
70- if aggregate_fn. is :: < AllNan > ( ) {
71- let Some ( nan_count) = self . bind_legacy_stat ( input, Stat :: NaNCount ) ? else {
72- return Ok ( None ) ;
73- } ;
74- return Ok ( Some ( eq ( nan_count, RowCount . new_expr ( EmptyOptions , [ ] ) ) ) ) ;
75- }
76-
77- if aggregate_fn. is :: < AllNonNan > ( ) {
78- let Some ( nan_count) = self . bind_legacy_stat ( input, Stat :: NaNCount ) ? else {
79- return Ok ( None ) ;
80- } ;
81- return Ok ( Some ( eq ( nan_count, lit ( 0u64 ) ) ) ) ;
82- }
83-
84- if aggregate_fn. is :: < AllNull > ( ) {
85- let Some ( null_count) = self . bind_legacy_stat ( input, Stat :: NullCount ) ? else {
86- return Ok ( None ) ;
87- } ;
88- return Ok ( Some ( eq ( null_count, RowCount . new_expr ( EmptyOptions , [ ] ) ) ) ) ;
89- }
90-
91- if aggregate_fn. is :: < AllNonNull > ( ) {
92- let Some ( null_count) = self . bind_legacy_stat ( input, Stat :: NullCount ) ? else {
93- return Ok ( None ) ;
94- } ;
95- return Ok ( Some ( eq ( null_count, lit ( 0u64 ) ) ) ) ;
96- }
97-
98- let Some ( stat) = Stat :: from_aggregate_fn ( aggregate_fn) else {
99- return Ok ( None ) ;
100- } ;
101- self . bind_stat ( input, stat, stat_dtype)
67+ bind_direct_aggregate_stat ( self , input, aggregate_fn, stat_dtype)
10268 }
10369
10470 /// Bind one of the legacy stat slots for `input`.
@@ -149,6 +115,77 @@ pub fn bind_stats(predicate: Expression, binder: &mut impl StatBinder) -> Vortex
149115 lowered. optimize_recursive ( & binder. bound_scope ( ) )
150116}
151117
118+ /// Bind aggregate stats that can be derived from legacy count stat slots.
119+ ///
120+ /// This is an opt-in helper for stats backends that materialize `NaNCount` and
121+ /// `NullCount`, but do not materialize aggregate boolean stats directly.
122+ pub fn bind_legacy_count_aggregate < B : StatBinder + ?Sized > (
123+ binder : & mut B ,
124+ input : & Expression ,
125+ aggregate_fn : & AggregateFnRef ,
126+ ) -> VortexResult < Option < Expression > > {
127+ if aggregate_fn. is :: < AllNan > ( ) {
128+ let Some ( nan_count) = binder. bind_legacy_stat ( input, Stat :: NaNCount ) ? else {
129+ return Ok ( None ) ;
130+ } ;
131+ return Ok ( Some ( eq ( nan_count, RowCount . new_expr ( EmptyOptions , [ ] ) ) ) ) ;
132+ }
133+
134+ if aggregate_fn. is :: < AllNonNan > ( ) {
135+ let Some ( nan_count) = binder. bind_legacy_stat ( input, Stat :: NaNCount ) ? else {
136+ return Ok ( None ) ;
137+ } ;
138+ return Ok ( Some ( eq ( nan_count, lit ( 0u64 ) ) ) ) ;
139+ }
140+
141+ if aggregate_fn. is :: < AllNull > ( ) {
142+ let Some ( null_count) = binder. bind_legacy_stat ( input, Stat :: NullCount ) ? else {
143+ return Ok ( None ) ;
144+ } ;
145+ return Ok ( Some ( eq ( null_count, RowCount . new_expr ( EmptyOptions , [ ] ) ) ) ) ;
146+ }
147+
148+ if aggregate_fn. is :: < AllNonNull > ( ) {
149+ let Some ( null_count) = binder. bind_legacy_stat ( input, Stat :: NullCount ) ? else {
150+ return Ok ( None ) ;
151+ } ;
152+ return Ok ( Some ( eq ( null_count, lit ( 0u64 ) ) ) ) ;
153+ }
154+
155+ Ok ( None )
156+ }
157+
158+ /// Bind an aggregate function that has a direct legacy [`Stat`] slot.
159+ pub fn bind_direct_aggregate_stat < B : StatBinder + ?Sized > (
160+ binder : & mut B ,
161+ input : & Expression ,
162+ aggregate_fn : & AggregateFnRef ,
163+ stat_dtype : & DType ,
164+ ) -> VortexResult < Option < Expression > > {
165+ let Some ( stat) = Stat :: from_aggregate_fn ( aggregate_fn) else {
166+ return Ok ( None ) ;
167+ } ;
168+ binder. bind_stat ( input, stat, stat_dtype)
169+ }
170+
171+ /// Bind aggregate stats for backends that expose legacy count-derived stats.
172+ ///
173+ /// Backends using this helper first bind aggregate facts derivable from
174+ /// `NaNCount` and `NullCount`, then fall back to direct aggregate-to-stat
175+ /// mappings.
176+ pub fn bind_legacy_count_or_direct_aggregate < B : StatBinder + ?Sized > (
177+ binder : & mut B ,
178+ input : & Expression ,
179+ aggregate_fn : & AggregateFnRef ,
180+ stat_dtype : & DType ,
181+ ) -> VortexResult < Option < Expression > > {
182+ if let Some ( bound) = bind_legacy_count_aggregate ( binder, input, aggregate_fn) ? {
183+ return Ok ( Some ( bound) ) ;
184+ }
185+
186+ bind_direct_aggregate_stat ( binder, input, aggregate_fn, stat_dtype)
187+ }
188+
152189fn bind_stat_fn (
153190 expr : & Expression ,
154191 scope : & DType ,
@@ -175,9 +212,11 @@ mod tests {
175212 use crate :: dtype:: Nullability ;
176213 use crate :: dtype:: PType ;
177214 use crate :: dtype:: StructFields ;
215+ use crate :: expr:: and;
178216 use crate :: expr:: col;
179217 use crate :: expr:: get_item;
180218 use crate :: expr:: is_null;
219+ use crate :: expr:: or;
181220 use crate :: expr:: root;
182221 use crate :: stats:: all_non_nan;
183222
@@ -230,6 +269,15 @@ mod tests {
230269 Ok ( None )
231270 }
232271 }
272+
273+ fn bind_aggregate (
274+ & mut self ,
275+ input : & Expression ,
276+ aggregate_fn : & AggregateFnRef ,
277+ stat_dtype : & DType ,
278+ ) -> VortexResult < Option < Expression > > {
279+ bind_legacy_count_or_direct_aggregate ( self , input, aggregate_fn, stat_dtype)
280+ }
233281 }
234282
235283 #[ test]
@@ -252,6 +300,51 @@ mod tests {
252300 Ok ( ( ) )
253301 }
254302
303+ #[ test]
304+ fn missing_stats_fold_when_kleene_semantics_allow_it ( ) -> VortexResult < ( ) > {
305+ let mut binder = TestBinder :: new ( false ) ;
306+
307+ let bound = bind_stats ( and ( lit ( false ) , all_non_nan ( col ( "f" ) ) ) , & mut binder) ?;
308+
309+ assert_eq ! ( bound, lit( false ) ) ;
310+
311+ let bound = bind_stats ( or ( lit ( true ) , all_non_nan ( col ( "f" ) ) ) , & mut binder) ?;
312+
313+ assert_eq ! ( bound, lit( true ) ) ;
314+ Ok ( ( ) )
315+ }
316+
317+ #[ test]
318+ fn default_binder_does_not_derive_all_non_nan_from_nan_count ( ) -> VortexResult < ( ) > {
319+ struct DefaultBinder ( TestBinder ) ;
320+
321+ impl StatBinder for DefaultBinder {
322+ fn scope ( & self ) -> & DType {
323+ self . 0 . scope ( )
324+ }
325+
326+ fn bound_scope ( & self ) -> DType {
327+ self . 0 . bound_scope ( )
328+ }
329+
330+ fn bind_stat (
331+ & mut self ,
332+ input : & Expression ,
333+ stat : Stat ,
334+ stat_dtype : & DType ,
335+ ) -> VortexResult < Option < Expression > > {
336+ self . 0 . bind_stat ( input, stat, stat_dtype)
337+ }
338+ }
339+
340+ let mut binder = DefaultBinder ( TestBinder :: new ( true ) ) ;
341+
342+ let bound = bind_stats ( all_non_nan ( col ( "f" ) ) , & mut binder) ?;
343+
344+ assert_eq ! ( bound, lit( Scalar :: null( DType :: Bool ( Nullability :: Nullable ) ) ) ) ;
345+ Ok ( ( ) )
346+ }
347+
255348 #[ test]
256349 fn unrelated_expressions_do_not_request_nan_count ( ) -> VortexResult < ( ) > {
257350 let mut binder = TestBinder :: new ( false ) ;
0 commit comments