@@ -1214,9 +1214,67 @@ async fn test_hashjoin_dynamic_filter_pushdown_partitioned() {
12141214 ) ;
12151215}
12161216
1217- /// Test that simulates behavior when some build side partitions are much slower to finish than others.
1218- /// In these cases we push down _partial_ filters that only apply scan filters to rows the belong to build side partitions that have completed.
1219- /// Once all build side partitions complete the full filter is pushed down without any hash key partition calculations.
1217+ /// Test demonstrating progressive dynamic filter evolution in partitioned hash joins
1218+ ///
1219+ /// This test validates that instead of waiting for all
1220+ /// build-side partitions to complete before applying any filters, we apply partial filters
1221+ /// immediately as each partition completes.
1222+ /// To be able to evaluate partial filters we need to know which partition each probe row belongs to,
1223+ /// so we push down a hash function to the probe side that computes the same hash as repartitioning will later on.
1224+ ///
1225+ /// ## Test Scenario Setup
1226+ ///
1227+ /// - **Build side**: Values [1, 2, 3, 4] distributed across 3 hash partitions
1228+ /// - **Probe side**: Values [2, 3] that need to be filtered
1229+ /// - **Partition 1 is artificially slowed**: Simulates real-world partition skew
1230+ ///
1231+ /// ## Progressive Filter Evolution Demonstration
1232+ ///
1233+ /// The test shows how the dynamic filter evolves through three distinct phases:
1234+ ///
1235+ /// ### Phase 1: Initial State (All Partitions Building)
1236+ /// ```sql
1237+ /// -- No filter applied yet
1238+ /// predicate=DynamicFilterPhysicalExpr [ true ]
1239+ /// ```
1240+ /// → All probe-side data passes through unfiltered
1241+ ///
1242+ /// ### Phase 2: Progressive Filtering (Some Partitions Complete)
1243+ /// ```sql
1244+ /// -- Hash-based progressive filter after partition 0 completes
1245+ /// predicate=DynamicFilterPhysicalExpr [
1246+ /// CASE repartition_hash(id@0) % 3
1247+ /// WHEN 0 THEN id@0 >= 3 AND id@0 <= 3 -- Only partition 0 bounds known
1248+ /// ELSE true -- Pass through partitions 1,2 data
1249+ /// END
1250+ /// ]
1251+ /// ```
1252+ /// → Filters probe data for partition 0, passes through everything else safely
1253+ ///
1254+ /// ### Phase 3: Final Optimization (All Partitions Complete)
1255+ /// ```sql
1256+ /// -- Optimized bounds-only filter
1257+ /// predicate=DynamicFilterPhysicalExpr [
1258+ /// id@0 >= 3 AND id@0 <= 3 OR -- Partition 0 bounds
1259+ /// id@0 >= 2 AND id@0 <= 2 OR -- Partition 1 bounds
1260+ /// id@0 >= 1 AND id@0 <= 4 -- Partition 2 bounds
1261+ /// ]
1262+ /// ```
1263+ /// → Bounds filter with no hash computation overhead
1264+ ///
1265+ /// ## Correctness Validation
1266+ ///
1267+ /// The test verifies:
1268+ /// 1. **No False Negatives**: All valid join results [2,3] are preserved throughout
1269+ /// 2. **Progressive Improvement**: Filter selectivity increases as partitions complete
1270+ /// 3. **Final Optimization**: Hash-based expressions are removed when all partitions finish
1271+ /// 4. **Partition Isolation**: Each partition's filter only affects its own hash bucket
1272+ ///
1273+ /// ## Real-World Impact
1274+ ///
1275+ /// This optimization addresses common production scenarios where:
1276+ /// - Some partitions finish much faster than others (data skew)
1277+ /// - Waiting for large build sides before starting the probe sides increases latency
12201278#[ tokio:: test]
12211279#[ cfg( not( feature = "force_hash_collisions" ) ) ] // this test relies on hash partitioning to separate rows
12221280async fn test_hashjoin_progressive_filter_reporting ( ) {
@@ -1295,7 +1353,8 @@ async fn test_hashjoin_progressive_filter_reporting() {
12951353 . unwrap ( ) ,
12961354 ) as Arc < dyn ExecutionPlan > ;
12971355
1298- // expect the predicate to be pushed down into the probe side DataSource
1356+ // Verify the initial optimization - should show DynamicFilterPhysicalExpr is set up
1357+ // but not yet populated with any bounds (shows as "true" initially)
12991358 insta:: assert_snapshot!(
13001359 OptimizationTest :: new( Arc :: clone( & plan) , FilterPushdown :: new_post_optimization( ) , true ) ,
13011360 @r"
@@ -1347,7 +1406,13 @@ async fn test_hashjoin_progressive_filter_reporting() {
13471406 }
13481407 }
13491408
1350- // Now check what our filter looks like
1409+ // CRITICAL VALIDATION: This snapshot shows the progressive filter in action!
1410+ // After partition 0 completes (but partition 1 is still blocked), we see:
1411+ // - CASE repartition_hash(id@0) % 3 WHEN 0 THEN id@0 >= 3 AND id@0 <= 3 ELSE true END
1412+ // This means:
1413+ // - For rows that hash to partition 0: Apply bounds check (id >= 3 AND id <= 3)
1414+ // - For rows that hash to partitions 1,2: Pass everything through (ELSE true)
1415+ // This is the core of progressive filtering - partial filtering without false negatives!
13511416 #[ cfg( not( feature = "force_hash_collisions" ) ) ]
13521417 insta:: assert_snapshot!(
13531418 format!( "{}" , format_plan_for_test( & plan) ) ,
@@ -1385,7 +1450,11 @@ async fn test_hashjoin_progressive_filter_reporting() {
13851450 batches. push ( batch. unwrap ( ) ) ;
13861451 }
13871452
1388- // Look at the final plan
1453+ // FINAL OPTIMIZATION VALIDATION: All partitions complete - filter is now optimized!
1454+ // The hash-based CASE expression has been replaced with a simple OR of bounds:
1455+ // - id@0 >= 3 AND id@0 <= 3 OR id@0 >= 2 AND id@0 <= 2 OR id@0 >= 1 AND id@0 <= 4
1456+ // This is much more efficient - no hash computation needed, just bounds checks.
1457+ // Each OR clause represents one partition's bounds: [3,3], [2,2], [1,4]
13891458 insta:: assert_snapshot!(
13901459 format!( "{}" , format_plan_for_test( & plan) ) ,
13911460 @r"
0 commit comments