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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new f4b2d35a4ab branch-4.1: [fix](nereids) Preserve date arithmetic 
evaluation boundaries #69113 (#69577)
f4b2d35a4ab is described below

commit f4b2d35a4ab9baa7bf11c2f4f94c75c915cbd72e
Author: Oliveira <[email protected]>
AuthorDate: Mon Oct 26 10:03:37 2026 +0800

    branch-4.1: [fix](nereids) Preserve date arithmetic evaluation boundaries 
#69113 (#69577)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A (no associated GitHub issue)
    
    Related PR: #69113
    
    Problem Summary:
    
    Backport #69113 to `branch-4.1`. An outer predicate on a projected date
    expression can be expanded and evaluated before an inner condition
    rejects the row. A filtered row containing `9999-12-31 23:59:59` can
    therefore cause `WEEKS_ADD(dt, 5)` to overflow instead of returning the
    expected empty result.
    
    Preserve potentially overflowing date arithmetic aliases above Filter,
    Join, Limit, EXCEPT, INTERSECT, Generate and CTE consumer boundaries in
    logical and physical project predicate pushdown. The branch adaptation
    uses its existing logical/physical EXCEPT and INTERSECT classes and
    retains its Project-level NoneMovableFunction and volatile-expression
    guards. Protect both scalar function calls and `TimestampArithmetic`
    (`dt +/- INTERVAL ...`). Preserve the compensating `Filter -> Limit ->
    Project` rewrite to `Project -> Filter -> Limit`, so OFFSET discards
    rows before date evaluation. Safe integer-zero offsets and ordinary
    UNION retain the behavior of the master fix.
    
    ### Release note
    
    Fix date arithmetic overflow errors caused by predicate pushdown
    evaluating expressions on rows excluded by an inner query.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [x] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    <details>
    <summary>Local validation on branch-4.1</summary>
    
    - FE/BE build and strict Checkstyle passed with JDK 17 and clang21/ASAN.
    The service build used the repository's `DISABLE_BUILD_UI=ON` option.
    - **17 FE unit tests passed**, with zero failures, errors or skips:
    `DateArithmeticFilterPushDownTest` (11),
    `PushDowFilterThroughProjectTest` (4), and
    `org.apache.doris.nereids.postprocess.PushDownFilterThroughProjectTest`
    (2).
    - **Three native P0 suites passed**, with zero failed/fatal/skipped
    suites:
    `nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary`,
    `nereids_rules_p0/date_function_rewrite/date_function_rewrite`, and
    `nereids_rules_p0/cte/test_cte_filter_pushdown`.
    - Tests ran on this branch's verified binaries in a fresh isolated 1 FE
    + 3 BE instance. Source, configuration, framework and process identities
    remained stable. The epoch-based date-rewrite suite was run with the
    private instance's default time zone set to `Asia/Shanghai`.
    - The unit tests cover logical/physical interval protection, integer and
    bigint zero offsets, and projection restoration above OFFSET with and
    without a lower Filter. The native regression adds OFFSET 1/100
    empty-result checks, interval addition/subtraction and zero-offset
    controls, and participating-row overflow controls. Expected output files
    were not edited.
    - **10 direct SQL checks passed** against the final branch binaries:
    OFFSET 1/100, interval addition, subtraction of a negative interval,
    subtraction, both zero-interval spellings, a zero scalar offset, and
    scalar/interval participating-row overflow.
    - Source whitespace checks passed; regression outputs retain the
    framework's standard final separator.
    
    Before this review update, manual validation passed **18 checks covering
    ORC and Parquet**, in each of UTC and Asia/Shanghai. That external-file
    validation was not rerun for this update; the final update was verified
    with the unit/native/direct-SQL checks above:
    
    1. Generate 200 rows: one row has `dt='9999-12-31 23:59:59'` and
    `filter_dt='2025-01-01'`; the other 199 have `dt='2020-01-01'` and
    `filter_dt='2020-01-01'`. Export the same input to ORC and Parquet on a
    fresh private HDFS fixture.
    2. For WEEKS_ADD, HOURS_ADD, SECOND_CEIL, YEAR_CEIL and DAYS_ADD, run
    the filtered CTE/outer-alias predicate shape from the original issue.
    All return zero rows without overflow. A surviving-row control returns a
    count of 199; a shared CTE control returns 398 rows.
    3. Verify that arithmetic on a participating maximum-date row still
    raises the expected out-of-range error, and that a NULL date returns
    NULL. Input row hashes and HDFS/runtime identities remained unchanged
    during each run.
    
    These are targeted local results, not a full TeamCity or External-group
    verdict. The five historical RQG executions have not been replayed
    because their actual external-table DDL and immutable ORC inputs were
    unavailable; the manual fixtures above were reconstructed.
    
    </details>
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Preserve date arithmetic evaluation boundaries while
    retaining safe zero-offset predicate pushdown and partition pruning.
    Nonzero date predicates can remain above rejecting boundaries.
    
    - Does this need documentation?
        - [x] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../post/PushDownFilterThroughProject.java         |   3 +-
 .../rewrite/PushDownFilterThroughProject.java      |  14 +-
 .../apache/doris/nereids/util/ExpressionUtils.java |  51 ++++
 .../rewrite/DateArithmeticFilterPushDownTest.java  | 339 +++++++++++++++++++++
 .../tvf/test_date_expression_filter.out            |  47 +++
 .../test_date_arithmetic_rejecting_boundary.out    |  13 +
 .../tvf/test_date_expression_filter.groovy         | 107 +++++++
 .../test_date_arithmetic_rejecting_boundary.groovy | 102 +++++++
 8 files changed, 670 insertions(+), 6 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushDownFilterThroughProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushDownFilterThroughProject.java
