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]])
]]>