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

tkalkirill pushed a commit to branch ignite-28896
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/ignite-28896 by this push:
     new ac25617f0a8 IGNITE-28896 Wip
ac25617f0a8 is described below

commit ac25617f0a817cc8bbad20c70b8423e06598ac77
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Wed Jul 15 15:35:44 2026 +0300

    IGNITE-28896 Wip
---
 .../enumerable/FetchOffsetRoundingPolicy.java      | 36 +++++++++
 .../query/calcite/exec/LogicalRelImplementor.java  | 48 ++++++++++--
 .../query/calcite/exec/rel/LimitNode.java          | 90 +++++++++++++---------
 .../query/calcite/exec/rel/SortNode.java           | 44 +++++------
 .../query/calcite/prepare/IgniteSqlValidator.java  | 33 +++++---
 .../query/calcite/util/IgniteResource.java         |  5 +-
 .../query/calcite/exec/rel/LimitExecutionTest.java | 61 ++++++++++++++-
 .../integration/LimitOffsetIntegrationTest.java    | 81 ++++++++++++++++---
 .../integration/QueryMetadataIntegrationTest.java  | 22 ++++++
 9 files changed, 332 insertions(+), 88 deletions(-)

diff --git 
a/modules/calcite/src/main/java/org/apache/calcite/adapter/enumerable/FetchOffsetRoundingPolicy.java
 
b/modules/calcite/src/main/java/org/apache/calcite/adapter/enumerable/FetchOffsetRoundingPolicy.java
new file mode 100644
index 00000000000..d1930d137bd
--- /dev/null
+++ 
b/modules/calcite/src/main/java/org/apache/calcite/adapter/enumerable/FetchOffsetRoundingPolicy.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.calcite.adapter.enumerable;
+
+import java.math.BigDecimal;
+
+/** Defines how enumerable FETCH and OFFSET values are rounded. */
+// TODO https://issues.apache.org/jira/browse/CALCITE-7624
+//  Check and remove after update to Calcite 1.43.
+public interface FetchOffsetRoundingPolicy {
+    /** Default policy that preserves the value produced by validation or 
parameter binding. */
+    FetchOffsetRoundingPolicy NONE = value -> value;
+
+    /**
+     * Rounds a FETCH or OFFSET value.
+     *
+     * @param value Value to round.
+     * @return Rounded value.
+     */
+    BigDecimal round(BigDecimal value);
+}
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index 1e93eecaf7c..0ec1781f6f5 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.query.calcite.exec;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -28,6 +30,7 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import com.google.common.collect.ImmutableList;
+import org.apache.calcite.adapter.enumerable.FetchOffsetRoundingPolicy;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.RelNode;
@@ -126,6 +129,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribut
 import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils;
 import 
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.processors.query.calcite.util.IgniteMath;
 import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.Nullable;
@@ -144,6 +148,9 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
     /** */
     private final ExecutionContext<Row> ctx;
 
+    /** FETCH/OFFSET rounding policy used by the Calcite engine. */
+    private final FetchOffsetRoundingPolicy fetchOffsetRoundingPolicy;
+
     /** */
     private final AffinityService affSrvc;
 
@@ -176,6 +183,7 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
         this.ctx = ctx;
 
         expressionFactory = ctx.expressionFactory();
+        fetchOffsetRoundingPolicy = getFetchOffsetRoundingPolicy(ctx);
     }
 
     /** {@inheritDoc} */
@@ -568,7 +576,7 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
                 rowType,
                 idxBndRel.first() ? cmp : cmp.reversed(),
                 null,
-                () -> 1
+                () -> BigDecimal.ONE
             );
 
             sortNode.register(scanNode);
