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 1be8d0b37a [CALCITE-7900] Query with DISTINCT ON crashes
1be8d0b37a is described below

commit 1be8d0b37a0e1139f4dc348a59d1816e85e02798
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Oct 29 11:40:52 2026 -0700

    [CALCITE-7900] Query with DISTINCT ON crashes
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 babel/src/test/resources/sql/select.iq             |  54 +++++++++++
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |  35 +++----
 .../apache/calcite/test/SqlToRelConverterTest.java |  49 ++++++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 101 +++++++++++++++++----
 4 files changed, 202 insertions(+), 37 deletions(-)

diff --git a/babel/src/test/resources/sql/select.iq 
b/babel/src/test/resources/sql/select.iq
index 73e9c5a541..5512dc515e 100755
--- a/babel/src/test/resources/sql/select.iq
+++ b/babel/src/test/resources/sql/select.iq
@@ -473,4 +473,58 @@ FROM "scott"."EMP" AS "EMP"
 ORDER BY "DEPTNO", "EMPNO"
 !explain-validated-on all
 
+# [CALCITE-7900] Query with DISTINCT ON crashes
+# Note: All results in this section have been verified against PostgreSQL and 
are identical
+
+# DISTINCT ON in a sub-query
+SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename, deptno
+  FROM emp
+  ORDER BY deptno, empno) AS t
+ORDER BY deptno;
++-------+-------+--------+
+| EMPNO | ENAME | DEPTNO |
++-------+-------+--------+
+|  7782 | CLARK |     10 |
+|  7369 | SMITH |     20 |
+|  7499 | ALLEN |     30 |
++-------+-------+--------+
+(3 rows)
+
+!ok
+
+# DISTINCT ON in a sub-query, ORDER BY columns not in the SELECT list
+SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename
+  FROM emp
+  ORDER BY deptno, sal DESC, empno) AS t
+ORDER BY empno;
++-------+-------+
+| EMPNO | ENAME |
++-------+-------+
+|  7698 | BLAKE |
+|  7788 | SCOTT |
+|  7839 | KING  |
++-------+-------+
+(3 rows)
+
+!ok
+
+# DISTINCT ON in a sub-query with LIMIT
+SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename, deptno
+  FROM emp
+  ORDER BY deptno DESC, sal, empno
+  LIMIT 2) AS t
+ORDER BY empno;
++-------+-------+--------+
+| EMPNO | ENAME | DEPTNO |
++-------+-------+--------+
+|  7369 | SMITH |     20 |
+|  7900 | JAMES |     30 |
++-------+-------+--------+
+(2 rows)
+
+!ok
+
 # End select.iq
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 7ff2a824a9..49953ced1f 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -831,13 +831,13 @@ protected void convertSelectImpl(
     }
 
     if (select.isDistinctOn()) {
-      convertOrder(select, bb, collation, orderExprList, null, null);
-    } else {
-      convertOrder(
-          select, bb, collation, orderExprList, select.getOffset(),
-          select.getFetch());
+      convertDistinctOn(bb, select, collationList);
     }
 
+    convertOrder(
+        select, bb, collation, orderExprList, select.getOffset(),
+        select.getFetch());
+
     if (select.hasHints()) {
       final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies, 
select.getHints());
       // Attach the hints to the first Hintable node we found from the root 
node.
@@ -857,20 +857,6 @@ protected void convertSelectImpl(
     } else {
       bb.setRoot(bb.root(), true);
     }
