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

mihaibudiu 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 7ccb3d1404 [CALCITE-7744] ORDER BY agg(col) on a query where an alias 
shadows col produces an invalid plan containing an aggregate call in a Project
7ccb3d1404 is described below

commit 7ccb3d14048be90f37e57f45a8449cc4f5b23f6b
Author: 1fanwang <[email protected]>
AuthorDate: Thu Oct 29 10:40:20 2026 -0700

    [CALCITE-7744] ORDER BY agg(col) on a query where an alias shadows col 
produces an invalid plan containing an aggregate call in a Project
    
    Signed-off-by: 1fanwang <[email protected]>
---
 .../apache/calcite/runtime/CalciteResource.java    |  3 +
 .../apache/calcite/sql/validate/OrderByScope.java  | 23 +++++++
 .../calcite/sql/validate/SqlValidatorImpl.java     | 28 +++++++--
 .../calcite/runtime/CalciteResource.properties     |  1 +
 .../java/org/apache/calcite/test/JdbcTest.java     | 61 ++++++++++++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 72 ++++++++++++++++++++++
 6 files changed, 182 insertions(+), 6 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 00e7667f80..093fcb4e50 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -409,6 +409,9 @@ ExInst<SqlValidatorException> 
naturalOrUsingColumnNotCompatible(String a0,
   @BaseMessage("Aggregate expressions cannot be nested")
   ExInst<SqlValidatorException> nestedAggIllegal();
 
+  @BaseMessage("Aggregate expressions cannot be nested: ''{0}'' is a 
SELECT-list alias for an aggregate expression")
+  ExInst<SqlValidatorException> orderByAliasNestedAgg(String a0);
+
   @BaseMessage("Measure expressions can only occur within AGGREGATE function")
   ExInst<SqlValidatorException> measureIllegal();
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java 
b/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
index b8e58222ad..025cce3b04 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/OrderByScope.java
@@ -18,6 +18,7 @@
 
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
@@ -27,6 +28,8 @@
 
 import java.util.List;
 
+import static org.apache.calcite.sql.validate.SqlValidatorUtil.flatten;
+
 /**
  * Represents the name-resolution context for expressions in an ORDER BY 
clause.
  *
@@ -93,6 +96,26 @@ public class OrderByScope extends DelegatingScope {
 
   @Override public void validateExpr(SqlNode expr) {
     SqlNode expanded = validator.expandOrderExpr(select, expr);
+    expanded.accept(
+        new AggVisitor(validator.getOperatorTable(), false, true,
+            false, null, validator.catalogReader.nameMatcher()) {
+          @Override public Void visit(SqlCall call) {
+            switch (call.getKind()) {
+            case FILTER:
+            case WITHIN_DISTINCT:
+              return found(call);
+            default:
+              return super.visit(call);
+            }
+          }
+
+          @Override protected Void found(SqlCall call) {
+            final SqlValidatorUtil.FlatAggregate flat = flatten(call);
+            validator.validateAggregateParams(flat.aggregateCall, flat.filter,
+                flat.distinctList, flat.orderList, OrderByScope.this);
+            return null;
+          }
+        });
 
     // expression needs to be valid in parent scope too
     parent.validateExpr(expanded);
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 7f57f0a144..837d90b1d2 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
@@ -7951,13 +7951,27 @@ private SqlNode navigationInDefine(SqlNode node, String 
alpha) {
     }
 
     for (SqlNode param : aggCall.getOperandList()) {
-      if (a.findAgg(param) != null) {
+      final SqlCall nestedAgg = a.findAgg(param);
+      if (nestedAgg != null) {
+        if (scope instanceof OrderByScope) {
+          for (SqlNode node
+              : SqlUtil.getAncestry(param, n -> n == nestedAgg, n -> false)) {
+            final SqlNode original = getOriginal(node);
+            if (original instanceof SqlIdentifier
+                && ((SqlIdentifier) original).isSimple()) {
+              throw newValidationError(getOriginal(aggCall),
+                  RESOURCE.orderByAliasNestedAgg(((SqlIdentifier) 
original).getSimple()));
+            }
+          }
+        }
         throw newValidationError(aggCall, RESOURCE.nestedAggIllegal());
       }
     }
     if (filter != null) {
       if (a.findAgg(filter) != null) {
-        throw newValidationError(filter, RESOURCE.aggregateInFilterIllegal());
+        throw newValidationError(
+            scope instanceof OrderByScope ? getOriginal(filter) : filter,
+            RESOURCE.aggregateInFilterIllegal());
       }
     }
     if (distinctList != null) {
@@ -8818,9 +8832,9 @@ private SqlNode nthSelectItem(int ordinal, final 
SqlParserPos pos) {
         final SqlNameMatcher nameMatcher = catalogReader.nameMatcher();
         RelDataTypeField field = nameMatcher.field(rowType, alias);
         if (field != null) {
-          return nthSelectItem(
-              field.getIndex(),
-              id.getParserPosition());
+          final SqlNode expanded = nthSelectItem(field.getIndex(), 
id.getParserPosition());
+          setOriginal(expanded, id);
+          return expanded;
         }
       }
 
@@ -8850,7 +8864,9 @@ private SqlNode nthSelectItem(int ordinal, final 
SqlParserPos pos) {
       // operand[1] is a field name, not a column reference, and must not be 
qualified.
       CallCopyingArgHandler argHandler = new CallCopyingArgHandler(call, 
false);
       call.getOperator().acceptCall(this, call, true, argHandler);
-      return argHandler.result();
+      final SqlNode expanded = argHandler.result();
+      setOriginal(expanded, call);
+      return expanded;
     }
   }
 
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 2e6e56fb6b..43c7e1db1b 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -143,6 +143,7 @@ NotSelectDistinctExpr=Expression ''{0}'' is not in the 
select clause
 AggregateIllegalInClause=Aggregate expression is illegal in {0} clause
 WindowedAggregateIllegalInClause=Windowed aggregate expression is illegal in 
{0} clause
 NestedAggIllegal=Aggregate expressions cannot be nested
+OrderByAliasNestedAgg=Aggregate expressions cannot be nested: ''{0}'' is a 
SELECT-list alias for an aggregate expression
 MeasureIllegal=Measure expressions can only occur within AGGREGATE function
 MeasureMustBeInAggregateQuery=Measure expressions can only occur within a 
GROUP BY query
 AggregateInFilterIllegal=FILTER must not contain aggregate expression
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index c782728ef6..e95a29ed1b 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -3462,6 +3462,67 @@ void testJoinWithTrimmingConfigs(boolean 
enableTrimmingByConfig,
             + "store_id=0; grocery_sqft=null\n");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7744";>[CALCITE-7744]</a>
+   * ORDER BY agg(col) on a query where an alias shadows col produces an 
invalid
+   * plan containing an aggregate call in a Project. */
+  @Test void testOrderByAggregateAliasShadowingRejected() {
+    CalciteAssert.that()
+        .with(CalciteAssert.Config.SCOTT)
+        .query("SELECT max(sal) AS sal, deptno, job "
+            + "FROM emp GROUP BY deptno, job ORDER BY max(sal)")
+        .throws_("Aggregate expressions cannot be nested: 'SAL' is a "
+            + "SELECT-list alias for an aggregate expression");
+  }
+
+  static Stream<Arguments> orderByAliasQueries() {
+    return Stream.of(
+        arguments("select deptno in (select deptno from dept) as b from emp 
order by b",
+            String.join("", Collections.nCopies(14, "B=true\n"))),
+        arguments("select exists (select 1 from dept d where d.deptno = 
e.deptno) as b "
+                + "from emp e order by b",
+            String.join("", Collections.nCopies(14, "B=true\n"))),
+        arguments("select sal > all (select sal from emp) as b from emp order 
by b",
+            String.join("", Collections.nCopies(14, "B=false\n"))),
+        arguments("select deptno + 1 as d1, d1 + 2 as measure d3 from emp 
order by d3 + 1",
+            String.join("", Collections.nCopies(3, "D1=11; D3=13\n"))
+                + String.join("", Collections.nCopies(5, "D1=21; D3=23\n"))
+                + String.join("", Collections.nCopies(6, "D1=31; D3=33\n"))));
+  }
+
+  @ParameterizedTest
+  @MethodSource("orderByAliasQueries")
+  void testOrderByAliasExecutesValidatedExpressions(String query, String 
expected) {
+    CalciteAssert.that()
+        .with(CalciteAssert.Config.SCOTT)
+        .query(query)
+        .returns(expected);
+  }
+
+  @Test void testOrderByAliasExecutesBoundParameter() {
+    CalciteAssert.that()
+        .with(CalciteAssert.Config.SCOTT)
+        .query("select sal + ? as x from emp order by x + 1")
+        .consumesPreparedStatement(p -> p.setInt(1, 1))
+        .returns2("X=801\nX=951\nX=1101\nX=1251\nX=1251\nX=1301\nX=1501\n"
+            + "X=1601\nX=2451\nX=2851\nX=2976\nX=3001\nX=3001\nX=5001\n");
+  }
+
+  @Test void testOrderByAliasExecutesCurrentDate() throws SQLException {
+    try (Connection connection = CalciteAssert.that()
+        .with(CalciteAssert.Config.SCOTT).connect();
+        Statement statement = connection.createStatement();
+        ResultSet resultSet =
+            statement.executeQuery("select current_date as d, empno from emp 
order by d")) {
+      assertThat(resultSet.getMetaData().getColumnType(1), is(Types.DATE));
+      int count = 0;
+      while (resultSet.next()) {
+        assertNotNull(resultSet.getDate(1));
+        ++count;
+      }
+      assertThat(count, is(14));
+    }
+  }
+
   /** Tests ORDER BY ... DESC. Nulls come first (they come last for ASC). */
   @Test void testOrderByDesc() {
     CalciteAssert.that()
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 deda4bb7d3..ba5d9b02de 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -85,6 +85,8 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -5070,6 +5072,10 @@ private void checkNegWindow(String s, String msg) {
     // various kinds of measure expressions
     sql("select deptno, empno + 1 as measure e1 from emp").ok();
     sql("select *, empno + 1 as measure e1 from emp").ok();
+    // Guards against validating measure expressions in the SELECT scope when 
expanding ORDER BY,
+    // which would fail because measures can reference aliases (e.g. d1) 
defined in SELECT.
+    sql("select deptno + 1 as d1, d1 + 2 as measure d3\n"
+        + "from emp order by d3").ok();
 
     // an aggregate function in a measure does not make it an aggregate query
     sql("select *, sum(empno) as measure e1 from emp").ok();
@@ -8588,6 +8594,72 @@ void testGroupExpressionEquivalenceParams() {
         .fails(ERR_NESTED_AGG);
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7744";>[CALCITE-7744]</a>
+   * ORDER BY agg(col) on a query where an alias shadows col produces an 
invalid
+   * plan containing an aggregate call in a Project. */
+  @ParameterizedTest
+  @ValueSource(strings = {
+      "^max(sal)^",
+      "^max(sal)^ desc",
+      "sal + ^max(sal)^",
+      "^sum(sal + 1)^",
+      "^max(1 + sal)^",
+      "^max(sal + sal)^",
+      "^max(\"SAL\")^ nulls last"
+  })
+  void testOrderByAggregateAliasShadowingRejected(String orderBy) {
+    final String error = "Aggregate expressions cannot be nested: 'SAL' is a "
+        + "SELECT-list alias for an aggregate expression";
+    sql("select max(sal) as sal, deptno, job from emp\n"
+        + "group by deptno, job order by " + orderBy)
+        .fails(error);
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {
+      "^max(sal)^",
+      "^max(2 * sal)^",
+      "case when true then ^sum(sal + 1)^ else 0 end"
+  })
+  void testOrderByAliasRejectsAggregateExpressions(String orderBy) {
+    sql("select max(sal) + 1 as sal, deptno, job from emp\n"
+        + "group by deptno, job order by " + orderBy)
+        .fails("Aggregate expressions cannot be nested: 'SAL' is a "
+            + "SELECT-list alias for an aggregate expression");
+  }
+
+  @Test void testOrderByAggregateAliasFilterRejected() {
+    sql("select max(sal) as sal, deptno, job from emp\n"
+        + "group by deptno, job order by count(*) filter (where ^sal > 1000^)")
+        .fails("FILTER must not contain aggregate expression");
+    sql("select max(sal) as m, deptno, job from emp\n"
+        + "group by deptno, job order by max(sal)").ok();
+  }
+
+  @Test void testOrderByAggregateAliasWithinDistinctRejected() {
+    sql("select max(sal) as m, deptno from emp group by deptno\n"
+        + "order by ^sum(sal)^ within distinct (m)")
+        .fails("WITHIN DISTINCT must not contain aggregate expression");
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {
+      "select deptno in (select deptno from dept) as b from emp order by b",
+      "select exists (select 1 from dept d where d.deptno = e.deptno) as b "
+          + "from emp e order by b",
+      "select sal > all (select sal from emp) as b from emp order by b",
+      "select sal + ? as x from emp order by x + 1",
+      "select current_date as d, empno from emp order by d",
+      "select deptno + 1 as d1, d1 + 2 as measure d3 from emp order by d3 + 1",
+      "select (select min(deptno) from dept) as b from emp order by b",
+      "select sal + ? as x from emp order by 1 desc",
+      "select current_timestamp as d, empno from emp order by 1",
+      "select deptno + 1 as d1, d1 + 2 as measure d3 from emp order by d3"
+  })
+  void testOrderByAliasPreservesValidatedExpressions(String query) {
+    sql(query).ok();
+  }
+
   @Test void testNestedAggOver() {
     sql("select sum(max(empno))\n"
         + " OVER (order by ^deptno^ ROWS 2 PRECEDING)\n"

Reply via email to