This is an automated email from the ASF dual-hosted git repository.

iwanttobepowerful 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 17bf870d32 [CALCITE-7540] Correlated outer reference in HAVING of 
grouped subquery is incorrectly reported as not grouped
17bf870d32 is described below

commit 17bf870d32d1362055e1df0050eec4e7b0f7e03c
Author: Weihua Zhang <[email protected]>
AuthorDate: Wed Jun 3 11:08:38 2026 +0800

    [CALCITE-7540] Correlated outer reference in HAVING of grouped subquery is 
incorrectly reported as not grouped
---
 .../apache/calcite/sql/validate/AggChecker.java    | 18 +++++
 .../apache/calcite/sql2rel/RelDecorrelator.java    | 15 +++-
 .../calcite/sql2rel/RelDecorrelatorTest.java       | 79 ++++++++++++++++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 10 +++
 core/src/test/resources/sql/sub-query.iq           | 26 +++++++
 5 files changed, 145 insertions(+), 3 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java 
b/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
index 21253ef31f..23928b83ac 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/AggChecker.java
@@ -139,6 +139,12 @@ && isMeasureExp(id)) {
     if (isGroupExpr(fqId.identifier)) {
       return null;
     }
+
+    // Outer references are not governed by the GROUP BY list of this
+    // aggregate query; they are validated in their own enclosing scope.
+    if (isOuterReference(firstScope, fqId)) {
+      return null;
+    }
     SqlNode originalExpr = validator.getOriginal(id);
     final String exprString = originalExpr.toString();
     throw validator.newValidationError(originalExpr,
@@ -147,6 +153,18 @@ && isMeasureExp(id)) {
             : RESOURCE.notGroupExpr(exprString));
   }
 
+  /** Returns whether an identifier refers to a scope outside this aggregate. 
*/
+  private boolean isOuterReference(SqlValidatorScope scope, SqlQualified fqId) 
{
+    if (!(scope instanceof AggregatingSelectScope) || fqId.prefixLength <= 0) {
+      return false;
+    }
+
+    final SqlValidatorScope currentSelectScope = ((AggregatingSelectScope) 
scope).parent;
+    final SqlValidatorScope.ResolvedImpl resolved = new 
SqlValidatorScope.ResolvedImpl();
+    scope.resolve(fqId.prefix(), validator.catalogReader.nameMatcher(), false, 
resolved);
+    return resolved.count() == 1 && 
!resolved.only().scope.isWithin(currentSelectScope);
+  }
+
   @Override public Void visit(SqlCall call) {
     final SqlValidatorScope scope =
         requireNonNull(scopes.peek(), () -> "scope for " + call);
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java 
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index a450a659dc..14d72f8dfe 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -1634,11 +1634,20 @@ private Frame 
decorrelateInputWithValueGenerator(RelNode rel, Frame inputFrame)
           findCorrelationEquivalent(correlation, ((Filter) 
rel).getCondition());
         } catch (Util.FoundOne e) {
           Object node = requireNonNull(e.getNode(), "e.getNode()");
-          if (node instanceof RexInputRef) {
-            map.put(def, ((RexInputRef) node).getIndex());
+          // findCorrelationEquivalent returns an expression from the original
+          // Filter condition, so its input refs are still in the 
pre-decorrelation
+          // coordinate system. Correlation outputs are recorded against the
+          // decorrelated input; translate the expression first, otherwise a
+          // correlated field may be bound to a column with the same ordinal 
but
+          // different meaning in the new RelNode.
+          final RexNode newNode = node instanceof RexInputRef
+              ? getNewForOldInputRef(rel, this.map, (RexInputRef) node)
+              : decorrelateExpr(rel, this.map, cm, (RexNode) node);
+          if (newNode instanceof RexInputRef) {
+            map.put(def, ((RexInputRef) newNode).getIndex());
           } else {
             map.put(def, inputFrame.r.getRowType().getFieldCount() + 
projects.size());
-            projects.add((RexNode) node);
+            projects.add(newNode);
           }
         }
       }
