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

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


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

commit bbbb52ca7dfae0b92c5bc567addaa6fe4307445f
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Tue Aug 11 16:39:11 2026 +0300

    IGNITE-28739 Wip
---
 modules/calcite/pom.xml                            | 11 +++--
 .../src/main/codegen/includes/parserImpls.ftl      |  2 +-
 .../codegen/patches/select-fetch-expression.patch  | 26 ----------
 .../calcite/prepare/IgniteRelFieldTrimmer.java     |  6 ++-
 .../query/calcite/prepare/IgniteSqlValidator.java  | 26 ++++++++--
 .../query/calcite/rule/SortConverterRule.java      | 56 +++++++++++++++++++++-
 .../integration/LimitOffsetIntegrationTest.java    | 30 ++++++++++++
 .../calcite/planner/LimitOffsetPlannerTest.java    | 50 +++++++++++++++++++
 8 files changed, 167 insertions(+), 40 deletions(-)

diff --git a/modules/calcite/pom.xml b/modules/calcite/pom.xml
index 7bfedc5caa8..53ea643b2ef 100644
--- a/modules/calcite/pom.xml
+++ b/modules/calcite/pom.xml
@@ -352,11 +352,12 @@
                                     
data="tdd(${project.build.directory}/codegen/config.fmpp), default: 
tdd(${project.build.directory}/codegen/default_config.fmpp)"
                                 />
                                 <!-- TODO: 
https://issues.apache.org/jira/browse/CALCITE-7592
-                                     Remove this workaround and 
select-fetch-expression.patch after upgrading to Calcite 1.43. -->
-                                <patch
-                                    
dir="${project.build.directory}/generated-sources/fmpp"
-                                    
patchfile="${project.build.directory}/codegen/patches/select-fetch-expression.patch"
-                                    strip="1"
+                                     Remove this workaround after upgrading to 
Calcite 1.43. -->
+                                <replace
+                                    
file="${project.build.directory}/generated-sources/fmpp/javacc/Parser.jj"
+                                    token="&lt;FETCH&gt; ( &lt;FIRST&gt; | 
&lt;NEXT&gt; ) offsetFetch[1] = UnsignedNumericLiteralOrParam()"
+                                    value="&lt;FETCH&gt; ( &lt;FIRST&gt; | 
&lt;NEXT&gt; ) offsetFetch[1] = FetchCount()"
+                                    failOnNoReplacements="true"
                                 />
                             </target>
                         </configuration>
diff --git a/modules/calcite/src/main/codegen/includes/parserImpls.ftl 
b/modules/calcite/src/main/codegen/includes/parserImpls.ftl
index c2cf7559328..4a05872bbf3 100644
--- a/modules/calcite/src/main/codegen/includes/parserImpls.ftl
+++ b/modules/calcite/src/main/codegen/includes/parserImpls.ftl
@@ -818,7 +818,7 @@ SqlDrop SqlDropView(Span s, boolean replace) :
 }
 
 // TODO: https://issues.apache.org/jira/browse/CALCITE-7592
