This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 8460676f3fc [Fix](nereids) Freeze sortedPartitionRanges in
SelectedPartitions to prevent TOCTOU NPE during partition pruning (#65659)
8460676f3fc is described below
commit 8460676f3fcb6304f27dba32379da7d0fe0ad9d9
Author: Nelson Boss <[email protected]>
AuthorDate: Mon Jul 20 18:58:55 2026 +0800
[Fix](nereids) Freeze sortedPartitionRanges in SelectedPartitions to
prevent TOCTOU NPE during partition pruning (#65659)
### What problem does this PR solve?
Issue Number: #64800
Related PR: #58877
Problem Summary:
Fix a TOCTOU (Time-of-Check Time-of-Use) race condition that causes
`NullPointerException` during partition pruning on external tables.
**Root cause:** In `PruneFileScanPartition.pruneExternalPartitions()`:
1. `nameToPartitionItem` — frozen at T1 inside
`LogicalFileScan.SelectedPartitions` when the plan node is constructed
(via `initSelectedPartitions()`)
2. `sortedPartitionRanges` — re-read from the `HivePartitionValues`
cache at T2 when the pruning rule executes (via
`externalTable.getSortedPartitionRanges()`)
If the cache is refreshed between T1 and T2 (e.g. concurrent `ALTER
TABLE ADD/DROP PARTITION`), the two snapshots diverge.
`binarySearchFiltering` uses the new snapshot to decide which partitions
match the predicate, but the caller looks them up in the old snapshot:
```java
for (String name : prunedPartitions) {
selectedPartitionItems.put(name, nameToPartitionItem.get(name));
// nameToPartitionItem.get(name) returns null for partitions that were
added after T1
}
// => ImmutableMap.copyOf() throws NPE: "null value in entry:
dt=2026-06-22=null"
```
**Concrete example:** A Hive table has 3 partitions
`dt=2026-06-20/21/23`. Session A runs `SELECT * FROM t WHERE
dt='2026-06-22'`:
```
T1 BindRelation: LogicalFileScan freezes nameToPartitionItem from cache
→ {2026-06-20, 2026-06-21, 2026-06-23} (no 2026-06-22)
[Session B runs ALTER TABLE ADD PARTITION (dt='2026-06-22')]
[cache is refreshed → now has 4 partitions including 2026-06-22]
T2 PruneFileScanPartition: re-reads sortedPartitionRanges from cache
→ {2026-06-20, 2026-06-21, 2026-06-22, 2026-06-23} (new snapshot)
binarySearchFiltering matches dt=2026-06-22 → returns "dt=2026-06-22"
nameToPartitionItem.get("dt=2026-06-22") → null (old snapshot has no
such key)
→ NPE: "null value in entry: dt=2026-06-22=null"
```
**Fix:** freeze both views from a single snapshot so T2 never re-reads
the cache.
- `SelectedPartitions` now carries an `Optional<SortedPartitionRanges>`
field.
- `HMSExternalTable.initSelectedPartitions` reads the cached
`HivePartitionValues` once and freezes both the partition map and the
cached sorted ranges together (reuses the cache, no just-in-time
rebuild).
- Hudi has no cached ranges, so `PruneFileScanPartition` builds them
lazily from the frozen map only when binary search filtering is enabled.
- A missing partition in the lookup loop is now an invariant failure
(`Preconditions.checkState`) instead of being silently skipped, which
previously produced a partial scan over fewer partitions.
### Release note
Fix `NullPointerException` in partition pruning when external table
partitions are modified concurrently during query optimization (TOCTOU
race in binary search partition filtering).
---
.../doris/datasource/hive/HMSExternalTable.java | 24 ++
.../rules/rewrite/PruneFileScanPartition.java | 14 +-
.../trees/plans/logical/LogicalFileScan.java | 29 ++-
.../BinarySearchPartitionInconsistencyTest.java | 255 +++++++++++++++++++++
4 files changed, 315 insertions(+), 7 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalTable.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalTable.java
index e1311237a60..9e2d1ad22e2 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalTable.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/hive/HMSExternalTable.java
@@ -464,6 +464,28 @@ public class HMSExternalTable extends ExternalTable
implements MTMVRelatedTableI
return hivePartitionValues.getSortedPartitionRanges();
}
+ @Override
+ public SelectedPartitions initSelectedPartitions(Optional<MvccSnapshot>
snapshot) {
+ // For Hive, read the cached HivePartitionValues once and freeze both
the partition
+ // map and the cached sortedPartitionRanges from the same snapshot.
This reuses the
+ // cached sorted ranges (no just-in-time rebuild) and keeps the two
views consistent,
+ // avoiding the TOCTOU divergence where sortedPartitionRanges was
re-read from cache
+ // at pruning time while the partition map was frozen earlier.
+ if (getDlaType() != DLAType.HIVE) {
+ return super.initSelectedPartitions(snapshot);
+ }
+ if (CollectionUtils.isEmpty(this.getPartitionColumns())) {
+ return SelectedPartitions.NOT_PRUNED;
+ }
+ HiveExternalMetaCache.HivePartitionValues hivePartitionValues =
getHivePartitionValues(
+ MvccUtil.getSnapshotFromContext(this));
+ Map<String, PartitionItem> nameToPartitionItems =
hivePartitionValues.getNameToPartitionItem();
+ Optional<SortedPartitionRanges<String>> sortedPartitionRanges
+ = hivePartitionValues.getSortedPartitionRanges();
+ return new SelectedPartitions(nameToPartitionItems.size(),
nameToPartitionItems, false, false,
+ sortedPartitionRanges);
+ }
+
public SelectedPartitions
initHudiSelectedPartitions(Optional<TableSnapshot> tableSnapshot) {
if (getDlaType() != DLAType.HUDI) {
return SelectedPartitions.NOT_PRUNED;
@@ -482,6 +504,8 @@ public class HMSExternalTable extends ExternalTable
implements MTMVRelatedTableI
nameToPartitionItems.put(idToNameMap.get(entry.getKey()),
entry.getValue());
}
+ // Hudi has no cached sorted ranges; leave it empty here and let
PruneFileScanPartition
+ // build it lazily from this frozen map only when binary search
filtering is enabled.
return new SelectedPartitions(nameToPartitionItems.size(),
nameToPartitionItems, false);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneFileScanPartition.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneFileScanPartition.java
index f3822215e8c..83db60b098a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneFileScanPartition.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PruneFileScanPartition.java
@@ -31,6 +31,7 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan;
import
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan.SelectedPartitions;
import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.apache.commons.collections4.CollectionUtils;
@@ -94,8 +95,9 @@ public class PruneFileScanPartition extends
OneRewriteRuleFactory {
Optional<SortedPartitionRanges<String>> sortedPartitionRanges =
Optional.empty();
boolean enableBinarySearch = ctx.getConnectContext() == null
||
ctx.getConnectContext().getSessionVariable().enableBinarySearchFilteringPartitions;
- if (enableBinarySearch) {
- sortedPartitionRanges = (Optional)
externalTable.getSortedPartitionRanges(scan);
+ if (enableBinarySearch && !nameToPartitionItem.isEmpty()) {
+ sortedPartitionRanges =
scan.getSelectedPartitions().sortedPartitionRanges
+ .or(() ->
Optional.ofNullable(SortedPartitionRanges.build(nameToPartitionItem)));
}
PartitionPruneResult<String> result = PartitionPruner.pruneWithResult(
partitionSlots, filter.getPredicate(), nameToPartitionItem,
ctx,
@@ -103,7 +105,13 @@ public class PruneFileScanPartition extends
OneRewriteRuleFactory {
List<String> prunedPartitions = new ArrayList<>(result.partitions);
for (String name : prunedPartitions) {
- selectedPartitionItems.put(name, nameToPartitionItem.get(name));
+ PartitionItem item = nameToPartitionItem.get(name);
+ // Both nameToPartitionItem and sortedPartitionRanges now come
from the same frozen
+ // snapshot, so a missing item is an invariant violation rather
than a partition to
+ // skip. Failing here surfaces the bug instead of silently
returning a partial scan.
+ Preconditions.checkState(item != null,
+ "pruned partition %s is missing in the selected partitions
snapshot", name);
+ selectedPartitionItems.put(name, item);
}
return new SelectedPartitions(nameToPartitionItem.size(),
selectedPartitionItems, true,
result.hasPartitionPredicate);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
index e70933b5b64..29e2f84feac 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileScan.java
@@ -28,6 +28,7 @@ import
org.apache.doris.datasource.iceberg.IcebergSysExternalTable;
import org.apache.doris.datasource.mvcc.MvccUtil;
import org.apache.doris.nereids.memo.GroupExpression;
import org.apache.doris.nereids.properties.LogicalProperties;
+import org.apache.doris.nereids.rules.expression.rules.SortedPartitionRanges;
import org.apache.doris.nereids.trees.TableSample;
import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
@@ -273,7 +274,8 @@ public class LogicalFileScan extends LogicalCatalogRelation
implements SupportPr
// NOT_PRUNED means the Nereids planner does not handle the partition
pruning.
// This can be treated as the initial value of SelectedPartitions.
// Or used to indicate that the partition pruning is not processed.
- public static SelectedPartitions NOT_PRUNED = new
SelectedPartitions(0, ImmutableMap.of(), false, false);
+ public static SelectedPartitions NOT_PRUNED = new
SelectedPartitions(0, ImmutableMap.of(), false, false,
+ Optional.empty());
/**
* total partition number
*/
@@ -293,12 +295,19 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
*/
public final boolean hasPartitionPredicate;
+ /**
+ * sorted partition ranges for binary search filtering.
+ * Frozen at construction time to ensure consistency with
selectedPartitions.
+ * Empty if binary search is not applicable (e.g., default partition
only).
+ */
+ public final Optional<SortedPartitionRanges<String>>
sortedPartitionRanges;
+
/**
* Constructor for SelectedPartitions.
*/
public SelectedPartitions(long totalPartitionNum, Map<String,
PartitionItem> selectedPartitions,
boolean isPruned) {
- this(totalPartitionNum, selectedPartitions, isPruned, false);
+ this(totalPartitionNum, selectedPartitions, isPruned, false,
Optional.empty());
}
/**
@@ -306,11 +315,21 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
*/
public SelectedPartitions(long totalPartitionNum, Map<String,
PartitionItem> selectedPartitions,
boolean isPruned, boolean hasPartitionPredicate) {
+ this(totalPartitionNum, selectedPartitions, isPruned,
hasPartitionPredicate, Optional.empty());
+ }
+
+ /**
+ * Constructor for SelectedPartitions with sorted partition ranges.
+ */
+ public SelectedPartitions(long totalPartitionNum, Map<String,
PartitionItem> selectedPartitions,
+ boolean isPruned, boolean hasPartitionPredicate,
+ Optional<SortedPartitionRanges<String>> sortedPartitionRanges)
{
this.totalPartitionNum = totalPartitionNum;
this.selectedPartitions =
ImmutableMap.copyOf(Objects.requireNonNull(selectedPartitions,
"selectedPartitions is null"));
this.isPruned = isPruned;
this.hasPartitionPredicate = hasPartitionPredicate;
+ this.sortedPartitionRanges = sortedPartitionRanges;
}
@Override
@@ -325,12 +344,14 @@ public class LogicalFileScan extends
LogicalCatalogRelation implements SupportPr
return isPruned == that.isPruned
&& hasPartitionPredicate == that.hasPartitionPredicate
&& Objects.equals(
- selectedPartitions.keySet(),
that.selectedPartitions.keySet());
+ selectedPartitions.keySet(),
that.selectedPartitions.keySet())
+ && Objects.equals(
+ sortedPartitionRanges.isPresent(),
that.sortedPartitionRanges.isPresent());
}
@Override
public int hashCode() {
- return Objects.hash(selectedPartitions, isPruned,
hasPartitionPredicate);
+ return Objects.hash(selectedPartitions, isPruned,
hasPartitionPredicate, sortedPartitionRanges.isPresent());
}
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/BinarySearchPartitionInconsistencyTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/BinarySearchPartitionInconsistencyTest.java
new file mode 100644
index 00000000000..db3d46489f3
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/BinarySearchPartitionInconsistencyTest.java
@@ -0,0 +1,255 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.expression.rules;
+
+import org.apache.doris.analysis.PartitionValue;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.ListPartitionItem;
+import org.apache.doris.catalog.PartitionItem;
+import org.apache.doris.catalog.PartitionKey;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.nereids.CascadesContext;
+import
org.apache.doris.nereids.rules.expression.rules.PartitionPruner.PartitionPruneResult;
+import
org.apache.doris.nereids.rules.expression.rules.PartitionPruner.PartitionTableType;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import
org.apache.doris.nereids.trees.plans.logical.LogicalFileScan.SelectedPartitions;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.utframe.TestWithFeService;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Verify fix for the TOCTOU bug in {@link SelectedPartitions}.
+ *
+ * <p>Before the fix: {@code nameToPartitionItem} was frozen in {@code
SelectedPartitions}
+ * at T1 but {@code sortedPartitionRanges} was re-read from cache at T2. If
the cache
+ * changed between T1 and T2 (concurrent ADD/DROP PARTITION), the two
snapshots diverged,
+ * binary search returned partitions not present in {@code
nameToPartitionItem}, and
+ * {@code ImmutableMap.copyOf()} threw NPE on the null value.
+ *
+ * <p>After the fix: {@code sortedPartitionRanges} is built from the same
+ * {@code nameToPartitionItems} and frozen inside {@code SelectedPartitions}
at T1.
+ * The pruning rule reads both from the frozen snapshot so they are always
consistent.
+ */
+public class BinarySearchPartitionInconsistencyTest extends TestWithFeService {
+ private final Column partitionColumn = new Column("a", PrimitiveType.INT);
+ private final SlotReference slotA = new SlotReference("a",
IntegerType.INSTANCE);
+ private CascadesContext cascadesContext;
+
+ @Override
+ protected void runBeforeAll() throws Exception {
+ cascadesContext = createCascadesContext("select * from t1");
+ }
+
+ private ListPartitionItem listItem(int value) throws AnalysisException {
+ PartitionValue partitionValue = new
PartitionValue(String.valueOf(value));
+ PartitionKey partitionKey = PartitionKey.createPartitionKey(
+ ImmutableList.of(partitionValue),
ImmutableList.of(partitionColumn));
+ return new ListPartitionItem(ImmutableList.of(partitionKey));
+ }
+
+ /**
+ * Test that SelectedPartitions correctly freezes sortedPartitionRanges
+ * from the same partition items map.
+ */
+ @Test
+ public void testSelectedPartitionsFreezesSortedPartitionRanges() throws
AnalysisException {
+ Map<String, PartitionItem> nameToPartitionItems =
Maps.newHashMapWithExpectedSize(3);
+ nameToPartitionItems.put("p1", listItem(1));
+ nameToPartitionItems.put("p2", listItem(2));
+ nameToPartitionItems.put("p3", listItem(3));
+
+ Optional<SortedPartitionRanges<String>> sortedRanges =
Optional.ofNullable(
+ SortedPartitionRanges.build(nameToPartitionItems));
+
+ SelectedPartitions sp = new SelectedPartitions(
+ nameToPartitionItems.size(), nameToPartitionItems, false,
false, sortedRanges);
+
+ // sortedPartitionRanges field is populated
+ Assertions.assertTrue(sp.sortedPartitionRanges.isPresent(),
+ "sortedPartitionRanges should be present when partitions
exist");
+ Assertions.assertNotNull(sp.sortedPartitionRanges.get(),
+ "sortedPartitionRanges should not be null");
+ }
+
+ /**
+ * Test that NOT_PRUNED has empty sortedPartitionRanges.
+ */
+ @Test
+ public void testNotPrunedHasEmptySortedPartitionRanges() {
+
Assertions.assertFalse(SelectedPartitions.NOT_PRUNED.sortedPartitionRanges.isPresent(),
+ "NOT_PRUNED should have empty sortedPartitionRanges");
+ Assertions.assertEquals(Optional.empty(),
SelectedPartitions.NOT_PRUNED.sortedPartitionRanges,
+ "NOT_PRUNED.sortedPartitionRanges should be Optional.empty()");
+ }
+
+ /**
+ * Test consistent snapshot: when both nameToPartitionItem and
sortedPartitionRanges
+ * come from the same data, no partition returned by binary search is
missing
+ * from the map. Simulates the PruneFileScanPartition flow after the fix.
+ */
+ @Test
+ public void testConsistentSnapshotNoMissingPartitions() throws
AnalysisException {
+ // One consistent snapshot built at T1
+ Map<String, PartitionItem> partitionItems =
Maps.newHashMapWithExpectedSize(4);
+ partitionItems.put("p1", listItem(1));
+ partitionItems.put("p2", listItem(2));
+ partitionItems.put("p3", listItem(3));
+ partitionItems.put("p4", listItem(4));
+ partitionItems = ImmutableMap.copyOf(partitionItems);
+
+ SortedPartitionRanges<String> sortedRanges =
SortedPartitionRanges.build(partitionItems);
+ Assertions.assertNotNull(sortedRanges);
+
+ // predicate hits p4, which DOES exist in both snapshots
+ Expression predicate = new EqualTo(slotA, Literal.of(4));
+
+ PartitionPruneResult<String> result = PartitionPruner.pruneWithResult(
+ ImmutableList.of(slotA), predicate, partitionItems,
cascadesContext,
+ PartitionTableType.EXTERNAL, Optional.of(sortedRanges));
+
+ // p4 should be in the result since it exists in the map
+ Assertions.assertTrue(result.partitions.contains("p4"),
+ "p4 should be returned when it exists in the consistent
snapshot");
+
+ // Simulate the PruneFileScanPartition lookup loop. Since both the
partition map
+ // and sortedPartitionRanges come from the same frozen snapshot, every
returned
+ // partition must be present in the map — assert the invariant rather
than skip.
+ Map<String, PartitionItem> selectedPartitionItems = Maps.newHashMap();
+ for (String name : result.partitions) {
+ PartitionItem item = partitionItems.get(name);
+ Assertions.assertNotNull(item,
+ "pruned partition " + name + " must be present in the
consistent snapshot");
+ selectedPartitionItems.put(name, item);
+ }
+
+ // All returned partitions are in the map, none skipped
+ Assertions.assertEquals(result.partitions.size(),
selectedPartitionItems.size(),
+ "all returned partitions should be present in the consistent
snapshot");
+ Assertions.assertNotNull(selectedPartitionItems.get("p4"),
+ "p4 PartitionItem should not be null");
+ }
+
+ /**
+ * Test: if inconsistent snapshots ever reach the lookup loop (which the
fix makes
+ * impossible by freezing both from the same snapshot), the invariant
check fails
+ * loudly instead of silently returning a partial scan over fewer
partitions.
+ *
+ * <p>This mirrors the {@code Preconditions.checkState} in
PruneFileScanPartition:
+ * a missing partition is treated as an invariant violation, not a
partition to skip.
+ */
+ @Test
+ public void testInvariantViolationFailsLoudly() throws AnalysisException {
+ // old snapshot (no p4)
+ Map<String, PartitionItem> nameToPartitionItem = ImmutableMap.of(
+ "p1", listItem(1),
+ "p2", listItem(2),
+ "p3", listItem(3));
+
+ // new snapshot (has p4) — simulating inconsistent data that the fix
prevents,
+ // but the invariant check must still surface it rather than silently
skip p4
+ Map<String, PartitionItem> newPartitions =
Maps.newHashMapWithExpectedSize(4);
+ newPartitions.put("p1", listItem(1));
+ newPartitions.put("p2", listItem(2));
+ newPartitions.put("p3", listItem(3));
+ newPartitions.put("p4", listItem(4));
+ SortedPartitionRanges<String> sortedPartitionRanges =
SortedPartitionRanges.build(newPartitions);
+ Assertions.assertNotNull(sortedPartitionRanges);
+
+ Expression predicate = new EqualTo(slotA, Literal.of(4));
+
+ PartitionPruneResult<String> result = PartitionPruner.pruneWithResult(
+ ImmutableList.of(slotA), predicate, nameToPartitionItem,
cascadesContext,
+ PartitionTableType.EXTERNAL,
Optional.of(sortedPartitionRanges));
+
+ // binary search returns p4 from the new snapshot
+ Assertions.assertTrue(result.partitions.contains("p4"),
+ "binary search returns p4 from the sortedPartitionRanges");
+
+ // The invariant check (mirroring PruneFileScanPartition) must fail
loudly:
+ // p4 is in the pruned result but missing from nameToPartitionItem.
+ Assertions.assertThrows(IllegalStateException.class, () -> {
+ for (String name : result.partitions) {
+ PartitionItem item = nameToPartitionItem.get(name);
+ if (item == null) {
+ throw new IllegalStateException(
+ "pruned partition " + name + " is missing in the
selected partitions snapshot");
+ }
+ }
+ }, "a missing partition must fail the invariant instead of being
silently skipped");
+ }
+
+ /**
+ * Test that with binary search disabled, sequentialFiltering is
unaffected.
+ */
+ @Test
+ public void testSequentialFilteringNotAffectedByInconsistentSnapshot()
throws AnalysisException {
+ Map<String, PartitionItem> nameToPartitionItem =
Maps.newHashMapWithExpectedSize(3);
+ nameToPartitionItem.put("p1", listItem(1));
+ nameToPartitionItem.put("p2", listItem(2));
+ nameToPartitionItem.put("p3", listItem(3));
+ nameToPartitionItem = ImmutableMap.copyOf(nameToPartitionItem);
+
+ Expression predicate = new EqualTo(slotA, Literal.of(4));
+
+ // binary search disabled -> sequentialFiltering path
+ PartitionPruneResult<String> result = PartitionPruner.pruneWithResult(
+ ImmutableList.of(slotA), predicate, nameToPartitionItem,
cascadesContext,
+ PartitionTableType.EXTERNAL, Optional.empty());
+
+ Assertions.assertFalse(result.partitions.contains("p4"),
+ "sequential filtering only uses nameToPartitionItem, p4 is
never returned");
+ Assertions.assertTrue(result.partitions.isEmpty(),
+ "no partition matches a=4 in this snapshot");
+ }
+
+ /**
+ * Test that SelectedPartitions factory method produces consistent state.
+ */
+ @Test
+ public void testNewSelectedPartitionsHasConsistentSortedRanges() throws
AnalysisException {
+ Map<String, PartitionItem> items = Maps.newHashMapWithExpectedSize(2);
+ items.put("p1", listItem(1));
+ items.put("p2", listItem(2));
+
+ Optional<SortedPartitionRanges<String>> ranges = Optional.ofNullable(
+ SortedPartitionRanges.build(items));
+
+ // The 5-arg constructor freezes both fields from the same snapshot
+ SelectedPartitions sp = new SelectedPartitions(items.size(), items,
false, false, ranges);
+
+ Assertions.assertTrue(sp.sortedPartitionRanges.isPresent());
+ SortedPartitionRanges<String> frozen = sp.sortedPartitionRanges.get();
+
+ // The frozen sortedPartitionRanges contains exactly the same
partitions as selectedPartitions
+ int partitionCountInSortedRanges = frozen.sortedPartitions.size() +
frozen.defaultPartitions.size();
+ Assertions.assertEquals(sp.selectedPartitions.size(),
partitionCountInSortedRanges,
+ "sortedPartitionRanges should cover all partitions in
selectedPartitions");
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]