diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/ColumnRange.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/ColumnRange.java index 3fcce02cde07ea..e932d4f2364681 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/ColumnRange.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/ColumnRange.java @@ -138,4 +138,12 @@ public static ColumnRange between(Literal lower, Literal upper) { public static ColumnRange range(Literal lower, BoundType lowerType, Literal upper, BoundType upperType) { return new ColumnRange(ColumnBound.range(lower, lowerType, upper, upperType)); } + + public ColumnRange withLowerBound(Literal lower) { + return this.intersect(atLeast(lower)); + } + + public ColumnRange withUpperBound(Literal upper) { + return this.intersect(atMost(upper)); + } } 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 8b01d2b8c672d2..446faffe0f597b 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 @@ -42,16 +42,18 @@ import org.apache.doris.nereids.trees.expressions.NullSafeEqual; import org.apache.doris.nereids.trees.expressions.Or; import org.apache.doris.nereids.trees.expressions.Slot; +import org.apache.doris.nereids.trees.expressions.functions.Monotonic; import org.apache.doris.nereids.trees.expressions.functions.scalar.Date; +import org.apache.doris.nereids.trees.expressions.functions.scalar.DateTrunc; import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral; import org.apache.doris.nereids.trees.expressions.literal.Literal; -import org.apache.doris.nereids.trees.expressions.literal.NullLiteral; import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor; import org.apache.doris.nereids.types.BooleanType; import org.apache.doris.nereids.types.DataType; import org.apache.doris.nereids.util.ExpressionUtils; import org.apache.doris.nereids.util.Utils; +import com.google.common.base.Preconditions; import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -62,6 +64,7 @@ import com.google.common.collect.Range; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -87,8 +90,9 @@ public class OneRangePartitionEvaluator private final List lowers; private final List uppers; private final List> inputs; - private final Map partitionSlotContainsNull; + private final Map partitionSlotContainsNull; private final Map slotToType; + private final Map rangeMap = new HashMap<>(); /** OneRangePartitionEvaluator */ public OneRangePartitionEvaluator(long partitionId, List partitionSlots, @@ -109,8 +113,8 @@ public OneRangePartitionEvaluator(long partitionId, List partitionSlots, // fast path Slot partSlot = partitionSlots.get(0); this.slotToType = ImmutableMap.of(partSlot, partitionSlotTypes.get(0)); - this.partitionSlotContainsNull - = ImmutableMap.of(partSlot, range.lowerEndpoint().getKeys().get(0).isMinValue()); + this.partitionSlotContainsNull = new HashMap<>(); + partitionSlotContainsNull.put(partSlot, range.lowerEndpoint().getKeys().get(0).isMinValue()); } else { // slow path this.slotToType = Maps.newHashMap(); @@ -169,9 +173,9 @@ public List> getOnePartitionInputs() { @Override public Expression evaluate(Expression expression, Map currentInputs) { - Map defaultColumnRanges = currentInputs.values().iterator().next().columnRanges; - EvaluateRangeResult result = expression.accept( - this, new EvaluateRangeInput(defaultColumnRanges, currentInputs)); + Map defaultColumnRanges = currentInputs.values().iterator().next().columnRanges; + rangeMap.putAll(defaultColumnRanges); + EvaluateRangeResult result = expression.accept(this, new EvaluateRangeInput(currentInputs)); return result.result; } @@ -194,22 +198,14 @@ public EvaluateRangeResult visit(Expression expr, EvaluateRangeInput context) { return result; } - @Override - public EvaluateRangeResult visitNullLiteral(NullLiteral nullLiteral, EvaluateRangeInput context) { - Map emptyRanges = Maps.newHashMap(); - for (Slot key : context.defaultColumnRanges.keySet()) { - emptyRanges.put(key, new ColumnRange()); - } - return new EvaluateRangeResult(nullLiteral, emptyRanges, ImmutableList.of()); - } - @Override public EvaluateRangeResult visitSlot(Slot slot, EvaluateRangeInput context) { // try to replace partition slot to literal PartitionSlotInput slotResult = context.slotToInput.get(slot); - return slotResult == null - ? new EvaluateRangeResult(slot, context.defaultColumnRanges, ImmutableList.of()) - : new EvaluateRangeResult(slotResult.result, slotResult.columnRanges, ImmutableList.of()); + Preconditions.checkState(slotResult != null); + Preconditions.checkState(slotResult.columnRanges.containsKey(slot)); + return new EvaluateRangeResult(slotResult.result, ImmutableMap.of(slot, slotResult.columnRanges.get(slot)), + ImmutableList.of()); } @Override @@ -219,19 +215,19 @@ public EvaluateRangeResult visitGreaterThan(GreaterThan greaterThan, EvaluateRan return result; } greaterThan = (GreaterThan) result.result; - if (greaterThan.left() instanceof Slot && greaterThan.right() instanceof Literal) { - Slot slot = (Slot) greaterThan.left(); - if (isPartitionSlot(slot)) { - Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (!(greaterThan.left() instanceof Literal) && greaterThan.right() instanceof Literal) { + Expression expr = greaterThan.left(); + Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (leftColumnRanges.containsKey(expr)) { ColumnRange greaterThenRange = ColumnRange.greaterThan((Literal) greaterThan.right()); - result = intersectSlotRange(result, leftColumnRanges, slot, greaterThenRange); + result = intersectSlotRange(result, leftColumnRanges, expr, greaterThenRange); } - } else if (greaterThan.left() instanceof Literal && greaterThan.right() instanceof Slot) { - Slot slot = (Slot) greaterThan.right(); - if (isPartitionSlot(slot)) { - Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + } else if (greaterThan.left() instanceof Literal && !(greaterThan.right() instanceof Literal)) { + Expression expr = greaterThan.right(); + Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + if (rightColumnRanges.containsKey(expr)) { ColumnRange lessThenRange = ColumnRange.lessThen((Literal) greaterThan.left()); - result = intersectSlotRange(result, rightColumnRanges, slot, lessThenRange); + result = intersectSlotRange(result, rightColumnRanges, expr, lessThenRange); } } return result; @@ -244,19 +240,19 @@ public EvaluateRangeResult visitGreaterThanEqual(GreaterThanEqual greaterThanEqu return result; } greaterThanEqual = (GreaterThanEqual) result.result; - if (greaterThanEqual.left() instanceof Slot && greaterThanEqual.right() instanceof Literal) { - Slot slot = (Slot) greaterThanEqual.left(); - if (isPartitionSlot(slot)) { - Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (!(greaterThanEqual.left() instanceof Literal) && greaterThanEqual.right() instanceof Literal) { + Expression expr = greaterThanEqual.left(); + Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (leftColumnRanges.containsKey(expr)) { ColumnRange atLeastRange = ColumnRange.atLeast((Literal) greaterThanEqual.right()); - result = intersectSlotRange(result, leftColumnRanges, slot, atLeastRange); + result = intersectSlotRange(result, leftColumnRanges, expr, atLeastRange); } - } else if (greaterThanEqual.left() instanceof Literal && greaterThanEqual.right() instanceof Slot) { - Slot slot = (Slot) greaterThanEqual.right(); - if (isPartitionSlot(slot)) { - Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + } else if (greaterThanEqual.left() instanceof Literal && !(greaterThanEqual.right() instanceof Literal)) { + Expression expr = greaterThanEqual.right(); + Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + if (rightColumnRanges.containsKey(expr)) { ColumnRange atMostRange = ColumnRange.atMost((Literal) greaterThanEqual.left()); - result = intersectSlotRange(result, rightColumnRanges, slot, atMostRange); + result = intersectSlotRange(result, rightColumnRanges, expr, atMostRange); } } return result; @@ -269,19 +265,19 @@ public EvaluateRangeResult visitLessThan(LessThan lessThan, EvaluateRangeInput c return result; } lessThan = (LessThan) result.result; - if (lessThan.left() instanceof Slot && lessThan.right() instanceof Literal) { - Slot slot = (Slot) lessThan.left(); - if (isPartitionSlot(slot)) { - Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (!(lessThan.left() instanceof Literal) && lessThan.right() instanceof Literal) { + Expression expr = lessThan.left(); + Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (leftColumnRanges.containsKey(expr)) { ColumnRange greaterThenRange = ColumnRange.lessThen((Literal) lessThan.right()); - result = intersectSlotRange(result, leftColumnRanges, slot, greaterThenRange); + result = intersectSlotRange(result, leftColumnRanges, expr, greaterThenRange); } - } else if (lessThan.left() instanceof Literal && lessThan.right() instanceof Slot) { - Slot slot = (Slot) lessThan.right(); - if (isPartitionSlot(slot)) { - Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + } else if (lessThan.left() instanceof Literal && !(lessThan.right() instanceof Literal)) { + Expression expr = lessThan.right(); + Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + if (rightColumnRanges.containsKey(expr)) { ColumnRange lessThenRange = ColumnRange.greaterThan((Literal) lessThan.left()); - result = intersectSlotRange(result, rightColumnRanges, slot, lessThenRange); + result = intersectSlotRange(result, rightColumnRanges, expr, lessThenRange); } } return result; @@ -294,19 +290,19 @@ public EvaluateRangeResult visitLessThanEqual(LessThanEqual lessThanEqual, Evalu return result; } lessThanEqual = (LessThanEqual) result.result; - if (lessThanEqual.left() instanceof Slot && lessThanEqual.right() instanceof Literal) { - Slot slot = (Slot) lessThanEqual.left(); - if (isPartitionSlot(slot)) { - Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (!(lessThanEqual.left() instanceof Literal) && lessThanEqual.right() instanceof Literal) { + Expression expr = lessThanEqual.left(); + Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (leftColumnRanges.containsKey(expr)) { ColumnRange atLeastRange = ColumnRange.atMost((Literal) lessThanEqual.right()); - result = intersectSlotRange(result, leftColumnRanges, slot, atLeastRange); + result = intersectSlotRange(result, leftColumnRanges, expr, atLeastRange); } - } else if (lessThanEqual.left() instanceof Literal && lessThanEqual.right() instanceof Slot) { - Slot slot = (Slot) lessThanEqual.right(); - if (isPartitionSlot(slot)) { - Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + } else if (lessThanEqual.left() instanceof Literal && !(lessThanEqual.right() instanceof Literal)) { + Expression expr = lessThanEqual.right(); + Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + if (rightColumnRanges.containsKey(expr)) { ColumnRange atMostRange = ColumnRange.atLeast((Literal) lessThanEqual.left()); - result = intersectSlotRange(result, rightColumnRanges, slot, atMostRange); + result = intersectSlotRange(result, rightColumnRanges, expr, atMostRange); } } return result; @@ -319,23 +315,23 @@ public EvaluateRangeResult visitEqualTo(EqualTo equalTo, EvaluateRangeInput cont return result; } boolean isRejectNot = false; - if (equalTo.left() instanceof Slot && equalTo.right() instanceof Literal) { - Slot slot = (Slot) equalTo.left(); - if (isPartitionSlot(slot)) { - Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (!(equalTo.left() instanceof Literal) && equalTo.right() instanceof Literal) { + Expression expr = equalTo.left(); + Map leftColumnRanges = result.childrenResult.get(0).columnRanges; + if (leftColumnRanges.containsKey(expr)) { ColumnRange atLeastRange = ColumnRange.singleton((Literal) equalTo.right()); - result = intersectSlotRange(result, leftColumnRanges, slot, atLeastRange); - if (leftColumnRanges.get(slot).isSingleton()) { + result = intersectSlotRange(result, leftColumnRanges, expr, atLeastRange); + if (leftColumnRanges.get(expr).isSingleton()) { isRejectNot = true; } } - } else if (equalTo.left() instanceof Literal && equalTo.right() instanceof Slot) { - Slot slot = (Slot) equalTo.right(); - if (isPartitionSlot(slot)) { - Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + } else if (equalTo.left() instanceof Literal && !(equalTo.right() instanceof Literal)) { + Expression expr = equalTo.right(); + Map rightColumnRanges = result.childrenResult.get(1).columnRanges; + if (rightColumnRanges.containsKey(expr)) { ColumnRange atMostRange = ColumnRange.singleton((Literal) equalTo.left()); - result = intersectSlotRange(result, rightColumnRanges, slot, atMostRange); - if (rightColumnRanges.get(slot).isSingleton()) { + result = intersectSlotRange(result, rightColumnRanges, expr, atMostRange); + if (rightColumnRanges.get(expr).isSingleton()) { isRejectNot = true; } } @@ -356,11 +352,11 @@ public EvaluateRangeResult visitNullSafeEqual(NullSafeEqual nullSafeEqual, Evalu } // "A <=> null" has been convert to "A is null" or false by NullSafeEqualToEqual rule // so we don't consider "A <=> null" here - if (nullSafeEqual.left() instanceof Slot && nullSafeEqual.right() instanceof Literal) { + if (!(nullSafeEqual.left() instanceof Literal) && nullSafeEqual.right() instanceof Literal) { // A <=> literal -> A = literal and A is not null return visit(ExpressionUtils.and(new EqualTo(nullSafeEqual.left(), nullSafeEqual.right()), new Not(new IsNull(nullSafeEqual.left()))), context); - } else if (nullSafeEqual.left() instanceof Literal && nullSafeEqual.right() instanceof Slot) { + } else if (nullSafeEqual.left() instanceof Literal && !(nullSafeEqual.right() instanceof Slot)) { // literal <=> A -> literal = A and A is not null return visit(ExpressionUtils.and(new EqualTo(nullSafeEqual.left(), nullSafeEqual.right()), new Not(new IsNull(nullSafeEqual.right()))), context); @@ -376,17 +372,17 @@ public EvaluateRangeResult visitInPredicate(InPredicate inPredicate, EvaluateRan return result; } inPredicate = (InPredicate) result.result; - if (inPredicate.getCompareExpr() instanceof Slot + Map exprRanges = result.childrenResult.get(0).columnRanges; + if (exprRanges.containsKey(inPredicate.getCompareExpr()) && inPredicate.getOptions().stream().allMatch(Literal.class::isInstance)) { - Slot slot = (Slot) inPredicate.getCompareExpr(); + Expression compareExpr = inPredicate.getCompareExpr(); ColumnRange unionLiteralRange = ColumnRange.empty(); - ColumnRange slotRange = result.childrenResult.get(0).columnRanges.get(slot); + ColumnRange compareExprRange = result.childrenResult.get(0).columnRanges.get(compareExpr); for (Expression expr : inPredicate.getOptions()) { unionLiteralRange = unionLiteralRange.union( - slotRange.intersect(ColumnRange.singleton((Literal) expr))); + compareExprRange.intersect(ColumnRange.singleton((Literal) expr))); } - Map slotRanges = result.childrenResult.get(0).columnRanges; - result = intersectSlotRange(result, slotRanges, slot, unionLiteralRange); + result = intersectSlotRange(result, exprRanges, compareExpr, unionLiteralRange); } result = result.withRejectNot(false); return result; @@ -400,11 +396,10 @@ public EvaluateRangeResult visitIsNull(IsNull isNull, EvaluateRangeInput context } result = result.withRejectNot(false); Expression child = isNull.child(); - if (!(child instanceof Slot) || !isPartitionSlot((Slot) child)) { + if (!partitionSlotContainsNull.containsKey(child)) { return result; } - - if (!partitionSlotContainsNull.get((Slot) child)) { + if (!partitionSlotContainsNull.get(child)) { return new EvaluateRangeResult(BooleanLiteral.FALSE, result.columnRanges, result.childrenResult, false); } @@ -416,7 +411,15 @@ public EvaluateRangeResult visitAnd(And and, EvaluateRangeInput context) { EvaluateRangeResult result = evaluateChildrenThenThis(and, context); result = mergeRanges(result.result, result.childrenResult.get(0), result.childrenResult.get(1), - (leftRange, rightRange) -> leftRange.intersect(rightRange)); + (leftRange, rightRange) -> { + if (leftRange == null) { + return rightRange; + } + if (rightRange == null) { + return leftRange; + } + return leftRange.intersect(rightRange); + }); result = returnFalseIfExistEmptyRange(result); if (result.result.equals(BooleanLiteral.FALSE)) { @@ -434,20 +437,29 @@ public EvaluateRangeResult visitOr(Or or, EvaluateRangeInput context) { EvaluateRangeResult result = evaluateChildrenThenThis(or, context); result = mergeRanges(result.result, result.childrenResult.get(0), result.childrenResult.get(1), - (leftRange, rightRange) -> leftRange.union(rightRange)); - return returnFalseIfExistEmptyRange(result); + (leftRange, rightRange) -> { + if (leftRange == null) { + return rightRange; + } + if (rightRange == null) { + return leftRange; + } + return leftRange.union(rightRange); + }); + return removeEmptyRange(result); } @Override public EvaluateRangeResult visitNot(Not not, EvaluateRangeInput context) { EvaluateRangeResult result = evaluateChildrenThenThis(not, context); if (result.isRejectNot() && !result.result.equals(BooleanLiteral.TRUE)) { - Map newRanges = Maps.newHashMap(); - for (Map.Entry entry : result.childrenResult.get(0).columnRanges.entrySet()) { - Slot slot = entry.getKey(); + Map newRanges = Maps.newHashMap(); + for (Map.Entry entry : result.childrenResult.get(0).columnRanges.entrySet()) { + Expression expr = entry.getKey(); ColumnRange childRange = entry.getValue(); - ColumnRange partitionRange = result.columnRanges.get(slot); - newRanges.put(slot, partitionRange.intersect(childRange.complete())); + ColumnRange partitionRange = rangeMap.containsKey(expr) + ? rangeMap.get(expr) : ColumnRange.all(); + newRanges.put(expr, partitionRange.intersect(childRange.complete())); } result = new EvaluateRangeResult(result.result, newRanges, result.childrenResult); } @@ -476,7 +488,7 @@ private EvaluateRangeResult evaluateChildrenThenThis(Expression expr, EvaluateRa // evaluate this expr = FoldConstantRuleOnFE.evaluate(expr, expressionRewriteContext); - return new EvaluateRangeResult(expr, context.defaultColumnRanges, childrenResults); + return new EvaluateRangeResult(expr, ImmutableMap.of(), childrenResults); } private EvaluateRangeResult returnFalseIfExistEmptyRange(EvaluateRangeResult result) { @@ -489,11 +501,11 @@ private EvaluateRangeResult returnFalseIfExistEmptyRange(EvaluateRangeResult res } private EvaluateRangeResult intersectSlotRange(EvaluateRangeResult originResult, - Map columnRanges, Slot slot, ColumnRange otherRange) { - ColumnRange columnRange = columnRanges.get(slot); + Map columnRanges, Expression expr, ColumnRange otherRange) { + ColumnRange columnRange = columnRanges.get(expr); ColumnRange intersect = columnRange.intersect(otherRange); - Map newColumnRanges = replaceSlotRange(columnRanges, slot, intersect); + Map newColumnRanges = replaceExprRange(columnRanges, expr, intersect); if (intersect.isEmptyRange()) { return new EvaluateRangeResult(BooleanLiteral.FALSE, newColumnRanges, originResult.childrenResult); @@ -513,6 +525,9 @@ private EvaluateRangeResult determinateRangeOfOtherType( 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; + } switch (partitionSlotType) { case CONST: continue; case RANGE: @@ -547,7 +562,7 @@ private EvaluateRangeResult determinateRangeOfOtherType( ColumnRange origin = context.columnRanges.get(qualifiedSlot); ColumnRange newRange = origin.intersect(qualifiedRange); - Map newRanges = replaceSlotRange(context.columnRanges, qualifiedSlot, newRange); + Map newRanges = replaceExprRange(context.columnRanges, qualifiedSlot, newRange); if (newRange.isEmptyRange()) { return new EvaluateRangeResult(BooleanLiteral.FALSE, newRanges, context.childrenResult); @@ -558,9 +573,10 @@ private EvaluateRangeResult determinateRangeOfOtherType( return context; } - private Map replaceSlotRange(Map originRange, Slot slot, ColumnRange range) { - LinkedHashMap newRanges = Maps.newLinkedHashMap(originRange); - newRanges.put(slot, range); + private Map replaceExprRange(Map originRange, Expression expr, + ColumnRange range) { + LinkedHashMap newRanges = Maps.newLinkedHashMap(originRange); + newRanges.put(expr, range); return ImmutableMap.copyOf(newRanges); } @@ -568,20 +584,19 @@ private EvaluateRangeResult mergeRanges( Expression originResult, EvaluateRangeResult left, EvaluateRangeResult right, BiFunction mergeFunction) { - Map leftRanges = left.columnRanges; - Map rightRanges = right.columnRanges; + Map leftRanges = left.columnRanges; + Map rightRanges = right.columnRanges; if (leftRanges.equals(rightRanges)) { return new EvaluateRangeResult(originResult, leftRanges, ImmutableList.of(left, right)); } - - Set slots = ImmutableSet.builder() + Set exprs = ImmutableSet.builder() .addAll(leftRanges.keySet()) .addAll(rightRanges.keySet()) .build(); - Map mergedRange = slots.stream() - .map(slot -> Pair.of(slot, mergeFunction.apply(leftRanges.get(slot), rightRanges.get(slot)))) + Map mergedRange = exprs.stream() + .map(expr -> Pair.of(expr, mergeFunction.apply(leftRanges.get(expr), rightRanges.get(expr)))) .collect(ImmutableMap.toImmutableMap(Pair::key, Pair::value)); return new EvaluateRangeResult(originResult, mergedRange, ImmutableList.of(left, right)); } @@ -616,6 +631,19 @@ private List toMultiNereidsLiterals(PartitionKey partitionKey) { return literals; } + @Override + public EvaluateRangeResult visitDateTrunc(DateTrunc dateTrunc, EvaluateRangeInput context) { + EvaluateRangeResult result = super.visitDateTrunc(dateTrunc, context); + if (!(result.result instanceof DateTrunc)) { + return result; + } + Expression dateTruncChild = dateTrunc.child(0); + if (partitionSlotContainsNull.containsKey(dateTruncChild)) { + partitionSlotContainsNull.put(dateTrunc, true); + } + return computeMonotonicFunctionRange(result); + } + @Override public EvaluateRangeResult visitDate(Date date, EvaluateRangeInput context) { EvaluateRangeResult result = super.visitDate(date, context); @@ -665,13 +693,13 @@ private Optional getPartitionSlotType(Slot slot) { private Map fillSlotRangesToInputs( Map inputs) { - Builder allColumnRangesBuilder = + Builder allColumnRangesBuilder = ImmutableMap.builderWithExpectedSize(16); for (Entry entry : inputs.entrySet()) { allColumnRangesBuilder.put(entry.getKey(), entry.getValue().columnRanges.get(entry.getKey())); } - Map allColumnRanges = allColumnRangesBuilder.build(); + Map allColumnRanges = allColumnRangesBuilder.build(); Builder partitionSlotInputs = ImmutableMap.builderWithExpectedSize(16); @@ -683,12 +711,9 @@ private Map fillSlotRangesToInputs( /** EvaluateRangeInput */ public static class EvaluateRangeInput { - private Map defaultColumnRanges; private Map slotToInput; - public EvaluateRangeInput(Map defaultColumnRanges, - Map slotToInput) { - this.defaultColumnRanges = defaultColumnRanges; + public EvaluateRangeInput(Map slotToInput) { this.slotToInput = slotToInput; } } @@ -702,7 +727,8 @@ public EvaluateRangeInput(Map defaultColumnRanges, */ public static class EvaluateRangeResult { private final Expression result; - private final Map columnRanges; + private final Map columnRanges; + // private final Map columnRanges; private final List childrenResult; // rejectNot = true, if \exist e \in R, pred(e)=true, then we have \forAll e \in R, !pred(e)=false @@ -717,7 +743,7 @@ public static class EvaluateRangeResult { // R=(1,10), pred: k < 11. "k<11" holds true over R, and "NOT k<11" dose not hold over R private final boolean rejectNot; - public EvaluateRangeResult(Expression result, Map columnRanges, + public EvaluateRangeResult(Expression result, Map columnRanges, List childrenResult, boolean rejectNot) { this.result = result; this.columnRanges = columnRanges; @@ -725,7 +751,7 @@ public EvaluateRangeResult(Expression result, Map columnRange this.rejectNot = rejectNot; } - public EvaluateRangeResult(Expression result, Map columnRanges, + public EvaluateRangeResult(Expression result, Map columnRanges, List childrenResult) { this(result, columnRanges, childrenResult, allIsRejectNot(childrenResult)); } @@ -757,7 +783,7 @@ private List> computeSinglePartitionValueInputs() Slot partitionSlot = partitionSlots.get(0); Literal literal = (Literal) inputs.get(0).get(0); ColumnRange slotRange = ColumnRange.singleton(literal); - ImmutableMap slotToRange = ImmutableMap.of(partitionSlot, slotRange); + ImmutableMap slotToRange = ImmutableMap.of(partitionSlot, slotRange); Map slotToInputs = ImmutableMap.of(partitionSlot, new PartitionSlotInput(literal, slotToRange)); return ImmutableList.of(slotToInputs); @@ -810,7 +836,7 @@ private List> commonComputeOnePartitionInputs() { previousIsLowerBoundLiteral = false; previousIsUpperBoundLiteral = false; } - ImmutableMap slotToRange = ImmutableMap.of(partitionSlot, slotRange); + ImmutableMap slotToRange = ImmutableMap.of(partitionSlot, slotRange); slotToInputs.put(partitionSlot, new PartitionSlotInput(expression, slotToRange)); } @@ -819,4 +845,63 @@ private List> commonComputeOnePartitionInputs() { } return onePartitionInputs; } + + private EvaluateRangeResult removeEmptyRange(EvaluateRangeResult result) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : result.columnRanges.entrySet()) { + if (entry.getValue().isEmptyRange()) { + continue; + } + builder.put(entry); + } + return new EvaluateRangeResult(result.result, builder.build(), result.childrenResult); + } + + private EvaluateRangeResult computeMonotonicFunctionRange(EvaluateRangeResult result) { + Monotonic func = (Monotonic) result.result; + if (rangeMap.containsKey(func)) { + return new EvaluateRangeResult((Expression) func, ImmutableMap.of((Expression) func, + rangeMap.get(func)), result.childrenResult); + } + int childIndex = func.getMonotonicFunctionChildIndex(); + Expression funcChild = func.child(childIndex); + if (!result.childrenResult.get(0).columnRanges.containsKey(funcChild)) { + return result; + } + ColumnRange childRange = result.childrenResult.get(0).columnRanges.get(funcChild); + if (childRange.isEmptyRange() || childRange.asRanges().size() != 1 + || (!childRange.span().hasLowerBound() && !childRange.span().hasUpperBound())) { + return result; + } + Range span = childRange.span(); + Literal lower = span.hasLowerBound() ? span.lowerEndpoint().getValue() : null; + Literal upper = span.hasUpperBound() ? span.upperEndpoint().getValue() : null; + Expression lowerValue = lower != null ? FoldConstantRuleOnFE.evaluate(func.withConstantArgs(lower), + expressionRewriteContext) : null; + Expression upperValue = upper != null ? FoldConstantRuleOnFE.evaluate(func.withConstantArgs(upper), + expressionRewriteContext) : null; + if (!func.isPositive()) { + Expression temp = lowerValue; + lowerValue = upperValue; + upperValue = temp; + } + LinkedHashMap newRanges = Maps.newLinkedHashMap(); + ColumnRange newRange = ColumnRange.all(); + if (lowerValue instanceof Literal && upperValue instanceof Literal && lowerValue.equals(upperValue)) { + newRange = ColumnRange.singleton((Literal) lowerValue); + rangeMap.put((Expression) func, newRange); + newRanges.put((Expression) func, newRange); + return new EvaluateRangeResult(lowerValue, newRanges, result.childrenResult); + } else { + if (lowerValue instanceof Literal) { + newRange = newRange.withLowerBound((Literal) lowerValue); + } + if (upperValue instanceof Literal) { + newRange = newRange.withUpperBound((Literal) upperValue); + } + rangeMap.put((Expression) func, newRange); + newRanges.put((Expression) func, newRange); + return new EvaluateRangeResult((Expression) func, newRanges, result.childrenResult); + } + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionSlotInput.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionSlotInput.java index 44aaeade53d1bb..994ae477edae22 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionSlotInput.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionSlotInput.java @@ -18,7 +18,6 @@ package org.apache.doris.nereids.rules.expression.rules; import org.apache.doris.nereids.trees.expressions.Expression; -import org.apache.doris.nereids.trees.expressions.Slot; import com.google.common.collect.ImmutableMap; @@ -116,9 +115,9 @@ public class PartitionSlotInput { // part_column1 IntegerLiteral(100) // // because we can't fold this predicate to BooleanLiteral.FALSE, so we should scan the partition. - public final Map columnRanges; + public final Map columnRanges; - public PartitionSlotInput(Expression result, Map columnRanges) { + public PartitionSlotInput(Expression result, Map columnRanges) { this.result = result; this.columnRanges = ImmutableMap.copyOf(columnRanges); } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Monotonic.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Monotonic.java new file mode 100644 index 00000000000000..2fdde0e7415613 --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Monotonic.java @@ -0,0 +1,36 @@ +// 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.trees.expressions.functions; + +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.literal.Literal; + +/** monotonicity of expressions */ +public interface Monotonic extends ExpressionTrait { + // true means that the function is an increasing function + boolean isPositive(); + + // return the range input child index + // e.g. date_trunc(dt,'xxx') return 0 + int getMonotonicFunctionChildIndex(); + + // return the function with the arguments replaced by literal + // e.g. date_trunc(dt, 'day'), dt in range ['2020-01-01 10:00:00', '2020-01-03 10:00:00'] + // return date_trunc('2020-01-01 10:00:00', 'day') + Expression withConstantArgs(Literal literal); +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DateTrunc.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DateTrunc.java index 743976c6c1afcb..cbd2da5627b375 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DateTrunc.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DateTrunc.java @@ -22,6 +22,8 @@ import org.apache.doris.nereids.trees.expressions.Expression; import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable; import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature; +import org.apache.doris.nereids.trees.expressions.functions.Monotonic; +import org.apache.doris.nereids.trees.expressions.literal.Literal; import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral; import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression; import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor; @@ -41,7 +43,7 @@ * ScalarFunction 'date_trunc'. This class is generated by GenerateFunction. */ public class DateTrunc extends ScalarFunction - implements BinaryExpression, ExplicitlyCastableSignature, AlwaysNullable { + implements BinaryExpression, ExplicitlyCastableSignature, AlwaysNullable, Monotonic { public static final List SIGNATURES = ImmutableList.of( FunctionSignature.ret(DateTimeV2Type.SYSTEM_DEFAULT) @@ -91,4 +93,19 @@ public List getSignatures() { public R accept(ExpressionVisitor visitor, C context) { return visitor.visitDateTrunc(this, context); } + + @Override + public boolean isPositive() { + return true; + } + + @Override + public int getMonotonicFunctionChildIndex() { + return 0; + } + + @Override + public Expression withConstantArgs(Literal literal) { + return new DateTrunc(literal, child(1)); + } } diff --git a/regression-test/suites/nereids_rules_p0/partition_prune/test_date_trunc_prune.groovy b/regression-test/suites/nereids_rules_p0/partition_prune/test_date_trunc_prune.groovy new file mode 100644 index 00000000000000..c4769b12b12e04 --- /dev/null +++ b/regression-test/suites/nereids_rules_p0/partition_prune/test_date_trunc_prune.groovy @@ -0,0 +1,296 @@ +// 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_date_trunc_prune") { + sql "SET enable_nereids_planner=true" + sql "SET enable_fallback_to_original_planner=false" + sql "drop table if exists mal_test_partition_range5" + sql""" + CREATE TABLE `mal_test_partition_range5` ( + `a` INT NULL, + `b` datetime not NULL, + `c` INT NULL + ) ENGINE=OLAP + DUPLICATE KEY(`a`, `b`, `c`) + PARTITION BY RANGE(`b`) + (PARTITION p1 VALUES [("2020-01-05 10:00:00"), ("2020-01-09 10:00:00")), + PARTITION p2 VALUES [("2020-01-09 10:00:00"), ("2020-01-13 10:00:00")), + PARTITION p3 VALUES [("2020-01-13 10:00:00"), ("2020-01-19 10:00:00"))) + DISTRIBUTED BY HASH(`a`) BUCKETS 10 + PROPERTIES ( + "replication_allocation" = "tag.location.default: 1" + );""" + sql """insert into mal_test_partition_range5 values(1,"2020-01-09 09:00:00",4),(1,"2020-01-09 11:00:00",4), + (1,"2020-01-13 11:00:00",4),(1,"2020-01-13 09:00:00",4)""" + // > >= < <= = <=> + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')<="2020-01-08" """ + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')<"2020-01-08" """ + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-08">=date_trunc(b,'day')""" + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-08" > date_trunc(b,'day')""" + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')="2020-01-08" """ + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-08" = date_trunc(b,'day')""" + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')>="2020-01-14" """ + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')>"2020-01-14" """ + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-14"<=date_trunc(b,'day')""" + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-14" < date_trunc(b,'day')""" + contains("partitions=1/3 (p3)") + } + + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') in ("2020-01-13 00:00:00")""" + contains("partitions=2/3 (p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where "2020-01-14" <=> date_trunc(b,'day')""" + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') <=>"2020-01-14" """ + contains("partitions=1/3 (p3)") + } + + // and or + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')>"2020-01-09" and date_trunc(b,'day') <"2020-01-13" """ + contains("partitions=1/3 (p2)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')>"2020-01-09" or date_trunc(b,'day') <"2020-01-13" """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')>"2020-01-14" or date_trunc(b,'day') <"2020-01-06" """ + contains("partitions=2/3 (p1,p3)") + } + + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') between "2020-01-09" and "2020-01-13" """ + contains("partitions=3/3 (p1,p2,p3)") + + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') between "2020-01-10" and "2020-01-14" """ + contains("partitions=2/3 (p2,p3)") + + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') between "2020-01-10" and "2020-01-12" """ + contains("partitions=1/3 (p2)") + } + + // test not + for (int i = 0; i < 2; i++) { + if (i == 0) { + // forbid rewrite not a>1 to a<=1 + sql "set disable_nereids_rules = 'REWRITE_FILTER_EXPRESSION'" + } else { + sql "set disable_nereids_rules = ''" + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day')<="2020-01-14" """ + contains("partitions=1/3 (p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where not date_trunc(b,'day')<"2020-01-14" """ + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not "2020-01-14">=date_trunc(b,'day')""" + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not "2020-01-14" > date_trunc(b,'day')""" + contains("partitions=1/3 (p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day')="2020-01-08" """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not "2020-01-08" = date_trunc(b,'day')""" + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day')>="2020-01-9" """ + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day')>"2020-01-9" """ + contains("partitions=2/3 (p1,p2)") + } + explain { + sql """select * from mal_test_partition_range5 where not "2020-01-9"<=date_trunc(b,'day')""" + contains("partitions=1/3 (p1)") + } + explain { + sql """select * from mal_test_partition_range5 where not "2020-01-9" < date_trunc(b,'day')""" + contains("partitions=2/3 (p1,p2)") + } + explain { + sql """ select * from mal_test_partition_range5 where not date_trunc(b,'day') in ("2020-01-13 00:00:00")""" + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where not "2020-01-14" <=> date_trunc(b,'day')""" + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day') <=>"2020-01-14" """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where not (date_trunc(b,'day')>"2020-01-09" and date_trunc(b,'day') <"2020-01-13") """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where not (date_trunc(b,'day')>="2020-01-13" or date_trunc(b,'day') <="2020-01-9") """ + contains("partitions=1/3 (p2)") + } + explain { + sql """ select * from mal_test_partition_range5 where not date_trunc(b,'day')<="2020-01-14" or date_trunc(b,'day') <"2020-01-06" """ + contains("partitions=2/3 (p1,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day') between "2020-01-09" and "2020-01-13" """ + contains("partitions=2/3 (p1,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where not date_trunc(b,'day') between "2020-01-10" and "2020-01-14" """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day') not between "2020-01-4" and "2020-01-15" """ + contains("partitions=1/3 (p3)") + } + } + + // trunc(b) and b + explain { + sql """select * from mal_test_partition_range5 where (not date_trunc(b,'day')<="2020-01-14") or b<"2020-01-9" """ + contains("partitions=2/3 (p1,p3)") + } + explain { + sql """select * from mal_test_partition_range5 where date_trunc(b,'day')<"2020-01-13" and b>"2020-01-10" """ + contains("partitions=1/3 (p2)") + } + explain { + sql """ select * from mal_test_partition_range5 where date_trunc(b,'day')<"2020-01-13" and b>"2020-01-9" """ + contains("partitions=2/3 (p1,p2)") + } + explain { + sql """ select * from mal_test_partition_range5 where ( date_trunc(b,'day')<="2020-01-14" and b >"2020-01-12") or b<"2020-01-9" """ + contains("partitions=3/3 (p1,p2,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where ( date_trunc(b,'day')<="2020-01-14" and b >"2020-01-19") or b<"2020-01-9" """ + contains("partitions=2/3 (p1,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where ( date_trunc(b,'day')<="2020-01-14" and b >"2020-01-20") or b<"2020-01-9" """ + contains("partitions=1/3 (p1)") + } + explain { + sql """ select * from mal_test_partition_range5 where ( not date_trunc(b,'day')<="2020-01-14" or b >"2020-01-20") or b<"2020-01-9" """ + contains("partitions=2/3 (p1,p3)") + } + explain { + sql """ select * from mal_test_partition_range5 where ( date_trunc(b,'day') between "2020-01-14" and "2020-01-20") or b<"2020-01-9" """ + contains("partitions=2/3 (p1,p3)") + } + + // is null, can support but now not + sql "drop table if exists null_range_date" + sql """ + create table null_range_date( + k0 datetime null + ) + partition by range (k0) + ( + PARTITION p10 values less than ('2022-01-01 10:00:00'), + PARTITION p100 values less than ('2022-01-04 10:00:00'), + PARTITION pMAX values less than (maxvalue) + ) + DISTRIBUTED BY HASH(`k0`) BUCKETS 1 properties("replication_num"="1") + """ + sql "insert into null_range_date values('2022-01-03 10:00:00'),('2019-01-01 10:00:00'),('2022-01-02 10:00:00'),('2024-01-01 10:00:00'),(null);" + explain { + sql "select * from null_range_date where date_trunc(k0,'day') is null" + contains("partitions=3/3 (p10,p100,pMAX)") + } + // test infinite range + explain { + sql "select * from null_range_date where date_trunc(k0,'day') <'2022-1-3'" + contains("partitions=2/3 (p10,p100)") + } + explain { + sql "select * from null_range_date where date_trunc(k0,'day') >'2022-1-3'" + contains("partitions=2/3 (p100,pMAX)") + } + + sql "drop table if exists mal_test_partition_range2_two_date_int" + sql """CREATE TABLE `mal_test_partition_range2_two_date_int` ( + `dt` DATETIME NULL, + `id` INT NULL, + `c` INT NULL + ) ENGINE=OLAP + DUPLICATE KEY(`dt`, `id`, `c`) + PARTITION BY RANGE(`dt`, `id`) + (PARTITION p201701_1000 VALUES [('0000-01-01', "-2147483648"), ('2017-02-01', "1000")), + PARTITION p201702_2000 VALUES [('2017-02-01', "1000"), ('2017-03-01', "2000")), + PARTITION p201703_all VALUES [('2017-03-01', "2000"), ('2017-04-01', "-2147483648"))) + DISTRIBUTED BY HASH(`dt`) BUCKETS 10 + PROPERTIES ( + "replication_allocation" = "tag.location.default: 1" + );""" + sql """insert into mal_test_partition_range2_two_date_int values('2017-01-03 10:00:00', 3,23),('2017-02-04 10:00:00', 333,4),('2017-03-05 10:00:00', 1222,6);""" + explain { + sql """select * from mal_test_partition_range2_two_date_int where date_trunc(dt,'month') = '2017-2-1 00:00:00' and id>0 ;""" + contains ("partitions=2/3 (p201701_1000,p201702_2000)") + } + explain { + sql "select * from mal_test_partition_range2_two_date_int where date_trunc(dt,'month') > '2017-2-1 00:00:00' and id>100;" + contains("partitions=2/3 (p201702_2000,p201703_all)") + } +} \ No newline at end of file