diff --git 
a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java 
b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
index 15f96eaa9e..245b248157 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -1831,6 +1831,85 @@ public static Frameworks.ConfigBuilder config() {
     assertThat(after, hasTree(planAfter));
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7540";>[CALCITE-7540]
+   * Correlated outer reference in HAVING of grouped subquery is incorrectly 
reported as not
+   * grouped</a>. */
+  @Test void test7540() {
+    final FrameworkConfig frameworkConfig = config().build();
+    final RelBuilder builder = RelBuilder.create(frameworkConfig);
+    final RelOptCluster cluster = builder.getCluster();
+    final Planner planner = Frameworks.getPlanner(frameworkConfig);
+    final String sql = ""
+        + "SELECT *\n"
+        + "FROM dept d\n"
+        + "INNER JOIN emp e\n"
+        + "  ON e.sal < (\n"
+        + "    SELECT MAX(e2.sal)\n"
+        + "    FROM emp e2\n"
+        + "    WHERE e2.job = e.job\n"
+        + "    GROUP BY e2.job\n"
+        + "    HAVING MIN(e2.deptno) = d.deptno\n"
+        + "  )";
+    final RelNode originalRel;
+    try {
+      final SqlNode parse = planner.parse(sql);
+      final SqlNode validate = planner.validate(parse);
+      originalRel = planner.rel(validate).rel;
+    } catch (Exception e) {
+      throw TestUtil.rethrow(e);
+    }
+
+    final HepProgram hepProgram = HepProgram.builder()
+        .addRuleCollection(
+            ImmutableList.of(
+                // SubQuery program rules
+                CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
+                CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
+                CoreRules.JOIN_SUB_QUERY_TO_CORRELATE))
+        .build();
+    final Program program =
+        Programs.of(hepProgram, true,
+            requireNonNull(cluster.getMetadataProvider()));
+    final RelNode before =
+        program.run(cluster.getPlanner(), originalRel, cluster.traitSet(),
+            Collections.emptyList(), Collections.emptyList());
+    final String planBefore = ""
+        + "LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EMPNO=[$3], 
ENAME=[$4], JOB=[$5], MGR=[$6], HIREDATE=[$7], SAL=[$8], COMM=[$9], 
DEPTNO0=[$10])\n"
+        + "  LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EMPNO=[$3], 
ENAME=[$4], JOB=[$5], MGR=[$6], HIREDATE=[$7], SAL=[$8], COMM=[$9], 
DEPTNO0=[$10])\n"
+        + "    LogicalFilter(condition=[<($8, $11)])\n"
+        + "      LogicalCorrelate(correlation=[$cor1], joinType=[left], 
requiredColumns=[{0, 5}])\n"
+        + "        LogicalJoin(condition=[true], joinType=[inner])\n"
+        + "          LogicalTableScan(table=[[scott, DEPT]])\n"
+        + "          LogicalTableScan(table=[[scott, EMP]])\n"
+        + "        LogicalAggregate(group=[{}], agg#0=[SINGLE_VALUE($0)])\n"
+        + "          LogicalProject(EXPR$0=[$1])\n"
+        + "            LogicalFilter(condition=[=($2, $cor1.DEPTNO)])\n"
+        + "              LogicalAggregate(group=[{0}], EXPR$0=[MAX($1)], 
agg#1=[MIN($2)])\n"
+        + "                LogicalProject(JOB=[$2], SAL=[$5], DEPTNO=[$7])\n"
+        + "                  LogicalFilter(condition=[=($2, $cor1.JOB)])\n"
+        + "                    LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    // Decorrelate without any rules, just "purely" decorrelation algorithm on 
RelDecorrelator
+    final RelNode after =
+        RelDecorrelator.decorrelateQuery(before, builder, 
RuleSets.ofList(Collections.emptyList()),
+            RuleSets.ofList(Collections.emptyList()));
+    final String planAfter = ""
+        + "LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EMPNO=[$3], 
ENAME=[$4], JOB=[$5], MGR=[$6], HIREDATE=[$7], SAL=[$8], COMM=[$9], 
DEPTNO0=[$10])\n"
+        + "  LogicalJoin(condition=[AND(=($0, $11), =($5, $12), <($8, $13))], 
joinType=[inner])\n"
+        + "    LogicalJoin(condition=[true], joinType=[inner])\n"
+        + "      LogicalTableScan(table=[[scott, DEPT]])\n"
+        + "      LogicalTableScan(table=[[scott, EMP]])\n"
+        + "    LogicalAggregate(group=[{0, 1}], agg#0=[SINGLE_VALUE($2)])\n"
+        + "      LogicalProject($f3=[$3], JOB3=[$1], EXPR$0=[$2])\n"
+        + "        LogicalFilter(condition=[IS NOT NULL($3)])\n"
+        + "          LogicalAggregate(group=[{0, 1}], EXPR$0=[MAX($2)], 
agg#1=[MIN($3)])\n"
+        + "            LogicalProject(JOB=[$2], JOB3=[$2], SAL=[$5], 
DEPTNO=[$7])\n"
+        + "              LogicalFilter(condition=[IS NOT NULL($2)])\n"
+        + "                LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
+
   /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7442";>[CALCITE-7442]
   * Getting Wrong index of Correlated variable inside Subquery after 
