This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new 1e3338484 GH-3639: Don't drop row groups for IN(..., null) when 
num_nulls is not set (#3640)
1e3338484 is described below

commit 1e3338484eebe41d80d86e63da4428eb91c4d416
Author: Eduard Tudenhoefner <[email protected]>
AuthorDate: Wed Jul 8 15:38:43 2026 +0200

    GH-3639: Don't drop row groups for IN(..., null) when num_nulls is not set 
(#3640)
---
 .../filter2/statisticslevel/StatisticsFilter.java  |  5 +++
 .../statisticslevel/TestStatisticsFilter.java      | 40 ++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git 
a/parquet-hadoop/src/main/java/org/apache/parquet/filter2/statisticslevel/StatisticsFilter.java
 
b/parquet-hadoop/src/main/java/org/apache/parquet/filter2/statisticslevel/StatisticsFilter.java
index 2574c8ea3..d06ea825d 100644
--- 
a/parquet-hadoop/src/main/java/org/apache/parquet/filter2/statisticslevel/StatisticsFilter.java
+++ 
b/parquet-hadoop/src/main/java/org/apache/parquet/filter2/statisticslevel/StatisticsFilter.java
@@ -305,6 +305,11 @@ public class StatisticsFilter implements 
FilterPredicate.Visitor<Boolean> {
       } else {
         if (values.contains(null)) return BLOCK_MIGHT_MATCH;
       }
+    } else if (values.contains(null)) {
+      // the number of nulls is unknown, so this chunk might contain nulls 
that match the null
+      // literal in the IN set. we cannot fall through to the min/max check 
(which only considers
+      // the non-null values) or we might incorrectly drop a chunk containing 
matching null rows.
+      return BLOCK_MIGHT_MATCH;
     }
 
     // If any value in the IN set is NaN, be conservative
diff --git 
a/parquet-hadoop/src/test/java/org/apache/parquet/filter2/statisticslevel/TestStatisticsFilter.java
 
b/parquet-hadoop/src/test/java/org/apache/parquet/filter2/statisticslevel/TestStatisticsFilter.java
index e9756fa82..3cbccb13c 100644
--- 
a/parquet-hadoop/src/test/java/org/apache/parquet/filter2/statisticslevel/TestStatisticsFilter.java
+++ 
b/parquet-hadoop/src/test/java/org/apache/parquet/filter2/statisticslevel/TestStatisticsFilter.java
@@ -37,6 +37,7 @@ import static 
org.apache.parquet.filter2.predicate.FilterApi.or;
 import static org.apache.parquet.filter2.predicate.FilterApi.userDefined;
 import static 
org.apache.parquet.filter2.statisticslevel.StatisticsFilter.canDrop;
 import static org.apache.parquet.io.api.Binary.fromString;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -389,6 +390,45 @@ public class TestStatisticsFilter {
         List.of(getIntColumnMeta(statsSomeNulls, 177L), 
getDoubleColumnMeta(doubleStats, 177L))));
   }
 
+  @Test
+  public void testInWithNullLiteralAndUnsetNumNulls() {
+    // Reproduces the bug where StatisticsFilter drops a row group for IN 
(..., null) when num_nulls
+    // is unset. min/max are present but the number of nulls is unknown, so we 
must not fall through
+    // to the min/max-only check (which only considers the non-null literals) 
and drop a chunk that
+    // may contain matching null rows
+    org.apache.parquet.column.statistics.Statistics<?> statsUnsetNulls =
+        org.apache.parquet.column.statistics.Statistics.getBuilderForReading(
+                Types.required(PrimitiveTypeName.INT32).named("test_int32"))
+            .withMin(BytesUtils.intToBytes(10))
+            .withMax(BytesUtils.intToBytes(100))
+            .build();
+    // min/max are available but num_nulls is not
+    assertThat(statsUnsetNulls.hasNonNullValue()).isTrue();
+    assertThat(statsUnsetNulls.isNumNullsSet()).isFalse();
+
+    List<ColumnChunkMetaData> metas =
+        List.of(getIntColumnMeta(statsUnsetNulls, 177L), 
getDoubleColumnMeta(doubleStats, 177L));
+
+    // IN (200, null) where 200 is outside [10, 100]. The chunk might contain 
null rows matching the
+    // null literal, so it must NOT be dropped
+    Set<Integer> valuesNullAndOutOfRange = new HashSet<>();
+    valuesNullAndOutOfRange.add(null);
+    valuesNullAndOutOfRange.add(200);
+    assertThat(canDrop(in(intColumn, valuesNullAndOutOfRange), 
metas)).isFalse();
+
+    // IN (200) without a null literal can still be dropped based on min/max 
even if num_nulls is
+    // unknown, confirming the fix does not over-broaden pruning.
+    Set<Integer> valuesOutOfRange = new HashSet<>();
+    valuesOutOfRange.add(200);
+    assertThat(canDrop(in(intColumn, valuesOutOfRange), metas)).isTrue();
+
+    // IN (50, null) where 50 is inside [10, 100] must also not be dropped.
+    Set<Integer> valuesNullAndInRange = new HashSet<>();
+    valuesNullAndInRange.add(null);
+    valuesNullAndInRange.add(50);
+    assertThat(canDrop(in(intColumn, valuesNullAndInRange), metas)).isFalse();
+  }
+
   @Test
   public void testContainsEqNonNull() {
     assertTrue(canDrop(contains(eq(intColumn, 9)), columnMetas));

Reply via email to