From ef7140019687d987205af07cb32198e5b6a9b201 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Wed, 12 Aug 2026 13:31:53 +0300 Subject: [PATCH 1/3] IGNITE-28971 Wip --- .../calcite/exec/LogicalRelImplementor.java | 6 +++- .../query/calcite/prepare/IgnitePlanner.java | 17 ++++++++-- .../calcite/prepare/IgniteSqlValidator.java | 22 +++++++++---- .../sql/IgniteSqlPaginationPolicy.java | 32 +++++++++++++++++++ .../DynamicParametersIntegrationTest.java | 9 +++--- .../LimitOffsetIntegrationTest.java | 12 +++---- .../OperatorsExtensionIntegrationTest.java | 20 ++++++++++++ .../src/test/sql/order/test_limit.test | 4 +++ 8 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java 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 b5c1c06f1ce81..71af0b907d222 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 @@ -125,6 +125,7 @@ import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor; import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex; import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable; +import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.trait.Destination; import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution; import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils; @@ -1084,7 +1085,10 @@ private long validateAndGetFetchOffsetParams(RexNode node, String op) { if (paramAsDecimal.signum() < 0) throw new IllegalArgumentException("Negative value for " + op); - return IgniteMath.convertToLongExact(paramAsDecimal, RoundingMode.DOWN); + IgniteSqlPaginationPolicy pagPlc = ctx.unwrap(IgniteSqlPaginationPolicy.class); + RoundingMode roundingMode = pagPlc == null ? IgniteMath.NUMERIC_ROUNDING_MODE : pagPlc.roundingMode(); + + return IgniteMath.convertToLongExact(paramAsDecimal, roundingMode); } catch (RuntimeException ex) { throw new IgniteSQLException(IgniteResource.INSTANCE.illegalFetchLimit(op).str(), diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java index 553ca3ba89a94..2a73046f94112 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java @@ -91,6 +91,7 @@ import org.apache.ignite.internal.processors.query.IgniteSQLException; import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetadata; import org.apache.ignite.internal.processors.query.calcite.metadata.RelMetadataQueryEx; +import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; 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.util.typedef.F; @@ -365,7 +366,7 @@ private static boolean isAsCall(SqlNode node) { } CalciteCatalogReader catalogReader = this.catalogReader.withSchemaPath(schemaPath); - SqlValidator validator = new IgniteSqlValidator(operatorTbl, catalogReader, typeFactory, validatorCfg, ctx.parameters()); + SqlValidator validator = createSqlValidator(catalogReader); SqlToRelConverter sqlToRelConverter = sqlToRelConverter(validator, catalogReader, sqlToRelConverterCfg); RelRoot root = sqlToRelConverter.convertQuery(sqlNode, true, false); root = root.withRel(sqlToRelConverter.decorrelate(sqlNode, root.rel)); @@ -426,7 +427,7 @@ public String dump() { /** */ private SqlValidator validator() { if (validator == null) - validator = new IgniteSqlValidator(operatorTbl, catalogReader, typeFactory, validatorCfg, ctx.parameters()); + validator = createSqlValidator(catalogReader); return validator; } @@ -798,4 +799,16 @@ protected VolcanoPlannerExt(RelOptCostFactory costFactory, Context externalCtx) super.checkCancel(); } } + + /** */ + private SqlValidator createSqlValidator(CalciteCatalogReader catalogReader) { + return new IgniteSqlValidator( + operatorTbl, + catalogReader, + typeFactory, + validatorCfg, + ctx.parameters(), + ctx.unwrap(IgniteSqlPaginationPolicy.class) + ); + } } 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 22d9be1f083dd..57ceb927de062 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 @@ -75,6 +75,7 @@ import org.apache.ignite.internal.processors.query.calcite.schema.IgniteCacheTable; 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.sql.IgniteSqlPaginationPolicy; 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; @@ -113,25 +114,31 @@ public class IgniteSqlValidator extends SqlValidatorImpl { /** */ private final RelDataType nullType; + /** */ + private final @Nullable IgniteSqlPaginationPolicy pagPlc; + /** * Creates a validator. * - * @param opTab Operator table - * @param catalogReader Catalog reader - * @param typeFactory Type factory - * @param cfg Config - * @param parameters Dynamic parameters + * @param opTab Operator table. + * @param catalogReader Catalog reader. + * @param typeFactory Type factory. + * @param cfg Config. + * @param parameters Dynamic parameters. + * @param pagPlc Pagination policy. */ public IgniteSqlValidator( SqlOperatorTable opTab, CalciteCatalogReader catalogReader, IgniteTypeFactory typeFactory, SqlValidator.Config cfg, - @Nullable Object[] parameters + @Nullable Object[] parameters, + @Nullable IgniteSqlPaginationPolicy pagPlc ) { super(opTab, catalogReader, typeFactory, cfg); this.parameters = parameters; + this.pagPlc = pagPlc; nullType = typeFactory.createSqlType(SqlTypeName.NULL); } @@ -306,7 +313,8 @@ private void checkLimitOffset(Number offsetFetchLimit, SqlNode n, String nodeNam if (val.signum() < 0) throw new IllegalArgumentException("Negative value for " + nodeName); - IgniteMath.convertToLongExact(val, RoundingMode.DOWN); + RoundingMode roundingMode = pagPlc == null ? IgniteMath.NUMERIC_ROUNDING_MODE : pagPlc.roundingMode(); + IgniteMath.convertToLongExact(val, roundingMode); } catch (RuntimeException e) { throw newValidationError(n, IgniteResource.INSTANCE.illegalFetchLimit(nodeName)); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java new file mode 100644 index 0000000000000..47eb4d153935f --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java @@ -0,0 +1,32 @@ +/* + * 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.ignite.internal.processors.query.calcite.sql; + +import java.math.RoundingMode; +import org.apache.calcite.plan.Context; +import org.apache.calcite.tools.Frameworks; + +/** + * Defines a policy for processing values of SQL pagination clauses: LIMIT, FETCH, and OFFSET. + * + *

Custom instance can be supplied through {@link Frameworks.ConfigBuilder#context(Context)}.

+ */ +@FunctionalInterface +public interface IgniteSqlPaginationPolicy { + /** Returns the rounding mode for FETCH, LIMIT and OFFSET values. */ + RoundingMode roundingMode(); +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DynamicParametersIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DynamicParametersIntegrationTest.java index 45f6db66f08b4..cea49a96c10c3 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DynamicParametersIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DynamicParametersIntegrationTest.java @@ -171,15 +171,15 @@ public void testDynamicParameters() { public void testFractionalLimitOffset() { createAndPopulateTable(); - assertQuery("SELECT id FROM person ORDER BY id LIMIT ?").withParams(0.5).resultSize(0).check(); + assertQuery("SELECT id FROM person ORDER BY id LIMIT ?").withParams(0.5).returns(0).check(); assertQuery("SELECT id FROM person ORDER BY id LIMIT ?").withParams(1.4).returns(0).check(); - assertQuery("SELECT id FROM person ORDER BY id LIMIT ?").withParams(1.6).returns(0).check(); + assertQuery("SELECT id FROM person ORDER BY id LIMIT ?").withParams(1.6).returns(0).returns(1).check(); assertThrowsSqlException("SELECT id FROM person ORDER BY id LIMIT ?", null, BigDecimal.valueOf(-1.5)); assertThrowsSqlException("SELECT id FROM person ORDER BY id LIMIT ?", null, BigDecimal.valueOf(-0.5)); assertQuery("SELECT id FROM person ORDER BY id FETCH FIRST ? ROWS ONLY") .withParams(BigDecimal.valueOf(0.5)) - .resultSize(0) + .returns(0) .check(); assertQuery("SELECT id FROM person ORDER BY id FETCH FIRST ? ROWS ONLY") .withParams(BigDecimal.valueOf(1.3)) @@ -188,13 +188,13 @@ public void testFractionalLimitOffset() { assertQuery("SELECT id FROM person ORDER BY id FETCH FIRST ? ROWS ONLY") .withParams(BigDecimal.valueOf(1.6)) .returns(0) + .returns(1) .check(); assertThrowsSqlException("SELECT id FROM person ORDER BY id FETCH FIRST ? ROWS ONLY", null, BigDecimal.valueOf(-1.5)); assertThrowsSqlException("SELECT id FROM person ORDER BY id FETCH FIRST ? ROWS ONLY", null, BigDecimal.valueOf(-0.5)); assertQuery("SELECT id FROM person ORDER BY id OFFSET ? ROWS") .withParams(BigDecimal.valueOf(0.5)) - .returns(0) .returns(1) .returns(2) .returns(3) @@ -208,7 +208,6 @@ public void testFractionalLimitOffset() { .check(); assertQuery("SELECT id FROM person ORDER BY id OFFSET ? ROWS") .withParams(BigDecimal.valueOf(2.6)) - .returns(2) .returns(3) .returns(4) .check(); 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 04eeb5e543e70..74b15d6e47694 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 @@ -134,18 +134,18 @@ public void testInvalidLimitOffset() { public void testFractionalLimitOffset() throws Exception { fillCache(cacheRepl, 4); - assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 0.5").check(); + assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 0.5").returns(0).check(); assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 1.2").returns(0).check(); - assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 1.5").returns(0).check(); + assertQuery("SELECT id FROM TEST_REPL ORDER BY id LIMIT 1.5").returns(0).returns(1).check(); - assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 0.5 ROWS ONLY").check(); + assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 0.5 ROWS ONLY").returns(0).check(); assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 1.3 ROWS ONLY").returns(0).check(); - assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 1.6 ROWS ONLY").returns(0).check(); + assertQuery("SELECT id FROM TEST_REPL ORDER BY id FETCH FIRST 1.6 ROWS ONLY").returns(0).returns(1).check(); assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 0.5 ROWS") - .returns(0).returns(1).returns(2).returns(3).check(); + .returns(1).returns(2).returns(3).check(); assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 2.3 ROWS").returns(2).returns(3).check(); - assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 2.6 ROWS").returns(2).returns(3).check(); + assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 2.6 ROWS").returns(3).check(); } /** diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java index 4d4e402f1e550..0ad8cd90e5cf9 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.processors.query.calcite.integration; import java.math.BigDecimal; +import java.math.RoundingMode; import java.sql.Timestamp; import java.util.HashSet; import java.util.List; @@ -64,6 +65,7 @@ import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteConvertletTable; import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlNodeRewriter; import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlValidator; +import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory; import org.apache.ignite.plugin.AbstractTestPluginProvider; import org.apache.ignite.plugin.PluginContext; @@ -93,6 +95,7 @@ public class OperatorsExtensionIntegrationTest extends AbstractBasicIntegrationT .withSqlNodeRewriter(new SqlRewriter())) .context(Contexts.chain( CalciteQueryProcessor.FRAMEWORK_CONFIG.getContext(), + Contexts.of((IgniteSqlPaginationPolicy)() -> RoundingMode.DOWN), Contexts.of(new AccumulatorFactoryProviderImpl()))) .build(); @@ -178,6 +181,23 @@ public void testCustomAggregateUsesDefaultDistinctHandling() { .check(); } + /** */ + @Test + public void testPaginationRoundingPolicy() { + assertQuery("SELECT x FROM (VALUES (0), (1), (2)) t(x) ORDER BY x LIMIT 1.9") + .returns(0) + .check(); + + assertQuery("SELECT x FROM (VALUES (0), (1), (2)) t(x) ORDER BY x FETCH FIRST 1.9 ROWS ONLY") + .returns(0) + .check(); + + assertQuery("SELECT x FROM (VALUES (0), (1), (2)) t(x) ORDER BY x OFFSET 1.9 ROWS") + .returns(1) + .returns(2) + .check(); + } + /** Rewrites LTRIM with 2 parameters. */ public static SqlCall rewriteLtrim(SqlValidator validator, SqlCall call) { if (call.operandCount() != 2) diff --git a/modules/calcite/src/test/sql/order/test_limit.test b/modules/calcite/src/test/sql/order/test_limit.test index 00ce8f9822d5e..f7fa98caf707d 100644 --- a/modules/calcite/src/test/sql/order/test_limit.test +++ b/modules/calcite/src/test/sql/order/test_limit.test @@ -28,12 +28,14 @@ query I SELECT a FROM test ORDER BY a LIMIT 1.5 ---- 11 +12 # decimal limit query I SELECT a FROM test ORDER BY a LIMIT 1.6 ---- 11 +12 # decimal limit query I @@ -57,12 +59,14 @@ query I SELECT a FROM test ORDER BY a FETCH FIRST 1.5 ROWS ONLY ---- 11 +12 # decimal limit query I SELECT a FROM test ORDER BY a FETCH FIRST 1.6 ROWS ONLY ---- 11 +12 # decimal offset/limit query I From 8d01be8c28c6da546f0570a938368a5a466ac28f Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Tue, 18 Aug 2026 16:13:08 +0300 Subject: [PATCH 2/3] IGNITE-28971 After review 1.0 --- .../query/calcite/exec/LogicalRelImplementor.java | 5 +---- .../query/calcite/prepare/AbstractQueryContext.java | 3 ++- .../processors/query/calcite/prepare/IgnitePlanner.java | 7 ++++++- .../query/calcite/prepare/IgniteSqlValidator.java | 4 +--- .../processors/query/calcite/prepare/PlanningContext.java | 2 +- .../query/calcite/sql/IgniteSqlPaginationPolicy.java | 8 ++++++++ .../calcite/integration/LimitOffsetIntegrationTest.java | 2 +- 7 files changed, 20 insertions(+), 11 deletions(-) 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 fea06c695d705..53c34766087d0 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 @@ -18,7 +18,6 @@ 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; @@ -1096,9 +1095,7 @@ private long validateAndGetFetchOffsetParams(RexNode node, String op) { throw new IllegalArgumentException("Negative value for " + op); IgniteSqlPaginationPolicy pagPlc = ctx.unwrap(IgniteSqlPaginationPolicy.class); - RoundingMode roundingMode = pagPlc == null ? IgniteMath.NUMERIC_ROUNDING_MODE : pagPlc.roundingMode(); - - return IgniteMath.convertToLongExact(paramAsDecimal, roundingMode); + return IgniteSqlPaginationPolicy.convertToLongExact(paramAsDecimal, pagPlc); } catch (RuntimeException ex) { throw new IgniteSQLException(IgniteResource.INSTANCE.illegalFetchLimit(op).str(), diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractQueryContext.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractQueryContext.java index 7fd4d73efc76d..b892136f5b7f5 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractQueryContext.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractQueryContext.java @@ -18,6 +18,7 @@ package org.apache.ignite.internal.processors.query.calcite.prepare; import org.apache.calcite.plan.Context; +import org.jetbrains.annotations.Nullable; /** * Abstract query context. @@ -32,7 +33,7 @@ public AbstractQueryContext(Context parentCtx) { } /** {@inheritDoc} */ - @Override public C unwrap(Class aCls) { + @Override public @Nullable C unwrap(Class aCls) { if (aCls == getClass()) return aCls.cast(this); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java index 5c4ddaf2c6e9d..8014321b93652 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java @@ -436,7 +436,7 @@ public boolean isAggregate(SqlSelect select, @Nullable SqlNodeList orderList) { /** */ private SqlValidator validator() { if (validator == null) - validator = createSqlValidator(catalogReader); + validator = createSqlValidator(); return validator; } @@ -820,4 +820,9 @@ private SqlValidator createSqlValidator(CalciteCatalogReader catalogReader) { ctx.unwrap(IgniteSqlPaginationPolicy.class) ); } + + /** */ + private SqlValidator createSqlValidator() { + return createSqlValidator(catalogReader); + } } 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 fcf01f158c3db..4f117efefdf0c 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 @@ -18,7 +18,6 @@ package org.apache.ignite.internal.processors.query.calcite.prepare; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; @@ -392,8 +391,7 @@ private void checkLimitOffset(Number offsetFetchLimit, SqlNode n, String nodeNam if (val.signum() < 0) throw new IllegalArgumentException("Negative value for " + nodeName); - RoundingMode roundingMode = pagPlc == null ? IgniteMath.NUMERIC_ROUNDING_MODE : pagPlc.roundingMode(); - IgniteMath.convertToLongExact(val, roundingMode); + IgniteSqlPaginationPolicy.convertToLongExact(val, pagPlc); } catch (RuntimeException e) { throw newValidationError(n, IgniteResource.INSTANCE.illegalFetchLimit(nodeName)); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java index 4b401dabfade0..b522f1a0db30b 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlanningContext.java @@ -161,7 +161,7 @@ public RelOptCluster cluster() { } /** {@inheritDoc} */ - @Override public C unwrap(Class aCls) { + @Override public @Nullable C unwrap(Class aCls) { if (aCls == getClass()) return aCls.cast(this); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java index 47eb4d153935f..de955fcdda481 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java @@ -19,6 +19,8 @@ import java.math.RoundingMode; import org.apache.calcite.plan.Context; import org.apache.calcite.tools.Frameworks; +import org.apache.ignite.internal.processors.query.calcite.util.IgniteMath; +import org.jetbrains.annotations.Nullable; /** * Defines a policy for processing values of SQL pagination clauses: LIMIT, FETCH, and OFFSET. @@ -29,4 +31,10 @@ public interface IgniteSqlPaginationPolicy { /** Returns the rounding mode for FETCH, LIMIT and OFFSET values. */ RoundingMode roundingMode(); + + /** Rounds the given value according to the specified policy and converts it to {@code long}. */ + static long convertToLongExact(Number value, @Nullable IgniteSqlPaginationPolicy policy) { + RoundingMode roundingMode = policy == null ? IgniteMath.NUMERIC_ROUNDING_MODE : policy.roundingMode(); + return IgniteMath.convertToLongExact(value, roundingMode); + } } 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 c115a55874d9d..ac0f38e46cc20 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 @@ -352,7 +352,7 @@ public void testFetchExpressionWithoutPushDown() throws Exception { assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 1 ROWS " + "FETCH FIRST (ABS(0.5)) ROWS ONLY") - .resultSize(0) + .returns(1) .check(); assertQuery("SELECT id FROM TEST_REPL ORDER BY id OFFSET 1 ROWS " From 9075366f10650d2670418297a43c0c7640ab3574 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Thu, 20 Aug 2026 16:55:55 +0300 Subject: [PATCH 3/3] IGNITE-28971 After review 2.0 --- .../calcite/exec/LogicalRelImplementor.java | 2 +- .../query/calcite/prepare/IgnitePlanner.java | 1 - .../IgniteSqlPaginationPolicy.java | 5 ++--- .../calcite/prepare/IgniteSqlValidator.java | 1 - .../query/calcite/rule/SortConverterRule.java | 14 ++++++++++++- .../OperatorsExtensionIntegrationTest.java | 2 +- .../calcite/planner/AbstractPlannerTest.java | 21 +++++++++++++++---- .../planner/LimitOffsetPlannerTest.java | 16 +++++++++++++- 8 files changed, 49 insertions(+), 13 deletions(-) rename modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/{sql => prepare}/IgniteSqlPaginationPolicy.java (86%) 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 53c34766087d0..5b7d70934abae 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 @@ -85,6 +85,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.rel.WindowNode; import org.apache.ignite.internal.processors.query.calcite.metadata.AffinityService; import org.apache.ignite.internal.processors.query.calcite.metadata.ColocationGroup; +import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCollect; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin; @@ -125,7 +126,6 @@ import org.apache.ignite.internal.processors.query.calcite.schema.CacheTableDescriptor; import org.apache.ignite.internal.processors.query.calcite.schema.IgniteIndex; import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable; -import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.trait.Destination; import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution; import org.apache.ignite.internal.processors.query.calcite.trait.TraitUtils; diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java index 8014321b93652..2ed2fe2ce4221 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java @@ -91,7 +91,6 @@ import org.apache.ignite.internal.processors.query.IgniteSQLException; import org.apache.ignite.internal.processors.query.calcite.metadata.IgniteMetadata; import org.apache.ignite.internal.processors.query.calcite.metadata.RelMetadataQueryEx; -import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; 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.util.typedef.F; diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlPaginationPolicy.java similarity index 86% rename from modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java rename to modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlPaginationPolicy.java index de955fcdda481..e7f81e9456e63 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlPaginationPolicy.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlPaginationPolicy.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.ignite.internal.processors.query.calcite.sql; +package org.apache.ignite.internal.processors.query.calcite.prepare; import java.math.RoundingMode; import org.apache.calcite.plan.Context; @@ -34,7 +34,6 @@ public interface IgniteSqlPaginationPolicy { /** Rounds the given value according to the specified policy and converts it to {@code long}. */ static long convertToLongExact(Number value, @Nullable IgniteSqlPaginationPolicy policy) { - RoundingMode roundingMode = policy == null ? IgniteMath.NUMERIC_ROUNDING_MODE : policy.roundingMode(); - return IgniteMath.convertToLongExact(value, roundingMode); + return policy == null ? IgniteMath.convertToLongExact(value) : IgniteMath.convertToLongExact(value, policy.roundingMode()); } } 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 4f117efefdf0c..b3c643b7b9b14 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 @@ -76,7 +76,6 @@ import org.apache.ignite.internal.processors.query.calcite.schema.IgniteCacheTable; 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.sql.IgniteSqlPaginationPolicy; 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; 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 6025930981eaf..9cd4c23cc3f21 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 @@ -35,6 +35,7 @@ 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.prepare.IgniteSqlPaginationPolicy; 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; @@ -139,8 +140,19 @@ private static boolean canPushLimit(Sort sort) { BigDecimal fetchVal = ((RexLiteral)reducedFetch.get(0)).getValueAs(BigDecimal.class); + if (fetchVal == null) + return false; + // SortNode does not accept zero FETCH; the outer IgniteLimit handles it. - return fetchVal != null && fetchVal.compareTo(BigDecimal.ONE) >= 0; + IgniteSqlPaginationPolicy pagPlc = sort.getCluster().getPlanner().getContext().unwrap(IgniteSqlPaginationPolicy.class); + + try { + return IgniteSqlPaginationPolicy.convertToLongExact(fetchVal, pagPlc) > 0; + } + catch (ArithmeticException ignored) { + // The outer IgniteLimit will report invalid FETCH during execution. + return false; + } } /** Returns {@code true} if the expression contains a dynamic parameter. */ diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java index 81e1e3c5d037a..8825c2b4eb3f5 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/OperatorsExtensionIntegrationTest.java @@ -67,8 +67,8 @@ import org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.Accumulators; import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteConvertletTable; import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlNodeRewriter; +import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteSqlValidator; -import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlPaginationPolicy; import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory; import org.apache.ignite.plugin.AbstractTestPluginProvider; import org.apache.ignite.plugin.PluginContext; diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java index 3da09de12185a..6c29252f6bd64 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java @@ -28,6 +28,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; +import org.apache.calcite.plan.Context; import org.apache.calcite.plan.Contexts; import org.apache.calcite.plan.RelOptListener; import org.apache.calcite.plan.RelOptUtil; @@ -220,7 +221,7 @@ protected PlanningContext plannerCtx( @Nullable RelOptListener planLsnr, String... disabledRules ) { - return plannerCtx(sql, Collections.singleton(publicSchema), planLsnr, null, ImmutableSet.copyOf(disabledRules)); + return plannerCtx(sql, Collections.singleton(publicSchema), planLsnr, null, ImmutableSet.copyOf(disabledRules), null); } /** */ @@ -229,10 +230,13 @@ private PlanningContext plannerCtx( Collection schemas, @Nullable RelOptListener planLsnr, Collection params, - Collection disabledRules + Collection disabledRules, + @Nullable Context additionalCtx ) { + Context parentCtx = Contexts.of(baseQueryContext(schemas), planLsnr); + PlanningContext.Builder ctxBuilder = PlanningContext.builder() - .parentContext(Contexts.of(baseQueryContext(schemas), planLsnr)) + .parentContext(additionalCtx == null ? parentCtx : Contexts.chain(parentCtx, additionalCtx)) .query(sql); if (params != null) @@ -842,6 +846,9 @@ public class TestPlanningContextBuilder { /** */ @Nullable private RelOptListener planListener; + /** */ + @Nullable private Context additionalCtx; + /** */ public TestPlanningContextBuilder query(String qry) { query = qry; @@ -884,9 +891,15 @@ public TestPlanningContextBuilder planListener(@Nullable RelOptListener planList return this; } + /** */ + public TestPlanningContextBuilder additionalCtx(Context additionalCtx) { + this.additionalCtx = additionalCtx; + return this; + } + /** */ PlanningContext build() { - return plannerCtx(query, schemas, planListener, params, disabledRules); + return plannerCtx(query, schemas, planListener, params, disabledRules, additionalCtx); } } 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 65514db6ea470..dc876471513dd 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,6 +17,8 @@ package org.apache.ignite.internal.processors.query.calcite.planner; +import java.math.RoundingMode; +import org.apache.calcite.plan.Contexts; import org.apache.calcite.rel.RelRoot; import org.apache.calcite.rel.core.Sort; import org.apache.calcite.rel.type.RelDataType; @@ -25,6 +27,7 @@ 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.IgniteSqlPaginationPolicy; 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; @@ -88,7 +91,18 @@ public void testFetchExpressionPushDown() throws Exception { .and(sort -> sort.fetch != null)))))); assertPlan("SELECT * FROM TEST ORDER BY ID OFFSET 1 ROWS " - + "FETCH FIRST (ABS(0.5)) ROWS ONLY", publicSchema, + + "FETCH FIRST (ABS(0.6)) 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(contextBuilder() + .query("SELECT * FROM TEST ORDER BY ID OFFSET 1 ROWS " + + "FETCH FIRST (ABS(0.6)) ROWS ONLY") + .schema(publicSchema) + .additionalCtx(Contexts.of((IgniteSqlPaginationPolicy)() -> RoundingMode.DOWN)), isInstanceOf(IgniteLimit.class) .and(limit -> limit.offset() != null && limit.fetch() != null) .and(input(isInstanceOf(IgniteExchange.class)