From f499d74023cc81551a39acf5de9a055c4b8dcc0b Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Tue, 28 Jul 2026 15:45:15 +0500
Subject: [PATCH v2] Allow a query to be written with its select list last

A query may now name its data first and compute from it afterwards, that
is, with the FROM clause leading and the select list at the end:

	FROM films WHERE len > '02:00' GROUP BY kind SELECT kind, count(*)

The WHERE, GROUP BY, HAVING and WINDOW clauses keep their order relative
to each other and all precede the select list; ORDER BY, LIMIT and the
locking clauses keep their place at the end of the query.  The SELECT
clause may also be left out entirely, in which case all columns are
selected, so that FROM films means SELECT * FROM films.

This is the order in which the clauses are logically evaluated, and it is
chiefly useful interactively: once the tables are known, a client can
complete the column names of the select list, which it has no way to do
while the select list comes first.  psql does so here, and completes the
columns in GROUP BY as well, which it did not offer before for a query of
any shape.  This is not standard SQL, but the standard is unlikely to
ever move the select list, so the spelling is ours to define.

The select list is accepted only at the end, not between the other
clauses: allowing both would mean deciding, at the SELECT token, whether
an omitted WHERE belongs before it or after it, which needs a lookahead
the parser does not have, and the resulting rule would be harder to
explain than it is worth.

The new productions build exactly the same SelectStmt as the standard
spelling does, so nothing downstream of the grammar changes, and stored
queries are displayed in the standard form.  PL/pgSQL now recognizes FROM
as the start of a SQL statement, so that the new form can be used there
too.
---
 doc/src/sgml/ref/select.sgml          |  61 +++++++++
 src/backend/parser/gram.y             |  95 ++++++++++++--
 src/bin/psql/t/010_tab_completion.pl  |  20 +++
 src/bin/psql/tab-complete.in.c        |  51 +++++++-
 src/pl/plpgsql/src/pl_gram.y          |   7 +-
 src/test/regress/expected/plpgsql.out |  16 +++
 src/test/regress/expected/select.out  | 178 ++++++++++++++++++++++++++
 src/test/regress/sql/plpgsql.sql      |  15 +++
 src/test/regress/sql/select.sql       |  48 +++++++
 9 files changed, 478 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 837bf6d7832..1214b4f2fe0 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1775,6 +1775,52 @@ SELECT * FROM (SELECT * FROM mytable FOR UPDATE) ss ORDER BY column1;
   </caution>
   </refsect2>
 
+  <refsect2 id="sql-select-last" xreflabel="Select List Last">
+   <title>Writing the Select List Last</title>
+
+   <para>
+    The clauses of a query may be written in the order in which they are
+    logically evaluated, that is with the <literal>FROM</literal> clause first
+    and the select list last of all:
+<programlisting>
+FROM <replaceable class="parameter">from_item</replaceable> [, ...] [ <replaceable class="parameter">other_clauses</replaceable> ] SELECT <replaceable class="parameter">select_list</replaceable>
+</programlisting>
+    This means the same as the usual spelling of the same query, so
+<programlisting>
+FROM films WHERE len &gt; '02:00' GROUP BY kind SELECT kind, count(*)
+</programlisting>
+    is another way of writing
+<programlisting>
+SELECT kind, count(*) FROM films WHERE len &gt; '02:00' GROUP BY kind
+</programlisting>
+    The <literal>WHERE</literal>, <literal>GROUP BY</literal>,
+    <literal>HAVING</literal> and <literal>WINDOW</literal> clauses keep their
+    order relative to each other, and all of them come before the select list.
+    The clauses that follow the select list in the usual spelling, that is
+    <literal>ORDER BY</literal>, <literal>LIMIT</literal> and the locking
+    clauses, keep their place at the end of the query.
+   </para>
+
+   <para>
+    The <literal>SELECT</literal> clause can also be left out entirely, which
+    is equivalent to writing <literal>SELECT *</literal>; so
+    <literal>FROM films</literal> is much like <literal>TABLE films</literal>,
+    except that the other clauses, such as <literal>WHERE</literal>, can still
+    be used with it.
+   </para>
+
+   <para>
+    This form is accepted wherever an ordinary <command>SELECT</command> is,
+    for example in a sub-<command>SELECT</command>, in a
+    <literal>WITH</literal> query, or in <command>INSERT</command> ...
+    <command>SELECT</command>.  It names the data the query is computed from
+    before the expressions computed from it.  That is convenient when a query
+    is composed interactively, since a client can then complete the column
+    names of the select list, which it has no way to do while the select list
+    comes first.
+   </para>
+  </refsect2>
+
   <refsect2 id="sql-table">
    <title><literal>TABLE</literal> Command</title>
 
