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 f541bf3319 [CALCITE-5929] Improve LogicalWindow print plan to add the 
constant value
f541bf3319 is described below

commit f541bf3319e00cc7eb61a8b5e4b3e6b49a6177b0
Author: Yu Xu <[email protected]>
AuthorDate: Thu Jun 11 20:29:53 2026 +0800

    [CALCITE-5929] Improve LogicalWindow print plan to add the constant value
---
 .../java/org/apache/calcite/rel/core/Window.java   |  88 ++++++++++++++++-
 .../calcite/rel/logical/LogicalWindowTest.java     | 110 +++++++++++++++++++++
 .../java/org/apache/calcite/test/JdbcTest.java     |   2 +-
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  14 +--
 4 files changed, 205 insertions(+), 9 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rel/core/Window.java 
b/core/src/main/java/org/apache/calcite/rel/core/Window.java
index b444c8468f..705e349b84 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Window.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Window.java
@@ -35,6 +35,7 @@
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexChecker;
 import org.apache.calcite.rex.RexFieldCollation;
+import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexLocalRef;
 import org.apache.calcite.rex.RexNode;
@@ -162,8 +163,10 @@ public Window(RelOptCluster cluster, RelTraitSet traitSet, 
RelNode input,
 
   @Override public RelWriter explainTerms(RelWriter pw) {
     super.explainTerms(pw);
+    final int inputFieldCount = getInput().getRowType().getFieldCount();
     for (Ord<Group> window : Ord.zip(groups)) {
-      pw.item("window#" + window.i, window.e.toString());
+      pw.item("window#" + window.i,
+          window.e.computeDisplayString(constants, inputFieldCount));
     }
     if (this.constants != null && this.constants.size() > 0) {
       pw.item("constants", constants);
@@ -345,6 +348,89 @@ private String computeString(@UnderInitialization Group 
this) {
       return buf.toString();
     }
 
+    /** Returns a display string with constant offsets in window bounds 
expanded
+     * to their values. Unlike {@link #toString()}, this is for display
+     * only and does not affect {@link #equals} or {@link #hashCode}.
+     * Constants can be literals or expressions (e.g., 5+5). */
+    public String computeDisplayString(List<RexLiteral> constants, int 
inputFieldCount) {
+      final StringBuilder buf = new StringBuilder("window(");
+      final int i = buf.length();
+      if (!keys.isEmpty()) {
+        buf.append("partition ");
+        buf.append(keys);
+      }
+      if (!orderKeys.getFieldCollations().isEmpty()) {
+        if (buf.length() > i) {
+          buf.append(' ');
+        }
+        buf.append("order by ");
+        buf.append(orderKeys);
+      }
+      if (orderKeys.getFieldCollations().isEmpty()
+          && lowerBound.isUnboundedPreceding()
+          && upperBound.isUnboundedFollowing()) {
+        // skip
+      } else if (!orderKeys.getFieldCollations().isEmpty()
+          && lowerBound.isUnboundedPreceding()
+          && upperBound.isCurrentRow()
+          && !isRows) {
+        // skip
+      } else {
+        if (buf.length() > i) {
+          buf.append(' ');
+        }
+        buf.append(isRows ? "rows " : "range ");
+        buf.append("between ");
+        buf.append(expandBound(lowerBound, constants, inputFieldCount));
+        buf.append(" and ");
+        buf.append(expandBound(upperBound, constants, inputFieldCount));
+        if (exclude != RexWindowExclusion.EXCLUDE_NO_OTHER) {
+          buf.append(" ").append(exclude);
+        }
+      }
+      if (!aggCalls.isEmpty()) {
+        if (buf.length() > i) {
+          buf.append(' ');
+        }
+        buf.append("aggs ");
+        buf.append(aggCalls);
+      }
+      buf.append(")");
+      return buf.toString();
+    }
+
+    /** Expands a window bound by replacing RexInputRef constants with their 
values.
+     *
+     * <p>If the bound offset is a RexInputRef pointing to a constant:
+     * - For RexLiteral constants, extracts the actual value (e.g., 10)
+     * - For other expressions, uses toString() to show the expression digest
+     *
+     * <p>Examples:
+     * - RexInputRef(1) pointing to RexLiteral(10) → "10 PRECEDING"
+     * - RexInputRef(1) pointing to RexCall(+, 5, 5) → digest representation
+     */
+    private static String expandBound(RexWindowBound bound,
+        List<RexLiteral> constants, int inputFieldCount) {
+      if (bound.isUnbounded() || bound.isCurrentRow()) {
+        return bound.toString();
+      }
+      final RexNode offset = bound.getOffset();
+      if (offset instanceof RexInputRef) {
+        final int index = ((RexInputRef) offset).getIndex();
+        if (index >= inputFieldCount && index - inputFieldCount < 
constants.size()) {
+          final RexNode constant = constants.get(index - inputFieldCount);
+          // Constants can be literals or constant expressions (e.g., 5+5 = 
RexCall).
+          // For literals, use getValue2() to get the actual value.
+          // For expressions, use toString() which shows the expression digest.
+          final String value = (constant instanceof RexLiteral)
+              ? String.valueOf(((RexLiteral) constant).getValue2())
+              : constant.toString();
+          return value + " " + (bound.isPreceding() ? "PRECEDING" : 
"FOLLOWING");
+        }
+      }
+      return bound.toString();
+    }
+
     @Override public boolean equals(@Nullable Object obj) {
       return this == obj
           || obj instanceof Group
diff --git 
a/core/src/test/java/org/apache/calcite/rel/logical/LogicalWindowTest.java 
b/core/src/test/java/org/apache/calcite/rel/logical/LogicalWindowTest.java
index 79182be959..726fd1cbef 100644
--- a/core/src/test/java/org/apache/calcite/rel/logical/LogicalWindowTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/logical/LogicalWindowTest.java
@@ -20,6 +20,7 @@
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.AbstractRelNode;
+import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Window;
 import org.apache.calcite.rel.type.RelDataType;
@@ -27,20 +28,30 @@
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
 import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexWindowBound;
+import org.apache.calcite.rex.RexWindowBounds;
+import org.apache.calcite.rex.RexWindowExclusion;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.BasicSqlType;
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.test.MockRelOptPlanner;
+import org.apache.calcite.util.ImmutableBitSet;
 
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 import static org.apache.calcite.rel.core.Window.Group;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.hasSize;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertSame;
@@ -84,4 +95,103 @@ public class LogicalWindowTest {
     assertThat(updated.getConstants(), hasSize(1));
     assertSame(newConstants.get(0), updated.getConstants().get(0));
   }
+
+  /** Test case of
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5929";>[CALCITE-5929]
+   * Improve LogicalWindow print plan to add the constant value</a>. */
+  @Test void testComputeDisplayStringWithLiteralConstant() {
+    // Test that computeDisplayString() correctly expands literal constants
+    // in window bounds (e.g., "10 PRECEDING" instead of "$1 PRECEDING")
+    final MockRelOptPlanner planner = new MockRelOptPlanner(Contexts.empty());
+    final SqlTypeFactoryImpl typeFactory =
+        new 
SqlTypeFactoryImpl(org.apache.calcite.rel.type.RelDataTypeSystem.DEFAULT);
+    final RexBuilder rexBuilder = new RexBuilder(typeFactory);
+    final RelOptCluster cluster = RelOptCluster.create(planner, rexBuilder);
+    final RelTraitSet traitSet = RelTraitSet.createEmpty();
+    final RelNode relNode = new AbstractRelNode(cluster, traitSet) {
+    };
+
+    // Create a literal constant: 10
+    final RexLiteral literalTen =
+        rexBuilder.makeExactLiteral(java.math.BigDecimal.TEN,
+        typeFactory.createSqlType(SqlTypeName.BIGINT));
+    final List<RexLiteral> constants = Collections.singletonList(literalTen);
+
+    // Create window bounds: 10 PRECEDING to CURRENT ROW
+    // The offset is RexInputRef(1) which maps to constants[0] = 10
+    final int inputFieldCount = 1;
+    final RexInputRef offsetRef = new RexInputRef(inputFieldCount, 
literalTen.getType());
+    final RexWindowBound lowerBound = RexWindowBounds.preceding(offsetRef);
+
+    // Create a window group with this bound
+    final List<Window.RexWinAggCall> aggCalls = new ArrayList<>();
+    final Group group =
+        new Group(ImmutableBitSet.of(),
+        true, // isRows
+        lowerBound,
+        RexWindowBounds.CURRENT_ROW,
+        RexWindowExclusion.EXCLUDE_NO_OTHER,
+        RelCollations.EMPTY,
+        aggCalls);
+
+    // Call computeDisplayString and verify it expands "10 PRECEDING"
+    final String displayString = group.computeDisplayString(constants, 
inputFieldCount);
+    assertThat(displayString, containsString("10 PRECEDING"));
+    assertThat(displayString, containsString("CURRENT ROW"));
+  }
+
+  @Test void testComputeDisplayStringWithConstantExpression() {
+    // Test that computeDisplayString() correctly handles constant expressions
+    // (not just literals) in window bounds. For example, when a window bound
+    // contains RexCall representing an expression like 5+5.
+    final MockRelOptPlanner planner = new MockRelOptPlanner(Contexts.empty());
+    final SqlTypeFactoryImpl typeFactory =
+        new 
SqlTypeFactoryImpl(org.apache.calcite.rel.type.RelDataTypeSystem.DEFAULT);
+    final RexBuilder rexBuilder = new RexBuilder(typeFactory);
+    final RelOptCluster cluster = RelOptCluster.create(planner, rexBuilder);
+    final RelTraitSet traitSet = RelTraitSet.createEmpty();
+    final RelNode relNode = new AbstractRelNode(cluster, traitSet) {
+    };
+
+    // Create a constant expression: 5 + 5
+    final RexLiteral five =
+        rexBuilder.makeExactLiteral(java.math.BigDecimal.valueOf(5),
+        typeFactory.createSqlType(SqlTypeName.BIGINT));
+    final SqlOperator plusOp = SqlStdOperatorTable.PLUS;
+    final RexCall addExpr =
+        (RexCall) rexBuilder.makeCall(plusOp, five, five);
+
+    // Test that expandBound() correctly handles both literals and expressions.
+    // Although the API accepts List<RexLiteral>, at runtime constants can 
include
+    // expressions like RexCall(+, 5, 5). We use an unchecked cast to simulate 
this.
+    @SuppressWarnings("unchecked")
+    final List<RexLiteral> constants =
+        (List<RexLiteral>) (List<?>) Collections.singletonList(addExpr);
+
+    // Create window bounds with RexInputRef pointing to this expression
+    final int inputFieldCount = 1;
+    final RexInputRef offsetRef = new RexInputRef(inputFieldCount, 
addExpr.getType());
+    final RexWindowBound lowerBound = RexWindowBounds.preceding(offsetRef);
+
+    // Create a window group
+    final List<Window.RexWinAggCall> aggCalls = new ArrayList<>();
+    final Group group =
+        new Group(ImmutableBitSet.of(),
+        true, // isRows
+        lowerBound,
+        RexWindowBounds.CURRENT_ROW,
+        RexWindowExclusion.EXCLUDE_NO_OTHER,
+        RelCollations.EMPTY,
+        aggCalls);
+
+    // Call computeDisplayString and verify it correctly renders the 
expression.
+    // Since the constant is RexCall(+, 5, 5), expandBound() should call 
toString()
+    // on it (the non-literal branch), which returns the digest representation.
+    final String displayString = group.computeDisplayString(constants, 
inputFieldCount);
+
+    // Verify the expression 5+5 is shown as "+(5:BIGINT, 5:BIGINT) PRECEDING",
+    // not as unexpanded "$1 PRECEDING"
+    assertThat(displayString, containsString("+(5:BIGINT, 5:BIGINT) 
PRECEDING"));
+    assertThat(displayString, containsString("CURRENT ROW"));
+  }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index f90c5525ca..9732b10c8f 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -4411,7 +4411,7 @@ void testOrderByOnSortedTable2(String format) {
             "[deptno INTEGER NOT NULL, empid INTEGER NOT NULL, S REAL, FIVE 
INTEGER NOT NULL, M REAL, C BIGINT NOT NULL]")
         .explainContains(""
             + "EnumerableCalc(expr#0..7=[{inputs}], expr#8=[0:BIGINT], 
expr#9=[>($t4, $t8)], expr#10=[null:JavaType(class java.lang.Float)], 
expr#11=[CASE($t9, $t5, $t10)], expr#12=[5], deptno=[$t1], empid=[$t0], 
S=[$t11], FIVE=[$t12], M=[$t6], C=[$t7])\n"
-            + "  EnumerableWindow(window#0=[window(partition {1} order by [0] 
rows between $4 PRECEDING and CURRENT ROW aggs [COUNT($3), $SUM0($3), MIN($2), 
COUNT()])], constants=[[1]])\n"
+            + "  EnumerableWindow(window#0=[window(partition {1} order by [0] 
rows between 1 PRECEDING and CURRENT ROW aggs [COUNT($3), $SUM0($3), MIN($2), 
COUNT()])], constants=[[1]])\n"
             + "    EnumerableCalc(expr#0..4=[{inputs}], expr#5=[+($t3, $t0)], 