@@ -630,8 +638,8 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
 
     /** {@inheritDoc} */
     @Override public Node<Row> visit(IgniteLimit rel) {
-        Supplier<Integer> offset = (rel.offset() == null) ? null : 
expressionFactory.execute(rel.offset());
-        Supplier<Integer> fetch = (rel.fetch() == null) ? null : 
expressionFactory.execute(rel.fetch());
+        Supplier<BigDecimal> offset = fetchOffsetSupplier(rel.offset(), 
"OFFSET");
+        Supplier<BigDecimal> fetch = fetchOffsetSupplier(rel.fetch(), "FETCH");
 
         LimitNode<Row> node = new LimitNode<>(ctx, rel.getRowType(), offset, 
fetch);
 
@@ -646,8 +654,8 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
     @Override public Node<Row> visit(IgniteSort rel) {
         RelCollation collation = rel.getCollation();
 
-        Supplier<Integer> offset = (rel.offset == null) ? null : 
expressionFactory.execute(rel.offset);
-        Supplier<Integer> fetch = (rel.fetch == null) ? null : 
expressionFactory.execute(rel.fetch);
+        Supplier<BigDecimal> offset = fetchOffsetSupplier(rel.offset, 
"OFFSET");
+        Supplier<BigDecimal> fetch = fetchOffsetSupplier(rel.fetch, "FETCH");
 
         SortNode<Row> node = new SortNode<>(ctx, rel.getRowType(), 
expressionFactory.comparator(collation), offset,
             fetch);
@@ -1050,4 +1058,34 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
             otherColMapping
         );
     }
+
+    /** */
+    private static FetchOffsetRoundingPolicy 
getFetchOffsetRoundingPolicy(ExecutionContext<?> ctx) {
+        FetchOffsetRoundingPolicy roundingPlc = 
ctx.unwrap(FetchOffsetRoundingPolicy.class);
+
+        return roundingPlc == null ? FetchOffsetRoundingPolicy.NONE : 
roundingPlc;
+    }
+
+    /** Converts a FETCH/OFFSET expression to the row count expected by Ignite 
execution nodes. */
+    private @Nullable Supplier<BigDecimal> fetchOffsetSupplier(@Nullable 
RexNode node, String kind) {
+        if (node == null)
+            return null;
+
+        Supplier<Number> val = expressionFactory.execute(node);
+
+        return () -> fetchOffsetValue(val.get(), kind);
+    }
+
+    /** Converts a FETCH/OFFSET runtime value to a row count. */
+    private BigDecimal fetchOffsetValue(Object val, String kind) {
+        if (!(val instanceof Number))
+            throw new IllegalArgumentException(kind + " must be a number");
+
+        BigDecimal decimal = IgniteMath.convertToBigDecimal((Number)val);
+
+        if (decimal.signum() < 0)
+            throw new IllegalArgumentException(kind + " must not be negative");
+
+        return fetchOffsetRoundingPolicy.round(decimal).setScale(0, 
RoundingMode.CEILING);
+    }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitNode.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitNode.java
index 9fa9d14090c..74fdac07e93 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitNode.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitNode.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
+import java.math.BigDecimal;
 import java.util.function.Supplier;
 import org.apache.calcite.rel.type.RelDataType;
 import 
org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext;
@@ -26,19 +27,16 @@ import org.jetbrains.annotations.Nullable;
 /** Offset, fetch|limit support node. */
 public class LimitNode<Row> extends AbstractNode<Row> implements 