-
-    if (select.isDistinctOn()) {
-      convertDistinctOn(bb, select, collationList);
-      final @Nullable RexNode offsetExpr = select.getOffset() == null
-          ? null : convertExpression(select.getOffset());
-      final @Nullable RexNode fetchExpr = select.getFetch() == null
-          ? null : convertExpression(select.getFetch());
-      if (offsetExpr != null || fetchExpr != null) {
-        bb.setRoot(
-            LogicalSort.create(bb.root(), RelCollations.EMPTY,
-                offsetExpr, fetchExpr),
-            false);
-      }
-    }
   }
 
   /**
@@ -1154,6 +1140,8 @@ protected void convertOrder(
             || (offset instanceof SqlLiteral
                 && Objects.equals(((SqlLiteral) offset).bigDecimalValue(), 
BigDecimal.ZERO)))
           && fetch == null) {
+        // DISTINCT ON may have added ORDER BY expressions; remove them
+        removeOrderExprs(bb, orderExprList);
         return;
       }
     }
@@ -1165,6 +1153,12 @@ protected void convertOrder(
             fetch == null ? null : convertExpression(fetch)),
         false);
 
+    removeOrderExprs(bb, orderExprList);
+  }
+
+  /** Removes the expressions that were added to the project list only to
+   * sort the result. */
+  private void removeOrderExprs(Blackboard bb, List<SqlNode> orderExprList) {
     // If extra expressions were added to the project list for sorting,
     // add another project to remove them. But make the collation empty, 
because
     // we can't represent the real collation.
@@ -4304,7 +4298,8 @@ protected void gatherOrderExprs(
       return;
     }
 
-    if (removeSortInSubQuery(bb.top)) {
+    // DISTINCT ON needs the ORDER BY even if the sort is removed
+    if (removeSortInSubQuery(bb.top) && !select.isDistinctOn()) {
       SqlNode offset = select.getOffset();
       if ((offset == null
               || (offset instanceof SqlLiteral
diff --git 
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 74da0946d0..06f991c803 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -6973,4 +6973,53 @@ void checkUserDefinedOrderByOver(NullCollation 
nullCollation) {
         + "LIMIT 5";
     sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
   }
+
+  /** Test case of
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7900";>[CALCITE-7900]
+   * Query with DISTINCT ON crashes</a>. The sub-query drops its sort, but
+   * the window that implements DISTINCT ON keeps the ORDER BY keys. */
+  @Test void testDistinctOnInSubQuery() {
+    final String sql = "SELECT * FROM (\n"
+        + "  SELECT DISTINCT ON (deptno) empno, ename, deptno\n"
+        + "  FROM emp\n"
+        + "  ORDER BY deptno, empno) AS t\n"
+        + "ORDER BY deptno";
+    sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+  }
+
+  /** Test case of
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7900";>[CALCITE-7900]
+   * Query with DISTINCT ON crashes</a>. The ORDER BY keys are not in the
+   * SELECT list, so the sub-query must remove them after DISTINCT ON. */
+  @Test void testDistinctOnInSubQueryOrderByNotInSelect() {
+    final String sql = "SELECT * FROM (\n"
+        + "  SELECT DISTINCT ON (deptno) empno, ename\n"
+        + "  FROM emp\n"
+        + "  ORDER BY deptno, sal DESC) AS t";
+    sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+  }
+
+  /** As {@link #testDistinctOnInSubQueryOrderByNotInSelect()}, but the
+   * sub-query keeps its sort. */
+  @Test void testDistinctOnInSubQueryKeepSort() {
+    final String sql = "SELECT * FROM (\n"
+        + "  SELECT DISTINCT ON (deptno) empno, ename\n"
+        + "  FROM emp\n"
+        + "  ORDER BY deptno, sal DESC) AS t";
+    sql(sql).withConformance(SqlConformanceEnum.LENIENT)
+        .withConfig(c -> c.withRemoveSortInSubQuery(false)).ok();
+  }
+
+  /** Test case of
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7900";>[CALCITE-7900]
+   * Query with DISTINCT ON crashes</a>. The Sort that applies LIMIT must keep
+   * the ORDER BY collation; otherwise it can keep any 2 rows. */
+  @Test void testDistinctOnInSubQueryWithLimit() {
+    final String sql = "SELECT * FROM (\n"
+        + "  SELECT DISTINCT ON (deptno) empno, ename\n"
+        + "  FROM emp\n"
+        + "  ORDER BY deptno DESC, sal\n"
+        + "  LIMIT 2) AS t";
+    sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+  }
 }
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index b793a776ba..22aeace682 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -2101,6 +2101,77 @@ LogicalTableModify(table=[[CATALOG, SALES, EMP]], 
operation=[DELETE], flattened=
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
     LogicalFilter(condition=[=($7, 10)])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDistinctOnInSubQuery">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename, deptno
+  FROM emp
+  ORDER BY deptno, empno) AS t
+ORDER BY deptno]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalSort(sort0=[$2], dir0=[ASC])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2])
+    LogicalFilter(condition=[=($3, 1)])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], $f3=[ROW_NUMBER() 
OVER (PARTITION BY $7 ORDER BY $7 NULLS LAST, $0 NULLS LAST)])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDistinctOnInSubQueryKeepSort">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename
+  FROM emp
+  ORDER BY deptno, sal DESC) AS t]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1])
+  LogicalSort(sort0=[$2], sort1=[$3], dir0=[ASC], dir1=[DESC])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], SAL=[$3])
+      LogicalFilter(condition=[=($4, 1)])
+        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], SAL=[$5], 
$f4=[ROW_NUMBER() OVER (PARTITION BY $7 ORDER BY $7 NULLS LAST, $5 DESC NULLS 
FIRST)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDistinctOnInSubQueryOrderByNotInSelect">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename
+  FROM emp
+  ORDER BY deptno, sal DESC) AS t]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1])
+  LogicalFilter(condition=[=($4, 1)])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], SAL=[$5], 
$f4=[ROW_NUMBER() OVER (PARTITION BY $7 ORDER BY $7 NULLS LAST, $5 DESC NULLS 
FIRST)])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDistinctOnInSubQueryWithLimit">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM (
+  SELECT DISTINCT ON (deptno) empno, ename
+  FROM emp
+  ORDER BY deptno DESC, sal
+  LIMIT 2) AS t]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1])
+  LogicalSort(sort0=[$2], sort1=[$3], dir0=[DESC], dir1=[ASC], fetch=[2])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], SAL=[$3])
+      LogicalFilter(condition=[=($4, 1)])
+        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], SAL=[$5], 
$f4=[ROW_NUMBER() OVER (PARTITION BY $7 ORDER BY $7 DESC NULLS FIRST, $5 NULLS 
LAST)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>
@@ -2113,12 +2184,11 @@ ORDER BY deptno, job, hiredate DESC]]>
     <Resource name="plan">
       <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1])
