IGNITE-28844 Calcite. Improve type checking in LIMIT / OFFSET clauses - #13311
IGNITE-28844 Calcite. Improve type checking in LIMIT / OFFSET clauses#13311zstan wants to merge 25 commits into
Conversation
There was a problem hiding this comment.
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/OFFSETliterals and dynamic parameters (numeric-only, long-range, non-negative). - Introduced a planner-test helper (
StatementChecker) and added new planner/integration tests for dynamic parameters inLIMIT/OFFSET. - Updated execution nodes (
LimitNode,SortNode) and implementor wiring to uselongfetch/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.
79554b4 to
6201e62
Compare
|
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; |
There was a problem hiding this comment.
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).
| private final boolean fetchUndefined; | ||
|
|
||
| /** Already processed (pushed to upstream) rows count. */ | ||
| private int rowsProcessed; |
There was a problem hiding this comment.
Comparing int with long, can overflow
| 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. |
There was a problem hiding this comment.
Comment should be started with an uppercase letter.
| */ | ||
| private void checkIntegerLimit(SqlNode n, String nodeName) { | ||
| private void invalidateFetchOffset(@Nullable SqlNode n, String nodeName) { | ||
| if (n == null) { |
| } | ||
|
|
||
| /** */ | ||
| protected <T extends RelNode> void assertPlan( |
There was a problem hiding this comment.
Redundant method. Used in assertThrows, but it's better to write own assertThrows method with parameters like PlannerContext or PlannerContextBuilder.
|
|
||
| /** */ | ||
| @SuppressWarnings("ThrowableNotThrown") | ||
| static void assertThrows( |
There was a problem hiding this comment.
Let's rewrite to assertThrows(PlannerContext, cls, msg) or assertThrows(PlannerContextBuilder, cls, msg) and use physicalPlan(...) method inside (not assertPlan)
| TestPlanningContextBuilder ctxBuilder, | ||
| Predicate<T> predicate | ||
| ) throws Exception { | ||
| IgniteRel plan = physicalPlan(plannerCtx(ctxBuilder.query, ctxBuilder.schemas, ctxBuilder.planListener, |
There was a problem hiding this comment.
Move plannerCtx(...) to TestPlanningContextBuilder.build() method.
| 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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
no we can`t add assert
| @@ -84,9 +87,6 @@ | |||
| /** Validator. */ | |||
| @Value.Enclosing | |||
| public class IgniteSqlValidator extends SqlValidatorImpl { | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Which issue are you referring to?
written in initial issue:
https://issues.apache.org/jira/browse/IGNITE-24227
There was a problem hiding this comment.
Thanks for the link; I also left some questions.
| Object param = parameters[dynamicParam.getIndex()]; | ||
|
|
||
| if (!(param instanceof Number)) { | ||
| SqlTypeName expectType = SqlTypeName.BIGINT; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
If we support the SqlTypeName.DECIMAL as in Calcite, then it will need to be fixed.
| waiting = -1; | ||
| waiting = NOT_WAITING; | ||
|
|
||
| downstream().end(); |
There was a problem hiding this comment.
requested field check required here. And requested = 0 before downstream().end(). See related problem https://issues.apache.org/jira/browse/IGNITE-28925
| this.fetchNode = fetchNode; | ||
| this.offset = offset; | ||
| rowsSummary = fetch == -1 ? Long.MAX_VALUE : IgniteMath.addExact(fetch, offset); | ||
| this.fetch = fetch == -1 ? 0 : fetch; |
There was a problem hiding this comment.
The documentation says nothing about what 0 means.
There was a problem hiding this comment.
change doc for :
/** How many rows need to be processed. */
private final long fetch;
is it ok ?
There was a problem hiding this comment.
No, what do you mean by "zero"?
There was a problem hiding this comment.
directly how it written in documentation: "How many rows need to be processed"
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
ok, i tried to fix it ..
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
You didn't fix the documentation.
There was a problem hiding this comment.
yep, missed, now it`s ok
| @Nullable Supplier<Integer> offset, | ||
| @Nullable Supplier<Integer> fetch | ||
| long offset, | ||
| long fetch |
There was a problem hiding this comment.
The documentation says nothing about what -1 means.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
You didn't fix the documentation.
You didn't fix the second constructor.
TCBot Test Analysis
Possible Blockers (0)No blockers found. New Tests (1)
|
| * @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. |
There was a problem hiding this comment.
| * @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. |
| * @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. |
There was a problem hiding this comment.
| * @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. |
| */ | ||
| public SortNode(ExecutionContext<Row> ctx, RelDataType rowType, Comparator<Row> comp) { | ||
| this(ctx, rowType, comp, null, null); | ||
| this(ctx, rowType, comp, 0, -1); |
There was a problem hiding this comment.
| this(ctx, rowType, comp, 0, -1); | |
| this(ctx, rowType, comp, OFFSET_DEFAULT, FETCH_DEFAULT); |
https://issues.apache.org/jira/browse/IGNITE-28844