@@ -2057,6 +2103,21 @@ SELECT 2+2;
    </para>
   </refsect2>
 
+  <refsect2>
+   <title>Writing the Select List Last</title>
+
+   <para>
+    Writing the select list after the other clauses, or leaving the
+    <literal>SELECT</literal> clause out altogether, is a
+    <productname>PostgreSQL</productname> extension; the SQL standard
+    requires a query specification to begin with <literal>SELECT</literal>.
+    Some other database systems accept a similar extension.  A query written
+    this way is stored and displayed in the standard form, for example when a
+    view definition is shown by <function>pg_get_viewdef</function>.
+    (See <xref linkend="sql-select-last"/> above.)
+   </para>
+  </refsect2>
+
   <refsect2>
    <title>Empty <literal>SELECT</literal> Lists</title>
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 17035fb4d15..a84992bce51 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -165,6 +165,7 @@ static Node *makeBitStringConst(char *str, int location);
 static Node *makeNullAConst(int location);
 static Node *makeAConst(Node *v, int location);
 static RoleSpec *makeRoleSpec(RoleSpecType type, int location);
+static List *makeStarTargetList(void);
 static void check_qualified_name(List *names, core_yyscan_t yyscanner);
 static List *check_func_name(List *names, core_yyscan_t yyscanner);
 static List *check_indirection(List *indirection, core_yyscan_t yyscanner);
@@ -13747,6 +13748,17 @@ select_clause:
  *
  * NOTE: only the leftmost component SelectStmt should have INTO.
  * However, this is not checked by the grammar; parse analysis must check it.
+ *
+ * The last three productions accept the clauses in the order in which they
+ * are logically evaluated: the FROM clause first, the select list last of
+ * all, or left out altogether, in which case it means SELECT *.  That is not
+ * in the SQL standard, but it is handy for clients completing column names,
+ * and it produces exactly the same SelectStmt as the standard spelling does.
+ *
+ * The select list stays at the end of the query, rather than being allowed
+ * anywhere among the other clauses: accepting it in both places would mean
+ * deciding, at the SELECT token, whether an omitted WHERE belongs before it
+ * or after it, which needs a lookahead the parser does not have.
  */
 simple_select:
 			SELECT opt_all_clause opt_target_list
@@ -13782,23 +13794,61 @@ simple_select:
 					n->windowClause = $9;
 					$$ = (Node *) n;
 				}
