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

Reply via email to