This is an automated email from the ASF dual-hosted git repository.

xuzifu666 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new c92f9e8b73 [CALCITE-7697] Simplify window PARTITION BY and ORDER BY 
keys in RelBuilder
c92f9e8b73 is described below

commit c92f9e8b7380679f0a665f31ffddef334663f645
Author: Yu Xu <[email protected]>
AuthorDate: Fri Aug 7 21:41:29 2026 +0800

    [CALCITE-7697] Simplify window PARTITION BY and ORDER BY keys in RelBuilder
---
 .../java/org/apache/calcite/tools/RelBuilder.java  |  63 ++++++++++++-
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |   5 +-
 .../org/apache/calcite/test/RelBuilderTest.java    | 102 +++++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  12 +--
 core/src/test/resources/sql/sub-query.iq           |   4 +-
 5 files changed, 175 insertions(+), 11 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java 
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 7cc7437c62..97d2e3080d 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -5142,12 +5142,73 @@ private OverCall 
orderBy_(ImmutableList<RexFieldCollation> sortKeys) {
             }
           };
       final RelDataType type = op.inferReturnType(bind);
+      final ImmutableList<RexNode> newPartitionKeys =
+          simplifyPartitionKeys(partitionKeys);
+      final ImmutableList<RexFieldCollation> newSortKeys =
+          simplifySortKeys(newPartitionKeys, sortKeys);
       final RexNode over = getRexBuilder()
-          .makeOver(pos, type, op, operands, partitionKeys, sortKeys,
+          .makeOver(pos, type, op, operands, newPartitionKeys, newSortKeys,
               lowerBound, upperBound, exclude, rows, allowPartial, 
nullWhenCountZero,
               distinct, ignoreNulls);
       return aliasMaybe(over, alias);
     }
