Skip to content

IGNITE-28844 Calcite. Improve type checking in LIMIT / OFFSET clauses - #13311

Open
zstan wants to merge 25 commits into
apache:masterfrom
zstan:ignite-28844
Open

IGNITE-28844 Calcite. Improve type checking in LIMIT / OFFSET clauses#13311
zstan wants to merge 25 commits into
apache:masterfrom
zstan:ignite-28844

Conversation

@zstan

@zstan zstan commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (“IGNITE-28844 Calcite. Improve type checking in LIMIT / OFFSET clauses”) tightens validation for LIMIT / OFFSET values (including dynamic parameters) and aligns execution nodes to accept long-based fetch/offset values, with accompanying tests.

Changes:

  • Added stricter validator checks for LIMIT/OFFSET literals and dynamic parameters (numeric-only, long-range, non-negative).
  • Introduced a planner-test helper (StatementChecker) and added new planner/integration tests for dynamic parameters in LIMIT/OFFSET.
  • Updated execution nodes (LimitNode, SortNode) and implementor wiring to use long fetch/offset values and centralize “illegal fetch/offset” error reporting.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
modules/calcite/src/test/sql/order/test_limit.test Adds negative SQL cases for invalid FETCH FIRST / LIMIT values and expressions.
modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java Registers the new dynamic-parameters planner test in the suite.
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/StatementChecker.java Adds a new test helper for validating statements/plans (currently has critical correctness gaps).
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/DynamicParametersPlannerTest.java Adds planner-level tests for dynamic parameter typing/range checks in LIMIT/OFFSET.
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java Adds checkStatement(...) helper and PlanChecker implementation to drive planner validation in tests.
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DynamicParametersIntegrationTest.java Adds integration coverage for LIMIT ? with integer and BigDecimal parameters.
modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitExecutionTest.java Updates execution tests to new LimitNode/SortNode APIs (currently inconsistent for “unlimited” fetch).
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/IgniteResource.java Adds new validator error messages for illegal fetch/offset and dynamic parameter type mismatch.
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java Implements the new fetch/offset validation logic for literals and dynamic params.
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/SortNode.java Switches limited-sort parameter types to long and adjusts bounded/unbounded buffer selection.
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/LimitNode.java Switches limit/offset parameter types to long and updates request/push logic for new semantics.
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/AbstractNode.java Introduces a shared NOT_WAITING sentinel constant.
modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java Wires runtime fetch/offset evaluation and validation into LimitNode / SortNode construction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Comment thread modules/calcite/src/test/sql/order/test_limit.test
@zstan
zstan force-pushed the ignite-28844 branch 2 times, most recently from 79554b4 to 6201e62 Compare July 18, 2026 16:50
@tkalkirill

Copy link
Copy Markdown
Contributor

Hi, regarding https://issues.apache.org/jira/browse/CALCITE-7624, I implemented support for BigDecimal for the OFFSET/FETCH and LIMIT in #13375.

private final long fetch;

/** Fetch can be unset. */
private final boolean fetchUndefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add long limit = fetch == -1 ? Long.MAX_VALUE : IgniteMath.addExact(fetch, offset) instead. To execute IgniteMath.addExact(fetch, offset) only once (and not for each request and each push), to simplify hasMoreData (rowsProcessed < limit).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

private final boolean fetchUndefined;