index 9b4e262d6a1..7c6425be1e8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushDownFilterThroughProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushDownFilterThroughProject.java
@@ -45,7 +45,8 @@ public class PushDownFilterThroughProject extends 
PlanPostProcessor {
         PhysicalProject<? extends Plan> project = (PhysicalProject<? extends 
Plan>) child;
         Map<Slot, Expression> childAlias = project.getAliasToProducer();
         if 
(filter.getInputSlots().stream().map(childAlias::get).filter(Objects::nonNull)
-                .anyMatch(Expression::containsVolatileExpression)) {
+                .anyMatch(expression -> expression.containsVolatileExpression()
+                        || 
ExpressionUtils.requiresDateArithmeticProtection(expression, project.child()))) 
{
             return filter;
         }
         PhysicalFilter<? extends Plan> newFilter = 
filter.withConjunctsAndChild(
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
index 1a46b51a246..5ad6f1900e6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java
@@ -82,7 +82,7 @@ public class PushDownFilterThroughProject implements 
RewriteRuleFactory {
         // splitConjuncts.first -> conjuncts having outer query slots which 
should NOT be pushed down
         // splitConjuncts.second -> conjuncts without any outer query slots 
which should be pushed down
         Pair<Set<Expression>, Set<Expression>> splitConjuncts =
-                splitConjunctsByChildOutput(filter.getConjuncts(), 
childOutputs, childAlias);
+                splitConjunctsByChildOutput(filter.getConjuncts(), 
childOutputs, childAlias, project.child(), true);
         Set<Expression> remainPredicates = splitConjuncts.first;
         Set<Expression> pushDownPredicates = splitConjuncts.second;
         if (pushDownPredicates.isEmpty()) {
@@ -99,9 +99,10 @@ public class PushDownFilterThroughProject implements 
RewriteRuleFactory {
         LogicalProject<Plan> project = limit.child();
         Set<Slot> childOutputs = project.getOutputSet();
         Map<Slot, Expression> childAlias = project.getAliasToProducer();
-        // split the conjuncts by child's output
+        // Restore evaluation above the enclosing Limit. The expanded filter 
stays above
+        // that Limit and every lower rejecting boundary, so date arithmetic 
is safe here.
         Pair<Set<Expression>, Set<Expression>> splitConjuncts =
-                splitConjunctsByChildOutput(filter.getConjuncts(), 
childOutputs, childAlias);
+                splitConjunctsByChildOutput(filter.getConjuncts(), 
childOutputs, childAlias, limit, false);
         Set<Expression> remainPredicates = splitConjuncts.first;
         Set<Expression> pushDownPredicates = splitConjuncts.second;
         if (pushDownPredicates.isEmpty()) {
@@ -113,7 +114,8 @@ public class PushDownFilterThroughProject implements 
RewriteRuleFactory {
     }
 
     private static Pair<Set<Expression>, Set<Expression>> 
splitConjunctsByChildOutput(
-            Set<Expression> conjuncts, Set<Slot> childOutputs, Map<Slot, 
Expression> childAlias) {
+            Set<Expression> conjuncts, Set<Slot> childOutputs, Map<Slot, 
Expression> childAlias, Plan input,
+            boolean protectDateArithmetic) {
         Set<Expression> pushDownPredicates = Sets.newLinkedHashSet();
         Set<Expression> remainPredicates = Sets.newLinkedHashSet();
         for (Expression conjunct : conjuncts) {
@@ -125,7 +127,9 @@ public class PushDownFilterThroughProject implements 
RewriteRuleFactory {
             // `project(b + random(1, 10) as a) -> filter(b + random(1, 10) > 
1)`, it contains two distinct RANDOM.
             if (childOutputs.containsAll(conjunctSlots)
                     && 
conjunctSlots.stream().map(childAlias::get).filter(Objects::nonNull)
-                            
.noneMatch(Expression::containsVolatileExpression)) {
+                            .noneMatch(expression -> 
expression.containsVolatileExpression()
+                                    || (protectDateArithmetic
+                                            && 
ExpressionUtils.requiresDateArithmeticProtection(expression, input)))) {
                 pushDownPredicates.add(conjunct);
             } else {
                 remainPredicates.add(conjunct);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
index 836db942c18..72a6a65b3b6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java
@@ -49,10 +49,13 @@ import org.apache.doris.nereids.trees.expressions.Not;
 import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.TimestampArithmetic;
 import org.apache.doris.nereids.trees.expressions.VolatileExpression;
 import org.apache.doris.nereids.trees.expressions.WhenClause;
 import org.apache.doris.nereids.trees.expressions.WindowExpression;
 import org.apache.doris.nereids.trees.expressions.functions.BoundFunction;
+import 
org.apache.doris.nereids.trees.expressions.functions.DateAddSubMonotonic;
+import 
org.apache.doris.nereids.trees.expressions.functions.DateCeilFloorMonotonic;
 import 
org.apache.doris.nereids.trees.expressions.functions.NoneMovableFunction;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Avg;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Count;
@@ -76,6 +79,8 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.Nullable;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Nvl;
 import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.ComparableLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
 import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
@@ -83,8 +88,18 @@ import 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewri
 import 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionVisitor;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitors;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.Filter;
+import org.apache.doris.nereids.trees.plans.algebra.Generate;
+import org.apache.doris.nereids.trees.plans.algebra.Join;
+import org.apache.doris.nereids.trees.plans.algebra.Limit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalEmptyRelation;
+import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
+import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalExcept;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalIntersect;
 import org.apache.doris.nereids.trees.plans.visitor.ExpressionLineageReplacer;
 import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.VariantType;
@@ -133,6 +148,42 @@ public class ExpressionUtils {
     // folding the conjunct). We restrict MAX_MARK_SLOT_COUNT to 4 to bound 
this cost.
     private static final int MAX_MARK_SLOT_COUNT = 4;
 
+    /**
+     * Date arithmetic can throw for values outside its valid range. Keep it 
above a lower
+     * condition, join, limit, rejecting set operation, generate, or CTE 
consumer so rejected
+     * rows are not evaluated early.
+     * A date add/subtract with literal zero is an identity and remains safe 
to push through.
+     */
+    public static boolean requiresDateArithmeticProtection(Expression 
expression, Plan input) {
+        return expression.anyMatch(expr -> isUnsafeDateArithmetic((Expression) 
expr))
+                && input.anyMatch(plan -> plan instanceof Filter || plan 
instanceof Join || plan instanceof Limit
+                        || plan instanceof LogicalExcept || plan instanceof 
PhysicalExcept
+                        || plan instanceof LogicalIntersect || plan instanceof 
PhysicalIntersect
+                        || plan instanceof Generate || plan instanceof 
LogicalCTEConsumer
+                        || plan instanceof PhysicalCTEConsumer);
+    }
+
+    private static boolean isUnsafeDateArithmetic(Expression expression) {
+        if (expression instanceof DateCeilFloorMonotonic) {
+            return true;
+        }
+        int dateChildIndex;
+        if (expression instanceof TimestampArithmetic) {
+            dateChildIndex = 0;
+        } else if (expression instanceof DateAddSubMonotonic) {
+            dateChildIndex = ((DateAddSubMonotonic) 
expression).getMonotonicFunctionChildIndex();
+        } else {
+            return false;
+        }
+        for (int i = 0; i < expression.children().size(); i++) {
+            if (i != dateChildIndex && !(expression.child(i) instanceof 
IntegerLikeLiteral
+                    && ((IntegerLikeLiteral) 
expression.child(i)).compareTo(new IntegerLiteral(0)) == 0)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static List<Expression> extractConjunction(Expression expr) {
         return extract(And.class, expr);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/DateArithmeticFilterPushDownTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/DateArithmeticFilterPushDownTest.java
new file mode 100644
index 00000000000..d376b6b42fd
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/DateArithmeticFilterPushDownTest.java
@@ -0,0 +1,339 @@
+// 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.rewrite;
+
+import org.apache.doris.analysis.ArithmeticExpr.Operator;
+import org.apache.doris.nereids.rules.exploration.join.JoinReorderContext;
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
+import org.apache.doris.nereids.trees.expressions.TimestampArithmetic;
+import org.apache.doris.nereids.trees.expressions.functions.generator.Explode;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.DaysAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.HoursAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.SecondCeil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.WeeksAdd;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.YearCeil;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Interval.TimeUnit;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.LimitPhase;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalExcept;
+import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
+import org.apache.doris.nereids.trees.plans.logical.LogicalGenerate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalIntersect;
+import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
+import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalExcept;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalFilter;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalGenerate;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalIntersect;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalOneRowRelation;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
+import org.apache.doris.nereids.types.DateTimeV2Type;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class DateArithmeticFilterPushDownTest {
+    private final Alias datetime = new Alias(new DateTimeV2Literal("9999-12-31 
23:59:59.000001"), "dt");
+    private final Alias id = new Alias(new IntegerLiteral(1), "id");
+    private final LogicalOneRowRelation relation = new LogicalOneRowRelation(
+            StatementScopeIdGenerator.newRelationId(), 
ImmutableList.of(datetime, id));
+
+    private List<Expression> addSubExpressions(Expression offset) {
+        return ImmutableList.of(
+                new WeeksAdd(datetime.toSlot(), offset),
+                new HoursAdd(datetime.toSlot(), offset),
+                new DaysAdd(datetime.toSlot(), offset),
+                new TimestampArithmetic(Operator.ADD, datetime.toSlot(), 
offset, TimeUnit.WEEK),
+                new TimestampArithmetic(Operator.SUBTRACT, datetime.toSlot(), 
offset, TimeUnit.WEEK));
+    }
+
+    private List<Expression> dateExpressions() {
+        return 
ImmutableList.<Expression>builder().addAll(addSubExpressions(new 
IntegerLiteral(5)))
+                .add(new SecondCeil(datetime.toSlot(), new IntegerLiteral(5)),
+                        new YearCeil(datetime.toSlot(), new 
IntegerLiteral(5))).build();
+    }
+
+    @Test
+    void protectRejectingSetOperations() {
+        Alias rightDatetime = new Alias(new DateTimeV2Literal("2020-01-01 
00:00:00"), "right_dt");
+        Alias rightId = new Alias(new IntegerLiteral(1), "right_id");
+        LogicalOneRowRelation right = new 
LogicalOneRowRelation(StatementScopeIdGenerator.newRelationId(),
+                ImmutableList.of(rightDatetime, rightId));
+        List<Plan> children = ImmutableList.of(relation, right);
+        List<NamedExpression> setOutputs = ImmutableList.of(datetime.toSlot(), 
id.toSlot());
+        List<List<SlotReference>> outputs = ImmutableList.of(
+                ImmutableList.of((SlotReference) datetime.toSlot(), 
(SlotReference) id.toSlot()),
+                ImmutableList.of((SlotReference) rightDatetime.toSlot(), 
(SlotReference) rightId.toSlot()));
+        LogicalExcept except = new LogicalExcept(Qualifier.DISTINCT, 
setOutputs, outputs, children);
+        LogicalIntersect intersect = new LogicalIntersect(Qualifier.DISTINCT,
+                setOutputs, outputs, children);
+        PhysicalOneRowRelation source = new 
PhysicalOneRowRelation(relation.getRelationId(),
+                relation.getProjects(), relation.getLogicalProperties());
+        PhysicalOneRowRelation physicalRight = new 
PhysicalOneRowRelation(right.getRelationId(),
+                right.getProjects(), right.getLogicalProperties());
+        List<Plan> physicalChildren = ImmutableList.of(source, physicalRight);
+        assertRejectingBoundary(except, new PhysicalExcept(Qualifier.DISTINCT,
+                setOutputs, outputs, except.getLogicalProperties(), 
physicalChildren));
+        assertRejectingBoundary(intersect, new 
PhysicalIntersect(Qualifier.DISTINCT,
+                setOutputs, outputs, intersect.getLogicalProperties(), 
physicalChildren));
+    }
+
+    @Test
+    void protectGenerate() {
+        Explode explode = new Explode(new ArrayLiteral(ImmutableList.of(new 
IntegerLiteral(1))));
+        SlotReference item = new SlotReference("item", IntegerType.INSTANCE);
+        LogicalGenerate<?> generate = new 
LogicalGenerate<>(ImmutableList.of(explode),
+                ImmutableList.of(item), relation);
+        PhysicalOneRowRelation source = new 
PhysicalOneRowRelation(relation.getRelationId(),
+                relation.getProjects(), relation.getLogicalProperties());
+        assertRejectingBoundary(generate, new 
PhysicalGenerate<>(ImmutableList.of(explode),
+                ImmutableList.of(item), ImmutableList.of(), 
generate.getLogicalProperties(), source));
+    }
+
+    private void assertRejectingBoundary(Plan logicalInput, Plan 
physicalInput) {
+        for (int offset : new int[] {0, 5}) {
+            for (Expression expression : addSubExpressions(new 
IntegerLiteral(offset))) {
+                Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+                Expression predicate = new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-02-05 00:00:00"));
+                LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result), logicalInput);
+                LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+                Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                        .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+                Assertions.assertEquals(offset == 0 ? LogicalProject.class : 
LogicalFilter.class, rewritten.getClass());
+                PhysicalProject<?> physicalProject = new 
PhysicalProject<>(ImmutableList.of(result),
+                        project.getLogicalProperties(), physicalInput);
+                PhysicalFilter<?> physicalFilter = new 
PhysicalFilter<>(ImmutableSet.of(predicate),
+                        physicalProject.getLogicalProperties(), 
physicalProject);
+                Plan physicalRewritten = physicalFilter.accept(
+                        new 
org.apache.doris.nereids.processor.post.PushDownFilterThroughProject(), null);
+                Assertions.assertEquals(offset == 0 ? PhysicalProject.class : 
PhysicalFilter.class,
+                        physicalRewritten.getClass());
+            }
+        }
+    }
+
+    @Test
+    void keepLogicalDatePredicateAboveProject() {
+        for (Expression expression : dateExpressions()) {
+            Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+            LogicalFilter<LogicalOneRowRelation> input = new LogicalFilter<>(
+                    ImmutableSet.of(new EqualTo(id.toSlot(), new 
IntegerLiteral(0))), relation);
+            LogicalProject<LogicalFilter<LogicalOneRowRelation>> project = new 
LogicalProject<>(
+                    ImmutableList.of(result, id.toSlot()), input);
+            Expression predicate = new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"));
+            LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+            Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                    .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+            Assertions.assertInstanceOf(LogicalFilter.class, rewritten, 
expression.toSql());
+            Assertions.assertInstanceOf(LogicalProject.class, 
rewritten.child(0));
+        }
+    }
+
+    @Test
+    void pushZeroOffsetDatePredicateThroughLowerFilter() {
+        for (Expression zero : ImmutableList.of(new IntegerLiteral(0), new 
BigIntLiteral(0))) {
+            for (Expression expression : addSubExpressions(zero)) {
+                Alias result = new Alias(expression, "result");
+                LogicalFilter<?> input = new LogicalFilter<>(
+                        ImmutableSet.of(new EqualTo(id.toSlot(), new 
IntegerLiteral(0))), relation);
+                LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result, id.toSlot()), input);
+                Expression predicate = new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"));
+                LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+                Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                        .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+                Assertions.assertInstanceOf(LogicalProject.class, rewritten);
+                Assertions.assertInstanceOf(LogicalFilter.class, 
rewritten.child(0));
+
+                PhysicalOneRowRelation source = new 
PhysicalOneRowRelation(relation.getRelationId(),
+                        relation.getProjects(), 
relation.getLogicalProperties());
+                PhysicalFilter<?> physicalInput = new 
PhysicalFilter<>(input.getConjuncts(),
+                        source.getLogicalProperties(), source);
+                PhysicalProject<?> physicalProject = new 
PhysicalProject<>(project.getProjects(),
+                        project.getLogicalProperties(), physicalInput);
+                PhysicalFilter<?> physicalFilter = new 
PhysicalFilter<>(ImmutableSet.of(predicate),
+                        physicalProject.getLogicalProperties(), 
physicalProject);
+                Plan physicalRewritten = physicalFilter.accept(
+                        new 
org.apache.doris.nereids.processor.post.PushDownFilterThroughProject(), null);
+                Assertions.assertInstanceOf(PhysicalProject.class, 
physicalRewritten);
+                Assertions.assertInstanceOf(PhysicalFilter.class, 
physicalRewritten.child(0));
+            }
+        }
+    }
+
+    @Test
+    void pushLogicalDatePredicateWithoutLowerFilter() {
+        for (Expression expression : 
ImmutableList.<Expression>builder().addAll(dateExpressions())
+                .add(new DaysAdd(datetime.toSlot(), new 
IntegerLiteral(0))).build()) {
+            Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+            LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result, id.toSlot()), relation);
+            Expression predicate = new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"));
+            LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+            Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                    .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+            Assertions.assertInstanceOf(LogicalProject.class, rewritten, 
expression.toSql());
+            Assertions.assertInstanceOf(LogicalFilter.class, 
rewritten.child(0));
+        }
+    }
+
+    @Test
+    void pushPhysicalDatePredicateWithoutLowerFilter() {
+        for (Expression expression : 
ImmutableList.<Expression>builder().addAll(dateExpressions())
+                .add(new DaysAdd(datetime.toSlot(), new 
IntegerLiteral(0))).build()) {
+            Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+            PhysicalOneRowRelation source = new 
PhysicalOneRowRelation(relation.getRelationId(),
+                    relation.getProjects(), relation.getLogicalProperties());
+            LogicalProject<?> logicalProject = new 
LogicalProject<>(ImmutableList.of(result, id.toSlot()), relation);
+            PhysicalProject<?> project = new 
PhysicalProject<>(ImmutableList.of(result, id.toSlot()),
+                    logicalProject.getLogicalProperties(), source);
+            PhysicalFilter<?> filter = new PhysicalFilter<>(
+                    ImmutableSet.of(new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"))),
+                    project.getLogicalProperties(), project);
+            Plan rewritten = filter.accept(
+                    new 
org.apache.doris.nereids.processor.post.PushDownFilterThroughProject(), null);
+            Assertions.assertInstanceOf(PhysicalProject.class, rewritten, 
expression.toSql());
+            Assertions.assertInstanceOf(PhysicalFilter.class, 
rewritten.child(0));
+        }
+    }
+
+    @Test
+    void keepLogicalDatePredicateAboveJoinOrLimit() {
+        LogicalOneRowRelation right = new LogicalOneRowRelation(
+                StatementScopeIdGenerator.newRelationId(), 
ImmutableList.of(new Alias(new IntegerLiteral(1), "right")));
+        for (Plan input : ImmutableList.of(
+                new LogicalJoin<>(JoinType.INNER_JOIN, relation, right, new 
JoinReorderContext()),
+                new LogicalLimit<>(0, 0, LimitPhase.ORIGIN, relation))) {
+            Alias result = new Alias(new WeeksAdd(datetime.toSlot(), new 
IntegerLiteral(5)), "result");
+            LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result), input);
+            LogicalFilter<?> filter = new LogicalFilter<>(ImmutableSet.of(
+                    new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"))), project);
+            Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                    .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+            Assertions.assertInstanceOf(LogicalFilter.class, rewritten);
+            Assertions.assertInstanceOf(LogicalProject.class, 
rewritten.child(0));
+        }
+    }
+
+    @Test
+    void liftDateEvaluationAboveLimitProject() {
+        LogicalFilter<?> filtered = new LogicalFilter<>(
+                ImmutableSet.of(new EqualTo(id.toSlot(), new 
IntegerLiteral(1))), relation);
+        for (Plan input : ImmutableList.of(relation, filtered)) {
+            for (long offset : new long[] {1, 100, 0}) {
+                for (Expression expression : dateExpressions()) {
+                    Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+                    LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result), input);
+                    LogicalLimit<?> limit = new LogicalLimit<>(1, offset, 
LimitPhase.ORIGIN, project);
+                    LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(
+                            new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-02-05 00:00:00"))), limit);
+                    Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                            .applyTopDown(new 
PushDownFilterThroughProject()).getPlan();
+                    Assertions.assertInstanceOf(LogicalProject.class, 
rewritten);
+                    Assertions.assertInstanceOf(LogicalFilter.class, 
rewritten.child(0));
+                    Assertions.assertInstanceOf(LogicalLimit.class, 
rewritten.child(0).child(0));
+                    LogicalLimit<?> liftedLimit = (LogicalLimit<?>) 
rewritten.child(0).child(0);
+                    Assertions.assertEquals(offset, liftedLimit.getOffset());
+                    Assertions.assertEquals(1, liftedLimit.getLimit());
+                    Assertions.assertEquals(input.getClass(), 
liftedLimit.child().getClass());
+                }
+            }
+        }
+    }
+
+    @Test
+    void keepDatePredicateAboveCteConsumer() {
+        LogicalCTEConsumer consumer = new 
LogicalCTEConsumer(StatementScopeIdGenerator.newRelationId(),
+                StatementScopeIdGenerator.newCTEId(), "c", relation);
+        Alias result = new Alias(new WeeksAdd(consumer.getOutput().get(0), new 
IntegerLiteral(5)), "result");
+        LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result), consumer);
+        Expression predicate = new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"));
+        LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+        Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                .applyTopDown(new PushDownFilterThroughProject()).getPlan();
+        Assertions.assertInstanceOf(LogicalFilter.class, rewritten);
+        Assertions.assertInstanceOf(LogicalProject.class, rewritten.child(0));
+
+        PhysicalCTEConsumer physicalConsumer = new 
PhysicalCTEConsumer(consumer.getRelationId(), consumer.getCteId(),
+                consumer.getConsumerToProducerOutputMap(), 
consumer.getProducerToConsumerOutputMap(),
+                consumer.getLogicalProperties());
+        PhysicalProject<?> physicalProject = new 
PhysicalProject<>(ImmutableList.of(result),
+                project.getLogicalProperties(), physicalConsumer);
+        PhysicalFilter<?> physicalFilter = new 
PhysicalFilter<>(ImmutableSet.of(predicate),
+                physicalProject.getLogicalProperties(), physicalProject);
+        Plan physicalRewritten = physicalFilter.accept(
+                new 
org.apache.doris.nereids.processor.post.PushDownFilterThroughProject(), null);
+        Assertions.assertInstanceOf(PhysicalFilter.class, physicalRewritten);
+        Assertions.assertInstanceOf(PhysicalProject.class, 
physicalRewritten.child(0));
+    }
+
+    @Test
+    void stillPushLogicalPassthroughPredicate() {
+        Alias result = new Alias(new WeeksAdd(datetime.toSlot(), new 
IntegerLiteral(5)), "result");
+        LogicalProject<?> project = new 
LogicalProject<>(ImmutableList.of(result, id.toSlot()), relation);
+        Expression predicate = new EqualTo(id.toSlot(), new IntegerLiteral(0));
+        LogicalFilter<?> filter = new 
LogicalFilter<>(ImmutableSet.of(predicate), project);
+        Plan rewritten = 
PlanChecker.from(MemoTestUtils.createConnectContext(), filter)
+                .applyTopDown(new PushDownFilterThroughProject()).getPlan();
+        Assertions.assertInstanceOf(LogicalProject.class, rewritten);
+        Assertions.assertInstanceOf(LogicalFilter.class, rewritten.child(0));
+    }
+
+    @Test
+    void keepPhysicalDatePredicateAboveProject() {
+        for (Expression expression : dateExpressions()) {
+            Alias result = new Alias(new Cast(expression, 
DateTimeV2Type.of(0)), "result");
+            PhysicalOneRowRelation source = new 
PhysicalOneRowRelation(relation.getRelationId(),
+                    relation.getProjects(), relation.getLogicalProperties());
+            PhysicalFilter<?> input = new PhysicalFilter<>(
+                    ImmutableSet.of(new EqualTo(id.toSlot(), new 
IntegerLiteral(0))),
+                    source.getLogicalProperties(), source);
+            LogicalProject<?> logicalProject = new 
LogicalProject<>(ImmutableList.of(result, id.toSlot()), relation);
+            PhysicalProject<?> project = new 
PhysicalProject<>(ImmutableList.of(result, id.toSlot()),
+                    logicalProject.getLogicalProperties(), input);
+            PhysicalFilter<?> filter = new PhysicalFilter<>(
+                    ImmutableSet.of(new EqualTo(result.toSlot(), new 
DateTimeV2Literal("2020-01-01 00:00:00"))),
+                    project.getLogicalProperties(), project);
+            Plan rewritten = filter.accept(
+                    new 
org.apache.doris.nereids.processor.post.PushDownFilterThroughProject(), null);
+            Assertions.assertInstanceOf(PhysicalFilter.class, rewritten, 
expression.toSql());
+            Assertions.assertInstanceOf(PhysicalProject.class, 
rewritten.child(0));
+        }
+    }
+}
diff --git 
a/regression-test/data/external_table_p0/tvf/test_date_expression_filter.out 
b/regression-test/data/external_table_p0/tvf/test_date_expression_filter.out
new file mode 100644
index 00000000000..7bc7653a638
--- /dev/null
+++ b/regression-test/data/external_table_p0/tvf/test_date_expression_filter.out
@@ -0,0 +1,47 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !valid_rows --
+2020-02-05 00:00:00    1
+
+-- !shared_cte --
+2020-02-05 00:00:00
+2020-02-05 00:00:00
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !filtered_date --
+
+-- !valid_rows --
+2020-02-05 00:00:00    1
+
+-- !shared_cte --
+2020-02-05 00:00:00
+2020-02-05 00:00:00
+
+-- !partition_pruning --
+2
+
+-- !partition_pruning --
+1
+
+-- !zero_offset_partition_pruning --
+2
+
+-- !null --
+\N
+
diff --git 
a/regression-test/data/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.out
 
b/regression-test/data/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.out
new file mode 100644
index 00000000000..fe7329764f8
--- /dev/null
+++ 
b/regression-test/data/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !except --
+2020-02-05T00:00
+
+-- !intersect --
+2020-02-05T00:00
+
+-- !generate --
+2020-02-05T00:00
+
+-- !zero_offset --
+2020-01-01T00:00
+
diff --git 
a/regression-test/suites/external_table_p0/tvf/test_date_expression_filter.groovy
 
b/regression-test/suites/external_table_p0/tvf/test_date_expression_filter.groovy
new file mode 100644
index 00000000000..9e453ec3124
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/tvf/test_date_expression_filter.groovy
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_date_expression_filter", "p0,external,hive,external_docker") {
+    if 
(!context.config.otherConfigs.get("enableHiveTest")?.equalsIgnoreCase("true")) {
+        logger.info("Hive external tests are disabled")
+        return
+    }
+    def host = context.config.otherConfigs.get("externalEnvIp")
+    def port = context.config.otherConfigs.get("hive2HdfsPort")
+    def defaultFS = "hdfs://${host}:${port}"
+    def rows = """SELECT CAST(1 AS INT) AS id,
+                        CAST('9999-12-31 23:59:59' AS DATETIME(6)) AS dt,
+                        CAST('2025-01-01' AS DATETIME(6)) AS filter_dt
+                  UNION ALL SELECT 2, CAST('2020-01-01' AS DATETIME(6)),
+                                       CAST('2020-01-01' AS DATETIME(6))"""
+    sql "set short_circuit_evaluation=true"
+    for (def format : ["orc", "parquet"]) {
+        def path = 
"${defaultFS}/user/doris/tmp_data/test_date_expression_filter/${UUID.randomUUID()}/exp_"
+        def exported = sql """SELECT * FROM (${rows}) AS input
+            INTO OUTFILE "${path}" FORMAT AS ${format}
+            PROPERTIES("fs.defaultFS"="${defaultFS}", 
"hadoop.username"="doris")"""
+        def source = """HDFS("uri"="${exported[0][3]}",
+                             "hadoop.username"="doris", 
"format"="${format}")"""
+        for (def function : ["WEEKS_ADD", "HOURS_ADD", "SECOND_CEIL", 
"YEAR_CEIL", "DAYS_ADD"]) {
+            // The outer predicate must not evaluate a partial date function 
before the inner WHERE.
+            order_qt_filtered_date """WITH cte1 AS (
+                SELECT CAST(${function}(dt,5) AS DATETIME) AS d,
+                       APPROX_COUNT_DISTINCT(id) AS n
+                FROM ${source}
+                WHERE filter_dt <= (SELECT MIN('2024-07-01') FROM ${source})
+                  AND CAST(WEEKS_SUB(filter_dt,0) AS DATETIME)='2012-05-05 
11:11:11'
+                GROUP BY d ORDER BY d,n
+            ), cte2 AS (SELECT * FROM cte1 WHERE d='2019-10-23 16:50:42')
+            SELECT * FROM cte2"""
+        }
+        order_qt_valid_rows """WITH cte AS (
+            SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME) AS d, 
APPROX_COUNT_DISTINCT(id) AS n
+            FROM ${source} WHERE filter_dt='2020-01-01' GROUP BY d
+        ) SELECT DATE_FORMAT(d,'%Y-%m-%d %H:%i:%s'),n FROM cte WHERE 
d='2020-02-05'"""
+        // Shared CTE predicates may be collected and pushed back into a 
producer with a hidden WHERE.
+        def inlineThreshold = sql("SHOW VARIABLES LIKE 
'inline_cte_referenced_threshold'")[0][1]
+        sql "set inline_cte_referenced_threshold=0"
+        try {
+            def branch = """SELECT DATE_FORMAT(d,'%Y-%m-%d %H:%i:%s') FROM (
+                SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME) AS d FROM 
filtered_input
+            ) s WHERE d='2020-02-05'"""
+            def sharedCte = """WITH filtered_input AS (
+                SELECT dt FROM ${source} WHERE filter_dt='2020-01-01'
+            ) ${branch} UNION ALL ${branch}"""
+            order_qt_shared_cte sharedCte
+        } finally {
+            sql "set inline_cte_referenced_threshold=${inlineThreshold}"
+        }
+        // Overflow on a row that actually participates in date arithmetic 
must still be reported.
+        test {
+            sql "SELECT WEEKS_ADD(dt,5) FROM ${source}"
+            exception "Operation week_add of 9999-12-31 23:59:59, 5 out of 
range"
+        }
+    }
+    sql "DROP TABLE IF EXISTS test_date_expression_filter_partition_pruning"
+    // Explicit partitions make the safe alias predicate's pruning boundary 
observable.
+    sql """CREATE TABLE test_date_expression_filter_partition_pruning (dt DATE 
NOT NULL, id INT)
+        DUPLICATE KEY(dt, id)
+        PARTITION BY RANGE(dt) (
+            PARTITION p1 VALUES [('2020-01-01'), ('2020-01-02')),
+            PARTITION p2 VALUES [('2020-01-02'), ('2020-01-03')),
+            PARTITION p3 VALUES [('2020-01-03'), ('2020-01-04'))
+        )
+        DISTRIBUTED BY HASH(dt) BUCKETS 3
+        PROPERTIES("replication_num"="1")"""
+    sql "INSERT INTO test_date_expression_filter_partition_pruning VALUES 
('2020-01-01',1),('2020-01-02',2),('2020-01-03',3)"
+    for (def offset : [0, 1]) {
+        def query = """SELECT id FROM (
+            SELECT DAYS_ADD(dt,${offset}) AS d,id FROM 
test_date_expression_filter_partition_pruning
+        ) s WHERE d='2020-01-02'"""
+        explain {
+            sql query
+            contains "partitions=1/3 (p${2 - offset})"
+        }
+        order_qt_partition_pruning query
+    }
+    // A zero-offset alias stays safe even with an unrelated lower WHERE.
+    def zeroOffsetQuery = """SELECT id FROM (
+        SELECT DAYS_ADD(dt,0) AS d,id FROM 
test_date_expression_filter_partition_pruning WHERE id > 0
+    ) s WHERE d='2020-01-02'"""
+    explain {
+        sql zeroOffsetQuery
+        contains "partitions=1/3 (p2)"
+    }
+    order_qt_zero_offset_partition_pruning zeroOffsetQuery
+    order_qt_null "SELECT WEEKS_ADD(CAST(NULL AS DATETIME),5)"
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.groovy
 
b/regression-test/suites/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.groovy
new file mode 100644
index 00000000000..b3fcb5399ce
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/filter_push_down/test_date_arithmetic_rejecting_boundary.groovy
@@ -0,0 +1,102 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_date_arithmetic_rejecting_boundary") {
+    sql "DROP TABLE IF EXISTS test_date_boundary_input"
+    sql "DROP TABLE IF EXISTS test_date_boundary_right"
+    sql """CREATE TABLE test_date_boundary_input (id INT, dt DATETIME(6), arr 
ARRAY<INT>)
+        DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES("replication_num"="1")"""
+    sql """CREATE TABLE test_date_boundary_right (dt DATETIME(6))
+        DUPLICATE KEY(dt) DISTRIBUTED BY HASH(dt) BUCKETS 1
+        PROPERTIES("replication_num"="1")"""
+    sql """INSERT INTO test_date_boundary_input VALUES
+        (1,'9999-12-31 23:59:59.000001',[]), (2,'2020-01-01 
00:00:00.000001',[1])"""
+    sql "INSERT INTO test_date_boundary_right VALUES ('9999-12-31 
23:59:59.000001')"
+
+    // The cast narrows precision so inverse date comparison rewriting cannot 
hide early evaluation.
+    // No optimizer rule is disabled: EXCEPT must reject the overflowing row 
before arithmetic.
+    order_qt_except """SELECT d FROM (
+        SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME(0)) AS d FROM (
+            SELECT dt FROM test_date_boundary_input
+            EXCEPT SELECT dt FROM test_date_boundary_right
+        ) difference
+    ) s WHERE d='2020-02-05'"""
+
+    // The compensating rewrite must restore date evaluation above the global 
OFFSET.
+    // The right-hand table still contains exactly one maximum-date row here.
+    for (def offset : [1, 100]) {
+        assert sql("""SELECT d FROM (
+            SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME(0)) AS d FROM (
+                SELECT dt FROM test_date_boundary_right LIMIT 1 OFFSET 
${offset}
+            ) limited
+        ) s WHERE d='2020-02-05'""").isEmpty()
+    }
+    test {
+        sql """SELECT d FROM (
+            SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME(0)) AS d FROM (
+                SELECT dt FROM test_date_boundary_right LIMIT 1 OFFSET 0
+            ) limited
+        ) s WHERE d='2020-02-05'"""
+        exception "out of range"
+    }
+
+    // INTERVAL syntax retains TimestampArithmetic rather than the scalar 
function node.
+    for (def scenario : [
+            ["dt + INTERVAL 5 WEEK", "2020-02-05"],
+            ["dt - INTERVAL -5 WEEK", "2020-02-05"],
+            ["dt - INTERVAL 5 WEEK", "2019-11-27"],
+            ["dt + INTERVAL 0 WEEK", "2020-01-01"],
+            ["dt - INTERVAL 0 WEEK", "2020-01-01"]]) {
+        def actual = sql """SELECT DATE_FORMAT(d, '%Y-%m-%d %H:%i:%s') FROM (
+            SELECT CAST(${scenario[0]} AS DATETIME(0)) AS d FROM (
+                SELECT dt FROM test_date_boundary_input
+                EXCEPT SELECT dt FROM test_date_boundary_right
+            ) difference
+        ) s WHERE d='${scenario[1]}'"""
+        assert actual == [[scenario[1] + " 00:00:00"]]
+    }
+    test {
+        sql "SELECT dt + INTERVAL 5 WEEK FROM test_date_boundary_input"
+        exception "out of range"
+    }
+
+    sql "TRUNCATE TABLE test_date_boundary_right"
+    sql "INSERT INTO test_date_boundary_right VALUES ('2020-01-01 
00:00:00.000001')"
+    order_qt_intersect """SELECT d FROM (
+        SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME(0)) AS d FROM (
+            SELECT dt FROM test_date_boundary_input
+            INTERSECT SELECT dt FROM test_date_boundary_right
+        ) common_rows
+    ) s WHERE d='2020-02-05'"""
+
+    // Non-outer explode drops the overflowing row because its array is empty.
+    order_qt_generate """SELECT d FROM (
+        SELECT CAST(WEEKS_ADD(dt,5) AS DATETIME(0)) AS d
+        FROM test_date_boundary_input LATERAL VIEW explode(arr) lv AS item
+    ) s WHERE d='2020-02-05'"""
+
+    // Identity date arithmetic and ordinary aliases remain pushable at these 
boundaries.
+    order_qt_zero_offset """SELECT d FROM (
+        SELECT CAST(WEEKS_ADD(dt,0) AS DATETIME(0)) AS d
+        FROM test_date_boundary_input LATERAL VIEW explode(arr) lv AS item
+    ) s WHERE d='2020-01-01'"""
+    test {
+        sql "SELECT WEEKS_ADD(dt,5) FROM test_date_boundary_input"
+        exception "out of range"
+    }
+}


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

Reply via email to