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 eaadc32db8 [CALCITE-5406] Support the SELECT DISTINCT ON statement for
PostgreSQL dialect
eaadc32db8 is described below
commit eaadc32db821186e4a887b804099c2dbdd96878d
Author: Yu Xu <[email protected]>
AuthorDate: Mon May 11 16:54:46 2026 +0800
[CALCITE-5406] Support the SELECT DISTINCT ON statement for PostgreSQL
dialect
---
babel/src/main/codegen/config.fmpp | 1 +
.../java/org/apache/calcite/test/BabelTest.java | 36 ++++++
babel/src/test/resources/sql/select.iq | 86 ++++++++++++++
core/src/main/codegen/config.fmpp | 5 +-
core/src/main/codegen/default_config.fmpp | 2 +
core/src/main/codegen/templates/Parser.jj | 31 ++++-
.../apache/calcite/runtime/CalciteResource.java | 9 ++
.../java/org/apache/calcite/sql/SqlSelect.java | 54 +++++++--
.../org/apache/calcite/sql/SqlSelectOperator.java | 13 ++-
.../sql/validate/SqlAbstractConformance.java | 4 +
.../calcite/sql/validate/SqlConformance.java | 10 ++
.../calcite/sql/validate/SqlConformanceEnum.java | 10 ++
.../sql/validate/SqlDelegatingConformance.java | 4 +
.../calcite/sql/validate/SqlValidatorImpl.java | 53 ++++++++-
.../apache/calcite/sql2rel/SqlToRelConverter.java | 98 +++++++++++++++-
.../calcite/runtime/CalciteResource.properties | 3 +
core/src/test/codegen/config.fmpp | 1 +
.../apache/calcite/test/SqlToRelConverterTest.java | 31 +++++
.../org/apache/calcite/test/SqlValidatorTest.java | 125 ++++++++++++++++++++-
.../apache/calcite/test/SqlToRelConverterTest.xml | 56 +++++++++
server/src/main/codegen/config.fmpp | 1 +
site/_docs/reference.md | 21 +++-
.../apache/calcite/sql/parser/SqlParserTest.java | 98 ++++++++++++++++
23 files changed, 733 insertions(+), 19 deletions(-)
diff --git a/babel/src/main/codegen/config.fmpp
b/babel/src/main/codegen/config.fmpp
index 30c2ce7d65..a4e0e9d1ce 100644
--- a/babel/src/main/codegen/config.fmpp
+++ b/babel/src/main/codegen/config.fmpp
@@ -618,6 +618,7 @@ data: {
includeParsingStringLiteralAsArrayLiteral: true
includeIntervalWithoutQualifier: true
includeSelectBy: true
+ includeDistinctOn: true
}
}
diff --git a/babel/src/test/java/org/apache/calcite/test/BabelTest.java
b/babel/src/test/java/org/apache/calcite/test/BabelTest.java
index 1deda157fa..6103d61ed4 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelTest.java
@@ -594,6 +594,41 @@ private void checkSqlResult(String funLibrary, String
query, String result) {
.returns(result);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement</a>. */
+ @Test void testDistinctOn() {
+ final SqlValidatorFixture v = Fixtures.forValidator()
+ .withParserConfig(c -> c.withParserFactory(SqlBabelParserImpl.FACTORY))
+ .withConformance(SqlConformanceEnum.BABEL);
+
+ // Basic DISTINCT ON
+ v.withSql("select distinct on (deptno) empno, ename from emp order by
deptno, empno")
+ .ok();
+
+ // DISTINCT ON with multiple columns
+ v.withSql("select distinct on (deptno, job) empno, ename from emp order by
deptno, job, empno")
+ .ok();
+
+ // DISTINCT ON with expression
+ v.withSql("select distinct on (deptno) empno, sal * 12 as annual_sal "
+ + "from emp order by deptno, sal desc").ok();
+
+ // DISTINCT ON without ORDER BY should fail
+ v.withSql("^select distinct on (deptno) empno from emp^")
+ .fails("SELECT DISTINCT ON requires an ORDER BY clause");
+
+ // DISTINCT ON with ORDER BY mismatch should fail
+ v.withSql("select distinct on (deptno) empno from emp order by ^empno^")
+ .fails("SELECT DISTINCT ON expressions must match ORDER BY
expressions");
+
+ // DISTINCT and DISTINCT ON are mutually exclusive
+ v.withSql("SELECT DISTINCT ^DISTINCT^ ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, empno")
+ .fails("(?s)Incorrect syntax near the keyword 'DISTINCT' at line 1,
column 17.*");
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-7337">[CALCITE-7337]
* Add age function (enabled in PostgreSQL library)</a>. */
@@ -637,4 +672,5 @@ private void checkSqlResult(String funLibrary, String
query, String result) {
.query("SELECT AGE(timestamp '2023-12-25') FROM (VALUES (1)) t")
.runs();
}
+
}
diff --git a/babel/src/test/resources/sql/select.iq
b/babel/src/test/resources/sql/select.iq
index f94905c16e..60af913f14 100755
--- a/babel/src/test/resources/sql/select.iq
+++ b/babel/src/test/resources/sql/select.iq
@@ -332,4 +332,90 @@ from emp e join dept d on e.deptno = d.deptno;
SELECT * REPLACE list contains unknown column(s): DEPTNO
!error
+# [CALCITE-5406] Support the SELECT DISTINCT ON statement for PostgreSQL
dialect
+# Note: All results in this section have been verified against PostgreSQL and
are identical
+
+# Test basic DISTINCT ON
+SELECT DISTINCT ON (deptno) empno, ename, deptno
+FROM emp
+ORDER BY deptno, empno;
++-------+-------+--------+
+| EMPNO | ENAME | DEPTNO |
++-------+-------+--------+
+| 7782 | CLARK | 10 |
+| 7369 | SMITH | 20 |
+| 7499 | ALLEN | 30 |
++-------+-------+--------+
+(3 rows)
+
+!ok
+
+# Test DISTINCT ON with descending order
+SELECT DISTINCT ON (deptno) empno, ename, sal, deptno
+FROM emp
+ORDER BY deptno, sal DESC, empno;
++-------+-------+---------+--------+
+| EMPNO | ENAME | SAL | DEPTNO |
++-------+-------+---------+--------+
+| 7839 | KING | 5000.00 | 10 |
+| 7788 | SCOTT | 3000.00 | 20 |
+| 7698 | BLAKE | 2850.00 | 30 |
++-------+-------+---------+--------+
+(3 rows)
+
+!ok
+
+# Test DISTINCT ON with multiple columns
+SELECT DISTINCT ON (deptno, job) empno, ename, deptno, job
+FROM emp
+ORDER BY deptno, job, empno;
++-------+--------+--------+-----------+
+| EMPNO | ENAME | DEPTNO | JOB |
++-------+--------+--------+-----------+
+| 7934 | MILLER | 10 | CLERK |
+| 7782 | CLARK | 10 | MANAGER |
+| 7839 | KING | 10 | PRESIDENT |
+| 7788 | SCOTT | 20 | ANALYST |
+| 7369 | SMITH | 20 | CLERK |
+| 7566 | JONES | 20 | MANAGER |
+| 7900 | JAMES | 30 | CLERK |
+| 7698 | BLAKE | 30 | MANAGER |
+| 7499 | ALLEN | 30 | SALESMAN |
++-------+--------+--------+-----------+
+(9 rows)
+
+!ok
+
+# Test DISTINCT ON with window function
+SELECT DISTINCT ON (deptno) empno, ename, ROW_NUMBER() OVER (ORDER BY sal) as
rn
+FROM emp
+ORDER BY deptno, empno;
++-------+-------+----+
+| EMPNO | ENAME | RN |
++-------+-------+----+
+| 7782 | CLARK | 9 |
+| 7369 | SMITH | 1 |
+| 7499 | ALLEN | 8 |
++-------+-------+----+
+(3 rows)
+
+!ok
+
+# Test DISTINCT and DISTINCT ON are mutually exclusive
+SELECT DISTINCT DISTINCT ON (deptno) empno, ename, deptno
+FROM emp
+ORDER BY deptno, empno;
+Incorrect syntax near the keyword 'DISTINCT'
+!error
+
+# Test DISTINCT ON unparse
+SELECT DISTINCT ON (deptno) empno, ename, deptno
+FROM emp
+ORDER BY deptno, empno;
+
+SELECT DISTINCT ON ("DEPTNO") "EMP"."EMPNO", "EMP"."ENAME", "EMP"."DEPTNO"
+FROM "scott"."EMP" AS "EMP"
+ORDER BY "DEPTNO", "EMPNO"
+!explain-validated-on all
+
# End select.iq
diff --git a/core/src/main/codegen/config.fmpp
b/core/src/main/codegen/config.fmpp
index 73d981bf39..d8286c4ce1 100644
--- a/core/src/main/codegen/config.fmpp
+++ b/core/src/main/codegen/config.fmpp
@@ -40,8 +40,9 @@ data: {
# FMPP will use the declaration from default_config.fmpp.
parser: {
# Generated parser implementation package and class name.
- package: "org.apache.calcite.sql.parser.impl",
- class: "SqlParserImpl",
+ package: "org.apache.calcite.sql.parser.impl"
+ class: "SqlParserImpl"
+ includeDistinctOn: true
# List of files in @includes directory that have parser method
# implementations for parsing custom SQL statements, literals or types
diff --git a/core/src/main/codegen/default_config.fmpp
b/core/src/main/codegen/default_config.fmpp
index a2547273cb..9cb0542337 100644
--- a/core/src/main/codegen/default_config.fmpp
+++ b/core/src/main/codegen/default_config.fmpp
@@ -460,4 +460,6 @@ parser: {
includeAdditionalDeclarations: false
includeParsingStringLiteralAsArrayLiteral: false
includeIntervalWithoutQualifier: false
+ includeStarExclude: false
+ includeDistinctOn: false
}
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 4bbf49840c..3b2209538e 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -1023,6 +1023,24 @@ List<SqlNode> FunctionParameterList(ExprContext
exprContext) :
}
}
+void AllOrDistinctOrDistinctOn(List<SqlLiteral> keywords, List<SqlNode>
distinctOnList) :
+{
+ final Span s;
+ final SqlNodeList distinctOn;
+}
+{
+ <ALL> { keywords.add(SqlSelectKeyword.ALL.symbol(getPos())); }
+|
+ <DISTINCT> { s = span(); }
+ (
+ <ON> <LPAREN>
+ distinctOn = ExpressionCommaList(s, ExprContext.ACCEPT_SUB_QUERY)
+ <RPAREN>
+ { distinctOnList.addAll(distinctOn.getList()); }
+ )?
+ { keywords.add(SqlSelectKeyword.DISTINCT.symbol(s.end(this))); }
+}
+
SqlLiteral AllOrDistinct() :
{
}
@@ -1389,6 +1407,7 @@ SqlSelect SqlSelect() :
final SqlNode qualify;
final SqlNodeList by;
final List<SqlNode> hints = new ArrayList<SqlNode>();
+ final List<SqlNode> distinctOnList = new ArrayList<SqlNode>();
final Span s;
}
{
@@ -1400,9 +1419,15 @@ SqlSelect SqlSelect() :
keywords.add(SqlSelectKeyword.STREAM.symbol(getPos()));
}
)?
+<#if parser.includeDistinctOn!default.parser.includeDistinctOn>
+ (
+ AllOrDistinctOrDistinctOn(keywords, distinctOnList)
+ )?
+<#else>
(
keyword = AllOrDistinct() { keywords.add(keyword); }
)?
+</#if>
{
keywordList = new SqlNodeList(keywords, s.addAll(keywords).pos());
}
@@ -1431,10 +1456,14 @@ SqlSelect SqlSelect() :
}
)
{
+ final SqlNodeList distinctOn = distinctOnList.isEmpty()
+ ? null
+ : new SqlNodeList(distinctOnList, Span.of(distinctOnList).pos());
final SqlSelect select = new SqlSelect(s.end(this), keywordList,
new SqlNodeList(selectList, Span.of(selectList).pos()),
fromClause, where, groupBy, having, windowDecls, qualify,
- null, null, null, new SqlNodeList(hints, getPos()));
+ null, null, null, new SqlNodeList(hints, getPos()),
+ distinctOn);
SqlByRewriter.rewrite(select, by);
return select;
}
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 13745bf0df..171a8da68a 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -508,6 +508,15 @@ ExInst<SqlValidatorException>
intervalFieldExceedsPrecision(Number a0,
@BaseMessage("QUALIFY expression ''{0}'' must contain a window function")
ExInst<SqlValidatorException>
qualifyExpressionMustContainWindowFunction(String a0);
+ @BaseMessage("SELECT DISTINCT ON is not supported under the current SQL
conformance level")
+ ExInst<SqlValidatorException> distinctOnNotAllowed();
+
+ @BaseMessage("SELECT DISTINCT ON requires an ORDER BY clause")
+ ExInst<SqlValidatorException> distinctOnRequiresOrderBy();
+
+ @BaseMessage("SELECT DISTINCT ON expressions must match ORDER BY
expressions")
+ ExInst<SqlValidatorException> distinctOnOrderByMismatch();
+
@BaseMessage("ROW/RANGE not allowed with RANK, DENSE_RANK, ROW_NUMBER,
PERCENTILE_CONT/DISC or LAG/LEAD functions")
ExInst<SqlValidatorException> rankWithFrame();
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
index 0faa1d024f..ad542c31a0 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelect.java
@@ -43,6 +43,7 @@ public class SqlSelect extends SqlCall {
public static final int WHERE_OPERAND = 3;
public static final int HAVING_OPERAND = 5;
public static final int QUALIFY_OPERAND = 7;
+ public static final int DISTINCT_ON_OPERAND = 12;
SqlNodeList keywordList;
SqlNodeList selectList;
@@ -56,6 +57,7 @@ public class SqlSelect extends SqlCall {
@Nullable SqlNode offset;
@Nullable SqlNode fetch;
@Nullable SqlNodeList hints;
+ @Nullable SqlNodeList distinctOn;
boolean hasByClause;
//~ Constructors -----------------------------------------------------------
@@ -72,7 +74,8 @@ public SqlSelect(SqlParserPos pos,
@Nullable SqlNodeList orderBy,
@Nullable SqlNode offset,
@Nullable SqlNode fetch,
- @Nullable SqlNodeList hints) {
+ @Nullable SqlNodeList hints,
+ @Nullable SqlNodeList distinctOn) {
super(pos);
this.keywordList = requireNonNull(keywordList != null
? keywordList : new SqlNodeList(pos));
@@ -88,10 +91,29 @@ public SqlSelect(SqlParserPos pos,
this.offset = offset;
this.fetch = fetch;
this.hints = hints;
+ this.distinctOn = distinctOn;
this.hasByClause = false;
}
- /** deprecated, without {@code qualify}. */
+ /** Constructor without {@code distinctOn}; distinctOn defaults to null. */
+ public SqlSelect(SqlParserPos pos,
+ @Nullable SqlNodeList keywordList,
+ SqlNodeList selectList,
+ @Nullable SqlNode from,
+ @Nullable SqlNode where,
+ @Nullable SqlNodeList groupBy,
+ @Nullable SqlNode having,
+ @Nullable SqlNodeList windowDecls,
+ @Nullable SqlNode qualify,
+ @Nullable SqlNodeList orderBy,
+ @Nullable SqlNode offset,
+ @Nullable SqlNode fetch,
+ @Nullable SqlNodeList hints) {
+ this(pos, keywordList, selectList, from, where, groupBy, having,
+ windowDecls, qualify, orderBy, offset, fetch, hints, null);
+ }
+
+ /** deprecated, without {@code qualify} and {@code distinctOn}. */
@Deprecated // to be removed before 2.0
public SqlSelect(SqlParserPos pos,
@Nullable SqlNodeList keywordList,
@@ -106,7 +128,7 @@ public SqlSelect(SqlParserPos pos,
@Nullable SqlNode fetch,
@Nullable SqlNodeList hints) {
this(pos, keywordList, selectList, from, where, groupBy, having,
- windowDecls, null, orderBy, offset, fetch, hints);
+ windowDecls, null, orderBy, offset, fetch, hints, null);
}
//~ Methods ----------------------------------------------------------------
@@ -122,7 +144,8 @@ public SqlSelect(SqlParserPos pos,
@SuppressWarnings("nullness")
@Override public List<SqlNode> getOperandList() {
return ImmutableNullableList.of(keywordList, selectList, from, where,
- groupBy, having, windowDecls, qualify, orderBy, offset, fetch, hints);
+ groupBy, having, windowDecls, qualify, orderBy, offset, fetch, hints,
+ distinctOn);
}
@Override public void setOperand(int i, @Nullable SqlNode operand) {
@@ -160,12 +183,23 @@ public SqlSelect(SqlParserPos pos,
case 10:
fetch = operand;
break;
+ case 11:
+ hints = (SqlNodeList) operand;
+ break;
+ case 12:
+ distinctOn = (SqlNodeList) operand;
+ break;
default:
throw new AssertionError(i);
}
}
public final boolean isDistinct() {
+ // DISTINCT ON is mutually exclusive with DISTINCT, so when DISTINCT ON is
present,
+ // we return false to indicate that standard DISTINCT processing is not
needed
+ if (isDistinctOn()) {
+ return false;
+ }
return getModifierNode(SqlSelectKeyword.DISTINCT) != null;
}
@@ -273,10 +307,6 @@ public void setFetch(@Nullable SqlNode fetch) {
this.fetch = fetch;
}
- public void setHints(@Nullable SqlNodeList hints) {
- this.hints = hints;
- }
-
@Pure
public @Nullable SqlNodeList getHints() {
return this.hints;
@@ -327,4 +357,12 @@ public boolean hasWhere() {
public boolean isKeywordPresent(SqlSelectKeyword targetKeyWord) {
return getModifierNode(targetKeyWord) != null;
}
+
+ public boolean isDistinctOn() {
+ return distinctOn != null && !distinctOn.isEmpty();
+ }
+
+ public @Nullable SqlNodeList getDistinctOn() {
+ return distinctOn;
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
index fea1c2235f..1c1a8edfe8 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSelectOperator.java
@@ -78,7 +78,8 @@ private SqlSelectOperator() {
(SqlNodeList) operands[8],
operands[9],
operands[10],
- (SqlNodeList) operands[11]);
+ (SqlNodeList) operands[11],
+ operands.length > 12 ? (SqlNodeList) operands[12] : null);
}
/**
@@ -114,7 +115,8 @@ public SqlSelect createCall(
orderBy,
offset,
fetch,
- hints);
+ hints,
+ null);
}
@Override public <R> void acceptCall(
@@ -150,6 +152,13 @@ public SqlSelect createCall(
final SqlNode keyword = select.keywordList.get(i);
keyword.unparse(writer, 0, 0);
}
+ if (select.isDistinctOn()) {
+ writer.keyword("ON");
+ final SqlWriter.Frame frame =
+ writer.startList("(", ")");
+ castNonNull(select.distinctOn).unparse(writer, 0, 0);
+ writer.endList(frame);
+ }
writer.topN(select.fetch, select.offset);
final SqlNodeList selectClause = select.selectList;
writer.list(SqlWriter.FrameTypeEnum.SELECT_LIST, SqlWriter.COMMA,
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 b19d8c071d..82f06a48d1 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
@@ -172,4 +172,8 @@ public abstract class SqlAbstractConformance implements
SqlConformance {
@Override public boolean supportsUnsignedTypes() {
return SqlConformanceEnum.DEFAULT.supportsUnsignedTypes();
}
+
+ @Override public boolean isDistinctOnAllowed() {
+ return SqlConformanceEnum.DEFAULT.isDistinctOnAllowed();
+ }
}
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 fcd3b46641..32b4a03b90 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
@@ -681,4 +681,14 @@ default boolean isColonFieldAccessAllowed() {
* True when the unsigned versions of integer types are supported.
*/
boolean supportsUnsignedTypes();
+
+ /**
+ * Whether {@code SELECT DISTINCT ON} is allowed.
+ *
+ * <p>Among the built-in conformance levels, true in
+ * {@link SqlConformanceEnum#BABEL},
+ * {@link SqlConformanceEnum#LENIENT};
+ * false otherwise.
+ */
+ boolean isDistinctOnAllowed();
}
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 f0f258171b..4475c4ce80 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
@@ -532,4 +532,14 @@ public enum SqlConformanceEnum implements SqlConformance {
return false;
}
}
+
+ @Override public boolean isDistinctOnAllowed() {
+ 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 25f8d7e037..0d415d8aec 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
@@ -177,4 +177,8 @@ protected SqlDelegatingConformance(SqlConformance delegate)
{
@Override public boolean supportsUnsignedTypes() {
return delegate.supportsUnsignedTypes();
}
+
+ @Override public boolean isDistinctOnAllowed() {
+ return delegate.isDistinctOnAllowed();
+ }
}
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 1e2b298808..a41be8c67b 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
@@ -3267,8 +3267,10 @@ private void registerQuery(
if (orderList != null) {
// If the query is 'SELECT DISTINCT', restrict the columns
// available to the ORDER BY clause.
+ // DISTINCT ON is an exception: ORDER BY may reference columns
+ // not in the SELECT list.
final SqlValidatorScope selectScope3 =
- select.isDistinct()
+ (select.isDistinct() && !select.isDistinctOn())
? new AggregatingSelectScope(selectScope, select, true)
: selectScope2;
OrderByScope orderScope =
@@ -4463,6 +4465,7 @@ protected void validateSelect(
// dialects you can refer to columns of the select list, e.g.
// "SELECT empno AS x FROM emp ORDER BY x"
validateOrderList(select);
+ validateDistinctOnClause(select);
if (shouldCheckForRollUp(from)) {
checkRollUpInSelectList(select);
@@ -4997,6 +5000,54 @@ protected void validateQualifyClause(SqlSelect select) {
}
}
+ protected void validateDistinctOnClause(SqlSelect select) {
+ SqlNodeList distinctOn = select.getDistinctOn();
+ if (distinctOn == null || distinctOn.isEmpty()) {
+ return;
+ }
+
+ if (!config.conformance().isDistinctOnAllowed()) {
+ throw newValidationError(select, RESOURCE.distinctOnNotAllowed());
+ }
+
+ SqlNodeList orderList = select.getOrderList();
+ if (orderList == null || orderList.isEmpty()) {
+ throw newValidationError(select, RESOURCE.distinctOnRequiresOrderBy());
+ }
+
+ if (orderList.size() < distinctOn.size()) {
+ throw newValidationError(orderList.get(orderList.size() - 1),
+ RESOURCE.distinctOnOrderByMismatch());
+ }
+
+ final SqlValidatorScope orderScope = getOrderScope(select);
+ for (int i = 0; i < distinctOn.size(); i++) {
+ SqlNode distinctOnExpr = expand(distinctOn.get(i), orderScope);
+ SqlNode orderItem = orderList.get(i);
+ SqlNode orderExpr = stripOrderByModifiers(orderItem);
+ orderExpr = expand(orderExpr, orderScope);
+ if (!SqlNode.equalDeep(distinctOnExpr, orderExpr, Litmus.IGNORE)) {
+ throw newValidationError(orderItem,
RESOURCE.distinctOnOrderByMismatch());
+ }
+ }
+ }
+
+ /** Strips ASC, DESC, NULLS FIRST, NULLS LAST from an ORDER BY item. */
+ private static SqlNode stripOrderByModifiers(SqlNode orderExpr) {
+ while (orderExpr instanceof SqlCall) {
+ SqlCall call = (SqlCall) orderExpr;
+ SqlKind kind = call.getKind();
+ if (kind == SqlKind.DESCENDING
+ || kind == SqlKind.NULLS_FIRST
+ || kind == SqlKind.NULLS_LAST) {
+ orderExpr = call.operand(0);
+ } else {
+ break;
+ }
+ }
+ return orderExpr;
+ }
+
protected void validateMustFilterRequirements(SqlSelect select,
SelectNamespace ns) {
ns.filterRequirement = FilterRequirement.EMPTY;
if (select.getFrom() != null) {
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 53d54dd6d2..1551d1630d 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -720,6 +720,9 @@ private static RelCollation requiredCollation(RelNode r) {
if (r instanceof Project) {
return requiredCollation(((Project) r).getInput());
}
+ if (r instanceof Filter) {
+ return requiredCollation(((Filter) r).getInput());
+ }
if (r instanceof Delta) {
return requiredCollation(((Delta) r).getInput());
}
@@ -816,9 +819,13 @@ protected void convertSelectImpl(
distinctify(bb, true);
}
- convertOrder(
- select, bb, collation, orderExprList, select.getOffset(),
- select.getFetch());
+ if (select.isDistinctOn()) {
+ convertOrder(select, bb, collation, orderExprList, null, null);
+ } else {
+ convertOrder(
+ select, bb, collation, orderExprList, select.getOffset(),
+ select.getFetch());
+ }
if (select.hasHints()) {
final List<RelHint> hints = SqlUtil.getRelHint(hintStrategies,
select.getHints());
@@ -839,6 +846,20 @@ 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);
+ }
+ }
}
/**
@@ -1021,6 +1042,77 @@ private void distinctify(
rel.getRowType().getFieldNames(), ImmutableSet.of()), false);
}
+ /**
+ * Converts a SELECT DISTINCT ON clause into a relational expression
+ * using ROW_NUMBER() window function.
+ */
+ private void convertDistinctOn(Blackboard bb, SqlSelect select,
+ List<RelFieldCollation> collationList) {
+ if (bb.root == null) {
+ throw new IllegalArgumentException("rel must not be null");
+ }
+ final SqlNodeList distinctOn = requireNonNull(select.getDistinctOn(),
"distinctOn");
+
+ relBuilder.push(bb.root());
+ final RelDataType inputRowType = bb.root().getRowType();
+
+ // Build PARTITION BY expressions from DISTINCT ON.
+ // DISTINCT ON expressions are a prefix of ORDER BY,
+ // so we can use the field indices from collationList.
+ final List<RexNode> partitionKeys = new ArrayList<>();
+ for (int i = 0; i < distinctOn.size(); i++) {
+ RelFieldCollation fieldCollation = collationList.get(i);
+ partitionKeys.add(
+ rexBuilder.makeInputRef(inputRowType,
fieldCollation.getFieldIndex()));
+ }
+
+ // Build ORDER BY expressions for the window function
+ final List<RexFieldCollation> orderKeys = new ArrayList<>();
+ for (RelFieldCollation fieldCollation : collationList) {
+ RexNode ref = rexBuilder.makeInputRef(inputRowType,
fieldCollation.getFieldIndex());
+ final Set<SqlKind> kinds = new HashSet<>();
+ if (fieldCollation.getDirection() ==
RelFieldCollation.Direction.DESCENDING) {
+ kinds.add(SqlKind.DESCENDING);
+ }
+ switch (fieldCollation.nullDirection) {
+ case FIRST:
+ kinds.add(SqlKind.NULLS_FIRST);
+ break;
+ case LAST:
+ kinds.add(SqlKind.NULLS_LAST);
+ break;
+ default:
+ break;
+ }
+ orderKeys.add(new RexFieldCollation(ref, kinds));
+ }
+
+ final RelDataType bigintType =
+ typeFactory.createSqlType(SqlTypeName.BIGINT);
+ final RexNode rowNumber =
+ rexBuilder.makeOver(bigintType, SqlStdOperatorTable.ROW_NUMBER,
ImmutableList.of(),
+ partitionKeys, ImmutableList.copyOf(orderKeys),
+ RexWindowBounds.UNBOUNDED_PRECEDING, RexWindowBounds.CURRENT_ROW,
+ RexWindowExclusion.EXCLUDE_NO_OTHER, true, true, false, false, false);
+
+ // Add ROW_NUMBER as the last column
+ final List<RexNode> fields = new ArrayList<>(relBuilder.fields());
+ fields.add(rowNumber);
+ relBuilder.project(fields);
+
+ // Filter rn = 1
+ relBuilder.filter(
+ relBuilder.equals(
+ Util.last(relBuilder.fields()),
+ relBuilder.literal(BigDecimal.ONE)));
+
+ // Remove the ROW_NUMBER column
+ relBuilder.project(
+ Util.skipLast(relBuilder.fields()));
+
+ bb.setRoot(relBuilder.build(), false);
+ }
+
/**
* Converts a query's ORDER BY clause, if any.
*
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 325b69d872..4096537b7f 100644
---
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -171,6 +171,9 @@ FollowingBeforePrecedingError=Upper frame boundary cannot
be PRECEDING when lowe
WindowNameMustBeSimple=Window name must be a simple identifier
DuplicateWindowName=Duplicate window names not allowed
EmptyWindowSpec=Empty window specification not allowed
+DistinctOnNotAllowed=SELECT DISTINCT ON is not supported under the current SQL
conformance level
+DistinctOnRequiresOrderBy=SELECT DISTINCT ON requires an ORDER BY clause
+DistinctOnOrderByMismatch=SELECT DISTINCT ON expressions must match ORDER BY
expressions
DupWindowSpec=Duplicate window specification not allowed in the same window
clause
QualifyExpressionMustContainWindowFunction=QUALIFY expression ''{0}'' must
contain a window function
RankWithFrame=ROW/RANGE not allowed with RANK, DENSE_RANK, ROW_NUMBER,
PERCENTILE_CONT/DISC or LAG/LEAD functions
diff --git a/core/src/test/codegen/config.fmpp
b/core/src/test/codegen/config.fmpp
index 99ecb93da9..3f98500191 100644
--- a/core/src/test/codegen/config.fmpp
+++ b/core/src/test/codegen/config.fmpp
@@ -69,6 +69,7 @@ data: {
implementationFiles: [
"parserImpls.ftl"
]
+ includeDistinctOn: true
}
}
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 218f018fb6..c9f409f8c3 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -6239,4 +6239,35 @@ void checkUserDefinedOrderByOver(NullCollation
nullCollation) {
assertThat(plan, not(containsString("FLOOR(FLOOR")));
assertThat(plan, containsString("FLOOR($4, FLAG(WEEK))"));
}
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnSimple() {
+ final String sql = "SELECT DISTINCT ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, empno";
+ sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnMultiple() {
+ final String sql = "SELECT DISTINCT ON (deptno, job) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, job, hiredate DESC";
+ sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnWithLimit() {
+ final String sql = "SELECT DISTINCT ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, empno\n"
+ + "LIMIT 5";
+ sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
+ }
}
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 4b4d2302de..c4e8993839 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6817,7 +6817,130 @@ void testReturnsCorrectRowTypeOnCombinedJoin() {
f.withSql(qualifyOnMultipleWindowFunctions).ok();
}
- /** Negative tests for the {@code QUALIFY} clause. */
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnNotAllowed() {
+ // DISTINCT ON is not allowed under default SQL conformance
+ sql("^SELECT DISTINCT ON (deptno) empno FROM emp^ ORDER BY deptno")
+ .fails("SELECT DISTINCT ON is not supported under the current SQL
conformance level");
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnPositive() {
+ final SqlValidatorFixture f =
+ fixture().withConformance(SqlConformanceEnum.LENIENT);
+
+ f.withSql("SELECT DISTINCT ON (deptno) empno, ename FROM emp ORDER BY
deptno, empno")
+ .ok();
+
+ f.withSql("SELECT DISTINCT ON (deptno, job) empno FROM emp ORDER BY
deptno, job, hiredate")
+ .ok();
+
+ f.withSql("SELECT DISTINCT ON (deptno) empno FROM emp ORDER BY deptno
DESC")
+ .ok();
+
+ f.withSql("SELECT DISTINCT ON (deptno) empno FROM emp ORDER BY deptno
NULLS FIRST")
+ .ok();
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnNegative() {
+ final SqlValidatorFixture f =
+ fixture().withConformance(SqlConformanceEnum.LENIENT);
+
+ // DISTINCT ON requires ORDER BY
+ f.withSql("^SELECT DISTINCT ON (deptno) empno FROM emp^")
+ .fails("SELECT DISTINCT ON requires an ORDER BY clause");
+
+ // ORDER BY must contain all DISTINCT ON expressions as prefix
+ f.withSql("SELECT DISTINCT ON (deptno, job) empno FROM emp ORDER BY
^deptno^")
+ .fails("SELECT DISTINCT ON expressions must match ORDER BY
expressions");
+
+ // ORDER BY prefix must match exactly
+ f.withSql("SELECT DISTINCT ON (deptno, job) empno FROM emp ORDER BY ^job^,
deptno")
+ .fails("SELECT DISTINCT ON expressions must match ORDER BY
expressions");
+
+ // DISTINCT ON with extra ORDER BY is ok
+ f.withSql("SELECT DISTINCT ON (deptno) empno FROM emp ORDER BY deptno,
job")
+ .ok();
+
+ // Duplicate expressions in DISTINCT ON are allowed but must be matched in
ORDER BY
+ f.withSql("SELECT DISTINCT ON (deptno, deptno) deptno, empno FROM emp
ORDER BY deptno, deptno")
+ .ok();
+
+ // DISTINCT ON with expression
+ f.withSql("SELECT DISTINCT ON (empno % 2) empno, ename FROM emp ORDER BY
empno % 2")
+ .ok();
+
+ // Empty DISTINCT ON is not allowed (parse error)
+ f.withSql("SELECT DISTINCT ON ((^)^) deptno FROM emp")
+ .fails("(?s)Encountered \"\\)\" at .*");
+
+ // DISTINCT ON can reference alias (like ORDER BY)
+ f.withSql("SELECT DISTINCT ON (x) empno AS x, deptno FROM emp ORDER BY x")
+ .ok();
+
+ // DISTINCT ON with alias-column name clash: alias in SELECT takes
precedence
+ f.withSql("SELECT DISTINCT ON (deptno) empno AS deptno, deptno AS d FROM
emp ORDER BY deptno")
+ .ok();
+
+ // DISTINCT ON with qualified column reference
+ f.withSql("SELECT DISTINCT ON (e.deptno) e.deptno AS x, e.empno "
+ + "FROM emp AS e ORDER BY e.deptno")
+ .ok();
+
+ // Integer literal in both DISTINCT ON and ORDER BY matches at validator
+ // (ordinal resolution happens later in SqlToRelConverter)
+ f.withSql("SELECT DISTINCT ON (2) empno, ename FROM emp ORDER BY 2")
+ .ok();
+
+ // Expressions in DISTINCT ON (not ordinals)
+ f.withSql("SELECT DISTINCT ON (empno % 2, CHAR_LENGTH(ename)) empno, ename
"
+ + "FROM emp ORDER BY empno % 2, CHAR_LENGTH(ename)")
+ .ok();
+
+ // DISTINCT ON referencing non-projected column requires ORDER BY
+ f.withSql("^SELECT DISTINCT ON (deptno) empno, ename FROM emp^")
+ .fails("SELECT DISTINCT ON requires an ORDER BY clause");
+
+ // DISTINCT ON with aggregate query
+ f.withSql("SELECT DISTINCT ON (deptno) deptno, SUM(sal) "
+ + "FROM emp GROUP BY deptno ORDER BY deptno")
+ .ok();
+
+ // DISTINCT ON with aggregate expression
+ f.withSql("SELECT DISTINCT ON (SUM(sal)) deptno, SUM(sal) "
+ + "FROM emp GROUP BY deptno ORDER BY SUM(sal)")
+ .ok();
+
+ // DISTINCT ON with aggregate query and alias
+ f.withSql("SELECT DISTINCT ON (sum_sal) deptno, SUM(sal) AS sum_sal "
+ + "FROM emp GROUP BY deptno ORDER BY sum_sal")
+ .ok();
+
+ // DISTINCT ON with USING join (requires qualified reference due to
Calcite limitation)
+ f.withSql("SELECT DISTINCT ON (emp.deptno) * "
+ + "FROM emp JOIN dept USING (deptno) ORDER BY emp.deptno")
+ .ok();
+
+ // DISTINCT ON with NATURAL join (requires qualified reference due to
Calcite limitation)
+ f.withSql("SELECT DISTINCT ON (emp.deptno) * FROM emp NATURAL JOIN dept
ORDER BY emp.deptno")
+ .ok();
+
+ // DISTINCT ON with unqualified column reference - should work if column
is unambiguous
+ f.withSql("SELECT DISTINCT ON (deptno) empno, deptno FROM emp ORDER BY
deptno, empno")
+ .ok();
+
+ // DISTINCT ON with unqualified column in ORDER BY - should match
+ f.withSql("SELECT DISTINCT ON (deptno) empno, deptno FROM emp ORDER BY
deptno")
+ .ok();
+ }
+
@Test void testQualifyNegative() {
final SqlValidatorFixture f =
fixture().withConformance(SqlConformanceEnum.LENIENT);
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 6900b06ff8..1d05fd1b2f 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -1991,6 +1991,62 @@ 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="testDistinctOnMultiple">
+ <Resource name="sql">
+ <![CDATA[SELECT DISTINCT ON (deptno, job) empno, ename
+FROM emp
+ORDER BY deptno, job, hiredate DESC]]>
+ </Resource>
+ <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]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testDistinctOnSimple">
+ <Resource name="sql">
+ <![CDATA[SELECT DISTINCT ON (deptno) empno, ename
+FROM emp
+ORDER BY deptno, empno]]>
+ </Resource>
+ <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]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testDistinctOnWithLimit">
+ <Resource name="sql">
+ <![CDATA[SELECT DISTINCT ON (deptno) empno, ename
+FROM emp
+ORDER BY deptno, empno
+LIMIT 5]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1])
+ LogicalSort(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]])
]]>
</Resource>
</TestCase>
diff --git a/server/src/main/codegen/config.fmpp
b/server/src/main/codegen/config.fmpp
index e5c3eaf4d2..ba64f0b71a 100644
--- a/server/src/main/codegen/config.fmpp
+++ b/server/src/main/codegen/config.fmpp
@@ -99,6 +99,7 @@ data: {
implementationFiles: [
"parserImpls.ftl"
]
+ includeDistinctOn: true
}
}
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 80337bbfb1..b6ecb6b52b 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -205,7 +205,7 @@ ## Grammar
expression [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ]
select:
- SELECT [ hintComment ] [ STREAM ] [ ALL | DISTINCT ]
+ SELECT [ hintComment ] [ STREAM ] [ ALL | DISTINCT [ ON '(' expression
[, expression ]* ')' ] ]
{ starWithExclude | projectItem [, projectItem ]* }
[ BY expression [, expression ]* ]
FROM tableExpression
@@ -214,6 +214,8 @@ ## Grammar
[ HAVING booleanExpression ]
[ WINDOW windowName AS windowSpec [, windowName AS windowSpec ]* ]
[ QUALIFY booleanExpression ]
+ [ ORDER BY orderItem [, orderItem ]* ]
+ [ LIMIT expression [ OFFSET expression ] ]
The optional, non-standard `BY` clause groups and orders the query by
the specified expressions, and automatically adds them to the SELECT list
@@ -221,6 +223,13 @@ ## Grammar
with an explicit `GROUP BY` or `ORDER BY` clause in the same query.
`SELECT ... BY` is recognized only when the Babel parser is enabled. It sets
the generated parser configuration flag `includeSelectBy` to `true`.
+The optional `DISTINCT ON` clause is a PostgreSQL extension that allows you to
+eliminate duplicate rows based on specified expressions, keeping the first row
+in each group as determined by the `ORDER BY` clause. This is recognized only
+when the Babel parser is enabled. It sets the generated parser configuration
flag
+`includeDistinctOn` to `true`. When using `DISTINCT ON`, the expressions in the
+`DISTINCT ON` clause must match the beginning of the `ORDER BY` clause.
+
For example:
{% highlight sql %}
@@ -236,6 +245,16 @@ ## Grammar
ORDER BY deptno
{% endhighlight %}
+Example of `DISTINCT ON`:
+
+{% highlight sql %}
+SELECT DISTINCT ON (deptno) empno, ename, deptno
+FROM emp
+ORDER BY deptno, empno
+{% endhighlight %}
+
+This query keeps only the first employee (ordered by empno) in each department.
+
selectWithoutFrom:
SELECT [ ALL | DISTINCT ]
{ * | projectItem [, projectItem ]* }
diff --git
a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 5c24c0a806..0adc5be12a 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -6230,6 +6230,104 @@ private static Matcher<SqlNode> isCharLiteral(String s)
{
sql(sql).fails("(?s).*Encountered \"QUALIFY\" at .*");
}
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOn() {
+ final String sql = "SELECT DISTINCT ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, empno";
+
+ final String expected = "SELECT DISTINCT ON (`DEPTNO`) `EMPNO`, `ENAME`\n"
+ + "FROM `EMP`\n"
+ + "ORDER BY `DEPTNO`, `EMPNO`";
+ sql(sql).ok(expected);
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnMultiple() {
+ final String sql = "SELECT DISTINCT ON (deptno, job) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, job, hiredate DESC";
+
+ final String expected = "SELECT DISTINCT ON (`DEPTNO`, `JOB`) `EMPNO`,
`ENAME`\n"
+ + "FROM `EMP`\n"
+ + "ORDER BY `DEPTNO`, `JOB`, `HIREDATE` DESC";
+ sql(sql).ok(expected);
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnWithEverything() {
+ final String sql = "SELECT DISTINCT ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "WHERE deptno > 3\n"
+ + "GROUP BY deptno, empno, ename\n"
+ + "HAVING COUNT(*) > 1\n"
+ + "ORDER BY deptno, empno\n"
+ + "LIMIT 5\n";
+
+ final String expected = "SELECT DISTINCT ON (`DEPTNO`) `EMPNO`, `ENAME`\n"
+ + "FROM `EMP`\n"
+ + "WHERE (`DEPTNO` > 3)\n"
+ + "GROUP BY `DEPTNO`, `EMPNO`, `ENAME`\n"
+ + "HAVING (COUNT(*) > 1)\n"
+ + "ORDER BY `DEPTNO`, `EMPNO`\n"
+ + "FETCH NEXT 5 ROWS ONLY";
+ sql(sql).ok(expected);
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnWithOffset() {
+ final String sql = "SELECT DISTINCT ON (deptno) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno, empno\n"
+ + "OFFSET 10 ROWS\n"
+ + "FETCH NEXT 5 ROWS ONLY";
+
+ final String expected = "SELECT DISTINCT ON (`DEPTNO`) `EMPNO`, `ENAME`\n"
+ + "FROM `EMP`\n"
+ + "ORDER BY `DEPTNO`, `EMPNO`\n"
+ + "OFFSET 10 ROWS\n"
+ + "FETCH NEXT 5 ROWS ONLY";
+ sql(sql).ok(expected);
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnWithExpression() {
+ final String sql = "SELECT DISTINCT ON (deptno + 1) empno, ename\n"
+ + "FROM emp\n"
+ + "ORDER BY deptno + 1, empno";
+
+ final String expected = "SELECT DISTINCT ON ((`DEPTNO` + 1)) `EMPNO`,
`ENAME`\n"
+ + "FROM `EMP`\n"
+ + "ORDER BY (`DEPTNO` + 1), `EMPNO`";
+ sql(sql).ok(expected);
+ }
+
+ /** Test case of
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5406">[CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect</a>. */
+ @Test void testDistinctOnWithJoin() {
+ final String sql = "SELECT DISTINCT ON (e.deptno) e.empno, d.dname\n"
+ + "FROM emp AS e JOIN dept AS d ON e.deptno = d.deptno\n"
+ + "ORDER BY e.deptno, e.empno";
+
+ final String expected = "SELECT DISTINCT ON (`E`.`DEPTNO`) "
+ + "`E`.`EMPNO`, `D`.`DNAME`\n"
+ + "FROM `EMP` AS `E`\n"
+ + "INNER JOIN `DEPT` AS `D` ON (`E`.`DEPTNO` = `D`.`DEPTNO`)\n"
+ + "ORDER BY `E`.`DEPTNO`, `E`.`EMPNO`";
+ sql(sql).ok(expected);
+ }
+
@Test void testNullTreatment() {
sql("select lead(x) respect nulls over (w) from t")
.ok("SELECT (LEAD(`X`) RESPECT NULLS OVER (`W`))\n"