+
+    /** Removes constant keys from a window's {@code PARTITION BY}. A constant
+     * partition key places every row in the same partition, so it does not
+     * partition the data and can be dropped. */
+    private ImmutableList<RexNode> simplifyPartitionKeys(
+        List<RexNode> partitionKeys) {
+      final ImmutableList.Builder<RexNode> newKeys = ImmutableList.builder();
+      for (RexNode key : partitionKeys) {
+        if (!RexUtil.isConstant(key)) {
+          newKeys.add(key);
+        }
+      }
+      return newKeys.build();
+    }
+
+    /** Removes redundant keys from a window's {@code ORDER BY}. A sort key is
+     * redundant if it is constant, or if it is functionally determined by the
+     * partition keys and earlier sort keys (those columns are fixed within a
+     * partition, so the key cannot affect the ordering). For example, with
+     * {@code PARTITION BY x, y ORDER BY x + y, z} the key {@code x + y} only
+     * references fixed columns and is dropped, leaving {@code ORDER BY z}. */
+    private ImmutableList<RexFieldCollation> simplifySortKeys(
+        List<RexNode> partitionKeys, List<RexFieldCollation> sortKeys) {
+      // A RANGE frame with a value offset (e.g. RANGE BETWEEN 5 PRECEDING)
+      // derives its bounds from the sort key values, so its keys must be kept.
+      if (!rows
+          && (lowerBound.getOffset() != null || upperBound.getOffset() != 
null)) {
+        return ImmutableList.copyOf(sortKeys);
+      }
+      // Columns whose value is fixed within a partition: partition keys plus
+      // columns pinned by an earlier single-column sort keys.
+      ImmutableBitSet fixedColumns = ImmutableBitSet.of();
+      for (RexNode key : partitionKeys) {
+        if (key instanceof RexInputRef) {
+          fixedColumns = fixedColumns.set(((RexInputRef) key).getIndex());
+        }
+      }
+      final ImmutableList.Builder<RexFieldCollation> newSortKeys =
+          ImmutableList.builder();
+      for (RexFieldCollation collation : sortKeys) {
+        final RexNode key = collation.left;
+        if (RexUtil.isConstant(key)) {
+          continue;
+        }
+        final ImmutableBitSet keyColumns = RelOptUtil.InputFinder.bits(key);
+        if (!keyColumns.isEmpty()
+            && RexUtil.isDeterministic(key)
+            && fixedColumns.contains(keyColumns)) {
+          continue;
+        }
+        newSortKeys.add(collation);
+        if (key instanceof RexInputRef) {
+          fixedColumns = fixedColumns.set(((RexInputRef) key).getIndex());
+        }
+      }
+      return newSortKeys.build();
+    }
   }
 
   /** Collects the extra expressions needed for {@link #aggregate}.
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 2a500bcce7..779b320574 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -2858,8 +2858,9 @@ private SqlDialect nonOrdinalDialect() {
   @Test void testNoNeedRewriteOrderByConstantsForOver() {
     final String query = "select row_number() over "
         + "(order by 1 nulls last) from \"employee\"";
-    // Default dialect keep numeric constant keys in the over of order-by.
-    sql(query).ok("SELECT ROW_NUMBER() OVER (ORDER BY 1)\n"
+    // A constant ORDER BY key places every row in the same peer group, so it
+    // is removed when the window is built, leaving an empty OVER clause.
+    sql(query).ok("SELECT ROW_NUMBER() OVER ()\n"
         + "FROM \"foodmart\".\"employee\"");
   }
 
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java 
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index cce1aea309..ebe8990c1d 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -1186,6 +1186,108 @@ private RexNode caseCall(RelBuilder b, RexNode ref, 
RexNode... nodes) {
     assertThat(f.apply(createBuilder()), hasTree(expected));
   }
 
+  /** Tests that RelBuilder removes a constant key from a window's
+   * {@code PARTITION BY}, since a constant partition key places every row in
+   * the same partition. */
+  @Test void testProjectOverConstantPartitionKey() {
+    final Function<RelBuilder, RelNode> f = b -> b.scan("EMP")
+        .project(b.field("DEPTNO"),
+            b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
+                .over()
+                .partitionBy(b.literal(1))
+                .orderBy(b.field("EMPNO"))
+                .rowsUnbounded()
+                .as("x"))
+        .build();
+    final String expected = ""
+        + "LogicalProject(DEPTNO=[$7], x=[ROW_NUMBER() OVER (ORDER BY $0)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  /** Tests that RelBuilder keeps non-constant partition keys and drops only 
the
+   * constant one. */
+  @Test void testProjectOverPartialConstantPartitionKey() {
+    final Function<RelBuilder, RelNode> f = b -> b.scan("EMP")
+        .project(b.field("DEPTNO"),
+            b.aggregateCall(SqlStdOperatorTable.SUM, b.field("SAL"))
+                .over()
+                .partitionBy(b.field("DEPTNO"), b.literal(1))
+                .orderBy(b.field("EMPNO"))
+                .rowsUnbounded()
+                .as("x"))
+        .build();
+    final String expected = ""
+        + "LogicalProject(DEPTNO=[$7], "
+        + "x=[SUM($5) OVER (PARTITION BY $7 ORDER BY $0 RANGE BETWEEN "
+        + "UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  /** Tests that RelBuilder removes a constant key from a window's
+   * {@code ORDER BY}. */
+  @Test void testProjectOverConstantSortKey() {
+    final Function<RelBuilder, RelNode> f = b -> b.scan("EMP")
+        .project(b.field("DEPTNO"),
+            b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
+                .over()
+                .partitionBy()
+                .orderBy(b.literal(1), b.field("EMPNO"))
+                .rowsUnbounded()
+                .as("x"))
+        .build();
+    final String expected = ""
+        + "LogicalProject(DEPTNO=[$7], x=[ROW_NUMBER() OVER (ORDER BY $0)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  /** Tests that RelBuilder removes a sort key that is functionally determined
+   * by the partition keys: with {@code PARTITION BY DEPTNO, SAL ORDER BY
+   * DEPTNO + SAL, EMPNO} the key {@code DEPTNO + SAL} references only fixed
+   * columns and is dropped, leaving {@code ORDER BY EMPNO}. */
+  @Test void testProjectOverFunctionallyDependentSortKey() {
+    final Function<RelBuilder, RelNode> f = b -> b.scan("EMP")
+        .project(b.field("DEPTNO"),
+            b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
+                .over()
+                .partitionBy(b.field("DEPTNO"), b.field("SAL"))
+                .orderBy(
+                    b.call(SqlStdOperatorTable.PLUS, b.field("DEPTNO"),
+                        b.field("SAL")),
+                    b.field("EMPNO"))
+                .rowsUnbounded()
+                .as("x"))
+        .build();
+    final String expected = ""
+        + "LogicalProject(DEPTNO=[$7], "
+        + "x=[ROW_NUMBER() OVER (PARTITION BY $7, $5 ORDER BY $0)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  /** Tests that RelBuilder keeps a sort key that would otherwise be dropped
+   * (here {@code DEPTNO}, which equals the partition key) when the frame is a
+   * RANGE with a value offset, because such a frame derives its bounds from 
the
+   * sort key values. */
+  @Test void testProjectOverRangeOffsetKeepsSortKey() {
+    final Function<RelBuilder, RelNode> f = b -> b.scan("EMP")
+        .project(b.field("DEPTNO"),
+            b.aggregateCall(SqlStdOperatorTable.SUM, b.field("SAL"))
+                .over()
+                .partitionBy(b.field("DEPTNO"))
+                .orderBy(b.field("DEPTNO"))
+                .rangeBetween(b.preceding(b.literal(5)), b.currentRow())
+                .as("x"))
+        .build();
+    final String expected = ""
+        + "LogicalProject(DEPTNO=[$7], "
+        + "x=[SUM($5) OVER (PARTITION BY $7 ORDER BY $7 RANGE 5 PRECEDING)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
   @Test void testRename() {
     final RelBuilder builder = RelBuilder.create(config().build());
 
diff --git 
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml 
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 49681f45a3..96ca66bbc7 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -17412,7 +17412,7 @@ from (
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(COL1=[$2], COL2=[$3], COL3=[$4])
-  LogicalWindow(window#0=[window(partition {1} range between UNBOUNDED 
PRECEDING and CURRENT ROW aggs [SUM($2)])], window#1=[window(order by [1] aggs 
[SUM($2)])], window#2=[window(partition {1} range between UNBOUNDED PRECEDING 
and CURRENT ROW aggs [SUM(5000)])], constants=[[100]])
+  LogicalWindow(window#0=[window(partition {1} aggs [SUM($2)])], 
window#1=[window(order by [1] aggs [SUM($2)])], window#2=[window(partition {1} 
range between UNBOUNDED PRECEDING and CURRENT ROW aggs [SUM(5000)])], 
constants=[[100]])
     LogicalProject(SAL=[$5], DEPTNO=[$7])
       LogicalFilter(condition=[=($5, 5000)])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -17420,7 +17420,7 @@ LogicalProject(COL1=[$2], COL2=[$3], COL3=[$4])
     </Resource>
     <Resource name="planBefore">
       <![CDATA[
-LogicalProject(COL1=[SUM(100) OVER (PARTITION BY $7, $5 ORDER BY $5)], 
COL2=[SUM(100) OVER (PARTITION BY $5 ORDER BY $7)], COL3=[SUM($5) OVER 
(PARTITION BY $7 ORDER BY $5)])
+LogicalProject(COL1=[SUM(100) OVER (PARTITION BY $7, $5)], COL2=[SUM(100) OVER 
(PARTITION BY $5 ORDER BY $7)], COL3=[SUM($5) OVER (PARTITION BY $7 ORDER BY 
$5)])
   LogicalFilter(condition=[=($5, 5000)])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -17437,7 +17437,7 @@ from (
     </Resource>
     <Resource name="planBefore">
       <![CDATA[
-LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 RANGE BETWEEN CURRENT ROW 
AND UNBOUNDED FOLLOWING)], COL2=[SUM(100) OVER (PARTITION BY $5, $7 ORDER BY 
$7, $0 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])
+LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 RANGE BETWEEN CURRENT ROW 
AND UNBOUNDED FOLLOWING)], COL2=[SUM(100) OVER (PARTITION BY $5, $7 ORDER BY $0 
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])
   LogicalFilter(condition=[=($5, 5000)])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -17445,7 +17445,7 @@ LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 
RANGE BETWEEN CURRENT ROW AN
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(COL1=[$3], COL2=[$4])
-  LogicalWindow(window#0=[window(order by [2, 0] range between CURRENT ROW and 
UNBOUNDED FOLLOWING aggs [SUM($3)])], window#1=[window(partition {2} order by 
[2, 0] range between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING aggs 
[SUM($3)])], constants=[[100]])
+  LogicalWindow(window#0=[window(order by [2, 0] range between CURRENT ROW and 
UNBOUNDED FOLLOWING aggs [SUM($3)])], window#1=[window(partition {2} order by 
[0] range between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING aggs [SUM($3)])], 
constants=[[100]])
     LogicalProject(EMPNO=[$0], SAL=[$5], DEPTNO=[$7])
       LogicalFilter(condition=[=($5, 5000)])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
