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

xuzifu666 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new e15c3954b6 [CALCITE-6036] Support WITHIN GROUP(ORDER BY x) OVER 
(PARTITION BY y)
e15c3954b6 is described below

commit e15c3954b643b054f3fb8a6b485af12f9bff25ed
Author: Yu Xu <[email protected]>
AuthorDate: Sun Jul 5 07:19:29 2026 +0800

    [CALCITE-6036] Support WITHIN GROUP(ORDER BY x) OVER (PARTITION BY y)
---
 babel/src/test/resources/sql/within-group-over.iq  | 93 ++++++++++++++++++++++
 .../apache/calcite/rel/rel2sql/SqlImplementor.java | 36 ++++++++-
 .../org/apache/calcite/sql/SqlOverOperator.java    | 26 +++++-
 .../sql/validate/SqlAbstractConformance.java       |  4 +
 .../calcite/sql/validate/SqlConformance.java       | 18 +++++
 .../calcite/sql/validate/SqlConformanceEnum.java   |  9 +++
 .../sql/validate/SqlDelegatingConformance.java     |  4 +
 .../apache/calcite/sql/validate/SqlValidator.java  | 23 +++++-
 .../calcite/sql/validate/SqlValidatorImpl.java     | 13 ++-
 .../apache/calcite/sql2rel/SqlToRelConverter.java  | 26 +++++-
 .../java/org/apache/calcite/tools/RelBuilder.java  |  9 +++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 86 ++++++++++++++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 27 +++++++
 site/_docs/reference.md                            | 22 +++++
 14 files changed, 384 insertions(+), 12 deletions(-)

diff --git a/babel/src/test/resources/sql/within-group-over.iq 
b/babel/src/test/resources/sql/within-group-over.iq
new file mode 100644
index 0000000000..82df38fcd8
--- /dev/null
+++ b/babel/src/test/resources/sql/within-group-over.iq
@@ -0,0 +1,93 @@
+# within-group-over.iq - WITHIN GROUP (ORDER BY) OVER (PARTITION BY)
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Tests for the non-standard (Oracle) syntax that lets an aggregate function
+# with a "WITHIN GROUP (ORDER BY ...)" sort key also carry an "OVER (...)"
+# clause, so that it behaves as an analytic (window) function. The WITHIN GROUP
+# order key orders the aggregate's input but does not restrict the window 
frame:
+# the aggregate is computed over the whole partition and broadcast to every row
+# (matching Oracle). This syntax is enabled by the BABEL conformance
+# (SqlConformance.allowWithinGroupOverAggregate).
+#
+!use scott-babel
+!set outputformat mysql
+
+# The following 3 tests are related to this issue.
+# Results were validated on Oracle.
+
+# LISTAGG as an analytic function, partitioned by deptno. The result is the 
same
+# for every row of the partition (broadcast), which distinguishes the analytic
+# form from a per-row accumulating window.
+select deptno,
+    listagg(ename, ',') within group (order by ename)
+        over (partition by deptno) as names
+from emp
+where deptno in (10, 20)
+order by deptno, ename;
++--------+------------------------------+
+| DEPTNO | NAMES                        |
++--------+------------------------------+
+|     10 | CLARK,KING,MILLER            |
+|     10 | CLARK,KING,MILLER            |
+|     10 | CLARK,KING,MILLER            |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
++--------+------------------------------+
+(8 rows)
+
+!ok
+
+# The WITHIN GROUP order key controls the concatenation order (here 
descending),
+# independently of the OVER partition.
+select deptno,
+    listagg(ename, ',') within group (order by ename desc)
+        over (partition by deptno) as names
+from emp
+where deptno = 10
+order by ename;
++--------+-------------------+
+| DEPTNO | NAMES             |
++--------+-------------------+
+|     10 | MILLER,KING,CLARK |
+|     10 | MILLER,KING,CLARK |
+|     10 | MILLER,KING,CLARK |
++--------+-------------------+
+(3 rows)
+
+!ok
+
+# Contrast: the aggregate (GROUP BY) form collapses each group to a single row.
+select deptno,
+    listagg(ename, ',') within group (order by ename) as names
+from emp
+where deptno in (10, 20)
+group by deptno
+order by deptno;
++--------+------------------------------+
+| DEPTNO | NAMES                        |
++--------+------------------------------+
+|     10 | CLARK,KING,MILLER            |
+|     20 | ADAMS,FORD,JONES,SCOTT,SMITH |
++--------+------------------------------+
+(2 rows)
+
+!ok
+
+# End within-group-over.iq
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java 
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 403d7f6e2f..d8c02a56f3 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -105,6 +105,7 @@
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.Optionality;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.RangeSets;
 import org.apache.calcite.util.Sarg;
