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 d54d50cebb [CALCITE-7341] RelFieldTrimmer should trim Scalar sub-query
d54d50cebb is described below
commit d54d50cebbc3e0f554647483a0312ba13c32e9e4
Author: Yu Xu <[email protected]>
AuthorDate: Sun Oct 11 11:54:57 2026 +0800
[CALCITE-7341] RelFieldTrimmer should trim Scalar sub-query
---
.../apache/calcite/sql2rel/RelFieldTrimmer.java | 66 +++++++++++++++++--
.../calcite/sql2rel/RelFieldTrimmerTest.java | 77 ++++++++++++++++++++--
core/src/test/resources/sql/new-decorr.iq | 17 ++---
core/src/test/resources/sql/sub-query.iq | 2 +-
4 files changed, 137 insertions(+), 25 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelFieldTrimmer.java
b/core/src/main/java/org/apache/calcite/sql2rel/RelFieldTrimmer.java
index b86f02898c..24328f1eb2 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelFieldTrimmer.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelFieldTrimmer.java
@@ -492,8 +492,12 @@ public TrimResult trimFields(
// Build new project expressions, and populate the mapping.
final List<RexNode> newProjects = new ArrayList<>();
final RexVisitor<RexNode> shuttle =
- new RexPermuteInputsShuttle(
- inputMapping, newInput);
+ new RexPermuteInputsShuttle(inputMapping, newInput) {
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ subQuery = (RexSubQuery) super.visitSubQuery(subQuery);
+ return trimSubQuery(subQuery);
+ }
+ };
final Mapping mapping =
Mappings.create(
MappingType.INVERSE_SURJECTION,
@@ -577,6 +581,7 @@ public TrimResult trimFields(
shuttle = new RexPermuteInputsShuttle(inputMapping, newInput) {
@Override public RexNode visitSubQuery(RexSubQuery subQuery) {
subQuery = (RexSubQuery) super.visitSubQuery(subQuery);
+ subQuery = trimSubQuery(subQuery);
return
RelOptUtil.remapCorrelatesInSuqQuery(relBuilder.getRexBuilder(),
subQuery, project.getVariablesSet().iterator().next(),
@@ -584,7 +589,12 @@ public TrimResult trimFields(
}
};
} else {
- shuttle = new RexPermuteInputsShuttle(inputMapping, newInput);
+ shuttle = new RexPermuteInputsShuttle(inputMapping, newInput) {
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ subQuery = (RexSubQuery) super.visitSubQuery(subQuery);
+ return trimSubQuery(subQuery);
+ }
+ };
}
final Mapping mapping =
@@ -610,6 +620,42 @@ public TrimResult trimFields(
return result(newProject, mapping, project);
}
+ /** Trims the fields of the relational expression wrapped by a sub-query.
+ *
+ * <p>Currently, only scalar sub-queries are trimmed: a scalar sub-query
+ * returns exactly one field, and that field is used by its consumer, so
+ * we can trim the sub-query's internal tree asking for its single output
+ * field. Sub-queries of other kinds (IN, EXISTS, ARRAY, MULTISET, MAP) are
+ * returned unchanged; their output fields are either all significant for
+ * the semantics or not distinguishable, so it is not safe to trim them.
+ *
+ * @param subQuery Sub-query
+ * @return Sub-query whose internal relational expression is trimmed,
+ * or the original sub-query if it cannot be trimmed
+ */
+ protected RexSubQuery trimSubQuery(RexSubQuery subQuery) {
+ if (subQuery.getKind() != SqlKind.SCALAR_QUERY) {
+ return subQuery;
+ }
+ final RelNode rel = subQuery.rel;
+ final int fieldCount = rel.getRowType().getFieldCount();
+ if (fieldCount != 1) {
+ // Cannot happen for a well-formed scalar sub-query, which has exactly
+ // one field; be defensive, and leave it unchanged.
+ return subQuery;
+ }
+
+ // The consumer of a scalar sub-query uses its single output field.
+ final ImmutableBitSet fieldsUsed = ImmutableBitSet.of(0);
+ final Set<RelDataTypeField> extraFields = Collections.emptySet();
+ final TrimResult trimResult =
+ dispatchTrimFields(rel, fieldsUsed, extraFields);
+ if (trimResult.left == rel) {
+ return subQuery;
+ }
+ return subQuery.clone(trimResult.left);
+ }
+
/** Creates a project with a dummy column, to protect the parts of the system
* that cannot handle a relational expression with no columns.
*
@@ -687,7 +733,12 @@ public TrimResult trimFields(
// Build new project expressions, and populate the mapping.
final RexVisitor<RexNode> shuttle =
- new RexPermuteInputsShuttle(inputMapping, newInput);
+ new RexPermuteInputsShuttle(inputMapping, newInput) {
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ subQuery = (RexSubQuery) super.visitSubQuery(subQuery);
+ return trimSubQuery(subQuery);
+ }
+ };
RexNode newConditionExpr =
conditionExpr.accept(shuttle);
@@ -950,7 +1001,12 @@ public TrimResult trimFields(
// Build new join.
final RexVisitor<RexNode> shuttle =
new RexPermuteInputsShuttle(
- mapping, newInputs.get(0), newInputs.get(1));
+ mapping, newInputs.get(0), newInputs.get(1)) {
+ @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
+ subQuery = (RexSubQuery) super.visitSubQuery(subQuery);
+ return trimSubQuery(subQuery);
+ }
+ };
RexNode newConditionExpr =
conditionExpr.accept(shuttle);
RexNode newMatchConditionExpr =
diff --git
a/core/src/test/java/org/apache/calcite/sql2rel/RelFieldTrimmerTest.java
b/core/src/test/java/org/apache/calcite/sql2rel/RelFieldTrimmerTest.java
index 7e12cac8ed..b0182145c5 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelFieldTrimmerTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelFieldTrimmerTest.java
@@ -694,10 +694,59 @@ public static Frameworks.ConfigBuilder config() {
assertThat(trimmed, hasTree(expected));
}
+ /**
+ * Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7341">[CALCITE-7341]
+ * RelFieldTrimmer should trim Scalar sub-query</a>.
+ *
+ * <p>The relational expression inside the scalar sub-query must be trimmed
+ * too: the table scan inside the sub-query gets a project that identifies
+ * the columns required by the sub-query, instead of returning all columns.
+ */
+ @Test void testTrimScalarSubquery() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ final RelNode root = builder.scan("DEPT")
+ .project(builder.field("DEPTNO"),
+ builder.scalarQuery(
+ b2 -> builder.scan("EMP")
+ .filter(
+ builder.call(SqlStdOperatorTable.GREATER_THAN,
+ builder.field("SAL"), builder.literal(1000)))
+ .aggregate(builder.groupKey(), builder.countStar("c"))
+ .build()))
+ .build();
+
+ String origTree = ""
+ + "LogicalProject(DEPTNO=[$0], $f1=[$SCALAR_QUERY({\n"
+ + "LogicalAggregate(group=[{}], c=[COUNT()])\n"
+ + " LogicalFilter(condition=[>($5, 1000)])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + "})])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+ assertThat(root, hasTree(origTree));
+
+ final RelFieldTrimmer fieldTrimmer = new RelFieldTrimmer(null, builder);
+ final RelNode trimmed = fieldTrimmer.trim(root);
+ final String expected = ""
+ + "LogicalProject(DEPTNO=[$0], $f1=[$SCALAR_QUERY({\n"
+ + "LogicalAggregate(group=[{}], c=[COUNT()])\n"
+ + " LogicalFilter(condition=[>($1, 1000)])\n"
+ + " LogicalProject(EMPNO=[$0], SAL=[$5])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + "})])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+ assertThat(trimmed, hasTree(expected));
+ }
+
/**
* Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-3772">[CALCITE-3772]
* RelFieldTrimmer incorrectly trims fields when the query includes
correlated-subquery</a>.
+ *
+ * <p>Also tests
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7341">[CALCITE-7341]
+ * RelFieldTrimmer should trim Scalar sub-query</a>: the relational
expression
+ * inside the scalar sub-query is trimmed as well.
*/
@Test void testTrimCorrelatedSubquery() {
final RelBuilder builder = RelBuilder.create(config().build());
@@ -735,8 +784,9 @@ public static Frameworks.ConfigBuilder config() {
final String expected = ""
+ "LogicalProject(variablesSet=[[$cor0]], EMPNO=[$0],
$f1=[$SCALAR_QUERY({\n"
+ "LogicalAggregate(group=[{}], c=[COUNT()])\n"
- + " LogicalFilter(condition=[<($3, $cor0.MGR)])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[<($1, $cor0.MGR)])\n"
+ + " LogicalProject(EMPNO=[$0], MGR=[$3])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ "})])\n"
+ " LogicalFilter(condition=[>($2, 10)])\n"
+ " LogicalProject(EMPNO=[$0], MGR=[$3], SAL=[$5])\n"
@@ -750,6 +800,10 @@ public static Frameworks.ConfigBuilder config() {
* <a
href="https://issues.apache.org/jira/browse/CALCITE-7336">[CALCITE-7336]
* RelFieldTrimmer generates an incorrect plan
* when handling correlated sub-query within Filter or Join condition</a>.
+ *
+ * <p>Also tests
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7341">[CALCITE-7341]
+ * RelFieldTrimmer should trim Scalar sub-query</a>.
*/
@Test void testTrimCorrelatedSubqueryInFilterCondition() {
final RelBuilder builder = RelBuilder.create(config().build());
@@ -784,14 +838,24 @@ public static Frameworks.ConfigBuilder config() {
+ "LogicalProject(EMPNO=[$0])\n"
+ " LogicalFilter(condition=[>($2, $SCALAR_QUERY({\n"
+ "LogicalAggregate(group=[{}], c=[COUNT()])\n"
- + " LogicalFilter(condition=[<($3, $cor0.MGR)])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[<($1, $cor0.MGR)])\n"
+ + " LogicalProject(EMPNO=[$0], MGR=[$3])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ "}))], variablesSet=[[$cor0]])\n"
+ " LogicalProject(EMPNO=[$0], MGR=[$3], SAL=[$5])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
assertThat(trimmed, hasTree(expected));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7336">[CALCITE-7336]
+ * RelFieldTrimmer generates an incorrect plan
+ * when handling correlated sub-query within Filter or Join condition</a>.
+ *
+ * <p>Also tests
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7341">[CALCITE-7341]
+ * RelFieldTrimmer should trim Scalar sub-query</a>.
+ */
@Test void testTrimCorrelatedSubqueryInJoinCondition() {
final RelBuilder builder = RelBuilder.create(config().build());
final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
@@ -835,8 +899,9 @@ public static Frameworks.ConfigBuilder config() {
+ "LogicalProject(ENAME=[$1], DNAME=[$5])\n"
+ " LogicalJoin(condition=[AND(=($3, $4), >($1, $SCALAR_QUERY({\n"
+ "LogicalAggregate(group=[{}], c=[COUNT()])\n"
- + " LogicalFilter(condition=[<($3, $cor0.MGR)])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[<($1, $cor0.MGR)])\n"
+ + " LogicalProject(EMPNO=[$0], MGR=[$3])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ "})))], joinType=[inner], variablesSet=[[$cor0]])\n"
+ " LogicalProject(EMPNO=[$0], ENAME=[$1], MGR=[$3], DEPTNO=[$7])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n"
diff --git a/core/src/test/resources/sql/new-decorr.iq
b/core/src/test/resources/sql/new-decorr.iq
index a1caa699d6..9f92250128 100644
--- a/core/src/test/resources/sql/new-decorr.iq
+++ b/core/src/test/resources/sql/new-decorr.iq
@@ -185,23 +185,14 @@ select deptno, (select sum(cast(empno as bigint)) from
"scott".emp where deptno
!ok
!if (use_old_decorr) {
-EnumerableCorrelate(correlation=[$cor0], joinType=[left],
requiredColumns=[{0}])
- EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
- EnumerableTableScan(table=[[scott, DEPT]])
- EnumerableValues(tuples=[[]])
+EnumerableCalc(expr#0..2=[{inputs}], expr#3=[null:BIGINT], DEPTNO=[$t0],
EXPR$0=[$t3])
+ EnumerableTableScan(table=[[scott, DEPT]])
!plan
!}
!if (use_new_decorr) {
-EnumerableCalc(expr#0..3=[{inputs}], DEPTNO=[$t0], EXPR$0=[$t2])
- EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[left])
- EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
- EnumerableTableScan(table=[[scott, DEPT]])
- EnumerableCalc(expr#0..2=[{inputs}], expr#3=[0], expr#4=[<=($t2, $t3)],
proj#0..2=[{exprs}], $condition=[$t4])
- EnumerableWindow(window#0=[window(partition {0} rows between UNBOUNDED
PRECEDING and CURRENT ROW aggs [ROW_NUMBER()])])
- EnumerableAggregate(group=[{1}], EXPR$0=[$SUM0($0)])
- EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t0):BIGINT NOT
NULL], expr#9=[IS NOT NULL($t7)], EMPNO=[$t8], DEPTNO=[$t7], $condition=[$t9])
- EnumerableTableScan(table=[[scott, EMP]])
+EnumerableCalc(expr#0..2=[{inputs}], expr#3=[null:BIGINT], DEPTNO=[$t0],
EXPR$0=[$t3])
+ EnumerableTableScan(table=[[scott, DEPT]])
!plan
!}
diff --git a/core/src/test/resources/sql/sub-query.iq
b/core/src/test/resources/sql/sub-query.iq
index 717697d29f..094ed12848 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -1195,7 +1195,7 @@ EnumerableCalc(expr#0..3=[{inputs}], ENAME=[$t1],
DEEP2SAL=[$t3])
EnumerableTableScan(table=[[scott, EMP]])
EnumerableSort(sort0=[$0], dir0=[ASC])
EnumerableAggregate(group=[{0}], EXPR$0=[SUM($1)])
- EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NOT NULL($t4)],
expr#6=[CAST($t4):DECIMAL(19, 2) NOT NULL], expr#7=[0.00:DECIMAL(19, 2)],
expr#8=[CASE($t5, $t6, $t7)], expr#9=[+($t2, $t8)], MGR9=[$t1], $f0=[$t9])
+ EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NOT NULL($t4)],
expr#6=[CAST($t4):DECIMAL(19, 2) NOT NULL], expr#7=[0.00:DECIMAL(19, 2)],
expr#8=[CASE($t5, $t6, $t7)], expr#9=[+($t2, $t8)], MGR4=[$t1], $f0=[$t9])
EnumerableMergeJoin(condition=[=($0, $3)], joinType=[left])
EnumerableCalc(expr#0..7=[{inputs}], expr#8=[IS NOT NULL($t3)],
EMPNO=[$t0], MGR=[$t3], SAL=[$t5], $condition=[$t8])
EnumerableTableScan(table=[[scott, EMP]])