-//  Remove this method and select-fetch-expression.patch after upgrading to 
Calcite 1.43.
+//  Remove this method and the corresponding replacement in pom.xml after 
upgrading to Calcite 1.43.
 JAVACODE
 SqlNode FetchCount() {
     SqlNode e;
diff --git 
a/modules/calcite/src/main/codegen/patches/select-fetch-expression.patch 
b/modules/calcite/src/main/codegen/patches/select-fetch-expression.patch
deleted file mode 100644
index 60ec72ffb85..00000000000
--- a/modules/calcite/src/main/codegen/patches/select-fetch-expression.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-#
-# 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.
-#
---- a/javacc/Parser.jj
-+++ b/javacc/Parser.jj
-@@ -765,7 +765,7 @@
- {
-     // SQL:2008-style syntax. "OFFSET ... FETCH ...".
-     // If you specify both LIMIT and FETCH, FETCH wins.
--    <FETCH> ( <FIRST> | <NEXT> ) offsetFetch[1] = 
UnsignedNumericLiteralOrParam()
-+    <FETCH> ( <FIRST> | <NEXT> ) offsetFetch[1] = FetchCount()
-     ( <ROW> | <ROWS> ) <ONLY>
- }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelFieldTrimmer.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelFieldTrimmer.java
index b1b4f97e866..61bfd0fca80 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelFieldTrimmer.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelFieldTrimmer.java
@@ -71,10 +71,12 @@ public class IgniteRelFieldTrimmer extends RelFieldTrimmer {
         if (newInput == input && inputMapping.isIdentity() && 
fieldsUsed.cardinality() == fieldCnt)
             return result(sort, Mappings.createIdentity(fieldCnt));
 
+        RelCollation newCollation = RexUtil.apply(inputMapping, collation);
+
         RelNode newSort = sort.copy(
-            sort.getTraitSet(),
+            sort.getTraitSet().replace(newCollation),
             newInput,
-            RexUtil.apply(inputMapping, collation),
+            newCollation,
             sort.offset,
             sort.fetch
         );
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 6386f346369..58ad3ce7acd 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
@@ -257,6 +257,9 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         if (fetch == null)
             return;
 
+        if (SqlUtil.isNullLiteral(fetch, true))
+            throw newValidationError(fetch, 
IgniteResource.INSTANCE.illegalFetchLimit(clauseName));
+
         validateFetchExpression(fetch, clauseName);
         deriveDynamicParameterTypes(fetch);
 
@@ -268,19 +271,34 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         validateFetchOffset(fetch, clauseName);
     }
 
-    /** Reject column references in a fetch expression. */
+    /** Reject column references, aggregate functions, and window functions in 
a fetch expression. */
     // TODO: https://issues.apache.org/jira/browse/CALCITE-7592
     //  Remove this method after upgrading to Calcite 1.43.
     private void validateFetchExpression(SqlNode node, String clauseName) {
-        if (node instanceof SqlIdentifier)
-            throw newValidationError(node, 
IgniteResource.INSTANCE.illegalFetchLimit(clauseName));
+        if (node instanceof SqlIdentifier) {
+            if (makeNullaryCall((SqlIdentifier)node) == null)
+                throw newValidationError(node, 
IgniteResource.INSTANCE.illegalFetchLimit(clauseName));
+
+            return;
+        }
 
         if (node instanceof SqlNodeList) {
             for (SqlNode child : (SqlNodeList)node)
                 validateFetchExpression(child, clauseName);
         }
         else if (node instanceof SqlCall) {
-            for (SqlNode child : ((SqlCall)node).getOperandList()) {
+            SqlCall call = (SqlCall)node;
+
+            if (call.isA(SqlKind.QUERY))
+                throw newValidationError(call, 
IgniteResource.INSTANCE.illegalFetchLimit(clauseName));
+
+            if (call.getKind() == SqlKind.OVER)
+                throw newValidationError(call, 
RESOURCE.windowedAggregateIllegalInClause(clauseName));
+
+            if (call.getOperator().isAggregator())
+                throw newValidationError(call, 
RESOURCE.aggregateIllegalInClause(clauseName));
+
+            for (SqlNode child : call.getOperandList()) {
                 if (child != null)
                     validateFetchExpression(child, clauseName);
             }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SortConverterRule.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SortConverterRule.java
index a3020b4c4d4..0883c7d46c5 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SortConverterRule.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SortConverterRule.java
@@ -17,6 +17,10 @@
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
 import com.google.common.collect.ImmutableMap;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
@@ -26,10 +30,16 @@ import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexDynamicParam;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
 import 
org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
 import 
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.immutables.value.Value;
 
 /**
@@ -74,13 +84,15 @@ public class SortConverterRule extends 
RelRule<SortConverterRule.Config> {
                     sort.fetch));
             }
             else {
+                boolean pushLimit = canPushLimit(sort);
+
                 RelNode igniteSort = new IgniteSort(
                     cluster,
                     
cluster.traitSetOf(IgniteConvention.INSTANCE).replace(sort.getCollation()),
                     convert(sort.getInput(), 
cluster.traitSetOf(IgniteConvention.INSTANCE)),
                     sort.getCollation(),
-                    sort.offset,
-                    sort.fetch,
+                    pushLimit ? sort.offset : null,
+                    pushLimit ? sort.fetch : null,
                     false
                 );
 
@@ -101,4 +113,44 @@ public class SortConverterRule extends 
RelRule<SortConverterRule.Config> {
             call.transformTo(new IgniteSort(cluster, outTraits, input, 
sort.getCollation(), false));
         }
     }
+
+    /** Returns {@code true} if FETCH can be safely pushed to the sort. */
+    private static boolean canPushLimit(Sort sort) {
+        RexNode fetch = sort.fetch;
+
+        // A non-deterministic FETCH must be evaluated only once by the outer 
IgniteLimit.
+        if (!RexUtil.isDeterministic(fetch))
+            return false;
+
+        if (!RexUtil.isConstant(fetch) || containsDynamicParameter(fetch))
+            return false;
+
+        List<RexNode> reducedFetch = new ArrayList<>(1);
+
+        
RexUtils.executor(sort.getCluster()).reduce(sort.getCluster().getRexBuilder(),
+            Collections.singletonList(fetch), reducedFetch);
+
+        if (!(reducedFetch.get(0) instanceof RexLiteral))
+            return false;
+
+        BigDecimal fetchValue = 
((RexLiteral)reducedFetch.get(0)).getValueAs(BigDecimal.class);
+
+        // SortNode does not accept zero FETCH; the outer IgniteLimit handles 
it.
+        return fetchValue != null && fetchValue.compareTo(BigDecimal.ONE) >= 0;
+    }
+
+    /** Returns {@code true} if the expression contains a dynamic parameter. */
+    private static boolean containsDynamicParameter(RexNode node) {
+        if (node instanceof RexDynamicParam)
+            return true;
+
+        if (node instanceof RexCall) {
+            for (RexNode operand : ((RexCall)node).getOperands()) {
+                if (containsDynamicParameter(operand))
+                    return true;
+            }
+        }
+
+        return false;
+    }
 }
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 434c16afff0..909064f7f54 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
@@ -211,11 +211,16 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
 
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST ('abc') 
ROWS ONLY", null);
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST 
(SUBSTRING('abc', 1, 1)) ROWS ONLY", null);
+        assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST 
(CAST(NULL AS INTEGER)) ROWS ONLY", null);
 
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST (id) 
ROWS ONLY", null);
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST (id + 1) 
ROWS ONLY", null);
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST 
(ABS(id)) ROWS ONLY", null);
 
+        assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST (SUM(1)) 
ROWS ONLY", null);
+        assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST 
(ROW_NUMBER() OVER ()) ROWS ONLY",  null);
+        assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST 
(ABS((SELECT 1))) ROWS ONLY", null);
+
         assertThrowsSqlException("SELECT * FROM TEST_REPL FETCH FIRST SQRT(4) 
ROWS ONLY", null);
     }
 
@@ -263,6 +268,14 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
             .returns(1)
             .returns(2)
             .check();
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 
(EXTRACT(YEAR FROM CURRENT_DATE)) ROWS ONLY")
+            .returns(0)
+            .returns(1)
+            .returns(2)
+            .returns(3)
+            .returns(4)
+            .check();
     }
 
     /** */
@@ -289,6 +302,23 @@ public class LimitOffsetIntegrationTest extends 
AbstractBasicIntegrationTransact
             .check();
     }
 
+    /** */
+    @Test
+    public void testFetchExpressionWithoutPushDown() throws Exception {
+        fillCache(cacheRepl, 5);
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 1 ROWS "
+                + "FETCH FIRST (ABS(0.5)) ROWS ONLY")
+            .resultSize(0)
+            .check();
+
+        assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 1 ROWS "
+                + "FETCH FIRST (RAND_INTEGER(1) + 2) ROWS ONLY")
+            .returns(1)
+            .returns(2)
+            .check();
+    }
+
     /**
      * @param c Cache.
      * @param rows Rows count.
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
index cbde3437299..ed244db9178 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/LimitOffsetPlannerTest.java
@@ -17,9 +17,14 @@
 
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
+import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.util.ImmutableIntList;
+import 
org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePlanner;
+import 
org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteExchange;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
@@ -61,6 +66,51 @@ public class LimitOffsetPlannerTest extends 
AbstractPlannerTest {
             .and(hasChildThat(isInstanceOf(IgniteSort.class)))));
     }
 
+    /** */
+    @Test
+    public void testFetchExpressionPushDown() throws Exception {
+        IgniteSchema publicSchema = 
createSchemaWithTable(IgniteDistributions.random());
+
+        assertPlan("SELECT * FROM TEST ORDER BY ID FETCH FIRST (ABS(-2)) ROWS 
ONLY", publicSchema,
+            isInstanceOf(IgniteLimit.class)
+                .and(input(isInstanceOf(IgniteExchange.class)
+                    .and(input(isInstanceOf(IgniteSort.class)
+                        .and(sort -> sort.fetch != null))))));
+
+        assertPlan("SELECT * FROM TEST ORDER BY ID OFFSET 1 ROWS "
+                + "FETCH FIRST (ABS(0.5)) ROWS ONLY", publicSchema,
+            isInstanceOf(IgniteLimit.class)
+                .and(limit -> limit.offset() != null && limit.fetch() != null)
+                .and(input(isInstanceOf(IgniteExchange.class)
+                    .and(input(isInstanceOf(IgniteSort.class)
+                        .and(sort -> sort.offset == null && sort.fetch == 
null))))));
+
+        assertPlan("SELECT * FROM TEST ORDER BY ID OFFSET 1 ROWS "
+                + "FETCH FIRST (RAND_INTEGER(1) + 2) ROWS ONLY", publicSchema,
+            isInstanceOf(IgniteLimit.class)
+                .and(limit -> limit.offset() != null && limit.fetch() != null)
+                .and(input(isInstanceOf(IgniteExchange.class)
+                    .and(input(isInstanceOf(IgniteSort.class)
+                        .and(sort -> sort.offset == null && sort.fetch == 
null))))));
+    }
+
+    /** */
+    @Test
+    public void testFetchExpressionFieldTrimmingPreservesCollationTrait() 
throws Exception {
+        IgniteSchema publicSchema = 
createSchemaWithTable(IgniteDistributions.random());
+        PlanningContext ctx = plannerCtx("SELECT val FROM (SELECT * FROM TEST 
ORDER BY val "
+            + "FETCH FIRST (1 + 1) ROWS ONLY)", publicSchema);
+
+        try (IgnitePlanner planner = ctx.planner()) {
+            SqlNode sql = planner.validate(planner.parse(ctx.query()));
+            RelRoot root = planner.trimUnusedFields(planner.rel(sql));
+            Sort sort = findFirstNode(root.rel, byClass(Sort.class));
+
+            assertNotNull(sort);
+            assertEquals(sort.getCollation(), 
sort.getTraitSet().getCollation());
+        }
+    }
+
     /** */
     @Test
     public void testNestedLimitOffsetWithUnion() throws Exception {

Reply via email to