From ea9dda3e6d16f91b34582a39e7f82df512c4821e Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Thu, 19 Feb 2026 16:28:23 +0300 Subject: [PATCH 1/7] IGNITE-27976 Support OVER clause with unbounded frame --- .../query/calcite/prepare/PlannerHelper.java | 2 + .../query/calcite/prepare/PlannerPhase.java | 18 + .../IgniteLogicalWindowRewriteRule.java | 373 ++++++++++++++++++ .../window/test_window_unbounded_over.test | 114 ++++++ 4 files changed, 507 insertions(+) create mode 100644 modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java create mode 100644 modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java index 1cef9ab4ce633..74837b49f4319 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java @@ -117,6 +117,8 @@ public static IgniteRel optimize(SqlNode sqlNode, IgnitePlanner planner, IgniteL // Transformation chain rel = planner.transform(PlannerPhase.HEP_DECORRELATE, rel.getTraitSet(), rel); + rel = planner.transform(PlannerPhase.HEP_REWRITE_WINDOWS, rel.getTraitSet(), rel); + // RelOptUtil#propagateRelHints(RelNode, equiv) may skip hints because current RelNode has no hints. // Or if hints reside in a child nodes which are not inputs of the current node. Like LogicalFlter#condition. // Such hints may appear or be required below in the tree, after rules applying. diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java index b8333a541663f..35dcd1d3d9cf1 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java @@ -72,6 +72,7 @@ import org.apache.ignite.internal.processors.query.calcite.rule.ValuesConverterRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.ExposeIndexRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.FilterScanMergeRule; +import org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteLogicalWindowRewriteRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.LogicalOrToUnionRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.ProjectScanMergeRule; @@ -102,6 +103,23 @@ public enum PlannerPhase { } }, + /** */ + HEP_REWRITE_WINDOWS("Heuristic phase to rewrite window functions") { + /** {@inheritDoc} */ + @Override public RuleSet getRules(PlanningContext ctx) { + return ctx.rules(RuleSets.ofList( + CoreRules.PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW, + IgniteLogicalWindowRewriteRule.INSTANCE + ) + ); + } + + /** {@inheritDoc} */ + @Override public Program getProgram(PlanningContext ctx) { + return hep(getRules(ctx)); + } + }, + /** */ HEP_FILTER_PUSH_DOWN("Heuristic phase to push down filters") { /** {@inheritDoc} */ diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java new file mode 100644 index 0000000000000..a1903a3654c9e --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -0,0 +1,373 @@ +/* + * 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.rule.logical; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import com.google.common.collect.ImmutableSet; +import org.apache.calcite.plan.RelOptRule; +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rel.RelCollations; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.AggregateCall; +import org.apache.calcite.rel.core.JoinRelType; +import org.apache.calcite.rel.core.Window; +import org.apache.calcite.rel.logical.LogicalAggregate; +import org.apache.calcite.rel.logical.LogicalJoin; +import org.apache.calcite.rel.logical.LogicalProject; +import org.apache.calcite.rel.logical.LogicalWindow; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.rex.RexWindowBound; +import org.apache.calcite.sql.ExplicitOperatorBinding; +import org.apache.calcite.sql.SqlAggFunction; +import org.apache.calcite.sql.SqlOperatorBinding; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.util.ImmutableBitSet; +import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode; +import org.apache.ignite.internal.processors.query.IgniteSQLException; +import org.immutables.value.Value; + +/** + * Rule that rewrites LogicalWindow to LogicalAggregate LogicalJoin LogicalProject. + * This approach is valid only for unbounded frame. + */ +@Value.Enclosing +public class IgniteLogicalWindowRewriteRule extends RelRule { + /** Rule instance. */ + public static final RelOptRule INSTANCE = Config.DEFAULT.toRule(); + + /** + * Constructor. + * + * @param config Rule configuration. + */ + private IgniteLogicalWindowRewriteRule(Config config) { + super(config); + } + + /** {@inheritDoc} */ + @Override public void onMatch(RelOptRuleCall call) { + LogicalWindow win = call.rel(0); + + if (win.groups.size() > 1) { + RelNode input = win.getInput(); + RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); + + for (LogicalWindow.Group grp : win.groups) { + RelDataType joinRowType = buildWindowRowType(typeFactory, input, grp); + + LogicalWindow single = LogicalWindow.create( + input.getTraitSet(), + input, + win.getConstants(), + joinRowType, + List.of(grp) + ); + + input = single; + } + + call.transformTo(input); + + return; + } + + LogicalWindow.Group grp = win.groups.get(0); + + validateSupported(grp); + + RelNode input = win.getInput(); + RexBuilder rexBuilder = win.getCluster().getRexBuilder(); + RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); + + List aggCalls = new ArrayList<>(grp.aggCalls.size()); + + for (Window.RexWinAggCall winAggCall : grp.aggCalls) { + aggCalls.add(toAggregateCall(winAggCall, typeFactory)); + } + + ImmutableBitSet grpSet = grp.keys; + + RelNode agg = LogicalAggregate.create( + input, + grpSet, + null, + aggCalls + ); + + RexNode condition = buildPartitionJoinCondition(rexBuilder, typeFactory, input, agg, grpSet); + + RelNode join = LogicalJoin.create( + input, + agg, + Collections.emptyList(), + condition, + Collections.emptySet(), + JoinRelType.INNER + ); + + RelNode project = LogicalProject.create( + join, + List.of(), + buildProjection(join, win, grp), + win.getRowType().getFieldNames(), + ImmutableSet.of() + ); + + call.transformTo(project); + } + + /** + * Builds a row type for a window with aggregate calls. + * + * @param typeFactory Type factory. + * @param input Input relation. + * @param grp Window group. + * @return A row type combining the input fields and windowed aggregate results. + */ + private static RelDataType buildWindowRowType( + RelDataTypeFactory typeFactory, + RelNode input, + LogicalWindow.Group grp + ) { + RelDataTypeFactory.Builder builder = typeFactory.builder(); + + builder.addAll(input.getRowType().getFieldList()); + + for (int i = 0; i < grp.aggCalls.size(); i++) { + Window.RexWinAggCall winAggCall = grp.aggCalls.get(i); + + String name = "agg$" + i; + + RelDataType type = winAggCall.getType(); + + builder.add(name, type); + } + + return builder.build(); + } + + /** + * Builds a join condition between input and aggregate results using partition keys. + * Returns TRUE for an empty partition set (cross join). + * + * @param rexBuilder Rex builder. + * @param typeFactory Type factory. + * @param input Input relation. + * @param agg Aggregate relation. + * @param groupSet Partition keys. + * @return Join a condition expression. + */ + private static RexNode buildPartitionJoinCondition( + RexBuilder rexBuilder, + RelDataTypeFactory typeFactory, + RelNode input, + RelNode agg, + ImmutableBitSet groupSet + ) { + if (groupSet.isEmpty()) + return rexBuilder.makeLiteral(true); + + int inputFieldCnt = input.getRowType().getFieldCount(); + List keys = groupSet.asList(); + + RelDataType joinRowType = typeFactory.builder() + .addAll(input.getRowType().getFieldList()) + .addAll(agg.getRowType().getFieldList()) + .build(); + + List conditions = new ArrayList<>(keys.size()); + + for (int i = 0; i < keys.size(); i++) { + int keyIdx = keys.get(i); + + RexNode left = rexBuilder.makeInputRef(joinRowType, keyIdx); + RexNode right = rexBuilder.makeInputRef(joinRowType, inputFieldCnt + i); + + conditions.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, left, right)); + } + + return RexUtil.composeConjunction(rexBuilder, conditions); + } + + /** + * Validates that the window definition is supported by this rule: + * only unbounded frames are allowed, and ORDER BY is supported only with a full unbounded frame. + * + * @param group Window group to validate. + */ + private void validateSupported(LogicalWindow.Group group) { + boolean hasOrderBy = !group.orderKeys.getKeys().isEmpty(); + boolean fullFrame = isUnbounded(group.lowerBound, group.upperBound); + + if (hasOrderBy && !fullFrame) { + throw new IgniteSQLException("ORDER BY with bounded frame is not supported yet.", + IgniteQueryErrorCode.UNSUPPORTED_OPERATION); + } + + if (!fullFrame) { + throw new IgniteSQLException("Window frame bounds are not supported yet.", + IgniteQueryErrorCode.UNSUPPORTED_OPERATION); + } + } + + /** + * Checks whether the window frame is fully unbounded (UNBOUNDED PRECEDING ... UNBOUNDED FOLLOWING). + * + * @param lower Lower frame bound. + * @param upper Upper frame bound. + * @return {@code true} if the frame is unbounded. + */ + private static boolean isUnbounded(RexWindowBound lower, RexWindowBound upper) { + if (lower == null && upper == null) + return true; + + if (lower == null || upper == null) + return false; + + return lower.isUnbounded() && lower.isPreceding() + && upper.isUnbounded() && upper.isFollowing(); + } + + /** + * Converts a window aggregate call to a regular AggregateCall, + * inferring the result type from the aggregate function. + * + * @param winAggCall Window aggregate call. + * @param typeFactory Type factory. + * @return AggregateCall for LogicalAggregate. + */ + private static AggregateCall toAggregateCall( + Window.RexWinAggCall winAggCall, + RelDataTypeFactory typeFactory + ) { + List argList = new ArrayList<>(winAggCall.getOperands().size()); + + for (RexNode operand : winAggCall.getOperands()) { + if (operand instanceof RexInputRef) { + RexInputRef ref = (RexInputRef)operand; + argList.add(ref.getIndex()); + } + else { + throw new IgniteSQLException("Window aggregate arguments must be input references.", + IgniteQueryErrorCode.UNSUPPORTED_OPERATION); + } + } + + List operandTypes = winAggCall.getOperands().stream() + .map(RexNode::getType) + .collect(Collectors.toList()); + + SqlAggFunction agg = (SqlAggFunction)winAggCall.getOperator(); + + SqlOperatorBinding binding = new ExplicitOperatorBinding(typeFactory, agg, operandTypes); + + RelDataType inferredType = agg.inferReturnType(binding); + + return AggregateCall.create( + agg, + winAggCall.distinct, + false, + winAggCall.ignoreNulls, + argList, + -1, + RelCollations.EMPTY, + inferredType, + null + ); + } + + /** + * Builds projection expressions for the final Project node: + * all input fields followed by aggregate results, with casts if needed. + * + * @param join Join node. + * @param win Original window node. + * @param grp Window group. + * @return List of projection expressions. + */ + private static List buildProjection(RelNode join, LogicalWindow win, LogicalWindow.Group grp) { + RexBuilder rexBuilder = win.getCluster().getRexBuilder(); + int inputFieldCnt = win.getInput().getRowType().getFieldCount(); + + int grpKeyCnt = grp.keys.cardinality(); + int aggFieldCnt = grp.aggCalls.size(); + + List projects = new ArrayList<>(inputFieldCnt + aggFieldCnt); + + for (int i = 0; i < inputFieldCnt; i++) + projects.add(rexBuilder.makeInputRef(join, i)); + + for (int i = 0; i < aggFieldCnt; i++) { + RexNode ref = rexBuilder.makeInputRef(join, inputFieldCnt + grpKeyCnt + i); + + RelDataType targetType = windowAggType(win, i); + + if (!targetType.equals(ref.getType())) + ref = rexBuilder.makeCast(targetType, ref); + + projects.add(ref); + } + + return projects; + } + + /** + * Returns the expected type of the window aggregate from the window row type. + * + * @param win Window node. + * @param aggIdx Aggregate index. + * @return Aggregate result type. + */ + private static RelDataType windowAggType(LogicalWindow win, int aggIdx) { + int inputFieldCnt = win.getInput().getRowType().getFieldCount(); + return win.getRowType().getFieldList().get(inputFieldCnt + aggIdx).getType(); + } + + /** Rule configuration. */ + @SuppressWarnings("ClassNameSameAsAncestorName") + @Value.Immutable + public interface Config extends RuleFactoryConfig { + /** Default configuration. */ + Config DEFAULT = ImmutableIgniteLogicalWindowRewriteRule.Config.builder() + .withRuleFactory(IgniteLogicalWindowRewriteRule::new) + .withDescription("IgniteLogicalWindowRewriteRule: rewrites LogicalWindow to LogicalAggregate LogicalJoin LogicalProject") + .withOperandSupplier(b -> { + return b.operand(LogicalWindow.class).anyInputs(); + }) + .build(); + + /** + * Returns the rule factory for this configuration. + * + * @return Rule factory. + */ + @Override @Value.Default + default java.util.function.Function ruleFactory() { + return IgniteLogicalWindowRewriteRule::new; + } + } +} diff --git a/modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test b/modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test new file mode 100644 index 0000000000000..19aea46675469 --- /dev/null +++ b/modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test @@ -0,0 +1,114 @@ +# name: test/sql/aggregate/window/test_window_unbounded_over.test +# description: Test window aggregates with unbounded frame +# group: [window] + +statement ok +CREATE TABLE win_emp(emp_id INTEGER, dept_id INTEGER, salary INTEGER); + +statement ok +INSERT INTO win_emp VALUES (1, 10, 100), (2, 10, 200), (3, 20, 50), (4, 20, 150), (5, 20, NULL); + +# OVER() - full partition (no PARTITION BY) +query II +SELECT emp_id, SUM(salary) OVER () AS total_sum FROM win_emp ORDER BY emp_id; +---- +1 500 +2 500 +3 500 +4 500 +5 500 + +# OVER(PARTITION BY) - unbounded frame per department +query III +SELECT emp_id, dept_id, COUNT(*) OVER (PARTITION BY dept_id) AS cnt_by_dept FROM win_emp ORDER BY emp_id; +---- +1 10 2 +2 10 2 +3 20 3 +4 20 3 +5 20 3 + +# multiple aggregates with the same partition +query III +SELECT emp_id, + SUM(salary) OVER (PARTITION BY dept_id) AS sum_by_dept, + MAX(salary) OVER (PARTITION BY dept_id) AS max_by_dept +FROM win_emp +ORDER BY emp_id; +---- +1 300 200 +2 300 200 +3 200 150 +4 200 150 +5 200 150 + +# NULL handling in partition keys +statement ok +INSERT INTO win_emp VALUES (6, NULL, 500), (7, NULL, NULL); + +query III +SELECT emp_id, + COUNT(*) OVER (PARTITION BY dept_id) AS cnt_by_dept, + SUM(salary) OVER (PARTITION BY dept_id) AS sum_by_dept +FROM win_emp +WHERE dept_id IS NULL OR emp_id IN (1,2) +ORDER BY emp_id; +---- +1 2 300 +2 2 300 +6 2 500 +7 2 500 + +# multiple window groups in one SELECT (different partitions) +query IIII +SELECT emp_id, + dept_id, + SUM(salary) OVER (PARTITION BY dept_id) AS sum_by_dept, + COUNT(*) OVER () AS total_cnt +FROM win_emp +ORDER BY emp_id; +---- +1 10 300 7 +2 10 300 7 +3 20 200 7 +4 20 200 7 +5 20 200 7 +6 NULL 500 7 +7 NULL 500 7 + +# Update to getting a fractional average value for average salary +statement ok +UPDATE win_emp SET salary = 60 WHERE emp_id = 7; + +# another multiple-group example (partition + no partition) +query III +SELECT emp_id, + COUNT(*) OVER (PARTITION BY dept_id) AS cnt_by_dept, + AVG(salary) OVER () AS avg_salary_all +FROM win_emp +ORDER BY emp_id; +---- +1 2 176 +2 2 176 +3 3 176 +4 3 176 +5 3 176 +6 2 176 +7 2 176 + +# multiple window groups with different PARTITION BY +query IIII +SELECT emp_id, + dept_id, + SUM(salary) OVER (PARTITION BY dept_id) AS sum_by_dept, + COUNT(*) OVER (PARTITION BY emp_id % 2) AS cnt_by_parity +FROM win_emp +ORDER BY emp_id; +---- +1 10 300 4 +2 10 300 3 +3 20 200 4 +4 20 200 3 +5 20 200 4 +6 NULL 560 3 +7 NULL 560 4 From 4afa58ebcfe58f1d3586587b0a634ba56c5581ae Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Tue, 3 Mar 2026 16:24:45 +0300 Subject: [PATCH 2/7] Added docs and tests that demonstrate a behavior when queries are not supported yet. --- docs/_data/toc.yaml | 2 ++ .../_docs/sql-reference/window-functions.adoc | 27 +++++++++++++++++++ .../test_window_unsupported_frames.test | 21 +++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 docs/_docs/sql-reference/window-functions.adoc create mode 100644 modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test diff --git a/docs/_data/toc.yaml b/docs/_data/toc.yaml index 75cb35532d972..132249cbfe476 100644 --- a/docs/_data/toc.yaml +++ b/docs/_data/toc.yaml @@ -238,6 +238,8 @@ url: sql-reference/operational-commands - title: Aggregate functions url: sql-reference/aggregate-functions + - title: Window Functions + url: sql-reference/window-functions - title: Numeric Functions url: sql-reference/numeric-functions - title: String Functions diff --git a/docs/_docs/sql-reference/window-functions.adoc b/docs/_docs/sql-reference/window-functions.adoc new file mode 100644 index 0000000000000..e1b2b72b5b0ac --- /dev/null +++ b/docs/_docs/sql-reference/window-functions.adoc @@ -0,0 +1,27 @@ +// 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. += Window Functions + +Ignite supports aggregate functions in the window form only for the following syntax: + +[source,sql] +---- +fun() OVER ([PARTITION BY | ]) +---- + +The following constructs are not supported for window aggregates and will throw an exception: + +- `ORDER BY` inside the `OVER(...)` clause; +- explicit window frame bounds (`ROWS`, `RANGE`, `BETWEEN ... AND ...`). diff --git a/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test b/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test new file mode 100644 index 0000000000000..35f00b9c9290b --- /dev/null +++ b/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test @@ -0,0 +1,21 @@ +# name: test/sql/aggregate/window/test_window_unsupported_frames.test +# description: Test unsupported window frame definitions for aggregate window functions +# group: [window] + +statement ok +CREATE TABLE win_emp_bounds(emp_id INTEGER, dept_id INTEGER, salary INTEGER); + +statement ok +INSERT INTO win_emp_bounds VALUES (1, 10, 100), (2, 10, 200), (3, 20, 50); + +# ORDER BY with bounded frame is not supported. +statement error +SELECT emp_id, + SUM(salary) OVER (PARTITION BY dept_id ORDER BY emp_id) +FROM win_emp_bounds; + +# Explicit frame bounds are not supported. +statement error +SELECT emp_id, + SUM(salary) OVER (PARTITION BY dept_id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) +FROM win_emp_bounds; \ No newline at end of file From c58a5648e8f5933e4c5232a05095cd32c287966e Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Tue, 3 Mar 2026 21:10:15 +0300 Subject: [PATCH 3/7] WIP: Added support for window constants --- .../IgniteLogicalWindowRewriteRule.java | 32 ++++++++++++++++++- .../test_window_unsupported_frames.test | 6 +++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java index a1903a3654c9e..7cc6e1b20e5a3 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -38,6 +38,7 @@ import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexUtil; import org.apache.calcite.rex.RexWindowBound; @@ -102,6 +103,7 @@ private IgniteLogicalWindowRewriteRule(Config config) { RelNode input = win.getInput(); RexBuilder rexBuilder = win.getCluster().getRexBuilder(); RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); + RelNode aggInput = appendConstants(input, win.getConstants()); List aggCalls = new ArrayList<>(grp.aggCalls.size()); @@ -112,7 +114,7 @@ private IgniteLogicalWindowRewriteRule(Config config) { ImmutableBitSet grpSet = grp.keys; RelNode agg = LogicalAggregate.create( - input, + aggInput, grpSet, null, aggCalls @@ -140,6 +142,34 @@ private IgniteLogicalWindowRewriteRule(Config config) { call.transformTo(project); } + /** + * Appends LogicalWindow constants to input as additional projection columns. + * + * @param input Input relation. + * @param constants Window constants. + * @return Input relation augmented with constants. + */ + private static RelNode appendConstants(RelNode input, List constants) { + if (constants.isEmpty()) + return input; + + RexBuilder rexBuilder = input.getCluster().getRexBuilder(); + int inputFieldCnt = input.getRowType().getFieldCount(); + + List projects = new ArrayList<>(inputFieldCnt + constants.size()); + List names = new ArrayList<>(input.getRowType().getFieldNames()); + + for (int i = 0; i < inputFieldCnt; i++) + projects.add(rexBuilder.makeInputRef(input, i)); + + projects.addAll(constants); + + for (int i = 0; i < constants.size(); i++) + names.add("_w_const$" + i); + + return LogicalProject.create(input, List.of(), projects, names, ImmutableSet.of()); + } + /** * Builds a row type for a window with aggregate calls. * diff --git a/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test b/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test index 35f00b9c9290b..40eb93a65c231 100644 --- a/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test +++ b/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test @@ -13,9 +13,13 @@ statement error SELECT emp_id, SUM(salary) OVER (PARTITION BY dept_id ORDER BY emp_id) FROM win_emp_bounds; +---- +:.*ORDER BY with bounded frame is not supported yet.* # Explicit frame bounds are not supported. statement error SELECT emp_id, SUM(salary) OVER (PARTITION BY dept_id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -FROM win_emp_bounds; \ No newline at end of file +FROM win_emp_bounds; +---- +:.*Window frame bounds are not supported yet.* \ No newline at end of file From 83d671da518a32b9e264264b31343795b71e332e Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Wed, 4 Mar 2026 17:42:05 +0300 Subject: [PATCH 4/7] Fixed type mismatch in window --- .../logical/IgniteLogicalWindowRewriteRule.java | 14 ++++++++------ .../window/test_window_unbounded_over.test | 11 +++++++++++ .../window/test_window_unsupported_frames.test | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) rename modules/calcite/src/test/sql/{aggregate => }/window/test_window_unbounded_over.test (95%) rename modules/calcite/src/test/sql/{aggregate => }/window/test_window_unsupported_frames.test (93%) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java index 7cc6e1b20e5a3..f5a19f3f67719 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -27,6 +27,7 @@ import org.apache.calcite.plan.RelRule; import org.apache.calcite.rel.RelCollations; import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.Aggregate; import org.apache.calcite.rel.core.AggregateCall; import org.apache.calcite.rel.core.JoinRelType; import org.apache.calcite.rel.core.Window; @@ -42,7 +43,6 @@ import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexUtil; import org.apache.calcite.rex.RexWindowBound; -import org.apache.calcite.sql.ExplicitOperatorBinding; import org.apache.calcite.sql.SqlAggFunction; import org.apache.calcite.sql.SqlOperatorBinding; import org.apache.calcite.sql.fun.SqlStdOperatorTable; @@ -105,14 +105,14 @@ private IgniteLogicalWindowRewriteRule(Config config) { RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); RelNode aggInput = appendConstants(input, win.getConstants()); + ImmutableBitSet grpSet = grp.keys; + List aggCalls = new ArrayList<>(grp.aggCalls.size()); for (Window.RexWinAggCall winAggCall : grp.aggCalls) { - aggCalls.add(toAggregateCall(winAggCall, typeFactory)); + aggCalls.add(toAggregateCall(winAggCall, typeFactory, grpSet.cardinality())); } - ImmutableBitSet grpSet = grp.keys; - RelNode agg = LogicalAggregate.create( aggInput, grpSet, @@ -288,11 +288,13 @@ private static boolean isUnbounded(RexWindowBound lower, RexWindowBound upper) { * * @param winAggCall Window aggregate call. * @param typeFactory Type factory. + * @param grpKeyCnt Partition key count. * @return AggregateCall for LogicalAggregate. */ private static AggregateCall toAggregateCall( Window.RexWinAggCall winAggCall, - RelDataTypeFactory typeFactory + RelDataTypeFactory typeFactory, + int grpKeyCnt ) { List argList = new ArrayList<>(winAggCall.getOperands().size()); @@ -313,7 +315,7 @@ private static AggregateCall toAggregateCall( SqlAggFunction agg = (SqlAggFunction)winAggCall.getOperator(); - SqlOperatorBinding binding = new ExplicitOperatorBinding(typeFactory, agg, operandTypes); + SqlOperatorBinding binding = new Aggregate.AggCallBinding(typeFactory, agg, operandTypes, grpKeyCnt, false); RelDataType inferredType = agg.inferReturnType(binding); diff --git a/modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test similarity index 95% rename from modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test rename to modules/calcite/src/test/sql/window/test_window_unbounded_over.test index 19aea46675469..013a14b5ef6ea 100644 --- a/modules/calcite/src/test/sql/aggregate/window/test_window_unbounded_over.test +++ b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test @@ -2,6 +2,17 @@ # description: Test window aggregates with unbounded frame # group: [window] +# test scalar window functions +query R +SELECT avg(42) OVER () +---- +42.000000 + +query I +SELECT COUNT(DISTINCT 42) OVER () +---- +1 + statement ok CREATE TABLE win_emp(emp_id INTEGER, dept_id INTEGER, salary INTEGER); diff --git a/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test b/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test similarity index 93% rename from modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test rename to modules/calcite/src/test/sql/window/test_window_unsupported_frames.test index 40eb93a65c231..19752b875e5f1 100644 --- a/modules/calcite/src/test/sql/aggregate/window/test_window_unsupported_frames.test +++ b/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test @@ -22,4 +22,4 @@ SELECT emp_id, SUM(salary) OVER (PARTITION BY dept_id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM win_emp_bounds; ---- -:.*Window frame bounds are not supported yet.* \ No newline at end of file +:.*Window frame bounds are not supported yet.* From 64401716e0906006d92d591fda7adf450994bc03 Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Wed, 4 Mar 2026 17:46:50 +0300 Subject: [PATCH 5/7] Fixed code style. --- .../calcite/rule/logical/IgniteLogicalWindowRewriteRule.java | 4 +--- .../src/test/sql/window/test_window_unbounded_over.test | 4 ++-- .../src/test/sql/window/test_window_unsupported_frames.test | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java index f5a19f3f67719..3495ca39e35e7 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -387,9 +387,7 @@ public interface Config extends RuleFactoryConfig { Config DEFAULT = ImmutableIgniteLogicalWindowRewriteRule.Config.builder() .withRuleFactory(IgniteLogicalWindowRewriteRule::new) .withDescription("IgniteLogicalWindowRewriteRule: rewrites LogicalWindow to LogicalAggregate LogicalJoin LogicalProject") - .withOperandSupplier(b -> { - return b.operand(LogicalWindow.class).anyInputs(); - }) + .withOperandSupplier(b -> b.operand(LogicalWindow.class).anyInputs()) .build(); /** diff --git a/modules/calcite/src/test/sql/window/test_window_unbounded_over.test b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test index 013a14b5ef6ea..00709b4272612 100644 --- a/modules/calcite/src/test/sql/window/test_window_unbounded_over.test +++ b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test @@ -1,5 +1,5 @@ -# name: test/sql/aggregate/window/test_window_unbounded_over.test -# description: Test window aggregates with unbounded frame +# name: test/sql/window/test_window_unbounded_over.test +# description: Test window with unbounded frame # group: [window] # test scalar window functions diff --git a/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test b/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test index 19752b875e5f1..67dca3d0670d9 100644 --- a/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test +++ b/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test @@ -1,5 +1,5 @@ -# name: test/sql/aggregate/window/test_window_unsupported_frames.test -# description: Test unsupported window frame definitions for aggregate window functions +# name: test/sql/window/test_window_unsupported_frames.test +# description: Test unsupported window frame definitions for window functions # group: [window] statement ok From f661c326bd0a11fb639f4825365e252a45d85123 Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Thu, 5 Mar 2026 15:46:59 +0300 Subject: [PATCH 6/7] Fixed after review from E.Stanilovskiy --- .../IgniteLogicalWindowRewriteRule.java | 25 ++---- .../window/test_window_unbounded_over.test | 88 +++++++++++++++++++ 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java index 3495ca39e35e7..9107920247185 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -73,9 +73,10 @@ private IgniteLogicalWindowRewriteRule(Config config) { @Override public void onMatch(RelOptRuleCall call) { LogicalWindow win = call.rel(0); + RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); + if (win.groups.size() > 1) { RelNode input = win.getInput(); - RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); for (LogicalWindow.Group grp : win.groups) { RelDataType joinRowType = buildWindowRowType(typeFactory, input, grp); @@ -102,7 +103,6 @@ private IgniteLogicalWindowRewriteRule(Config config) { RelNode input = win.getInput(); RexBuilder rexBuilder = win.getCluster().getRexBuilder(); - RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); RelNode aggInput = appendConstants(input, win.getConstants()); ImmutableBitSet grpSet = grp.keys; @@ -120,7 +120,7 @@ private IgniteLogicalWindowRewriteRule(Config config) { aggCalls ); - RexNode condition = buildPartitionJoinCondition(rexBuilder, typeFactory, input, agg, grpSet); + RexNode condition = buildPartitionJoinCondition(rexBuilder, input, agg, grpSet); RelNode join = LogicalJoin.create( input, @@ -157,17 +157,13 @@ private static RelNode appendConstants(RelNode input, List constants int inputFieldCnt = input.getRowType().getFieldCount(); List projects = new ArrayList<>(inputFieldCnt + constants.size()); - List names = new ArrayList<>(input.getRowType().getFieldNames()); for (int i = 0; i < inputFieldCnt; i++) projects.add(rexBuilder.makeInputRef(input, i)); projects.addAll(constants); - for (int i = 0; i < constants.size(); i++) - names.add("_w_const$" + i); - - return LogicalProject.create(input, List.of(), projects, names, ImmutableSet.of()); + return LogicalProject.create(input, List.of(), projects, (List) null, ImmutableSet.of()); } /** @@ -202,10 +198,9 @@ private static RelDataType buildWindowRowType( /** * Builds a join condition between input and aggregate results using partition keys. - * Returns TRUE for an empty partition set (cross join). + * Returns literal TRUE for an empty partition set (cross join). * * @param rexBuilder Rex builder. - * @param typeFactory Type factory. * @param input Input relation. * @param agg Aggregate relation. * @param groupSet Partition keys. @@ -213,7 +208,6 @@ private static RelDataType buildWindowRowType( */ private static RexNode buildPartitionJoinCondition( RexBuilder rexBuilder, - RelDataTypeFactory typeFactory, RelNode input, RelNode agg, ImmutableBitSet groupSet @@ -224,18 +218,13 @@ private static RexNode buildPartitionJoinCondition( int inputFieldCnt = input.getRowType().getFieldCount(); List keys = groupSet.asList(); - RelDataType joinRowType = typeFactory.builder() - .addAll(input.getRowType().getFieldList()) - .addAll(agg.getRowType().getFieldList()) - .build(); - List conditions = new ArrayList<>(keys.size()); for (int i = 0; i < keys.size(); i++) { int keyIdx = keys.get(i); - RexNode left = rexBuilder.makeInputRef(joinRowType, keyIdx); - RexNode right = rexBuilder.makeInputRef(joinRowType, inputFieldCnt + i); + RexNode left = rexBuilder.makeInputRef(input.getRowType(), keyIdx); + RexNode right = rexBuilder.makeInputRef(agg.getRowType(), inputFieldCnt + i); conditions.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, left, right)); } diff --git a/modules/calcite/src/test/sql/window/test_window_unbounded_over.test b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test index 00709b4272612..7f5610048e0a6 100644 --- a/modules/calcite/src/test/sql/window/test_window_unbounded_over.test +++ b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test @@ -123,3 +123,91 @@ ORDER BY emp_id; 5 20 200 4 6 NULL 560 3 7 NULL 560 4 + +# multiple constraints with PARTITION BY +statement ok +CREATE TABLE win_emp_multi(emp_id INTEGER, dept_id INTEGER, salary INTEGER, grade INTEGER); + +statement ok +INSERT INTO win_emp_multi VALUES (1, 10, 100, 1), (2, 10, 100, 1), (3, 10, 200, 2), (4, 20, 100, 1), (5, 20, 100, 1), (6, 20, 150, 2), (7, 20, 150, 2), (8, 30, NULL, 1); + +query IIII +SELECT + emp_id, + dept_id, + salary, + COUNT(*) OVER (PARTITION BY dept_id, salary) AS cnt_by_dept_salary +FROM win_emp_multi +ORDER BY emp_id; +---- +1 10 100 2 +2 10 100 2 +3 10 200 1 +4 20 100 2 +5 20 100 2 +6 20 150 2 +7 20 150 2 +8 30 NULL 1 + +query IIIIIII +SELECT + emp_id, + dept_id, + salary, + grade, + COUNT(*) OVER (PARTITION BY dept_id, salary) AS cnt_by_group, + AVG(grade) OVER (PARTITION BY dept_id, salary) AS avg_grade_by_group, + SUM(salary) OVER (PARTITION BY dept_id, salary) AS sum_salary_by_group +FROM win_emp_multi +ORDER BY emp_id; +---- +1 10 100 1 2 1 200 +2 10 100 1 2 1 200 +3 10 200 2 1 2 200 +4 20 100 1 2 1 200 +5 20 100 1 2 1 200 +6 20 150 2 2 2 300 +7 20 150 2 2 2 300 +8 30 NULL 1 1 1 NULL + +# PARTITION BY with NULL valuse in both fields +statement ok +INSERT INTO win_emp_multi VALUES (9, NULL, NULL, 2), (10, NULL, NULL, 3); + +query IIIIII +SELECT + emp_id, + dept_id, + salary, + grade, + COUNT(*) OVER (PARTITION BY dept_id, salary) AS cnt_by_group, + AVG(grade) OVER (PARTITION BY dept_id, salary) AS avg_grade_by_group +FROM win_emp_multi +WHERE emp_id > 7 +ORDER BY emp_id; +---- +8 30 NULL 1 1 1 +9 NULL NULL 2 2 2 +10 NULL NULL 3 2 2 + +# сравнение с PARTITION BY по одному полю +query IIIII +SELECT + emp_id, + dept_id, + salary, + COUNT(*) OVER (PARTITION BY dept_id) AS cnt_by_dept_only, + COUNT(*) OVER (PARTITION BY dept_id, salary) AS cnt_by_dept_salary +FROM win_emp_multi +ORDER BY emp_id; +---- +1 10 100 3 2 +2 10 100 3 2 +3 10 200 3 1 +4 20 100 4 2 +5 20 100 4 2 +6 20 150 4 2 +7 20 150 4 2 +8 30 NULL 1 1 +9 NULL NULL 2 2 +10 NULL NULL 2 2 From eb6f82ea2d74d874a76d4d9acdc6b17d25ddb37e Mon Sep 17 00:00:00 2001 From: Vladislav Pyatkov Date: Thu, 5 Mar 2026 22:34:52 +0300 Subject: [PATCH 7/7] Code style. --- .../calcite/rule/logical/IgniteLogicalWindowRewriteRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java index 9107920247185..745e89e402820 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -163,7 +163,7 @@ private static RelNode appendConstants(RelNode input, List constants projects.addAll(constants); - return LogicalProject.create(input, List.of(), projects, (List) null, ImmutableSet.of()); + return LogicalProject.create(input, List.of(), projects, (List)null, ImmutableSet.of()); } /**