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/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..745e89e402820 --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteLogicalWindowRewriteRule.java @@ -0,0 +1,392 @@ +/* + * 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.Aggregate; +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.RexLiteral; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.rex.RexWindowBound; +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); + + RelDataTypeFactory typeFactory = win.getCluster().getTypeFactory(); + + if (win.groups.size() > 1) { + RelNode input = win.getInput(); + + 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(); + 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, grpSet.cardinality())); + } + + RelNode agg = LogicalAggregate.create( + aggInput, + grpSet, + null, + aggCalls + ); + + RexNode condition = buildPartitionJoinCondition(rexBuilder, 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); + } + + /** + * 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()); + + for (int i = 0; i < inputFieldCnt; i++) + projects.add(rexBuilder.makeInputRef(input, i)); + + projects.addAll(constants); + + return LogicalProject.create(input, List.of(), projects, (List)null, ImmutableSet.of()); + } + + /** + * 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 literal TRUE for an empty partition set (cross join). + * + * @param rexBuilder Rex builder. + * @param input Input relation. + * @param agg Aggregate relation. + * @param groupSet Partition keys. + * @return Join a condition expression. + */ + private static RexNode buildPartitionJoinCondition( + RexBuilder rexBuilder, + RelNode input, + RelNode agg, + ImmutableBitSet groupSet + ) { + if (groupSet.isEmpty()) + return rexBuilder.makeLiteral(true); + + int inputFieldCnt = input.getRowType().getFieldCount(); + List keys = groupSet.asList(); + + List conditions = new ArrayList<>(keys.size()); + + for (int i = 0; i < keys.size(); i++) { + int keyIdx = keys.get(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)); + } + + 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. + * @param grpKeyCnt Partition key count. + * @return AggregateCall for LogicalAggregate. + */ + private static AggregateCall toAggregateCall( + Window.RexWinAggCall winAggCall, + RelDataTypeFactory typeFactory, + int grpKeyCnt + ) { + 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 Aggregate.AggCallBinding(typeFactory, agg, operandTypes, grpKeyCnt, false); + + 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 -> 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/window/test_window_unbounded_over.test b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test new file mode 100644 index 0000000000000..7f5610048e0a6 --- /dev/null +++ b/modules/calcite/src/test/sql/window/test_window_unbounded_over.test @@ -0,0 +1,213 @@ +# name: test/sql/window/test_window_unbounded_over.test +# description: Test window 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); + +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 + +# 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 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 new file mode 100644 index 0000000000000..67dca3d0670d9 --- /dev/null +++ b/modules/calcite/src/test/sql/window/test_window_unsupported_frames.test @@ -0,0 +1,25 @@ +# name: test/sql/window/test_window_unsupported_frames.test +# description: Test unsupported window frame definitions for 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; +---- +:.*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; +---- +:.*Window frame bounds are not supported yet.*