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 2adf54c4f379..6bd47c29b351 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 @@ -34,6 +34,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; @@ -161,8 +162,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 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); @@ -340,6 +343,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 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. + * + *

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 + * + *

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 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 79182be959fe..726fd1cbefdd 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 + * [CALCITE-5929] + * Improve LogicalWindow print plan to add the constant value. */ + @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 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 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, at runtime constants can include + // expressions like RexCall(+, 5, 5). We use an unchecked cast to simulate this. + @SuppressWarnings("unchecked") + final List constants = + (List) (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 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 e4dca3fc448d..e9f071ff41da 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -4420,7 +4420,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 c782bb913cc1..59798650a8eb 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 @@ -7477,7 +7477,7 @@ LogicalProject(EMPNO=[$0], DEPTNO=[$1], W_COUNT=[$2]) @@ -10915,17 +10915,17 @@ FROM t1]]> @@ -11979,7 +11979,7 @@ LogicalProject(EXPR$0=[CAST(/(CASE(>(COUNT($5) OVER (ORDER BY $0 ROWS 3 PRECEDIN ($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]]) ]]>