From 0772e4bf7e22605ebd94e21cf220431a61efc30d Mon Sep 17 00:00:00 2001 From: Rushabh Lathia Date: Tue, 24 Mar 2026 11:34:33 +0530 Subject: [PATCH 1/2] Add ORDER BY ALL with ASC/DESC/NULLS support ORDER BY ALL is a form of ORDER BY that automatically adds all non-junk columns from the SELECT target list to the ORDER BY clause. This is similar to GROUP BY ALL and is useful for data exploration. This implementation supports: - ORDER BY ALL (default ascending order) - ORDER BY ALL ASC - ORDER BY ALL DESC - ORDER BY ALL NULLS FIRST/LAST - ORDER BY ALL ASC/DESC NULLS FIRST/LAST The syntax works by creating a special marker SortBy node with a NULL node pointer that carries the sort direction and nulls ordering. During query transformation, this marker is detected and expanded to order by all non-junk columns in the target list with the specified direction. Example usage: SELECT a, b, c FROM table ORDER BY ALL DESC; -- Equivalent to: ORDER BY a DESC, b DESC, c DESC --- src/backend/parser/analyze.c | 17 ++++++++ src/backend/parser/gram.y | 43 ++++++++++++++++++++- src/backend/parser/parse_agg.c | 1 + src/backend/parser/parse_clause.c | 57 +++++++++++++++++++++++++++ src/backend/utils/adt/ruleutils.c | 64 ++++++++++++++++++++++++++++++- src/include/nodes/parsenodes.h | 2 + src/include/parser/parse_clause.h | 1 + 7 files changed, 182 insertions(+), 3 deletions(-) diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 539c16c4f79..b57725b4c37 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1472,9 +1472,17 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, */ qry->sortClause = transformSortClause(pstate, stmt->sortClause, + stmt->orderByAll, &qry->targetList, EXPR_KIND_ORDER_BY, false /* allow SQL92 rules */ ); + /* + * Preserve the orderByAll flag for deparsing. Even though sortClause + * is expanded to individual columns during transformation, we need to + * remember that the original query used ORDER BY ALL so that + * pg_get_viewdef() can recreate it correctly in view definitions. + */ + qry->orderByAll = stmt->orderByAll; qry->groupClause = transformGroupClause(pstate, stmt->groupClause, @@ -1737,9 +1745,17 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) */ qry->sortClause = transformSortClause(pstate, stmt->sortClause, + stmt->orderByAll, &qry->targetList, EXPR_KIND_ORDER_BY, false /* allow SQL92 rules */ ); + /* + * Preserve the orderByAll flag for deparsing. Even though sortClause + * is expanded to individual columns during transformation, we need to + * remember that the original query used ORDER BY ALL so that + * pg_get_viewdef() can recreate it correctly in view definitions. + */ + qry->orderByAll = stmt->orderByAll; qry->limitOffset = transformLimitClause(pstate, stmt->limitOffset, EXPR_KIND_OFFSET, "OFFSET", @@ -1981,6 +1997,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->sortClause = transformSortClause(pstate, sortClause, + stmt->orderByAll, &qry->targetList, EXPR_KIND_ORDER_BY, false /* allow SQL92 rules */ ); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9cbe8eafc45..1a59dbe4fb2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -13465,6 +13465,21 @@ opt_sort_clause: sort_clause: ORDER BY sortby_list { $$ = $3; } + | ORDER BY ALL opt_asc_desc opt_nulls_order + { + /* + * Create a special marker SortBy node for ORDER BY ALL. + * We use a SortBy with node=NULL as the marker, and store + * the sort direction and nulls ordering to apply to all columns. + */ + SortBy *sortby = makeNode(SortBy); + sortby->node = NULL; /* NULL node indicates ORDER BY ALL */ + sortby->sortby_dir = $4; + sortby->sortby_nulls = $5; + sortby->useOp = NIL; + sortby->location = @2; + $$ = list_make1(sortby); + } ; sortby_list: @@ -17850,7 +17865,25 @@ PLpgSQL_Expr: opt_distinct_clause opt_target_list n->groupByAll = ($5)->all; n->havingClause = $6; n->windowClause = $7; - n->sortClause = $8; + /* + * Check for ORDER BY ALL marker (SortBy with NULL node). + * We detect this in the grammar action because PLpgSQL_Expr + * creates SelectStmt nodes directly without going through + * insertSelectOptions(), so we must set the orderByAll flag + * here to ensure it gets properly transformed. + */ + if ($8 && list_length($8) == 1 && + IsA(linitial($8), SortBy) && + ((SortBy *) linitial($8))->node == NULL) + { + n->orderByAll = true; + /* Store the SortBy marker to preserve direction/nulls info */ + n->sortClause = $8; + } + else + { + n->sortClause = $8; + } if ($9) { n->limitOffset = $9->limitOffset; @@ -19330,7 +19363,15 @@ insertSelectOptions(SelectStmt *stmt, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple ORDER BY clauses not allowed"), parser_errposition(exprLocation((Node *) sortClause)))); + stmt->sortClause = sortClause; + /* Check for ORDER BY ALL marker (SortBy with NULL node) */ + if (list_length(sortClause) == 1 && + IsA(linitial(sortClause), SortBy) && + ((SortBy *) linitial(sortClause))->node == NULL) + { + stmt->orderByAll = true; + } } /* We can handle multiple locking clauses, though */ stmt->lockingClause = list_concat(stmt->lockingClause, lockingClause); diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 33fd2cccae5..53af2afda55 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -191,6 +191,7 @@ transformAggregateCall(ParseState *pstate, Aggref *agg, torder = transformSortClause(pstate, aggorder, + false /* ORDER BY ALL not applicable - aggregate ORDER BY sorts within groups, not query output */, &tlist, EXPR_KIND_ORDER_BY, true /* force SQL99 rules */ ); diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 06b65d4a605..1544f628088 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -2793,6 +2793,7 @@ transformGroupClause(ParseState *pstate, List *grouplist, bool groupByAll, List * transformSortClause(ParseState *pstate, List *orderlist, + bool orderByAll, List **targetlist, ParseExprKind exprKind, bool useSQL99) @@ -2800,6 +2801,61 @@ transformSortClause(ParseState *pstate, List *sortlist = NIL; ListCell *olitem; + /* + * Handle ORDER BY ALL expansion. + * + * The target list has already been transformed and contains the + * columns we should sort by. We create a SortBy node for each + * non-junk target entry, applying the same sort direction and + * NULLS ordering specified in the marker to all columns. + */ + if (orderByAll) + { + SortBy *sortby; + SortBy *all_marker; + SortByDir sort_dir; + SortByNulls nulls_order; + + /* + * orderlist should contain a single SortBy marker with node=NULL + * that specifies the sort direction and nulls ordering to apply + * to all columns. + */ + Assert(list_length(orderlist) == 1); + all_marker = (SortBy *) linitial(orderlist); + Assert(IsA(all_marker, SortBy) && all_marker->node == NULL); + + /* Extract sort direction and nulls ordering from the marker */ + sort_dir = all_marker->sortby_dir; + nulls_order = all_marker->sortby_nulls; + + /* Iterate over targets, adding all non-junk columns to sort clause */ + foreach_ptr(TargetEntry, tle, *targetlist) + { + /* + * Ignore junk TLEs. These are system columns (like ctid, xmin) + * or internal working columns that shouldn't be part of the + * user-visible ordering. + */ + if (tle->resjunk) + continue; + + /* Create a SortBy for this target entry with specified direction */ + sortby = makeNode(SortBy); + + sortby->node = (Node *) tle->expr; + sortby->sortby_dir = sort_dir; + sortby->sortby_nulls = nulls_order; + sortby->useOp = NIL; + sortby->location = exprLocation((Node *) tle->expr); + + sortlist = addTargetToSortList(pstate, tle, + sortlist, *targetlist, sortby); + } + + return sortlist; + } + foreach(olitem, orderlist) { SortBy *sortby = (SortBy *) lfirst(olitem); @@ -2875,6 +2931,7 @@ transformWindowDefinitions(ParseState *pstate, */ orderClause = transformSortClause(pstate, windef->orderClause, + false /* not applicable for window ORDER BY */ , targetlist, EXPR_KIND_WINDOW_ORDER, true /* force SQL99 rules */ ); diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index f16f1535785..44bbbd9420c 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -5942,8 +5942,68 @@ get_select_query_def(Query *query, deparse_context *context) { appendContextKeyword(context, " ORDER BY ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); - get_rule_orderby(query->sortClause, query->targetList, - force_colno, context); + if (query->orderByAll) + { + SortGroupClause *srt; + TargetEntry *tle; + Node *sortexpr; + Oid sortcoltype; + TypeCacheEntry *typentry; + + appendStringInfoString(buf, "ALL"); + + /* + * Extract sort direction and NULLS ordering from the first + * SortGroupClause. After transformation, sortClause contains + * SortGroupClause nodes (one per column). All columns share the + * same intended direction (ASC/DESC) and NULLS ordering, though + * the actual sort operators differ by column type. We examine the + * first column to determine what modifiers to output. + */ + Assert(query->sortClause != NIL); + srt = (SortGroupClause *) linitial(query->sortClause); + + /* + * Get the sort expression to determine its type without + * appending anything to the output buffer. + */ + tle = get_sortgroupref_tle(srt->tleSortGroupRef, query->targetList); + sortexpr = (Node *) tle->expr; + sortcoltype = exprType(sortexpr); + + /* See whether operator is default < or > for datatype */ + typentry = lookup_type_cache(sortcoltype, + TYPECACHE_LT_OPR | TYPECACHE_GT_OPR); + + if (srt->sortop == typentry->lt_opr) + { + /* ASC is default, so emit nothing for it */ + if (srt->nulls_first) + appendStringInfoString(buf, " NULLS FIRST"); + } + else if (srt->sortop == typentry->gt_opr) + { + appendStringInfoString(buf, " DESC"); + /* DESC defaults to NULLS FIRST */ + if (!srt->nulls_first) + appendStringInfoString(buf, " NULLS LAST"); + } + else + { + appendStringInfo(buf, " USING %s", + generate_operator_name(srt->sortop, + sortcoltype, + sortcoltype)); + /* be specific to eliminate ambiguity */ + if (srt->nulls_first) + appendStringInfoString(buf, " NULLS FIRST"); + else + appendStringInfoString(buf, " NULLS LAST"); + } + } + else + get_rule_orderby(query->sortClause, query->targetList, + force_colno, context); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4ee092206b0..867aac0158d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -226,6 +226,7 @@ typedef struct Query List *distinctClause; /* a list of SortGroupClause's */ List *sortClause; /* a list of SortGroupClause's */ + bool orderByAll; /* was ORDER BY ALL used? */ Node *limitOffset; /* # of result tuples to skip (int8 expr) */ Node *limitCount; /* # of result tuples to return (int8 expr) */ @@ -2242,6 +2243,7 @@ typedef struct SelectStmt * SelectStmts. */ List *sortClause; /* sort clause (a list of SortBy's) */ + bool orderByAll; /* Is this ORDER BY ALL? */ Node *limitOffset; /* # of result tuples to skip */ Node *limitCount; /* # of result tuples to return */ LimitOption limitOption; /* limit type */ diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index fe234611007..5adc9aaf327 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -31,6 +31,7 @@ extern List *transformGroupClause(ParseState *pstate, List *grouplist, List **targetlist, List *sortClause, ParseExprKind exprKind, bool useSQL99); extern List *transformSortClause(ParseState *pstate, List *orderlist, + bool orderByAll, List **targetlist, ParseExprKind exprKind, bool useSQL99); -- 2.43.0