Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion core/src/main/java/org/apache/calcite/rel/core/Window.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -161,8 +162,10 @@

@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);
Expand Down Expand Up @@ -340,6 +343,89 @@
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) {

Check failure on line 350 in core/src/main/java/org/apache/calcite/rel/core/Window.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ62xlsaCo1wZDOMr7to&open=AZ62xlsaCo1wZDOMr7to&pullRequest=5015
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that bounds may not be literals, but constant expressions. Please add a test case for that to make sure that this toString() does the right thing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for the reminder, I added tests to cover this.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,38 @@
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;
import org.apache.calcite.rel.type.RelDataTypeFactory;
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;
Expand Down Expand Up @@ -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"));
Comment thread
xiedeyantu marked this conversation as resolved.
}
}
2 changes: 1 addition & 1 deletion core/src/test/java/org/apache/calcite/test/JdbcTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]])
]]>
Expand Down Expand Up @@ -7477,7 +7477,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]])
]]>
Expand Down Expand Up @@ -10915,17 +10915,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]])
]]>
Expand Down Expand Up @@ -11979,7 +11979,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]])
]]>
Expand Down
Loading