From cfebf56c4b52e3ab7526a2200cc1ef2738f20ade Mon Sep 17 00:00:00 2001
From: Andrey Borodin <amborodin@acm.org>
Date: Mon, 27 Jul 2026 14:52:11 +0500
Subject: [PATCH 1/2] Allow the FROM clause to be written before the SELECT
 clause

A query may now name its data sources first, as in

	FROM films SELECT title, kind

which means the same as SELECT title, kind FROM films.  The SELECT clause
may also be left out entirely, in which case all columns are selected, so
that FROM films is equivalent to SELECT * FROM films.  The remaining
clauses keep their usual positions.

Writing a query this way puts the clauses in the order in which they are
logically evaluated.  It is chiefly useful interactively: naming the
source tables first is what lets a client complete the column names of
the select list, which is impossible while the select list comes first.
This is not standard SQL, but several other systems accept the same
extension.

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          |  50 +++++++
 src/backend/parser/gram.y             |  91 +++++++++++--
 src/pl/plpgsql/src/pl_gram.y          |   7 +-
 src/test/regress/expected/plpgsql.out |  16 +++
 src/test/regress/expected/select.out  | 179 ++++++++++++++++++++++++++
 src/test/regress/sql/plpgsql.sql      |  15 +++
 src/test/regress/sql/select.sql       |  49 +++++++
 7 files changed, 395 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 837bf6d7832..3926e95c7ce 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1775,6 +1775,40 @@ SELECT * FROM (SELECT * FROM mytable FOR UPDATE) ss ORDER BY column1;
   </caution>
   </refsect2>
 
+  <refsect2 id="sql-from-first" xreflabel="FROM-First Syntax">
+   <title><literal>FROM</literal>-First Syntax</title>
+
+   <para>
+    The <literal>FROM</literal> clause can be written ahead of the
+    <literal>SELECT</literal> clause, so that
+<programlisting>
+FROM <replaceable class="parameter">from_item</replaceable> [, ...] SELECT <replaceable class="parameter">select_list</replaceable>
+</programlisting>
+    means the same as
+<programlisting>
+SELECT <replaceable class="parameter">select_list</replaceable> FROM <replaceable class="parameter">from_item</replaceable> [, ...]
+</programlisting>
+    All the remaining clauses keep their usual positions, that is, they
+    follow the select list.  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 sources before the
+    expressions computed from them, which is the order in which the clauses
+    are logically evaluated (see above).  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 +2091,22 @@ SELECT 2+2;
    </para>
   </refsect2>
 
+  <refsect2>
+   <title><literal>FROM</literal>-First Syntax</title>
+
+   <para>
+    Writing the <literal>FROM</literal> clause ahead of the
+    <literal>SELECT</literal> clause, 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 the same 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-from-first"/> 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..8c5672e5192 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,13 @@ 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 FROM-first productions accept the FROM clause ahead of the SELECT
+ * clause, and allow the SELECT clause to be omitted altogether; leaving it
+ * out means SELECT *.  That is not in the SQL standard, but it lets a query
+ * be written in the order its clauses are logically evaluated in, which is
+ * handy for clients completing column names.  They produce exactly the same
+ * SelectStmt as the standard spelling does.
  */
 simple_select:
 			SELECT opt_all_clause opt_target_list
@@ -13782,23 +13790,61 @@ simple_select:
 					n->windowClause = $9;
 					$$ = (Node *) n;
 				}