@@ -23476,13 +23476,13 @@ window w as (partition by empno order by empno)]]>
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(EXPR$0=[$9], EXPR$1=[$9])
-  LogicalWindow(window#0=[window(partition {0} order by [0] aggs [COUNT()])])
+  LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
     <Resource name="planBefore">
       <![CDATA[
-LogicalProject(EXPR$0=[COUNT() OVER (PARTITION BY $0 ORDER BY $0)], 
EXPR$1=[COUNT() OVER (PARTITION BY $0 ORDER BY $0)])
+LogicalProject(EXPR$0=[COUNT() OVER (PARTITION BY $0)], EXPR$1=[COUNT() OVER 
(PARTITION BY $0)])
   LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index d6364652cc..5d1bbb7386 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -8640,7 +8640,7 @@ EnumerableCalc(expr#0..1=[{inputs}], T1B=[$t1])
     EnumerableValues(tuples=[[{ 'val1a', 6 }, { 'val1b', 8 }, { 'val1a', 16 }, 
{ 'val1a', 16 }, { 'val1c', 8 }, { 'val1d', null }, { 'val1d', null }, { 
'val1e', 10 }, { 'val1e', 10 }, { 'val1d', 10 }, { 'val1a', 6 }, { 'val1e', 10 
}]])
     EnumerableSort(sort0=[$0], dir0=[ASC])
       EnumerableAggregate(group=[{0}], EXPR$0=[MAX($4)])
-        EnumerableWindow(window#0=[window(partition {0, 1, 3} order by [3] 
aggs [RANK()])])
+        EnumerableWindow(window#0=[window(partition {0, 1, 3} aggs [RANK()])])
           EnumerableMergeJoin(condition=[=($2, $3)], joinType=[inner])
             EnumerableSort(sort0=[$2], dir0=[ASC])
               EnumerableValues(tuples=[[{ 'val2a', 6, 12 }, { 'val1b', 10, 12 
}, { 'val1b', 8, 16 }, { 'val1c', 12, 16 }, { 'val1b', null, 16 }, { 'val2e', 
8, null }, { 'val1f', 19, null }, { 'val1b', 10, 12 }, { 'val1b', 8, 16 }, { 
'val1c', 12, 16 }, { 'val1e', 8, null }, { 'val1f', 19, null }, { 'val1b', 
null, 16 }]])
@@ -8681,7 +8681,7 @@ EnumerableSort(sort0=[$0], dir0=[ASC])
     EnumerableNestedLoopJoin(condition=[>(CAST($0):BIGINT, $1)], 
joinType=[inner])
       EnumerableValues(tuples=[[{ 6 }, { 8 }, { 16 }, { 16 }, { 8 }, { null }, 
{ null }, { 10 }, { 10 }, { 10 }, { 6 }, { 10 }]])
       EnumerableAggregate(group=[{}], EXPR$0=[MAX($3)])
-        EnumerableWindow(window#0=[window(partition {1, 2} order by [1] aggs 
[RANK()])])
+        EnumerableWindow(window#0=[window(partition {1, 2} aggs [RANK()])])
           EnumerableAggregate(group=[{0, 1}], T3D=[MAX($2)])
             EnumerableValues(tuples=[[{ 6, 12, 110 }, { 6, 12, 10 }, { 10, 12, 
219 }, { 10, 12, 19 }, { 8, 16, 319 }, { 8, 16, 19 }, { 17, 16, 519 }, { 17, 
16, 19 }, { null, 16, 419 }, { null, 16, 19 }, { 8, null, 719 }, { 8, null, 19 
}]])
 !plan

Reply via email to