From 106b2168250dd0e2c96593cc9a9eaa0d83dc6c1e Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Fri, 26 Jun 2026 10:45:13 +0800 Subject: [PATCH 01/21] [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query --- .../sql/validate/SqlValidatorImpl.java | 251 ++++++++++++++++++ core/src/test/resources/sql/agg.iq | 25 ++ 2 files changed, 276 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 37f6712e90a7..a58ba382dc3e 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -161,6 +161,8 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; @@ -5599,6 +5601,11 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, // First pass, ensure that aliases are unique. "*" and "TABLE.*" items // are ignored. + // Rewrite scalar sub-queries whose single select item is an aggregate + // over outer columns. The aggregate belongs to the outer query per SQL + // standard. + rewriteOuterAggregatesInSelectList(select); + // Validate SELECT list. Expand terms of the form "*" or "TABLE.*". final SqlValidatorScope selectScope = getSelectScope(select); final List expandedSelectItems = new ArrayList<>(); @@ -5658,6 +5665,250 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, return typeFactory.createStructType(fieldList); } + /** + * Rewrites scalar sub-queries in the SELECT list whose single select item is + * an aggregate function whose arguments reference only outer columns. Per the + * SQL standard, such aggregates belong to the outer query. + * + *

For example, + *

SELECT (SELECT sum(a) FROM t LIMIT 1) FROM aa
+ * is rewritten to + *
SELECT sum(a) * (SELECT 1 FROM t LIMIT 1) FROM aa
+ */ + private void rewriteOuterAggregatesInSelectList(SqlSelect select) { + final SqlNodeList selectItems = select.getSelectList(); + if (selectItems == null) { + return; + } + final SqlValidatorScope selectScope = getSelectScope(select); + final boolean wasAggregate = isAggregate(select); + for (int i = 0; i < selectItems.size(); i++) { + final SqlNode selectItem = selectItems.get(i); + final SqlNode rewrittenItem = + rewriteOuterAggregateItem(select, selectScope, selectItem); + if (rewrittenItem != selectItem) { + selectItems.set(i, rewrittenItem); + } + } + // The rewrite may have introduced an aggregate into a query that was not + // previously aggregate. Update the SELECT clause scope accordingly so that + // subsequent validation and conversion see an AggregatingSelectScope. + if (!wasAggregate && isAggregate(select)) { + SqlValidatorScope scope = + clauseScopes.get(IdPair.of(select, Clause.SELECT)); + if (scope instanceof AggregatingSelectScope) { + scope = ((AggregatingSelectScope) scope).getParent(); + } + clauseScopes.put(IdPair.of(select, Clause.SELECT), + new AggregatingSelectScope((SelectScope) scope, select, false)); + } + } + + /** + * Rewrites a single SELECT list item if it is a scalar sub-query containing + * an aggregate over outer columns. + * + * @return the rewritten expression, or {@code selectItem} if no rewrite applies + */ + private SqlNode rewriteOuterAggregateItem(SqlSelect parentSelect, + SqlValidatorScope parentScope, SqlNode selectItem) { + // Unwrap AS. + SqlNode expr = selectItem; + @Nullable SqlIdentifier alias = null; + if (SqlUtil.isCallTo(selectItem, SqlStdOperatorTable.AS)) { + SqlCall asCall = (SqlCall) selectItem; + expr = asCall.operand(0); + alias = asCall.operand(1); + } + if (!SqlUtil.isCallTo(expr, SqlStdOperatorTable.SCALAR_QUERY)) { + return selectItem; + } + final SqlBasicCall scalarSubQuery = (SqlBasicCall) expr; + final SqlNode query = scalarSubQuery.operand(0); + final SqlSelect subQuery = query instanceof SqlWith + ? (SqlSelect) ((SqlWith) query).body + : query instanceof SqlSelect + ? (SqlSelect) query + : null; + if (subQuery == null) { + return selectItem; + } + // Validate the sub-query first so that its scope is established. + deriveType(parentScope, scalarSubQuery); + final SqlNode rewrittenSubQuery = + rewriteOuterAggregate(query, subQuery, parentScope); + if (rewrittenSubQuery == query) { + return selectItem; + } + SqlNode result = rewrittenSubQuery; + if (alias != null) { + result = + SqlStdOperatorTable.AS.createCall(selectItem.getParserPosition(), + rewrittenSubQuery, alias); + } + return result; + } + + /** + * Rewrites a scalar sub-query whose single select item is an aggregate + * function whose arguments reference only outer columns. The aggregate is + * pulled out to the enclosing query, and the sub-query's select item is + * replaced with the constant 1. + * + * @return the rewritten sub-query expression, or {@code subQuery} if no rewrite applies + */ + private SqlNode rewriteOuterAggregate(SqlNode originalQuery, + SqlSelect subQuery, SqlValidatorScope parentScope) { + final SqlNodeList subSelectItems = SqlNonNullableAccessors.getSelectList(subQuery); + if (subSelectItems.size() != 1) { + return originalQuery; + } + if (subQuery.getGroup() != null && !subQuery.getGroup().isEmpty()) { + return originalQuery; + } + SqlNode subSelectItem = subSelectItems.get(0); + if (SqlUtil.isCallTo(subSelectItem, SqlStdOperatorTable.AS)) { + subSelectItem = ((SqlCall) subSelectItem).operand(0); + } + if (!(subSelectItem instanceof SqlCall)) { + return originalQuery; + } + final SqlCall aggCall = (SqlCall) subSelectItem; + if (!aggCall.getOperator().isAggregator()) { + return originalQuery; + } + final SqlValidatorScope subScope = getSelectScope(subQuery); + if (subScope == null) { + return originalQuery; + } + final SqlValidatorScope operandScope; + if (subScope instanceof AggregatingSelectScope) { + operandScope = ((AggregatingSelectScope) subScope).parent; + } else { + operandScope = subScope; + } + if (operandScope == null) { + return originalQuery; + } + for (SqlNode operand : aggCall.getOperandList()) { + if (!referencesOnlyOuterColumns(operand, parentScope, operandScope)) { + return originalQuery; + } + } + // Rewrite: replace aggregate with 1 in a new sub-query, and multiply + // by the aggregate in the outer query. + final SqlLiteral one = SqlLiteral.createExactNumeric("1", SqlParserPos.ZERO); + final SqlNodeList newSubSelectItems = + new SqlNodeList(ImmutableList.of(one), subSelectItems.getParserPosition()); + final SqlNodeList keywordList = (SqlNodeList) subQuery.getOperandList().get(0); + final SqlSelect newSubQuery = + new SqlSelect(subQuery.getParserPosition(), + keywordList, + newSubSelectItems, + subQuery.getFrom(), + subQuery.getWhere(), + subQuery.getGroup(), + subQuery.getHaving(), + subQuery.getWindowList(), + subQuery.getQualify(), + subQuery.getOrderList(), + subQuery.getOffset(), + subQuery.getFetch(), + subQuery.getHints(), + subQuery.getDistinctOn()); + // Ensure the rewritten sub-query still returns at most one row, because + // removing the aggregate may otherwise produce multiple rows. + if (newSubQuery.getFetch() == null) { + newSubQuery.setFetch( + SqlLiteral.createExactNumeric("1", SqlParserPos.ZERO)); + } + final SqlNode newQuery = originalQuery instanceof SqlWith + ? new SqlWith(originalQuery.getParserPosition(), + ((SqlWith) originalQuery).withList, newSubQuery) + : newSubQuery; + registerQuery(parentScope, null, newQuery, newQuery, null, false); + validateQuery(newQuery, parentScope, unknownType); + final SqlNode scalarSubQuery = + SqlStdOperatorTable.SCALAR_QUERY.createCall(newQuery.getParserPosition(), newQuery); + return SqlStdOperatorTable.MULTIPLY.createCall( + aggCall.getParserPosition(), aggCall, scalarSubQuery); + } + + /** + * Returns whether an expression contains only references to columns that are + * outside the current select scope, and can be resolved in the parent scope. + */ + private boolean referencesOnlyOuterColumns(SqlNode node, + SqlValidatorScope parentScope, SqlValidatorScope currentScope) { + // Do not rewrite if the aggregate argument contains a sub-query; the + // sub-query may reference inner columns, and pulling the aggregate out + // would change the semantics. + if (containsSubQuery(node)) { + return false; + } + final AtomicBoolean ok = new AtomicBoolean(true); + final AtomicInteger outerColumnCount = new AtomicInteger(0); + node.accept(new SqlBasicVisitor() { + @Override public Void visit(SqlIdentifier id) { + if (!isOuterReference(currentScope, id)) { + ok.set(false); + return null; + } + // The identifier must be resolvable in the parent scope; otherwise it + // references an intermediate scope, not the immediate enclosing query. + try { + parentScope.fullyQualify(id); + outerColumnCount.incrementAndGet(); + } catch (Exception e) { + ok.set(false); + } + return null; + } + }); + return ok.get() && outerColumnCount.get() > 0; + } + + /** + * Returns whether an expression contains a sub-query. + */ + private static boolean containsSubQuery(SqlNode node) { + final AtomicBoolean found = new AtomicBoolean(false); + node.accept(new SqlBasicVisitor() { + @Override public Void visit(SqlCall call) { + switch (call.getKind()) { + case SCALAR_QUERY: + case EXISTS: + case IN: + case NOT_IN: + case SOME: + case ALL: + case MULTISET_QUERY_CONSTRUCTOR: + case ARRAY_QUERY_CONSTRUCTOR: + case MAP_QUERY_CONSTRUCTOR: + case CURSOR: + found.set(true); + return null; + default: + return super.visit(call); + } + } + }); + return found.get(); + } + + /** + * Returns whether an identifier refers to a scope outside the current select. + */ + private boolean isOuterReference(SqlValidatorScope scope, SqlIdentifier id) { + final SqlQualified fqId = scope.fullyQualify(id); + if (fqId.prefixLength <= 0) { + return false; + } + final SqlValidatorScope.ResolvedImpl resolved = new SqlValidatorScope.ResolvedImpl(); + scope.resolve(fqId.prefix(), catalogReader.nameMatcher(), false, resolved); + return resolved.count() == 1 && !resolved.only().scope.isWithin(scope); + } + /** * Validates an expression. * diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 5478f6bb6e2c..2e7ec7f92eb3 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4301,4 +4301,29 @@ GROUP BY GROUPING SETS ((deptno), ()); !ok +# [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query +WITH aa (a) AS (VALUES 1, 2, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa +FROM aa; ++----+ +| SA | ++----+ +| 6 | ++----+ +(1 row) + +!ok + +SELECT (SELECT sum(sal) FROM dept) AS sum_sal +FROM emp; ++----------+ +| SUM_SAL | ++----------+ +| 29025.00 | ++----------+ +(1 row) + +!ok + # End agg.iq From cd8346a7ccd604a851e281b9dff873dc4f85fc8e Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Fri, 26 Jun 2026 22:41:14 +0800 Subject: [PATCH 02/21] Addressed --- .../java/org/apache/calcite/sql/validate/SqlValidatorImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index a58ba382dc3e..984bbda4f6e0 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5700,7 +5700,8 @@ private void rewriteOuterAggregatesInSelectList(SqlSelect select) { scope = ((AggregatingSelectScope) scope).getParent(); } clauseScopes.put(IdPair.of(select, Clause.SELECT), - new AggregatingSelectScope((SelectScope) scope, select, false)); + new AggregatingSelectScope( + requireNonNull(scope, "scope"), select, false)); } } From 47e152f1069e57a6b7e762d37dee1332b93fdcd0 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Fri, 26 Jun 2026 23:15:24 +0800 Subject: [PATCH 03/21] Addressed --- .../org/apache/calcite/sql/validate/SqlValidatorImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 984bbda4f6e0..4d41285e3fc2 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5685,7 +5685,7 @@ private void rewriteOuterAggregatesInSelectList(SqlSelect select) { for (int i = 0; i < selectItems.size(); i++) { final SqlNode selectItem = selectItems.get(i); final SqlNode rewrittenItem = - rewriteOuterAggregateItem(select, selectScope, selectItem); + rewriteOuterAggregateItem(selectScope, selectItem); if (rewrittenItem != selectItem) { selectItems.set(i, rewrittenItem); } @@ -5711,8 +5711,8 @@ private void rewriteOuterAggregatesInSelectList(SqlSelect select) { * * @return the rewritten expression, or {@code selectItem} if no rewrite applies */ - private SqlNode rewriteOuterAggregateItem(SqlSelect parentSelect, - SqlValidatorScope parentScope, SqlNode selectItem) { + private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, + SqlNode selectItem) { // Unwrap AS. SqlNode expr = selectItem; @Nullable SqlIdentifier alias = null; From 7555c70cdc078f36a2da401660f36ea75505b3d7 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 27 Jun 2026 09:55:44 +0800 Subject: [PATCH 04/21] Addressed --- .../sql/validate/SqlValidatorImpl.java | 18 ++++++ core/src/test/resources/sql/agg.iq | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 4d41285e3fc2..cff8e4a35d79 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5670,6 +5670,24 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, * an aggregate function whose arguments reference only outer columns. Per the * SQL standard, such aggregates belong to the outer query. * + *

The algorithm is: + *

    + *
  1. For each item in the SELECT list, check whether it is a scalar + * sub-query, optionally wrapped in {@code AS} or {@code WITH}. + *
  2. Inside the sub-query, require the SELECT list to contain exactly one + * item, and that item to be an aggregate function call. + *
  3. Require every argument of the aggregate to reference only columns from + * the outer query (no inner columns), and to contain no nested sub-queries. + *
  4. If all conditions hold, lift the aggregate out of the sub-query: keep + * the aggregate in the outer SELECT list, and replace the original sub-query + * with {@code (SELECT 1 FROM ... LIMIT 1)}. The result is equivalent because + * the scalar sub-query contributes a factor of one per outer row, while the + * aggregate is evaluated over the outer rows. + *
  5. If the outer query becomes an aggregate query as a result, upgrade its + * SELECT clause scope from {@link SelectScope} to + * {@link AggregatingSelectScope}. + *
+ * *

For example, *

SELECT (SELECT sum(a) FROM t LIMIT 1) FROM aa
* is rewritten to diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 2e7ec7f92eb3..904206b04c05 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4315,6 +4315,61 @@ FROM aa; !ok +# Aggregate with no column references aggregates at the innermost level +WITH aa (a) AS (VALUES 1, 2, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(1) FROM xx LIMIT 1) AS s +FROM aa; ++---+ +| S | ++---+ +| 3 | +| 3 | +| 3 | ++---+ +(3 rows) + +!ok + +# Aggregate referencing only inner column aggregates at the inner level +WITH aa (a) AS (VALUES 1, 2, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(x) FROM xx LIMIT 1) AS s +FROM aa; ++----+ +| S | ++----+ +| 60 | +| 60 | +| 60 | ++----+ +(3 rows) + +!ok + +# Two scalar sub-queries with outer-only aggregates are both lifted to the outer query +WITH aa (a) AS (VALUES 1, 2, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa, + (SELECT count(a) FROM xx LIMIT 1) AS ca +FROM aa; ++----+----+ +| SA | CA | ++----+----+ +| 6 | 3 | ++----+----+ +(1 row) + +!ok + +EnumerableCalc(expr#0..2=[{inputs}], expr#3=[*($t0, $t2)], expr#4=[*($t1, $t2)], SA=[$t3], CA=[$t4]) + EnumerableNestedLoopJoin(condition=[true], joinType=[left]) + EnumerableAggregate(group=[{}], agg#0=[SUM($0)], agg#1=[COUNT()]) + EnumerableValues(tuples=[[{ 1 }, { 2 }, { 3 }]]) + EnumerableLimit(fetch=[1]) + EnumerableValues(tuples=[[{ 1 }, { 1 }, { 1 }]]) +!plan + SELECT (SELECT sum(sal) FROM dept) AS sum_sal FROM emp; +----------+ From e3c977e312b64ce41c8f018864049a16e4dde8b2 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 27 Jun 2026 09:58:28 +0800 Subject: [PATCH 05/21] Addressed --- core/src/test/resources/sql/agg.iq | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 904206b04c05..8c43b8776ae8 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4302,6 +4302,8 @@ GROUP BY GROUPING SETS ((deptno), ()); !ok # [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query +# The expected results below have been verified in PostgreSQL by running +# equivalent standard-SQL rewrites of each query. WITH aa (a) AS (VALUES 1, 2, 3), xx (x) AS (VALUES 10, 20, 30) SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa From c9fe712224e0ce8dfc2b89c358dee495cdb83ae2 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 27 Jun 2026 11:19:47 +0800 Subject: [PATCH 06/21] Addressed --- .../sql/validate/SqlValidatorImpl.java | 12 +++- core/src/test/resources/sql/agg.iq | 59 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index cff8e4a35d79..78626fd5b54d 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5689,9 +5689,17 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, * * *

For example, - *

SELECT (SELECT sum(a) FROM t LIMIT 1) FROM aa
+ *
+   * WITH aa(a) AS (VALUES 1, 2, 3),
+   *      t(x) AS (VALUES 10, 20, 30)
+   * SELECT (SELECT sum(a) FROM t LIMIT 1) FROM aa
+   * 
* is rewritten to - *
SELECT sum(a) * (SELECT 1 FROM t LIMIT 1) FROM aa
+ *
+   * WITH aa(a) AS (VALUES 1, 2, 3),
+   *      t(x) AS (VALUES 10, 20, 30)
+   * SELECT sum(a) * (SELECT 1 FROM t LIMIT 1) FROM aa
+   * 
*/ private void rewriteOuterAggregatesInSelectList(SqlSelect select) { final SqlNodeList selectItems = select.getSelectList(); diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 8c43b8776ae8..0faadf12eedc 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4317,6 +4317,65 @@ FROM aa; !ok +# Inner table is empty: the scalar sub-query contributes NULL +WITH aa (a) AS (VALUES 1, 2, 3), + empty_xx (x) AS (SELECT 1 FROM emp WHERE 1 = 0) +SELECT (SELECT sum(a) FROM empty_xx LIMIT 1) AS sa +FROM aa; ++----+ +| SA | ++----+ +| | ++----+ +(1 row) + +!ok + +# Outer table is empty: the outer aggregate has no input rows +WITH empty_aa (a) AS (SELECT 1 FROM emp WHERE 1 = 0), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa +FROM empty_aa; ++----+ +| SA | ++----+ +| | ++----+ +(1 row) + +!ok + +# Aggregated column contains only NULLs +# Currently disabled: EnumerableAggregate fails to implement SUM over an +# all-NULL input even without the correlated-aggregate rewrite. This is a +# pre-existing limitation, not caused by CALCITE-6104. +# WITH aa (a) AS (VALUES NULL, NULL, NULL), +# xx (x) AS (VALUES 10, 20, 30) +# SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa +# FROM aa; +# +----+ +# | SA | +# +----+ +# | | +# +----+ +# (1 row) +# +# !ok + +# Aggregated column contains a mix of values and NULLs +WITH aa (a) AS (VALUES 1, NULL, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa +FROM aa; ++----+ +| SA | ++----+ +| 4 | ++----+ +(1 row) + +!ok + # Aggregate with no column references aggregates at the innermost level WITH aa (a) AS (VALUES 1, 2, 3), xx (x) AS (VALUES 10, 20, 30) From 7f81a121fe349a6a2442f5f2c126c312573fa131 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 27 Jun 2026 11:35:22 +0800 Subject: [PATCH 07/21] Addressed --- core/src/test/resources/sql/agg.iq | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 0faadf12eedc..d24b92bc90a4 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4345,23 +4345,6 @@ FROM empty_aa; !ok -# Aggregated column contains only NULLs -# Currently disabled: EnumerableAggregate fails to implement SUM over an -# all-NULL input even without the correlated-aggregate rewrite. This is a -# pre-existing limitation, not caused by CALCITE-6104. -# WITH aa (a) AS (VALUES NULL, NULL, NULL), -# xx (x) AS (VALUES 10, 20, 30) -# SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa -# FROM aa; -# +----+ -# | SA | -# +----+ -# | | -# +----+ -# (1 row) -# -# !ok - # Aggregated column contains a mix of values and NULLs WITH aa (a) AS (VALUES 1, NULL, 3), xx (x) AS (VALUES 10, 20, 30) From d92b678a89dcea46e649cf1cc3bf097bae103dd3 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 27 Jun 2026 13:21:17 +0800 Subject: [PATCH 08/21] Addressed --- core/src/test/resources/sql/agg.iq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index d24b92bc90a4..6200d9e96adf 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4303,7 +4303,7 @@ GROUP BY GROUPING SETS ((deptno), ()); # [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query # The expected results below have been verified in PostgreSQL by running -# equivalent standard-SQL rewrites of each query. +# equivalent standard-SQL rewrites of each query(some of them are not standard-SQL). WITH aa (a) AS (VALUES 1, 2, 3), xx (x) AS (VALUES 10, 20, 30) SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa From fb6ac703fc308eac82bcde509074333cb8ac76e0 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sun, 28 Jun 2026 07:42:03 +0800 Subject: [PATCH 09/21] Addressed --- .../sql/validate/SqlAbstractConformance.java | 4 ++++ .../calcite/sql/validate/SqlConformance.java | 16 ++++++++++++++++ .../calcite/sql/validate/SqlConformanceEnum.java | 10 ++++++++++ .../sql/validate/SqlDelegatingConformance.java | 4 ++++ .../calcite/sql/validate/SqlValidatorImpl.java | 7 +++++-- core/src/test/resources/sql/agg.iq | 7 +++++-- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java index 82f06a48d168..3d4df00f10e5 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java @@ -176,4 +176,8 @@ public abstract class SqlAbstractConformance implements SqlConformance { @Override public boolean isDistinctOnAllowed() { return SqlConformanceEnum.DEFAULT.isDistinctOnAllowed(); } + + @Override public boolean isCorrelatedAggregateAllowed() { + return SqlConformanceEnum.DEFAULT.isCorrelatedAggregateAllowed(); + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java index 32b4a03b90d9..4034d3f51bc9 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java @@ -691,4 +691,20 @@ default boolean isColonFieldAccessAllowed() { * false otherwise. */ boolean isDistinctOnAllowed(); + + /** + * Whether an aggregate function inside a scalar sub-query is allowed to + * reference columns from an outer query. + * + *

This is not allowed by the SQL standard, but is supported by some + * databases, including SQLite, DuckDB and SQL Server. + * + *

Among the built-in conformance levels, true in + * {@link SqlConformanceEnum#BABEL}, + * {@link SqlConformanceEnum#LENIENT}; + * false otherwise. + */ + default boolean isCorrelatedAggregateAllowed() { + return false; + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java index 4475c4ce8096..fe57cb2b8c26 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java @@ -542,4 +542,14 @@ public enum SqlConformanceEnum implements SqlConformance { return false; } } + + @Override public boolean isCorrelatedAggregateAllowed() { + switch (this) { + case BABEL: + case LENIENT: + return true; + default: + return false; + } + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java index 0d415d8aec24..aa1b361177a8 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java @@ -181,4 +181,8 @@ protected SqlDelegatingConformance(SqlConformance delegate) { @Override public boolean isDistinctOnAllowed() { return delegate.isDistinctOnAllowed(); } + + @Override public boolean isCorrelatedAggregateAllowed() { + return delegate.isCorrelatedAggregateAllowed(); + } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 78626fd5b54d..47e0cfc7ea97 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5603,8 +5603,11 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, // Rewrite scalar sub-queries whose single select item is an aggregate // over outer columns. The aggregate belongs to the outer query per SQL - // standard. - rewriteOuterAggregatesInSelectList(select); + // standard, but only if the current conformance allows this non-standard + // correlated-aggregate construct. + if (config.conformance().isCorrelatedAggregateAllowed()) { + rewriteOuterAggregatesInSelectList(select); + } // Validate SELECT list. Expand terms of the form "*" or "TABLE.*". final SqlValidatorScope selectScope = getSelectScope(select); diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 6200d9e96adf..584e0a9794e4 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4302,8 +4302,9 @@ GROUP BY GROUPING SETS ((deptno), ()); !ok # [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query -# The expected results below have been verified in PostgreSQL by running -# equivalent standard-SQL rewrites of each query(some of them are not standard-SQL). +# Correlated aggregates are a non-standard extension; use LENIENT conformance. +!use scott-lenient + WITH aa (a) AS (VALUES 1, 2, 3), xx (x) AS (VALUES 10, 20, 30) SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa @@ -4425,4 +4426,6 @@ FROM emp; !ok +!use scott + # End agg.iq From 3b449e4eef1c81294ba4f40436ceb61ad9746336 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sun, 28 Jun 2026 07:55:27 +0800 Subject: [PATCH 10/21] Addressed --- .../org/apache/calcite/sql/validate/SqlValidatorImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 47e0cfc7ea97..1fbaf347c8b7 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5686,6 +5686,8 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, * with {@code (SELECT 1 FROM ... LIMIT 1)}. The result is equivalent because * the scalar sub-query contributes a factor of one per outer row, while the * aggregate is evaluated over the outer rows. + *

  • This rewrite will only work for numeric aggregates on types which + * have a multiplication operation. *
  • If the outer query becomes an aggregate query as a result, upgrade its * SELECT clause scope from {@link SelectScope} to * {@link AggregatingSelectScope}. @@ -5695,7 +5697,7 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, *
        * WITH aa(a) AS (VALUES 1, 2, 3),
        *      t(x) AS (VALUES 10, 20, 30)
    -   * SELECT (SELECT sum(a) FROM t LIMIT 1) FROM aa
    +   * SELECT (SELECT sum(a) FROM t) FROM aa
        * 
    * is rewritten to *
    
    From f0544c5909f6250088a7d0340dbe46becb108c8f Mon Sep 17 00:00:00 2001
    From: xuzifu666 <1206332514@qq.com>
    Date: Sun, 28 Jun 2026 09:50:51 +0800
    Subject: [PATCH 11/21] Addressed
    
    ---
     .../calcite/runtime/CalciteResource.java      |   3 +
     .../sql/validate/SqlValidatorImpl.java        | 115 ++++++++++++++----
     .../runtime/CalciteResource.properties        |   1 +
     .../apache/calcite/test/SqlValidatorTest.java |  12 ++
     core/src/test/resources/sql/agg.iq            |  25 +++-
     5 files changed, 129 insertions(+), 27 deletions(-)
    
    diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
    index ea8e1772c678..d760f20b9b72 100644
    --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
    +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
    @@ -924,6 +924,9 @@ ExInst illegalArgumentForTableFunctionCall(String a0,
       @BaseMessage("Extended columns not allowed under the current SQL conformance level")
       ExInst extendNotAllowed();
     
    +  @BaseMessage("Aggregate function referencing outer column is not allowed under the current SQL conformance level")
    +  ExInst correlatedAggregateNotAllowed();
    +
       @BaseMessage("Rolled up column ''{0}'' is not allowed in {1}")
       ExInst rolledUpNotAllowed(String column, String context);
     
    diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    index 1fbaf347c8b7..018112335fab 100644
    --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    @@ -5604,9 +5604,12 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems,
         // Rewrite scalar sub-queries whose single select item is an aggregate
         // over outer columns. The aggregate belongs to the outer query per SQL
         // standard, but only if the current conformance allows this non-standard
    -    // correlated-aggregate construct.
    +    // correlated-aggregate construct. If the conformance does not allow it,
    +    // validate that no such construct is present.
         if (config.conformance().isCorrelatedAggregateAllowed()) {
           rewriteOuterAggregatesInSelectList(select);
    +    } else {
    +      checkNoCorrelatedAggregatesInSelectList(select);
         }
     
         // Validate SELECT list. Expand terms of the form "*" or "TABLE.*".
    @@ -5736,6 +5739,33 @@ private void rewriteOuterAggregatesInSelectList(SqlSelect select) {
         }
       }
     
    +  /**
    +   * Validates that the SELECT list does not contain a correlated aggregate
    +   * when the current conformance does not allow it.
    +   */
    +  private void checkNoCorrelatedAggregatesInSelectList(SqlSelect select) {
    +    final SqlNodeList selectItems = select.getSelectList();
    +    if (selectItems == null) {
    +      return;
    +    }
    +    final SqlValidatorScope selectScope = getSelectScope(select);
    +    for (SqlNode selectItem : selectItems) {
    +      final SqlSelect subQuery = findScalarSubQuerySelect(selectItem);
    +      if (subQuery == null) {
    +        continue;
    +      }
    +      // Ensure the sub-query's scope is established before inspecting it.
    +      deriveType(selectScope,
    +          SqlStdOperatorTable.SCALAR_QUERY.createCall(
    +              subQuery.getParserPosition(), subQuery));
    +      final SqlCall aggCall = findCorrelatedAggregate(subQuery, selectScope);
    +      if (aggCall != null) {
    +        throw newValidationError(aggCall,
    +            RESOURCE.correlatedAggregateNotAllowed());
    +      }
    +    }
    +  }
    +
       /**
        * Rewrites a single SELECT list item if it is a scalar sub-query containing
        * an aggregate over outer columns.
    @@ -5744,6 +5774,10 @@ private void rewriteOuterAggregatesInSelectList(SqlSelect select) {
        */
       private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope,
           SqlNode selectItem) {
    +    final SqlSelect subQuery = findScalarSubQuerySelect(selectItem);
    +    if (subQuery == null) {
    +      return selectItem;
    +    }
         // Unwrap AS.
         SqlNode expr = selectItem;
         @Nullable SqlIdentifier alias = null;
    @@ -5752,19 +5786,8 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope,
           expr = asCall.operand(0);
           alias = asCall.operand(1);
         }
    -    if (!SqlUtil.isCallTo(expr, SqlStdOperatorTable.SCALAR_QUERY)) {
    -      return selectItem;
    -    }
         final SqlBasicCall scalarSubQuery = (SqlBasicCall) expr;
         final SqlNode query = scalarSubQuery.operand(0);
    -    final SqlSelect subQuery = query instanceof SqlWith
    -        ? (SqlSelect) ((SqlWith) query).body
    -        : query instanceof SqlSelect
    -            ? (SqlSelect) query
    -            : null;
    -    if (subQuery == null) {
    -      return selectItem;
    -    }
         // Validate the sub-query first so that its scope is established.
         deriveType(parentScope, scalarSubQuery);
         final SqlNode rewrittenSubQuery =
    @@ -5782,36 +5805,58 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope,
       }
     
       /**
    -   * Rewrites a scalar sub-query whose single select item is an aggregate
    -   * function whose arguments reference only outer columns. The aggregate is
    -   * pulled out to the enclosing query, and the sub-query's select item is
    -   * replaced with the constant 1.
    +   * Extracts the {@link SqlSelect} from a SELECT list item that is a scalar
    +   * sub-query, optionally wrapped in {@code AS}.
        *
    -   * @return the rewritten sub-query expression, or {@code subQuery} if no rewrite applies
    +   * @return the sub-query's SELECT, or {@code null} if the item is not a
    +   * scalar sub-query
        */
    -  private SqlNode rewriteOuterAggregate(SqlNode originalQuery,
    -      SqlSelect subQuery, SqlValidatorScope parentScope) {
    +  private @Nullable SqlSelect findScalarSubQuerySelect(SqlNode selectItem) {
    +    SqlNode expr = selectItem;
    +    if (SqlUtil.isCallTo(selectItem, SqlStdOperatorTable.AS)) {
    +      expr = ((SqlCall) selectItem).operand(0);
    +    }
    +    if (!SqlUtil.isCallTo(expr, SqlStdOperatorTable.SCALAR_QUERY)) {
    +      return null;
    +    }
    +    final SqlNode query = ((SqlBasicCall) expr).operand(0);
    +    return query instanceof SqlWith
    +        ? (SqlSelect) ((SqlWith) query).body
    +        : query instanceof SqlSelect
    +            ? (SqlSelect) query
    +            : null;
    +  }
    +
    +  /**
    +   * Validates that the SELECT list does not contain a correlated aggregate
    +   * when the current conformance does not allow it.
    +   * that is an aggregate function whose arguments reference only outer columns.
    +   *
    +   * @return the aggregate call, or {@code null} if no such aggregate is found
    +   */
    +  private @Nullable SqlCall findCorrelatedAggregate(SqlSelect subQuery,
    +      SqlValidatorScope parentScope) {
         final SqlNodeList subSelectItems = SqlNonNullableAccessors.getSelectList(subQuery);
         if (subSelectItems.size() != 1) {
    -      return originalQuery;
    +      return null;
         }
         if (subQuery.getGroup() != null && !subQuery.getGroup().isEmpty()) {
    -      return originalQuery;
    +      return null;
         }
         SqlNode subSelectItem = subSelectItems.get(0);
         if (SqlUtil.isCallTo(subSelectItem, SqlStdOperatorTable.AS)) {
           subSelectItem = ((SqlCall) subSelectItem).operand(0);
         }
         if (!(subSelectItem instanceof SqlCall)) {
    -      return originalQuery;
    +      return null;
         }
         final SqlCall aggCall = (SqlCall) subSelectItem;
         if (!aggCall.getOperator().isAggregator()) {
    -      return originalQuery;
    +      return null;
         }
         final SqlValidatorScope subScope = getSelectScope(subQuery);
         if (subScope == null) {
    -      return originalQuery;
    +      return null;
         }
         final SqlValidatorScope operandScope;
         if (subScope instanceof AggregatingSelectScope) {
    @@ -5820,13 +5865,31 @@ private SqlNode rewriteOuterAggregate(SqlNode originalQuery,
           operandScope = subScope;
         }
         if (operandScope == null) {
    -      return originalQuery;
    +      return null;
         }
         for (SqlNode operand : aggCall.getOperandList()) {
           if (!referencesOnlyOuterColumns(operand, parentScope, operandScope)) {
    -        return originalQuery;
    +        return null;
           }
         }
    +    return aggCall;
    +  }
    +
    +  /**
    +   * Rewrites a scalar sub-query whose single select item is an aggregate
    +   * function whose arguments reference only outer columns. The aggregate is
    +   * pulled out to the enclosing query, and the sub-query's select item is
    +   * replaced with the constant 1.
    +   *
    +   * @return the rewritten sub-query expression, or {@code subQuery} if no rewrite applies
    +   */
    +  private SqlNode rewriteOuterAggregate(SqlNode originalQuery,
    +      SqlSelect subQuery, SqlValidatorScope parentScope) {
    +    final SqlCall aggCall = findCorrelatedAggregate(subQuery, parentScope);
    +    if (aggCall == null) {
    +      return originalQuery;
    +    }
    +    final SqlNodeList subSelectItems = SqlNonNullableAccessors.getSelectList(subQuery);
         // Rewrite: replace aggregate with 1 in a new sub-query, and multiply
         // by the aggregate in the outer query.
         final SqlLiteral one = SqlLiteral.createExactNumeric("1", SqlParserPos.ZERO);
    diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
    index 49552b41985d..8821486b966e 100644
    --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
    +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
    @@ -303,6 +303,7 @@ IntervalMustBeNonNegative=Interval must be non-negative ''{0}''
     CannotUseWithinWithoutOrderBy=Must contain an ORDER BY clause when WITHIN is used
     FirstColumnOfOrderByMustBeTimestamp=First column of ORDER BY must be of type TIMESTAMP
     ExtendNotAllowed=Extended columns not allowed under the current SQL conformance level
    +CorrelatedAggregateNotAllowed=Aggregate function referencing outer column is not allowed under the current SQL conformance level
     RolledUpNotAllowed=Rolled up column ''{0}'' is not allowed in {1}
     SchemaExists=Schema ''{0}'' already exists
     SchemaInvalidType=Invalid schema type ''{0}''; valid values: {1}
    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 0cf1966f99a8..2c98cd103958 100644
    --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
    +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
    @@ -8464,6 +8464,18 @@ void testGroupExpressionEquivalenceParams() {
             .columnType("INTEGER NOT NULL");
       }
     
    +  @Test void testCorrelatedAggregateConformance() {
    +    final String sql = "select (select ^sum(sal)^ from dept) from emp";
    +    // LENIENT and BABEL allow the non-standard correlated-aggregate construct.
    +    sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
    +    sql(sql).withConformance(SqlConformanceEnum.BABEL).ok();
    +    // DEFAULT and STRICT_2003 do not.
    +    final String err =
    +        "Aggregate function referencing outer column is not allowed under the current SQL conformance level";
    +    sql(sql).withConformance(SqlConformanceEnum.DEFAULT).fails(err);
    +    sql(sql).withConformance(SqlConformanceEnum.STRICT_2003).fails(err);
    +  }
    +
       @Test void testAggregateInOrderByFails() {
         sql("select empno from emp order by ^sum(empno)^")
             .fails(ERR_AGG_IN_ORDER_BY);
    diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq
    index 584e0a9794e4..016f826e196c 100644
    --- a/core/src/test/resources/sql/agg.iq
    +++ b/core/src/test/resources/sql/agg.iq
    @@ -4302,7 +4302,30 @@ GROUP BY GROUPING SETS ((deptno), ());
     !ok
     
     # [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query
    -# Correlated aggregates are a non-standard extension; use LENIENT conformance.
    +# Correlated aggregates are a non-standard extension.
    +# They are rejected under DEFAULT conformance ...
    +!use scott
    +
    +SELECT (SELECT sum(sal) FROM dept) FROM emp;
    +Aggregate function referencing outer column is not allowed under the current SQL conformance level
    +!error
    +
    +WITH aa (a) AS (VALUES 1, 2, 3),
    +  xx (x) AS (VALUES 10, 20, 30)
    +SELECT (SELECT count(a) FROM xx LIMIT 1) AS ca
    +FROM aa;
    +Aggregate function referencing outer column is not allowed under the current SQL conformance level
    +!error
    +
    +WITH aa (a) AS (VALUES 1, 2, 3),
    +  xx (x) AS (VALUES 10, 20, 30)
    +SELECT (SELECT sum(a) FROM xx LIMIT 1) AS sa,
    +  (SELECT count(a) FROM xx LIMIT 1) AS ca
    +FROM aa;
    +Aggregate function referencing outer column is not allowed under the current SQL conformance level
    +!error
    +
    +# ... and allowed under LENIENT conformance.
     !use scott-lenient
     
     WITH aa (a) AS (VALUES 1, 2, 3),
    
    From 6969a5a8ae16aa8cf0d0170ed55f814b46849c08 Mon Sep 17 00:00:00 2001
    From: xuzifu666 <1206332514@qq.com>
    Date: Sun, 28 Jun 2026 09:57:29 +0800
    Subject: [PATCH 12/21] Addressed
    
    ---
     core/src/test/resources/sql/agg.iq | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq
    index 016f826e196c..5ee4f3b20f70 100644
    --- a/core/src/test/resources/sql/agg.iq
    +++ b/core/src/test/resources/sql/agg.iq
    @@ -4303,7 +4303,7 @@ GROUP BY GROUPING SETS ((deptno), ());
     
     # [CALCITE-6104] Aggregate function that references outer column should be evaluated in outer query
     # Correlated aggregates are a non-standard extension.
    -# They are rejected under DEFAULT conformance ...
    +# They are rejected under DEFAULT conformance.
     !use scott
     
     SELECT (SELECT sum(sal) FROM dept) FROM emp;
    
    From a0b7478879305918c7ca9e6296315b47eb64c67f Mon Sep 17 00:00:00 2001
    From: xuzifu666 <1206332514@qq.com>
    Date: Sun, 28 Jun 2026 10:18:53 +0800
    Subject: [PATCH 13/21] Addressed
    
    ---
     core/src/test/resources/sql/agg.iq | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq
    index 5ee4f3b20f70..f34049538dc1 100644
    --- a/core/src/test/resources/sql/agg.iq
    +++ b/core/src/test/resources/sql/agg.iq
    @@ -4325,7 +4325,7 @@ FROM aa;
     Aggregate function referencing outer column is not allowed under the current SQL conformance level
     !error
     
    -# ... and allowed under LENIENT conformance.
    +# allowed under LENIENT conformance.
     !use scott-lenient
     
     WITH aa (a) AS (VALUES 1, 2, 3),
    
    From 2822919ef338d5a324047d956547c0f507f71cc0 Mon Sep 17 00:00:00 2001
    From: xuzifu666 <1206332514@qq.com>
    Date: Sun, 28 Jun 2026 10:19:11 +0800
    Subject: [PATCH 14/21] Addressed
    
    ---
     core/src/test/resources/sql/agg.iq | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq
    index f34049538dc1..faabfbd5abae 100644
    --- a/core/src/test/resources/sql/agg.iq
    +++ b/core/src/test/resources/sql/agg.iq
    @@ -4325,7 +4325,7 @@ FROM aa;
     Aggregate function referencing outer column is not allowed under the current SQL conformance level
     !error
     
    -# allowed under LENIENT conformance.
    +# Allowed under LENIENT conformance.
     !use scott-lenient
     
     WITH aa (a) AS (VALUES 1, 2, 3),
    
    From 4706e6cbed3d53adfa6029a2a286ee120835874c Mon Sep 17 00:00:00 2001
    From: xuzifu666 <1206332514@qq.com>
    Date: Fri, 3 Jul 2026 11:23:04 +0800
    Subject: [PATCH 15/21] Addressed
    
    ---
     .../sql/validate/SqlValidatorImpl.java        | 41 ++++++++++++-------
     .../apache/calcite/test/SqlValidatorTest.java |  8 ++++
     2 files changed, 34 insertions(+), 15 deletions(-)
    
    diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    index 018112335fab..cf74440acf61 100644
    --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
    @@ -161,8 +161,6 @@
     import java.util.Objects;
     import java.util.Set;
     import java.util.TreeSet;
    -import java.util.concurrent.atomic.AtomicBoolean;
    -import java.util.concurrent.atomic.AtomicInteger;
     import java.util.function.BiFunction;
     import java.util.function.Consumer;
     import java.util.function.Supplier;
    @@ -5828,9 +5826,14 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope,
       }
     
       /**
    -   * Validates that the SELECT list does not contain a correlated aggregate
    -   * when the current conformance does not allow it.
    -   * that is an aggregate function whose arguments reference only outer columns.
    +   * Finds the aggregate function in a scalar sub-query's SELECT list whose
    +   * arguments reference only columns from the outer query. Such an aggregate
    +   * belongs to the outer query per the SQL standard.
    +   *
    +   * 

    Returns {@code null} unless the sub-query has exactly one select item, + * that item is an aggregate call, all of its arguments reference only outer + * columns, and its result type is numeric (so that the rewrite's + * multiplication by a scalar sub-query is well-typed). * * @return the aggregate call, or {@code null} if no such aggregate is found */ @@ -5872,6 +5875,13 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, return null; } } + // The rewrite multiplies the aggregate by a scalar sub-query, so it only + // applies to aggregates whose result type is numeric (e.g. SUM, COUNT). + // Non-numeric aggregates such as MAX() are left untouched. + final RelDataType aggType = deriveType(subScope, aggCall); + if (!SqlTypeUtil.isNumeric(aggType)) { + return null; + } return aggCall; } @@ -5941,33 +5951,34 @@ private boolean referencesOnlyOuterColumns(SqlNode node, if (containsSubQuery(node)) { return false; } - final AtomicBoolean ok = new AtomicBoolean(true); - final AtomicInteger outerColumnCount = new AtomicInteger(0); + // ok[0] is cleared if any identifier is not an outer reference; + // ok[1] is set once at least one outer column is found. + final boolean[] ok = {true, false}; node.accept(new SqlBasicVisitor() { @Override public Void visit(SqlIdentifier id) { if (!isOuterReference(currentScope, id)) { - ok.set(false); + ok[0] = false; return null; } // The identifier must be resolvable in the parent scope; otherwise it // references an intermediate scope, not the immediate enclosing query. try { parentScope.fullyQualify(id); - outerColumnCount.incrementAndGet(); - } catch (Exception e) { - ok.set(false); + ok[1] = true; + } catch (CalciteException e) { + ok[0] = false; } return null; } }); - return ok.get() && outerColumnCount.get() > 0; + return ok[0] && ok[1]; } /** * Returns whether an expression contains a sub-query. */ private static boolean containsSubQuery(SqlNode node) { - final AtomicBoolean found = new AtomicBoolean(false); + final boolean[] found = {false}; node.accept(new SqlBasicVisitor() { @Override public Void visit(SqlCall call) { switch (call.getKind()) { @@ -5981,14 +5992,14 @@ private static boolean containsSubQuery(SqlNode node) { case ARRAY_QUERY_CONSTRUCTOR: case MAP_QUERY_CONSTRUCTOR: case CURSOR: - found.set(true); + found[0] = true; return null; default: return super.visit(call); } } }); - return found.get(); + return found[0]; } /** 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 2c98cd103958..ccff90765281 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8474,6 +8474,14 @@ void testGroupExpressionEquivalenceParams() { "Aggregate function referencing outer column is not allowed under the current SQL conformance level"; sql(sql).withConformance(SqlConformanceEnum.DEFAULT).fails(err); sql(sql).withConformance(SqlConformanceEnum.STRICT_2003).fails(err); + + // A non-numeric aggregate over an outer column (MAX of a VARCHAR) cannot + // be rewritten via multiplication, so it is left untouched rather than + // producing an ill-typed '*' expression. + final String nonNumeric = + "select (select max(ename) from dept) from emp"; + sql(nonNumeric).withConformance(SqlConformanceEnum.LENIENT).ok(); + sql(nonNumeric).withConformance(SqlConformanceEnum.BABEL).ok(); } @Test void testAggregateInOrderByFails() { From 2bdf0e62051105ef4521c1533d9a11f51c97d0cf Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sat, 4 Jul 2026 08:31:50 +0800 Subject: [PATCH 16/21] Addressed --- .../sql/validate/SqlValidatorImpl.java | 40 ++++++++++--------- .../apache/calcite/test/SqlValidatorTest.java | 5 +-- core/src/test/resources/sql/agg.iq | 15 ++++++- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index cf74440acf61..5ad0b834aaae 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5683,12 +5683,11 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, *

  • Require every argument of the aggregate to reference only columns from * the outer query (no inner columns), and to contain no nested sub-queries. *
  • If all conditions hold, lift the aggregate out of the sub-query: keep - * the aggregate in the outer SELECT list, and replace the original sub-query - * with {@code (SELECT 1 FROM ... LIMIT 1)}. The result is equivalent because - * the scalar sub-query contributes a factor of one per outer row, while the - * aggregate is evaluated over the outer rows. - *
  • This rewrite will only work for numeric aggregates on types which - * have a multiplication operation. + * the aggregate in the outer SELECT list, and guard it with + * {@code (SELECT 1 FROM ... LIMIT 1) IS NOT NULL}. The result is equivalent + * because the aggregate is evaluated over the outer rows, but yields NULL + * whenever the inner query has no rows (as the original scalar sub-query + * would). *
  • If the outer query becomes an aggregate query as a result, upgrade its * SELECT clause scope from {@link SelectScope} to * {@link AggregatingSelectScope}. @@ -5704,7 +5703,8 @@ protected RelDataType validateSelectList(final SqlNodeList selectItems, *
        * WITH aa(a) AS (VALUES 1, 2, 3),
        *      t(x) AS (VALUES 10, 20, 30)
    -   * SELECT sum(a) * (SELECT 1 FROM t LIMIT 1) FROM aa
    +   * SELECT CASE WHEN (SELECT 1 FROM t LIMIT 1) IS NOT NULL
    +   *        THEN sum(a) END FROM aa
        * 
    */ private void rewriteOuterAggregatesInSelectList(SqlSelect select) { @@ -5831,9 +5831,8 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, * belongs to the outer query per the SQL standard. * *

    Returns {@code null} unless the sub-query has exactly one select item, - * that item is an aggregate call, all of its arguments reference only outer - * columns, and its result type is numeric (so that the rewrite's - * multiplication by a scalar sub-query is well-typed). + * that item is an aggregate call, and all of its arguments reference only + * outer columns. * * @return the aggregate call, or {@code null} if no such aggregate is found */ @@ -5875,13 +5874,6 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, return null; } } - // The rewrite multiplies the aggregate by a scalar sub-query, so it only - // applies to aggregates whose result type is numeric (e.g. SUM, COUNT). - // Non-numeric aggregates such as MAX() are left untouched. - final RelDataType aggType = deriveType(subScope, aggCall); - if (!SqlTypeUtil.isNumeric(aggType)) { - return null; - } return aggCall; } @@ -5935,8 +5927,18 @@ private SqlNode rewriteOuterAggregate(SqlNode originalQuery, validateQuery(newQuery, parentScope, unknownType); final SqlNode scalarSubQuery = SqlStdOperatorTable.SCALAR_QUERY.createCall(newQuery.getParserPosition(), newQuery); - return SqlStdOperatorTable.MULTIPLY.createCall( - aggCall.getParserPosition(), aggCall, scalarSubQuery); + // Rewrite to CASE WHEN (SELECT 1 FROM ... LIMIT 1) IS NOT NULL THEN END. + // This is type-agnostic (unlike multiplication, which only works for + // numeric aggregates): the scalar sub-query yields a non-null value when + // the inner query has rows, otherwise the whole expression is NULL, which + // matches the semantics of the original scalar sub-query. + final SqlParserPos pos = aggCall.getParserPosition(); + final SqlNode condition = + SqlStdOperatorTable.IS_NOT_NULL.createCall(pos, scalarSubQuery); + return new SqlCase(pos, null, + new SqlNodeList(ImmutableList.of(condition), pos), + new SqlNodeList(ImmutableList.of(aggCall), pos), + SqlLiteral.createNull(pos)); } /** 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 ccff90765281..86ede1fb8a17 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8475,9 +8475,8 @@ void testGroupExpressionEquivalenceParams() { sql(sql).withConformance(SqlConformanceEnum.DEFAULT).fails(err); sql(sql).withConformance(SqlConformanceEnum.STRICT_2003).fails(err); - // A non-numeric aggregate over an outer column (MAX of a VARCHAR) cannot - // be rewritten via multiplication, so it is left untouched rather than - // producing an ill-typed '*' expression. + // A non-numeric aggregate over an outer column (MAX of a VARCHAR) is also + // lifted to the outer query; the CASE-based rewrite is type-agnostic. final String nonNumeric = "select (select max(ename) from dept) from emp"; sql(nonNumeric).withConformance(SqlConformanceEnum.LENIENT).ok(); diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index faabfbd5abae..57c76ccc0912 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4341,6 +4341,19 @@ FROM aa; !ok +# A non-numeric aggregate (MAX over a VARCHAR outer column) is also lifted to +# the outer query. The CASE-based rewrite is type-agnostic, so the aggregate is +# evaluated over the outer rows and produces a single row. +SELECT (SELECT max(ename) FROM dept) AS m FROM emp; ++------+ +| M | ++------+ +| WARD | ++------+ +(1 row) + +!ok + # Inner table is empty: the scalar sub-query contributes NULL WITH aa (a) AS (VALUES 1, 2, 3), empty_xx (x) AS (SELECT 1 FROM emp WHERE 1 = 0) @@ -4430,7 +4443,7 @@ FROM aa; !ok -EnumerableCalc(expr#0..2=[{inputs}], expr#3=[*($t0, $t2)], expr#4=[*($t1, $t2)], SA=[$t3], CA=[$t4]) +EnumerableCalc(expr#0..2=[{inputs}], expr#3=[IS NOT NULL($t2)], expr#4=[null:INTEGER], expr#5=[CASE($t3, $t0, $t4)], expr#6=[null:BIGINT], expr#7=[CASE($t3, $t1, $t6)], SA=[$t5], CA=[$t7]) EnumerableNestedLoopJoin(condition=[true], joinType=[left]) EnumerableAggregate(group=[{}], agg#0=[SUM($0)], agg#1=[COUNT()]) EnumerableValues(tuples=[[{ 1 }, { 2 }, { 3 }]]) From 6ce5af23a849946f4a5477b19d76a110388c50c8 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sun, 5 Jul 2026 06:47:57 +0800 Subject: [PATCH 17/21] Addressed --- .../java/org/apache/calcite/sql/validate/SqlValidatorImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 5ad0b834aaae..b752dbda1980 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5928,8 +5928,7 @@ private SqlNode rewriteOuterAggregate(SqlNode originalQuery, final SqlNode scalarSubQuery = SqlStdOperatorTable.SCALAR_QUERY.createCall(newQuery.getParserPosition(), newQuery); // Rewrite to CASE WHEN (SELECT 1 FROM ... LIMIT 1) IS NOT NULL THEN END. - // This is type-agnostic (unlike multiplication, which only works for - // numeric aggregates): the scalar sub-query yields a non-null value when + // This is type-agnostic : the scalar sub-query yields a non-null value when // the inner query has rows, otherwise the whole expression is NULL, which // matches the semantics of the original scalar sub-query. final SqlParserPos pos = aggCall.getParserPosition(); From 9cb2ffc390ed0f8f68588b75e70bef93002fa425 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Sun, 5 Jul 2026 07:44:13 +0800 Subject: [PATCH 18/21] Addressed --- .../org/apache/calcite/sql/validate/SqlValidatorImpl.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index b752dbda1980..f06cbca1c2b4 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5927,10 +5927,7 @@ private SqlNode rewriteOuterAggregate(SqlNode originalQuery, validateQuery(newQuery, parentScope, unknownType); final SqlNode scalarSubQuery = SqlStdOperatorTable.SCALAR_QUERY.createCall(newQuery.getParserPosition(), newQuery); - // Rewrite to CASE WHEN (SELECT 1 FROM ... LIMIT 1) IS NOT NULL THEN END. - // This is type-agnostic : the scalar sub-query yields a non-null value when - // the inner query has rows, otherwise the whole expression is NULL, which - // matches the semantics of the original scalar sub-query. + final SqlParserPos pos = aggCall.getParserPosition(); final SqlNode condition = SqlStdOperatorTable.IS_NOT_NULL.createCall(pos, scalarSubQuery); From 050a953e186fb7a2fb30fabb255defdf6d4e2788 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Tue, 7 Jul 2026 10:25:33 +0800 Subject: [PATCH 19/21] Addressed --- .../sql/validate/SqlValidatorImpl.java | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index f06cbca1c2b4..802cac3eaabe 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5752,10 +5752,6 @@ private void checkNoCorrelatedAggregatesInSelectList(SqlSelect select) { if (subQuery == null) { continue; } - // Ensure the sub-query's scope is established before inspecting it. - deriveType(selectScope, - SqlStdOperatorTable.SCALAR_QUERY.createCall( - subQuery.getParserPosition(), subQuery)); final SqlCall aggCall = findCorrelatedAggregate(subQuery, selectScope); if (aggCall != null) { throw newValidationError(aggCall, @@ -5786,8 +5782,6 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, } final SqlBasicCall scalarSubQuery = (SqlBasicCall) expr; final SqlNode query = scalarSubQuery.operand(0); - // Validate the sub-query first so that its scope is established. - deriveType(parentScope, scalarSubQuery); final SqlNode rewrittenSubQuery = rewriteOuterAggregate(query, subQuery, parentScope); if (rewrittenSubQuery == query) { @@ -5979,22 +5973,11 @@ private static boolean containsSubQuery(SqlNode node) { final boolean[] found = {false}; node.accept(new SqlBasicVisitor() { @Override public Void visit(SqlCall call) { - switch (call.getKind()) { - case SCALAR_QUERY: - case EXISTS: - case IN: - case NOT_IN: - case SOME: - case ALL: - case MULTISET_QUERY_CONSTRUCTOR: - case ARRAY_QUERY_CONSTRUCTOR: - case MAP_QUERY_CONSTRUCTOR: - case CURSOR: + if (call.getKind().belongsTo(SqlKind.QUERY)) { found[0] = true; return null; - default: - return super.visit(call); } + return super.visit(call); } }); return found[0]; From 5a714dd21eeaa755aa663f99539e38745c45907f Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Wed, 8 Jul 2026 10:50:09 +0800 Subject: [PATCH 20/21] Addressed --- .../sql/validate/SqlValidatorImpl.java | 64 ++++++++----------- .../apache/calcite/test/SqlValidatorTest.java | 19 ++++++ core/src/test/resources/sql/agg.iq | 49 ++++++++++++++ 3 files changed, 95 insertions(+), 37 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 802cac3eaabe..b4a549c4cae9 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -5752,9 +5752,9 @@ private void checkNoCorrelatedAggregatesInSelectList(SqlSelect select) { if (subQuery == null) { continue; } - final SqlCall aggCall = findCorrelatedAggregate(subQuery, selectScope); - if (aggCall != null) { - throw newValidationError(aggCall, + final SqlNode aggExpr = findCorrelatedAggregate(subQuery, selectScope); + if (aggExpr != null) { + throw newValidationError(aggExpr, RESOURCE.correlatedAggregateNotAllowed()); } } @@ -5820,17 +5820,20 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, } /** - * Finds the aggregate function in a scalar sub-query's SELECT list whose - * arguments reference only columns from the outer query. Such an aggregate - * belongs to the outer query per the SQL standard. + * Finds the single select item of a scalar sub-query that can be lifted to + * the outer query. Such an item contains at least one aggregate function and + * references only columns from the outer query, so per the SQL standard it + * belongs to the outer query. * - *

    Returns {@code null} unless the sub-query has exactly one select item, - * that item is an aggregate call, and all of its arguments reference only - * outer columns. + *

    The item may be a bare aggregate call (e.g. {@code sum(a)}) or an + * expression over aggregates of outer columns (e.g. {@code sum(a) + sum(b)}). + * It is not lifted if any column reference resolves to an inner column, which + * covers mixed cases such as {@code sum(a) + sum(x)} where {@code x} is an + * inner column. * - * @return the aggregate call, or {@code null} if no such aggregate is found + * @return the liftable select item, or {@code null} if none applies */ - private @Nullable SqlCall findCorrelatedAggregate(SqlSelect subQuery, + private @Nullable SqlNode findCorrelatedAggregate(SqlSelect subQuery, SqlValidatorScope parentScope) { final SqlNodeList subSelectItems = SqlNonNullableAccessors.getSelectList(subQuery); if (subSelectItems.size() != 1) { @@ -5843,32 +5846,18 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, if (SqlUtil.isCallTo(subSelectItem, SqlStdOperatorTable.AS)) { subSelectItem = ((SqlCall) subSelectItem).operand(0); } - if (!(subSelectItem instanceof SqlCall)) { - return null; - } - final SqlCall aggCall = (SqlCall) subSelectItem; - if (!aggCall.getOperator().isAggregator()) { + if (aggFinder.findAgg(subSelectItem) == null) { return null; } final SqlValidatorScope subScope = getSelectScope(subQuery); - if (subScope == null) { - return null; - } - final SqlValidatorScope operandScope; - if (subScope instanceof AggregatingSelectScope) { - operandScope = ((AggregatingSelectScope) subScope).parent; - } else { - operandScope = subScope; - } - if (operandScope == null) { + final SqlValidatorScope operandScope = + subScope instanceof AggregatingSelectScope + ? ((AggregatingSelectScope) subScope).parent + : subScope; + if (!referencesOnlyOuterColumns(subSelectItem, parentScope, operandScope)) { return null; } - for (SqlNode operand : aggCall.getOperandList()) { - if (!referencesOnlyOuterColumns(operand, parentScope, operandScope)) { - return null; - } - } - return aggCall; + return subSelectItem; } /** @@ -5881,8 +5870,8 @@ private SqlNode rewriteOuterAggregateItem(SqlValidatorScope parentScope, */ private SqlNode rewriteOuterAggregate(SqlNode originalQuery, SqlSelect subQuery, SqlValidatorScope parentScope) { - final SqlCall aggCall = findCorrelatedAggregate(subQuery, parentScope); - if (aggCall == null) { + final SqlNode aggExpr = findCorrelatedAggregate(subQuery, parentScope); + if (aggExpr == null) { return originalQuery; } final SqlNodeList subSelectItems = SqlNonNullableAccessors.getSelectList(subQuery); @@ -5922,12 +5911,12 @@ private SqlNode rewriteOuterAggregate(SqlNode originalQuery, final SqlNode scalarSubQuery = SqlStdOperatorTable.SCALAR_QUERY.createCall(newQuery.getParserPosition(), newQuery); - final SqlParserPos pos = aggCall.getParserPosition(); + final SqlParserPos pos = aggExpr.getParserPosition(); final SqlNode condition = SqlStdOperatorTable.IS_NOT_NULL.createCall(pos, scalarSubQuery); return new SqlCase(pos, null, new SqlNodeList(ImmutableList.of(condition), pos), - new SqlNodeList(ImmutableList.of(aggCall), pos), + new SqlNodeList(ImmutableList.of(aggExpr), pos), SqlLiteral.createNull(pos)); } @@ -5984,7 +5973,8 @@ private static boolean containsSubQuery(SqlNode node) { } /** - * Returns whether an identifier refers to a scope outside the current select. + * Returns whether an identifier resolves to a scope which is not the + * supplied one. */ private boolean isOuterReference(SqlValidatorScope scope, SqlIdentifier id) { final SqlQualified fqId = scope.fullyQualify(id); 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 86ede1fb8a17..8db12222f35f 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8481,6 +8481,25 @@ void testGroupExpressionEquivalenceParams() { "select (select max(ename) from dept) from emp"; sql(nonNumeric).withConformance(SqlConformanceEnum.LENIENT).ok(); sql(nonNumeric).withConformance(SqlConformanceEnum.BABEL).ok(); + + // An expression over aggregates of outer columns is lifted to the outer + // query as a whole, and is subject to the same conformance rules as a bare + // aggregate. + final String twoAggs = + "select (select ^sum(sal) + sum(comm)^ from dept) from emp"; + sql(twoAggs).withConformance(SqlConformanceEnum.LENIENT).ok(); + sql(twoAggs).withConformance(SqlConformanceEnum.BABEL).ok(); + sql(twoAggs).withConformance(SqlConformanceEnum.DEFAULT).fails(err); + sql(twoAggs).withConformance(SqlConformanceEnum.STRICT_2003).fails(err); + + // Mixed case: an expression aggregating both an outer column (sal) and an + // inner column (deptno) references an inner column, so it is not lifted and + // is not rejected under any conformance level. + final String mixed = + "select (select sum(sal) + sum(deptno) from dept) from emp"; + sql(mixed).withConformance(SqlConformanceEnum.LENIENT).ok(); + sql(mixed).withConformance(SqlConformanceEnum.DEFAULT).ok(); + sql(mixed).withConformance(SqlConformanceEnum.STRICT_2003).ok(); } @Test void testAggregateInOrderByFails() { diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 57c76ccc0912..9e1b13ad574d 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4428,6 +4428,55 @@ FROM aa; !ok +# An expression over aggregates of different outer columns is lifted to the +# outer query as a whole: sum(a) = 1+2+3 = 6, sum(b) = 10+20+30 = 60, so the +# result is a single row 66. +WITH aa (a, b) AS (VALUES (1, 10), (2, 20), (3, 30)), + xx (x) AS (VALUES 100, 200, 300) +SELECT (SELECT sum(a) + sum(b) FROM xx LIMIT 1) AS s +FROM aa; ++----+ +| S | ++----+ +| 66 | ++----+ +(1 row) + +!ok + +# Lifted expression with an empty inner table: the guard sub-query yields no +# row, so the whole expression is NULL. +WITH aa (a, b) AS (VALUES (1, 10), (2, 20), (3, 30)), + empty_xx (x) AS (SELECT 1 FROM emp WHERE 1 = 0) +SELECT (SELECT sum(a) + sum(b) FROM empty_xx LIMIT 1) AS s +FROM aa; ++---+ +| S | ++---+ +| | ++---+ +(1 row) + +!ok + +# Mixed case: the expression aggregates an outer column (a) and an inner +# column (x). It references an inner column, so it is not lifted and keeps the +# pre-existing correlated-sub-query behaviour (evaluated once per outer row). +WITH aa (a) AS (VALUES 1, 2, 3), + xx (x) AS (VALUES 10, 20, 30) +SELECT (SELECT sum(a) + sum(x) FROM xx LIMIT 1) AS s +FROM aa; ++----+ +| S | ++----+ +| 63 | +| 66 | +| 69 | ++----+ +(3 rows) + +!ok + # Two scalar sub-queries with outer-only aggregates are both lifted to the outer query WITH aa (a) AS (VALUES 1, 2, 3), xx (x) AS (VALUES 10, 20, 30) From dad06755bbf477d29e95006365c6481ce7ff5680 Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Thu, 9 Jul 2026 09:35:19 +0800 Subject: [PATCH 21/21] Addressed --- core/src/test/resources/sql/agg.iq | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/test/resources/sql/agg.iq b/core/src/test/resources/sql/agg.iq index 9e1b13ad574d..b637a6cbc196 100644 --- a/core/src/test/resources/sql/agg.iq +++ b/core/src/test/resources/sql/agg.iq @@ -4444,6 +4444,21 @@ FROM aa; !ok +# A single aggregate over an arithmetic expression of outer columns is also +# lifted: sum(a + b) = (1+10) + (2+20) + (3+30) = 66. +WITH aa (a, b) AS (VALUES (1, 10), (2, 20), (3, 30)), + xx (x) AS (VALUES 100, 200, 300) +SELECT (SELECT sum(a + b) FROM xx LIMIT 1) AS s +FROM aa; ++----+ +| S | ++----+ +| 66 | ++----+ +(1 row) + +!ok + # Lifted expression with an empty inner table: the guard sub-query yields no # row, so the whole expression is NULL. WITH aa (a, b) AS (VALUES (1, 10), (2, 20), (3, 30)),