From 21739e6ea975162da1acffd119221e7ad8abb1ca Mon Sep 17 00:00:00 2001
From: David Christensen <david@pgguru.net>
Date: Tue, 27 Oct 2026 15:19:01 -0500
Subject: [PATCH] GROUP BY ALL redo (inherits ORDER BY equality semantics)

GROUP BY ALL adds every non-junk target-list expression that does not
contain an aggregate or window function to the query's groupClause,
making it exactly equivalent to spelling those expressions out in an
explicit GROUP BY list.

This reimplements the feature reverted in a32733d8f10.  The original
commit (ef38a4d9756) built the GROUP BY ALL list by calling
addTargetToGroupList() directly on each target, which uses default
sort/group semantics.  That bypassed the logic in
transformGroupClauseExpr() that copies operator information from a
matching ORDER BY item, so a query like

    SELECT a, count(*) FROM t GROUP BY ALL ORDER BY a USING <opclass op>

silently grouped with the type's default equality instead of the
equality implied by ORDER BY -- giving different (wrong) results than
the equivalent explicit GROUP BY, e.g. collapsing record-image-distinct
composite values that the explicit form keeps separate.

To fix this without duplicating logic, the per-target handling of
transformGroupClauseExpr() (local-duplicate elimination, the
already-present check, and the ORDER BY operator copy) is factored into
a new helper, addTargetToGroupClause().  Both the explicit GROUP BY
path and the GROUP BY ALL path now route each target through it, so
GROUP BY ALL inherits ORDER BY equality/ordering semantics exactly as
an explicit GROUP BY does.

Window functions are treated like aggregates (left out of the
constructed GROUP BY list); the SQL standard is silent on them.  If no
acceptable targets remain, the clause is treated as GROUP BY ().

Regression tests cover the plan shapes for the various target-list
cases and, specifically, that GROUP BY ALL matches an explicit GROUP BY
when ORDER BY ... USING selects non-default (record_image) equality.

catversion bump due to the new Query.groupByAll field.
---
 doc/src/sgml/queries.sgml                |  25 ++++
 doc/src/sgml/ref/select.sgml             |  12 +-
 doc/src/sgml/ref/select_into.sgml        |   2 +-
 src/backend/parser/analyze.c             |   2 +
 src/backend/parser/gram.y                |  15 +++
 src/backend/parser/parse_clause.c        | 151 ++++++++++++++++++----
 src/backend/utils/adt/ruleutils.c        |   4 +-
 src/include/catalog/catversion.h         |   2 +-
 src/include/nodes/parsenodes.h           |   2 +
 src/include/parser/parse_clause.h        |   1 +
 src/test/regress/expected/aggregates.out | 157 +++++++++++++++++++++++
 src/test/regress/sql/aggregates.sql      |  76 +++++++++++
 12 files changed, 422 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
index eadec641674..b93e8abafa4 100644
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -1154,6 +1154,31 @@ SELECT product_id, p.name, (sum(s.units) * p.price) AS sales
     expressions cannot contain aggregate functions or window functions).
    </para>
 
+   <para>
+    <productname>PostgreSQL</productname> also supports the syntax
+    <literal>GROUP BY ALL</literal>, which is equivalent to explicitly
+    writing all select-list entries that do not contain either an aggregate
+    function or a window function.  This can greatly simplify ad-hoc
+    exploration of data.  As an example, these queries are equivalent:
+<screen>
+<prompt>=&gt;</prompt> <userinput>SELECT a, b, a + b, sum(c) FROM test1 GROUP BY ALL;</userinput>
+ a | b | ?column? | sum
+---+---+----------+----
+ 1 | 4 |        5 |  9
+ 2 | 5 |        7 | 12
+ 3 | 6 |        9 | 15
+(3 rows)
+
+<prompt>=&gt;</prompt> <userinput>SELECT a, b, a + b, sum(c) FROM test1 GROUP BY a, b, a + b;</userinput>
+ a | b | ?column? | sum
+---+---+----------+----
+ 1 | 4 |        5 |  9
+ 2 | 5 |        7 | 12
+ 3 | 6 |        9 | 15
+(3 rows)
+</screen>
+   </para>
+
    <indexterm>
     <primary>HAVING</primary>
    </indexterm>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 18392f7caec..9ce61987795 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -37,7 +37,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replaceable class="parameter">expression</replac
     [ { * | <replaceable class="parameter">expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
     [ FROM <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> ]
-    [ GROUP BY [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...] ]
+    [ GROUP BY { ALL | [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...] } ]
     [ HAVING <replaceable class="parameter">condition</replaceable> ]
     [ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceable class="parameter">window_definition</replaceable> ) [, ...] ]
     [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] <replaceable class="parameter">select</replaceable> ]
