@@ -145,13 +145,26 @@ impl ZoneMap {
145145 & self ,
146146 predicate : & BoundExpression ,
147147 session : & VortexSession ,
148+ ) -> VortexResult < Mask > {
149+ let predicate = self . lower_stats ( predicate. clone ( ) ) ?;
150+ self . prune_lowered ( & predicate, session)
151+ }
152+
153+ /// Apply a predicate that [`Self::lower_stats`] already lowered to this zone map's
154+ /// stats table.
155+ ///
156+ /// Lowering depends only on the column and stats-table dtypes, so callers that evaluate
157+ /// the same predicate many times lower it once and call this function for each evaluation.
158+ pub ( super ) fn prune_lowered (
159+ & self ,
160+ predicate : & BoundExpression ,
161+ session : & VortexSession ,
148162 ) -> VortexResult < Mask > {
149163 let mut ctx = session. create_execution_ctx ( ) ;
150164 let num_zones = self . array . len ( ) ;
151- let predicate = self . lower_stats ( predicate. clone ( ) ) ?;
152165
153166 let array = self . array . clone ( ) . into_array ( ) ;
154- let applied = array. apply_bound ( & predicate) ?;
167+ let applied = array. apply_bound ( predicate) ?;
155168
156169 if !contains_row_count ( & applied) {
157170 return null_as_false_mask ( applied, & mut ctx) ;
@@ -162,7 +175,8 @@ impl ZoneMap {
162175 null_as_false_mask ( substituted, & mut ctx)
163176 }
164177
165- fn lower_stats ( & self , predicate : BoundExpression ) -> VortexResult < BoundExpression > {
178+ /// Rewrite the stats references in `predicate` into expressions over the stats table.
179+ pub ( super ) fn lower_stats ( & self , predicate : BoundExpression ) -> VortexResult < BoundExpression > {
166180 let binder = ZoneMapStatsBinder { zone_map : self } ;
167181 bind_stats ( predicate, & binder)
168182 }
@@ -377,6 +391,8 @@ fn row_count_array(zone_len: u64, row_count: u64, num_zones: usize) -> VortexRes
377391mod tests {
378392 use std:: num:: NonZeroUsize ;
379393 use std:: sync:: Arc ;
394+ use std:: sync:: atomic:: AtomicI32 ;
395+ use std:: sync:: atomic:: Ordering ;
380396
381397 use rstest:: rstest;
382398 use vortex_array:: IntoArray ;
@@ -408,6 +424,7 @@ mod tests {
408424 use vortex_array:: expr:: BoundExpression ;
409425 use vortex_array:: expr:: Expression ;
410426 use vortex_array:: expr:: cast;
427+ use vortex_array:: expr:: dynamic;
411428 use vortex_array:: expr:: gt;
412429 use vortex_array:: expr:: gt_eq;
413430 use vortex_array:: expr:: is_not_null;
@@ -417,6 +434,8 @@ mod tests {
417434 use vortex_array:: expr:: not_eq;
418435 use vortex_array:: expr:: root;
419436 use vortex_array:: expr:: stats:: Stat ;
437+ use vortex_array:: scalar:: ScalarValue ;
438+ use vortex_array:: scalar_fn:: fns:: operators:: CompareOperator ;
420439 use vortex_array:: stats:: all_nan;
421440 use vortex_array:: stats:: all_non_nan;
422441 use vortex_array:: stats:: all_non_null;
@@ -1076,4 +1095,60 @@ mod tests {
10761095 & mut SESSION . create_execution_ctx( )
10771096 ) ;
10781097 }
1098+
1099+ #[ test]
1100+ fn lowered_dynamic_predicate_tracks_rhs_updates ( ) -> VortexResult < ( ) > {
1101+ let max = Max . bind ( NumericalAggregateOpts :: skip_nans ( ) ) ;
1102+ let min = Min . bind ( NumericalAggregateOpts :: skip_nans ( ) ) ;
1103+ let zone_map = ZoneMap :: try_new (
1104+ PType :: I32 . into ( ) ,
1105+ StructArray :: from_fields ( & [
1106+ (
1107+ max. to_string ( ) ,
1108+ PrimitiveArray :: new ( buffer ! [ 5i32 , 6i32 , 7i32 ] , Validity :: AllValid ) . into_array ( ) ,
1109+ ) ,
1110+ (
1111+ min. to_string ( ) ,
1112+ PrimitiveArray :: new ( buffer ! [ 1i32 , 2i32 , 3i32 ] , Validity :: AllValid ) . into_array ( ) ,
1113+ ) ,
1114+ ] ) ?,
1115+ Arc :: new ( [ max, min] ) ,
1116+ 3 ,
1117+ 10 ,
1118+ ) ?;
1119+ let ctx = & mut SESSION . create_execution_ctx ( ) ;
1120+
1121+ let threshold = Arc :: new ( AtomicI32 :: new ( 5 ) ) ;
1122+ let rhs = Arc :: clone ( & threshold) ;
1123+ let expr = dynamic (
1124+ CompareOperator :: Gt ,
1125+ move || Some ( ScalarValue :: from ( rhs. load ( Ordering :: Relaxed ) ) ) ,
1126+ PType :: I32 . into ( ) ,
1127+ true ,
1128+ root ( ) ,
1129+ ) ;
1130+
1131+ // Lower once, as the zoned reader does, and evaluate the lowered predicate after each
1132+ // update of the dynamic value.
1133+ let lowered = zone_map. lower_stats ( falsify ( & expr, PType :: I32 . into ( ) ) ) ?;
1134+
1135+ // A > 5 => A.max <= 5
1136+ let mask = zone_map. prune_lowered ( & lowered, & SESSION ) ?;
1137+ assert_arrays_eq ! (
1138+ mask. into_array( ) ,
1139+ BoolArray :: from_iter( [ true , false , false ] ) ,
1140+ ctx
1141+ ) ;
1142+
1143+ // A > 6 => A.max <= 6
1144+ threshold. store ( 6 , Ordering :: Relaxed ) ;
1145+ let mask = zone_map. prune_lowered ( & lowered, & SESSION ) ?;
1146+ assert_arrays_eq ! (
1147+ mask. into_array( ) ,
1148+ BoolArray :: from_iter( [ true , true , false ] ) ,
1149+ ctx
1150+ ) ;
1151+
1152+ Ok ( ( ) )
1153+ }
10791154}
0 commit comments