proj#0..1=[{exprs}], salary=[$t3], $3=[$t5])\n"
             + "      EnumerableTableScan(table=[[hr, emps]])\n")
         .returnsUnordered(
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 c2fb106555..29e539a0f7 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -5522,7 +5522,7 @@ ROWS BETWEEN 5 + 5 PRECEDING AND 1 PRECEDING) AS w_count 
from emp
     <Resource name="planBefore">
       <![CDATA[
 LogicalProject(W_COUNT=[$1])
-  LogicalWindow(window#0=[window(order by [0] rows between $1 PRECEDING and $2 
PRECEDING aggs [COUNT()])], constants=[[10, 1]])
+  LogicalWindow(window#0=[window(order by [0] rows between 10 PRECEDING and 1 
PRECEDING aggs [COUNT()])], constants=[[10, 1]])
     LogicalProject(EMPNO=[$0])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -7476,7 +7476,7 @@ LogicalProject(EMPNO=[$0], DEPTNO=[$1], W_COUNT=[$2])
       <![CDATA[
 LogicalProject(EMPNO=[$0], DEPTNO=[$1], W_COUNT=[$2])
   LogicalFilter(condition=[IS NULL($2)])
-    LogicalWindow(window#0=[window(rows between $2 PRECEDING and $3 PRECEDING 
aggs [COUNT($0)])], constants=[[10, 1]])
+    LogicalWindow(window#0=[window(rows between 10 PRECEDING and 1 PRECEDING 
aggs [COUNT($0)])], constants=[[10, 1]])
       LogicalProject(EMPNO=[$0], DEPTNO=[$7])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -10914,17 +10914,17 @@ FROM t1]]>
     <Resource name="planBefore">
       <![CDATA[
 LogicalProject(DEPTNO=[$1], F1=[$2], F2=[$3])
-  LogicalWindow(window#0=[window(order by [0] rows between $3 PRECEDING and $4 
FOLLOWING aggs [LAST_VALUE($1)])], constants=[[2, 1]])
+  LogicalWindow(window#0=[window(order by [0] rows between 2 PRECEDING and 1 
FOLLOWING aggs [LAST_VALUE($1)])], constants=[[2, 1]])
     LogicalProject(EMPNO=[$0], DEPTNO=[$7], F1=[$9])
