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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 955f72035a3 [opt](partition prunning) Infer bare-column predicates 
from monotonic functions for partition pruning (#66288)
955f72035a3 is described below

commit 955f72035a3b8ae1c38856c145c1ab62b7117b95
Author: foxtail463 <[email protected]>
AuthorDate: Wed Aug 26 12:03:17 2026 +0800

    [opt](partition prunning) Infer bare-column predicates from monotonic 
functions for partition pruning (#66288)
    
    Problem Summary:
    Partition pruning can binary-search sorted partitions only for
    bare-column comparisons.
    Predicates such as date_trunc(dt, 'day'), year(dt), and substring(col,
    1, n) hide the
    partition column and may fall back to slower per-partition evaluation.
    
    Solution:
    Derive safe necessary bounds on the bare partition column and use them
    only during
    partition pruning, while preserving the original predicate and AND/OR
    structure.
    A dedicated rounding trait models FLOOR/CEIL semantics and validates
    unsafe arguments
    and casts. Compound interval syntax is bound through DATE_ADD/DATE_SUB,
    but is not
    inverse-rewritten because overflow and clamping could change query
    semantics.
    
    Follow-up work:
    This PR keeps the safety-checked inference local to partition pruning. A
    follow-up PR will reuse it in global expression optimization so OLAP
    zone maps
    and ORC/Parquet min/max pruning can also benefit, with broader pushdown
    semantics validated separately.
    
    Co-authored-by: yangtao555 <[email protected]>
---
 .../rules/InferPredicateFromMonotonicFunction.java | 338 ++++++++++++++++
 .../rules/expression/rules/PartitionPruner.java    |   3 +
 .../functions/CalendarCeilMonotonic.java           |  43 ++
 .../functions/DateCeilFloorMonotonic.java          |  22 +-
 ...lFloorMonotonic.java => RoundingMonotonic.java} |  33 +-
 .../trees/expressions/functions/scalar/Date.java   |  11 +-
 .../expressions/functions/scalar/DateTrunc.java    |  18 +-
 .../expressions/functions/scalar/DayCeil.java      |   6 +
 .../expressions/functions/scalar/DayFloor.java     |   6 +
 .../expressions/functions/scalar/HourCeil.java     |   6 +
 .../expressions/functions/scalar/HourFloor.java    |   6 +
 .../expressions/functions/scalar/MinuteCeil.java   |   6 +
 .../expressions/functions/scalar/MinuteFloor.java  |   6 +
 .../expressions/functions/scalar/MonthCeil.java    |  10 +-
 .../expressions/functions/scalar/MonthFloor.java   |   6 +
 .../expressions/functions/scalar/QuarterCeil.java  |  10 +-
 .../expressions/functions/scalar/QuarterFloor.java |   6 +
 .../expressions/functions/scalar/SecondCeil.java   |   6 +
 .../expressions/functions/scalar/SecondFloor.java  |   6 +
 .../trees/expressions/functions/scalar/ToDate.java |  11 +-
 .../expressions/functions/scalar/ToMonday.java     |  10 +-
 .../expressions/functions/scalar/WeekCeil.java     |  23 +-
 .../expressions/functions/scalar/WeekFloor.java    |  23 +-
 .../expressions/functions/scalar/YearCeil.java     |  10 +-
 .../expressions/functions/scalar/YearFloor.java    |   6 +
 .../InferPredicateFromMonotonicFunctionTest.java   | 446 +++++++++++++++++++++
 .../test_add_sub_diff_ceil_floor.out               |   4 +
 .../partition_prune/always_mono_func.groovy        |  14 +-
 .../multi_column_range_partition.groovy            |   6 +-
 .../partition_prune/one_col_range_partition.groovy |   4 +-
 .../partition_prune/one_key_range_part_test.groovy |   8 +-
 .../one_key_range_part_update_test.groovy          |   4 +-
 .../test_add_sub_diff_ceil_floor.groovy            |  23 ++
 33 files changed, 1081 insertions(+), 59 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunction.java
new file mode 100644
index 00000000000..598bd0a2064
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunction.java
@@ -0,0 +1,338 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.expression.rules;
+
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
+import org.apache.doris.nereids.trees.expressions.LessThan;
+import org.apache.doris.nereids.trees.expressions.LessThanEqual;
+import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Left;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Substring;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Year;
+import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.types.coercion.CharacterType;
+import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+// When a partition predicate wraps the partition column in a function
+// (e.g. date_trunc(dt, 'day') >= '2020-01-01'), this class derives an extra 
predicate on the
+// bare column (dt >= '2020-01-01') and appends it, so partition pruning has a 
range to work with.
+//
+// Why: the fast pruning path binary-searches a partition list sorted by the 
partition column
+// (PartitionPredicateToRange). Binary search only understands "bare column op 
literal" shapes that
+// map to a range; once the column is wrapped by date_trunc / year / substring 
it cannot, and pruning
+// falls back to per-partition evaluation. The derived predicate is the key 
that unlocks binary
+// search. It lives only inside pruning and is never written back as a runtime 
filter, so it adds no
+// per-row cost. See the caller PartitionPruner.pruneInternal.
+// Like the existing monotonic partition evaluator, it may avoid evaluating 
the wrapped function in
+// a partition proven irrelevant; runtime errors from rows in such a partition 
are therefore not observable.
+//
+//   input:   date_trunc(dt, 'day') >= '2020-01-05'   (date_trunc is a floor, 
floor(x) <= x)
+//   infer:   floor(dt) >= '2020-01-05'  =>  dt >= '2020-01-05'
+//   output:  date_trunc(dt, 'day') >= '2020-01-05' AND dt >= '2020-01-05'
+//
+// Every derived predicate is a necessary condition of the original, resting 
only on the function's
+// monotonic/rounding property and never on data distribution, so appending it 
never changes results.
+// Three kinds are supported:
+//   - prefix:   substring(col,1,n) >= 'abc'  =>  col >= 'abc'   (a prefix 
never sorts after the whole string)
+//   - rounding: floor(col) >= c => col >= c;  ceil(col) <= c => col <= c
+//   - year:     year(col) op y  =>  a boundary date or the half-open range 
[y-01-01, (y+1)-01-01)
+// For each kind, EqualTo yields only a single-sided non-strict bound 
(floor(col)=c gives col >= c only).
+//
+// from_unixtime and similar are excluded on purpose: they map a unix 
timestamp to local time, so the
+// inverse depends on timezone shifts (DST) rather than a globally fixed 
property, and no safe bare-column
+// bound can be derived.
+final class InferPredicateFromMonotonicFunction {
+    private InferPredicateFromMonotonicFunction() {
+    }
+
+    // Comparison: derive directly. AND/OR: recurse into children and keep the 
boolean structure.
+    // NOT and other shapes are returned unchanged, since a necessary 
condition cannot cross negation.
+    static Expression inferForPartitionPrune(Expression partitionPredicate) {
+        if (partitionPredicate instanceof ComparisonPredicate) {
+            return appendInferredPredicate((ComparisonPredicate) 
partitionPredicate);
+        }
+        if (partitionPredicate instanceof And) {
+            return rewriteConjunction((And) partitionPredicate);
+        }
+        if (partitionPredicate instanceof Or) {
+            return rewriteDisjunction((Or) partitionPredicate);
+        }
+        return partitionPredicate;
+    }
+
+    // Append the derived bare-column predicate with AND; return the 
comparison unchanged if nothing is derived.
+    private static Expression appendInferredPredicate(ComparisonPredicate 
comparison) {
+        Optional<Expression> inferred = infer(comparison);
+        if (!inferred.isPresent()) {
+            return comparison;
+        }
+        // year EqualTo derives a lower AND upper bound; flatten so everything 
joins one AND.
+        List<Expression> conjuncts = Lists.newArrayList(comparison);
+        conjuncts.addAll(ExpressionUtils.extractConjunction(inferred.get()));
+        return ExpressionUtils.and(conjuncts);
+    }
+
+    // Preserve every original conjunct and append only new inferred 
predicates.
+    private static Expression rewriteConjunction(And predicate) {
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(predicate);
+        Set<Expression> existing = Sets.newHashSet(conjuncts);
+        List<Expression> rewrittenConjuncts = 
Lists.newArrayListWithCapacity(conjuncts.size());
+        List<Expression> inferredConjuncts = Lists.newArrayList();
+        for (Expression conjunct : conjuncts) {
+            if (!(conjunct instanceof ComparisonPredicate)) {
+                rewrittenConjuncts.add(inferForPartitionPrune(conjunct));
+                continue;
+            }
+            rewrittenConjuncts.add(conjunct);
+            Optional<Expression> inferred = infer((ComparisonPredicate) 
conjunct);
+            if (!inferred.isPresent()) {
+                continue;
+            }
+            for (Expression inferredConjunct : 
ExpressionUtils.extractConjunction(inferred.get())) {
+                if (existing.add(inferredConjunct)) {
+                    inferredConjuncts.add(inferredConjunct);
+                }
+            }
+        }
+        rewrittenConjuncts.addAll(inferredConjuncts);
+        return predicate.withChildren(rewrittenConjuncts);
+    }
+
+    // Derive per branch and keep the OR: necessary conditions of different 
branches must not be
+    // lifted out and ANDed together. PartitionPredicateToRange unions the 
branch ranges.
+    private static Expression rewriteDisjunction(Or predicate) {
+        List<Expression> children = predicate.children();
+        List<Expression> rewrittenChildren = 
Lists.newArrayListWithCapacity(children.size());
+        for (Expression child : children) {
+            Expression rewritten = inferForPartitionPrune(child);
+            rewrittenChildren.add(rewritten);
+        }
+        return predicate.withChildren(rewrittenChildren);
+    }
+
+    // Try the three inference kinds in order. Precondition: the right side 
must be a literal.
+    private static Optional<Expression> infer(ComparisonPredicate comparison) {
+        if (!(comparison.right() instanceof Literal)) {
+            return Optional.empty();
+        }
+
+        return inferPrefixPredicate(comparison)
+                .or(() -> inferYearPredicate(comparison))
+                .or(() -> inferRoundingPredicate(comparison));
+    }
+
+    // Prefix inference: a prefix never sorts after the whole string 
(prefix(s) <= s), so
+    // prefix(col) >= 'abc' => col >= 'abc'. >/>= keep the operator; = yields 
only the lower bound;
+    // </<= do not hold (prefix(col) <= 'abc' allows col = 'abd', which is 
larger).
+    private static Optional<Expression> 
inferPrefixPredicate(ComparisonPredicate comparison) {
+        if (!(comparison.right() instanceof StringLikeLiteral)) {
+            return Optional.empty();
+        }
+        Optional<Expression> source = prefixSource(comparison.left());
+        if (!source.isPresent()) {
+            return Optional.empty();
+        }
+
+        ComparisonPredicate inferred;
+        if (comparison instanceof GreaterThan || comparison instanceof 
GreaterThanEqual) {
+            inferred = (ComparisonPredicate) 
comparison.withChildren(source.get(), comparison.right());
+        } else if (comparison instanceof EqualTo) {
+            inferred = new GreaterThanEqual(source.get(), comparison.right());
+        } else {
+            return Optional.empty();
+        }
+        return Optional.of(inferredPredicate(inferred));
+    }
+
+    // Extract the bare column from a fixed-length, start-anchored prefix over 
a character Slot:
+    // substring(col, 1, n) (position 1, length present) or left(col, n). 
Otherwise empty.
+    private static Optional<Expression> prefixSource(Expression expression) {
+        Expression source;
+        Expression length;
+        if (expression instanceof Substring) {
+            Substring substring = (Substring) expression;
+            if (!(substring.getPosition() instanceof IntegerLikeLiteral)
+                    || ((IntegerLikeLiteral) 
substring.getPosition()).getBigDecimalValue()
+                            .compareTo(BigDecimal.ONE) != 0
+                    || !substring.getLength().isPresent()) {
+                return Optional.empty();
+            }
+            source = substring.getSource();
+            length = substring.getLength().get();
+        } else if (expression instanceof Left) {
+            source = expression.child(0);
+            length = expression.child(1);
+        } else {
+            return Optional.empty();
+        }
+
+        // A clean fixed-length prefix requires a character Slot source and a 
positive integer length.
+        if (!(source instanceof Slot) || !(source.getDataType() instanceof 
CharacterType)
+                || !(length instanceof IntegerLikeLiteral)
+                || ((IntegerLikeLiteral) length).getBigDecimalValue().signum() 
<= 0) {
+            return Optional.empty();
+        }
+        return Optional.of(source);
+    }
+
+    // Rounding inference over a bare date column. floor(x) <= x gives a lower 
bound;
+    // ceil(x) >= x gives an upper bound. EqualTo yields only the single-sided 
non-strict bound.
+    //   floor(dt) >= '2020-01-05'  =>  dt >= '2020-01-05'
+    //   ceil(dt)  <= '2020-01-05'  =>  dt <= '2020-01-05'
+    private static Optional<Expression> 
inferRoundingPredicate(ComparisonPredicate comparison) {
+        if (!(comparison.left() instanceof RoundingMonotonic)) {
+            return Optional.empty();
+        }
+        RoundingMonotonic function = (RoundingMonotonic) comparison.left();
+        // A rounding function may take several arguments (e.g. date_trunc(dt, 
'day'));
+        // getMonotonicFunctionChildIndex points at the rounded input column.
+        Expression source = 
function.child(function.getMonotonicFunctionChildIndex());
+        if (!function.isRoundingRelationGuaranteed()) {
+            return Optional.empty();
+        }
+
+        ComparisonPredicate inferred;
+        if (function.getRoundingType() == 
RoundingMonotonic.RoundingType.FLOOR) {
+            if (comparison instanceof GreaterThan || comparison instanceof 
GreaterThanEqual) {
+                inferred = (ComparisonPredicate) 
comparison.withChildren(source, comparison.right());
+            } else if (comparison instanceof EqualTo) {
+                inferred = new GreaterThanEqual(source, comparison.right());
+            } else {
+                // floor gives no upper bound: floor(dt) <= c allows dt beyond 
c within the same bucket.
+                return Optional.empty();
+            }
+        } else {
+            if (comparison instanceof LessThan || comparison instanceof 
LessThanEqual) {
+                inferred = (ComparisonPredicate) 
comparison.withChildren(source, comparison.right());
+            } else if (comparison instanceof EqualTo) {
+                inferred = new LessThanEqual(source, comparison.right());
+            } else {
+                return Optional.empty();
+            }
+        }
+        return inferredDatePredicate(inferred);
+    }
+
+    // Year inference: map year(dt) op y to a range on dt, where start = 
y-01-01, end = (y+1)-01-01,
+    // so year(dt) = y is the half-open range [start, end). Operator mapping 
(y=2020 example):
+    //     year(dt) >= 2020  ->  dt >= 2020-01-01
+    //     year(dt) >  2020  ->  dt >= 2021-01-01   (after year y means not 
before next year's start)
+    //     year(dt) <  2020  ->  dt <  2020-01-01
+    //     year(dt) <= 2020  ->  dt <  2021-01-01   (not after year y means 
before next year's start)
+    //     year(dt) =  2020  ->  dt >= 2020-01-01 AND dt < 2021-01-01
+    private static Optional<Expression> inferYearPredicate(ComparisonPredicate 
comparison) {
+        // year returns SmallInt, so comparing against a wider integer literal 
wraps it in an integer
+        // cast (cast(year(dt) as INT)); such a widening cast is 
value-preserving, so see through it.
+        Expression left = comparison.left();
+        if (left instanceof Cast && left.child(0) instanceof Year
+                && left.getDataType().isIntegerLikeType()
+                && left.child(0).getDataType().width() <= 
left.getDataType().width()) {
+            left = left.child(0);
+        }
+        if (!(left instanceof Year) || !(comparison.right() instanceof 
IntegerLikeLiteral)) {
+            return Optional.empty();
+        }
+        Expression source = left.child(0);
+        if (!isDateSlot(source)) {
+            return Optional.empty();
+        }
+
+        BigInteger year = ((IntegerLikeLiteral) 
comparison.right()).getBigDecimalValue().toBigIntegerExact();
+        Optional<DateV2Literal> start = firstDayOfYear(year);
+        // y=9999 is the date upper bound and has no next year, so end is 
absent and branches that
+        // depend on it (>, <=, and the upper side of =) cannot be derived.
+        Optional<DateV2Literal> end = year.compareTo(BigInteger.valueOf(9999)) 
< 0
+                ? firstDayOfYear(year.add(BigInteger.ONE)) : Optional.empty();
+        if (comparison instanceof GreaterThanEqual && start.isPresent()) {
+            return Optional.of(inferredPredicate(new GreaterThanEqual(source, 
start.get())));
+        }
+        if (comparison instanceof GreaterThan && end.isPresent()) {
+            return Optional.of(inferredPredicate(new GreaterThanEqual(source, 
end.get())));
+        }
+        if (comparison instanceof LessThan && start.isPresent()) {
+            return Optional.of(inferredPredicate(new LessThan(source, 
start.get())));
+        }
+        if (comparison instanceof LessThanEqual && end.isPresent()) {
+            return Optional.of(inferredPredicate(new LessThan(source, 
end.get())));
+        }
+        if (!(comparison instanceof EqualTo) || !start.isPresent()) {
+            return Optional.empty();
+        }
+
+        // EqualTo: lower bound dt >= start, plus upper bound dt < end when 
end is present.
+        Expression lower = inferredPredicate(new GreaterThanEqual(source, 
start.get()));
+        if (!end.isPresent()) {
+            return Optional.of(lower);
+        }
+        Expression upper = inferredPredicate(new LessThan(source, end.get()));
+        return Optional.of(ExpressionUtils.and(ImmutableList.of(lower, 
upper)));
+    }
+
+    // Build the year-01-01 literal; empty when the year is outside the valid 
[0, 9999] range.
+    private static Optional<DateV2Literal> firstDayOfYear(BigInteger year) {
+        return year.signum() >= 0 && year.compareTo(BigInteger.valueOf(9999)) 
<= 0
+                ? Optional.of(new DateV2Literal(year.intValueExact(), 1, 1))
+                : Optional.empty();
+    }
+
+    // A date column we can infer on: a Slot of date-like type, excluding 
timezone-aware timestamptz
+    // (its comparison depends on the session timezone and is not a globally 
fixed relation).
+    private static boolean isDateSlot(Expression expression) {
+        return expression instanceof Slot && 
expression.getDataType().isDateLikeType()
+                && !expression.getDataType().isTimeStampTzType();
+    }
+
+    // Type-coerce the derived predicate and mark it inferred, so downstream 
keeps it inside pruning
+    // and never writes it back as a user filter.
+    private static Expression inferredPredicate(ComparisonPredicate predicate) 
{
+        return 
TypeCoercionUtils.processComparisonPredicate(predicate).withInferred(true);
+    }
+
+    // The date floor/ceil binder may promote a DATE slot to DATETIMEV2 
because its hidden default
+    // origin is DATETIMEV2. Derive against that actual argument first, then 
reuse the comparison
+    // simplifier to recover a bare date Slot without losing non-midnight 
boundary semantics.
+    private static Optional<Expression> 
inferredDatePredicate(ComparisonPredicate predicate) {
+        ComparisonPredicate coerced = (ComparisonPredicate) 
TypeCoercionUtils.processComparisonPredicate(predicate);
+        Expression simplified = SimplifyComparisonPredicate.simplify(coerced);
+        return simplified instanceof ComparisonPredicate && 
isDateSlot(simplified.child(0))
+                ? Optional.of(simplified.withInferred(true))
+                : Optional.empty();
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionPruner.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionPruner.java
index 6d866da9154..353d019b742 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionPruner.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/PartitionPruner.java
@@ -185,6 +185,9 @@ public class PartitionPruner extends 
DefaultExpressionRewriter<Void> {
         partitionPredicate = PartitionPruneExpressionExtractor.extract(
                 partitionPredicate, ImmutableSet.copyOf(partitionSlots), 
cascadesContext);
         Expression originalPartitionPredicate = partitionPredicate;
+        // Keep inferred ranges local to pruning. They can unlock 
sorted-partition binary search,
+        // but must not be written back as extra runtime filter conjuncts.
+        partitionPredicate = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(partitionPredicate);
         partitionPredicate = 
PredicateRewriteForPartitionPrune.rewrite(partitionPredicate, cascadesContext);
         int expandThreshold = cascadesContext.getAndCacheSessionVariable(
                 "partitionPruningExpandThreshold",
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarCeilMonotonic.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarCeilMonotonic.java
new file mode 100644
index 00000000000..85224838205
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/CalendarCeilMonotonic.java
@@ -0,0 +1,43 @@
+// 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.DateLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
+
+/**
+ * Monotonicity of year, quarter, and month ceil functions. Calendar 
arithmetic can clamp the
+ * origin's day at month end, so custom origins do not universally guarantee 
ceil(x) >= x. For
+ * partition pruning, only the canonical first-day origin is accepted.
+ */
+public interface CalendarCeilMonotonic extends DateCeilFloorMonotonic {
+    @Override
+    default boolean isRoundingRelationGuaranteed() {
+        if (!DateCeilFloorMonotonic.super.isRoundingRelationGuaranteed()) {
+            return false;
+        }
+        if (arity() == 1 || (arity() == 2 && getArgument(1) instanceof 
IntegerLikeLiteral)) {
+            return true;
+        }
+        Expression origin = getArgument(arity() - 1);
+        return origin instanceof DateLiteral
+                && ((DateLiteral) 
origin).compareTo(DateTimeV2Literal.USE_IN_FLOOR_CEIL) == 0;
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
index 71ad80a3471..2885a205230 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
@@ -17,10 +17,12 @@
 
 package org.apache.doris.nereids.trees.expressions.functions;
 
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 
-/** monotonicity of XX_CEIL and XX_FLOOR */
-public interface DateCeilFloorMonotonic extends Monotonic {
+/** Monotonicity and rounding relation of date/time ceil and floor functions. 
*/
+public interface DateCeilFloorMonotonic extends RoundingMonotonic {
     @Override
     default boolean isMonotonic(Literal lower, Literal upper) {
         switch (arity()) {
@@ -44,4 +46,20 @@ public interface DateCeilFloorMonotonic extends Monotonic {
     default int getMonotonicFunctionChildIndex() {
         return 0;
     }
+
+    @Override
+    default boolean isRoundingRelationGuaranteed() {
+        if (arity() == 1) {
+            return true;
+        }
+        if (arity() == 2 && getArgument(1).getDataType().isDateLikeType()) {
+            return true;
+        }
+        return (arity() == 2 || arity() == 3) && 
isPositiveIntegerLiteral(getArgument(1));
+    }
+
+    private boolean isPositiveIntegerLiteral(Expression expression) {
+        return expression instanceof IntegerLikeLiteral
+                && ((IntegerLikeLiteral) 
expression).getBigDecimalValue().signum() > 0;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RoundingMonotonic.java
similarity index 54%
copy from 
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
copy to 
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RoundingMonotonic.java
index 71ad80a3471..eccdfa95ab4 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/DateCeilFloorMonotonic.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/RoundingMonotonic.java
@@ -17,31 +17,18 @@
 
 package org.apache.doris.nereids.trees.expressions.functions;
 
-import org.apache.doris.nereids.trees.expressions.literal.Literal;
-
-/** monotonicity of XX_CEIL and XX_FLOOR */
-public interface DateCeilFloorMonotonic extends Monotonic {
-    @Override
-    default boolean isMonotonic(Literal lower, Literal upper) {
-        switch (arity()) {
-            case 1:
-                return true;
-            case 2:
-                return !(child(0) instanceof Literal) && child(1) instanceof 
Literal;
-            case 3:
-                return !(child(0) instanceof Literal) && child(1) instanceof 
Literal && child(2) instanceof Literal;
-            default:
-                return false;
-        }
+/** A monotonic function whose result is guaranteed to be on one side of its 
input. */
+public interface RoundingMonotonic extends Monotonic {
+    /** Direction in which the function rounds its input. */
+    enum RoundingType {
+        FLOOR,
+        CEIL
     }
 
-    @Override
-    default boolean isPositive() {
-        return true;
-    }
+    RoundingType getRoundingType();
 
-    @Override
-    default int getMonotonicFunctionChildIndex() {
-        return 0;
+    /** Whether the current arguments preserve the declared relation between 
the result and input. */
+    default boolean isRoundingRelationGuaranteed() {
+        return true;
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Date.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Date.java
index 4b058ade725..f3b5577c1ea 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Date.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/Date.java
@@ -20,9 +20,10 @@ 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.ExplicitlyCastableSignature;
-import org.apache.doris.nereids.trees.expressions.functions.Monotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -37,7 +38,8 @@ import java.util.List;
  * ScalarFunction 'date'. This class is generated by GenerateFunction.
  */
 public class Date extends ScalarFunction
-        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, PropagateNullLiteral, Monotonic {
+        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, PropagateNullLiteral,
+        RoundingMonotonic {
 
     //TODO: eliminate this function for Date input
     public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
@@ -80,6 +82,11 @@ public class Date extends ScalarFunction
         return true;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public int getMonotonicFunctionChildIndex() {
         return 0;
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 3e057f2ff23..f2e5aa7355b 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
@@ -21,9 +21,10 @@ import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
-import org.apache.doris.nereids.trees.expressions.functions.Monotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
 import 
org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
@@ -42,7 +43,7 @@ import java.util.List;
  * ScalarFunction 'date_trunc'. This class is generated by GenerateFunction.
  */
 public class DateTrunc extends ScalarFunction
-        implements BinaryExpression, PropagateNullLiteral, PropagateNullable, 
Monotonic, CustomSignature {
+        implements BinaryExpression, PropagateNullLiteral, PropagateNullable, 
RoundingMonotonic, CustomSignature {
     private static final List<String> LEGAL_TIME_UNIT =
             ImmutableList.of("year", "quarter", "month", "week", "day", 
"hour", "minute", "second");
 
@@ -168,6 +169,19 @@ public class DateTrunc extends ScalarFunction
         return true;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
+    @Override
+    public boolean isRoundingRelationGuaranteed() {
+        int dateArgumentIndex = getMonotonicFunctionChildIndex();
+        Expression unit = child(dateArgumentIndex == 0 ? 1 : 0);
+        return unit instanceof StringLikeLiteral
+                && LEGAL_TIME_UNIT.contains(((StringLikeLiteral) 
unit).getStringValue().toLowerCase());
+    }
+
     @Override
     public int getMonotonicFunctionChildIndex() {
         return getArgument(0).getDataType().isDateLikeType() ? 0 : 1;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayCeil.java
index 260d3d4037a..56af70ff3fd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayCeil.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -102,6 +103,11 @@ public class DayCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitDayCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayFloor.java
index dece77f6ed9..c2fe1f899a1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DayFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -102,6 +103,11 @@ public class DayFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitDayFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourCeil.java
index 86d6cc0fc41..f7411630e8c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourCeil.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -99,6 +100,11 @@ public class HourCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitHourCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourFloor.java
index e27c9aaa409..0efcf1cdba4 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/HourFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -99,6 +100,11 @@ public class HourFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitHourFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteCeil.java
index 8896553e609..2046d8c0c0a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteCeil.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -98,6 +99,11 @@ public class MinuteCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitMinuteCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteFloor.java
index ebcd13ee2b4..30156f4377e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MinuteFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -98,6 +99,11 @@ public class MinuteFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitMinuteFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthCeil.java
index 0dc10ad8ab7..853930e9d43 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthCeil.java
@@ -19,10 +19,11 @@ 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.DateCeilFloorMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarCeilMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -38,7 +39,7 @@ import java.util.List;
  * ScalarFunction 'month_ceil'. This class is generated by GenerateFunction.
  */
 public class MonthCeil extends ScalarFunction
-        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, DateCeilFloorMonotonic {
+        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, CalendarCeilMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateTimeV2Type.WILDCARD).args(DateTimeV2Type.WILDCARD),
@@ -102,6 +103,11 @@ public class MonthCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitMonthCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthFloor.java
index 8fe2d8aa3b9..ecc9535f155 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MonthFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -102,6 +103,11 @@ public class MonthFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitMonthFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterCeil.java
index 28e19e85e1f..db5a1cbe32e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterCeil.java
@@ -19,10 +19,11 @@ 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.DateCeilFloorMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarCeilMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -38,7 +39,7 @@ import java.util.List;
  * ScalarFunction 'quarter_ceil'. This class is generated by GenerateFunction.
  */
 public class QuarterCeil extends ScalarFunction
-        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, DateCeilFloorMonotonic {
+        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, CalendarCeilMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateTimeV2Type.WILDCARD).args(DateTimeV2Type.WILDCARD),
@@ -103,6 +104,11 @@ public class QuarterCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitQuarterCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterFloor.java
index 026aaef6f86..0a8b4d020c1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/QuarterFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -103,6 +104,11 @@ public class QuarterFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitQuarterFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondCeil.java
index 6cd0f37470a..4c8ab90e072 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondCeil.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -98,6 +99,11 @@ public class SecondCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitSecondCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondFloor.java
index 0731e50cbc3..29ba55a0538 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SecondFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.IntegerType;
@@ -98,6 +99,11 @@ public class SecondFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitSecondFloor(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToDate.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToDate.java
index 2bc2014822a..7bdb4627dfc 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToDate.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToDate.java
@@ -20,9 +20,10 @@ 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.ExplicitlyCastableSignature;
-import org.apache.doris.nereids.trees.expressions.functions.Monotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -37,7 +38,8 @@ import java.util.List;
  * ScalarFunction 'to_date'. This class is generated by GenerateFunction.
  */
 public class ToDate extends ScalarFunction
-        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, PropagateNullLiteral, Monotonic {
+        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, PropagateNullLiteral,
+        RoundingMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateV2Type.INSTANCE).args(DateTimeV2Type.WILDCARD));
@@ -78,6 +80,11 @@ public class ToDate extends ScalarFunction
         return true;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public int getMonotonicFunctionChildIndex() {
         return 0;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToMonday.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToMonday.java
index 101b7b6f46d..dec6449d2e5 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToMonday.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ToMonday.java
@@ -20,8 +20,9 @@ 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.ExplicitlyCastableSignature;
-import org.apache.doris.nereids.trees.expressions.functions.Monotonic;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -36,7 +37,7 @@ import java.util.List;
  * ScalarFunction 'to_monday'. This class is generated by GenerateFunction.
  */
 public class ToMonday extends ScalarFunction
-        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, Monotonic {
+        implements UnaryExpression, ExplicitlyCastableSignature, 
PropagateNullable, RoundingMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateV2Type.INSTANCE).args(DateV2Type.INSTANCE),
@@ -79,6 +80,11 @@ public class ToMonday extends ScalarFunction
         return true;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public int getMonotonicFunctionChildIndex() {
         return 0;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekCeil.java
index a88751ea1ff..59c221257c6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekCeil.java
@@ -19,9 +19,11 @@ 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.DateCeilFloorMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -37,7 +39,7 @@ import java.util.List;
  * ScalarFunction 'week_ceil'. This class is generated by GenerateFunction.
  */
 public class WeekCeil extends ScalarFunction
-        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable {
+        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, DateCeilFloorMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateTimeV2Type.WILDCARD).args(DateTimeV2Type.WILDCARD),
@@ -101,8 +103,27 @@ public class WeekCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitWeekCeil(this, context);
     }
+
+    @Override
+    public Expression withConstantArgs(Expression literal) {
+        switch (arity()) {
+            case 1:
+                return new WeekCeil(literal);
+            case 2:
+                return new WeekCeil(literal, child(1));
+            case 3:
+                return new WeekCeil(literal, child(1), child(2));
+            default:
+                throw new IllegalStateException("The function " + getName() + 
" has invalid child number.");
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekFloor.java
index b2e0a916a82..64e46e6858d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/WeekFloor.java
@@ -19,9 +19,11 @@ 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.DateCeilFloorMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -37,7 +39,7 @@ import java.util.List;
  * ScalarFunction 'week_floor'. This class is generated by GenerateFunction.
  */
 public class WeekFloor extends ScalarFunction
-        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable {
+        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, DateCeilFloorMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateTimeV2Type.WILDCARD).args(DateTimeV2Type.WILDCARD),
@@ -101,8 +103,27 @@ public class WeekFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitWeekFloor(this, context);
     }
+
+    @Override
+    public Expression withConstantArgs(Expression literal) {
+        switch (arity()) {
+            case 1:
+                return new WeekFloor(literal);
+            case 2:
+                return new WeekFloor(literal, child(1));
+            case 3:
+                return new WeekFloor(literal, child(1), child(2));
+            default:
+                throw new IllegalStateException("The function " + getName() + 
" has invalid child number.");
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearCeil.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearCeil.java
index 358227c3677..f62ee50d99e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearCeil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearCeil.java
@@ -19,10 +19,11 @@ 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.DateCeilFloorMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.CalendarCeilMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -38,7 +39,7 @@ import java.util.List;
  * ScalarFunction 'year_ceil'. This class is generated by GenerateFunction.
  */
 public class YearCeil extends ScalarFunction
-        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, DateCeilFloorMonotonic {
+        implements ExplicitlyCastableSignature, PropagateNullLiteral, 
PropagateNullable, CalendarCeilMonotonic {
 
     private static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             
FunctionSignature.ret(DateTimeV2Type.WILDCARD).args(DateTimeV2Type.WILDCARD),
@@ -102,6 +103,11 @@ public class YearCeil extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.CEIL;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitYearCeil(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearFloor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearFloor.java
index 7ba43fb29ff..380cb846d29 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearFloor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/YearFloor.java
@@ -23,6 +23,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonoton
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import 
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
 import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import 
org.apache.doris.nereids.trees.expressions.functions.RoundingMonotonic.RoundingType;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 import org.apache.doris.nereids.types.DateV2Type;
@@ -102,6 +103,11 @@ public class YearFloor extends ScalarFunction
         return SIGNATURES;
     }
 
+    @Override
+    public RoundingType getRoundingType() {
+        return RoundingType.FLOOR;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitYearFloor(this, context);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunctionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunctionTest.java
new file mode 100644
index 00000000000..cdaed30b6cc
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/expression/rules/InferPredicateFromMonotonicFunctionTest.java
@@ -0,0 +1,446 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.expression.rules;
+
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteTestHelper;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.ComparisonPredicate;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.GreaterThanEqual;
+import org.apache.doris.nereids.trees.expressions.LessThan;
+import org.apache.doris.nereids.trees.expressions.LessThanEqual;
+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.executable.TimeRoundSeries;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.DateTrunc;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.DayCeil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.DayFloor;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Left;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.MonthCeil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.QuarterCeil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Substring;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ToDate;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.WeekFloor;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Year;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.YearCeil;
+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.LargeIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
+import org.apache.doris.nereids.types.DateTimeV2Type;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.TimeStampTzType;
+import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.util.ExpressionUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+
+class InferPredicateFromMonotonicFunctionTest extends 
ExpressionRewriteTestHelper {
+    private final SlotReference stringSlot = new SlotReference("s", 
StringType.INSTANCE, true);
+    private final SlotReference dateTimeSlot = new SlotReference(
+            "dt", DateTimeV2Type.SYSTEM_DEFAULT, true);
+    private final SlotReference nonPartitionDateTimeSlot = new SlotReference(
+            "non_partition_dt", DateTimeV2Type.SYSTEM_DEFAULT, true);
+
+    @Test
+    void inferOnlyAfterExtractingPartitionPredicate() {
+        EqualTo partitionPredicate = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2026));
+        EqualTo nonPartitionPredicate = new EqualTo(
+                new Year(nonPartitionDateTimeSlot), new IntegerLiteral(2026));
+        Expression filter = typeCoercion(ExpressionUtils.and(
+                ImmutableList.of(partitionPredicate, nonPartitionPredicate)));
+
+        Expression extracted = PartitionPruneExpressionExtractor.extract(
+                filter, ImmutableSet.of(dateTimeSlot), cascadesContext);
+        Expression inferred = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(extracted);
+
+        assertInferredBound(inferred, GreaterThanEqual.class, dateTimeSlot);
+        assertInferredBound(inferred, LessThan.class, dateTimeSlot);
+        Assertions.assertTrue(inferred.getInputSlots().contains(dateTimeSlot));
+        
Assertions.assertFalse(inferred.getInputSlots().contains(nonPartitionDateTimeSlot));
+    }
+
+    @Test
+    void inferPrefixLowerBoundFromSqlSyntax() {
+        Map<String, Slot> slots = Maps.newHashMap();
+        Expression original = 
typeCoercion(replaceUnboundSlot(PARSER.parseExpression(
+                "substring(VS, 1, 4) >= 'a-value-longer-than-four'"), slots));
+
+        Expression rewritten = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(original);
+
+        assertInferredBound(rewritten, GreaterThanEqual.class, 
slots.get("VS"));
+        assertOriginalPreserved(rewritten, original);
+    }
+
+    @Test
+    void inferStrictPrefixLowerBound() {
+        GreaterThan original = new GreaterThan(
+                new Left(stringSlot, new IntegerLiteral(4)), new 
VarcharLiteral("abcd"));
+
+        Expression rewritten = rewrite(original);
+
+        assertInferredBound(rewritten, GreaterThan.class, stringSlot);
+    }
+
+    @Test
+    void inferPrefixEqualityAsLowerBound() {
+        EqualTo original = new EqualTo(substring(stringSlot, 1, 4), new 
VarcharLiteral("abcd"));
+
+        Expression rewritten = rewrite(original);
+
+        assertInferredBound(rewritten, GreaterThanEqual.class, stringSlot);
+    }
+
+    @Test
+    void doNotInferNonPrefixOrUpperBound() {
+        LessThan upperBound = new LessThan(
+                substring(stringSlot, 1, 4), new VarcharLiteral("abcd"));
+        GreaterThanEqual nonPrefix = new GreaterThanEqual(
+                substring(stringSlot, 2, 4), new VarcharLiteral("abcd"));
+
+        Assertions.assertEquals(typeCoercion(upperBound), rewrite(upperBound));
+        Assertions.assertEquals(typeCoercion(nonPrefix), rewrite(nonPrefix));
+    }
+
+    @Test
+    void inferDateTruncAndToDateLowerBounds() {
+        GreaterThanEqual dateTrunc = new GreaterThanEqual(
+                new DateTrunc(dateTimeSlot, new VarcharLiteral("month")),
+                new DateTimeV2Literal("2026-07-01 00:00:00"));
+        EqualTo toDate = new EqualTo(new ToDate(dateTimeSlot), new 
VarcharLiteral("2026-07-28"));
+
+        assertInferredBound(rewrite(dateTrunc), GreaterThanEqual.class, 
dateTimeSlot);
+        assertInferredBound(rewrite(toDate), GreaterThanEqual.class, 
dateTimeSlot);
+    }
+
+    @Test
+    void inferFloorAndCeilBounds() {
+        GreaterThan floor = new GreaterThan(
+                new DayFloor(dateTimeSlot), new DateTimeV2Literal("2026-07-28 
00:00:00"));
+        GreaterThanEqual floorWithOrigin = new GreaterThanEqual(
+                new DayFloor(dateTimeSlot, new IntegerLiteral(3),
+                        new DateTimeV2Literal("2020-01-01 00:00:00")),
+                new DateTimeV2Literal("2026-07-28 00:00:00"));
+        LessThanEqual ceil = new LessThanEqual(
+                new DayCeil(dateTimeSlot), new DateTimeV2Literal("2026-07-28 
00:00:00"));
+        EqualTo weekFloor = new EqualTo(
+                new WeekFloor(dateTimeSlot), new DateTimeV2Literal("2026-07-27 
00:00:00"));
+
+        assertInferredBound(rewrite(floor), GreaterThan.class, dateTimeSlot);
+        assertInferredBound(rewrite(floorWithOrigin), GreaterThanEqual.class, 
dateTimeSlot);
+        assertInferredBound(rewrite(ceil), LessThanEqual.class, dateTimeSlot);
+        assertInferredBound(rewrite(weekFloor), GreaterThanEqual.class, 
dateTimeSlot);
+    }
+
+    @Test
+    void inferDateCeilBoundFromSqlSyntax() {
+        Map<String, Slot> slots = Maps.newHashMap();
+        Expression predicate = 
typeCoercion(replaceUnboundSlot(PARSER.parseExpression(
+                "date_ceil(AA, interval 1 day) <= '2026-07-28 00:00:00'"), 
slots));
+        Expression dateCeil = predicate.child(0);
+
+        Assertions.assertInstanceOf(DayCeil.class, dateCeil);
+        Assertions.assertEquals(3, dateCeil.arity());
+        
assertInferredBound(InferPredicateFromMonotonicFunction.inferForPartitionPrune(predicate),
+                LessThanEqual.class, slots.get("AA"));
+    }
+
+    @Test
+    void inferCalendarCeilWithDefaultOriginFromSqlSyntax() {
+        Map<String, Slot> slots = Maps.newHashMap();
+        Expression predicate = 
typeCoercion(replaceUnboundSlot(PARSER.parseExpression(
+                "date_ceil(AA, interval 1 month) <= '2026-07-28 00:00:00'"), 
slots));
+        Expression dateCeil = predicate.child(0);
+
+        Assertions.assertInstanceOf(MonthCeil.class, dateCeil);
+        Assertions.assertEquals(3, dateCeil.arity());
+        
assertInferredBound(InferPredicateFromMonotonicFunction.inferForPartitionPrune(predicate),
+                LessThanEqual.class, slots.get("AA"));
+    }
+
+    @Test
+    void inferDateV2BoundsFromSqlSyntax() {
+        Map<String, Slot> slots = Maps.newHashMap();
+        Expression ceilPredicate = 
typeCoercion(replaceUnboundSlot(PARSER.parseExpression(
+                "date_ceil(CC, interval 1 day) < '2026-07-28 12:00:00'"), 
slots));
+        Expression floorPredicate = 
typeCoercion(replaceUnboundSlot(PARSER.parseExpression(
+                "date_floor(CC, interval 1 day) >= '2026-07-28 12:00:00'"), 
slots));
+
+        Assertions.assertInstanceOf(Cast.class, 
ceilPredicate.child(0).child(0));
+        Assertions.assertInstanceOf(Cast.class, 
floorPredicate.child(0).child(0));
+
+        ComparisonPredicate ceilBound = findInferredBound(
+                
InferPredicateFromMonotonicFunction.inferForPartitionPrune(ceilPredicate),
+                LessThan.class, slots.get("CC"));
+        ComparisonPredicate floorBound = findInferredBound(
+                
InferPredicateFromMonotonicFunction.inferForPartitionPrune(floorPredicate),
+                GreaterThanEqual.class, slots.get("CC"));
+        DateV2Literal nextDay = new DateV2Literal("2026-07-29");
+        Assertions.assertEquals(nextDay, ceilBound.right());
+        Assertions.assertEquals(nextDay, floorBound.right());
+    }
+
+    @Test
+    void inferWithCustomOriginButOnlyPositiveLiteralPeriod() {
+        GreaterThanEqual zeroPeriod = new GreaterThanEqual(
+                new DayFloor(dateTimeSlot, new IntegerLiteral(0)),
+                new DateTimeV2Literal("2026-07-28 00:00:00"));
+        LessThanEqual zeroPeriodWithOrigin = new LessThanEqual(
+                new DayCeil(dateTimeSlot, new IntegerLiteral(0),
+                        DateTimeV2Literal.USE_IN_FLOOR_CEIL),
+                new DateTimeV2Literal("2026-07-28 00:00:00"));
+        SlotReference periodSlot = new SlotReference("period", 
IntegerType.INSTANCE, false);
+        LessThanEqual dynamicPeriod = new LessThanEqual(
+                new DayCeil(dateTimeSlot, periodSlot, 
DateTimeV2Literal.USE_IN_FLOOR_CEIL),
+                new DateTimeV2Literal("2026-07-28 00:00:00"));
+        LessThanEqual withOrigin = new LessThanEqual(
+                new DayCeil(dateTimeSlot, new DateTimeV2Literal("2020-01-01 
00:00:00")),
+                new DateTimeV2Literal("2026-07-28 00:00:00"));
+
+        Assertions.assertEquals(typeCoercion(zeroPeriod), rewrite(zeroPeriod));
+        Assertions.assertEquals(typeCoercion(zeroPeriodWithOrigin), 
rewrite(zeroPeriodWithOrigin));
+        Assertions.assertEquals(typeCoercion(dynamicPeriod), 
rewrite(dynamicPeriod));
+        assertInferredBound(rewrite(withOrigin), LessThanEqual.class, 
dateTimeSlot);
+    }
+
+    @Test
+    void doNotInferCalendarCeilWithCustomOrigin() {
+        DateTimeV2Literal boundary = new DateTimeV2Literal("2021-02-28 
00:00:00");
+        DateTimeV2Literal monthEndOrigin = new DateTimeV2Literal("2021-01-31 
00:00:00");
+        Assertions.assertEquals(boundary, TimeRoundSeries.monthCeil(
+                new DateTimeV2Literal("2021-02-28 12:00:00"), new 
IntegerLiteral(1), monthEndOrigin));
+
+        List<Expression> predicates = ImmutableList.of(
+                new LessThanEqual(new MonthCeil(dateTimeSlot, monthEndOrigin), 
boundary),
+                new LessThanEqual(new MonthCeil(dateTimeSlot, new 
IntegerLiteral(1), monthEndOrigin), boundary),
+                new LessThanEqual(new QuarterCeil(dateTimeSlot, new 
IntegerLiteral(1),
+                        new DateTimeV2Literal("2020-11-30 00:00:00")), 
boundary),
+                new LessThanEqual(new YearCeil(dateTimeSlot, new 
IntegerLiteral(1),
+                        new DateTimeV2Literal("2020-02-29 00:00:00")), 
boundary));
+
+        for (Expression predicate : predicates) {
+            Assertions.assertEquals(typeCoercion(predicate), 
rewrite(predicate));
+        }
+    }
+
+    @Test
+    void doNotInferTimestampTzRoundingBound() {
+        SlotReference timestampTzSlot = new SlotReference(
+                "ts", TimeStampTzType.SYSTEM_DEFAULT, true);
+        LessThanEqual predicate = new LessThanEqual(
+                new DayCeil(timestampTzSlot), new 
TimestampTzLiteral("2026-07-28 00:00:00+00:00"));
+
+        Assertions.assertEquals(typeCoercion(predicate), rewrite(predicate));
+    }
+
+    @Test
+    void inferExactYearRange() {
+        EqualTo original = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2026));
+
+        Expression rewritten = rewrite(original);
+
+        assertInferredBound(rewritten, GreaterThanEqual.class, dateTimeSlot);
+        assertInferredBound(rewritten, LessThan.class, dateTimeSlot);
+        Assertions.assertEquals(3, 
ExpressionUtils.extractConjunction(rewritten).size());
+    }
+
+    @Test
+    void inferAllYearComparisonDirections() {
+        assertInferredBound(rewrite(new GreaterThan(new Year(dateTimeSlot), 
new IntegerLiteral(2026))),
+                GreaterThanEqual.class, dateTimeSlot);
+        assertInferredBound(rewrite(new GreaterThanEqual(new 
Year(dateTimeSlot), new IntegerLiteral(2026))),
+                GreaterThanEqual.class, dateTimeSlot);
+        assertInferredBound(rewrite(new LessThan(new Year(dateTimeSlot), new 
IntegerLiteral(2026))),
+                LessThan.class, dateTimeSlot);
+        assertInferredBound(rewrite(new LessThanEqual(new Year(dateTimeSlot), 
new IntegerLiteral(2026))),
+                LessThan.class, dateTimeSlot);
+    }
+
+    @Test
+    void inferYearAtDateDomainBounds() {
+        EqualTo yearZero = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(0));
+        Expression rewrittenYearZero = rewrite(yearZero);
+        assertInferredBound(rewrittenYearZero, GreaterThanEqual.class, 
dateTimeSlot);
+        assertInferredBound(rewrittenYearZero, LessThan.class, dateTimeSlot);
+
+        EqualTo year9999 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(9999));
+        Expression rewrittenYear9999 = rewrite(year9999);
+        Expression expectedLowerBound = typeCoercion(
+                new GreaterThanEqual(dateTimeSlot, new DateV2Literal(9999, 1, 
1))).withInferred(true);
+        
Assertions.assertTrue(ExpressionUtils.extractConjunction(rewrittenYear9999).contains(expectedLowerBound));
+        Assertions.assertEquals(2, 
ExpressionUtils.extractConjunction(rewrittenYear9999).size());
+
+        GreaterThan afterLastYear = new GreaterThan(new Year(dateTimeSlot), 
new IntegerLiteral(9999));
+        LessThanEqual throughLastYear = new LessThanEqual(new 
Year(dateTimeSlot), new IntegerLiteral(9999));
+        EqualTo outsideDateDomain = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(10000));
+        Assertions.assertEquals(typeCoercion(afterLastYear), 
rewrite(afterLastYear));
+        Assertions.assertEquals(typeCoercion(throughLastYear), 
rewrite(throughLastYear));
+        Assertions.assertEquals(typeCoercion(outsideDateDomain), 
rewrite(outsideDateDomain));
+    }
+
+    @Test
+    void doNotInferThroughNarrowingYearCast() {
+        EqualTo predicate = new EqualTo(
+                new Cast(new Year(dateTimeSlot), TinyIntType.INSTANCE), new 
IntegerLiteral(1));
+
+        Assertions.assertEquals(typeCoercion(predicate), rewrite(predicate));
+    }
+
+    @Test
+    void doNotTruncateLargeYearLiteral() {
+        LessThan predicate = new LessThan(new Year(dateTimeSlot),
+                new LargeIntLiteral(new BigInteger("18446744073709553642")));
+
+        Assertions.assertEquals(typeCoercion(predicate), rewrite(predicate));
+    }
+
+    @Test
+    void preserveOriginalConjuncts() {
+        GreaterThanEqual predicate = new GreaterThanEqual(
+                dateTimeSlot, new DateTimeV2Literal("2026-07-28 00:00:00"));
+        Expression duplicateConjunction = new And(ImmutableList.of(predicate, 
predicate));
+
+        Expression rewritten = InferPredicateFromMonotonicFunction
+                .inferForPartitionPrune(duplicateConjunction);
+
+        Assertions.assertInstanceOf(And.class, rewritten);
+        Assertions.assertEquals(2, rewritten.children().size());
+    }
+
+    @Test
+    void preserveOriginalOrderAndAppendOnceInsideConjunction() {
+        GreaterThanEqual prefix = new GreaterThanEqual(
+                substring(stringSlot, 1, 4), new VarcharLiteral("abcd"));
+        GreaterThan dateFloor = new GreaterThan(
+                new DayFloor(dateTimeSlot), new DateTimeV2Literal("2026-07-28 
00:00:00"));
+        Expression original = ExpressionUtils.and(ImmutableList.of(prefix, 
dateFloor));
+
+        Expression analyzedOriginal = typeCoercion(original);
+        Expression once = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(analyzedOriginal);
+        Expression twice = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(once);
+        List<Expression> originalConjuncts = 
ExpressionUtils.extractConjunction(analyzedOriginal);
+        List<Expression> rewrittenConjuncts = 
ExpressionUtils.extractConjunction(once);
+
+        Assertions.assertEquals(once, twice);
+        Assertions.assertEquals(originalConjuncts,
+                rewrittenConjuncts.subList(0, originalConjuncts.size()));
+        Assertions.assertEquals(4, rewrittenConjuncts.size());
+    }
+
+    @Test
+    void doNotAppendBoundAlreadyPresent() {
+        DateTimeV2Literal boundary = new DateTimeV2Literal("2026-07-28 
00:00:00");
+        GreaterThanEqual dateFloor = new GreaterThanEqual(new 
DayFloor(dateTimeSlot), boundary);
+        GreaterThanEqual existingBound = new GreaterThanEqual(dateTimeSlot, 
boundary);
+        Expression original = 
typeCoercion(ExpressionUtils.and(ImmutableList.of(dateFloor, existingBound)));
+
+        Expression rewritten = 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(original);
+
+        Assertions.assertEquals(ExpressionUtils.extractConjunction(original),
+                ExpressionUtils.extractConjunction(rewritten));
+    }
+
+    @Test
+    void inferEachDisjunctionBranch() {
+        EqualTo year2025 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2025));
+        EqualTo year2026 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2026));
+
+        Expression rewritten = rewrite(new Or(year2025, year2026));
+        List<Expression> disjunctions = 
ExpressionUtils.extractDisjunction(rewritten);
+
+        Assertions.assertEquals(2, disjunctions.size());
+        for (Expression disjunction : disjunctions) {
+            assertInferredBound(disjunction, GreaterThanEqual.class, 
dateTimeSlot);
+            assertInferredBound(disjunction, LessThan.class, dateTimeSlot);
+            Assertions.assertEquals(3, 
ExpressionUtils.extractConjunction(disjunction).size());
+        }
+        Assertions.assertEquals(rewritten,
+                
InferPredicateFromMonotonicFunction.inferForPartitionPrune(rewritten));
+    }
+
+    @Test
+    void inferNestedConjunctionAndDisjunction() {
+        EqualTo year2024 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2024));
+        EqualTo year2025 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2025));
+        EqualTo year2026 = new EqualTo(new Year(dateTimeSlot), new 
IntegerLiteral(2026));
+        GreaterThan dayFloor = new GreaterThan(
+                new DayFloor(dateTimeSlot), new DateTimeV2Literal("2026-07-28 
00:00:00"));
+        Expression original = ExpressionUtils.and(ImmutableList.of(
+                year2024,
+                new Or(year2025, 
ExpressionUtils.and(ImmutableList.of(year2026, dayFloor)))));
+
+        Expression rewritten = rewrite(original);
+        Or rewrittenOr = (Or) 
ExpressionUtils.extractConjunction(rewritten).stream()
+                .filter(Or.class::isInstance)
+                .findFirst()
+                .orElseThrow(AssertionError::new);
+        List<Expression> disjunctions = 
ExpressionUtils.extractDisjunction(rewrittenOr);
+
+        Assertions.assertEquals(2, disjunctions.size());
+        assertInferredBound(rewritten, GreaterThanEqual.class, dateTimeSlot);
+        assertInferredBound(disjunctions.get(0), GreaterThanEqual.class, 
dateTimeSlot);
+        assertInferredBound(disjunctions.get(0), LessThan.class, dateTimeSlot);
+        assertInferredBound(disjunctions.get(1), GreaterThan.class, 
dateTimeSlot);
+        assertInferredBound(disjunctions.get(1), LessThan.class, dateTimeSlot);
+    }
+
+    private Substring substring(Expression source, int position, int length) {
+        return new Substring(source, new IntegerLiteral(position), new 
IntegerLiteral(length));
+    }
+
+    private Expression rewrite(Expression expression) {
+        return 
InferPredicateFromMonotonicFunction.inferForPartitionPrune(typeCoercion(expression));
+    }
+
+    private void assertOriginalPreserved(Expression rewritten, Expression 
original) {
+        Expression analyzedOriginal = typeCoercion(original);
+        
Assertions.assertTrue(ExpressionUtils.extractConjunction(rewritten).stream()
+                .anyMatch(conjunct -> conjunct.equals(analyzedOriginal) && 
!conjunct.isInferred()));
+    }
+
+    private void assertInferredBound(Expression expression,
+            Class<? extends ComparisonPredicate> predicateType, Expression 
source) {
+        findInferredBound(expression, predicateType, source);
+    }
+
+    private ComparisonPredicate findInferredBound(Expression expression,
+            Class<? extends ComparisonPredicate> predicateType, Expression 
source) {
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(expression);
+        return (ComparisonPredicate) conjuncts.stream()
+                .filter(conjunct -> predicateType.isInstance(conjunct)
+                        && conjunct.child(0).equals(source) && 
conjunct.isInferred())
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("missing inferred " + 
predicateType.getSimpleName()
+                        + " bound on " + source + " in " + expression));
+    }
+}
diff --git 
a/regression-test/data/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.out
 
b/regression-test/data/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.out
new file mode 100644
index 00000000000..dfa61163525
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.out
@@ -0,0 +1,4 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !date_ceil_upper_bound --
+2021-01-01T00:00
+
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/always_mono_func.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/always_mono_func.groovy
index 82c3795a7b2..1be795d1037 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/always_mono_func.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/always_mono_func.groovy
@@ -123,11 +123,11 @@ suite("always_mono_func") {
     // year
     explain {
         sql """ select * from always_mono_func where year(dt) >= 2019 """
-        contains("partitions=3/5 (p3,p4,p5)")
+        contains("partitions=2/5 (p4,p5)")
     }
     explain {
         sql """select * from always_mono_func where year(dt) < 2019 and 
year(dt) > 2017"""
-        contains("partitions=2/5 (p2,p3)")
+        contains("partitions=1/5 (p3)")
     }
     explain {
         sql """select * from always_mono_func where year(dt) <2023"""
@@ -140,15 +140,15 @@ suite("always_mono_func") {
     }
     explain {
         sql """select * from always_mono_func where to_monday(dt) 
='2019-01-01' """
-        contains("partitions=2/5 (p1,p4)")
+        contains("partitions=1/5 (p4)")
     }
     explain {
         sql """select * from always_mono_func where to_monday(dt) 
>='2018-01-01' and to_monday(dt) <'2019-01-01' """
-        contains("partitions=4/5 (p1,p2,p3,p4)")
+        contains("partitions=2/5 (p3,p4)")
     }
     explain {
         sql """ select * from always_mono_func where to_monday(dt) >= 
"2019-01-01" """
-        contains("partitions=3/5 (p1,p4,p5)")
+        contains("partitions=2/5 (p4,p5)")
     }
     // to_date
     explain {
@@ -169,7 +169,7 @@ suite("always_mono_func") {
     }
     explain {
         sql """select * from always_mono_func where to_date(dt) >='2018-01-01' 
and to_date(dt) <'2019-01-01' """
-        contains("partitions=2/5 (p2,p3)")
+        contains("partitions=1/5 (p3)")
     }
     explain {
         sql """ select * from always_mono_func where to_date(dt) > 
"2019-01-01" """
@@ -218,4 +218,4 @@ suite("always_mono_func") {
         sql """select * from always_mono_func  where 
date_format(date_trunc(last_day(to_monday(dt)),'month'), 'yyyyMMdd') > 
"20190101" """
         contains("partitions=3/5 (p1,p4,p5)")
     }
-}
\ No newline at end of file
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/multi_column_range_partition.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/multi_column_range_partition.groovy
index 7869e1f3ec2..f401b64606b 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/multi_column_range_partition.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/multi_column_range_partition.groovy
@@ -71,7 +71,7 @@ suite("multi_column_range_partition") {
         }
         explain {
             sql "select * from t_multi_column_range_partition where a=10 and 
date_trunc(dt, 'day') ='2024-01-10'"
-            contains("partitions=2/5 (p0,p10)")
+            contains("partitions=1/5 (p10)")
         }
         explain {
             sql "select * from t_multi_column_range_partition where a<19 and 
(date_trunc(dt, 'day') <'2024-01-20' OR date_trunc(dt, 'day') >'2024-02-10')"
@@ -211,7 +211,7 @@ suite("multi_column_range_partition") {
         }
         explain {
             sql "SELECT * FROM t_multi_column_partition_datetime_first WHERE 
date_trunc(dt,'day')='2024-1-10 00:00:00' and a >100"
-            contains("partitions=2/5 (p0,p10)")
+            contains("partitions=1/5 (p10)")
         }
         explain {
             sql "SELECT * FROM t_multi_column_partition_datetime_first WHERE 
date_trunc(dt,'day')='2024-1-1 00:00:00' and a <0"
@@ -257,4 +257,4 @@ suite("multi_column_range_partition") {
             contains("partitions=2/5 (p0,p10)")
         }
     }
-}
\ No newline at end of file
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_col_range_partition.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_col_range_partition.groovy
index 4aa880e9c79..b29edd8c527 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_col_range_partition.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_col_range_partition.groovy
@@ -79,7 +79,7 @@ suite("one_col_range_partition") {
     explain {
         sql """SELECT count(*) FROM one_col_range_partition_date WHERE
         (dt>='2020-10-01 00:00:00' or dt<='2020-9-01 00:00:00' ) and 
(date_trunc(dt,'month')>='2020-9-01' or date_trunc(dt,'month')<='2020-6-01' )"""
-        contains("partitions=327/1826")
+        contains("partitions=326/1826")
     }
 
     // !
@@ -324,4 +324,4 @@ suite("one_col_range_partition") {
         contains("partitions=417/1826")
     }
 
-}
\ No newline at end of file
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_test.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_test.groovy
index e008d186485..d68f9cf7340 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_test.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_test.groovy
@@ -83,11 +83,11 @@ suite("one_key_range_part_test") {
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_fixed_range_date_part WHERE 
date_trunc('month', dt) = '2023-03-01 00:00:00';")
-        contains "2/14 (p_202302,p_202303)"
+        contains "1/14 (p_202303)"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_fixed_range_date_part WHERE 
date_trunc('month', dt) BETWEEN '2023-04-01 00:00:00' AND '2023-05-01 
00:00:00';")
-        contains "3/14 (p_202303,p_202304,p_202305)"
+        contains "2/14 (p_202304,p_202305)"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_fixed_range_date_part WHERE 
date_add(dt, INTERVAL 1 MONTH) = '2023-11-15 10:00:00';")
@@ -214,11 +214,11 @@ suite("one_key_range_part_test") {
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part WHERE 
date_trunc('month', dt) = '2023-03-01 00:00:00';")
-        contains "1/10 (p_202302)"
+        contains "0:VEMPTYSET"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part WHERE 
date_trunc('month', dt) BETWEEN '2023-06-01 00:00:00' AND '2023-07-01 
00:00:00';")
-        contains "2/10 (p_202305,p_202306)"
+        contains "1/10 (p_202306)"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part WHERE 
date_add(dt, INTERVAL 1 MONTH) = '2023-11-15 10:00:00';")
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_update_test.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_update_test.groovy
index 95d05e66f8d..4d037f431f0 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_update_test.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/one_key_range_part_update_test.groovy
@@ -76,11 +76,11 @@ suite("one_key_range_part_update_test") {
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part_update 
WHERE date_trunc('month', dt) = '2023-08-01 00:00:00';")
-        contains "2/11 (p_202306,p_202308)"
+        contains "1/11 (p_202308)"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part_update 
WHERE date_trunc('month', dt) BETWEEN '2023-06-01 00:00:00' AND '2023-08-01 
00:00:00';")
-        contains "3/11 (p_202305,p_202306,p_202308)"
+        contains "2/11 (p_202306,p_202308)"
     }
     explain {
         sql("SELECT a, dt, c FROM key_1_special_fixed_range_date_part_update 
WHERE date_add(dt, INTERVAL 1 MONTH) = '2023-09-15 10:00:00';")
diff --git 
a/regression-test/suites/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.groovy
 
b/regression-test/suites/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.groovy
index 77e59823d42..d822951585e 100644
--- 
a/regression-test/suites/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/partition_prune/test_add_sub_diff_ceil_floor.groovy
@@ -191,6 +191,11 @@ suite("test_add_sub_diff_ceil_floor") {
         sql """select * from test_add_sub_diff_ceil_floor_t where day_ceil(dt) 
<'2019-01-01' """
         contains("partitions=3/5 (p1,p2,p3)")
     }
+    explain {
+        sql """select * from test_add_sub_diff_ceil_floor_t
+                where date_ceil(dt, interval 1 day) <'2019-01-01' """
+        contains("partitions=3/5 (p1,p2,p3)")
+    }
     explain {
         sql """select * from test_add_sub_diff_ceil_floor_t where 
hour_ceil(dt) <'2019-01-01' """
         contains("partitions=3/5 (p1,p2,p3)")
@@ -455,4 +460,22 @@ suite("test_add_sub_diff_ceil_floor") {
     //    sql """select * from max_t where weeks_diff(dt, 
quarter(weeks_sub(dt, 1))) >'2020-01-01'"""
     //    contains("partitions=6/6 (p1,p2,p3,p4,p5,p6)")
     //}
+
+    // Partition pruning already skips evaluation errors in partitions 
excluded by a necessary
+    // condition. Keep that established behavior when the inferred condition 
enables binary search.
+    sql "drop table if exists date_ceil_upper_bound_t"
+    sql """create table date_ceil_upper_bound_t (dt datetimev2 not null) 
duplicate key(dt)
+    partition by range(dt) (
+            partition p_old values less than ('2022-01-01'),
+            partition p_max values less than MAXVALUE
+    ) distributed by hash(dt) buckets 1 properties('replication_num'='1');"""
+    sql """insert into date_ceil_upper_bound_t values
+            ('2021-01-01 00:00:00'), ('2021-12-31 00:00:00'), ('9999-12-31 
23:59:59')"""
+    test {
+        sql """select date_ceil(dt, interval 1 day)
+                from date_ceil_upper_bound_t partition(p_max)"""
+        exception "out of range"
+    }
+    order_qt_date_ceil_upper_bound """select * from date_ceil_upper_bound_t
+            where date_ceil(dt, interval 1 day) <= '2021-01-01'"""
 }


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

Reply via email to