@@ -796,7 +796,7 @@ WHERE <replaceable class="parameter">condition</replaceable>
    <para>
     The optional <literal>GROUP BY</literal> clause has the general form
 <synopsis>
-GROUP BY [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...]
+GROUP BY { ALL | [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...] }
 </synopsis>
    </para>
 
@@ -814,6 +814,14 @@ GROUP BY [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</rep
     input-column name rather than an output column name.
    </para>
 
+   <para>
+    The form <literal>GROUP BY ALL</literal> with no explicit
+    <replaceable class="parameter">grouping_element</replaceable>s
+    provided is equivalent to writing <literal>GROUP BY</literal> with the
+    numbers of all <command>SELECT</command> output columns that do not
+    contain either an aggregate function or a window function.
+   </para>
+
    <para>
     If any of <literal>GROUPING SETS</literal>, <literal>ROLLUP</literal> or
     <literal>CUBE</literal> are present as grouping elements, then the
diff --git a/doc/src/sgml/ref/select_into.sgml b/doc/src/sgml/ref/select_into.sgml
index 550ba69d5d1..cbf865ff838 100644
--- a/doc/src/sgml/ref/select_into.sgml
+++ b/doc/src/sgml/ref/select_into.sgml
@@ -27,7 +27,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replaceable class="parameter">expression</replac
     INTO [ TEMPORARY | TEMP | UNLOGGED ] [ TABLE ] <replaceable class="parameter">new_table</replaceable>
     [ FROM <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> ]
-    [ GROUP BY [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...] ]
+    [ GROUP BY { ALL | [ ALL | DISTINCT ] <replaceable class="parameter">grouping_element</replaceable> [, ...] } ]
     [ HAVING <replaceable class="parameter">condition</replaceable> ]
     [ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceable class="parameter">window_definition</replaceable> ) [, ...] ]
     [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] <replaceable class="parameter">select</replaceable> ]
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 2dc81a8d3b2..7bec46b7cd9 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1490,12 +1490,14 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 
 	qry->groupClause = transformGroupClause(pstate,
 											stmt->groupClause,
+											stmt->groupByAll,
 											&qry->groupingSets,
 											&qry->targetList,
 											qry->sortClause,
 											EXPR_KIND_GROUP_BY,
 											false /* allow SQL92 rules */ );
 	qry->groupDistinct = stmt->groupDistinct;
+	qry->groupByAll = stmt->groupByAll;
 
 	if (stmt->distinctClause == NIL)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..9536ab06245 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -120,6 +120,7 @@ typedef struct SelectLimit
 typedef struct GroupClause
 {
 	bool		distinct;
+	bool		all;
 	List	   *list;
 } GroupClause;
 
@@ -13221,6 +13222,7 @@ simple_select:
 					n->whereClause = $6;
 					n->groupClause = ($7)->list;
 					n->groupDistinct = ($7)->distinct;
+					n->groupByAll = ($7)->all;
 					n->havingClause = $8;
 					n->windowClause = $9;
 					$$ = (Node *) n;
@@ -13238,6 +13240,7 @@ simple_select:
 					n->whereClause = $6;
 					n->groupClause = ($7)->list;
 					n->groupDistinct = ($7)->distinct;
+					n->groupByAll = ($7)->all;
 					n->havingClause = $8;
 					n->windowClause = $9;
 					$$ = (Node *) n;
@@ -13735,14 +13738,25 @@ group_clause:
 					GroupClause *n = palloc_object(GroupClause);
 
 					n->distinct = $3 == SET_QUANTIFIER_DISTINCT;
+					n->all = false;
 					n->list = $4;
 					$$ = n;
 				}