-      LogicalWindow(window#0=[window(order by [0] rows between $9 PRECEDING 
and $10 FOLLOWING aggs [FIRST_VALUE($7)])], constants=[[2, 1]])
+      LogicalWindow(window#0=[window(order by [0] rows between 2 PRECEDING and 
1 FOLLOWING aggs [FIRST_VALUE($7)])], constants=[[2, 1]])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(DEPTNO=[$1], F1=[$2], F2=[$3])
-  LogicalWindow(window#0=[window(order by [0] rows between $3 PRECEDING and $4 
FOLLOWING aggs [LAST_VALUE($1)])], constants=[[2, 1]])
-    LogicalWindow(window#0=[window(order by [0] rows between $2 PRECEDING and 
$3 FOLLOWING aggs [FIRST_VALUE($1)])], constants=[[2, 1]])
+  LogicalWindow(window#0=[window(order by [0] rows between 2 PRECEDING and 1 
FOLLOWING aggs [LAST_VALUE($1)])], constants=[[2, 1]])
+    LogicalWindow(window#0=[window(order by [0] rows between 2 PRECEDING and 1 
FOLLOWING aggs [FIRST_VALUE($1)])], constants=[[2, 1]])
       LogicalProject(EMPNO=[$0], DEPTNO=[$7])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
@@ -11978,7 +11978,7 @@ LogicalProject(EXPR$0=[CAST(/(CASE(>(COUNT($5) OVER 
(ORDER BY $0 ROWS 3 PRECEDIN
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(EXPR$0=[CAST(/(CASE(>($2, 0), $3, null:INTEGER), $2)):INTEGER])
-  LogicalWindow(window#0=[window(order by [0] rows between $2 PRECEDING and 
CURRENT ROW aggs [COUNT($1), $SUM0($1)])], constants=[[3]])
+  LogicalWindow(window#0=[window(order by [0] rows between 3 PRECEDING and 
CURRENT ROW aggs [COUNT($1), $SUM0($1)])], constants=[[3]])
     LogicalProject(EMPNO=[$0], SAL=[$5])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>

Reply via email to