From 07e6380adf9a5147539a71985ef2387f9f341ffb Mon Sep 17 00:00:00 2001 From: Tisya Bhatia Date: Mon, 8 Jun 2026 16:35:00 -0500 Subject: [PATCH 1/2] [CALCITE-7597] Support ORDER BY ALL When ORDER BY ALL appears with no order items, sort by every expression in the SELECT clause, in select-list order. An optional trailing ASC/DESC and NULLS FIRST/LAST applies to all keys. The parser emits an ORDER_BY_ALL marker (wrapped in DESC/NULLS_FIRST/NULLS_LAST when a direction is given); SqlValidatorImpl.validateOrderList expands it to the SELECT items, re-applying the direction to each key. SELECT * is rejected. Includes parser + validator tests and reference docs. --- core/src/main/codegen/templates/Parser.jj | 31 ++++++++-- .../calcite/runtime/CalciteResource.java | 3 + .../java/org/apache/calcite/sql/SqlKind.java | 4 ++ .../calcite/sql/fun/SqlInternalOperators.java | 12 ++++ .../sql/validate/SqlValidatorImpl.java | 57 +++++++++++++++++++ .../runtime/CalciteResource.properties | 1 + .../calcite/sql/test/SqlAdvisorTest.java | 12 +++- .../apache/calcite/test/SqlValidatorTest.java | 21 +++++++ site/_docs/reference.md | 8 ++- .../calcite/sql/parser/SqlParserTest.java | 23 ++++++++ 10 files changed, 164 insertions(+), 8 deletions(-) diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index d4b589d6c1a7..d95020912dc5 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -3041,6 +3041,7 @@ SqlNodeList OrderBy(boolean accept) : { final List list = new ArrayList(); final Span s; + SqlNode all; } { { @@ -3052,10 +3053,32 @@ SqlNodeList OrderBy(boolean accept) : throw SqlUtil.newContextException(s.pos(), RESOURCE.illegalOrderBy()); } } - OrderItemList(list) - { - return new SqlNodeList(list, s.addAll(list).pos()); - } + + ( + { all = SqlInternalOperators.ORDER_BY_ALL.createCall(getPos()); } + ( + + | { all = SqlStdOperatorTable.DESC.createCall(getPos(), all); } + )? + ( + LOOKAHEAD(2) + { + all = SqlStdOperatorTable.NULLS_FIRST.createCall(getPos(), all); + } + | + { + all = SqlStdOperatorTable.NULLS_LAST.createCall(getPos(), all); + } + )? + { + list.add(all); + return new SqlNodeList(list, s.addAll(list).pos()); + } + | + OrderItemList(list) { + return new SqlNodeList(list, s.addAll(list).pos()); + } + ) } <#if parser.includeSelectBy!false> 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 ea60315ae6ae..9f4208dfebf2 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -765,6 +765,9 @@ ExInst illegalArgumentForTableFunctionCall(String a0, @BaseMessage("Streaming ORDER BY must start with monotonic expression") ExInst streamMustOrderByMonotonic(); + @BaseMessage("ORDER BY ALL requires an explicit SELECT list; ''*'' is not supported") + ExInst orderByAllRequiresExplicitSelectList(); + @BaseMessage("Set operator cannot combine streaming and non-streaming inputs") ExInst streamSetOpInconsistentInputs(); diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java b/core/src/main/java/org/apache/calcite/sql/SqlKind.java index 40111fe0afcb..f6f581df85eb 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java @@ -192,6 +192,9 @@ public enum SqlKind { */ ORDER_BY, + /** The ALL keyword of the ORDER BY clause. */ + ORDER_BY_ALL, + /** WITH clause. */ WITH, @@ -1479,6 +1482,7 @@ public enum SqlKind { TIMESTAMP_ADD, TIMESTAMP_DIFF, TIMESTAMP_SUB, EXTRACT, INTERVAL, LITERAL_CHAIN, JDBC_FN, PRECEDING, FOLLOWING, ORDER_BY, + ORDER_BY_ALL, NULLS_FIRST, NULLS_LAST, COLLECTION_TABLE, TABLESAMPLE, VALUES, WITH, WITH_ITEM, ITEM, SKIP_TO_FIRST, SKIP_TO_LAST, JSON_VALUE_EXPRESSION, UNNEST), diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlInternalOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlInternalOperators.java index dd1098fe4fce..d50ac8b9f36e 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlInternalOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlInternalOperators.java @@ -203,6 +203,18 @@ private SqlInternalOperators() { public static final SqlInternalOperator GROUP_BY_DISTINCT = new SqlRollupOperator("GROUP BY DISTINCT", SqlKind.GROUP_BY_DISTINCT); + /** {@code ORDER BY ALL}, a placeholder expanded during validation into + * a standard {@code ORDER BY}. */ + public static final SqlInternalOperator ORDER_BY_ALL = + // High precedence so the placeholder is never wrapped in parentheses when it + // appears alone or inside DESC / NULLS FIRST | LAST in an always-parentheses writer + new SqlInternalOperator("ORDER BY ALL", SqlKind.ORDER_BY_ALL, 100) { + @Override public void unparse(SqlWriter writer, SqlCall call, + int leftPrec, int rightPrec) { + writer.keyword("ALL"); + } + }; + /** Fetch operator is ONLY used for its precedence during unparsing. */ public static final SqlOperator FETCH = SqlBasicOperator.create("FETCH") 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 e2762d79264c..d88997254c4f 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 @@ -5157,6 +5157,7 @@ protected void validateOrderList(SqlSelect select) { // ORDER BY is validated in a scope where aliases in the SELECT clause // are visible. For example, "SELECT empno AS x FROM emp ORDER BY x" // is valid. + rewriteOrderByAll(select); SqlNodeList orderList = select.getOrderList(); if (orderList == null) { return; @@ -5184,6 +5185,62 @@ protected void validateOrderList(SqlSelect select) { } } + protected void rewriteOrderByAll(SqlSelect select) { + final SqlNodeList orderList = select.getOrderList(); + if (orderList == null || orderList.size() != 1) { + return; + } + + SqlNode node = orderList.get(0); + boolean desc = false; + SqlKind nulls = null; + + while (node instanceof SqlCall) { + final SqlKind kind = node.getKind(); + if (kind == SqlKind.NULLS_FIRST || kind == SqlKind.NULLS_LAST) { + nulls = kind; + node = ((SqlCall) node).operand(0); + } else if (kind == SqlKind.DESCENDING) { + desc = true; + node = ((SqlCall) node).operand(0); + } else { + break; + } + } + + if (node.getKind() != SqlKind.ORDER_BY_ALL) { + return; + } + final SqlParserPos pos = orderList.getParserPosition(); + final List keys = new ArrayList<>(); + + for (SqlNode selectItem : select.getSelectList()) { + final SqlNode expr = SqlUtil.stripAs(selectItem); + if (expr instanceof SqlIdentifier && ((SqlIdentifier) expr).isStar()) { + throw newValidationError(expr, + RESOURCE.orderByAllRequiresExplicitSelectList()); + } + keys.add(applyOrderByAllDirection(expr, desc, nulls, pos)); + } + select.setOrderBy(new SqlNodeList(keys, pos)); + } + + /** Wraps a single ORDER BY ALL key with the optional descending direction + * and null-ordering that apply to every expanded key. */ + private static SqlNode applyOrderByAllDirection(SqlNode key, boolean desc, + @Nullable SqlKind nulls, SqlParserPos pos) { + SqlNode result = key; + if (desc) { + result = SqlStdOperatorTable.DESC.createCall(pos, result); + } + if (nulls == SqlKind.NULLS_FIRST) { + result = SqlStdOperatorTable.NULLS_FIRST.createCall(pos, result); + } else if (nulls == SqlKind.NULLS_LAST) { + result = SqlStdOperatorTable.NULLS_LAST.createCall(pos, result); + } + return result; + } + /** * Validates an item in the GROUP BY clause of a SELECT statement. * 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 8906e451214e..23892e6e12a9 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -252,6 +252,7 @@ CannotConvertToStream=Cannot convert table ''{0}'' to stream CannotConvertToRelation=Cannot convert stream ''{0}'' to relation StreamMustGroupByMonotonic=Streaming aggregation requires at least one monotonic expression in GROUP BY clause StreamMustOrderByMonotonic=Streaming ORDER BY must start with monotonic expression +OrderByAllRequiresExplicitSelectList=ORDER BY ALL requires an explicit SELECT list; ''*'' is not supported StreamSetOpInconsistentInputs=Set operator cannot combine streaming and non-streaming inputs CannotStreamValues=Cannot stream VALUES CyclicDefinition=Cannot resolve ''{0}''; it references view ''{1}'', whose definition is cyclic diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java index 94cbc6fb9ed4..fad2778a384e 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java @@ -71,6 +71,10 @@ class SqlAdvisorTest extends SqlValidatorTestCase { Collections.singletonList( "KEYWORD(*)"); + private static final List ORDER_BY_ALL_KEYWORD = + Collections.singletonList( + "KEYWORD(ALL)"); + protected static final List FROM_KEYWORDS = Arrays.asList( "KEYWORD(()", @@ -744,10 +748,12 @@ protected List getJoinKeywords() { String sql; sql = "select emp.empno from sales.emp where empno=1 order by ^dummy"; - f.withSql(sql).assertHint(EXPR_KEYWORDS, EMP_COLUMNS, EMP_TABLE); + f.withSql(sql).assertHint(EXPR_KEYWORDS, ORDER_BY_ALL_KEYWORD, EMP_COLUMNS, + EMP_TABLE); sql = "select emp.empno from sales.emp where empno=1 order by ^"; - f.withSql(sql).assertComplete(EXPR_KEYWORDS, EMP_COLUMNS, EMP_TABLE); + f.withSql(sql).assertComplete(EXPR_KEYWORDS, ORDER_BY_ALL_KEYWORD, + EMP_COLUMNS, EMP_TABLE); sql = "select emp.empno\n" @@ -755,7 +761,7 @@ protected List getJoinKeywords() { + " mpno,name,ob,gr,iredate,al,omm,eptno,lacker)\n" + "where e.mpno=1 order by ^"; f.withSql(sql) - .assertComplete(EXPR_KEYWORDS, + .assertComplete(EXPR_KEYWORDS, ORDER_BY_ALL_KEYWORD, Arrays.asList("COLUMN(MPNO)", "COLUMN(NAME)", "COLUMN(OB)", 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 0f40dfbc6dfd..1573dd89d07a 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -7075,6 +7075,27 @@ public boolean isBangEqualAllowed() { .ok(); } + @Test void testOrderByAll() { + // expands to all SELECT items, validates fine + sql("select deptno, sal from emp order by all").ok(); + // direction applies to every expanded key + sql("select deptno, sal from emp order by all desc").ok(); + // SELECT * can't be expanded here + sql("select ^*^ from emp order by all") + .fails("(?s).*ORDER BY ALL requires an explicit SELECT list.*"); + // Aliases that shadow other column names must not confuse expansion + sql("select empno as deptno, deptno as empno from emp order by all").ok(); + // verify "x" still resolves and the two features coexist + sql("select sal as x, x + 1 as y from emp order by all") + .withValidatorIdentifierExpansion(true) + .withConformance(SqlConformanceEnum.BABEL) + .ok(); + sql("select sal as x, x + 1 as y from emp order by all desc") + .withValidatorIdentifierExpansion(true) + .withConformance(SqlConformanceEnum.BABEL) + .ok(); + } + @Test void testOrder() { final SqlConformance conformance = fixture().conformance(); sql("select empno as x from emp order by empno").ok(); diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 026fa59c4741..a448dba12399 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -191,7 +191,7 @@ query: | query MINUS [ ALL | DISTINCT ] query | query INTERSECT [ ALL | DISTINCT ] query } - [ ORDER BY orderItem [, orderItem ]* ] + [ ORDER BY { ALL [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ] | orderItem [, orderItem]* } ] [ LIMIT [ start, ] { count | ALL } ] [ OFFSET start { ROW | ROWS } ] [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ] @@ -402,6 +402,12 @@ in those same conformance levels, any *column* in *insert* may be replaced by In *orderItem*, if *expression* is a positive integer *n*, it denotes the nth item in the SELECT clause. +`ORDER BY ALL` sorts by every expression in the SELECT clause, +in the order that they appear in the list; for example: +"SELECT x, y FROM t ORDER BY ALL" is equivalent to +"SELECT x, y FROM t ORDER BY x, y" +An optional trailing ASC / DESC and NULLS FIRST / NULLS LAST applies to all keys. + In *query*, *count* and *start* may each be either an unsigned integer literal or a dynamic parameter whose value is an integer. 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 4ab6f776ba4a..8355ce0ceb6c 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 @@ -3931,6 +3931,29 @@ void checkPeriodPredicate(Checker checker) { + "ORDER BY `EMPNO`, `GENDER` DESC, `DEPTNO`, `EMPNO`, `NAME` DESC"); } + @Test void testOrderByAll() { + final String sql = "select x, y from t\n" + + "order by all"; + final String expected = "SELECT `X`, `Y`\n" + + "FROM `T`\n" + + "ORDER BY ALL"; + sql(sql).ok(expected); + + final String sql1 = "select x, y from t\n" + + "order by all desc"; + final String expected1 = "SELECT `X`, `Y`\n" + + "FROM `T`\n" + + "ORDER BY ALL DESC"; + sql(sql1).ok(expected1); + + final String sql2 = "select x, y from t\n" + + "order by all desc nulls last"; + final String expected2 = "SELECT `X`, `Y`\n" + + "FROM `T`\n" + + "ORDER BY ALL DESC NULLS LAST"; + sql(sql2).ok(expected2); + } + @Test void testOrderNullsFirst() { final String sql = "select * from emp\n" + "order by gender desc nulls last,\n" From 1f00496c03a0230b25abf4c52a899e8e7dbb9b33 Mon Sep 17 00:00:00 2001 From: Tisya Bhatia Date: Mon, 15 Jun 2026 11:00:51 -0700 Subject: [PATCH 2/2] [CALCITE-7597] Add NULLS FIRST parser test, QuidemTest, and JIRA link --- .../org/apache/calcite/test/SqlValidatorTest.java | 3 +++ core/src/test/resources/sql/sort.iq | 14 ++++++++++++++ .../apache/calcite/sql/parser/SqlParserTest.java | 7 +++++++ 3 files changed, 24 insertions(+) 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 1573dd89d07a..e1c37fffeb62 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -7075,6 +7075,9 @@ public boolean isBangEqualAllowed() { .ok(); } + /** Test case for + * [CALCITE-7597] + * Support ORDER BY ALL. */ @Test void testOrderByAll() { // expands to all SELECT items, validates fine sql("select deptno, sal from emp order by all").ok(); diff --git a/core/src/test/resources/sql/sort.iq b/core/src/test/resources/sql/sort.iq index b7df52bff2c6..f9149a54822a 100644 --- a/core/src/test/resources/sql/sort.iq +++ b/core/src/test/resources/sql/sort.iq @@ -549,4 +549,18 @@ order by au."books"[1]."title"; !ok +# [CALCITE-7597] ORDER BY ALL sorts by every SELECT expression, in list order +select x, y from (values (2, 'b'), (1, 'a'), (1, 'c')) as t(x, y) +order by all; ++---+---+ +| X | Y | ++---+---+ +| 1 | a | +| 1 | c | +| 2 | b | ++---+---+ +(3 rows) + +!ok + # End sort.iq 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 8355ce0ceb6c..3fc49c76331a 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 @@ -3952,6 +3952,13 @@ void checkPeriodPredicate(Checker checker) { + "FROM `T`\n" + "ORDER BY ALL DESC NULLS LAST"; sql(sql2).ok(expected2); + + final String sql3 = "select x, y from t\n" + + "order by all nulls first"; + final String expected3 = "SELECT `X`, `Y`\n" + + "FROM `T`\n" + + "ORDER BY ALL NULLS FIRST"; + sql(sql3).ok(expected3); } @Test void testOrderNullsFirst() {