+			| GROUP_P BY ALL
+				{
+					GroupClause *n = palloc_object(GroupClause);
+
+					n->distinct = false;
+					n->all = true;
+					n->list = NIL;
+					$$ = n;
+				}
 			| /*EMPTY*/
 				{
 					GroupClause *n = palloc_object(GroupClause);
 
 					n->distinct = false;
+					n->all = false;
 					n->list = NIL;
 					$$ = n;
 				}
@@ -17957,6 +17971,7 @@ PLpgSQL_Expr: opt_distinct_clause opt_target_list
 					n->whereClause = $4;
 					n->groupClause = ($5)->list;
 					n->groupDistinct = ($5)->distinct;
+					n->groupByAll = ($5)->all;
 					n->havingClause = $6;
 					n->windowClause = $7;
 					n->sortClause = $8;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 7a5179b9053..22899514478 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -2346,40 +2346,33 @@ flatten_grouping_sets(Node *expr, bool toplevel, bool *hasGroupingSets)
 }
 
 /*
- * Transform a single expression within a GROUP BY clause or grouping set.
+ * Add a resolved targetlist entry to a GROUP BY (or PARTITION BY) clause.
  *
- * The expression is added to the targetlist if not already present, and to the
- * flatresult list (which will become the groupClause) if not already present
- * there.  The sortClause is consulted for operator and sort order hints.
+ * This is the common core shared by transformGroupClauseExpr() and the
+ * GROUP BY ALL path in transformGroupClause(): given a TargetEntry that is
+ * to become a grouping column, add it to *flatresult, inheriting equality
+ * and ordering semantics from a matching ORDER BY item if one exists.
  *
- * Returns the ressortgroupref of the expression.
+ * Returns the ressortgroupref of the entry, or 0 if it was a local-level
+ * duplicate that we dropped.
  *
  * flatresult	reference to flat list of SortGroupClause nodes
  * seen_local	bitmapset of sortgrouprefs already seen at the local level
  * pstate		ParseState
- * gexpr		node to transform
+ * tle			targetlist entry to add
  * targetlist	reference to TargetEntry list
  * sortClause	ORDER BY clause (SortGroupClause nodes)
- * exprKind		expression kind
- * useSQL99		SQL99 rather than SQL92 syntax
  * toplevel		false if within any grouping set
+ * location		parse location to finger in event of trouble
  */
 static Index