+			| FROM from_list SELECT opt_all_clause opt_target_list
+			into_clause where_clause
+			group_clause having_clause window_clause
+				{
+					SelectStmt *n = makeNode(SelectStmt);
+
+					n->targetList = $5;
+					n->intoClause = $6;
+					n->fromClause = $2;
+					n->whereClause = $7;
+					n->groupClause = ($8)->list;
+					n->groupDistinct = ($8)->distinct;
+					n->havingClause = $9;
+					n->windowClause = $10;
+					$$ = (Node *) n;
+				}
+			| FROM from_list SELECT distinct_clause target_list
+			into_clause where_clause
+			group_clause having_clause window_clause
+				{
+					SelectStmt *n = makeNode(SelectStmt);
+
+					n->distinctClause = $4;
+					n->targetList = $5;
+					n->intoClause = $6;
+					n->fromClause = $2;
+					n->whereClause = $7;
+					n->groupClause = ($8)->list;
+					n->groupDistinct = ($8)->distinct;
+					n->havingClause = $9;
+					n->windowClause = $10;
+					$$ = (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;
+				}
 			| 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 +20188,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/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..9599a9fbfe8 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 select count(*) into strict x where unique1 < p1;
+  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..f38753460e0 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -521,6 +521,185 @@ SELECT * FROM nocols n, LATERAL (VALUES(n.*)) v;
 --
 (1 row)
 
+--
+-- Test FROM-first syntax: FROM may be written ahead of SELECT, and SELECT
+-- may then be omitted 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 SELECT WHERE q1 = 123;
+--
+(2 rows)
+
+-- the other clauses keep their usual places and meanings
+FROM int8_tbl SELECT q1, count(*) WHERE q2 <> 456
+  GROUP BY q1 HAVING count(*) > 1 ORDER BY q1;
+        q1        | count 
+------------------+-------
+ 4567890123456789 |     3
+(1 row)
+
+FROM int8_tbl SELECT q1, count(*) OVER w
+  WINDOW w AS (PARTITION BY q1) 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 SELECT q1 WHERE q1 = 123;
+INSERT INTO fromfirst_tbl FROM int8_tbl SELECT DISTINCT q2 WHERE q2 = 456;
+TABLE fromfirst_tbl;
+  q  
+-----
+ 123
+ 123
+ 456
+(3 rows)
+
+COPY (FROM fromfirst_tbl) TO stdout;
+123
+123
+456
+FROM fromfirst_tbl SELECT q INTO TEMP TABLE fromfirst_into WHERE q = 123;
+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 SELECT q1 WHERE q2 > 0;
+SELECT pg_get_viewdef('fromfirst_view'::regclass);
+  pg_get_viewdef   
+-------------------
+  SELECT q1       +
+    FROM int8_tbl +
+   WHERE (q2 > 0);
+(1 row)
+
+-- SELECT has to come right after the FROM list, and DISTINCT still requires
+-- a target list
+FROM int8_tbl WHERE q1 = 123 SELECT q1;
+ERROR:  syntax error at or near "SELECT"
+LINE 1: FROM int8_tbl WHERE q1 = 123 SELECT q1;
+                                     ^
+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..abe2cb37f3a 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 select count(*) into strict x where unique1 < p1;
+  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..3ecbec44dd5 100644
--- a/src/test/regress/sql/select.sql
+++ b/src/test/regress/sql/select.sql
@@ -148,6 +148,55 @@ CREATE TEMP TABLE nocols();
 INSERT INTO nocols DEFAULT VALUES;
 SELECT * FROM nocols n, LATERAL (VALUES(n.*)) v;
 
+--
+-- Test FROM-first syntax: FROM may be written ahead of SELECT, and SELECT
+-- may then be omitted 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 SELECT WHERE q1 = 123;
+
+-- the other clauses keep their usual places and meanings
+FROM int8_tbl SELECT q1, count(*) WHERE q2 <> 456
+  GROUP BY q1 HAVING count(*) > 1 ORDER BY q1;
+FROM int8_tbl SELECT q1, count(*) OVER w
+  WINDOW w AS (PARTITION BY q1) 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 SELECT q1 WHERE q1 = 123;
+INSERT INTO fromfirst_tbl FROM int8_tbl SELECT DISTINCT q2 WHERE q2 = 456;
+TABLE fromfirst_tbl;
+COPY (FROM fromfirst_tbl) TO stdout;
+FROM fromfirst_tbl SELECT q INTO TEMP TABLE fromfirst_into WHERE q = 123;
+TABLE fromfirst_into;
+
+-- a stored query is deparsed as an ordinary SELECT
+CREATE TEMP VIEW fromfirst_view AS FROM int8_tbl SELECT q1 WHERE q2 > 0;
+SELECT pg_get_viewdef('fromfirst_view'::regclass);
+
+-- SELECT has to come right after the FROM list, and DISTINCT still requires
+-- a target list
+FROM int8_tbl WHERE q1 = 123 SELECT q1;
+FROM int8_tbl SELECT DISTINCT;
+FROM;
+
 --
 -- Test ORDER BY options
 --
-- 
2.50.1 (Apple Git-155)

