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

924060929 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 3cfbf220312 [fix](nereids) fix limit + offset overflow when pushing 
down TopN/Limit (#64633)
3cfbf220312 is described below

commit 3cfbf220312f601e87dcdedb3b8efbdb0882e4bd
Author: 924060929 <[email protected]>
AuthorDate: Thu Jul 2 11:30:04 2026 +0800

    [fix](nereids) fix limit + offset overflow when pushing down TopN/Limit 
(#64633)
    
    When combining a `limit` and an `offset` into the number of rows a child
    must keep — pushing a
    `TopN`/`Limit` down, building a two-phase sort, splitting a limit into
    local + global phases, etc. —
    the raw `limit + offset` overflows the `long` range when both are close
    to `BIGINT_MAX`
    (e.g. `LIMIT 9223372036854775807 OFFSET 9223372036854775807`) and wraps
    to a negative value.
    
    A negative limit is an illegal plan. On the BE side it is reinterpreted
    as a huge unsigned value
    (`uint64_t limit = _offset + _limit` in the sorter / `_heap_size(limit +
    offset)` in HeapSorter),
    so a trivial query that should immediately return an empty set instead
    runs until it hits the query
    timeout, or triggers UBSan (signed integer overflow is undefined
    behavior in C++).
    
    ### Minimal reproducer (no table required)
    
    ```sql
    select count(*) as c from (
        select id from (
            select 1 as id union all select 2 as id union all select 3 as id
        ) t
        order by id limit 9223372036854775807 offset 9223372036854775807
    ) s;
    ```
    
    - Original planner, or Nereids with `PUSH_DOWN_TOP_N_THROUGH_UNION`
    disabled: returns `0` immediately.
    - Nereids with the rule enabled: times out (before this fix).
    
    ### Fix
    
    Add `Utils.addOverflows(long, long)` to detect when `limit + offset`
    exceeds the `long` range.
    On overflow, the behavior depends on whether the code path can be
    declined:
    
    **Skip the optimization** (can decline — the unoptimized plan is
    correct):
    - `TopN`/`Limit` push-down through `Union` / `Join` / `Project-Join` /
    `Window` and their distinct
    variants skip the rewrite; the parent `TopN`/`Limit` still applies the
    original `limit`/`offset`.
    - The `topn_opt_limit_threshold` gates (`PushDownTopNThroughJoin` /
    `PushDownTopNDistinctThroughJoin` /
    `PushDownLimitDistinctThroughJoin` / `LimitAggToTopNAgg` /
    `PushTopnToAgg`) treat overflow as
      over-threshold and do not fire.
    - The score / vector `TopN`-into-`OlapScan` push-downs and the
    sort-limit-into-`OlapScan` translation
      in `PhysicalPlanTranslator` are skipped.
    - `CollectLimitAboveConsumer` skips recording the consumer row count on
    overflow, leaving the CTE
      producer unbounded (the consumer's own limit still applies).
    
    **Report an error** (cannot decline — overflowing `limit + offset`
    cannot be correctly represented):
    - `LogicalTopNToPhysicalTopN`: the two-phase local sort needs `limit +
    offset` rows, and even a
    single-phase `GATHER_SORT` would pass the overflowing values to BE where
    `HeapSorter` also computes
    `limit + offset` (triggering UBSan). Since no valid physical plan can be
    produced, report an error.
    - `SplitLimit`: the two-phase local limit needs `limit + offset`; an
    unsplit `ORIGIN` limit is silently
    dropped during translation, and `Long.MAX_VALUE` is not treated as
    "unlimited" by the legacy
      `PlanNode` layer (`hasLimit()` returns `limit > -1`). Report an error.
    - `MergeLimits.mergeOffset` / `MergeTopNs`: consecutive limits/TopNs
    must be merged, and the merged
    offset (`upperOffset + bottomOffset`) cannot be represented on overflow.
    Report an error.
    
    For non-overflowing inputs the behavior is unchanged.
    
    **Defense-in-depth**: the `LogicalTopN` / `PhysicalTopN` /
    `LogicalLimit` / `PhysicalLimit`
    constructors now assert that `limit` and `offset` are non-negative. Any
    future path that reintroduces
    an overflowing `limit + offset` fails fast during planning instead of
    silently hanging in BE.
    
    ### Tests
    
    - `UtilsTest#testAddOverflows` covers the overflow detection boundary.
    - Regression cases in `push_down_top_n_through_union` assert that the
    reproducer (both TopN and Limit
    paths with BIGINT_MAX limit/offset) reports an error instead of timing
    out.
---
 .../glue/translator/PhysicalPlanTranslator.java    |  8 ++++++-
 .../nereids/processor/post/PushTopnToAgg.java      |  6 +++++
 .../implementation/LogicalTopNToPhysicalTopN.java  | 15 +++++++++---
 .../rules/rewrite/CollectLimitAboveConsumer.java   | 13 +++++++++--
 .../nereids/rules/rewrite/LimitAggToTopNAgg.java   |  5 ++++
 .../doris/nereids/rules/rewrite/MergeLimits.java   | 11 +++++++++
 .../doris/nereids/rules/rewrite/MergeTopNs.java    |  6 +++++
 .../rewrite/PushDownLimitDistinctThroughJoin.java  |  8 +++++++
 .../rewrite/PushDownLimitDistinctThroughUnion.java |  7 ++++++
 .../rewrite/PushDownScoreTopNIntoOlapScan.java     |  7 ++++++
 .../rewrite/PushDownTopNDistinctThroughJoin.java   | 18 +++++++++++----
 .../rewrite/PushDownTopNDistinctThroughUnion.java  | 11 ++++++++-
 .../rules/rewrite/PushDownTopNThroughJoin.java     | 16 ++++++++++---
 .../rules/rewrite/PushDownTopNThroughUnion.java    | 12 ++++++++--
 .../rules/rewrite/PushDownTopNThroughWindow.java   | 11 +++++++++
 .../rewrite/PushDownVectorTopNIntoOlapScan.java    |  7 ++++++
 .../doris/nereids/rules/rewrite/SplitLimit.java    |  9 +++++++-
 .../nereids/trees/plans/logical/LogicalLimit.java  |  6 +++++
 .../nereids/trees/plans/logical/LogicalTopN.java   |  5 ++++
 .../trees/plans/physical/PhysicalLimit.java        | 10 ++++++++
 .../nereids/trees/plans/physical/PhysicalTopN.java |  5 ++++
 .../java/org/apache/doris/nereids/util/Utils.java  | 16 +++++++++++++
 .../org/apache/doris/nereids/util/UtilsTest.java   | 17 ++++++++++++++
 .../push_down_top_n_through_union.groovy           | 27 ++++++++++++++++++++++
 24 files changed, 239 insertions(+), 17 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
index 1c037c65edb..002cec00050 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PhysicalPlanTranslator.java
@@ -2609,7 +2609,13 @@ public class PhysicalPlanTranslator extends 
DefaultPlanVisitor<PlanFragment, Pla
             // push sort to scan opt
             if (sortNode.getChild(0) instanceof OlapScanNode) {
                 OlapScanNode scanNode = ((OlapScanNode) sortNode.getChild(0));
-                if (checkPushSort(sortNode, scanNode.getOlapTable())) {
+                // When the offset is set, the pushed scan limit is limit + 
offset. If that overflows the
+                // long range it would wrap to a negative limit, so skip the 
whole push-sort-to-scan
+                // optimization (do not push the sort info either); the sort 
node still applies the real
+                // limit/offset. Sort info and sort limit are always pushed 
together.
+                boolean limitOverflows = sortNode.getOffset() > 0
+                        && Utils.addOverflows(sortNode.getLimit(), 
sortNode.getOffset());
+                if (checkPushSort(sortNode, scanNode.getOlapTable()) && 
!limitOverflows) {
                     SortInfo sortInfo = sortNode.getSortInfo();
                     scanNode.setSortInfo(sortInfo);
                     if (sortNode.getOffset() > 0) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushTopnToAgg.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushTopnToAgg.java
index 44bbfacc794..768a15143e2 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushTopnToAgg.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/PushTopnToAgg.java
@@ -28,6 +28,7 @@ import 
org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate.TopnP
 import org.apache.doris.nereids.trees.plans.physical.PhysicalProject;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
 import org.apache.doris.nereids.util.AggregateUtils;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 /**
@@ -49,6 +50,11 @@ public class PushTopnToAgg extends PlanPostProcessor {
     @Override
     public Plan visitPhysicalTopN(PhysicalTopN<? extends Plan> topN, 
CascadesContext ctx) {
         topN.child().accept(this, ctx);
+        if (Utils.addOverflows(topN.getLimit(), topN.getOffset())) {
+            // limit + offset overflows the long range: no aggregate can hold 
that many rows, so
+            // pushing a topn limit into the aggregate cannot reduce anything; 
leave the topn as is.
+            return topN;
+        }
         if (ConnectContext.get().getSessionVariable().topnOptLimitThreshold <= 
topN.getLimit() + topN.getOffset()
                 && !ConnectContext.get().getSessionVariable().pushTopnToAgg) {
             return topN;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalTopNToPhysicalTopN.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalTopNToPhysicalTopN.java
index 2371407555b..359e4b0bf9c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalTopNToPhysicalTopN.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/implementation/LogicalTopNToPhysicalTopN.java
@@ -17,12 +17,14 @@
 
 package org.apache.doris.nereids.rules.implementation;
 
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.SortPhase;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.trees.plans.physical.PhysicalTopN;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.Lists;
@@ -46,13 +48,20 @@ public class LogicalTopNToPhysicalTopN extends 
OneImplementationRuleFactory {
      *     mergeTopN(limit, off, require gather) -> localTopN(off+limit, 0, 
require any)
      */
     private List<PhysicalTopN<? extends Plan>> twoPhaseSort(LogicalTopN<? 
extends Plan> logicalTopN) {
-        PhysicalTopN<Plan> localSort = new 
PhysicalTopN<>(logicalTopN.getOrderKeys(),
-                logicalTopN.getLimit() + logicalTopN.getOffset(), 0, 
SortPhase.LOCAL_SORT,
-                logicalTopN.getLogicalProperties(), logicalTopN.child(0));
         int sortPhaseNum = 0;
         if (ConnectContext.get() != null) {
             sortPhaseNum = 
ConnectContext.get().getSessionVariable().sortPhaseNum;
         }
+        // The local sort keeps limit + offset rows. When that overflows the 
long range (e.g. LIMIT
+        // and OFFSET both BIGINT_MAX), we cannot produce a valid physical 
TopN: the two-phase
+        // MERGE_SORT needs a local limit that overflows, and even a 
single-phase GATHER_SORT would
+        // pass the overflowing limit/offset to BE where HeapSorter also 
computes limit + offset.
+        if (Utils.addOverflows(logicalTopN.getLimit(), 
logicalTopN.getOffset())) {
+            throw new AnalysisException("limit + offset overflows the long 
range");
+        }
+        PhysicalTopN<Plan> localSort = new 
PhysicalTopN<>(logicalTopN.getOrderKeys(),
+                logicalTopN.getLimit() + logicalTopN.getOffset(), 0, 
SortPhase.LOCAL_SORT,
+                logicalTopN.getLogicalProperties(), logicalTopN.child(0));
         if (sortPhaseNum == 1) {
             PhysicalTopN<Plan> onePhaseSort = new 
PhysicalTopN<>(logicalTopN.getOrderKeys(), logicalTopN.getLimit(),
                     logicalTopN.getOffset(), SortPhase.GATHER_SORT,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
index bb573304e3a..0b0009fcdaf 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CollectLimitAboveConsumer.java
@@ -23,6 +23,7 @@ import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
 import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -52,7 +53,15 @@ public class CollectLimitAboveConsumer implements 
RewriteRuleFactory {
 
     private void collectLimitRows(CascadesContext cascadesContext, 
LogicalLimit<?> limit,
             LogicalCTEConsumer cteConsumer) {
-        cascadesContext.putConsumerIdToLimitRows(
-                cteConsumer.getRelationId(), limit.getLimit() + 
limit.getOffset());
+        // The recorded value is the number of rows this consumer needs (limit 
+ offset). When that
+        // overflows the long range the consumer effectively needs all rows, 
so do not record anything:
+        // tryToConstructLimit treats a missing entry as "unbounded" and 
leaves the producer unlimited
+        // (this consumer's own limit still applies above it). Recording a 
wrapped-around negative count
+        // would corrupt the producer row bound.
+        if (Utils.addOverflows(limit.getLimit(), limit.getOffset())) {
+            return;
+        }
+        cascadesContext.putConsumerIdToLimitRows(cteConsumer.getRelationId(),
+                limit.getLimit() + limit.getOffset());
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LimitAggToTopNAgg.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LimitAggToTopNAgg.java
index f2ca5e68817..a209a1dd2da 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LimitAggToTopNAgg.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/LimitAggToTopNAgg.java
@@ -30,6 +30,7 @@ import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
@@ -57,6 +58,7 @@ public class LimitAggToTopNAgg implements RewriteRuleFactory {
                 logicalLimit(logicalAggregate())
                         .when(limit -> ConnectContext.get() != null
                                 && 
ConnectContext.get().getSessionVariable().pushTopnToAgg
+                                && !Utils.addOverflows(limit.getLimit(), 
limit.getOffset())
                                 && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                 >= limit.getLimit() + limit.getOffset())
                         .when(limit -> {
@@ -73,6 +75,7 @@ public class LimitAggToTopNAgg implements RewriteRuleFactory {
                 logicalLimit(logicalProject(logicalAggregate()))
                         .when(limit -> ConnectContext.get() != null
                                 && 
ConnectContext.get().getSessionVariable().pushTopnToAgg
+                                && !Utils.addOverflows(limit.getLimit(), 
limit.getOffset())
                                 && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                 >= limit.getLimit() + limit.getOffset())
                         .when(limit -> {
@@ -93,6 +96,7 @@ public class LimitAggToTopNAgg implements RewriteRuleFactory {
                 logicalTopN(logicalAggregate())
                         .when(topn -> ConnectContext.get() != null
                                 && 
ConnectContext.get().getSessionVariable().pushTopnToAgg
+                                && !Utils.addOverflows(topn.getLimit(), 
topn.getOffset())
                                 && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                 >= topn.getLimit() + topn.getOffset())
                         .when(topn -> {
@@ -115,6 +119,7 @@ public class LimitAggToTopNAgg implements 
RewriteRuleFactory {
                 logicalTopN(logicalProject(logicalAggregate()))
                         .when(topn -> ConnectContext.get() != null
                                 && 
ConnectContext.get().getSessionVariable().pushTopnToAgg
+                                && !Utils.addOverflows(topn.getLimit(), 
topn.getOffset())
                                 && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                 >= topn.getLimit() + topn.getOffset())
                         .when(topn -> {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeLimits.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeLimits.java
index e544eed07ce..e9e466123b0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeLimits.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeLimits.java
@@ -17,10 +17,12 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.util.Utils;
 
 /**
  * This rule aims to merge consecutive limits.
@@ -54,7 +56,16 @@ public class MergeLimits extends OneRewriteRuleFactory {
                 }).toRule(RuleType.MERGE_LIMITS);
     }
 
+    /**
+     * Merge two consecutive limits' offsets into a single offset. Consecutive 
limits must be merged,
+     * and an overflowing combined offset cannot be represented as a single 
offset, so fail fast
+     * instead of wrapping to a negative offset.
+     */
     public static long mergeOffset(long upperOffset, long bottomOffset) {
+        if (Utils.addOverflows(upperOffset, bottomOffset)) {
+            throw new AnalysisException(
+                    "offset overflows long range when merging limits: " + 
upperOffset + " + " + bottomOffset);
+        }
         return upperOffset + bottomOffset;
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
index 27ef659118a..f4549ea9bdd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/MergeTopNs.java
@@ -17,11 +17,13 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.properties.OrderKey;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
+import org.apache.doris.nereids.util.Utils;
 
 import java.util.List;
 
@@ -57,6 +59,10 @@ public class MergeTopNs extends OneRewriteRuleFactory {
                     long limit = topN.getLimit();
                     long childOffset = childTopN.getOffset();
                     long childLimit = childTopN.getLimit();
+                    if (Utils.addOverflows(offset, childOffset)) {
+                        throw new AnalysisException(
+                                "offset overflows long range when merging 
TopNs: " + offset + " + " + childOffset);
+                    }
                     long newOffset = offset + childOffset;
                     // The parent's offset is applied on top of the child's 
output, so only
                     // (childLimit - offset) of the child's rows survive. 
Clamp the merged limit
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughJoin.java
index 906007decf4..960e0e0b128 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughJoin.java
@@ -26,6 +26,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
 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.LogicalProject;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
@@ -46,6 +47,7 @@ public class PushDownLimitDistinctThroughJoin implements 
RewriteRuleFactory {
                         .when(LogicalAggregate::isDistinct))
                         .when(limit ->
                                 ConnectContext.get() != null
+                                        && 
!Utils.addOverflows(limit.getLimit(), limit.getOffset())
                                         && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                         >= limit.getLimit() + 
limit.getOffset())
                         .then(limit -> {
@@ -64,6 +66,12 @@ public class PushDownLimitDistinctThroughJoin implements 
RewriteRuleFactory {
                 
logicalLimit(logicalAggregate(logicalProject(logicalJoin()).when(LogicalProject::isAllSlots))
                         .when(LogicalAggregate::isDistinct))
                         .then(limit -> {
+                            // limit + offset overflowing means no child can 
hold that many rows, so the
+                            // push-down cannot reduce anything; skip it. (The 
direct branch is gated the
+                            // same way via topn_opt_limit_threshold.)
+                            if (Utils.addOverflows(limit.getLimit(), 
limit.getOffset())) {
+                                return null;
+                            }
                             LogicalAggregate<LogicalProject<LogicalJoin<Plan, 
Plan>>> agg = limit.child();
                             LogicalProject<LogicalJoin<Plan, Plan>> project = 
agg.child();
                             LogicalJoin<Plan, Plan> join = project.child();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughUnion.java
index 1a136673a1d..fd75ca129cc 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownLimitDistinctThroughUnion.java
@@ -27,6 +27,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
 import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
 import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -64,6 +65,12 @@ public class PushDownLimitDistinctThroughUnion implements 
RewriteRuleFactory {
                 logicalLimit(logicalAggregate(logicalUnion().when(union -> 
union.getQualifier() == Qualifier.ALL))
                         .when(agg -> agg.isDistinct()))
                         .then(limit -> {
+                            // limit + offset overflowing the long range means 
no child can hold that
+                            // many rows, so pushing the limit below the union 
cannot reduce anything;
+                            // skip the rewrite. The parent limit still 
applies the original limit/offset.
+                            if (Utils.addOverflows(limit.getLimit(), 
limit.getOffset())) {
+                                return null;
+                            }
                             LogicalAggregate<LogicalUnion> agg = limit.child();
                             LogicalUnion union = agg.child();
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java
index d24a6018438..1395c2febb0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java
@@ -44,6 +44,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.thrift.TExprOpcode;
 
 import com.google.common.collect.ImmutableList;
@@ -188,6 +189,12 @@ public class PushDownScoreTopNIntoOlapScan implements 
RewriteRuleFactory {
                             + " for push down optimization");
         }
 
+        // When limit + offset overflows the long range, the pushed scan limit 
would wrap to a
+        // negative value; skip the push-down and let the TopN above the scan 
apply limit/offset.
+        if (Utils.addOverflows(topN.getLimit(), topN.getOffset())) {
+            return null;
+        }
+
         // All conditions met, perform the push down.
         // This is the core action: push score() as a virtual column and also 
push the
         // topN info.
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughJoin.java
index 4b0b8a89dc4..a67f06decd7 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughJoin.java
@@ -28,6 +28,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.util.PlanUtils;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
@@ -49,6 +50,7 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
                         // TODO: complex order by
                         .when(topn ->
                                 ConnectContext.get() != null
+                                        && 
!Utils.addOverflows(topn.getLimit(), topn.getOffset())
                                         && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                         >= topn.getLimit() + topn.getOffset())
                         .when(topN -> 
topN.getOrderKeys().stream().map(OrderKey::getExpr)
@@ -70,6 +72,13 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
                         .when(topN -> 
topN.getOrderKeys().stream().map(OrderKey::getExpr)
                                 .allMatch(Slot.class::isInstance))
                         .then(topN -> {
+                            // limit + offset overflowing the long range means 
no child can hold that
+                            // many rows, so pushing the TopN below the join 
cannot reduce anything;
+                            // skip the rewrite. (The direct branch is gated 
the same way via
+                            // topn_opt_limit_threshold.)
+                            if (Utils.addOverflows(topN.getLimit(), 
topN.getOffset())) {
+                                return null;
+                            }
                             LogicalAggregate<LogicalProject<LogicalJoin<Plan, 
Plan>>> distinct = topN.child();
                             LogicalProject<LogicalJoin<Plan, Plan>> project = 
distinct.child();
                             LogicalJoin<Plan, Plan> join = project.child();
@@ -95,6 +104,7 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
     }
 
     private Plan pushTopNThroughJoin(LogicalTopN<? extends Plan> topN, 
LogicalJoin<Plan, Plan> join) {
+        long childLimit = topN.getLimit() + topN.getOffset();
         Set<Slot> groupBySlots = ((LogicalAggregate<?>) 
topN.child()).getGroupByExpressions().stream()
                 .flatMap(e -> 
e.getInputSlots().stream()).collect(Collectors.toSet());
         switch (join.getJoinType()) {
@@ -107,7 +117,7 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
                         join.left().getOutputSet(), topN.getOrderKeys());
                 if (!pushedOrderKeys.isEmpty()) {
                     LogicalTopN<Plan> left = topN.withLimitOrderKeyAndChild(
-                            topN.getLimit() + topN.getOffset(), 0, 
pushedOrderKeys,
+                            childLimit, 0, pushedOrderKeys,
                             PlanUtils.distinct(join.left()));
                     return join.withChildren(left, join.right());
                 }
@@ -122,7 +132,7 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
                         join.right().getOutputSet(), topN.getOrderKeys());
                 if (!pushedOrderKeys.isEmpty()) {
                     LogicalTopN<Plan> right = topN.withLimitOrderKeyAndChild(
-                            topN.getLimit() + topN.getOffset(), 0, 
pushedOrderKeys,
+                            childLimit, 0, pushedOrderKeys,
                             PlanUtils.distinct(join.right()));
                     return join.withChildren(join.left(), right);
                 }
@@ -135,14 +145,14 @@ public class PushDownTopNDistinctThroughJoin implements 
RewriteRuleFactory {
                         join.left().getOutputSet(), topN.getOrderKeys());
                 if (!(join.left() instanceof TopN) && 
!leftPushedOrderKeys.isEmpty()) {
                     leftChild = topN.withLimitOrderKeyAndChild(
-                            topN.getLimit() + topN.getOffset(), 0, 
leftPushedOrderKeys,
+                            childLimit, 0, leftPushedOrderKeys,
                             PlanUtils.distinct(join.left()));
                 }
                 List<OrderKey> rightPushedOrderKeys = 
getPushedOrderKeys(groupBySlots,
                         join.right().getOutputSet(), topN.getOrderKeys());
                 if (!(join.right() instanceof TopN) && 
!rightPushedOrderKeys.isEmpty()) {
                     rightChild = topN.withLimitOrderKeyAndChild(
-                            topN.getLimit() + topN.getOffset(), 0, 
rightPushedOrderKeys,
+                            childLimit, 0, rightPushedOrderKeys,
                             PlanUtils.distinct(join.right()));
                 }
                 if (leftChild == join.left() && rightChild == join.right()) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughUnion.java
index cde42c7ad24..3e0dee97aca 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNDistinctThroughUnion.java
@@ -29,6 +29,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
 import org.apache.doris.nereids.util.ExpressionUtils;
 import org.apache.doris.nereids.util.PlanUtils;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -65,6 +66,14 @@ public class PushDownTopNDistinctThroughUnion implements 
RewriteRuleFactory {
                 logicalTopN(logicalAggregate(logicalUnion().when(union -> 
union.getQualifier() == Qualifier.ALL))
                         .when(agg -> agg.isDistinct()))
                         .then(topN -> {
+                            // The child TopN must keep limit + offset rows. 
When that sum overflows the
+                            // long range (e.g. LIMIT and OFFSET both 
BIGINT_MAX) no child can hold that many
+                            // rows, so pushing down cannot reduce anything 
and would create an illegal
+                            // negative limit. Skip the rewrite; the parent 
TopN still applies limit/offset.
+                            if (Utils.addOverflows(topN.getLimit(), 
topN.getOffset())) {
+                                return null;
+                            }
+                            long childLimit = topN.getLimit() + 
topN.getOffset();
                             LogicalAggregate<LogicalUnion> agg = topN.child();
                             LogicalUnion union = agg.child();
                             List<Plan> newChildren = new ArrayList<>();
@@ -78,7 +87,7 @@ public class PushDownTopNDistinctThroughUnion implements 
RewriteRuleFactory {
                                         .map(orderKey -> 
orderKey.withExpression(
                                                 
ExpressionUtils.replace(orderKey.getExpr(), replaceMap)))
                                         
.collect(ImmutableList.toImmutableList());
-                                newChildren.add(new LogicalTopN<>(orderKeys, 
topN.getLimit() + topN.getOffset(), 0,
+                                newChildren.add(new LogicalTopN<>(orderKeys, 
childLimit, 0,
                                         
PlanUtils.distinct(union.child(childIdx))));
                             }
                             if (union.children().equals(newChildren)) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughJoin.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughJoin.java
index 14d56ad9172..69dfffd5af3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughJoin.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughJoin.java
@@ -28,6 +28,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.util.PlanUtils;
+import org.apache.doris.nereids.util.Utils;
 import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
@@ -51,6 +52,7 @@ public class PushDownTopNThroughJoin implements 
RewriteRuleFactory {
                         // TODO: complex orderby
                         .when(topn ->
                                 ConnectContext.get() != null
+                                        && 
!Utils.addOverflows(topn.getLimit(), topn.getOffset())
                                         && 
ConnectContext.get().getSessionVariable().topnOptLimitThreshold
                                         >= topn.getLimit() + topn.getOffset())
                         .when(topN -> 
topN.getOrderKeys().stream().map(OrderKey::getExpr)
@@ -70,6 +72,13 @@ public class PushDownTopNThroughJoin implements 
RewriteRuleFactory {
                         .when(topN -> 
topN.getOrderKeys().stream().map(OrderKey::getExpr)
                                 .allMatch(Slot.class::isInstance))
                         .then(topN -> {
+                            // limit + offset overflowing the long range means 
no child can hold that
+                            // many rows, so pushing the TopN below the join 
cannot reduce anything;
+                            // skip the rewrite. (The direct TopN -> Join 
branch is gated the same way
+                            // via topn_opt_limit_threshold.)
+                            if (Utils.addOverflows(topN.getLimit(), 
topN.getOffset())) {
+                                return null;
+                            }
                             LogicalProject<LogicalJoin<Plan, Plan>> project = 
topN.child();
                             LogicalJoin<Plan, Plan> join = project.child();
 
@@ -183,6 +192,7 @@ public class PushDownTopNThroughJoin implements 
RewriteRuleFactory {
 
     private Plan pushTopNToChild(LogicalTopN<? extends Plan> topN, Plan child, 
List<Slot> orderbySlots,
             List<Slot> requiredOutputSlots) {
+        long childLimit = topN.getLimit() + topN.getOffset();
         // Keep only the slots that this child actually outputs. When pushing 
to
         // one side of an outer join, requiredOutputSlots may include columns 
from
         // the other side which this child cannot provide.
@@ -191,19 +201,19 @@ public class PushDownTopNThroughJoin implements 
RewriteRuleFactory {
                 .filter(childOutputSet::contains)
                 .collect(Collectors.toList());
         if (childRequired.isEmpty()) {
-            return topN.withLimitChild(topN.getLimit() + topN.getOffset(), 0, 
child);
+            return topN.withLimitChild(childLimit, 0, child);
         }
         // Ensure the child outputs all required columns. If the child already
         // outputs everything needed, no extra Project is required.
         Set<Slot> requiredSet = new HashSet<>(childRequired);
         requiredSet.addAll(orderbySlots);
         if (childOutputSet.containsAll(requiredSet)) {
-            return topN.withLimitChild(topN.getLimit() + topN.getOffset(), 0, 
child);
+            return topN.withLimitChild(childLimit, 0, child);
         }
         List<NamedExpression> childOutput = child.getOutput().stream()
                 .filter(requiredSet::contains)
                 .collect(Collectors.toList());
         Plan topNChild = PlanUtils.projectOrSelf(childOutput, child);
-        return topN.withLimitChild(topN.getLimit() + topN.getOffset(), 0, 
topNChild);
+        return topN.withLimitChild(childLimit, 0, topNChild);
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughUnion.java
index b02d7a1e45d..0416a4c5a79 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughUnion.java
@@ -28,6 +28,7 @@ import 
org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
 import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -62,6 +63,14 @@ public class PushDownTopNThroughUnion implements 
RewriteRuleFactory {
         return ImmutableList.of(
                 logicalTopN(logicalUnion().when(union -> union.getQualifier() 
== Qualifier.ALL))
                         .then(topN -> {
+                            // The child TopN must keep limit + offset rows. 
When that sum overflows the
+                            // long range (e.g. LIMIT and OFFSET both 
BIGINT_MAX) no child can hold that many
+                            // rows, so pushing down cannot reduce anything 
and would create an illegal
+                            // negative limit. Skip the rewrite; the parent 
TopN still applies limit/offset.
+                            if (Utils.addOverflows(topN.getLimit(), 
topN.getOffset())) {
+                                return null;
+                            }
+                            long childLimit = topN.getLimit() + 
topN.getOffset();
                             LogicalUnion union = topN.child();
                             List<Plan> newChildren = new ArrayList<>();
                             for (int j = 0; j < union.children().size(); j++) {
@@ -77,8 +86,7 @@ public class PushDownTopNThroughUnion implements 
RewriteRuleFactory {
                                         .map(orderKey -> 
orderKey.withExpression(
                                                 
ExpressionUtils.replace(orderKey.getExpr(), replaceMap)))
                                         
.collect(ImmutableList.toImmutableList());
-                                newChildren.add(
-                                        new LogicalTopN<>(orderKeys, 
topN.getLimit() + topN.getOffset(), 0, child));
+                                newChildren.add(new LogicalTopN<>(orderKeys, 
childLimit, 0, child));
                             }
                             if (union.children().equals(newChildren)) {
                                 return null;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughWindow.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughWindow.java
index 8dc4cd6f73a..6c5117448bf 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughWindow.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownTopNThroughWindow.java
@@ -30,6 +30,7 @@ import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.trees.plans.logical.LogicalWindow;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 
@@ -53,6 +54,11 @@ public class PushDownTopNThroughWindow implements 
RewriteRuleFactory {
                 if (!checkTopNForPartitionLimitPushDown(topn, windowExprId)) {
                     return topn;
                 }
+                // When limit + offset overflows the long range (e.g. LIMIT 
and OFFSET both
+                // BIGINT_MAX) the partition limit cannot reduce anything; 
skip the push-down.
+                if (Utils.addOverflows(topn.getLimit(), topn.getOffset())) {
+                    return topn;
+                }
                 long partitionLimit = topn.getLimit() + topn.getOffset();
                 Pair<WindowExpression, Long> windowFuncLongPair = window
                         .getPushDownWindowFuncAndLimit(null, partitionLimit);
@@ -76,6 +82,11 @@ public class PushDownTopNThroughWindow implements 
RewriteRuleFactory {
                 if (!checkTopNForPartitionLimitPushDown(topn, windowExprId)) {
                     return topn;
                 }
+                // When limit + offset overflows the long range (e.g. LIMIT 
and OFFSET both
+                // BIGINT_MAX) the partition limit cannot reduce anything; 
skip the push-down.
+                if (Utils.addOverflows(topn.getLimit(), topn.getOffset())) {
+                    return topn;
+                }
                 long partitionLimit = topn.getLimit() + topn.getOffset();
                 Pair<WindowExpression, Long> windowFuncLongPair = window
                         .getPushDownWindowFuncAndLimit(null, partitionLimit);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownVectorTopNIntoOlapScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownVectorTopNIntoOlapScan.java
index 6d449b3f51c..597f331e28d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownVectorTopNIntoOlapScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownVectorTopNIntoOlapScan.java
@@ -36,6 +36,7 @@ import 
org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalTopN;
 import org.apache.doris.nereids.util.ExpressionUtils;
+import org.apache.doris.nereids.util.Utils;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
@@ -146,6 +147,12 @@ public class PushDownVectorTopNIntoOlapScan implements 
RewriteRuleFactory {
             return null;
         }
 
+        // When limit + offset overflows the long range, the pushed scan limit 
would wrap to a
+        // negative value; skip the push-down and let the TopN above the scan 
apply limit/offset.
+        if (Utils.addOverflows(topN.getLimit(), topN.getOffset())) {
+            return null;
+        }
+
         Plan plan = scan.appendVirtualColumnsAndTopN(
                 ImmutableList.of(orderKeyAlias),
                 topN.getOrderKeys(), Optional.of(topN.getLimit() + 
topN.getOffset()),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SplitLimit.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SplitLimit.java
index 06f7760c433..0db8a0380d0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SplitLimit.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SplitLimit.java
@@ -17,10 +17,12 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.rules.Rule;
 import org.apache.doris.nereids.rules.RuleType;
 import org.apache.doris.nereids.trees.plans.LimitPhase;
 import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
+import org.apache.doris.nereids.util.Utils;
 
 /**
  * Split limit into two phase
@@ -38,8 +40,13 @@ public class SplitLimit extends OneRewriteRuleFactory {
                 .then(limit -> {
                     long l = limit.getLimit();
                     long o = limit.getOffset();
+                    if (Utils.addOverflows(l, o)) {
+                        throw new AnalysisException(
+                                "limit + offset overflows the long range in 
two-phase limit");
+                    }
                     return new LogicalLimit<>(l, o,
-                            LimitPhase.GLOBAL, new LogicalLimit<>(l + o, 0, 
LimitPhase.LOCAL, limit.child())
+                            LimitPhase.GLOBAL,
+                            new LogicalLimit<>(l + o, 0, LimitPhase.LOCAL, 
limit.child())
                     );
                 }).toRule(RuleType.SPLIT_LIMIT);
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLimit.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLimit.java
index ca418e361cc..e92d396bbf8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLimit.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLimit.java
@@ -55,9 +55,15 @@ public class LogicalLimit<CHILD_TYPE extends Plan> extends 
LogicalUnary<CHILD_TY
         this(limit, offset, phase, Optional.empty(), Optional.empty(), child);
     }
 
+    /** Build a LogicalLimit with the given limit, offset and phase. */
     public LogicalLimit(long limit, long offset, LimitPhase phase, 
Optional<GroupExpression> groupExpression,
             Optional<LogicalProperties> logicalProperties, CHILD_TYPE child) {
         super(PlanType.LOGICAL_LIMIT, groupExpression, logicalProperties, 
child);
+        // limit/offset are always non-negative ("no limit" is represented by 
Long.MAX_VALUE). A
+        // negative value here means limit + offset overflowed somewhere 
upstream and would produce an
+        // illegal plan that hangs in BE; fail fast instead.
+        Preconditions.checkArgument(limit >= 0 && offset >= 0,
+                "LogicalLimit limit and offset must be non-negative, but got 
limit=%s, offset=%s", limit, offset);
         this.limit = limit;
         this.offset = offset;
         this.phase = phase;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTopN.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTopN.java
index bc685158b57..c1d7f37060f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTopN.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalTopN.java
@@ -61,6 +61,11 @@ public class LogicalTopN<CHILD_TYPE extends Plan> extends 
LogicalUnary<CHILD_TYP
             Optional<LogicalProperties> logicalProperties, CHILD_TYPE child) {
         super(PlanType.LOGICAL_TOP_N, groupExpression, logicalProperties, 
child);
         this.orderKeys = 
ImmutableList.copyOf(Objects.requireNonNull(orderKeys, "orderKeys can not be 
null"));
+        // limit/offset are always non-negative ("no limit" is represented by 
Long.MAX_VALUE). A
+        // negative value here means limit + offset overflowed somewhere 
upstream and would produce an
+        // illegal plan that hangs in BE; fail fast instead.
+        Preconditions.checkArgument(limit >= 0 && offset >= 0,
+                "LogicalTopN limit and offset must be non-negative, but got 
limit=%s, offset=%s", limit, offset);
         this.limit = limit;
         this.offset = offset;
         this.expressions = Suppliers.memoize(() -> {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLimit.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLimit.java
index 61babb6de6b..2ced8023e91 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLimit.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLimit.java
@@ -64,6 +64,11 @@ public class PhysicalLimit<CHILD_TYPE extends Plan> extends 
PhysicalUnary<CHILD_
             LimitPhase phase, Optional<GroupExpression> groupExpression, 
LogicalProperties logicalProperties,
             CHILD_TYPE child) {
         super(PlanType.PHYSICAL_LIMIT, groupExpression, logicalProperties, 
child);
+        // limit/offset are always non-negative ("no limit" is represented by 
Long.MAX_VALUE). A
+        // negative value here means limit + offset overflowed somewhere 
upstream and would produce an
+        // illegal plan that hangs in BE; fail fast instead.
+        Preconditions.checkArgument(limit >= 0 && offset >= 0,
+                "PhysicalLimit limit and offset must be non-negative, but got 
limit=%s, offset=%s", limit, offset);
         this.limit = limit;
         this.offset = offset;
         this.phase = phase;
@@ -82,6 +87,11 @@ public class PhysicalLimit<CHILD_TYPE extends Plan> extends 
PhysicalUnary<CHILD_
             Statistics statistics, CHILD_TYPE child) {
         super(PlanType.PHYSICAL_LIMIT, groupExpression, logicalProperties, 
physicalProperties, statistics,
                 child);
+        // limit/offset are always non-negative ("no limit" is represented by 
Long.MAX_VALUE). A
+        // negative value here means limit + offset overflowed somewhere 
upstream and would produce an
+        // illegal plan that hangs in BE; fail fast instead.
+        Preconditions.checkArgument(limit >= 0 && offset >= 0,
+                "PhysicalLimit limit and offset must be non-negative, but got 
limit=%s, offset=%s", limit, offset);
         this.limit = limit;
         this.offset = offset;
         this.phase = phase;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalTopN.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalTopN.java
index 1d6ff2f28d9..5868a3a48b2 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalTopN.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalTopN.java
@@ -71,6 +71,11 @@ public class PhysicalTopN<CHILD_TYPE extends Plan> extends 
AbstractPhysicalSort<
         super(PlanType.PHYSICAL_TOP_N, orderKeys, phase, groupExpression, 
logicalProperties, physicalProperties,
                 statistics, child);
         Objects.requireNonNull(orderKeys, "orderKeys should not be null in 
PhysicalTopN.");
+        // limit/offset are always non-negative ("no limit" is represented by 
Long.MAX_VALUE). A
+        // negative value here means limit + offset overflowed somewhere 
upstream and would produce an
+        // illegal plan that hangs in BE; fail fast instead.
+        Preconditions.checkArgument(limit >= 0 && offset >= 0,
+                "PhysicalTopN limit and offset must be non-negative, but got 
limit=%s, offset=%s", limit, offset);
         this.limit = limit;
         this.offset = offset;
     }
diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
index e01ae2baf60..c268f01f73c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/Utils.java
@@ -130,6 +130,22 @@ public class Utils {
         return false;
     }
 
+    /**
+     * Whether {@code a + b} overflows the signed long range. {@code a} and 
{@code b} are expected to
+     * be non-negative (e.g. a limit and an offset).
+     *
+     * <p>When combining a {@code limit} and an {@code offset} into the number 
of rows a child must
+     * keep (e.g. pushing a TopN/Limit down, or building a two-phase sort), 
the raw {@code limit +
+     * offset} can exceed the long range when both are close to {@code 
BIGINT_MAX}; it then wraps to a
+     * negative number, which is an illegal limit and produces a broken plan. 
Overflow here means the
+     * child would need more rows than any relation can hold (no relation 
exceeds {@code
+     * Long.MAX_VALUE} rows), so the optimization cannot reduce anything: 
callers should skip the
+     * rewrite and fall back to the correct unoptimized execution.
+     */
+    public static boolean addOverflows(long a, long b) {
+        return a > Long.MAX_VALUE - b;
+    }
+
     /**
      * Wrapper to a function without return value.
      */
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/util/UtilsTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/util/UtilsTest.java
index 0c7d903311f..f1fcccab3e7 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/util/UtilsTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/util/UtilsTest.java
@@ -32,4 +32,21 @@ public class UtilsTest {
         String en = "database123";
         Assertions.assertFalse(Utils.containChinese(en));
     }
+
+    @Test
+    public void testAddOverflows() {
+        // sums that fit in the long range do not overflow
+        Assertions.assertFalse(Utils.addOverflows(0, 0));
+        Assertions.assertFalse(Utils.addOverflows(1, 2));
+        Assertions.assertFalse(Utils.addOverflows(Long.MAX_VALUE, 0));
+        Assertions.assertFalse(Utils.addOverflows(0, Long.MAX_VALUE));
+        Assertions.assertFalse(Utils.addOverflows(Long.MAX_VALUE - 1, 1));
+
+        // limit + offset that exceeds the long range overflows. This is the 
case (e.g. LIMIT and
+        // OFFSET both BIGINT_MAX) that previously produced a negative, 
illegal child limit when
+        // pushing TopN/Limit down; callers now skip the rewrite instead.
+        Assertions.assertTrue(Utils.addOverflows(Long.MAX_VALUE, 1));
+        Assertions.assertTrue(Utils.addOverflows(Long.MAX_VALUE, 
Long.MAX_VALUE));
+        Assertions.assertTrue(Utils.addOverflows(Long.MAX_VALUE - 1, 2));
+    }
 }
diff --git 
a/regression-test/suites/nereids_rules_p0/push_down_top_n/push_down_top_n_through_union.groovy
 
b/regression-test/suites/nereids_rules_p0/push_down_top_n/push_down_top_n_through_union.groovy
index 28ef3d1b61a..cb2c819cd1f 100644
--- 
a/regression-test/suites/nereids_rules_p0/push_down_top_n/push_down_top_n_through_union.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/push_down_top_n/push_down_top_n_through_union.groovy
@@ -312,4 +312,31 @@ suite("push_down_top_n_through_union") {
     ORDER BY logTimestamp desc
     LIMIT 8;
     """
+
+    // When limit + offset overflows the long range (e.g. LIMIT and OFFSET 
both at BIGINT_MAX),
+    // the planner reports an error instead of silently producing a broken 
plan. Both the TopN path
+    // (ORDER BY + LIMIT/OFFSET) and the Limit path (LIMIT/OFFSET without 
ORDER BY) are covered.
+    test {
+        sql """
+            select count(*) as c from (
+                select id from (
+                    select 1 as id union all select 2 as id union all select 3 
as id
+                ) t
+                order by id limit 9223372036854775807 offset 
9223372036854775807
+            ) s;
+        """
+        exception "limit + offset overflows"
+    }
+
+    test {
+        sql """
+            select count(*) as c from (
+                select id from (
+                    select 1 as id union all select 2 as id union all select 3 
as id
+                ) t
+                limit 9223372036854775807 offset 9223372036854775807
+            ) s;
+        """
+        exception "limit + offset overflows"
+    }
 }


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

Reply via email to