FilterJoinRule</a>. */
   @Test void testCorrelatedVariableIndexForInClause() {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 9a080efbf8..1cba948097 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6569,6 +6569,16 @@ void testReturnsCorrectRowTypeOnCombinedJoin() {
     sql("select sum(sal + sal) from emp having sum(sal) > 10").ok();
     sql("SELECT deptno FROM emp GROUP BY deptno HAVING ^sal^ > 10")
         .fails("Expression 'SAL' is not being grouped");
+    sql("SELECT *\n"
+        + "FROM dept d\n"
+        + "INNER JOIN emp e\n"
+        + "  ON e.sal < (\n"
+        + "    SELECT MAX(e2.sal)\n"
+        + "    FROM emp e2\n"
+        + "    WHERE e2.job = e.job\n"
+        + "    GROUP BY e2.job\n"
+        + "    HAVING MIN(e2.deptno) = d.deptno\n"
+        + "  )").ok();
   }
 
   @Test void testHavingBetween() {
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 2860493795..7c7e9fb053 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -9379,4 +9379,30 @@ FROM (SELECT deptno, SUM(sal) AS s FROM emp GROUP BY 
deptno) t;
 
 !ok
 
+# [CALCITE-7540] Correlated outer reference in HAVING of grouped subquery is 
incorrectly reported as not grouped
+SELECT *
+FROM dept d
+INNER JOIN emp e
+  ON e.sal < (
+    SELECT MAX(e2.sal)
+    FROM emp e2
+    WHERE e2.job = e.job
+    GROUP BY e2.job
+    HAVING MIN(e2.deptno) = d.deptno
+  );
++--------+------------+----------+-------+--------+----------+------+------------+---------+---------+---------+
+| DEPTNO | DNAME      | LOC      | EMPNO | ENAME  | JOB      | MGR  | HIREDATE 
  | SAL     | COMM    | DEPTNO0 |
++--------+------------+----------+-------+--------+----------+------+------------+---------+---------+---------+
+|     10 | ACCOUNTING | NEW YORK |  7369 | SMITH  | CLERK    | 7902 | 
1980-12-17 |  800.00 |         |      20 |
+|     10 | ACCOUNTING | NEW YORK |  7698 | BLAKE  | MANAGER  | 7839 | 
1981-01-05 | 2850.00 |         |      30 |
+|     10 | ACCOUNTING | NEW YORK |  7782 | CLARK  | MANAGER  | 7839 | 
1981-06-09 | 2450.00 |         |      10 |
+|     10 | ACCOUNTING | NEW YORK |  7876 | ADAMS  | CLERK    | 7788 | 
1987-05-23 | 1100.00 |         |      20 |
+|     10 | ACCOUNTING | NEW YORK |  7900 | JAMES  | CLERK    | 7698 | 
1981-12-03 |  950.00 |         |      30 |
+|     30 | SALES      | CHICAGO  |  7521 | WARD   | SALESMAN | 7698 | 
1981-02-22 | 1250.00 |  500.00 |      30 |
+|     30 | SALES      | CHICAGO  |  7654 | MARTIN | SALESMAN | 7698 | 
1981-09-28 | 1250.00 | 1400.00 |      30 |
+|     30 | SALES      | CHICAGO  |  7844 | TURNER | SALESMAN | 7698 | 
1981-09-08 | 1500.00 |    0.00 |      30 |
++--------+------------+----------+-------+--------+----------+------+------------+---------+---------+---------+
+(8 rows)
+
+!ok
 # End sub-query.iq

Reply via email to