+			| FROM from_list where_clause
+			group_clause having_clause window_clause
+				{
+					/* omitted SELECT clause is the same as SELECT * */
+					SelectStmt *n = makeNode(SelectStmt);
+
+					n->targetList = makeStarTargetList();
+					n->fromClause = $2;
+					n->whereClause = $3;
+					n->groupClause = ($4)->list;
+					n->groupDistinct = ($4)->distinct;
+					n->havingClause = $5;
+					n->windowClause = $6;
+					$$ = (Node *) n;
+				}
+			| FROM from_list where_clause
+			group_clause having_clause window_clause
+			SELECT opt_all_clause opt_target_list into_clause
+				{
+					SelectStmt *n = makeNode(SelectStmt);
+
+					n->targetList = $9;
+					n->intoClause = $10;
+					n->fromClause = $2;
+					n->whereClause = $3;
+					n->groupClause = ($4)->list;
+					n->groupDistinct = ($4)->distinct;
+					n->havingClause = $5;
+					n->windowClause = $6;
+					$$ = (Node *) n;
+				}
+			| FROM from_list where_clause
+			group_clause having_clause window_clause
+			SELECT distinct_clause target_list into_clause
+				{
+					SelectStmt *n = makeNode(SelectStmt);
+
+					n->distinctClause = $8;
+					n->targetList = $9;
+					n->intoClause = $10;
+					n->fromClause = $2;
+					n->whereClause = $3;
+					n->groupClause = ($4)->list;
+					n->groupDistinct = ($4)->distinct;
+					n->havingClause = $5;
+					n->windowClause = $6;
+					$$ = (Node *) n;
+				}
 			| values_clause							{ $$ = $1; }
 			| TABLE relation_expr
 				{
 					/* same as SELECT * FROM relation_expr */
-					ColumnRef  *cr = makeNode(ColumnRef);
-					ResTarget  *rt = makeNode(ResTarget);
 					SelectStmt *n = makeNode(SelectStmt);
 
-					cr->fields = list_make1(makeNode(A_Star));
-					cr->location = -1;
-
-					rt->name = NULL;
-					rt->indirection = NIL;
-					rt->val = (Node *) cr;
-					rt->location = -1;
-
-					n->targetList = list_make1(rt);
+					n->targetList = makeStarTargetList();
 					n->fromClause = list_make1($2);
 					$$ = (Node *) n;
 				}
@@ -20142,6 +20192,29 @@ makeRoleSpec(RoleSpecType type, int location)
 	return spec;
 }
 