@@ -1119,6 +1120,23 @@ private SqlCall toSql(@Nullable RexProgram program, 
RexOver rexOver) {
       for (RexFieldCollation rfc : rexWindow.orderKeys) {
         addOrderItem(orderNodes, program, rfc);
       }
+
+      SqlAggFunction sqlAggregateFunction = rexOver.getAggOperator();
+
+      // Inverse distribution functions such as PERCENTILE_CONT/DISC take their
+      // sort key from a "WITHIN GROUP (ORDER BY ...)" clause rather than the
+      // window's ORDER BY, as in
+      // "PERCENTILE_CONT(x) WITHIN GROUP (ORDER BY y) OVER (PARTITION BY z)".
+      // Route the window's order keys into a WITHIN GROUP wrapper and leave 
the
+      // OVER clause with only the partition.
+      final SqlNodeList groupOrderList;
+      if (sqlAggregateFunction.requiresGroupOrder() == Optionality.MANDATORY
+          && !orderNodes.isEmpty()) {
+        groupOrderList = new SqlNodeList(orderNodes, POS);
+        orderNodes = Expressions.list();
+      } else {
+        groupOrderList = null;
+      }
       final SqlNodeList orderList =
           new SqlNodeList(orderNodes, POS);
 
@@ -1132,8 +1150,6 @@ private SqlCall toSql(@Nullable RexProgram program, 
RexOver rexOver) {
       // "disallow partial" and set the allowPartial = false.
       final SqlLiteral allowPartial = null;
 
-      SqlAggFunction sqlAggregateFunction = rexOver.getAggOperator();
-
       SqlNode lowerBound = null;
       SqlNode upperBound = null;
       SqlLiteral exclude = toSql(rexWindow.getExclude());
@@ -1149,15 +1165,22 @@ private SqlCall toSql(@Nullable RexProgram program, 
RexOver rexOver) {
 
       final List<SqlNode> nodeList = toSql(program, rexOver.getOperands());
       return createOverCall(sqlAggregateFunction, nodeList, sqlWindow,
-          rexOver.isDistinct(), rexOver.ignoreNulls());
+          rexOver.isDistinct(), rexOver.ignoreNulls(), groupOrderList);
     }
 
     private static SqlCall createOverCall(SqlAggFunction op, List<SqlNode> 
operands,
         SqlWindow window, boolean isDistinct, boolean ignoreNulls) {
+      return createOverCall(op, operands, window, isDistinct, ignoreNulls, 
null);
+    }
+
+    private static SqlCall createOverCall(SqlAggFunction op, List<SqlNode> 
operands,
+        SqlWindow window, boolean isDistinct, boolean ignoreNulls,
+        @Nullable SqlNodeList groupOrderList) {
       if (op instanceof SqlSumEmptyIsZeroAggFunction) {
         // Rewrite "SUM0(x) OVER w" to "COALESCE(SUM(x) OVER w, 0)"
         final SqlCall node =
-            createOverCall(SqlStdOperatorTable.SUM, operands, window, 
isDistinct, ignoreNulls);
+            createOverCall(SqlStdOperatorTable.SUM, operands, window, 
isDistinct, ignoreNulls,
+                groupOrderList);
         return SqlStdOperatorTable.COALESCE.createCall(POS, node, ZERO);
       }
       SqlCall aggFunctionCall;
@@ -1171,6 +1194,11 @@ private static SqlCall createOverCall(SqlAggFunction op, 
List<SqlNode> operands,
         aggFunctionCall =
             SqlStdOperatorTable.IGNORE_NULLS.createCall(null, POS, 
aggFunctionCall);
       }
+      if (groupOrderList != null && !groupOrderList.isEmpty()) {
+        aggFunctionCall =
+            SqlStdOperatorTable.WITHIN_GROUP.createCall(POS, aggFunctionCall,
+                groupOrderList);
+      }
       return SqlStdOperatorTable.OVER.createCall(POS, aggFunctionCall,
           window);
     }
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java 
b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
index b1ed13808d..1ec62f3309 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
@@ -78,6 +78,19 @@ public SqlOverOperator() {
     default:
       break;
     }
+    // Support "agg WITHIN GROUP (ORDER BY ...) OVER (...)" for inverse
+    // distribution functions such as PERCENTILE_CONT/PERCENTILE_DISC. The
+    // WITHIN GROUP wrapper carries the sort key, and the underlying operand
+    // is the actual aggregate. This is non-standard (Oracle) syntax, gated by
+    // conformance; otherwise the WITHIN GROUP call is not an aggregator and 
the
+    // overNonAggregate error below fires.
+    SqlNodeList groupOrderList = null;
+    if (aggCall.getKind() == SqlKind.WITHIN_GROUP
+        && validator.config().conformance().allowWithinGroupOverAggregate()) {
+      validator.validateCall(aggCall, scope);
+      groupOrderList = aggCall.operand(1);
+      aggCall = aggCall.operand(0);
+    }
     if (!aggCall.getOperator().isAggregator()) {
       throw validator.newValidationError(aggCall, RESOURCE.overNonAggregate());
     }
@@ -87,7 +100,7 @@ public SqlOverOperator() {
       throw validator.newValidationError(aggCall, RESOURCE.overNonAggregate());
     }
     final SqlNode window = call.operand(1);
-    validator.validateWindow(window, scope, aggCall);
+    validator.validateWindow(window, scope, aggCall, groupOrderList);
   }
 
   @Override public RelDataType deriveType(
@@ -113,6 +126,17 @@ public SqlOverOperator() {
     SqlWindow w = validator.resolveWindow(window, scope);
 
     SqlCall aggCall = (SqlCall) agg;
+    // "agg WITHIN GROUP (ORDER BY ...) OVER (...)": the WITHIN GROUP wrapper
+    // has already derived the correct return type (e.g. the collation column
+    // type for PERCENTILE_CONT/DISC via SqlWithinGroupOperator.deriveType), so
+    // reuse it rather than re-inferring from the bare aggregate call, which
+    // would fail because the sort key is not available to the aggregate alone.
+    if (aggCall.getKind() == SqlKind.WITHIN_GROUP) {
+      RelDataType ret = validator.deriveType(scope, aggCall);
+      validator.setValidatedNodeType(call, ret);
+      validator.setValidatedNodeType(agg, ret);
+      return ret;
+    }
     // Unwrap FILTER, RESPECT_NULLS, or IGNORE_NULLS to get the actual 
aggregate call
     while (aggCall != null
         && (aggCall.getKind() == SqlKind.FILTER
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 9ef7559c53..84cf77e3c9 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
@@ -33,6 +33,10 @@ public abstract class SqlAbstractConformance implements 
SqlConformance {
     return SqlConformanceEnum.DEFAULT.allowCharLiteralAlias();
   }
 
+  @Override public boolean allowWithinGroupOverAggregate() {
+    return SqlConformanceEnum.DEFAULT.allowWithinGroupOverAggregate();
+  }
+
   @Override public boolean isSupportedDualTable() {
     return SqlConformanceEnum.DEFAULT.isSupportedDualTable();
   }
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 3ccc860a0d..db0dd693a9 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
@@ -89,6 +89,24 @@ public interface SqlConformance {
    */
   boolean allowCharLiteralAlias();
 
+  /**
+   * Whether to allow an inverse distribution function such as
+   * {@code PERCENTILE_CONT} or {@code PERCENTILE_DISC} to combine a
+   * {@code WITHIN GROUP (ORDER BY ...)} clause with an {@code OVER} clause, so
+   * that it may be used as an analytic (window) function. For example,
+   *
+   * <blockquote><pre>
+   *   PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)
+   *     OVER (PARTITION BY y)</pre></blockquote>
+   *
+   * <p>This is non-standard SQL supported by Oracle.
+   *
+   * <p>Among the built-in conformance levels, true in
+   * {@link SqlConformanceEnum#BABEL};
+   * false otherwise.
+   */
+  boolean allowWithinGroupOverAggregate();
+
   /**
    * Whether to allow aliases from the {@code SELECT} clause to be used as
    * column names in the {@code GROUP BY} clause.
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 fd083560e0..4ed6cb93c2 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
@@ -103,6 +103,15 @@ public enum SqlConformanceEnum implements SqlConformance {
     }
   }
 
+  @Override public boolean allowWithinGroupOverAggregate() {
+    switch (this) {
+    case BABEL:
+      return true;
+    default:
+      return false;
+    }
+  }
+
   @Override public boolean isSupportedDualTable() {
     switch (this) {
     case MYSQL_5:
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 daac4b5c41..e142893891 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
@@ -39,6 +39,10 @@ protected SqlDelegatingConformance(SqlConformance delegate) {
     return delegate.allowCharLiteralAlias();
   }
 
+  @Override public boolean allowWithinGroupOverAggregate() {
+    return delegate.allowWithinGroupOverAggregate();
+  }
+
   @Override public boolean isSupportedDualTable() {
     return delegate.isSupportedDualTable();
   }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index 813247376d..94b90c6789 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -290,10 +290,31 @@ void validateQuery(SqlNode node, SqlValidatorScope scope,
    * @param call       the SqlNode if a function call if the window is attached
    *                   to one.
    */
+  default void validateWindow(
+      SqlNode windowOrId,
+      SqlValidatorScope scope,
+      @Nullable SqlCall call) {
+    validateWindow(windowOrId, scope, call, null);
+  }
+
+  /**
+   * Validates a window clause where the windowed function carries an inline
+   * {@code WITHIN GROUP (ORDER BY ...)} sort key, as in
+   * {@code PERCENTILE_CONT(x) WITHIN GROUP (ORDER BY y) OVER (PARTITION BY 
z)}.
+   *
+   * @param windowOrId    SqlNode that can be either SqlWindow with all the
+   *                      components of a window spec or a SqlIdentifier with 
the
+   *                      name of a window spec.
+   * @param scope         Naming scope
+   * @param call          the aggregate function call the window is attached 
to.
+   * @param groupOrderList the {@code WITHIN GROUP} order list carried by the
+   *                       aggregate, or null if there is none.
+   */
   void validateWindow(
       SqlNode windowOrId,
       SqlValidatorScope scope,
-      @Nullable SqlCall call);
+      @Nullable SqlCall call,
+      @Nullable SqlNodeList groupOrderList);
 
   /** Returns whether the validator is currently validating within a window
    * expression. */
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 2897202926..d8fd81a827 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
@@ -6604,7 +6604,8 @@ public void setOriginal(SqlNode expr, SqlNode original) {
   @Override public void validateWindow(
       SqlNode windowOrId,
       SqlValidatorScope scope,
-      @Nullable SqlCall call) {
+      @Nullable SqlCall call,
+      @Nullable SqlNodeList groupOrderList) {
     // Enable nested aggregates with window aggregates (OVER operator)
     inWindow = true;
 
@@ -6627,9 +6628,15 @@ public void setOriginal(SqlNode expr, SqlNode original) {
     targetWindow.setWindowCall(call);
     targetWindow.validate(this, scope);
     targetWindow.setWindowCall(null);
-    call.validate(this, scope);
+    if (groupOrderList == null) {
+      // A bare "PERCENTILE_CONT(x) WITHIN GROUP (ORDER BY y)" call has already
+      // been validated by SqlWithinGroupOperator, so re-validating the naked
+      // aggregate here would fail (it needs the WITHIN GROUP sort key to
+      // derive its type). Only validate when there is no group order list.
+      call.validate(this, scope);
+    }
 
-    validateAggregateParams(call, null, null, null, scope);
+    validateAggregateParams(call, null, null, groupOrderList, scope);
 
     // Disable nested aggregates post validation
     inWindow = false;
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java 
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 240b8bd02b..f8b661c53f 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -2527,6 +2527,15 @@ private RexNode convertOver(Blackboard bb, SqlNode node) 
{
     default:
       break;
     }
+    // "agg WITHIN GROUP (ORDER BY ...) OVER (...)": the WITHIN GROUP sort key
+    // (used by inverse distribution functions such as PERCENTILE_CONT/DISC) is
+    // carried as the window's ORDER BY. Oracle forbids ORDER BY inside the 
OVER
+    // clause for these functions, so the window's own order list is empty 
here.
+    @Nullable SqlNodeList groupOrderList = null;
+    if (aggCall.getKind() == SqlKind.WITHIN_GROUP) {
+      groupOrderList = aggCall.operand(1);
+      aggCall = aggCall.operand(0);
+    }
     if (filter != null) {
       final SqlOperator op = aggCall.getOperator();
       if (op instanceof SqlAggFunction
@@ -2551,9 +2560,20 @@ private RexNode convertOver(Blackboard bb, SqlNode node) 
{
     SqlNode sqlLowerBound = window.getLowerBound();
     SqlNode sqlUpperBound = window.getUpperBound();
     boolean rows = window.isRows();
-    SqlNodeList orderList = window.getOrderList();
-
-    if (!aggCall.getOperator().allowsFraming()) {
+    // For "agg WITHIN GROUP (ORDER BY ...) OVER (...)", the sort key comes 
from
+    // the WITHIN GROUP clause rather than the window's own (empty) ORDER BY.
+    SqlNodeList orderList =
+        groupOrderList != null ? groupOrderList : window.getOrderList();
+
+    if (groupOrderList != null) {
+      // For "agg WITHIN GROUP (ORDER BY ...) OVER (...)", the sort key orders
+      // the aggregate's input but does not restrict the window frame: the
+      // aggregate is computed over the whole partition and broadcast to every
+      // row (matching Oracle). Force a full-partition frame so that framing
+      // aggregates such as LISTAGG do not accumulate row by row.
+      sqlLowerBound = SqlWindow.createUnboundedPreceding(SqlParserPos.ZERO);
+      sqlUpperBound = SqlWindow.createUnboundedFollowing(SqlParserPos.ZERO);
+    } else if (!aggCall.getOperator().allowsFraming()) {
       // If the operator does not allow framing, bracketing is implicitly
       // everything up to the current row.
       sqlLowerBound = SqlWindow.createUnboundedPreceding(SqlParserPos.ZERO);
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java 
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 54d662a3a1..36d56a9f04 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -5063,6 +5063,15 @@ private OverCall 
orderBy_(ImmutableList<RexFieldCollation> sortKeys) {
             @Override public boolean hasEmptyGroup() {
               return !SqlWindow.isAlwaysNonEmpty(lowerBound, upperBound);
             }
+
+            @Override public RelDataType getCollationType() {
+              // Inverse distribution functions such as PERCENTILE_CONT/DISC
+              // used as analytic functions ("... WITHIN GROUP (ORDER BY x)
+              // OVER (...)") derive their return type from the sort key.
+              checkArgument(!sortKeys.isEmpty(),
+                  "collation type requested but no sort key present");
+              return sortKeys.get(0).left.getType();
+            }
           };
       final RelDataType type = op.inferReturnType(bind);
       final RexNode over = getRexBuilder()
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 1d3dda5af6..95bbe98f6a 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -8275,6 +8275,92 @@ private void checkLiteral2(String expression, String 
expected) {
     sql(query).ok(expected);
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6036";>[CALCITE-6036]
+   * Support WITHIN GROUP (ORDER BY x) OVER (PARTITION BY y)</a>. Checks that 
an
+   * inverse distribution function used as an analytic function is unparsed 
with
+   * the sort key inside a {@code WITHIN GROUP} clause and the partition inside
+   * the {@code OVER} clause. */
+  @Test void testWithinGroupOver() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("EMP")
+        .project(
+            b.aggregateCall(SqlStdOperatorTable.PERCENTILE_CONT, 
b.literal(0.5))
+                .over()
+                .partitionBy(b.field("DEPTNO"))
+                .orderBy(b.field("SAL"))
+                .rowsUnbounded()
+                .allowPartial(true)
+                .nullWhenCountZero(false)
+                .as("c"))
+        .build();
+    final String expected = "SELECT PERCENTILE_CONT(5E-1) "
+        + "WITHIN GROUP (ORDER BY \"SAL\") "
+        + "OVER (PARTITION BY \"DEPTNO\") AS \"c\"\n"
+        + "FROM \"scott\".\"EMP\"";
+    relFn(relFn).ok(expected);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6036";>[CALCITE-6036]
+   * Support WITHIN GROUP (ORDER BY x) OVER (PARTITION BY y)</a>. Checks that
+   * expressions (rather than plain column references) under both the WITHIN
+   * GROUP order key and the OVER partition key are unparsed correctly. */
+  @Test void testWithinGroupOverWithExpressions() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("EMP")
+        .project(
+            b.aggregateCall(SqlStdOperatorTable.PERCENTILE_CONT, 
b.literal(0.5))
+                .over()
+                .partitionBy(
+                    b.call(SqlStdOperatorTable.PLUS, b.field("DEPTNO"),
+                        b.literal(1)))
+                .orderBy(
+                    b.call(SqlStdOperatorTable.MULTIPLY, b.field("SAL"),
+                        b.literal(2)))
+                .rowsUnbounded()
+                .allowPartial(true)
+                .nullWhenCountZero(false)
+                .as("c"))
+        .build();
+    final String expected = "SELECT PERCENTILE_CONT(5E-1) "
+        + "WITHIN GROUP (ORDER BY \"SAL\" * 2) "
+        + "OVER (PARTITION BY \"DEPTNO\" + 1) AS \"c\"\n"
+        + "FROM \"scott\".\"EMP\"";
+    relFn(relFn).ok(expected);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6036";>[CALCITE-6036]
+   * Support WITHIN GROUP (ORDER BY x) OVER (PARTITION BY y)</a>. Checks
+   * unparsing with multiple partition and sort expressions, including a
+   * descending sort key. */
+  @Test void testWithinGroupOverWithMultipleExpressions() {
+    final Function<RelBuilder, RelNode> relFn = b -> b
+        .scan("EMP")
+        .project(
+            b.aggregateCall(SqlStdOperatorTable.PERCENTILE_DISC, 
b.literal(0.5))
+                .over()
+                .partitionBy(
+                    b.field("DEPTNO"),
+                    b.call(SqlStdOperatorTable.PLUS, b.field("MGR"),
+                        b.literal(1)))
+                .orderBy(
+                    b.desc(
+                        b.call(SqlStdOperatorTable.MINUS, b.field("SAL"),
+                            b.field("COMM"))))
+                .rowsUnbounded()
+                .allowPartial(true)
+                .nullWhenCountZero(false)
+                .as("c"))
+        .build();
+    final String expected = "SELECT PERCENTILE_DISC(5E-1) "
+        + "WITHIN GROUP (ORDER BY \"SAL\" - \"COMM\" DESC) "
+        + "OVER (PARTITION BY \"DEPTNO\", \"MGR\" + 1) AS \"c\"\n"
+        + "FROM \"scott\".\"EMP\"";
+    relFn(relFn).ok(expected);
+  }
+
   @Test void testJsonValueExpressionOperator() {
     String query = "select \"product_name\" format json, "
         + "\"product_name\" format json encoding utf8, "
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 123dbe01d0..094e0447b1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -8549,6 +8549,33 @@ void testGroupExpressionEquivalenceParams() {
         .type("RecordType(INTEGER NOT NULL C, INTEGER NOT NULL D) NOT NULL");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6036";>[CALCITE-6036]
+   * Support WITHIN GROUP (ORDER BY x) OVER (PARTITION BY y)</a>. Combining a
+   * WITHIN GROUP clause with an OVER clause is non-standard (Oracle) syntax 
that
+   * is only allowed under a conformance that enables it, such as BABEL. */
+  @Test void testPercentileWithinGroupOver() {
+    final String sql = "select\n"
+        + " percentile_cont(0.25) within group (order by sal)\n"
+        + "   over (partition by deptno) as c\n"
+        + "from emp";
+    // Enabled under BABEL conformance.
+    sql(sql)
+        .withConformance(SqlConformanceEnum.BABEL)
+        .type("RecordType(INTEGER NOT NULL C) NOT NULL");
+  }
+
+  @Test void testPercentileWithinGroupOverFailsInDefaultConformance() {
+    final String sql = "select\n"
+        + " ^percentile_cont(0.25) within group (order by sal)^\n"
+        + "   over (partition by deptno) as c\n"
+        + "from emp";
+    // Rejected under the default conformance, which does not allow WITHIN 
GROUP
+    // to be combined with an OVER clause.
+    sql(sql)
+        .fails("OVER must be applied to aggregate function");
+  }
+
   /** Tests that {@code PERCENTILE_CONT} only allows numeric fields. */
   @Test void testPercentileContMustOrderByNumeric() {
     final String sql = "select\n"
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 064d6fb95e..90511aa406 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2166,6 +2166,28 @@ ### Window functions
 `DISTINCT`, `FILTER` and `WITHIN GROUP` are as described for aggregate
 functions.
 
+#### WITHIN GROUP clause in window functions
+
+Combining a `WITHIN GROUP (ORDER BY ...)` clause with an `OVER` clause lets an
+aggregate function whose ordering is supplied by `WITHIN GROUP` be used as a
+window function, as in
+
+{% highlight sql %}
+LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) OVER (PARTITION BY deptno)
+{% endhighlight %}
+
+The `WITHIN GROUP` order key orders the function's input but does not restrict
+the window frame: the function is computed over the whole partition and the
+result is broadcast to every row of that partition. Because the sort key is
+supplied by `WITHIN GROUP`, the `OVER` clause must not contain its own
+`ORDER BY` or frame specification.
+
+This is non-standard syntax (supported by Oracle) and is only allowed under a
+conformance that returns true for
+[SqlConformance.allowWithinGroupOverAggregate()]({{ site.apiRoot 
}}/org/apache/calcite/sql/validate/SqlConformance.html#allowWithinGroupOverAggregate--),
+such as `BABEL`; otherwise the validator reports "OVER must be applied to
+aggregate function".
+
 #### FILTER clause in window functions
 
 When `FILTER` is used with window functions, it is applied in the following 
order:

Reply via email to