This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 3341ad74a32 branch-4.2: [fix](partition) Preserve lexicographic range
partition boundaries #67888 (#70056)
3341ad74a32 is described below
commit 3341ad74a32ef31cc2b2aea95df1d8336429ebf8
Author: morrySnow <[email protected]>
AuthorDate: Wed Nov 4 08:17:07 2026 +0800
branch-4.2: [fix](partition) Preserve lexicographic range partition
boundaries #67888 (#70056)
picked from master #67888
---
.../rules/OneRangePartitionEvaluator.java | 341 ++++++++++++++++-----
.../nereids/rules/rewrite/PartitionPrunerTest.java | 198 ++++++++++++
.../test_lexicographic_range_partition.out | 21 ++
.../test_lexicographic_range_partition.groovy | 93 ++++++
4 files changed, 584 insertions(+), 69 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OneRangePartitionEvaluator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OneRangePartitionEvaluator.java
index 2a58b4d7c73..21d309ab6e4 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OneRangePartitionEvaluator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OneRangePartitionEvaluator.java
@@ -23,7 +23,6 @@ import org.apache.doris.catalog.RangePartitionItem;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.Pair;
import org.apache.doris.nereids.CascadesContext;
-import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
import
org.apache.doris.nereids.rules.expression.rules.OneRangePartitionEvaluator.EvaluateRangeInput;
import
org.apache.doris.nereids.rules.expression.rules.OneRangePartitionEvaluator.EvaluateRangeResult;
@@ -52,9 +51,9 @@ import
org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.BooleanType;
import org.apache.doris.nereids.util.ExpressionUtils;
import org.apache.doris.nereids.util.Utils;
+import org.apache.doris.qe.SessionVariable;
import com.google.common.base.Preconditions;
-import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
@@ -93,8 +92,26 @@ public class OneRangePartitionEvaluator<K>
// whether the Expression in partition range may be null.
private final Map<Expression, Boolean> partitionSlotContainsNull;
private final Map<Slot, PartitionSlotType> slotToType;
+ // An impossible expander/evaluator state makes pruning unsafe. Production
keeps the partition;
+ // fe_debug throws at the point where the broken invariant is observed.
+ private boolean disablePruning;
- /** OneRangePartitionEvaluator */
+ /**
+ * Create an evaluator for one RANGE partition.
+ *
+ * <p>The constructor converts the tuple endpoints to Nereids literals,
classifies every
+ * partition coordinate as {@code CONST}, {@code RANGE}, or {@code OTHER},
and expands the
+ * enumerable prefix up to {@code expandThreshold}. It also records
whether each coordinate can
+ * contain NULL so expression evaluation can fold null-sensitive
predicates safely. An unknown
+ * coordinate type violates the expander/evaluator contract: fe_debug
reports it immediately,
+ * while production marks this evaluator as non-prunable.
+ *
+ * @param partitionIdent identifier returned when this partition must be
scanned
+ * @param partitionSlots partition-key slots in lexicographic order
+ * @param partitionItem inclusive-lower/exclusive-upper RANGE partition
definition
+ * @param cascadesContext planner context used by expression rewrite rules
+ * @param expandThreshold maximum number of enumerable values expanded
from the partition range
+ */
public OneRangePartitionEvaluator(K partitionIdent, List<Slot>
partitionSlots,
RangePartitionItem partitionItem, CascadesContext cascadesContext,
int expandThreshold) {
this.partitionIdent = partitionIdent;
@@ -140,7 +157,10 @@ public class OneRangePartitionEvaluator<K>
maybeNull = true;
break;
default:
- throw new AnalysisException("Unknown partition slot
type: " + partitionSlotType);
+ disablePruningOnInvalidState(
+ "Unknown partition slot type while deriving
nullability: " + partitionSlotType);
+ maybeNull = true;
+ break;
}
partitionSlotContainsNull.put(slot, maybeNull);
}
@@ -171,12 +191,28 @@ public class OneRangePartitionEvaluator<K>
}
}
+ /**
+ * Evaluate a partition predicate under one projected input row.
+ *
+ * <p>The projected ranges seed expression evaluation and are then
narrowed by the predicate.
+ * If input construction or range refinement discovers an invalid internal
state, returning any
+ * folded result could incorrectly remove data. In production this method
therefore returns the
+ * original predicate, which {@link PartitionPruner} treats as unknown and
keeps the partition.
+ * In fe_debug the invalid state is thrown before this fallback is reached.
+ *
+ * @param expression partition predicate to simplify
+ * @param currentInputs replacement expression and projected range for
every partition slot
+ * @return the simplified predicate, or {@code expression} when pruning
has been disabled
+ */
@Override
public Expression evaluate(Expression expression, Map<Slot,
PartitionSlotInput> currentInputs) {
+ if (disablePruning) {
+ return expression;
+ }
Map<Expression, ColumnRange> defaultColumnRanges =
currentInputs.values().iterator().next().columnRanges;
Map<Expression, ColumnRange> rangeMap = new
HashMap<>(defaultColumnRanges);
EvaluateRangeResult result = expression.accept(this, new
EvaluateRangeInput(currentInputs, rangeMap));
- return result.result;
+ return disablePruning ? expression : result.result;
}
@Override
@@ -408,6 +444,19 @@ public class OneRangePartitionEvaluator<K>
return result;
}
+ /**
+ * Evaluate a conjunction and refine the independent column ranges with
the tuple boundary.
+ *
+ * <p>The normal child evaluation first intersects the ranges contributed
by every conjunct.
+ * A composite RANGE partition needs one additional pass: a suffix column
is constrained by a
+ * lower or upper endpoint only while every preceding coordinate is still
equal to that endpoint.
+ * Applying this pass after merging the children also recovers prefix
coordinates whose equality
+ * expression was folded to a literal and therefore disappeared from the
child range map.
+ *
+ * @param and conjunction being evaluated
+ * @param context replacement values and projected ranges for the current
expanded partition input
+ * @return the folded conjunction together with its lexicographically
refined column ranges
+ */
@Override
public EvaluateRangeResult visitAnd(And and, EvaluateRangeInput context) {
EvaluateRangeResult result = evaluateChildrenThenThis(and, context);
@@ -425,8 +474,8 @@ public class OneRangePartitionEvaluator<K>
}
// shrink range and prune the other type: if previous column is
literal and equals to the bound
- andResult = determinateRangeOfOtherType(andResult, lowers, true);
- andResult = determinateRangeOfOtherType(andResult, uppers, false);
+ andResult = determinateRangeOfOtherType(andResult, lowers, true,
context.rangeMap);
+ andResult = determinateRangeOfOtherType(andResult, uppers, false,
context.rangeMap);
return andResult;
}
@@ -527,60 +576,83 @@ public class OneRangePartitionEvaluator<K>
}
}
+ /**
+ * Refine the first unresolved suffix column against one lexicographic
partition endpoint.
+ *
+ * <p>Partition slot types have the shape {@code CONST*, RANGE, OTHER*}.
The scan proceeds from
+ * left to right and keeps an endpoint active only while the observed
singleton values equal its
+ * prefix. For the first unresolved {@code OTHER} coordinate, the active
lower endpoint contributes
+ * {@code >= bound}; an active upper endpoint contributes {@code <=
bound}, except that the final
+ * partition column uses {@code < bound} because RANGE partitions are
upper-exclusive. The method
+ * returns immediately after that coordinate because once it may differ
from the endpoint, later
+ * coordinates are lexicographically unconstrained.
+ *
+ * <p>Constant folding can remove an expanded {@code RANGE} slot from
{@code context.columnRanges}.
+ * For that slot only, the method reads the singleton from {@code
defaultColumnRanges} so the
+ * omitted equal-prefix coordinate can still participate in prefix
comparison. A missing
+ * {@code OTHER} slot is different: its default range describes only the
partition boundary, not
+ * the predicate. Adding it to the predicate result would let {@code NOT}
complement a synthetic
+ * range and could incorrectly prune the partition, so a missing {@code
OTHER} stops refinement.
+ *
+ * @param context result of merging all conjunct ranges
+ * @param partitionBound lower or upper endpoint of the composite partition
+ * @param isLowerBound whether {@code partitionBound} is the inclusive
lower endpoint
+ * @param defaultColumnRanges projected ranges for the current expanded
partition input
+ * @return {@code context} refined at the first decisive suffix
coordinate, or an equivalent
+ * false result if the refined range is empty
+ */
private EvaluateRangeResult determinateRangeOfOtherType(
- EvaluateRangeResult context, List<Literal> partitionBound, boolean
isLowerBound) {
+ EvaluateRangeResult context, List<Literal> partitionBound, boolean
isLowerBound,
+ Map<Expression, ColumnRange> defaultColumnRanges) {
if (context.result instanceof Literal) {
return context;
}
- Slot qualifiedSlot = null;
- ColumnRange qualifiedRange = null;
+ LexicographicBoundState boundState = new LexicographicBoundState(
+ partitionBound, isLowerBound, partitionSlots.size());
for (int i = 0; i < partitionSlotTypes.size(); i++) {
PartitionSlotType partitionSlotType = partitionSlotTypes.get(i);
Slot slot = partitionSlots.get(i);
- if (!context.columnRanges.containsKey(slot)) {
- return context;
- }
+ ColumnRange columnRange = context.columnRanges.get(slot);
switch (partitionSlotType) {
case CONST: continue;
case RANGE:
- ColumnRange columnRange = context.columnRanges.get(slot);
- if (!columnRange.isSingleton()
- ||
!columnRange.getLowerBound().getValue().equals(partitionBound.get(i))) {
+ // Expanded RANGE literals can disappear after constant
folding. Recover only this
+ // equal-prefix coordinate from the partition projection.
+ if (columnRange == null) {
+ columnRange = defaultColumnRanges.get(slot);
+ }
+ if (columnRange == null || !columnRange.isSingleton()) {
+ return context;
+ }
+
boundState.observeLiteral(columnRange.getLowerBound().getValue(), i);
+ if (!boundState.hasEqualPrefix()) {
return context;
}
continue;
case OTHER:
- columnRange = context.columnRanges.get(slot);
+ // Do not expose a default-only partition range to the
predicate tree. In
+ // particular, visitNot() must never complement a range
that the predicate did
+ // not contribute.
+ if (columnRange == null) {
+ return context;
+ }
if (columnRange.isSingleton()
- &&
columnRange.getLowerBound().getValue().equals(partitionBound.get(i))) {
+ &&
columnRange.getLowerBound().getValue().equals(partitionBound.get(i))
+ && i + 1 < partitionSlots.size()) {
continue;
}
-
- qualifiedSlot = slot;
- if (isLowerBound) {
- qualifiedRange =
ColumnRange.atLeast(partitionBound.get(i));
- } else {
- qualifiedRange = i + 1 == partitionSlots.size()
- ? ColumnRange.lessThen(partitionBound.get(i))
- : ColumnRange.atMost(partitionBound.get(i));
- }
- break;
+ ColumnRange newRange =
boundState.constrainFirstUnresolvedColumn(columnRange, i);
+ Map<Expression, ColumnRange> newRanges = replaceExprRange(
+ context.columnRanges, slot, newRange);
+ return newRange.isEmptyRange()
+ ? new EvaluateRangeResult(BooleanLiteral.FALSE,
newRanges, context.childrenResult)
+ : new EvaluateRangeResult(context.result,
newRanges, context.childrenResult);
default:
- throw new AnalysisException("Unknown partition slot type:
" + partitionSlotType);
- }
- }
-
- if (qualifiedSlot != null) {
- ColumnRange origin = context.columnRanges.get(qualifiedSlot);
- ColumnRange newRange = origin.intersect(qualifiedRange);
-
- Map<Expression, ColumnRange> newRanges =
replaceExprRange(context.columnRanges, qualifiedSlot, newRange);
-
- if (newRange.isEmptyRange()) {
- return new EvaluateRangeResult(BooleanLiteral.FALSE,
newRanges, context.childrenResult);
- } else {
- return new EvaluateRangeResult(context.result, newRanges,
context.childrenResult);
+ disablePruningOnInvalidState(
+ "Unknown partition slot type while refining a
lexicographic bound: "
+ + partitionSlotType);
+ return context;
}
}
return context;
@@ -754,52 +826,69 @@ public class OneRangePartitionEvaluator<K>
return ImmutableList.of(slotToInputs);
}
+ /**
+ * Build evaluator inputs for every expanded representation of this
composite RANGE partition.
+ *
+ * <p>Range expansion replaces enumerable coordinates with literals and
leaves unexpanded
+ * coordinates as slots. Separate lower- and upper-bound states track
whether the literal prefix
+ * of each generated input still equals the corresponding endpoint. Only
the first unresolved
+ * coordinate while a state is active receives that endpoint's constraint;
after it can diverge,
+ * all suffix coordinates remain unbounded. This preserves tuple ordering
instead of incorrectly
+ * treating each partition column as an independent interval.
+ *
+ * <p>The returned {@link PartitionSlotInput}s all contain the complete
projected range map for
+ * their generated input. Expression evaluation can therefore recover
ranges for slots that were
+ * replaced by literals and removed by constant folding.
+ *
+ * <p>A {@code CONST} coordinate must have been expanded to a literal. If
it is not, the input is
+ * structurally invalid: fe_debug throws, while production makes the
coordinate unbounded and
+ * disables pruning for the whole evaluator so planning can continue
without dropping data.
+ *
+ * @return one slot-to-input map for each Cartesian-product row produced
by range expansion
+ */
private List<Map<Slot, PartitionSlotInput>>
commonComputeOnePartitionInputs() {
List<Map<Slot, PartitionSlotInput>> onePartitionInputs =
Lists.newArrayListWithCapacity(inputs.size());
for (List<Expression> input : inputs) {
- boolean previousIsLowerBoundLiteral = true;
- boolean previousIsUpperBoundLiteral = true;
+ LexicographicBoundState lowerState = new LexicographicBoundState(
+ lowers, true, partitionSlots.size());
+ LexicographicBoundState upperState = new LexicographicBoundState(
+ uppers, false, partitionSlots.size());
Builder<Slot, PartitionSlotInput> slotToInputs =
ImmutableMap.builderWithExpectedSize(16);
for (int i = 0; i < partitionSlots.size(); ++i) {
Slot partitionSlot = partitionSlots.get(i);
// partitionSlot will be replaced to this expression
Expression expression = input.get(i);
- ColumnRange slotRange = null;
+ ColumnRange slotRange;
PartitionSlotType partitionSlotType =
partitionSlotTypes.get(i);
if (expression instanceof Literal) {
// const or expanded range
slotRange = ColumnRange.singleton((Literal) expression);
- if (!expression.equals(lowers.get(i))) {
- previousIsLowerBoundLiteral = false;
- }
- if (!expression.equals(uppers.get(i))) {
- previousIsUpperBoundLiteral = false;
- }
+ lowerState.observeLiteral(expression, i);
+ upperState.observeLiteral(expression, i);
} else {
- // un expanded range
+ // The first unresolved column carries every still-active
lexicographic bound.
+ // Once that column can diverge, every suffix column must
remain unbounded.
switch (partitionSlotType) {
case RANGE:
- boolean isLastPartitionColumn = i + 1 ==
partitionSlots.size();
- BoundType rightBoundType = isLastPartitionColumn
- ? BoundType.OPEN : BoundType.CLOSED;
- slotRange = ColumnRange.range(
- lowers.get(i), BoundType.CLOSED,
uppers.get(i), rightBoundType);
- break;
case OTHER:
- if (previousIsLowerBoundLiteral) {
- slotRange = ColumnRange.atLeast(lowers.get(i));
- } else if (previousIsUpperBoundLiteral) {
- slotRange =
ColumnRange.lessThen(uppers.get(i));
- } else {
- // unknown range
- slotRange = ColumnRange.all();
- }
+ slotRange =
lowerState.constrainFirstUnresolvedColumn(ColumnRange.all(), i);
+ slotRange =
upperState.constrainFirstUnresolvedColumn(slotRange, i);
+ break;
+ case CONST:
+ disablePruningOnInvalidState("CONST partition
input must be a literal: slot="
+ + partitionSlot + ", expression=" +
expression);
+ slotRange = ColumnRange.all();
+ lowerState.diverge();
+ upperState.diverge();
break;
default:
- throw new AnalysisException("Unknown partition
slot type: " + partitionSlotType);
+ disablePruningOnInvalidState(
+ "Unknown partition slot type while
building evaluator inputs: "
+ + partitionSlotType);
+ slotRange = ColumnRange.all();
+ lowerState.diverge();
+ upperState.diverge();
}
- previousIsLowerBoundLiteral = false;
- previousIsUpperBoundLiteral = false;
}
ImmutableMap<Expression, ColumnRange> slotToRange =
ImmutableMap.of(partitionSlot, slotRange);
slotToInputs.put(partitionSlot, new
PartitionSlotInput(expression, slotToRange));
@@ -811,6 +900,120 @@ public class OneRangePartitionEvaluator<K>
return onePartitionInputs;
}
+ /**
+ * Handle a state that violates the contract between {@link
PartitionRangeExpander} and this evaluator.
+ *
+ * <p>Tests and debugging sessions set {@code fe_debug=true}, so they fail
immediately and expose
+ * the broken invariant. Production planning must remain available: after
logging the problem,
+ * evaluation returns the original predicate for this partition, which
conservatively keeps it.
+ *
+ * @param message description of the invalid state
+ */
+ private void disablePruningOnInvalidState(String message) {
+ SessionVariable.throwAnalysisExceptionWhenFeDebug(message);
+ disablePruning = true;
+ }
+
+ /** Describes whether the coordinates already consumed are still equal to
an endpoint prefix. */
+ private enum BoundPrefixState {
+ /** No consumed coordinate differs from the tracked lower or upper
endpoint. */
+ EQUAL_PREFIX,
+
+ /** An earlier coordinate can differ, so this endpoint cannot
constrain any later coordinate. */
+ DIVERGED
+ }
+
+ /**
+ * Tracks one lower or upper endpoint while projected ranges are built or
refined.
+ *
+ * <p>Lexicographic comparison makes an endpoint relevant only as long as
all preceding
+ * coordinates equal its prefix. For example, with lower endpoint {@code
(1, 10, 100)}, an input
+ * whose first coordinate is {@code 1} must constrain the first unresolved
coordinate to
+ * {@code >= 10}. If the first coordinate is {@code 2}, the lower endpoint
is already satisfied
+ * and neither the second nor third coordinate receives a lower constraint.
+ *
+ * <p>The state is monotonic: it starts at {@link
BoundPrefixState#EQUAL_PREFIX} and can transition
+ * to {@link BoundPrefixState#DIVERGED} only once.
+ */
+ private static class LexicographicBoundState {
+ private final List<Literal> bound;
+ private final boolean lowerBound;
+ private final int columnCount;
+ private BoundPrefixState prefixState = BoundPrefixState.EQUAL_PREFIX;
+
+ /**
+ * Create state for one endpoint of a composite partition.
+ *
+ * @param bound endpoint values in partition-column order
+ * @param lowerBound true for the inclusive lower endpoint, false for
the exclusive upper endpoint
+ * @param columnCount total number of partition columns, used to
identify the final coordinate
+ */
+ private LexicographicBoundState(List<Literal> bound, boolean
lowerBound, int columnCount) {
+ this.bound = bound;
+ this.lowerBound = lowerBound;
+ this.columnCount = columnCount;
+ }
+
+ /**
+ * Return whether every coordinate observed so far is equal to this
endpoint's prefix.
+ *
+ * @return true while this endpoint may still constrain the next
unresolved coordinate
+ */
+ private boolean hasEqualPrefix() {
+ return prefixState == BoundPrefixState.EQUAL_PREFIX;
+ }
+
+ /**
+ * Consume a literal coordinate and deactivate this endpoint if the
literal differs from it.
+ *
+ * <p>Once a coordinate differs, later values cannot make the tuple
equal to the endpoint
+ * again, so an already-diverged state is intentionally left unchanged.
+ *
+ * @param literal literal selected for the current partition coordinate
+ * @param index zero-based partition-column index of {@code literal}
+ */
+ private void observeLiteral(Expression literal, int index) {
+ if (hasEqualPrefix() && !literal.equals(bound.get(index))) {
+ diverge();
+ }
+ }
+
+ /**
+ * Intersect an unresolved coordinate with this endpoint when its
prefix is still equal.
+ *
+ * <p>The lower endpoint is inclusive on every coordinate. The upper
endpoint is inclusive on
+ * non-terminal coordinates because equality there leaves later
coordinates to decide tuple
+ * membership; only the final coordinate is exclusive. Consuming an
unresolved coordinate
+ * always deactivates the endpoint so no suffix coordinate is
independently constrained.
+ *
+ * @param origin range already inferred for the unresolved coordinate
+ * @param index zero-based partition-column index of that coordinate
+ * @return {@code origin} intersected with the active endpoint, or
unchanged if an earlier
+ * coordinate has already diverged
+ */
+ private ColumnRange constrainFirstUnresolvedColumn(ColumnRange origin,
int index) {
+ if (!hasEqualPrefix()) {
+ return origin;
+ }
+ diverge();
+ Literal boundary = bound.get(index);
+ ColumnRange boundaryRange;
+ if (lowerBound) {
+ boundaryRange = ColumnRange.atLeast(boundary);
+ } else if (index + 1 == columnCount) {
+ boundaryRange = ColumnRange.lessThen(boundary);
+ } else {
+ boundaryRange = ColumnRange.atMost(boundary);
+ }
+ return origin.intersect(boundaryRange);
+ }
+
+ /** Mark this endpoint as satisfied or violated by an earlier decisive
coordinate. */
+ private void diverge() {
+ prefixState = BoundPrefixState.DIVERGED;
+ }
+ }
+
public EvaluateRangeResult visitMonotonic(Expression monotonic,
EvaluateRangeInput context) {
EvaluateRangeResult rangeResult = evaluateChildrenThenThis(monotonic,
context);
if (!rangeResult.result.getClass().equals(monotonic.getClass())) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PartitionPrunerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PartitionPrunerTest.java
index 6d38a17fca1..11ea103bd44 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PartitionPrunerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PartitionPrunerTest.java
@@ -24,11 +24,13 @@ import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.PartitionItem;
import org.apache.doris.catalog.PartitionKey;
import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.RangePartitionItem;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Pair;
import org.apache.doris.nereids.CascadesContext;
import
org.apache.doris.nereids.rules.expression.rules.OneListPartitionEvaluator;
import org.apache.doris.nereids.rules.expression.rules.OnePartitionEvaluator;
+import
org.apache.doris.nereids.rules.expression.rules.OneRangePartitionEvaluator;
import org.apache.doris.nereids.rules.expression.rules.PartitionPruner;
import
org.apache.doris.nereids.rules.expression.rules.PartitionPruner.PartitionPruneResult;
import
org.apache.doris.nereids.rules.expression.rules.PartitionPruner.PartitionTableType;
@@ -36,11 +38,13 @@ import org.apache.doris.nereids.trees.expressions.And;
import org.apache.doris.nereids.trees.expressions.EqualTo;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
import org.apache.doris.nereids.trees.expressions.InPredicate;
import org.apache.doris.nereids.trees.expressions.IsNull;
import org.apache.doris.nereids.trees.expressions.LessThan;
import org.apache.doris.nereids.trees.expressions.Not;
import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
@@ -50,15 +54,19 @@ import
org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
import org.apache.doris.nereids.types.IntegerType;
import org.apache.doris.nereids.types.VarcharType;
+import org.apache.doris.nereids.util.ExpressionUtils;
import org.apache.doris.utframe.TestWithFeService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Range;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -72,6 +80,7 @@ public class PartitionPrunerTest extends TestWithFeService {
private final SlotReference slotA = new SlotReference("a",
IntegerType.INSTANCE);
private final SlotReference slotB = new SlotReference("b",
IntegerType.INSTANCE);
private final SlotReference slotC = new SlotReference("c",
IntegerType.INSTANCE);
+ private final SlotReference slotD = new SlotReference("d",
IntegerType.INSTANCE);
@Override
protected void runBeforeAll() throws Exception {
@@ -600,6 +609,195 @@ public class PartitionPrunerTest extends
TestWithFeService {
Assertions.assertTrue(result.hasPartitionPredicate);
}
+ /**
+ * Verify lower/upper boundary transitions for a three-column partition in
both evaluator paths.
+ *
+ * <p>The low threshold leaves the RANGE coordinate unexpanded, while the
high threshold expands
+ * it into literals. The witness tuples cover divergence in a suffix
coordinate, an exactly equal
+ * lower endpoint, and tuples at or beyond the exclusive upper endpoint.
+ */
+ @Test
+ public void testThreeColumnLexicographicRangeBoundaries()
+ throws AnalysisException, InvocationTargetException,
IllegalAccessException {
+ List<Column> columns = ImmutableList.of(
+ new Column("a", PrimitiveType.INT),
+ new Column("b", PrimitiveType.INT),
+ new Column("c", PrimitiveType.INT));
+ List<Slot> slots = ImmutableList.of(slotA, slotB, slotC);
+ RangePartitionItem partitionItem = createRangePartitionItem(
+ columns, new int[] {1, 10, 100}, new int[] {100, 20, 200});
+
+ for (int expandThreshold : new int[] {1, 200}) {
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 1, 11, 50);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 100, 19, 250);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 100, 20, 199);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 1, 10, 100);
+
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 1, 10, 99);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 100, 20, 200);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 100, 20, 250);
+ }
+ }
+
+ /**
+ * Verify that only the first unresolved coordinate receives a
lexicographic endpoint constraint.
+ *
+ * <p>Four columns exercise divergence at the first, middle, and final
coordinates. Extreme suffix
+ * values demonstrate that once an earlier coordinate differs, later
columns are intentionally
+ * unbounded; exact-prefix cases verify inclusive lower and exclusive
upper semantics at the end.
+ */
+ @Test
+ public void testFourColumnLexicographicRangeBoundaries()
+ throws AnalysisException, InvocationTargetException,
IllegalAccessException {
+ List<Column> columns = ImmutableList.of(
+ new Column("a", PrimitiveType.INT),
+ new Column("b", PrimitiveType.INT),
+ new Column("c", PrimitiveType.INT),
+ new Column("d", PrimitiveType.INT));
+ List<Slot> slots = ImmutableList.of(slotA, slotB, slotC, slotD);
+ RangePartitionItem partitionItem = createRangePartitionItem(
+ columns, new int[] {1, 10, 100, 1000}, new int[] {4, 20, 200,
2000});
+
+ for (int expandThreshold : new int[] {1, 10}) {
+ // Once an earlier column diverges, all suffix columns are
unbounded.
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 2, -1000, -1000, -1000);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 3, 9999, 9999, 9999);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 1, 11, -1, -1);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 4, 19, 9999, 9999);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 1, 10, 101, -1);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 4, 20, 199, 9999);
+
+ // An equal prefix leaves the final column to decide the inclusive
lower and exclusive upper bounds.
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 1, 10, 100, 1000);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
false, 4, 20, 200, 1999);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 1, 10, 100, 999);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 4, 20, 200, 2000);
+ assertRangeTuplePruned(partitionItem, slots, expandThreshold,
true, 4, 20, 200, 2001);
+ }
+ }
+
+ /**
+ * A range copied only from the partition projection must not become part
of a predicate result.
+ *
+ * <p>This is the reduced tree produced by the Date-IN partition-pruning
rewrite for
+ * {@code NOT(date(c) IN (day2, day3))}. For expanded {@code a = 1}, the
predicate constrains only
+ * {@code c}; injecting the default {@code b >= 10} range would let {@code
NOT} complement it and
+ * incorrectly turn the entire partition into false.
+ */
+ @Test
+ public void testNotDoesNotComplementDefaultOnlySuffixRange()
+ throws AnalysisException, InvocationTargetException,
IllegalAccessException {
+ List<Column> columns = ImmutableList.of(
+ new Column("a", PrimitiveType.INT),
+ new Column("b", PrimitiveType.INT),
+ new Column("c", PrimitiveType.INT));
+ List<Slot> slots = ImmutableList.of(slotA, slotB, slotC);
+ RangePartitionItem partitionItem = createRangePartitionItem(
+ columns, new int[] {1, 10, 1}, new int[] {2, 20, 4});
+ Expression predicate = new Not(new Or(
+ new And(new GreaterThanEqual(slotC, Literal.of(2)), new
LessThan(slotC, Literal.of(3))),
+ new And(new GreaterThanEqual(slotC, Literal.of(3)), new
LessThan(slotC, Literal.of(4)))));
+ OneRangePartitionEvaluator<String> evaluator = new
OneRangePartitionEvaluator<>(
+ "p1", slots, partitionItem, cascadesContext, 200);
+
+ Pair<Boolean, Boolean> result =
+ (Pair<Boolean, Boolean>) canBePrunedOutMethod.invoke(null,
predicate, evaluator);
+
+ Assertions.assertFalse(result.first);
+ }
+
+ /**
+ * Invalid expander/evaluator states fail in fe_debug but conservatively
keep the partition in production.
+ */
+ @Test
+ public void testInvalidConstInputUsesFeDebugPolicy() throws Exception {
+ List<Column> columns = ImmutableList.of(
+ new Column("a", PrimitiveType.INT),
+ new Column("b", PrimitiveType.INT));
+ List<Slot> slots = ImmutableList.of(slotA, slotB);
+ RangePartitionItem partitionItem = createRangePartitionItem(
+ columns, new int[] {1, 10}, new int[] {1, 20});
+ Expression predicate = new EqualTo(slotA, Literal.of(2));
+ boolean oldFeDebug = connectContext.getSessionVariable().feDebug;
+ try {
+ connectContext.getSessionVariable().feDebug = false;
+ OneRangePartitionEvaluator<String> productionEvaluator =
+ createEvaluatorWithNonLiteralConstInput(partitionItem,
slots);
+ Pair<Boolean, Boolean> productionResult = (Pair<Boolean, Boolean>)
canBePrunedOutMethod.invoke(
+ null, predicate, productionEvaluator);
+ Assertions.assertFalse(productionResult.first);
+
+ connectContext.getSessionVariable().feDebug = true;
+ OneRangePartitionEvaluator<String> debugEvaluator =
+ createEvaluatorWithNonLiteralConstInput(partitionItem,
slots);
+ InvocationTargetException exception =
Assertions.assertThrows(InvocationTargetException.class,
+ () -> canBePrunedOutMethod.invoke(null, predicate,
debugEvaluator));
+ Assertions.assertInstanceOf(
+
org.apache.doris.nereids.exceptions.AnalysisException.class,
exception.getCause());
+ } finally {
+ connectContext.getSessionVariable().feDebug = oldFeDebug;
+ }
+ }
+
+ private OneRangePartitionEvaluator<String>
createEvaluatorWithNonLiteralConstInput(
+ RangePartitionItem partitionItem, List<Slot> slots) throws
ReflectiveOperationException {
+ OneRangePartitionEvaluator<String> evaluator = new
OneRangePartitionEvaluator<>(
+ "p1", slots, partitionItem, cascadesContext, 200);
+ Field inputsField =
OneRangePartitionEvaluator.class.getDeclaredField("inputs");
+ inputsField.setAccessible(true);
+ inputsField.set(evaluator, ImmutableList.of(ImmutableList.of(slotA,
slotB)));
+ return evaluator;
+ }
+
+ /**
+ * Evaluate an equality predicate for one tuple and assert whether the
partition is pruned.
+ *
+ * @param partitionItem composite RANGE partition under test
+ * @param slots partition slots in tuple order
+ * @param expandThreshold threshold selecting expanded or unexpanded
evaluator inputs
+ * @param expectedPruned expected result from {@code
PartitionPruner.canBePrunedOut}
+ * @param values tuple values used to build one equality per partition slot
+ */
+ private void assertRangeTuplePruned(RangePartitionItem partitionItem,
List<Slot> slots,
+ int expandThreshold, boolean expectedPruned, int... values)
+ throws InvocationTargetException, IllegalAccessException {
+ ImmutableList.Builder<Expression> equalities =
ImmutableList.builderWithExpectedSize(values.length);
+ for (int i = 0; i < values.length; i++) {
+ equalities.add(new EqualTo(slots.get(i), Literal.of(values[i])));
+ }
+ Expression predicate = ExpressionUtils.and(equalities.build());
+ OneRangePartitionEvaluator<String> evaluator = new
OneRangePartitionEvaluator<>(
+ "p1", slots, partitionItem, cascadesContext, expandThreshold);
+ Pair<Boolean, Boolean> result =
+ (Pair<Boolean, Boolean>) canBePrunedOutMethod.invoke(null,
predicate, evaluator);
+ Assertions.assertEquals(expectedPruned, result.first,
+ "tuple=" + Arrays.toString(values) + ", expandThreshold=" +
expandThreshold);
+ }
+
+ /**
+ * Construct a closed-open composite RANGE partition from integer tuple
endpoints.
+ *
+ * @param columns partition columns in key order
+ * @param lowerValues inclusive lower endpoint values
+ * @param upperValues exclusive upper endpoint values
+ * @return partition item representing {@code [lowerValues, upperValues)}
+ * @throws AnalysisException if an endpoint cannot be converted to a typed
partition key
+ */
+ private RangePartitionItem createRangePartitionItem(
+ List<Column> columns, int[] lowerValues, int[] upperValues) throws
AnalysisException {
+ ImmutableList.Builder<PartitionValue> lower =
ImmutableList.builderWithExpectedSize(lowerValues.length);
+ ImmutableList.Builder<PartitionValue> upper =
ImmutableList.builderWithExpectedSize(upperValues.length);
+ for (int value : lowerValues) {
+ lower.add(new PartitionValue(Integer.toString(value)));
+ }
+ for (int value : upperValues) {
+ upper.add(new PartitionValue(Integer.toString(value)));
+ }
+ PartitionKey lowerKey = PartitionKey.createPartitionKey(lower.build(),
columns);
+ PartitionKey upperKey = PartitionKey.createPartitionKey(upper.build(),
columns);
+ return new RangePartitionItem(Range.closedOpen(lowerKey, upperKey));
+ }
+
private ListPartitionItem createListPartitionItem(String... values) throws
AnalysisException {
ImmutableList.Builder<PartitionKey> partitionKeys =
ImmutableList.builder();
for (String value : values) {
diff --git
a/regression-test/data/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.out
b/regression-test/data/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.out
new file mode 100644
index 00000000000..4cf8fb2485d
--- /dev/null
+++
b/regression-test/data/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.out
@@ -0,0 +1,21 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !expanded_lower_suffix --
+1
+
+-- !expanded_upper_suffix --
+2
+
+-- !expanded_upper_equal_prefix --
+3
+
+-- !unexpanded_lower_suffix --
+1
+
+-- !unexpanded_upper_suffix --
+2
+
+-- !unexpanded_upper_equal_prefix --
+3
+
+-- !negated_multi_date_in --
+42
diff --git
a/regression-test/suites/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.groovy
b/regression-test/suites/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.groovy
new file mode 100644
index 00000000000..91c2a36ad8c
--- /dev/null
+++
b/regression-test/suites/nereids_rules_p0/partition_prune/test_lexicographic_range_partition.groovy
@@ -0,0 +1,93 @@
+// 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.
+
+suite("test_lexicographic_range_partition") {
+ sql "DROP TABLE IF EXISTS lexicographic_range_partition"
+ sql """
+ CREATE TABLE lexicographic_range_partition (
+ k1 INT NOT NULL,
+ k2 INT NOT NULL,
+ k3 INT NOT NULL,
+ v INT NOT NULL
+ )
+ DUPLICATE KEY(k1, k2, k3)
+ PARTITION BY RANGE(k1, k2, k3) (
+ PARTITION p_target VALUES [("1", "10", "100"), ("100", "20",
"200"))
+ )
+ DISTRIBUTED BY HASH(k1) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO lexicographic_range_partition VALUES
+ (1, 11, 50, 1),
+ (100, 19, 250, 2),
+ (100, 20, 199, 3),
+ (1, 10, 100, 4)
+ """
+
+ // Verify the optimizer retains the only partition before checking the
query result. A wrong
+ // empty result alone is ambiguous here because it could also come from
execution or test data.
+ def assertTargetPartition = { String query ->
+ explain {
+ sql query
+ contains "partitions=1/1 (p_target)"
+ }
+ }
+
+ sql "SET partition_pruning_expand_threshold=200"
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=1 AND k2=11 AND k3=50")
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=100 AND k2=19 AND k3=250")
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=100 AND k2=20 AND k3=199")
+ qt_expanded_lower_suffix "SELECT v FROM lexicographic_range_partition
WHERE k1=1 AND k2=11 AND k3=50"
+ qt_expanded_upper_suffix "SELECT v FROM lexicographic_range_partition
WHERE k1=100 AND k2=19 AND k3=250"
+ qt_expanded_upper_equal_prefix "SELECT v FROM
lexicographic_range_partition WHERE k1=100 AND k2=20 AND k3=199"
+
+ sql "SET partition_pruning_expand_threshold=1"
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=1 AND k2=11 AND k3=50")
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=100 AND k2=19 AND k3=250")
+ assertTargetPartition("SELECT v FROM lexicographic_range_partition WHERE
k1=100 AND k2=20 AND k3=199")
+ qt_unexpanded_lower_suffix "SELECT v FROM lexicographic_range_partition
WHERE k1=1 AND k2=11 AND k3=50"
+ qt_unexpanded_upper_suffix "SELECT v FROM lexicographic_range_partition
WHERE k1=100 AND k2=19 AND k3=250"
+ qt_unexpanded_upper_equal_prefix "SELECT v FROM
lexicographic_range_partition WHERE k1=100 AND k2=20 AND k3=199"
+
+ sql "DROP TABLE IF EXISTS lexicographic_range_not_date_in"
+ sql """
+ CREATE TABLE lexicographic_range_not_date_in (
+ k1 INT NOT NULL,
+ k2 INT NOT NULL,
+ k3 DATE NOT NULL,
+ v INT NOT NULL
+ )
+ DUPLICATE KEY(k1, k2, k3)
+ PARTITION BY RANGE(k1, k2, k3) (
+ PARTITION p_target VALUES [("1", "10", "2020-01-01"), ("2", "20",
"2020-01-04"))
+ )
+ DISTRIBUTED BY HASH(k1) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql "INSERT INTO lexicographic_range_not_date_in VALUES (1, 11,
'2020-01-01', 42)"
+
+ // The Date-IN rewrite creates NOT(OR(day ranges)). Default-only k2 ranges
must not leak into
+ // that predicate tree, otherwise NOT turns the valid partition into an
empty set.
+ sql "SET partition_pruning_expand_threshold=200"
+ def negatedDateInQuery = """
+ SELECT v FROM lexicographic_range_not_date_in
+ WHERE NOT(DATE(k3) IN ('2020-01-02', '2020-01-03'))
+ """
+ assertTargetPartition(negatedDateInQuery)
+ qt_negated_multi_date_in negatedDateInQuery
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]