+/* makeStarTargetList --- build the target list for an implicit "SELECT *"
+ *
+ * Used where the syntax doesn't spell out a target list, that is TABLE and
+ * a FROM-first SELECT with the SELECT clause omitted.  The locations are
+ * unknown, since nothing in the source text corresponds to the star.
+ */
+static List *
+makeStarTargetList(void)
+{
+	ColumnRef  *cr = makeNode(ColumnRef);
+	ResTarget  *rt = makeNode(ResTarget);
+
+	cr->fields = list_make1(makeNode(A_Star));
+	cr->location = -1;
+
+	rt->name = NULL;
+	rt->indirection = NIL;
+	rt->val = (Node *) cr;
+	rt->location = -1;
+
+	return list_make1(rt);
+}
+
 /* check_qualified_name --- check the result of qualified_name production
  *
  * It's easiest to let the grammar production for qualified_name allow
diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 64e27ef87a3..1d9e7a84a6b 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -186,6 +186,26 @@ check_completion("select * from TAB\t", qr/tab1 /, "automatically fold case");
 
 clear_query();
 
+# check a query that names its table before the select list: that order is
+# what lets us offer the columns of the table
+check_completion("fr\t", qr/from /, "complete fr<tab> to from");
+
+check_completion("tab1 where c\t\t", qr/c1 +c2/,
+	"offer the table's columns in WHERE when the table comes first");
+
+check_completion("1 = 1 select \t\t", qr/c1 +c2/,
+	"offer the table's columns in a select list written last");
+
+check_completion("c1, c2\t", qr/c1, c2 /,
+	"offer the columns again after a comma");
+
+clear_query();
+
+check_completion("from tab1 group by c\t\t", qr/c1 +c2/,
+	"offer the table's columns in GROUP BY");
+
+clear_query();
+
 # check case-sensitive keyword replacement
 # note: various versions of readline/libedit handle backspacing
 # differently, so just check that the replacement comes out correctly
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1cacc8c3ea2..ced7344b9d1 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1260,7 +1260,8 @@ static const char *const sql_commands[] = {
 	"ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER",
 	"COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE",
 	"DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN",
-	"FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK",
+	"FETCH", "FROM", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO",
+	"LISTEN", "LOAD", "LOCK",
 	"MERGE INTO", "MOVE", "NOTIFY", "PREPARE",
 	"REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "REPACK",
 	"RESET", "REVOKE", "ROLLBACK",
@@ -1885,6 +1886,13 @@ psql_completion(const char *text, int start, int end)
 #define prev8_wd  (previous_words[7])
 #define prev9_wd  (previous_words[8])
 
+	/*
+	 * previous_words[] runs backwards, so this is how to get at the second
+	 * word on the line.  As above, only use this where a HeadMatches() or
+	 * similar test has already proven that there are that many words.
+	 */
+#define second_wd (previous_words[previous_words_count - 2])
+
 	/* Match the last N words before point, case-insensitively. */
 #define TailMatches(...) \
 	TailMatchesImpl(false, previous_words_count, previous_words, \
@@ -4608,6 +4616,47 @@ match_previous_words(int pattern_id,
 	else if (TailMatches("FOREIGN", "SERVER"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_servers);
 
+/*
+ * Queries that name their tables before the select list.  That order is what
+ * makes the select list itself completable: a query beginning with SELECT
+ * doesn't say yet what it selects from.  Columns in the clauses before the
+ * select list are already completed for ordinary queries, but by rules that
+ * take the word just before the clause to be the table name, which is not
+ * where it is here.
+ */
+	/* Complete "FROM <table>" with SELECT or the clauses that can follow */
+	else if (Matches("FROM", MatchAny) && !ends_with(prev_wd, ','))
+		COMPLETE_WITH("SELECT", "WHERE", "GROUP BY", "HAVING", "WINDOW",
+					  "UNION", "INTERSECT", "EXCEPT", "ORDER BY",
+					  "LIMIT", "OFFSET", "FETCH FIRST", "FOR");
+	/* Complete "FROM <table> SELECT" with the table's columns */
+	else if (Matches("FROM", MatchAny, "SELECT"))
+		COMPLETE_WITH_ATTR_PLUS(prev2_wd, "*", "ALL", "DISTINCT");
+	/* Likewise for a select list written after the other clauses */
+	else if (HeadMatches("FROM", MatchAny) && TailMatches("SELECT"))
+		COMPLETE_WITH_ATTR_PLUS(second_wd, "*", "ALL", "DISTINCT");
+	else if (Matches("FROM", MatchAny, "SELECT", "ALL|DISTINCT"))
+		COMPLETE_WITH_ATTR_PLUS(prev3_wd, "*");
+	/* Likewise for each further item of a list, wherever it is written */
+	else if (HeadMatches("FROM", MatchAny) && ends_with(prev_wd, ','))
+		COMPLETE_WITH_ATTR(second_wd);
+	/* The columns are also what WHERE, GROUP BY and ORDER BY are written from */
+	else if (HeadMatches("FROM", MatchAny) &&
+			 TailMatches("WHERE|BY|HAVING"))
+		COMPLETE_WITH_ATTR(second_wd);
+
+	/*
+	 * Complete a select list with the clauses that can follow it.  The last
+	 * word is an item of the list only if the one before it is SELECT or
+	 * ended with a comma; otherwise we are somewhere else in the query and
+	 * have nothing useful to say.
+	 */
+	else if (HeadMatches("FROM", MatchAny) &&
+			 (pg_strcasecmp(prev2_wd, "SELECT") == 0 ||
+			  ends_with(prev2_wd, ',')))
+		COMPLETE_WITH("AS", "INTO", "UNION", "INTERSECT", "EXCEPT",
+					  "ORDER BY", "LIMIT", "OFFSET", "FETCH FIRST", "FOR");
+
 /*
  * GRANT and REVOKE are allowed inside CREATE SCHEMA and
  * ALTER DEFAULT PRIVILEGES, so use TailMatches
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 5009e59a78f..91edc87c19c 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -1996,7 +1996,12 @@ loop_body		: proc_sect K_END K_LOOP opt_label ';'
  * assignment.  Give an appropriate complaint for that, instead of letting
  * the core parser throw an unhelpful "syntax error".
  */
-stmt_execsql	: K_IMPORT
+stmt_execsql	: K_FROM
+					{
+						/* a FROM-first SELECT */
+						$$ = make_execsql_stmt(K_FROM, @1, NULL, &yylval, &yylloc, yyscanner);
+					}
+				| K_IMPORT
 					{
 						$$ = make_execsql_stmt(K_IMPORT, @1, NULL, &yylval, &yylloc, yyscanner);
 					}
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index b37b2abaf80..6e5fe21aef3 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2702,6 +2702,22 @@ select multi_datum_use(42);
  t
 (1 row)
 
+--
+-- Test a FROM-first SELECT as a PL/pgSQL statement
+--
+create function fromfirst_use(p1 int) returns bigint as $$
+declare x bigint;
+begin
+  from tenk1 where unique1 < p1 select count(*) into strict x;
+  return x;
+end$$ language plpgsql;
+select fromfirst_use(10);
+ fromfirst_use 
+---------------
+            10
+(1 row)
+
+drop function fromfirst_use(int);
 --
 -- Test STRICT limiter in both planned and EXECUTE invocations.
 -- Note that a data-modifying query is quasi strict (disallow multi rows)
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 34f040beecc..83c1ca164ad 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -521,6 +521,184 @@ SELECT * FROM nocols n, LATERAL (VALUES(n.*)) v;
 --
 (1 row)
 
+--
+-- Test writing the clauses in the order they are evaluated: FROM first and
+-- the select list last, or left out entirely
+--
+FROM int8_tbl SELECT q1, q2 ORDER BY q1, q2;
+        q1        |        q2         
+------------------+-------------------
+              123 |               456
+              123 |  4567890123456789
+ 4567890123456789 | -4567890123456789
+ 4567890123456789 |               123
+ 4567890123456789 |  4567890123456789
+(5 rows)
+
+FROM int8_tbl ORDER BY q1, q2;
+        q1        |        q2         
+------------------+-------------------
+              123 |               456
+              123 |  4567890123456789
+ 4567890123456789 | -4567890123456789
+ 4567890123456789 |               123
+ 4567890123456789 |  4567890123456789
+(5 rows)
+
+FROM int8_tbl SELECT ALL q1 ORDER BY q1;
+        q1        
+------------------
+              123
+              123
+ 4567890123456789
+ 4567890123456789
+ 4567890123456789
+(5 rows)
+
+FROM int8_tbl SELECT DISTINCT q1 ORDER BY q1;
+        q1        
+------------------
+              123
+ 4567890123456789
+(2 rows)
+
+FROM int8_tbl SELECT DISTINCT ON (q1) q1, q2 ORDER BY q1, q2;
+        q1        |        q2         
+------------------+-------------------
+              123 |               456
+ 4567890123456789 | -4567890123456789
+(2 rows)
+
+-- an empty target list is as acceptable as it is in SELECT ... FROM
+FROM int8_tbl WHERE q1 = 123 SELECT;
+--
+(2 rows)
+
+-- the other clauses keep their usual meanings, and precede the select list
+FROM int8_tbl WHERE q2 <> 456 GROUP BY q1 HAVING count(*) > 1
+  SELECT q1, count(*) ORDER BY q1;
+        q1        | count 
+------------------+-------
+ 4567890123456789 |     3
+(1 row)
+
+FROM int8_tbl WINDOW w AS (PARTITION BY q1)
+  SELECT q1, count(*) OVER w ORDER BY q1 LIMIT 3;
+        q1        | count 
+------------------+-------
+              123 |     2
+              123 |     2
+ 4567890123456789 |     3
+(3 rows)
+
+FROM int8_tbl WHERE q1 <> q2 ORDER BY q1, q2 LIMIT 2 FOR UPDATE;
+ q1  |        q2        
+-----+------------------
+ 123 |              456
+ 123 | 4567890123456789
+(2 rows)
+
+FROM int8_tbl a JOIN int8_tbl b ON a.q2 = b.q1 SELECT a.q1, b.q2
+  ORDER BY a.q1, b.q2;
+        q1        |        q2         
+------------------+-------------------
+              123 | -4567890123456789
+              123 |               123
+              123 |  4567890123456789
+ 4567890123456789 | -4567890123456789
+ 4567890123456789 |               123
+ 4567890123456789 |               456
+ 4567890123456789 |  4567890123456789
+ 4567890123456789 |  4567890123456789
+(8 rows)
+
+FROM generate_series(1, 3) g(i), LATERAL (SELECT i * 2) s(j) SELECT i, j
+  ORDER BY i;
+ i | j 
+---+---
+ 1 | 2
+ 2 | 4
+ 3 | 6
+(3 rows)
+
+-- and it nests like any other SELECT
+SELECT * FROM (FROM int8_tbl SELECT q1 AS x) ss ORDER BY x;
+        x         
+------------------
+              123
+              123
+ 4567890123456789
+ 4567890123456789
+ 4567890123456789
+(5 rows)
+
+WITH cte AS (FROM int8_tbl) FROM cte SELECT count(*);
+ count 
+-------
+     5
+(1 row)
+
+FROM int8_tbl SELECT q1 UNION FROM int8_tbl SELECT q2 ORDER BY 1;
+        q1         
+-------------------
+ -4567890123456789
+               123
+               456
+  4567890123456789
+(4 rows)
+
+(FROM int8_tbl SELECT q1) EXCEPT (FROM int8_tbl SELECT q2) ORDER BY 1;
+ q1 
+----
+(0 rows)
+
+-- works in the places a SELECT can be embedded in another statement
+CREATE TEMP TABLE fromfirst_tbl (q bigint);
+INSERT INTO fromfirst_tbl FROM int8_tbl WHERE q1 = 123 SELECT q1;
+INSERT INTO fromfirst_tbl FROM int8_tbl WHERE q2 = 456 SELECT DISTINCT q2;
+TABLE fromfirst_tbl;
+  q  
+-----
+ 123
+ 123
+ 456
+(3 rows)
+
+COPY (FROM fromfirst_tbl) TO stdout;
+123
+123
+456
+FROM fromfirst_tbl WHERE q = 123 SELECT q INTO TEMP TABLE fromfirst_into;
+TABLE fromfirst_into;
+  q  
+-----
+ 123
+ 123
+(2 rows)
+
+-- a stored query is deparsed as an ordinary SELECT
+CREATE TEMP VIEW fromfirst_view AS FROM int8_tbl WHERE q2 > 0 SELECT q1;
+SELECT pg_get_viewdef('fromfirst_view'::regclass);
+  pg_get_viewdef   
+-------------------
+  SELECT q1       +
+    FROM int8_tbl +
+   WHERE (q2 > 0);
+(1 row)
+
+-- nothing may follow the select list, and DISTINCT still requires one
+FROM int8_tbl SELECT q1 WHERE q1 = 123;
+ERROR:  syntax error at or near "WHERE"
+LINE 1: FROM int8_tbl SELECT q1 WHERE q1 = 123;
+                                ^
+FROM int8_tbl SELECT DISTINCT;
+ERROR:  syntax error at or near ";"
+LINE 1: FROM int8_tbl SELECT DISTINCT;
+                                     ^
+FROM;
+ERROR:  syntax error at or near ";"
+LINE 1: FROM;
+            ^
 --
 -- Test ORDER BY options
 --
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index ae6b67e3e22..8c4e8615691 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -2300,6 +2300,21 @@ end$$ language plpgsql;
 
 select multi_datum_use(42);
 
+--
+-- Test a FROM-first SELECT as a PL/pgSQL statement
+--
+
+create function fromfirst_use(p1 int) returns bigint as $$
+declare x bigint;
+begin
+  from tenk1 where unique1 < p1 select count(*) into strict x;
+  return x;
+end$$ language plpgsql;
+
+select fromfirst_use(10);
+
+drop function fromfirst_use(int);
+
 --
 -- Test STRICT limiter in both planned and EXECUTE invocations.
 -- Note that a data-modifying query is quasi strict (disallow multi rows)
diff --git a/src/test/regress/sql/select.sql b/src/test/regress/sql/select.sql
index 2dfe88d2054..63224f10256 100644
--- a/src/test/regress/sql/select.sql
+++ b/src/test/regress/sql/select.sql
@@ -148,6 +148,54 @@ CREATE TEMP TABLE nocols();
 INSERT INTO nocols DEFAULT VALUES;
 SELECT * FROM nocols n, LATERAL (VALUES(n.*)) v;
 
+--
+-- Test writing the clauses in the order they are evaluated: FROM first and
+-- the select list last, or left out entirely
+--
+FROM int8_tbl SELECT q1, q2 ORDER BY q1, q2;
+FROM int8_tbl ORDER BY q1, q2;
+FROM int8_tbl SELECT ALL q1 ORDER BY q1;
+FROM int8_tbl SELECT DISTINCT q1 ORDER BY q1;
+FROM int8_tbl SELECT DISTINCT ON (q1) q1, q2 ORDER BY q1, q2;
+
+-- an empty target list is as acceptable as it is in SELECT ... FROM
+FROM int8_tbl WHERE q1 = 123 SELECT;
+
+-- the other clauses keep their usual meanings, and precede the select list
+FROM int8_tbl WHERE q2 <> 456 GROUP BY q1 HAVING count(*) > 1
+  SELECT q1, count(*) ORDER BY q1;
+FROM int8_tbl WINDOW w AS (PARTITION BY q1)
+  SELECT q1, count(*) OVER w ORDER BY q1 LIMIT 3;
+FROM int8_tbl WHERE q1 <> q2 ORDER BY q1, q2 LIMIT 2 FOR UPDATE;
+FROM int8_tbl a JOIN int8_tbl b ON a.q2 = b.q1 SELECT a.q1, b.q2
+  ORDER BY a.q1, b.q2;
+FROM generate_series(1, 3) g(i), LATERAL (SELECT i * 2) s(j) SELECT i, j
+  ORDER BY i;
+
+-- and it nests like any other SELECT
+SELECT * FROM (FROM int8_tbl SELECT q1 AS x) ss ORDER BY x;
+WITH cte AS (FROM int8_tbl) FROM cte SELECT count(*);
+FROM int8_tbl SELECT q1 UNION FROM int8_tbl SELECT q2 ORDER BY 1;
+(FROM int8_tbl SELECT q1) EXCEPT (FROM int8_tbl SELECT q2) ORDER BY 1;
+
+-- works in the places a SELECT can be embedded in another statement
+CREATE TEMP TABLE fromfirst_tbl (q bigint);
+INSERT INTO fromfirst_tbl FROM int8_tbl WHERE q1 = 123 SELECT q1;
+INSERT INTO fromfirst_tbl FROM int8_tbl WHERE q2 = 456 SELECT DISTINCT q2;
+TABLE fromfirst_tbl;
+COPY (FROM fromfirst_tbl) TO stdout;
+FROM fromfirst_tbl WHERE q = 123 SELECT q INTO TEMP TABLE fromfirst_into;
+TABLE fromfirst_into;
+
+-- a stored query is deparsed as an ordinary SELECT
+CREATE TEMP VIEW fromfirst_view AS FROM int8_tbl WHERE q2 > 0 SELECT q1;
+SELECT pg_get_viewdef('fromfirst_view'::regclass);
+
+-- nothing may follow the select list, and DISTINCT still requires one
+FROM int8_tbl SELECT q1 WHERE q1 = 123;
+FROM int8_tbl SELECT DISTINCT;
+FROM;
+
 --
 -- Test ORDER BY options
 --
-- 
2.50.1 (Apple Git-155)