-  LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], JOB=[$3], HIREDATE=[$4])
-    LogicalFilter(condition=[=($5, 1)])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], JOB=[$3], 
HIREDATE=[$4], $f5=[ROW_NUMBER() OVER (PARTITION BY $2, $3 ORDER BY $2 NULLS 
LAST, $3 NULLS LAST, $4 DESC NULLS FIRST)])
-        LogicalSort(sort0=[$2], sort1=[$3], sort2=[$4], dir0=[ASC], 
dir1=[ASC], dir2=[DESC])
-          LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], JOB=[$2], 
HIREDATE=[$4])
-            LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalSort(sort0=[$2], sort1=[$3], sort2=[$4], dir0=[ASC], dir1=[ASC], 
dir2=[DESC])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], JOB=[$3], 
HIREDATE=[$4])
+      LogicalFilter(condition=[=($5, 1)])
+        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], JOB=[$2], 
HIREDATE=[$4], $f5=[ROW_NUMBER() OVER (PARTITION BY $7, $2 ORDER BY $7 NULLS 
LAST, $2 NULLS LAST, $4 DESC NULLS FIRST)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>
@@ -2131,12 +2201,11 @@ ORDER BY deptno, empno]]>
     <Resource name="plan">
       <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1])
-  LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2])
-    LogicalFilter(condition=[=($3, 1)])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], $f3=[ROW_NUMBER() 
OVER (PARTITION BY $2 ORDER BY $2 NULLS LAST, $0 NULLS LAST)])
-        LogicalSort(sort0=[$2], sort1=[$0], dir0=[ASC], dir1=[ASC])
-          LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])
-            LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalSort(sort0=[$2], sort1=[$0], dir0=[ASC], dir1=[ASC])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2])
+      LogicalFilter(condition=[=($3, 1)])
+        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], $f3=[ROW_NUMBER() 
OVER (PARTITION BY $7 ORDER BY $7 NULLS LAST, $0 NULLS LAST)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>
@@ -2150,13 +2219,11 @@ LIMIT 5]]>
     <Resource name="plan">
       <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1])
-  LogicalSort(fetch=[5])
+  LogicalSort(sort0=[$2], sort1=[$0], dir0=[ASC], dir1=[ASC], fetch=[5])
     LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2])
       LogicalFilter(condition=[=($3, 1)])
-        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2], $f3=[ROW_NUMBER() 
OVER (PARTITION BY $2 ORDER BY $2 NULLS LAST, $0 NULLS LAST)])
-          LogicalSort(sort0=[$2], sort1=[$0], dir0=[ASC], dir1=[ASC])
-            LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7])
-              LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], $f3=[ROW_NUMBER() 
OVER (PARTITION BY $7 ORDER BY $7 NULLS LAST, $0 NULLS LAST)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>

Reply via email to