SingleNode<Row>, Downstream<Row> {
     /** Offset if its present, otherwise 0. */
-    private final int offset;
+    private final BigDecimal offset;
 
-    /** Fetch if its present, otherwise 0. */
-    private final int fetch;
+    /** Fetch if its present, otherwise all rows. */
+    private final @Nullable BigDecimal fetch;
 
     /** Already processed (pushed to upstream) rows count. */
-    private int rowsProcessed;
+    private BigDecimal rowsProcessed = BigDecimal.ZERO;
 
-    /** Fetch can be unset, in this case we need all rows. */
-    private @Nullable Supplier<Integer> fetchNode;
-
-    /** Waiting results counter. */
-    private int waiting;
+    /** Number of upstream rows that remain to be processed for the current 
downstream request. */
+    private BigDecimal waiting = BigDecimal.ZERO;
 
     /**
      * Constructor.
@@ -49,20 +47,19 @@ public class LimitNode<Row> extends AbstractNode<Row> 
implements SingleNode<Row>
     public LimitNode(
         ExecutionContext<Row> ctx,
         RelDataType rowType,
-        Supplier<Integer> offsetNode,
-        Supplier<Integer> fetchNode
+        @Nullable Supplier<BigDecimal> offsetNode,
+        @Nullable Supplier<BigDecimal> fetchNode
     ) {
         super(ctx, rowType);
 
-        offset = offsetNode == null ? 0 : offsetNode.get();
-        fetch = fetchNode == null ? 0 : fetchNode.get();
-        this.fetchNode = fetchNode;
+        offset = offsetNode == null ? BigDecimal.ZERO : offsetNode.get();
+        fetch = fetchNode == null ? null : fetchNode.get();
     }
 
     /** {@inheritDoc} */
     @Override public void request(int rowsCnt) throws Exception {
         assert !F.isEmpty(sources()) && sources().size() == 1;
-        assert rowsCnt > 0;
+        assert rowsCnt > 0 : rowsCnt;
 
         if (fetchNone()) {
             end();
@@ -70,54 +67,56 @@ public class LimitNode<Row> extends AbstractNode<Row> 
implements SingleNode<Row>
             return;
         }
 
-        if (offset > 0 && rowsProcessed == 0)
-            rowsCnt = offset + rowsCnt;
-
-        waiting = rowsCnt;
+        waiting = BigDecimal.valueOf(rowsCnt);
 
-        if (fetch > 0)
-            rowsCnt = Math.min(rowsCnt, (fetch + offset) - rowsProcessed);
+        if (rowsProcessed.compareTo(offset) < 0)
+            waiting = waiting.add(offset.subtract(rowsProcessed));
 
-        checkState();
-
-        source().request(rowsCnt);
+        requestNextBatch();
     }
 
     /** {@inheritDoc} */
     @Override public void push(Row row) throws Exception {
-        if (waiting == -1)
+        if (waiting.signum() < 0)
             return;
 
-        ++rowsProcessed;
+        rowsProcessed = rowsProcessed.add(BigDecimal.ONE);
 
-        --waiting;
+        waiting = waiting.subtract(BigDecimal.ONE);
 
         checkState();
 
-        if (rowsProcessed > offset) {
-            if (fetchNode == null || (fetchNode != null && rowsProcessed <= 
fetch + offset))
-                downstream().push(row);
+        boolean endAfterRow = fetchNone() && waiting.signum() > 0;
+        boolean reqNextAfterRow = !endAfterRow && waiting.signum() > 0
+            && 
rowsProcessed.remainder(BigDecimal.valueOf(IN_BUFFER_SIZE)).signum() == 0;
+
+        if (rowsProcessed.compareTo(offset) > 0
+            && (fetch == null || rowsProcessed.compareTo(offset.add(fetch)) <= 
0)) {
+            downstream().push(row);
         }
 
-        if (fetch > 0 && rowsProcessed == fetch + offset && waiting > 0)
+        if (endAfterRow)
             end();
+        else if (reqNextAfterRow)
+            requestNextBatch();
     }
 
     /** {@inheritDoc} */
     @Override public void end() throws Exception {
-        if (waiting == -1)
+        if (waiting.signum() < 0)
             return;
 
         assert downstream() != null;
 
-        waiting = -1;
+        waiting = BigDecimal.ONE.negate();
 
         downstream().end();
     }
 
     /** {@inheritDoc} */
     @Override protected void rewindInternal() {
-        rowsProcessed = 0;
+        rowsProcessed = BigDecimal.ZERO;
+        waiting = BigDecimal.ZERO;
     }
 
     /** {@inheritDoc} */
@@ -130,6 +129,27 @@ public class LimitNode<Row> extends AbstractNode<Row> 
implements SingleNode<Row>
 
     /** {@code True} if requested 0 results, or all already processed. */
     private boolean fetchNone() {
-        return (fetchNode != null && fetch == 0) || (fetch > 0 && 
rowsProcessed == fetch + offset);
+        return fetch != null && (fetch.signum() == 0 || 
rowsProcessed.compareTo(offset.add(fetch)) >= 0);
+    }
+
+    /** Requests the next upstream batch, taking large decimal offsets into 
account. */
+    private void requestNextBatch() throws Exception {
+        BigDecimal bufSize = BigDecimal.valueOf(IN_BUFFER_SIZE);
+        BigDecimal rowsAvailable = waiting;
+
+        if (fetch != null)
+            rowsAvailable = 
rowsAvailable.min(offset.add(fetch).subtract(rowsProcessed));
+
+        BigDecimal rowsToReq = 
bufSize.subtract(rowsProcessed.remainder(bufSize)).min(rowsAvailable);
+
+        if (rowsToReq.signum() == 0) {
+            end();
+
+            return;
+        }
+
+        checkState();
+
+        source().request(rowsToReq.intValueExact());
     }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortNode.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortNode.java
index 09376b210f6..d68bee3cc29 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortNode.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortNode.java
@@ -16,9 +16,9 @@
  */
 package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.List;
 import java.util.PriorityQueue;
 import java.util.function.Supplier;
 import org.apache.calcite.rel.type.RelDataType;
@@ -43,11 +43,8 @@ public class SortNode<Row> extends MemoryTrackingNode<Row> 
implements SingleNode
     /** Rows buffer. */
     private final PriorityQueue<Row> rows;
 
-    /** SQL select limit. Negative if disabled. */
-    private final int limit;
-
     /** Reverse-ordered rows in case of limited sort. */
-    private List<Row> reversed;
+    private ArrayList<Row> reversed;
 
     /**
      * @param ctx Execution context.
@@ -58,21 +55,25 @@ public class SortNode<Row> extends MemoryTrackingNode<Row> 
implements SingleNode
     public SortNode(
         ExecutionContext<Row> ctx, RelDataType rowType,
         Comparator<Row> comp,
-        @Nullable Supplier<Integer> offset,
-        @Nullable Supplier<Integer> fetch
+        @Nullable Supplier<BigDecimal> offset,
+        @Nullable Supplier<BigDecimal> fetch
     ) {
         super(ctx, rowType);
 
-        assert fetch == null || fetch.get() >= 0;
-        assert offset == null || offset.get() >= 0;
+        BigDecimal offsetVal = offset == null ? BigDecimal.ZERO : offset.get();
+        BigDecimal fetchVal = fetch == null ? null : fetch.get();
 
-        limit = fetch == null ? -1 : fetch.get() + (offset == null ? 0 : 
offset.get());
+        BigDecimal rowsToKeep = fetchVal == null ? null : 
fetchVal.add(offsetVal);
 
-        if (limit < 0)
+        if (rowsToKeep == null || rowsToKeep.signum() == 0
+            || rowsToKeep.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 
0) {
             rows = new PriorityQueue<>(comp);
+        }
         else {
-            rows = new GridBoundedPriorityQueue<>(limit, comp == null ? 
(Comparator<Row>)Comparator.reverseOrder()
-                : comp.reversed());
+            rows = new GridBoundedPriorityQueue<>(rowsToKeep.intValueExact(),
+                comp == null ? (Comparator<Row>)Comparator.reverseOrder() : 
comp.reversed());
+
+            reversed = new ArrayList<>();
         }
     }
 
@@ -106,8 +107,8 @@ public class SortNode<Row> extends MemoryTrackingNode<Row> 
implements SingleNode
     /** {@inheritDoc} */
     @Override public void request(int rowsCnt) throws Exception {
         assert !F.isEmpty(sources()) && sources().size() == 1;
-        assert rowsCnt > 0 && requested == 0;
-        assert waiting <= 0;
+        assert rowsCnt > 0 && requested == 0 : "rowsCnt=" + rowsCnt + ", 
requested=" + requested;
+        assert waiting <= 0 : waiting;
 
         checkState();
 
@@ -122,8 +123,8 @@ public class SortNode<Row> extends MemoryTrackingNode<Row> 
implements SingleNode
     /** {@inheritDoc} */
     @Override public void push(Row row) throws Exception {
         assert downstream() != null;
-        assert waiting > 0;
-        assert reversed == null || reversed.isEmpty();
+        assert waiting > 0 : waiting;
+        assert reversed == null || reversed.isEmpty() : reversed.size();
 
         checkState();
 
@@ -146,7 +147,7 @@ public class SortNode<Row> extends MemoryTrackingNode<Row> 
implements SingleNode
     /** {@inheritDoc} */
     @Override public void end() throws Exception {
         assert downstream() != null;
-        assert waiting > 0;
+        assert waiting > 0 : waiting;
 
         checkState();
 
@@ -160,16 +161,15 @@ public class SortNode<Row> extends 
MemoryTrackingNode<Row> implements SingleNode
         if (isClosed())
             return;
 
-        assert waiting == -1;
+        assert waiting == -1 : waiting;
 
         int processed = 0;
 
         inLoop = true;
         try {
             // Prepare final order (reversed).
-            if (limit > 0 && !rows.isEmpty()) {
-                if (reversed == null)
-                    reversed = new ArrayList<>(rows.size());
+            if (reversed != null && !rows.isEmpty()) {
+                reversed.ensureCapacity(rows.size());
 
                 while (!rows.isEmpty()) {
                     reversed.add(rows.poll());
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
index 1c7f00da265..d09946450cd 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java
@@ -74,6 +74,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
 import 
org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlDecimalLiteral;
 import 
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.type.OtherType;
+import org.apache.ignite.internal.processors.query.calcite.util.IgniteMath;
 import org.apache.ignite.internal.processors.query.calcite.util.IgniteResource;
 import org.apache.ignite.internal.util.typedef.F;
 import org.immutables.value.Value;
@@ -84,9 +85,6 @@ import static org.apache.calcite.util.Static.RESOURCE;
 /** Validator. */
 @Value.Enclosing
 public class IgniteSqlValidator extends SqlValidatorImpl {
-    /** Decimal of Integer.MAX_VALUE for fetch/offset bounding. */
-    private static final BigDecimal DEC_INT_MAX = 
BigDecimal.valueOf(Integer.MAX_VALUE);
-
     /** **/
     private static final int MAX_LENGTH_OF_ALIASES = 256;
 
@@ -241,10 +239,21 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
 
     /** {@inheritDoc} */
     @Override protected void validateSelect(SqlSelect select, RelDataType 
targetRowType) {
-        checkIntegerLimit(select.getFetch(), "fetch / limit");
-        checkIntegerLimit(select.getOffset(), "offset");
+        checkLimit(select.getFetch(), "fetch / limit");
+        checkLimit(select.getOffset(), "offset");
 
         super.validateSelect(select, targetRowType);
+
+        setFetchOffsetType(select.getFetch());
+        setFetchOffsetType(select.getOffset());
+    }
+
+    /** */
+    // TODO https://issues.apache.org/jira/browse/CALCITE-7624
+    //  Check SqlValidatorImpl#handleOffsetFetch and remove after update to 
Calcite 1.43.
+    private void setFetchOffsetType(@Nullable SqlNode node) {
+        if (node instanceof SqlDynamicParam)
+            setValidatedNodeType(node, 
typeFactory.createSqlType(SqlTypeName.DECIMAL));
     }
 
     /** {@inheritDoc} */
@@ -265,12 +274,12 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
      * @param n Node to check limit.
      * @param nodeName Node name.
      */
-    private void checkIntegerLimit(SqlNode n, String nodeName) {
+    private void checkLimit(SqlNode n, String nodeName) {
         if (n instanceof SqlLiteral) {
             BigDecimal offFetchLimit = ((SqlLiteral)n).bigDecimalValue();
 
-            if (offFetchLimit.compareTo(DEC_INT_MAX) > 0 || 
offFetchLimit.compareTo(BigDecimal.ZERO) < 0)
-                throw newValidationError(n, 
IgniteResource.INSTANCE.correctIntegerLimit(nodeName));
+            if (offFetchLimit.compareTo(BigDecimal.ZERO) < 0)
+                throw newValidationError(n, 
IgniteResource.INSTANCE.illegalLimit(nodeName));
         }
         else if (n instanceof SqlDynamicParam) {
             // will fail in params check.
@@ -281,9 +290,11 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
 
             if (idx < parameters.length) {
                 Object param = parameters[idx];
-                if (parameters[idx] instanceof Integer) {
-                    if ((Integer)param < 0)
-                        throw newValidationError(n, 
IgniteResource.INSTANCE.correctIntegerLimit(nodeName));
+                if (param instanceof Number) {
+                    BigDecimal val = 
IgniteMath.convertToBigDecimal((Number)param);
+
+                    if (val.compareTo(BigDecimal.ZERO) < 0)
+                        throw newValidationError(n, 
IgniteResource.INSTANCE.illegalLimit(nodeName));
                 }
             }
         }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteResource.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteResource.java
index df74d56a916..7e0df675a0a 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteResource.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteResource.java
@@ -40,9 +40,8 @@ public interface IgniteResource {
     Resources.ExInst<SqlValidatorException> 
unsupportedAggregationFunction(String a0);
 
     /** */
-    @Resources.BaseMessage("Illegal value of {0}. The value must be positive 
and less than Integer.MAX_VALUE " +
-        "(" + Integer.MAX_VALUE + ")." )
-    Resources.ExInst<SqlValidatorException> correctIntegerLimit(String a0);
+    @Resources.BaseMessage("Illegal value of {0}. The value must be positive")
+    Resources.ExInst<SqlValidatorException> illegalLimit(String a0);
 
     /** */
     @Resources.BaseMessage("Option ''{0}'' has already been defined")
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitExecutionTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitExecutionTest.java
index 6dd3c7b36cf..874cf687008 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitExecutionTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitExecutionTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
+import java.math.BigDecimal;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
@@ -64,6 +65,59 @@ public class LimitExecutionTest extends 
AbstractExecutionTest {
         checkLimitSort(2000, 3000);
     }
 
+    /** Tests sort limit values greater than the integer range. */
+    @Test
+    public void testBigDecimalSortLimit() {
+        ExecutionContext<Object[]> ctx = executionContext(F.first(nodes()), 
UUID.randomUUID(), 0);
+        IgniteTypeFactory tf = ctx.getTypeFactory();
+        RelDataType rowType = TypeUtils.createRowType(tf, int.class);
+
+        RootNode<Object[]> rootNode = new RootNode<>(ctx, rowType);
+        SortNode<Object[]> sortNode = new SortNode<>(ctx, rowType, 
F::compareArrays, null,
+            () -> BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE));
+
+        List<Object[]> data = IntStream.range(0, 10).boxed().map(i -> new 
Object[] {i}).collect(Collectors.toList());
+
+        Collections.reverse(data);
+
+        rootNode.register(sortNode);
+        sortNode.register(new ScanNode<>(ctx, rowType, data));
+
+        for (int i = 0; i < data.size(); i++) {
+            assertTrue(rootNode.hasNext());
+            assertEquals(i, rootNode.next()[0]);
+        }
+
+        assertFalse(rootNode.hasNext());
+    }
+
+    /** Tests a reentrant downstream request at an input buffer boundary. */
+    @Test
+    public void testReentrantRequestToLimit() {
+        ExecutionContext<Object[]> ctx = executionContext(F.first(nodes()), 
UUID.randomUUID(), 0);
+        IgniteTypeFactory tf = ctx.getTypeFactory();
+        RelDataType rowType = TypeUtils.createRowType(tf, int.class);
+
+        RootNode<Object[]> rootNode = new RootNode<>(ctx, rowType);
+        SortNode<Object[]> sortNode = new SortNode<>(ctx, rowType, 
F::compareArrays);
+        LimitNode<Object[]> limitNode = new LimitNode<>(ctx, rowType, null,
+            () -> BigDecimal.valueOf(IN_BUFFER_SIZE * 2L));
+        List<Object[]> data = IntStream.range(0, IN_BUFFER_SIZE * 2).boxed()
+            .map(i -> new Object[] {i})
+            .collect(Collectors.toList());
+
+        rootNode.register(sortNode);
+        sortNode.register(limitNode);
+        limitNode.register(new ScanNode<>(ctx, rowType, data));
+
+        for (int i = 0; i < data.size(); i++) {
+            assertTrue(rootNode.hasNext());
+            assertEquals(i, rootNode.next()[0]);
+        }
+
+        assertFalse(rootNode.hasNext());
+    }
+
     /**
      * @param offset Rows offset.
      * @param fetch Fetch rows count (zero means unlimited).
@@ -78,8 +132,8 @@ public class LimitExecutionTest extends 
AbstractExecutionTest {
 
         RootNode<Object[]> rootNode = new RootNode<>(ctx, rowType);
 
-        SortNode<Object[]> sortNode = new SortNode<>(ctx, rowType, 
F::compareArrays, () -> offset,
-            fetch == 0 ? null : () -> fetch);
+        SortNode<Object[]> sortNode = new SortNode<>(ctx, rowType, 
F::compareArrays,
+            () -> BigDecimal.valueOf(offset), fetch == 0 ? null : () -> 
BigDecimal.valueOf(fetch));
 
         List<Object[]> data = IntStream.range(0, SourceNode.IN_BUFFER_SIZE + 
fetch + offset).boxed()
             .map(i -> new Object[] {i}).collect(Collectors.toList());
@@ -109,7 +163,8 @@ public class LimitExecutionTest extends 
AbstractExecutionTest {
         RelDataType rowType = TypeUtils.createRowType(tf, int.class);
 
         RootNode<Object[]> rootNode = new RootNode<>(ctx, rowType);
-        LimitNode<Object[]> limitNode = new LimitNode<>(ctx, rowType, () -> 
offset, fetch == 0 ? null : () -> fetch);
+        LimitNode<Object[]> limitNode = new LimitNode<>(ctx, rowType, () -> 
BigDecimal.valueOf(offset),
+            fetch == 0 ? null : () -> BigDecimal.valueOf(fetch));
         SourceNode srcNode = new SourceNode(ctx, rowType);
 
         rootNode.register(limitNode);
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/LimitOffsetIntegrationTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/LimitOffsetIntegrationTest.java
index 428d4d273d9..af841b9c698 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/LimitOffsetIntegrationTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/LimitOffsetIntegrationTest.java
@@ -18,9 +18,14 @@
 package org.apache.ignite.internal.processors.query.calcite.integration;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.Arrays;
 import java.util.List;
+import org.apache.calcite.adapter.enumerable.FetchOffsetRoundingPolicy;
+import org.apache.calcite.plan.Contexts;
 import org.apache.calcite.sql.validate.SqlValidatorException;
+import org.apache.calcite.tools.FrameworkConfig;
+import org.apache.calcite.tools.Frameworks;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cache.QueryEntity;
@@ -33,6 +38,7 @@ import org.apache.ignite.internal.util.typedef.internal.U;
 import org.junit.Test;
 
 import static java.util.Collections.singletonList;
+import static 
org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor.FRAMEWORK_CONFIG;
 
 /**
  * Limit / offset tests.
@@ -54,7 +60,8 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
 
     /** {@inheritDoc} */
     @Override protected void afterTest() throws Exception {
-        // Override method to keep caches after tests.
+        // Keep caches between tests, but do not leak an active transaction 
into the next test.
+        clearTransaction();
     }
 
     /** {@inheritDoc} */
@@ -94,6 +101,8 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
     /** */
     @Test
     public void testNestedLimitOffsetWithUnion() {
+        cacheRepl.clear();
+
         sql("INSERT into TEST_REPL VALUES (1, 'a'), (2, 'b'), (3, 'c'), (4, 
'd')");
 
         assertQuery("(SELECT id FROM TEST_REPL WHERE id = 2) UNION ALL " +
@@ -101,20 +110,71 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
         ).returns(2).returns(4).check();
     }
 
-    /** Tests correctness of fetch / offset params. */
+    /** */
     @Test
-    public void testInvalidLimitOffset() {
+    public void testFractionalLimitOffset() {
+        cacheRepl.clear();
+
+        sql("INSERT into TEST_REPL VALUES (1, 'a'), (2, 'b'), (3, 'c'), (4, 
'd')");
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 1.2")
+            .returns(1)
+            .returns(2)
+            .check();
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 1.1 ROWS 
FETCH FIRST 1.1 ROWS ONLY")
+            .returns(3)
+            .returns(4)
+            .check();
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET ? ROWS FETCH 
FIRST ? ROWS ONLY")
+            .withParams(new BigDecimal("1.1"), new BigDecimal("1.2"))
+            .returns(3)
+            .returns(4)
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testFetchOffsetRoundingPolicy() {
+        FetchOffsetRoundingPolicy floorPlc = value -> value.setScale(0, 
RoundingMode.FLOOR);
+        FrameworkConfig floorPlcCfg = 
Frameworks.newConfigBuilder(FRAMEWORK_CONFIG)
+            .context(Contexts.of(floorPlc))
+            .build();
+
+        if (sqlTxMode != SqlTransactionMode.NONE)
+            startTransaction(client);
+
+        assertQuery("SELECT * FROM (VALUES (1), (2), (3), (4)) LIMIT 1.1")
+            .withFrameworkConfig(floorPlcCfg)
+            .returns(1)
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testBigDecimalLimitOffset() {
         String bigInt = BigDecimal.valueOf(10000000000L).toString();
 
-        assertThrows("SELECT * FROM TEST_REPL OFFSET " + bigInt + " ROWS",
-            SqlValidatorException.class, "Illegal value of offset");
+        // QueryChecker expects an active transaction for transactional test 
modes.
+        sql("SELECT 1");
+
+        assertQuery("SELECT * FROM (VALUES (1)) OFFSET " + bigInt + " ROWS")
+            .resultSize(0)
+            .check();
 
-        assertThrows("SELECT * FROM TEST_REPL FETCH FIRST " + bigInt + " ROWS 
ONLY",
-            SqlValidatorException.class, "Illegal value of fetch / limit");
+        assertQuery("SELECT * FROM (VALUES (1)) FETCH FIRST " + bigInt + " 
ROWS ONLY")
+            .returns(1)
+            .check();
 
-        assertThrows("SELECT * FROM TEST_REPL LIMIT " + bigInt,
-            SqlValidatorException.class, "Illegal value of fetch / limit");
+        assertQuery("SELECT * FROM (VALUES (1)) LIMIT " + bigInt)
+            .returns(1)
+            .check();
+    }
 
+    /** Tests correctness of fetch / offset params. */
+    @Test
+    public void testInvalidLimitOffset() {
         assertThrows("SELECT * FROM TEST_REPL OFFSET -1 ROWS FETCH FIRST -1 
ROWS ONLY",
             IgniteSQLException.class, null);
 
@@ -133,6 +193,9 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
 
         assertThrows("SELECT * FROM TEST_REPL FETCH FIRST ? ROWS ONLY",
             SqlValidatorException.class, "Illegal value of fetch / limit", -1);
+
+        assertThrows("SELECT * FROM TEST_REPL OFFSET ? ROWS",
+            SqlValidatorException.class, "Illegal value of offset", new 
BigDecimal("-1.5"));
     }
 
     /**
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
index 36ee4dc647a..e0c28fde7d5 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
@@ -177,6 +177,28 @@ public class QueryMetadataIntegrationTest extends 
AbstractBasicIntegrationTest {
             ).check();
     }
 
+    /** */
+    @Test
+    public void testFetchOffsetParameters() throws Exception {
+        executeSql("CREATE TABLE tbl (id BIGINT, PRIMARY KEY(id))");
+
+        checker("SELECT * FROM tbl OFFSET ? ROWS FETCH FIRST ? ROWS ONLY")
+            .addMeta(
+                builder -> builder.add("PUBLIC", "TBL", Long.class, "ID", 19, 
0, true),
+                builder -> builder
+                    .add(null, null, BigDecimal.class, "?0", 32767, 0, false)
+                    .add(null, null, BigDecimal.class, "?1", 32767, 0, false)
+            )
+            .check();
+
+        checker("SELECT * FROM tbl LIMIT ?")
+            .addMeta(
+                builder -> builder.add("PUBLIC", "TBL", Long.class, "ID", 19, 
0, true),
+                builder -> builder.add(null, null, BigDecimal.class, "?0", 
32767, 0, false)
+            )
+            .check();
+    }
+
     /** */
     private MetadataChecker checker(String sql) {
         return new MetadataChecker(queryEngine(client), sql);


Reply via email to