void acceptCall(
@@ -152,6 +159,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 39799a733cad..1a99f52fefb0 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
@@ -168,4 +168,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 5e652be9b839..8884328d4b7a 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
@@ -668,4 +668,14 @@ enum SelectAliasLookup {
* True when the unsigned versions of integer types are supported.
*/
boolean supportsUnsignedTypes();
+
+ /**
+ * Whether {@code SELECT DISTINCT ON} is allowed.
+ *
+ * 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 8d7d0b530608..196b4c7bec91 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
@@ -527,4 +527,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 00a77b0ee042..645361def848 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
@@ -173,4 +173,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 6881bae03525..2b58628527ed 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
@@ -3069,8 +3069,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 =
@@ -4265,6 +4267,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);
@@ -4799,6 +4802,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 4622176a0733..af8f9124a21a 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());
}
@@ -813,12 +816,18 @@ protected void convertSelectImpl(
select.getQualify());
if (select.isDistinct()) {
- distinctify(bb, true);
+ if (!select.isDistinctOn()) {
+ 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 hints = SqlUtil.getRelHint(hintStrategies, select.getHints());
@@ -839,6 +848,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 +1044,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 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 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 orderKeys = new ArrayList<>();
+ for (RelFieldCollation fieldCollation : collationList) {
+ RexNode ref = rexBuilder.makeInputRef(inputRowType, fieldCollation.getFieldIndex());
+ final Set 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 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 ac137d058e70..d374ea908326 100644
--- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -170,6 +170,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 initial 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 99ecb93da962..3f985001913a 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 dc18d05520ea..5bbea360288f 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -6039,4 +6039,35 @@ void checkUserDefinedOrderByOver(NullCollation nullCollation) {
final String sql = "select distinct deptno, deptno, empno, 1, 'a' from emp order by rand(), 1";
sql(sql).ok();
}
+
+ /** Test case of
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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 3d7eda0edf55..a0d7480b73a5 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6507,7 +6507,122 @@ void testReturnsCorrectRowTypeOnCombinedJoin() {
f.withSql(qualifyOnMultipleWindowFunctions).ok();
}
- /** Negative tests for the {@code QUALIFY} clause. */
+ /** Test case of
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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 initial 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 initial 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();
+ }
+
@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 a4dc246f8e25..4a645a5e51b4 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -1887,6 +1887,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]])
+]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/main/codegen/config.fmpp b/server/src/main/codegen/config.fmpp
index e5c3eaf4d27a..ba64f0b71a51 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/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index fad96f70b956..7c0fde601a78 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
@@ -6081,6 +6081,104 @@ private static Matcher isCharLiteral(String s) {
sql(sql).fails("(?s).*Encountered \"QUALIFY\" at .*");
}
+ /** Test case of
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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
+ * [CALCITE-5406]
+ * Support the SELECT DISTINCT ON statement for PostgreSQL dialect. */
+ @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"