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

jacktengg 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 19dfb8711ae [fix](partition prunning) Correct partition pruning for 
calendar arithmetic (#69828)
19dfb8711ae is described below

commit 19dfb8711ae3dbb8f56cc945f5dc7af20a844607
Author: TengJianPing <[email protected]>
AuthorDate: Wed Nov 4 21:02:46 2026 +0800

    [fix](partition prunning) Correct partition pruning for calendar arithmetic 
(#69828)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: Calendar month, quarter, year and YEAR_MONTH additions
    and subtractions clamp dates at month end while preserving the time of
    day. Consecutive input dates can therefore produce decreasing datetime
    values. Treating these functions as globally monotonic lets RANGE
    partition pruning remove partitions containing matching rows, such as
    January 29 filtered by months_add(datetime_column, 1) < '2023-02-28
    10:00:00'. Share a calendar arithmetic monotonicity trait that retains
    the optimization for DATE and single-day ranges and keeps other datetime
    ranges conservatively. This also covers DATE_ADD, DATE_SUB,
    TIMESTAMPADD, INTERVAL expressions and runtime-filter classification.
    
    MONTHS_DIFF, QUARTERS_DIFF and YEARS_DIFF also fold some month-end
    datetime arguments differently from BE because LocalDateTime.until
    adjusts the end date before counting calendar units. Count date units
    first, then compare time parts only when the remaining date fields
    match, preserving BE's truncation semantics for both argument orders and
    all supported date-like literal types.
    
    ### Release note
    
    Fix missing query rows caused by calendar arithmetic partition pruning
    and align month, quarter and year difference constant folding with
    runtime results.
    
    ### Check List (For Author)
    
    - Test: FE build with Checkstyle; 94 FE unit tests; regression tests
    - Unit Test: CalendarAddSubMonotonicTest,
    DateTimeExtractAndTransformTest, PartitionPrunerTest,
    RuntimeFilterPruneClassifierTest, TimestampNsMonotonicityTest and
    InferPredicateFromMonotonicFunctionTest
    - Regression test: test_calendar_arithmetic_prune (73 generated result
    checks) and test_date_function_prune; verify the new suite fails on the
    original FE
    - Behavior changed: Yes, retain matching rows and correct calendar
    difference constants
    - Does this need documentation: No
    
    ### What problem does this PR solve?
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../rules/OneRangePartitionEvaluator.java          |  72 ++-----
 .../functions/CalendarAddSubMonotonic.java         |  87 ++++++++
 .../trees/expressions/functions/Monotonic.java     |  25 ++-
 .../expressions/functions/RangeInferable.java      |  72 +++++++
 .../executable/DateTimeExtractAndTransform.java    |  49 +++--
 .../expressions/functions/scalar/MonthsAdd.java    |   4 +-
 .../expressions/functions/scalar/MonthsSub.java    |   4 +-
 .../expressions/functions/scalar/QuartersAdd.java  |   4 +-
 .../expressions/functions/scalar/QuartersSub.java  |   4 +-
 .../expressions/functions/scalar/YearMonthAdd.java |   4 +-
 .../expressions/functions/scalar/YearMonthSub.java |   4 +-
 .../expressions/functions/scalar/YearsAdd.java     |   4 +-
 .../expressions/functions/scalar/YearsSub.java     |   4 +-
 .../nereids/rules/rewrite/PartitionPrunerTest.java |  89 ++++++++
 .../expressions/functions/RangeInferableTest.java  | 112 ++++++++++
 .../DateTimeExtractAndTransformTest.java           |  43 ++++
 .../scalar/CalendarAddSubMonotonicTest.java        | 235 +++++++++++++++++++++
 .../test_calendar_arithmetic_prune.out             | 220 +++++++++++++++++++
 .../test_calendar_arithmetic_prune.groovy          | 157 ++++++++++++++
 19 files changed, 1102 insertions(+), 91 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 d5c6f7debdf..6fe4b1db6f1 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
@@ -40,13 +40,12 @@ import org.apache.doris.nereids.trees.expressions.Not;
 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.RangeInferable;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.NonNullable;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
 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.MaxLiteral;
-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.util.ExpressionUtils;
@@ -69,6 +68,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiFunction;
 
@@ -532,8 +532,8 @@ public class OneRangePartitionEvaluator<K>
         for (int i = 0; i < children.size(); i++) {
             Expression child = children.get(i);
             EvaluateRangeResult childResult;
-            if (child instanceof Monotonic) {
-                childResult = visitMonotonic(child, context);
+            if (child instanceof RangeInferable) {
+                childResult = visitRangeInferable(child, context);
             } else {
                 childResult = child.accept(this, context);
             }
@@ -1005,21 +1005,21 @@ public class OneRangePartitionEvaluator<K>
         }
     }
 
-    public EvaluateRangeResult visitMonotonic(Expression monotonic, 
EvaluateRangeInput context) {
-        EvaluateRangeResult rangeResult = evaluateChildrenThenThis(monotonic, 
context);
-        if (!rangeResult.result.getClass().equals(monotonic.getClass())) {
+    public EvaluateRangeResult visitRangeInferable(Expression expression, 
EvaluateRangeInput context) {
+        EvaluateRangeResult rangeResult = evaluateChildrenThenThis(expression, 
context);
+        if (!rangeResult.result.getClass().equals(expression.getClass())) {
             return rangeResult;
         }
-        return computeMonotonicFunctionRange(rangeResult, context);
+        return computeFunctionRange(rangeResult, context);
     }
 
-    private EvaluateRangeResult 
computeMonotonicFunctionRange(EvaluateRangeResult result, EvaluateRangeInput 
context) {
-        Monotonic func = (Monotonic) result.result;
+    private EvaluateRangeResult computeFunctionRange(EvaluateRangeResult 
result, EvaluateRangeInput context) {
+        RangeInferable func = (RangeInferable) result.result;
         if (context.rangeMap.containsKey(func)) {
             return new EvaluateRangeResult((Expression) func, 
ImmutableMap.of((Expression) func,
                     context.rangeMap.get(func)), result.childrenResult);
         }
-        int childIndex = func.getMonotonicFunctionChildIndex();
+        int childIndex = func.getRangeInputChildIndex();
         Expression funcChild = func.child(childIndex);
         boolean isNullable = partitionSlotContainsNull.getOrDefault(funcChild, 
true);
         Expression withNullable = func.withConstantArgs(isNullable ? new 
Nullable(funcChild)
@@ -1039,51 +1039,17 @@ public class OneRangePartitionEvaluator<K>
         Literal lower = span.hasLowerBound() ? span.lowerEndpoint().getValue() 
: null;
         Literal upper = span.hasUpperBound() && 
!(span.upperEndpoint().getValue() instanceof MaxLiteral)
                 ? span.upperEndpoint().getValue() : null;
-        if (!func.isMonotonic(lower, upper)) {
+        Optional<ColumnRange> inferredRange = func.inferRange(lower, upper, 
expressionRewriteContext);
+        if (!inferredRange.isPresent()) {
             return result;
         }
-        Expression lowerValue = lower != null ? 
FoldConstantRuleOnFE.evaluate(func.withConstantArgs(lower),
-                expressionRewriteContext) : null;
-        Expression upperValue = upper != null ? 
FoldConstantRuleOnFE.evaluate(func.withConstantArgs(upper),
-                expressionRewriteContext) : null;
-        if (!checkFoldConstantValueIsValid(lowerValue, upperValue)) {
-            return result;
-        }
-
-        if (!func.isPositive()) {
-            Expression temp = lowerValue;
-            lowerValue = upperValue;
-            upperValue = temp;
-        }
-        LinkedHashMap<Expression, ColumnRange> newRanges = 
Maps.newLinkedHashMap();
-        ColumnRange newRange = ColumnRange.all();
-        if (lowerValue instanceof Literal && upperValue instanceof Literal && 
lowerValue.equals(upperValue)) {
-            newRange = ColumnRange.singleton((Literal) lowerValue);
-            context.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);
-            }
-            if (newRange.isEmptyRange() || !newRange.span().hasLowerBound() && 
!newRange.span().hasUpperBound()) {
-                return result;
-            }
-            context.rangeMap.put((Expression) func, newRange);
-            newRanges.put((Expression) func, newRange);
-            return new EvaluateRangeResult((Expression) func, newRanges, 
result.childrenResult);
-        }
-    }
 
-    // only allow literal(except NullLiteral) and null
-    private boolean checkFoldConstantValueIsValid(Expression lowerValue, 
Expression upperValue) {
-        if (lowerValue instanceof NullLiteral || upperValue instanceof 
NullLiteral) {
-            return false;
+        ColumnRange newRange = inferredRange.get();
+        context.rangeMap.put((Expression) func, newRange);
+        Map<Expression, ColumnRange> newRanges = ImmutableMap.of((Expression) 
func, newRange);
+        if (newRange.isSingleton()) {
+            return new 
EvaluateRangeResult(newRange.getLowerBound().getValue(), newRanges, 
result.childrenResult);
         }
-        return (lowerValue == null || lowerValue instanceof Literal)
-                && (upperValue == null || upperValue instanceof Literal);
+        return new EvaluateRangeResult((Expression) func, newRanges, 
result.childrenResult);
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarAddSubMonotonic.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarAddSubMonotonic.java
new file mode 100644
index 00000000000..c374ed2305c
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarAddSubMonotonic.java
@@ -0,0 +1,87 @@
+// 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.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
+import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Optional;
+
+/** Monotonicity of date arithmetic that can clamp the day to the end of a 
month. */
+public interface CalendarAddSubMonotonic extends DateAddSubMonotonic {
+    @Override
+    default boolean isMonotonic(Literal lower, Literal upper) {
+        // Clamping consecutive dates to the same date resets the time at 
midnight. DATE has no
+        // time part, and within one input day the calendar adjustment 
preserves the time order.
+        return canEvaluateRange()
+                && (child(0).getDataType().isDateV2Type() || 
MonotonicityUtils.isWithinSameDay(lower, upper));
+    }
+
+    /** The date part remains non-decreasing even when clamping resets the 
time part. */
+    default boolean canEvaluateRange() {
+        return MonotonicityUtils.hasConstantOtherArguments(this, 0);
+    }
+
+    @Override
+    default Optional<ColumnRange> inferRange(Literal lower, Literal upper, 
ExpressionRewriteContext context) {
+        if (!canEvaluateRange()) {
+            return Optional.empty();
+        }
+        boolean monotonic = isMonotonic(lower, upper);
+        return evaluateRangeEndpoints(lower, upper, context).flatMap(bounds -> 
RangeInferable.createRange(
+                monotonic || lower == null ? bounds.first
+                        : getRangeLowerBound((DateLiteral) lower, 
(DateLiteral) bounds.first),
+                monotonic || upper == null ? bounds.second
+                        : getRangeUpperBound((DateLiteral) upper, 
(DateLiteral) bounds.second)));
+    }
+
+    /** Later input days can map to this output date with an earlier time. */
+    default Literal getRangeLowerBound(DateLiteral input, DateLiteral output) {
+        if (input.getDay() < 
input.toJavaDateType().toLocalDate().lengthOfMonth()
+                && output.getDay() == 
output.toJavaDateType().toLocalDate().lengthOfMonth()) {
+            return withTime(output, LocalTime.MIN);
+        }
+        return output;
+    }
+
+    /** Earlier input days can map to a clamped output date with a later time. 
*/
+    default Literal getRangeUpperBound(DateLiteral input, DateLiteral output) {
+        if (output.getDay() < input.getDay()) {
+            return withTime(output, LocalTime.MAX);
+        }
+        return output;
+    }
+
+    private static Literal withTime(DateLiteral output, LocalTime time) {
+        LocalDateTime dateTime = output.toJavaDateType().with(time);
+        if (output instanceof TimeStampNsLiteral) {
+            return TimeStampNsLiteral.fromJavaDateType(dateTime);
+        }
+        if (output instanceof TimestampTzLiteral) {
+            return TimestampTzLiteral.fromJavaDateType(dateTime, 
((TimestampTzLiteral) output).getScale());
+        }
+        return DateTimeV2Literal.fromJavaDateType(dateTime, 
((DateTimeV2Literal) output).getDataType().getScale());
+    }
+}
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
index feec5933890..e819f3b511c 100644
--- 
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
@@ -17,11 +17,14 @@
 
 package org.apache.doris.nereids.trees.expressions.functions;
 
-import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 
+import java.util.Optional;
+
 /** monotonicity of expressions */
-public interface Monotonic extends ExpressionTrait {
+public interface Monotonic extends RangeInferable {
     default boolean isMonotonic(Literal lower, Literal upper) {
         return true;
     }
@@ -33,8 +36,18 @@ public interface Monotonic extends ExpressionTrait {
     // 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(Expression literal);
+    @Override
+    default int getRangeInputChildIndex() {
+        return getMonotonicFunctionChildIndex();
+    }
+
+    @Override
+    default Optional<ColumnRange> inferRange(Literal lower, Literal upper, 
ExpressionRewriteContext context) {
+        if (!isMonotonic(lower, upper)) {
+            return Optional.empty();
+        }
+        return evaluateRangeEndpoints(lower, upper, context).flatMap(bounds -> 
isPositive()
+                ? RangeInferable.createRange(bounds.first, bounds.second)
+                : RangeInferable.createRange(bounds.second, bounds.first));
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferable.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferable.java
new file mode 100644
index 00000000000..064f1158338
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferable.java
@@ -0,0 +1,72 @@
+// 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.common.Pair;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+
+import java.util.Optional;
+
+/** Infer an enclosing result range without requiring the expression to be 
monotonic. */
+public interface RangeInferable extends ExpressionTrait {
+    int getRangeInputChildIndex();
+
+    // Replace the range input with a constant while preserving the other 
arguments.
+    Expression withConstantArgs(Expression literal);
+
+    /**
+     * Return a range containing every possible result, or empty when no safe 
range can be inferred.
+     * A null lower or upper endpoint means that the input is unbounded on 
that side.
+     */
+    Optional<ColumnRange> inferRange(Literal lower, Literal upper, 
ExpressionRewriteContext context);
+
+    /** Fold the input endpoints, retaining null for an unbounded side, and 
reject SQL NULL or unfolded results. */
+    default Optional<Pair<Literal, Literal>> evaluateRangeEndpoints(
+            Literal lower, Literal upper, ExpressionRewriteContext context) {
+        Expression lowerValue = lower == null ? null
+                : FoldConstantRuleOnFE.evaluate(withConstantArgs(lower), 
context);
+        Expression upperValue = upper == null ? null
+                : FoldConstantRuleOnFE.evaluate(withConstantArgs(upper), 
context);
+        if (lowerValue instanceof NullLiteral || upperValue instanceof 
NullLiteral
+                || lowerValue != null && !(lowerValue instanceof Literal)
+                || upperValue != null && !(upperValue instanceof Literal)) {
+            return Optional.empty();
+        }
+        return Optional.of(Pair.of((Literal) lowerValue, (Literal) 
upperValue));
+    }
+
+    /** Build inclusive result bounds. Input endpoint exclusions need not hold 
after applying a function. */
+    static Optional<ColumnRange> createRange(Literal lower, Literal upper) {
+        ColumnRange range = ColumnRange.all();
+        if (lower != null) {
+            range = range.withLowerBound(lower);
+        }
+        if (upper != null) {
+            range = range.withUpperBound(upper);
+        }
+        if (range.isEmptyRange() || !range.span().hasLowerBound() && 
!range.span().hasUpperBound()) {
+            return Optional.empty();
+        }
+        return Optional.of(range);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
index 051a191d5fd..bf60ad786cb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
@@ -1652,82 +1652,99 @@ public class DateTimeExtractAndTransform {
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(DateTimeV2Literal t1, 
DateTimeV2Literal t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(TimestampTzLiteral t1, 
TimestampTzLiteral t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(TimeStampNsLiteral t1, 
TimeStampNsLiteral t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(DateTimeV2Literal t1, DateV2Literal 
t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(DateV2Literal t1, DateTimeV2Literal 
t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "months_diff")
     public static Expression monthsDiff(DateV2Literal t1, DateV2Literal t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS));
     }
 
     @ExecFunction(name = "quarters_diff")
     public static Expression quartersDiff(DateTimeV2Literal t1, 
DateTimeV2Literal t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()) / 3);
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS) / 3);
     }
 
     @ExecFunction(name = "quarters_diff")
     public static Expression quartersDiff(TimestampTzLiteral t1, 
TimestampTzLiteral t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()) / 3);
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS) / 3);
     }
 
     @ExecFunction(name = "quarters_diff")
     public static Expression quartersDiff(TimeStampNsLiteral t1, 
TimeStampNsLiteral t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()) / 3);
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS) / 3);
     }
 
     @ExecFunction(name = "quarters_diff")
     public static Expression quartersDiff(DateV2Literal t1, DateV2Literal t2) {
-        return new 
BigIntLiteral(ChronoUnit.MONTHS.between(t2.toJavaDateType(), 
t1.toJavaDateType()) / 3);
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.MONTHS) / 3);
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(DateTimeV2Literal t1, DateTimeV2Literal 
t2) {
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(TimestampTzLiteral t1, 
TimestampTzLiteral t2) {
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(TimeStampNsLiteral t1, 
TimeStampNsLiteral t2) {
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(DateTimeV2Literal t1, DateV2Literal t2) 
{
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(DateV2Literal t1, DateTimeV2Literal t2) 
{
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
     }
 
     @ExecFunction(name = "years_diff")
     public static Expression yearsDiff(DateV2Literal t1, DateV2Literal t2) {
-        return new BigIntLiteral(ChronoUnit.YEARS.between(t2.toJavaDateType(), 
t1.toJavaDateType()));
+        return new BigIntLiteral(dateDiffInCalendarUnits(t1, t2, 
ChronoUnit.YEARS));
+    }
+
+    private static long dateDiffInCalendarUnits(DateLiteral t1, DateLiteral 
t2, ChronoUnit unit) {
+        // LocalDateTime.until adjusts the end date by one day before counting 
months or years.
+        // At a month end that can remove an extra calendar unit, unlike BE's 
remainder comparison.
+        long diff = unit.between(t2.toJavaDateType().toLocalDate(), 
t1.toJavaDateType().toLocalDate());
+        boolean sameRemainderDate = t1.getDay() == t2.getDay()
+                && (unit == ChronoUnit.MONTHS || t1.getMonth() == 
t2.getMonth());
+        if (sameRemainderDate) {
+            long timeDiff = t1.getTimePartInNanoseconds() - 
t2.getTimePartInNanoseconds();
+            if (diff > 0 && timeDiff < 0) {
+                diff--;
+            } else if (diff < 0 && timeDiff > 0) {
+                diff++;
+            }
+        }
+        return diff;
     }
 
     /**
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsAdd.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsAdd.java
index e9f44840d78..000e8db3b34 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsAdd.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsAdd.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'months_add'.
  */
 public class MonthsAdd extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsSub.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsSub.java
index da58e6270a6..67b15a3e5c3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsSub.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthsSub.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'months_sub'.
  */
 public class MonthsSub extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersAdd.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersAdd.java
index 7e9865c4a5d..ec056f1779a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersAdd.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersAdd.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'quarters_add'.
  */
 public class QuartersAdd extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersSub.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersSub.java
index d56bab92048..13d81f08b89 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersSub.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuartersSub.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'quarters_sub'.
  */
 public class QuartersSub extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthAdd.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthAdd.java
index 49f52d7a6cc..0f42e3a8b8e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthAdd.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthAdd.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  */
 public class YearMonthAdd extends ScalarFunction
         implements BinaryExpression, ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
     public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             FunctionSignature.ret(DateTimeV2Type.WILDCARD)
                     .args(DateTimeV2Type.WILDCARD, VarcharType.SYSTEM_DEFAULT),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthSub.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthSub.java
index 2dd05536cf3..14233b1f408 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthSub.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearMonthSub.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  */
 public class YearMonthSub extends ScalarFunction
         implements BinaryExpression, ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
     public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             FunctionSignature.ret(DateTimeV2Type.WILDCARD)
                     .args(DateTimeV2Type.WILDCARD, VarcharType.SYSTEM_DEFAULT),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsAdd.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsAdd.java
index d09e3d4c517..c675b09e9f0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsAdd.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsAdd.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'years_add'.
  */
 public class YearsAdd extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsSub.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsSub.java
index cfd373ba70a..1f346f990bd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsSub.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearsSub.java
@@ -19,8 +19,8 @@ package 
org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ComputeSignatureForDateArithmetic;
-import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -40,7 +40,7 @@ import java.util.List;
  * ScalarFunction 'days_add'.
  */
 public class YearsSub extends ScalarFunction implements BinaryExpression, 
ExplicitlyCastableSignature,
-        ComputeSignatureForDateArithmetic, PropagateNullable, 
DateAddSubMonotonic {
+        ComputeSignatureForDateArithmetic, PropagateNullable, 
CalendarAddSubMonotonic {
 
     //ATTN: must place Datetime before Date, because for castring from string 
like literal, date and datetime has
     // the same precedence, but we prefer datetime to avoid data loss. for 
string literal which could cast to Date
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 f26c4701ce7..8b3c0bd408c 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
@@ -27,6 +27,8 @@ 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.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
 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;
@@ -45,8 +47,16 @@ 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.functions.RangeInferable;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Abs;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.MonthsAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.YearsAdd;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.VarcharType;
 import org.apache.doris.nereids.util.ExpressionUtils;
@@ -83,6 +93,85 @@ public class PartitionPrunerTest extends TestWithFeService {
         cascadesContext = createCascadesContext("select * from t1");
     }
 
+    @Test
+    public void testCalendarRangeBoundsRetainSafePartitionPruning() throws 
Exception {
+        SlotReference date = new SlotReference("dt", 
DateTimeV2Type.SYSTEM_DEFAULT);
+        Expression month = new MonthsAdd(date, new IntegerLiteral(1));
+        Expression year = new YearsAdd(date, new IntegerLiteral(1));
+        assertCalendarRangePruned(date, "2023-01-28 10:00:00", "2023-01-31 
23:59:59",
+                new LessThan(month, new DateTimeV2Literal("2023-02-28 
10:00:00")), false);
+        assertCalendarRangePruned(date, "2023-01-27 00:00:00", "2023-01-31 
00:00:00",
+                new GreaterThan(month, new DateTimeV2Literal("2023-02-28 
12:00:00")), false);
+        assertCalendarRangePruned(date, "2017-01-01 00:00:00", "2018-01-01 
00:00:00",
+                new GreaterThan(year, new DateTimeV2Literal("2019-01-01 
00:00:00")), true);
+        assertCalendarRangePruned(date, "2018-01-01 00:00:00", "2019-01-01 
00:00:00",
+                new GreaterThan(year, new DateTimeV2Literal("2019-01-01 
00:00:00")), false);
+        assertCalendarRangePruned(date, "2023-01-01 00:00:00", "2023-02-01 
00:00:00",
+                new EqualTo(month, new DateTimeV2Literal("2023-11-15 
10:00:00")), true);
+        assertCalendarRangePruned(date, "2023-10-01 00:00:00", "2023-11-01 
00:00:00",
+                new EqualTo(month, new DateTimeV2Literal("2023-11-15 
10:00:00")), false);
+    }
+
+    private void assertCalendarRangePruned(SlotReference date, String lower, 
String upper,
+            Expression predicate, boolean expectedPruned) throws Exception {
+        List<Column> columns = ImmutableList.of(new Column("dt", 
PrimitiveType.DATETIMEV2));
+        PartitionKey lowerKey = 
PartitionKey.createPartitionKey(ImmutableList.of(new PartitionValue(lower)), 
columns);
+        PartitionKey upperKey = 
PartitionKey.createPartitionKey(ImmutableList.of(new PartitionValue(upper)), 
columns);
+        OneRangePartitionEvaluator<String> evaluator = new 
OneRangePartitionEvaluator<>(
+                "p1", ImmutableList.of(date), new 
RangePartitionItem(Range.closedOpen(lowerKey, upperKey)),
+                cascadesContext, 0);
+        Pair<Boolean, Boolean> result =
+                (Pair<Boolean, Boolean>) canBePrunedOutMethod.invoke(null, 
predicate, evaluator);
+        Assertions.assertEquals(expectedPruned, result.first, 
predicate.toSql());
+    }
+
+    @Test
+    public void testRangeInferenceWithoutMonotonicityInterface() throws 
Exception {
+        PartitionKey lower = 
PartitionKey.createPartitionKey(ImmutableList.of(new PartitionValue("-5")),
+                ImmutableList.of(partitionColumn));
+        PartitionKey upper = 
PartitionKey.createPartitionKey(ImmutableList.of(new PartitionValue("11")),
+                ImmutableList.of(partitionColumn));
+        Expression absolute = new RangeOnlyAbs(slotA);
+        for (int value : new int[] {5, 12}) {
+            OneRangePartitionEvaluator<String> evaluator = new 
OneRangePartitionEvaluator<>(
+                    "p1", ImmutableList.of(slotA), new 
RangePartitionItem(Range.closedOpen(lower, upper)),
+                    cascadesContext, 0);
+            Expression predicate = new EqualTo(absolute, new 
BigIntLiteral(value));
+            Pair<Boolean, Boolean> result =
+                    (Pair<Boolean, Boolean>) canBePrunedOutMethod.invoke(null, 
predicate, evaluator);
+            Assertions.assertEquals(value == 12, result.first);
+        }
+    }
+
+    // Test a range capability that does not implement Monotonic and crosses 
ABS's turning point.
+    private static class RangeOnlyAbs extends Abs implements RangeInferable {
+        RangeOnlyAbs(Expression child) {
+            super(child);
+        }
+
+        @Override
+        public int getRangeInputChildIndex() {
+            return 0;
+        }
+
+        @Override
+        public Expression withConstantArgs(Expression literal) {
+            return new Abs(literal);
+        }
+
+        @Override
+        public RangeOnlyAbs withChildren(List<Expression> children) {
+            return new RangeOnlyAbs(children.get(0));
+        }
+
+        @Override
+        public Optional<ColumnRange> inferRange(Literal lower, Literal upper, 
ExpressionRewriteContext context) {
+            long max = Math.max(Math.abs(((IntegerLiteral) 
lower).getIntValue()),
+                    Math.abs(((IntegerLiteral) upper).getIntValue()));
+            return Optional.of(ColumnRange.between(new BigIntLiteral(0), new 
BigIntLiteral(max)));
+        }
+    }
+
     // test canBePrunedOut res
     // list partition p1, partition value is 1
     // predicate: a = 1
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferableTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferableTest.java
new file mode 100644
index 00000000000..a1848652946
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/RangeInferableTest.java
@@ -0,0 +1,112 @@
+// 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.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.DaysAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.MonthsAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.MonthsDiff;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.types.DateTimeV2Type;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.qe.ConnectContext;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+class RangeInferableTest {
+    private final SlotReference date = new SlotReference("dt", 
DateTimeV2Type.SYSTEM_DEFAULT);
+    private ConnectContext previousContext;
+    private ExpressionRewriteContext context;
+
+    @BeforeEach
+    void setUp() {
+        previousContext = ConnectContext.get();
+        new ConnectContext().setThreadLocalInfo();
+        context = new 
ExpressionRewriteContext(CascadesContext.initTempContext());
+    }
+
+    @AfterEach
+    void tearDown() {
+        ConnectContext.remove();
+        if (previousContext != null) {
+            previousContext.setThreadLocalInfo();
+        }
+    }
+
+    @Test
+    void testIncreasingAndSingletonRanges() {
+        DaysAdd function = new DaysAdd(date, new IntegerLiteral(2));
+        DateTimeV2Literal lower = new DateTimeV2Literal("2023-01-01 10:00:00");
+        DateTimeV2Literal upper = new DateTimeV2Literal("2023-01-03 11:00:00");
+        DateTimeV2Literal outputLower = new DateTimeV2Literal("2023-01-03 
10:00:00");
+        DateTimeV2Literal outputUpper = new DateTimeV2Literal("2023-01-05 
11:00:00");
+        assertRange(ColumnRange.between(outputLower, outputUpper), 
function.inferRange(lower, upper, context));
+        assertRange(ColumnRange.singleton(outputLower), 
function.inferRange(lower, lower, context));
+        assertRange(ColumnRange.atLeast(outputLower), 
function.inferRange(lower, null, context));
+        assertRange(ColumnRange.atMost(outputUpper), function.inferRange(null, 
upper, context));
+        Assertions.assertFalse(function.inferRange(null, null, 
context).isPresent());
+    }
+
+    @Test
+    void testDecreasingRangesSwapBoundedAndUnboundedSides() {
+        MonthsDiff function = new MonthsDiff(new DateTimeV2Literal("2023-04-01 
00:00:00"), date);
+        DateTimeV2Literal lower = new DateTimeV2Literal("2023-01-01 00:00:00");
+        DateTimeV2Literal upper = new DateTimeV2Literal("2023-03-01 00:00:00");
+        Assertions.assertEquals(1, function.getRangeInputChildIndex());
+        Assertions.assertFalse(function.isPositive());
+        assertRange(ColumnRange.between(new BigIntLiteral(1), new 
BigIntLiteral(3)),
+                function.inferRange(lower, upper, context));
+        assertRange(ColumnRange.atMost(new BigIntLiteral(3)), 
function.inferRange(lower, null, context));
+        assertRange(ColumnRange.atLeast(new BigIntLiteral(1)), 
function.inferRange(null, upper, context));
+    }
+
+    @Test
+    void testUnknownIntervalsAndInvalidEndpointResultsHaveNoRange() {
+        DateTimeV2Literal lower = new DateTimeV2Literal("2023-01-01 00:00:00");
+        DateTimeV2Literal upper = new DateTimeV2Literal("2023-01-02 00:00:00");
+        DaysAdd variable = new DaysAdd(date, new SlotReference("days", 
IntegerType.INSTANCE));
+        Assertions.assertFalse(variable.inferRange(lower, upper, 
context).isPresent());
+        MonthsAdd calendarVariable = new MonthsAdd(date, new 
SlotReference("months", IntegerType.INSTANCE));
+        Assertions.assertFalse(calendarVariable.inferRange(lower, upper, 
context).isPresent());
+
+        DaysAdd nullInterval = new DaysAdd(date, new 
NullLiteral(IntegerType.INSTANCE));
+        Assertions.assertFalse(nullInterval.inferRange(lower, upper, 
context).isPresent());
+
+        DaysAdd overflow = new DaysAdd(date, new IntegerLiteral(1));
+        DateTimeV2Literal lastDay = new DateTimeV2Literal("9999-12-31 
00:00:00");
+        Assertions.assertFalse(overflow.inferRange(lastDay, lastDay, 
context).isPresent());
+        MonthsAdd calendarOverflow = new MonthsAdd(date, new 
IntegerLiteral(1));
+        Assertions.assertFalse(calendarOverflow.inferRange(lower, lastDay, 
context).isPresent());
+    }
+
+    private void assertRange(ColumnRange expected, Optional<ColumnRange> 
actual) {
+        Assertions.assertTrue(actual.isPresent());
+        Assertions.assertEquals(expected.rangeSet, actual.get().rangeSet);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
index 239f13c5578..29cb9774566 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
@@ -28,6 +28,7 @@ import 
org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.SmallIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -40,6 +41,48 @@ import java.math.BigDecimal;
 import java.time.LocalDateTime;
 
 class DateTimeExtractAndTransformTest {
+    @Test
+    void testCalendarDiffAtMonthEnd() {
+        assertCalendarDiff("2023-03-01 11:00:00", "2023-01-31 12:00:00", 1, 0, 
0);
+        assertCalendarDiff("2023-03-01 11:00:00", "2022-11-30 12:00:00", 3, 1, 
0);
+        assertCalendarDiff("2021-03-01 11:00:00", "2020-02-29 12:00:00", 12, 
4, 1);
+        assertCalendarDiff("2023-02-28 11:00:00", "2023-01-31 12:00:00", 0, 0, 
0);
+        assertCalendarDiff("2023-03-31 11:00:00", "2023-01-31 12:00:00", 1, 0, 
0);
+        assertCalendarDiff("2021-02-28 11:00:00", "2020-02-29 12:00:00", 11, 
3, 0);
+        assertCalendarDiff("2021-03-01 11:00:00", "2020-03-01 12:00:00", 11, 
3, 0);
+        assertCalendarDiff("2021-03-01 12:00:00", "2020-03-01 12:00:00", 12, 
4, 1);
+        Assertions.assertEquals(new BigIntLiteral(1), 
DateTimeExtractAndTransform.monthsDiff(
+                new DateV2Literal("2023-03-01"), new 
DateTimeV2Literal("2023-01-31 12:00:00")));
+        Assertions.assertEquals(new BigIntLiteral(-1), 
DateTimeExtractAndTransform.monthsDiff(
+                new DateTimeV2Literal("2023-01-31 12:00:00"), new 
DateV2Literal("2023-03-01")));
+    }
+
+    private void assertCalendarDiff(String end, String start, long months, 
long quarters, long years) {
+        // Check both argument positions, including the negative differences 
used by pruning.
+        for (int sign : new int[] {1, -1}) {
+            String left = sign == 1 ? end : start;
+            String right = sign == 1 ? start : end;
+            DateTimeV2Literal dt1 = new DateTimeV2Literal(left);
+            DateTimeV2Literal dt2 = new DateTimeV2Literal(right);
+            TimeStampNsLiteral ns1 = new TimeStampNsLiteral(left);
+            TimeStampNsLiteral ns2 = new TimeStampNsLiteral(right);
+            TimestampTzLiteral tz1 = new TimestampTzLiteral(left);
+            TimestampTzLiteral tz2 = new TimestampTzLiteral(right);
+            BigIntLiteral expectedMonths = new BigIntLiteral(sign * months);
+            BigIntLiteral expectedQuarters = new BigIntLiteral(sign * 
quarters);
+            BigIntLiteral expectedYears = new BigIntLiteral(sign * years);
+            Assertions.assertEquals(expectedMonths, 
DateTimeExtractAndTransform.monthsDiff(dt1, dt2));
+            Assertions.assertEquals(expectedMonths, 
DateTimeExtractAndTransform.monthsDiff(ns1, ns2));
+            Assertions.assertEquals(expectedMonths, 
DateTimeExtractAndTransform.monthsDiff(tz1, tz2));
+            Assertions.assertEquals(expectedQuarters, 
DateTimeExtractAndTransform.quartersDiff(dt1, dt2));
+            Assertions.assertEquals(expectedQuarters, 
DateTimeExtractAndTransform.quartersDiff(ns1, ns2));
+            Assertions.assertEquals(expectedQuarters, 
DateTimeExtractAndTransform.quartersDiff(tz1, tz2));
+            Assertions.assertEquals(expectedYears, 
DateTimeExtractAndTransform.yearsDiff(dt1, dt2));
+            Assertions.assertEquals(expectedYears, 
DateTimeExtractAndTransform.yearsDiff(ns1, ns2));
+            Assertions.assertEquals(expectedYears, 
DateTimeExtractAndTransform.yearsDiff(tz1, tz2));
+        }
+    }
+
     @Test
     void testAdditionalTimestampNsCalendarFunctions() {
         TimeStampNsLiteral leapDay = new TimeStampNsLiteral("2024-02-29 
12:34:56.123456789");
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CalendarAddSubMonotonicTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CalendarAddSubMonotonicTest.java
new file mode 100644
index 00000000000..337084ea693
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CalendarAddSubMonotonicTest.java
@@ -0,0 +1,235 @@
+// 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.scalar;
+
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.ColumnRange;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarAddSubMonotonic;
+import org.apache.doris.nereids.trees.expressions.literal.DateLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DateTimeV2Type;
+import org.apache.doris.nereids.types.DateV2Type;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.TimeStampNsType;
+import org.apache.doris.nereids.types.TimeStampTzType;
+import org.apache.doris.qe.ConnectContext;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.List;
+
+class CalendarAddSubMonotonicTest {
+    private ConnectContext previousContext;
+    private ExpressionRewriteContext context;
+
+    @BeforeEach
+    void setUp() {
+        previousContext = ConnectContext.get();
+        ConnectContext connectContext = new ConnectContext();
+        connectContext.getSessionVariable().setTimeZone("+00:00");
+        connectContext.setThreadLocalInfo();
+        context = new 
ExpressionRewriteContext(CascadesContext.initTempContext());
+    }
+
+    @AfterEach
+    void tearDown() {
+        ConnectContext.remove();
+        if (previousContext != null) {
+            previousContext.setThreadLocalInfo();
+        }
+    }
+
+    @Test
+    void testClampingCanResetTimeAcrossDays() {
+        List<DataType> types = Arrays.asList(DateTimeV2Type.SYSTEM_DEFAULT,
+                TimeStampNsType.INSTANCE, TimeStampTzType.SYSTEM_DEFAULT);
+        List<Literal> lowerBounds = Arrays.asList(new 
DateTimeV2Literal("2023-01-28 10:00:00"),
+                new TimeStampNsLiteral("2023-01-28 10:00:00"), new 
TimestampTzLiteral("2023-01-28 10:00:00"));
+        List<Literal> upperBounds = Arrays.asList(new 
DateTimeV2Literal("2023-01-31 23:59:59"),
+                new TimeStampNsLiteral("2023-01-31 23:59:59"), new 
TimestampTzLiteral("2023-01-31 23:59:59"));
+        for (int i = 0; i < types.size(); i++) {
+            for (CalendarAddSubMonotonic function : calendarFunctions(new 
SlotReference("dt", types.get(i)))) {
+                Literal lower = lowerBounds.get(i);
+                Assertions.assertFalse(function.isMonotonic(lower, 
upperBounds.get(i)));
+                Assertions.assertFalse(function.isMonotonic(lower, null));
+                Assertions.assertFalse(function.isMonotonic(null, 
upperBounds.get(i)));
+                Assertions.assertTrue(function.isMonotonic(lower, lower));
+            }
+        }
+    }
+
+    @Test
+    void testDateOnlyAndSingleDayRemainMonotonic() {
+        for (CalendarAddSubMonotonic function : calendarFunctions(new 
SlotReference("d", DateV2Type.INSTANCE))) {
+            Assertions.assertTrue(function.isMonotonic(
+                    new DateV2Literal("2023-01-28"), new 
DateV2Literal("2023-01-31")));
+            Assertions.assertTrue(function.isMonotonic(null, null));
+        }
+        for (CalendarAddSubMonotonic function : calendarFunctions(
+                new SlotReference("dt", DateTimeV2Type.SYSTEM_DEFAULT))) {
+            Assertions.assertTrue(function.isMonotonic(new 
DateTimeV2Literal("2023-01-29 00:00:00"),
+                    new DateTimeV2Literal("2023-01-29 23:59:59")));
+        }
+    }
+
+    @Test
+    void testVariableIntervalsAreNotMonotonic() {
+        SlotReference date = new SlotReference("dt", 
DateTimeV2Type.SYSTEM_DEFAULT);
+        Assertions.assertFalse(new MonthsAdd(date, new SlotReference("months", 
IntegerType.INSTANCE)).isMonotonic(
+                new DateTimeV2Literal("2023-01-29 00:00:00"), new 
DateTimeV2Literal("2023-01-29 23:59:59")));
+    }
+
+    @Test
+    void testClampedRangeBoundsPreserveTypesAndPrecision() {
+        List<DateLiteral> inputs = calendarLiterals("2023-01-28 
10:00:00.123456");
+        List<DateLiteral> outputs = calendarLiterals("2023-02-28 
10:00:00.123456");
+        List<DateLiteral> clampedInputs = calendarLiterals("2023-01-31 
10:00:00.123456");
+        for (int i = 0; i < inputs.size(); i++) {
+            CalendarAddSubMonotonic function = new MonthsAdd(
+                    new SlotReference("dt", inputs.get(i).getDataType()), new 
IntegerLiteral(1));
+            DateLiteral output = outputs.get(i);
+            Literal lower = function.getRangeLowerBound(inputs.get(i), output);
+            Literal upper = function.getRangeUpperBound(clampedInputs.get(i), 
output);
+            Assertions.assertEquals(output.getDataType(), lower.getDataType());
+            Assertions.assertEquals(output.getDataType(), upper.getDataType());
+            
Assertions.assertEquals(output.toJavaDateType().with(LocalTime.MIN),
+                    ((DateLiteral) lower).toJavaDateType());
+            int maxNano = output instanceof TimeStampNsLiteral ? 999999999 : 
999999000;
+            
Assertions.assertEquals(output.toJavaDateType().with(LocalTime.of(23, 59, 59, 
maxNano)),
+                    ((DateLiteral) upper).toJavaDateType());
+            // The last source day has no later day that can reset its lower 
bound's clock.
+            Assertions.assertSame(output, 
function.getRangeLowerBound(clampedInputs.get(i), output));
+            // The first day mapping to the last destination day has no 
earlier collapsed day.
+            Assertions.assertSame(output, 
function.getRangeUpperBound(inputs.get(i), output));
+        }
+    }
+
+    @Test
+    void testOrdinaryMonthBoundariesKeepExactBounds() {
+        List<DateLiteral> inputs = calendarLiterals("2023-01-01 
10:00:00.123456");
+        List<DateLiteral> outputs = calendarLiterals("2023-02-01 
10:00:00.123456");
+        for (int i = 0; i < inputs.size(); i++) {
+            CalendarAddSubMonotonic function = new MonthsAdd(
+                    new SlotReference("dt", inputs.get(i).getDataType()), new 
IntegerLiteral(1));
+            Assertions.assertTrue(function.canEvaluateRange());
+            Assertions.assertSame(outputs.get(i), 
function.getRangeLowerBound(inputs.get(i), outputs.get(i)));
+            Assertions.assertSame(outputs.get(i), 
function.getRangeUpperBound(inputs.get(i), outputs.get(i)));
+        }
+    }
+
+    @Test
+    void testRangeInferenceDoesNotImplyMonotonicity() {
+        List<DateLiteral> lowerBounds = calendarLiterals("2023-01-28 
10:00:00.123456");
+        List<DateLiteral> upperBounds = calendarLiterals("2023-01-31 
00:00:00.123456");
+        for (int i = 0; i < lowerBounds.size(); i++) {
+            DateLiteral lower = lowerBounds.get(i);
+            DateLiteral upper = upperBounds.get(i);
+            CalendarAddSubMonotonic function = new MonthsAdd(
+                    new SlotReference("dt", lower.getDataType()), new 
IntegerLiteral(1));
+            Assertions.assertFalse(function.isMonotonic(lower, upper));
+            ColumnRange range = function.inferRange(lower, upper, 
context).get();
+            Assertions.assertEquals(lower.getDataType(), 
range.getLowerBound().getValue().getDataType());
+            Assertions.assertEquals(upper.getDataType(), 
range.getUpperBound().getValue().getDataType());
+            Assertions.assertEquals(LocalDateTime.of(2023, 2, 28, 0, 0),
+                    ((DateLiteral) 
range.getLowerBound().getValue()).toJavaDateType());
+            int maxNano = lower instanceof TimeStampNsLiteral ? 999999999 : 
999999000;
+            Assertions.assertEquals(LocalDateTime.of(2023, 2, 28, 23, 59, 59, 
maxNano),
+                    ((DateLiteral) 
range.getUpperBound().getValue()).toJavaDateType());
+            Assertions.assertFalse(function.inferRange(lower, null, 
context).get().span().hasUpperBound());
+            Assertions.assertFalse(function.inferRange(null, upper, 
context).get().span().hasLowerBound());
+        }
+    }
+
+    @Test
+    void testDateOnlyAndSameDayInferenceKeepsEndpointTimes() {
+        CalendarAddSubMonotonic dateOnly = new MonthsAdd(
+                new SlotReference("d", DateV2Type.INSTANCE), new 
IntegerLiteral(1));
+        ColumnRange dateRange = dateOnly.inferRange(new 
DateV2Literal("2023-01-28"),
+                new DateV2Literal("2023-01-31"), context).get();
+        Assertions.assertTrue(dateRange.isSingleton());
+        Assertions.assertEquals(new DateV2Literal("2023-02-28"), 
dateRange.getLowerBound().getValue());
+
+        DateLiteral lower = new DateTimeV2Literal("2023-01-29 10:00:00");
+        DateLiteral upper = new DateTimeV2Literal("2023-01-29 11:00:00");
+        CalendarAddSubMonotonic sameDay = new MonthsAdd(
+                new SlotReference("dt", lower.getDataType()), new 
IntegerLiteral(1));
+        ColumnRange range = sameDay.inferRange(lower, upper, context).get();
+        Assertions.assertEquals(new DateTimeV2Literal("2023-02-28 10:00:00"), 
range.getLowerBound().getValue());
+        Assertions.assertEquals(new DateTimeV2Literal("2023-02-28 11:00:00"), 
range.getUpperBound().getValue());
+    }
+
+    @Test
+    void testRangeEnclosesMonthEndAndLeapDayValues() {
+        String[][] ranges = {
+                {"2023-01-28T10:00:00", "2023-01-31T23:00:00"},
+                {"2023-01-27T00:00:00", "2023-01-31T00:00:00"},
+                {"2023-03-28T10:00:00", "2023-03-31T23:00:00"},
+                {"2024-02-28T10:00:00", "2024-02-29T23:00:00"},
+                {"2023-01-01T00:00:00", "2024-01-01T00:00:00"}
+        };
+        for (String[] range : ranges) {
+            LocalDateTime start = LocalDateTime.parse(range[0]);
+            LocalDateTime end = LocalDateTime.parse(range[1]);
+            for (int months : new int[] {-12, -3, -1, 0, 1, 3, 12}) {
+                for (int type = 0; type < 3; type++) {
+                    DateLiteral inputLower = 
calendarLiterals(start.toString()).get(type);
+                    DateLiteral inputUpper = 
calendarLiterals(end.toString()).get(type);
+                    CalendarAddSubMonotonic function = new MonthsAdd(
+                            new SlotReference("dt", inputLower.getDataType()), 
new IntegerLiteral(months));
+                    ColumnRange rangeResult = function.inferRange(inputLower, 
inputUpper, context).get();
+                    LocalDateTime lower = ((DateLiteral) 
rangeResult.getLowerBound().getValue()).toJavaDateType();
+                    LocalDateTime upper = ((DateLiteral) 
rangeResult.getUpperBound().getValue()).toJavaDateType();
+                    for (LocalDateTime value = start; !value.isAfter(end); 
value = value.plusHours(1)) {
+                        LocalDateTime shifted = value.plusMonths(months);
+                        Assertions.assertFalse(shifted.isBefore(lower));
+                        Assertions.assertFalse(shifted.isAfter(upper));
+                    }
+                }
+            }
+        }
+    }
+
+    private List<DateLiteral> calendarLiterals(String value) {
+        return Arrays.asList(new DateTimeV2Literal(DateTimeV2Type.MAX, value),
+                new TimeStampNsLiteral(value), new 
TimestampTzLiteral(TimeStampTzType.of(6), value));
+    }
+
+    private List<CalendarAddSubMonotonic> calendarFunctions(SlotReference 
date) {
+        IntegerLiteral one = new IntegerLiteral(1);
+        return Arrays.asList(new MonthsAdd(date, one), new MonthsSub(date, 
one),
+                new QuartersAdd(date, one), new QuartersSub(date, one),
+                new YearsAdd(date, one), new YearsSub(date, one),
+                new YearMonthAdd(date, new StringLiteral("0-1")),
+                new YearMonthSub(date, new StringLiteral("0-1")));
+    }
+}
diff --git 
a/regression-test/data/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.out
 
b/regression-test/data/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.out
new file mode 100644
index 00000000000..aa5913791ab
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.out
@@ -0,0 +1,220 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !0_months_add --
+2023-01-29T00:00       2023-02-28T00:00
+
+-- !0_months_add_equal --
+2023-01-29T00:00
+
+-- !0_months_add_in --
+2023-01-29T00:00
+
+-- !0_months_sub --
+2023-03-29T00:00       2023-02-28T00:00
+
+-- !0_quarters_add --
+2023-05-31T00:00       2024-02-29T00:00
+
+-- !0_quarters_sub --
+2023-05-31T00:00       2023-02-28T00:00
+
+-- !0_years_add --
+2024-02-29T00:00       2025-02-28T00:00
+
+-- !0_years_sub --
+2024-02-29T00:00       2023-02-28T00:00
+
+-- !0_year_month_add --
+2023-01-29T00:00
+
+-- !0_year_month_sub --
+2023-03-29T00:00
+
+-- !0_date_add --
+2023-01-29T00:00
+
+-- !0_date_sub --
+2023-03-29T00:00
+
+-- !0_timestampadd --
+2023-01-29T00:00
+
+-- !0_nested --
+2023-01-29T00:00
+
+-- !0_upper_bound --
+2023-01-29T23:00
+
+-- !0_months_diff --
+2021-03-01T10:00       1
+
+-- !0_months_diff_reverse --
+2021-03-01T10:00       -1
+
+-- !0_months_diff_constant --
+1
+
+-- !0_quarters_diff --
+2021-03-01T10:00       1
+
+-- !0_quarters_diff_reverse --
+2021-03-01T10:00       -1
+
+-- !0_quarters_diff_constant --
+1
+
+-- !0_years_diff --
+2021-03-01T10:00       1
+
+-- !0_years_diff_reverse --
+2021-03-01T10:00       -1
+
+-- !0_years_diff_constant --
+1
+
+-- !1_months_add --
+2023-01-29 00:00:00.000000000  2023-02-28 00:00:00.000000000
+
+-- !1_months_add_equal --
+2023-01-29 00:00:00.000000000
+
+-- !1_months_add_in --
+2023-01-29 00:00:00.000000000
+
+-- !1_months_sub --
+2023-03-29 00:00:00.000000000  2023-02-28 00:00:00.000000000
+
+-- !1_quarters_add --
+2023-05-31 00:00:00.000000000  2024-02-29 00:00:00.000000000
+
+-- !1_quarters_sub --
+2023-05-31 00:00:00.000000000  2023-02-28 00:00:00.000000000
+
+-- !1_years_add --
+2024-02-29 00:00:00.000000000  2025-02-28 00:00:00.000000000
+
+-- !1_years_sub --
+2024-02-29 00:00:00.000000000  2023-02-28 00:00:00.000000000
+
+-- !1_year_month_add --
+2023-01-29 00:00:00.000000000
+
+-- !1_year_month_sub --
+2023-03-29 00:00:00.000000000
+
+-- !1_date_add --
+2023-01-29 00:00:00.000000000
+
+-- !1_date_sub --
+2023-03-29 00:00:00.000000000
+
+-- !1_timestampadd --
+2023-01-29 00:00:00.000000000
+
+-- !1_nested --
+2023-01-29 00:00:00.000000000
+
+-- !1_upper_bound --
+2023-01-29 23:00:00.000000000
+
+-- !1_months_diff --
+2021-03-01 10:00:00.000000000  1
+
+-- !1_months_diff_reverse --
+2021-03-01 10:00:00.000000000  -1
+
+-- !1_months_diff_constant --
+1
+
+-- !1_quarters_diff --
+2021-03-01 10:00:00.000000000  1
+
+-- !1_quarters_diff_reverse --
+2021-03-01 10:00:00.000000000  -1
+
+-- !1_quarters_diff_constant --
+1
+
+-- !1_years_diff --
+2021-03-01 10:00:00.000000000  1
+
+-- !1_years_diff_reverse --
+2021-03-01 10:00:00.000000000  -1
+
+-- !1_years_diff_constant --
+1
+
+-- !2_months_add --
+2023-01-29 00:00:00.000000+00:00       2023-02-28 00:00:00.000000+00:00
+
+-- !2_months_add_equal --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_months_add_in --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_months_sub --
+2023-03-29 00:00:00.000000+00:00       2023-02-28 00:00:00.000000+00:00
+
+-- !2_quarters_add --
+2023-05-31 00:00:00.000000+00:00       2024-02-29 00:00:00.000000+00:00
+
+-- !2_quarters_sub --
+2023-05-31 00:00:00.000000+00:00       2023-02-28 00:00:00.000000+00:00
+
+-- !2_years_add --
+2024-02-29 00:00:00.000000+00:00       2025-02-28 00:00:00.000000+00:00
+
+-- !2_years_sub --
+2024-02-29 00:00:00.000000+00:00       2023-02-28 00:00:00.000000+00:00
+
+-- !2_year_month_add --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_year_month_sub --
+2023-03-29 00:00:00.000000+00:00
+
+-- !2_date_add --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_date_sub --
+2023-03-29 00:00:00.000000+00:00
+
+-- !2_timestampadd --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_nested --
+2023-01-29 00:00:00.000000+00:00
+
+-- !2_upper_bound --
+2023-01-29 23:00:00.000000+00:00
+
+-- !2_months_diff --
+2021-03-01 10:00:00.000000+00:00       1
+
+-- !2_months_diff_reverse --
+2021-03-01 10:00:00.000000+00:00       -1
+
+-- !2_months_diff_constant --
+1
+
+-- !2_quarters_diff --
+2021-03-01 10:00:00.000000+00:00       1
+
+-- !2_quarters_diff_reverse --
+2021-03-01 10:00:00.000000+00:00       -1
+
+-- !2_quarters_diff_constant --
+1
+
+-- !2_years_diff --
+2021-03-01 10:00:00.000000+00:00       1
+
+-- !2_years_diff_reverse --
+2021-03-01 10:00:00.000000+00:00       -1
+
+-- !2_years_diff_constant --
+1
+
+-- !date_only --
+2023-01-29
+
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.groovy
new file mode 100644
index 00000000000..ca34149485d
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/test_calendar_arithmetic_prune.groovy
@@ -0,0 +1,157 @@
+// 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_calendar_arithmetic_prune") {
+    sql "set time_zone = '+00:00'"
+    // Each type carries a time part that can go backwards when month-end 
clamping merges dates.
+    ["datetimev2(6)", "timestamp_ns", "timestamptz(6)"].eachWithIndex { 
dateType, typeIndex ->
+        sql "drop table if exists calendar_add_sub_prune"
+        sql """
+            create table calendar_add_sub_prune(d ${dateType})
+            duplicate key(d)
+            partition by range(d) (
+                partition p_month values [('2023-01-28 10:00:00'), 
('2023-01-31 23:59:59')),
+                partition p_sub values [('2023-03-28 10:00:00'), ('2023-03-31 
23:59:59')),
+                partition p_quarter values [('2023-05-30 10:00:00'), 
('2023-05-31 23:59:59')),
+                partition p_leap values [('2024-02-28 10:00:00'), ('2024-02-29 
23:59:59'))
+            )
+            distributed by hash(d) buckets 1
+            properties('replication_num' = '1')
+        """
+        sql """insert into calendar_add_sub_prune values
+            ('2023-01-29 00:00:00'), ('2023-03-29 00:00:00'),
+            ('2023-05-31 00:00:00'), ('2024-02-29 00:00:00')"""
+
+        // Cross-day inputs still have useful bounds even when the time part 
is not monotonic.
+        explain {
+            sql "select d from calendar_add_sub_prune where months_add(d, 1) > 
'2024-03-15 00:00:00'"
+            contains "partitions=1/4 (p_leap)"
+        }
+        explain {
+            sql "select d from calendar_add_sub_prune where years_add(d, 1) > 
'2024-02-29 00:00:00'"
+            contains "partitions=3/4 (p_sub,p_quarter,p_leap)"
+        }
+
+        "order_qt_${typeIndex}_months_add" """
+            select d, months_add(d, 1) from calendar_add_sub_prune
+            where months_add(d, 1) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_months_add_equal" """
+            select d from calendar_add_sub_prune where months_add(d, 1) = 
'2023-02-28 00:00:00'
+        """
+        "order_qt_${typeIndex}_months_add_in" """
+            select d from calendar_add_sub_prune
+            where months_add(d, 1) in ('2023-02-28 00:00:00', '2023-02-28 
01:00:00')
+        """
+        "order_qt_${typeIndex}_months_sub" """
+            select d, months_sub(d, 1) from calendar_add_sub_prune
+            where d >= '2023-03-01' and d < '2023-04-01'
+                and months_sub(d, 1) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_quarters_add" """
+            select d, quarters_add(d, 3) from calendar_add_sub_prune
+            where d >= '2023-05-01' and d < '2023-06-01'
+                and quarters_add(d, 3) < '2024-02-29 10:00:00'
+        """
+        "order_qt_${typeIndex}_quarters_sub" """
+            select d, quarters_sub(d, 1) from calendar_add_sub_prune
+            where d >= '2023-05-01' and d < '2023-06-01'
+                and quarters_sub(d, 1) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_years_add" """
+            select d, years_add(d, 1) from calendar_add_sub_prune
+            where d >= '2024-02-01' and years_add(d, 1) < '2025-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_years_sub" """
+            select d, years_sub(d, 1) from calendar_add_sub_prune
+            where d >= '2024-02-01' and years_sub(d, 1) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_year_month_add" """
+            select d from calendar_add_sub_prune
+            where d + interval '0-1' year_month < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_year_month_sub" """
+            select d from calendar_add_sub_prune where d >= '2023-03-01' and d 
< '2023-04-01'
+                and d - interval '0-1' year_month < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_date_add" """
+            select d from calendar_add_sub_prune where date_add(d, interval 1 
month) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_date_sub" """
+            select d from calendar_add_sub_prune where d >= '2023-03-01' and d 
< '2023-04-01'
+                and date_sub(d, interval 1 month) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_timestampadd" """
+            select d from calendar_add_sub_prune where timestampadd(month, 1, 
d) < '2023-02-28 10:00:00'
+        """
+        "order_qt_${typeIndex}_nested" """
+            select d from calendar_add_sub_prune where 
date_trunc(months_add(d, 1), 'hour') < '2023-02-28 10:00:00'
+        """
+
+        sql "drop table if exists calendar_add_upper_prune"
+        sql """
+            create table calendar_add_upper_prune(d ${dateType})
+            duplicate key(d)
+            partition by range(d) (partition p1 values [('2023-01-27 
00:00:00'), ('2023-01-31 00:00:00')))
+            distributed by hash(d) buckets 1 properties('replication_num' = 
'1')
+        """
+        sql "insert into calendar_add_upper_prune values ('2023-01-29 
23:00:00')"
+        "order_qt_${typeIndex}_upper_bound" """
+            select d from calendar_add_upper_prune where months_add(d, 1) > 
'2023-02-28 12:00:00'
+        """
+
+        // LocalDateTime.until used to fold both partition endpoints to zero 
instead of one.
+        sql "drop table if exists calendar_diff_prune"
+        sql """
+            create table calendar_diff_prune(d ${dateType})
+            duplicate key(d)
+            partition by range(d) (partition p1 values [('2021-03-01 
00:00:00'), ('2021-03-01 11:00:00')))
+            distributed by hash(d) buckets 1 properties('replication_num' = 
'1')
+        """
+        sql "insert into calendar_diff_prune values ('2021-03-01 10:00:00')"
+        ["months": "2021-01-31 12:00:00", "quarters": "2020-11-30 12:00:00",
+                "years": "2020-02-29 12:00:00"].each { unit, origin ->
+            "order_qt_${typeIndex}_${unit}_diff" """
+                select d, ${unit}_diff(d, '${origin}') from calendar_diff_prune
+                where ${unit}_diff(d, '${origin}') = 1
+            """
+            "order_qt_${typeIndex}_${unit}_diff_reverse" """
+                select d, ${unit}_diff('${origin}', d) from calendar_diff_prune
+                where ${unit}_diff('${origin}', d) = -1
+            """
+            "order_qt_${typeIndex}_${unit}_diff_constant" """
+                select ${unit}_diff(cast('2021-03-01 10:00:00' as 
${dateType}), cast('${origin}' as ${dateType}))
+            """
+        }
+    }
+
+    // DATE has no time reset and must keep its existing monotonic partition 
pruning.
+    sql "drop table if exists calendar_date_prune"
+    sql """
+        create table calendar_date_prune(d datev2) duplicate key(d)
+        partition by range(d) (
+            partition p1 values [('2023-01-28'), ('2023-02-01')),
+            partition p2 values [('2023-03-01'), ('2023-04-01'))
+        ) distributed by hash(d) buckets 1 properties('replication_num' = '1')
+    """
+    sql "insert into calendar_date_prune values ('2023-01-29'), ('2023-03-29')"
+    order_qt_date_only "select d from calendar_date_prune where months_add(d, 
1) = '2023-02-28'"
+    explain {
+        sql "select d from calendar_date_prune where months_add(d, 1) = 
'2023-02-28'"
+        contains "partitions=1/2 (p1)"
+    }
+}


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

Reply via email to