/** Already processed (pushed to upstream) rows count. */
private int rowsProcessed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing int with long, can overflow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (fetchNode == null || (fetchNode != null && rowsProcessed <= fetch + offset))
downstream().push(row);
if (rowsProcessed >= offset && hasMoreData()) {
// this two rows can`t be swapped, cause if all requested rows have been pushed it will trigger further request call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should be started with an uppercase letter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

*/
private void checkIntegerLimit(SqlNode n, String nodeName) {
private void invalidateFetchOffset(@Nullable SqlNode n, String nodeName) {
if (n == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant braces

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

/** */
protected <T extends RelNode> void assertPlan(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant method. Used in assertThrows, but it's better to write own assertThrows method with parameters like PlannerContext or PlannerContextBuilder.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/** */
@SuppressWarnings("ThrowableNotThrown")
static void assertThrows(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rewrite to assertThrows(PlannerContext, cls, msg) or assertThrows(PlannerContextBuilder, cls, msg) and use physicalPlan(...) method inside (not assertPlan)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

TestPlanningContextBuilder ctxBuilder,
Predicate<T> predicate
) throws Exception {
IgniteRel plan = physicalPlan(plannerCtx(ctxBuilder.query, ctxBuilder.schemas, ctxBuilder.planListener,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move plannerCtx(...) to TestPlanningContextBuilder.build() method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.CU;
import org.apache.ignite.testframework.GridTestUtils;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwize it resolves as : org.apache.ignite.internal.processors.query.calcite.planner.AbstractPlannerTest#assertThrows

limit = fetch == -1 ? -1 : (fetch > Long.MAX_VALUE - offset ? -1 : fetch + offset);

if (limit < 0)
if (limit < 1 || limit > Integer.MAX_VALUE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rich this line with limit == 0? If yes, it's more correct to use `limit <= 1' or add other logic to return empty result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no we can`t add assert

@@ -84,9 +87,6 @@
/** Validator. */
@Value.Enclosing
public class IgniteSqlValidator extends SqlValidatorImpl {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should use SqlTypeName.DECIMAL, similar to the CALCITE-7624?
org.apache.calcite.sql.validate.SqlValidatorImpl#handleOffsetFetch

A question regarding rounding might arise; we can use the Calcite org.apache.calcite.adapter.enumerable.FetchOffsetRoundingPolicy interface, and it will handle rounding when creating nodes.

@zstan zstan Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this issue is an adoption of related ignite-3 issue, let`s move sequentially here and implement it during different activity, also i suppose that Rounding policy need to work equal for aggregates avg too, need additional tests and check that it work the same as in common DB`s

@tkalkirill tkalkirill Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this issue is an adoption of related ignite-3 issue###

Which issue are you referring to?

let`s move sequentially here and implement it during different activity

Why? In this ticket, you're checking how it handles BigDecimal, and it’s not obvious to me how it will behave with floating-point numbers.

Rounding policy need to work equal for aggregates avg too

Rounding within a set of values ​​is one thing, while rounding during calculations is another. That is precisely why the interface is named FetchOffsetRoundingPolicy to avoid conflating the two types of logic.

need additional tests and check that it work the same as in common DB`s

As a Calcite ticket investigation revealed, different DBMSs handle floating-point numbers differently for FETCH, OFFSET, and LIMIT. That is why this interface was introduced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which issue are you referring to?
written in initial issue:
https://issues.apache.org/jira/browse/IGNITE-24227

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the link; I also left some questions.

Object param = parameters[dynamicParam.getIndex()];

if (!(param instanceof Number)) {
SqlTypeName expectType = SqlTypeName.BIGINT;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we support the SqlTypeName.DECIMAL as in Calcite, then it will need to be fixed.

try {
long res = IgniteMath.convertToLongExact(offsetFetchLimit);

if (res < 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we support the SqlTypeName.DECIMAL as in Calcite, then it will need to be fixed.

waiting = -1;
waiting = NOT_WAITING;

downstream().end();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requested field check required here. And requested = 0 before downstream().end(). See related problem https://issues.apache.org/jira/browse/IGNITE-28925

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@zstan
zstan requested a review from isapego as a code owner August 3, 2026 14:20
this.fetchNode = fetchNode;
this.offset = offset;
rowsSummary = fetch == -1 ? Long.MAX_VALUE : IgniteMath.addExact(fetch, offset);
this.fetch = fetch == -1 ? 0 : fetch;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says nothing about what 0 means.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change doc for :

    /** How many rows need to be processed. */
    private final long fetch;

is it ok ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, what do you mean by "zero"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

directly how it written in documentation: "How many rows need to be processed"

@tkalkirill tkalkirill Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description is misleading because fetch == 0 has two opposite meanings: either no rows should be returned when 1FETCH 01 is specified, or all remaining rows should be returned when FETCH is not specified. These two opposite cases should not be represented by the same value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i tried to fix it ..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is fine, but replace the duplicated -1 sentinel here:

rowsSummary = fetch == FETCH_DEFAULT ? Long.MAX_VALUE : IgniteMath.addExact(fetch, offset);

Also update the constructor parameter documentation to reference {@link #FETCH_DEFAULT} instead of mentioning the literal -1 directly. This keeps the implementation and documentation tied to the declared constant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't fix the documentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, missed, now it`s ok

@Nullable Supplier<Integer> offset,
@Nullable Supplier<Integer> fetch
long offset,
long fetch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says nothing about what -1 means.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the declared default constants consistently throughout this constructor. Replace the literal -1 in the fetch parameter documentation with {@link #FETCH_DEFAULT} and update the assertion accordingly:

assert fetch == FETCH_DEFAULT || fetch > 0 : "Unexpected fetch = " + fetch;

The delegating constructor should also pass OFFSET_DEFAULT and FETCH_DEFAULT instead of the literal 0 and -1 values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't fix the documentation.
You didn't fix the second constructor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@zstan
zstan requested a review from tkalkirill August 5, 2026 05:04
@ignitetcbot

Copy link
Copy Markdown
Contributor

TCBot Test Analysis

Possible Blockers (0)

No blockers found.

New Tests (1)

  • Calcite SQL 2: 1 tests
    • IgniteCalciteTestSuite2: DynamicParametersPlannerTest.testLimitOffset - PASSED

* @param ctx Execution context.
* @param rowType Row type.
* @param offset How many rows need to be skipped.
* @param fetch How many rows need to be processed, {@code -1} if param is undefined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param fetch How many rows need to be processed, {@code -1} if param is undefined.
* @param fetch How many rows need to be processed, {@link #FETCH_DEFAULT} if param is undefined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* @param comp Rows comparator.
* @param offset Offset.
* @param fetch Limit.
* @param fetch How many rows need to be processed, {@code -1} if param is undefined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param fetch How many rows need to be processed, {@code -1} if param is undefined.
* @param fetch How many rows need to be processed, {@link #FETCH_DEFAULT} if param is undefined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done thanks

*/
public SortNode(ExecutionContext<Row> ctx, RelDataType rowType, Comparator<Row> comp) {
this(ctx, rowType, comp, null, null);
this(ctx, rowType, comp, 0, -1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this(ctx, rowType, comp, 0, -1);
this(ctx, rowType, comp, OFFSET_DEFAULT, FETCH_DEFAULT);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants