From 0772e4bf7e22605ebd94e21cf220431a61efc30d Mon Sep 17 00:00:00 2001
From: Rushabh Lathia <rushabh.lathia@enterprisedb.com>
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

