From 16f2f1cda1108495d32adfdfb32c95caa41f6da4 Mon Sep 17 00:00:00 2001 From: Tisya Bhatia Date: Mon, 22 Jun 2026 10:03:01 -0500 Subject: [PATCH] [CALCITE-7597] Support ORDER BY ALL --- 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 | 24 ++++++++ core/src/test/resources/sql/sort.iq | 14 +++++ site/_docs/reference.md | 8 ++- .../calcite/sql/parser/SqlParserTest.java | 30 ++++++++++ 11 files changed, 188 insertions(+), 8 deletions(-) diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 4bbf49840c1f..e46c819b043a 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -3064,6 +3064,7 @@ SqlNodeList OrderBy(boolean accept) : { final List list = new ArrayList(); final Span s; + SqlNode all; } { { @@ -3075,10 +3076,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 13745bf0dfdb..6eebc2165109 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -768,6 +768,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 680c833c1721..a70c71df7614 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java @@ -195,6 +195,9 @@ public enum SqlKind { */ ORDER_BY, + /** The ALL keyword of the ORDER BY clause. */ + ORDER_BY_ALL, + /** WITH clause. */ WITH, @@ -1482,6 +1485,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 8757e1e17753..d1429f501b04 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 @@ -208,6 +208,18 @@ private SqlInternalOperators() { public static final SqlInternalOperator GROUP_BY_ALL = new SqlRollupOperator("GROUP BY ALL", SqlKind.GROUP_BY_ALL); + /** {@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 7989b7e7d830..4fc151cb5f38 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 @@ -5158,6 +5158,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; @@ -5185,6 +5186,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 325b69d87235..d640a251868e 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -253,6 +253,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 283ea802aa24..a4844fe2bf12 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -7183,6 +7183,30 @@ 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(); + // 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/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/site/_docs/reference.md b/site/_docs/reference.md index 454d13456a7e..552669d0a72a 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 5c24c0a806db..6b6c4307d7de 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 @@ -3947,6 +3947,36 @@ 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); + + 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() { final String sql = "select * from emp\n" + "order by gender desc nulls last,\n"