-transformGroupClauseExpr(List **flatresult, Bitmapset *seen_local,
-						 ParseState *pstate, Node *gexpr,
-						 List **targetlist, List *sortClause,
-						 ParseExprKind exprKind, bool useSQL99, bool toplevel)
+addTargetToGroupClause(List **flatresult, Bitmapset *seen_local,
+					   ParseState *pstate, TargetEntry *tle,
+					   List **targetlist, List *sortClause,
+					   bool toplevel, int location)
 {
-	TargetEntry *tle;
 	bool		found = false;
 
-	if (useSQL99)
-		tle = findTargetlistEntrySQL99(pstate, gexpr,
-									   targetlist, exprKind);
-	else
-		tle = findTargetlistEntrySQL92(pstate, gexpr,
-									   targetlist, exprKind);
-
 	if (tle->ressortgroupref > 0)
 	{
 		ListCell   *sl;
@@ -2446,7 +2439,7 @@ transformGroupClauseExpr(List **flatresult, Bitmapset *seen_local,
 	if (!found)
 		*flatresult = addTargetToGroupList(pstate, tle,
 										   *flatresult, *targetlist,
-										   exprLocation(gexpr));
+										   location);
 
 	/*
 	 * _something_ must have assigned us a sortgroupref by now...
@@ -2455,6 +2448,46 @@ transformGroupClauseExpr(List **flatresult, Bitmapset *seen_local,
 	return tle->ressortgroupref;
 }
 
+/*
+ * Transform a single expression within a GROUP BY clause or grouping set.
+ *
+ * The expression is added to the targetlist if not already present, and to the
+ * flatresult list (which will become the groupClause) if not already present
+ * there.  The sortClause is consulted for operator and sort order hints.
+ *
+ * Returns the ressortgroupref of the expression.
+ *
+ * flatresult	reference to flat list of SortGroupClause nodes
+ * seen_local	bitmapset of sortgrouprefs already seen at the local level
+ * pstate		ParseState
+ * gexpr		node to transform
+ * targetlist	reference to TargetEntry list
+ * sortClause	ORDER BY clause (SortGroupClause nodes)
+ * exprKind		expression kind
+ * useSQL99		SQL99 rather than SQL92 syntax
+ * toplevel		false if within any grouping set
+ */
+static Index
+transformGroupClauseExpr(List **flatresult, Bitmapset *seen_local,
+						 ParseState *pstate, Node *gexpr,
+						 List **targetlist, List *sortClause,
+						 ParseExprKind exprKind, bool useSQL99, bool toplevel)
+{
+	TargetEntry *tle;
+
+	if (useSQL99)
+		tle = findTargetlistEntrySQL99(pstate, gexpr,
+									   targetlist, exprKind);
+	else
+		tle = findTargetlistEntrySQL92(pstate, gexpr,
+									   targetlist, exprKind);
+
+	return addTargetToGroupClause(flatresult, seen_local,
+								  pstate, tle,
+								  targetlist, sortClause,
+								  toplevel, exprLocation(gexpr));
+}
+
 /*
  * Transform a list of expressions within a GROUP BY clause or grouping set.
  *
@@ -2619,10 +2652,14 @@ transformGroupingSet(List **flatresult,
  * aggregates or a HAVING clause with no GROUP BY; the output is one row per
  * grouping set even if the input is empty.
  *
+ * If GROUP BY ALL is specified, the groupClause is inferred to be all the
+ * non-aggregate, non-window expressions in the targetlist.
+ *
  * Returns the transformed (flat) groupClause.
  *
  * pstate		ParseState
  * grouplist	clause to transform
+ * groupByAll	is this a GROUP BY ALL statement?
  * groupingSets reference to list to contain the grouping set tree
  * targetlist	reference to TargetEntry list
  * sortClause	ORDER BY clause (SortGroupClause nodes)
@@ -2630,7 +2667,8 @@ transformGroupingSet(List **flatresult,
  * useSQL99		SQL99 rather than SQL92 syntax
  */
 List *
-transformGroupClause(ParseState *pstate, List *grouplist, List **groupingSets,
+transformGroupClause(ParseState *pstate, List *grouplist, bool groupByAll,
+					 List **groupingSets,
 					 List **targetlist, List *sortClause,
 					 ParseExprKind exprKind, bool useSQL99)
 {
@@ -2641,6 +2679,74 @@ transformGroupClause(ParseState *pstate, List *grouplist, List **groupingSets,
 	bool		hasGroupingSets = false;
 	Bitmapset  *seen_local = NULL;
 
+	/* Handle GROUP BY ALL */
+	if (groupByAll)
+	{
+		/* There cannot have been any explicit grouplist items */
+		Assert(grouplist == NIL);
+
+		/* Iterate over targets, adding acceptable ones to the result list */
+		foreach_ptr(TargetEntry, tle, *targetlist)
+		{
+			Index		ref;
+
+			/* Ignore junk TLEs */
+			if (tle->resjunk)
+				continue;
+
+			/*
+			 * TLEs containing aggregates are not okay to add to GROUP BY
+			 * (compare checkTargetlistEntrySQL92).  But the SQL standard
+			 * directs us to skip them, so it's fine.
+			 */
+			if (pstate->p_hasAggs &&
+				contain_aggs_of_level((Node *) tle->expr, 0))
+				continue;
+
+			/*
+			 * Likewise, TLEs containing window functions are not okay to add
+			 * to GROUP BY.  At this writing, the SQL standard is silent on
+			 * what to do with them, but by analogy to aggregates we'll just
+			 * skip them.
+			 */
+			if (pstate->p_hasWindowFuncs &&
+				contain_windowfuncs((Node *) tle->expr))
+				continue;
+
+			/*
+			 * Otherwise, add the TLE to the result.  We route it through the
+			 * same code path that an explicit GROUP BY item uses, so that a
+			 * target expression also named in ORDER BY inherits that item's
+			 * equality/ordering semantics (see addTargetToGroupClause).  This
+			 * is what makes GROUP BY ALL exactly equivalent to spelling out
+			 * the same expressions.
+			 *
+			 * We specify the parse location as the TLE's location, despite
+			 * the comment for addTargetToGroupList discouraging that.  The
+			 * only other thing we could point to is the ALL keyword, which
+			 * seems unhelpful when there are multiple TLEs.
+			 */
+			ref = addTargetToGroupClause(&result, seen_local,
+										 pstate, tle,
+										 targetlist, sortClause,
+										 true /* toplevel */ ,
+										 exprLocation((Node *) tle->expr));
+			if (ref > 0)
+				seen_local = bms_add_member(seen_local, ref);
+		}
+
+		/* If we found any acceptable targets, we're done */
+		if (result != NIL)
+			return result;
+
+		/*
+		 * Otherwise, the SQL standard says to treat it like "GROUP BY ()".
+		 * Build a representation of that, and let the rest of this function
+		 * handle it.
+		 */
+		grouplist = list_make1(makeGroupingSet(GROUPING_SET_EMPTY, NIL, -1));
+	}
+
 	/*
 	 * Recursively flatten implicit RowExprs. (Technically this is only needed
 	 * for GROUP BY, per the syntax rules for grouping sets, but we do it
@@ -2819,6 +2925,7 @@ transformWindowDefinitions(ParseState *pstate,
 										  true /* force SQL99 rules */ );
 		partitionClause = transformGroupClause(pstate,
 											   windef->partitionClause,
+											   false /* not GROUP BY ALL */ ,
 											   NULL,
 											   targetlist,
 											   orderClause,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index ccfd7f4728f..999ec963f9b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -6198,7 +6198,9 @@ get_basic_select_query(Query *query, deparse_context *context)
 		save_ingroupby = context->inGroupBy;
 		context->inGroupBy = true;
 
-		if (query->groupingSets == NIL)
+		if (query->groupByAll)
+			appendStringInfoString(buf, "ALL");
+		else if (query->groupingSets == NIL)
 		{
 			sep = "";
 			foreach(l, query->groupClause)
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 056d48eb279..061d0cc38ea 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202610201
+#define CATALOG_VERSION_NO	202610271
 
 #endif
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0debcd193ab..4a0646943d7 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -217,6 +217,7 @@ typedef struct Query
 
 	List	   *groupClause;	/* a list of SortGroupClause's */
 	bool		groupDistinct;	/* was GROUP BY DISTINCT used? */
+	bool		groupByAll;		/* was GROUP BY ALL used? */
 
 	List	   *groupingSets;	/* a list of GroupingSet's if present */
 
@@ -2237,6 +2238,7 @@ typedef struct SelectStmt
 	Node	   *whereClause;	/* WHERE qualification */
 	List	   *groupClause;	/* GROUP BY clauses */
 	bool		groupDistinct;	/* Is this GROUP BY DISTINCT? */
+	bool		groupByAll;		/* Is this GROUP BY ALL? */
 	Node	   *havingClause;	/* HAVING conditional-expression */
 	List	   *windowClause;	/* WINDOW window_name AS (...), ... */
 
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index ca815a9d1bb..fe234611007 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -26,6 +26,7 @@ extern Node *transformLimitClause(ParseState *pstate, Node *clause,
 								  ParseExprKind exprKind, const char *constructName,
 								  LimitOption limitOption);
 extern List *transformGroupClause(ParseState *pstate, List *grouplist,
+								  bool groupByAll,
 								  List **groupingSets,
 								  List **targetlist, List *sortClause,
 								  ParseExprKind exprKind, bool useSQL99);
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index 7d07619956f..b5e6dc6b5d2 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1716,6 +1716,163 @@ NOTICE:  drop cascades to table t1c
 drop table t2;
 drop table t3;
 drop table p_t1;
+--
+-- Test GROUP BY ALL
+--
+-- We don't care about the data here, just the proper transformation of the
+-- GROUP BY clause, so test some queries and verify the EXPLAIN plans.
+--
+CREATE TEMP TABLE t1 (
+  a int,
+  b int,
+  c int
+);
+-- basic example
+EXPLAIN (COSTS OFF) SELECT b, COUNT(*) FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: b
+   ->  Seq Scan on t1
+(3 rows)
+
+-- multiple columns, non-consecutive order
+EXPLAIN (COSTS OFF) SELECT a, SUM(b), b FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: a, b
+   ->  Seq Scan on t1
+(3 rows)
+
+-- multi columns, no aggregate
+EXPLAIN (COSTS OFF) SELECT a + b FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: (a + b)
+   ->  Seq Scan on t1
+(3 rows)
+
+-- check we detect a non-top-level aggregate
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + 4 FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: a
+   ->  Seq Scan on t1
+(3 rows)
+
+-- including grouped column is okay
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + a FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: a
+   ->  Seq Scan on t1
+(3 rows)
+
+-- including non-grouped column, not so much
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY ALL;
+ERROR:  column "t1.c" must appear in the GROUP BY clause or be used in an aggregate function
+LINE 1: EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY AL...
+                                               ^
+-- all aggregates, should reduce to GROUP BY ()
+EXPLAIN (COSTS OFF) SELECT COUNT(a), SUM(b) FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ Aggregate
+   Group Key: ()
+   ->  Seq Scan on t1
+(3 rows)
+
+-- likewise with empty target list
+EXPLAIN (COSTS OFF) SELECT FROM t1 GROUP BY ALL;
+      QUERY PLAN       
+-----------------------
+ Result
+   Replaces: Aggregate
+(2 rows)
+
+-- window functions are not to be included in GROUP BY, either
+EXPLAIN (COSTS OFF) SELECT a, COUNT(a) OVER (PARTITION BY a) FROM t1 GROUP BY ALL;
+            QUERY PLAN            
+----------------------------------
+ WindowAgg
+   Window: w1 AS (PARTITION BY a)
+   ->  Sort
+         Sort Key: a
+         ->  HashAggregate
+               Group Key: a
+               ->  Seq Scan on t1
+(7 rows)
+
+-- all cols
+EXPLAIN (COSTS OFF) SELECT *, count(*) FROM t1 GROUP BY ALL;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: a, b, c
+   ->  Seq Scan on t1
+(3 rows)
+
+-- group by all with grouping element(s) (equivalent to GROUP BY's
+-- default behavior, explicit antithesis to GROUP BY DISTINCT)
+EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t1 GROUP BY ALL a;
+      QUERY PLAN      
+----------------------
+ HashAggregate
+   Group Key: a
+   ->  Seq Scan on t1
+(3 rows)
+
+-- verify deparsing of GROUP BY ALL
+CREATE TEMP VIEW v1 AS SELECT b, COUNT(*) FROM t1 GROUP BY ALL;
+SELECT pg_get_viewdef('v1'::regclass);
+    pg_get_viewdef     
+-----------------------
+  SELECT b,           +
+     count(*) AS count+
+    FROM t1           +
+   GROUP BY ALL;
+(1 row)
+
+DROP VIEW v1;
+DROP TABLE t1;
+-- GROUP BY ALL must inherit equality semantics from a matching ORDER BY item,
+-- exactly as an explicitly-spelled-out GROUP BY does.  Using record_image_ops
+-- (bytewise equality) via ORDER BY ... USING, the byte-distinct values
+-- row(1.0) and row(1.00) must form two groups, not one.
+CREATE TYPE gba_rec AS (x numeric);
+CREATE TEMP TABLE t_gba (a gba_rec);
+INSERT INTO t_gba VALUES (row(1.0)::gba_rec), (row(1.00)::gba_rec);
+-- explicit GROUP BY: two groups (record_image inequality from ORDER BY)
+SELECT a, count(*) FROM t_gba
+  GROUP BY a ORDER BY a USING operator(pg_catalog.*<);
+   a    | count 
+--------+-------
+ (1.00) |     1
+ (1.0)  |     1
+(2 rows)
+
+-- GROUP BY ALL: must match the explicit form (two groups)
+SELECT a, count(*) FROM t_gba
+  GROUP BY ALL ORDER BY a USING operator(pg_catalog.*<);
+   a    | count 
+--------+-------
+ (1.00) |     1
+ (1.0)  |     1
+(2 rows)
+
+-- without the record_image ORDER BY, default record_ops merges them (one group)
+SELECT a, count(*) FROM t_gba GROUP BY ALL ORDER BY a;
+   a   | count 
+-------+-------
+ (1.0) |     2
+(1 row)
+
+DROP TABLE t_gba;
+DROP TYPE gba_rec;
 -- A composite type used by the tests below to exercise the asymmetry
 -- between record_ops (per-field equality, the default) and record_image_ops
 -- (bytewise equality): values like row(1.0) and row(1.00) are field-equal
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 91f8342166f..8f132acbd0b 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -605,6 +605,82 @@ drop table t2;
 drop table t3;
 drop table p_t1;
 
+--
+-- Test GROUP BY ALL
+--
+-- We don't care about the data here, just the proper transformation of the
+-- GROUP BY clause, so test some queries and verify the EXPLAIN plans.
+--
+
+CREATE TEMP TABLE t1 (
+  a int,
+  b int,
+  c int
+);
+
+-- basic example
+EXPLAIN (COSTS OFF) SELECT b, COUNT(*) FROM t1 GROUP BY ALL;
+
+-- multiple columns, non-consecutive order
+EXPLAIN (COSTS OFF) SELECT a, SUM(b), b FROM t1 GROUP BY ALL;
+
+-- multi columns, no aggregate
+EXPLAIN (COSTS OFF) SELECT a + b FROM t1 GROUP BY ALL;
+
+-- check we detect a non-top-level aggregate
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + 4 FROM t1 GROUP BY ALL;
+
+-- including grouped column is okay
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + a FROM t1 GROUP BY ALL;
+
+-- including non-grouped column, not so much
+EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY ALL;
+
+-- all aggregates, should reduce to GROUP BY ()
+EXPLAIN (COSTS OFF) SELECT COUNT(a), SUM(b) FROM t1 GROUP BY ALL;
+
+-- likewise with empty target list
+EXPLAIN (COSTS OFF) SELECT FROM t1 GROUP BY ALL;
+
+-- window functions are not to be included in GROUP BY, either
+EXPLAIN (COSTS OFF) SELECT a, COUNT(a) OVER (PARTITION BY a) FROM t1 GROUP BY ALL;
+
+-- all cols
+EXPLAIN (COSTS OFF) SELECT *, count(*) FROM t1 GROUP BY ALL;
+
+-- group by all with grouping element(s) (equivalent to GROUP BY's
+-- default behavior, explicit antithesis to GROUP BY DISTINCT)
+EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t1 GROUP BY ALL a;
+
+-- verify deparsing of GROUP BY ALL
+CREATE TEMP VIEW v1 AS SELECT b, COUNT(*) FROM t1 GROUP BY ALL;
+SELECT pg_get_viewdef('v1'::regclass);
+
+DROP VIEW v1;
+DROP TABLE t1;
+
+-- GROUP BY ALL must inherit equality semantics from a matching ORDER BY item,
+-- exactly as an explicitly-spelled-out GROUP BY does.  Using record_image_ops
+-- (bytewise equality) via ORDER BY ... USING, the byte-distinct values
+-- row(1.0) and row(1.00) must form two groups, not one.
+CREATE TYPE gba_rec AS (x numeric);
+CREATE TEMP TABLE t_gba (a gba_rec);
+INSERT INTO t_gba VALUES (row(1.0)::gba_rec), (row(1.00)::gba_rec);
+
+-- explicit GROUP BY: two groups (record_image inequality from ORDER BY)
+SELECT a, count(*) FROM t_gba
+  GROUP BY a ORDER BY a USING operator(pg_catalog.*<);
+
+-- GROUP BY ALL: must match the explicit form (two groups)
+SELECT a, count(*) FROM t_gba
+  GROUP BY ALL ORDER BY a USING operator(pg_catalog.*<);
+
+-- without the record_image ORDER BY, default record_ops merges them (one group)
+SELECT a, count(*) FROM t_gba GROUP BY ALL ORDER BY a;
+
+DROP TABLE t_gba;
+DROP TYPE gba_rec;
+
 -- A composite type used by the tests below to exercise the asymmetry
 -- between record_ops (per-field equality, the default) and record_image_ops
 -- (bytewise equality): values like row(1.0) and row(1.00) are field-equal
-- 
2.54.0 (Apple Git-157)

