sunchao commented on code in PR #25602:
URL: https://github.com/apache/datafusion/pull/25602#discussion_r4076638507


##########
datafusion/pruning/src/pruning_predicate.rs:
##########
@@ -1816,6 +1858,41 @@ fn build_predicate_expression(
             return unhandled_hook.handle(expr);
         }
     }
+    if let Some(lookup) = expr.downcast_ref::<HashTableLookupExpr>() {
+        return build_hash_lookup_pruning_expr(lookup, schema, required_columns)
+            .unwrap_or_else(|| unhandled_hook.handle(expr));
+    }
+    // A partitioned hash join hides its per-partition filters under a `CASE` 
on the
+    // repartition hash. A row takes exactly one branch, so a container may 
match
+    // only if some branch may: the branches' disjunction is a sound 
relaxation, and
+    // the `WHEN`s (a hash, which no statistics describe) can be dropped.
+    if let Some(case) = expr.downcast_ref::<phys_expr::CaseExpr>() {
+        // Only a Boolean `CASE` is a predicate; anything else is a value for
+        // whatever compares it to handle.
+        if !matches!(case.data_type(schema), Ok(DataType::Boolean)) {
+            return unhandled_hook.handle(expr);
+        }
+        // A missing `ELSE` yields NULL, which never matches, so it adds 
nothing.
+        return case
+            .when_then_expr()
+            .iter()
+            .map(|(_, then)| then)
+            .chain(case.else_expr())

Review Comment:
   [P1] Preserve the implicit NULL branch during full-match inference
   
   Omitting the missing `ELSE` is conservative for ordinary filtering, but this 
rewriter is also used to build the inverse predicate that proves an entire 
Parquet row group matches. That makes this change return incorrect rows when 
`datafusion.execution.parquet.pushdown_filters=true`.
   
   I reproduced this with required Int64 columns `(a,b)`, two one-row groups 
containing `(2,1)` and `(1,2)`, and:
   
   ```sql
   SELECT * FROM t
   WHERE NOT (CASE WHEN a = 1 THEN false END) AND b > 0
   ORDER BY a;
   ```
   
   Base `714956b3` returns only `(1,2)`; head `adbb3ce4` also returns `(2,1)`, 
although its predicate result is NULL. With `LIMIT 1` instead of `ORDER BY`, 
head returns the invalid `(2,1)`. Disabling filter pushdown restores the 
correct result.
   
   The forward rewrite falls back for `NOT CASE`, so it still permits 
full-match inversion. The inverse exposes `CASE ... OR b <= 0`; dropping the 
implicit NULL branch lets it prune both groups, which are then marked fully 
matched and skip row filtering. Head reports two fully matched groups and 
`row_filter_skipped_fully_matched=1`.
   
   Could we keep CASE expressions without an explicit `ELSE` conservatively 
unhandled here, and add an end-to-end regression test? The join-generated CASE 
already has an explicit ELSE. Merely setting `has_filter_semantics_only` inside 
this branch would miss the forward `NOT CASE` path, which never descends into 
it.



##########
datafusion/physical-plan/src/joins/hash_join/exec.rs:
##########
@@ -3094,19 +3095,41 @@ async fn collect_left_input(
             .iter()
             .map(|arr| arr.get_array_memory_size())
             .sum::<usize>();
-        if left_values.is_empty()
-            || left_values[0].is_empty()
-            || estimated_size > 
config.optimizer.hash_join_inlist_pushdown_max_size
-            || map.num_of_distinct_key()
-                > config
+
+        let pushdown_inlist = !left_values.is_empty()
+            && !left_values[0].is_empty()
+            && estimated_size <= 
config.optimizer.hash_join_inlist_pushdown_max_size
+            && map.num_of_distinct_key()
+                <= config
                     .optimizer
-                    .hash_join_inlist_pushdown_max_distinct_values
+                    .hash_join_inlist_pushdown_max_distinct_values;
+
+        if pushdown_inlist
+            && let Some(in_list_values) = 
build_struct_inlist_values(&left_values)?
         {
-            PushdownStrategy::Map(Arc::clone(&map))
-        } else if let Some(in_list_values) = 
build_struct_inlist_values(&left_values)? {
             PushdownStrategy::InList(in_list_values)
         } else {
-            PushdownStrategy::Map(Arc::clone(&map))
+            // Past the InList threshold use a bucket bitmap for container 
pruning.
+            let pruning_bitmap = match (left_values.as_slice(), 
bounds.as_ref()) {
+                ([keys], Some(bounds)) if !keys.is_empty() => bounds

Review Comment:
   [P2] Skip bitmap construction when dynamic filtering is inactive
   
   `bounds` can exist solely for perfect-hash-join candidacy even when 
`should_compute_dynamic_filters` is false. This branch still constructs and 
reserves a pruning bitmap before those bounds are cleared below, so a join with 
dynamic filtering disabled can now fail for memory that provides no pruning 
benefit.
   
   Using a native `CollectLeft` join with 151 Int64 build keys `i * 10_000` (`i 
= 0..151`), probe keys `[0, 500_000, 1_500_000]`, and 
`enable_join_dynamic_filter_pushdown=false` (otherwise default configuration), 
base `714956b3` succeeds in a 100,000-byte memory pool with 6,220 bytes 
reserved. Head `adbb3ce4` fails with `ResourcesExhausted` requesting another 
131,072 bytes. At a 1,000,000-byte limit both return the exact expected rows, 
but head reserves 137,292 bytes. I also reproduced the same failure with a Full 
join.
   
   Could we gate this bitmap construction on `should_compute_dynamic_filters` 
and cover the disabled-filter case with a memory-limit regression test?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to