Make subquery aliases optional in the FROM clause.
authorDean Rasheed <[email protected]>
Wed, 20 Jul 2022 08:29:42 +0000 (09:29 +0100)
committerDean Rasheed <[email protected]>
Wed, 20 Jul 2022 08:29:42 +0000 (09:29 +0100)
This allows aliases for sub-SELECTs and VALUES clauses in the FROM
clause to be omitted.

This is an extension of the SQL standard, supported by some other
database systems, and so eases the transition from such systems, as
well as removing the minor inconvenience caused by requiring these
aliases.

Patch by me, reviewed by Tom Lane.

Discussion: https://postgr.es/m/CAEZATCUCGCf82=hxd9N5n6xGHPyYpQnxW8HneeH+uP7yNALkWA@mail.gmail.com

doc/src/sgml/ref/select.sgml
src/backend/parser/analyze.c
src/backend/parser/gram.y
src/backend/parser/parse_clause.c
src/backend/parser/parse_relation.c
src/backend/utils/adt/ruleutils.c
src/include/parser/parse_node.h
src/interfaces/ecpg/preproc/ecpg.addons
src/test/regress/expected/subselect.out
src/test/regress/sql/subselect.sql

index 80bb8adcaccc1c7197bf1a3c5f2e9e1fa7bd2e2b..410c80e730b02d776f7254240bdb3bc92ef931ce 100644 (file)
@@ -51,7 +51,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replaceable class="parameter">expression</replac
 
     [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
                 [ TABLESAMPLE <replaceable class="parameter">sampling_method</replaceable> ( <replaceable class="parameter">argument</replaceable> [, ...] ) [ REPEATABLE ( <replaceable class="parameter">seed</replaceable> ) ] ]
-    [ LATERAL ] ( <replaceable class="parameter">select</replaceable> ) [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ]
+    [ LATERAL ] ( <replaceable class="parameter">select</replaceable> ) [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
     <replaceable class="parameter">with_query_name</replaceable> [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
     [ LATERAL ] <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> [, ...] ] )
                 [ WITH ORDINALITY ] [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
@@ -490,8 +490,8 @@ TABLE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
         output were created as a temporary table for the duration of
         this single <command>SELECT</command> command.  Note that the
         sub-<command>SELECT</command> must be surrounded by
-        parentheses, and an alias <emphasis>must</emphasis> be
-        provided for it.  A
+        parentheses, and an alias can be provided in the same way as for a
+        table.  A
         <link linkend="sql-values"><command>VALUES</command></link> command
         can also be used here.
        </para>
@@ -2041,6 +2041,16 @@ SELECT 2+2;
    </para>
   </refsect2>
 
+  <refsect2>
+   <title>Omitting sub-<command>SELECT</command> aliases in <literal>FROM</literal></title>
+
+   <para>
+    According to the SQL standard, a sub-<command>SELECT</command> in the
+    <literal>FROM</literal> list must have an alias.  In
+    <productname>PostgreSQL</productname>, this alias may be omitted.
+   </para>
+  </refsect2>
+
   <refsect2>
    <title><literal>ONLY</literal> and Inheritance</title>
 
index 8ed2c4b8c7f5cee7b44f93d4d2d0ce278d073262..6688c2a865b8ed2ad26771ece5a344d2f02767c4 100644 (file)
@@ -3291,7 +3291,7 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
            foreach(rt, qry->rtable)
            {
                RangeTblEntry *rte = (RangeTblEntry *) lfirst(rt);
-               char       *rtename;
+               char       *rtename = rte->eref->aliasname;
 
                ++i;
                if (!rte->inFromCl)
@@ -3302,15 +3302,22 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
                 * name and needs to be skipped (otherwise it might hide a
                 * base relation with the same name), except if it has a USING
                 * alias, which *is* visible.
+                *
+                * Subquery and values RTEs without aliases are never visible
+                * as relation names and must always be skipped.
                 */
-               if (rte->rtekind == RTE_JOIN && rte->alias == NULL)
+               if (rte->alias == NULL)
                {
-                   if (rte->join_using_alias == NULL)
+                   if (rte->rtekind == RTE_JOIN)
+                   {
+                       if (rte->join_using_alias == NULL)
+                           continue;
+                       rtename = rte->join_using_alias->aliasname;
+                   }
+                   else if (rte->rtekind == RTE_SUBQUERY ||
+                            rte->rtekind == RTE_VALUES)
                        continue;
-                   rtename = rte->join_using_alias->aliasname;
                }
-               else
-                   rtename = rte->eref->aliasname;
 
                if (strcmp(rtename, thisrel->relname) == 0)
                {
index df5ceea910c6f1d6aa11ead824fc148060babb4a..d649a1b8d13bc6754c50265aae1f236ac63b0df3 100644 (file)
@@ -13379,33 +13379,6 @@ table_ref: relation_expr opt_alias_clause
                    n->lateral = false;
                    n->subquery = $1;
                    n->alias = $2;
-                   /*
-                    * The SQL spec does not permit a subselect
-                    * (<derived_table>) without an alias clause,
-                    * so we don't either.  This avoids the problem
-                    * of needing to invent a unique refname for it.
-                    * That could be surmounted if there's sufficient
-                    * popular demand, but for now let's just implement
-                    * the spec and see if anyone complains.
-                    * However, it does seem like a good idea to emit
-                    * an error message that's better than "syntax error".
-                    */
-                   if ($2 == NULL)
-                   {
-                       if (IsA($1, SelectStmt) &&
-                           ((SelectStmt *) $1)->valuesLists)
-                           ereport(ERROR,
-                                   (errcode(ERRCODE_SYNTAX_ERROR),
-                                    errmsg("VALUES in FROM must have an alias"),
-                                    errhint("For example, FROM (VALUES ...) [AS] foo."),
-                                    parser_errposition(@1)));
-                       else
-                           ereport(ERROR,
-                                   (errcode(ERRCODE_SYNTAX_ERROR),
-                                    errmsg("subquery in FROM must have an alias"),
-                                    errhint("For example, FROM (SELECT ...) [AS] foo."),
-                                    parser_errposition(@1)));
-                   }
                    $$ = (Node *) n;
                }
            | LATERAL_P select_with_parens opt_alias_clause
@@ -13415,23 +13388,6 @@ table_ref: relation_expr opt_alias_clause
                    n->lateral = true;
                    n->subquery = $2;
                    n->alias = $3;
-                   /* same comment as above */
-                   if ($3 == NULL)
-                   {
-                       if (IsA($2, SelectStmt) &&
-                           ((SelectStmt *) $2)->valuesLists)
-                           ereport(ERROR,
-                                   (errcode(ERRCODE_SYNTAX_ERROR),
-                                    errmsg("VALUES in FROM must have an alias"),
-                                    errhint("For example, FROM (VALUES ...) [AS] foo."),
-                                    parser_errposition(@2)));
-                       else
-                           ereport(ERROR,
-                                   (errcode(ERRCODE_SYNTAX_ERROR),
-                                    errmsg("subquery in FROM must have an alias"),
-                                    errhint("For example, FROM (SELECT ...) [AS] foo."),
-                                    parser_errposition(@2)));
-                   }
                    $$ = (Node *) n;
                }
            | joined_table
index c655d188c765c8f45b9e8d5934c77dd33e713e54..5a18107e799b44798d385467fa550a35edc8ce0f 100644 (file)
@@ -403,16 +403,6 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
 {
    Query      *query;
 
-   /*
-    * We require user to supply an alias for a subselect, per SQL92. To relax
-    * this, we'd have to be prepared to gin up a unique alias for an
-    * unlabeled subselect.  (This is just elog, not ereport, because the
-    * grammar should have enforced it already.  It'd probably be better to
-    * report the error here, but we don't have a good error location here.)
-    */
-   if (r->alias == NULL)
-       elog(ERROR, "subquery in FROM must have an alias");
-
    /*
     * Set p_expr_kind to show this parse level is recursing to a subselect.
     * We can't be nested within any expression, so don't need save-restore
@@ -430,10 +420,14 @@ transformRangeSubselect(ParseState *pstate, RangeSubselect *r)
    pstate->p_lateral_active = r->lateral;
 
    /*
-    * Analyze and transform the subquery.
+    * Analyze and transform the subquery.  Note that if the subquery doesn't
+    * have an alias, it can't be explicitly selected for locking, but locking
+    * might still be required (if there is an all-tables locking clause).
     */
    query = parse_sub_analyze(r->subquery, pstate, NULL,
-                             isLockedRefname(pstate, r->alias->aliasname),
+                             isLockedRefname(pstate,
+                                             r->alias == NULL ? NULL :
+                                             r->alias->aliasname),
                              true);
 
    /* Restore state */
index 926dcbf30e2f8af4ba30ae78a464194dd089edb7..4e635561aaf12ceb4d8ba8e475f715e88d7a0449 100644 (file)
@@ -1577,7 +1577,11 @@ addRangeTableEntryForRelation(ParseState *pstate,
  * Then, construct and return a ParseNamespaceItem for the new RTE.
  *
  * This is much like addRangeTableEntry() except that it makes a subquery RTE.
- * Note that an alias clause *must* be supplied.
+ *
+ * If the subquery does not have an alias, the auto-generated relation name in
+ * the returned ParseNamespaceItem will be marked as not visible, and so only
+ * unqualified references to the subquery columns will be allowed, and the
+ * relation name will not conflict with others in the pstate's namespace list.
  */
 ParseNamespaceItem *
 addRangeTableEntryForSubquery(ParseState *pstate,
@@ -1587,7 +1591,6 @@ addRangeTableEntryForSubquery(ParseState *pstate,
                              bool inFromCl)
 {
    RangeTblEntry *rte = makeNode(RangeTblEntry);
-   char       *refname = alias->aliasname;
    Alias      *eref;
    int         numaliases;
    List       *coltypes,
@@ -1595,6 +1598,7 @@ addRangeTableEntryForSubquery(ParseState *pstate,
               *colcollations;
    int         varattno;
    ListCell   *tlistitem;
+   ParseNamespaceItem *nsitem;
 
    Assert(pstate != NULL);
 
@@ -1602,7 +1606,7 @@ addRangeTableEntryForSubquery(ParseState *pstate,
    rte->subquery = subquery;
    rte->alias = alias;
 
-   eref = copyObject(alias);
+   eref = alias ? copyObject(alias) : makeAlias("unnamed_subquery", NIL);
    numaliases = list_length(eref->colnames);
 
    /* fill in any unspecified alias columns, and extract column type info */
@@ -1634,7 +1638,7 @@ addRangeTableEntryForSubquery(ParseState *pstate,
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
                 errmsg("table \"%s\" has %d columns available but %d columns specified",
-                       refname, varattno, numaliases)));
+                       eref->aliasname, varattno, numaliases)));
 
    rte->eref = eref;
 
@@ -1665,8 +1669,15 @@ addRangeTableEntryForSubquery(ParseState *pstate,
     * Build a ParseNamespaceItem, but don't add it to the pstate's namespace
     * list --- caller must do that if appropriate.
     */
-   return buildNSItemFromLists(rte, list_length(pstate->p_rtable),
-                               coltypes, coltypmods, colcollations);
+   nsitem = buildNSItemFromLists(rte, list_length(pstate->p_rtable),
+                                 coltypes, coltypmods, colcollations);
+
+   /*
+    * Mark it visible as a relation name only if it had a user-written alias.
+    */
+   nsitem->p_rel_visible = (alias != NULL);
+
+   return nsitem;
 }
 
 /*
@@ -2520,6 +2531,10 @@ addRangeTableEntryForENR(ParseState *pstate,
  * This is used when we have not yet done transformLockingClause, but need
  * to know the correct lock to take during initial opening of relations.
  *
+ * Note that refname may be NULL (for a subquery without an alias), in which
+ * case the relation can't be locked by name, but it might still be locked if
+ * a locking clause requests that all tables be locked.
+ *
  * Note: we pay no attention to whether it's FOR UPDATE vs FOR SHARE,
  * since the table-level lock is the same either way.
  */
@@ -2544,7 +2559,7 @@ isLockedRefname(ParseState *pstate, const char *refname)
            /* all tables used in query */
            return true;
        }
-       else
+       else if (refname != NULL)
        {
            /* just the named tables */
            ListCell   *l2;
index 26cf4fa9a0e48d2978cf5a534e2065908e30cfc1..513bf0f026f2d740af05945fd12f35a6fd613883 100644 (file)
@@ -11725,7 +11725,11 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
        else if (rte->rtekind == RTE_SUBQUERY ||
                 rte->rtekind == RTE_VALUES)
        {
-           /* Alias is syntactically required for SUBQUERY and VALUES */
+           /*
+            * For a subquery, always print alias.  This makes the output SQL
+            * spec-compliant, even though we allow the alias to be omitted on
+            * input.
+            */
            printalias = true;
        }
        else if (rte->rtekind == RTE_CTE)
index cf9c75902588ad0561a962a5bc0d6c53ea76b518..962ebf65de185d0f86dc4145219a5bdac7edd113 100644 (file)
@@ -246,8 +246,11 @@ struct ParseState
  * visible for qualified references) but it does hide their columns
  * (unqualified references to the columns refer to the JOIN, not the member
  * tables, so we must not complain that such a reference is ambiguous).
- * Various special RTEs such as NEW/OLD for rules may also appear with only
- * one flag set.
+ * Conversely, a subquery without an alias does not hide the columns selected
+ * by the subquery, but it does hide the auto-generated relation name (so the
+ * subquery columns are visible for unqualified references only).  Various
+ * special RTEs such as NEW/OLD for rules may also appear with only one flag
+ * set.
  *
  * While processing the FROM clause, namespace items may appear with
  * p_lateral_only set, meaning they are visible only to LATERAL
index 94f7d4a3ec2b0ac053089f9fb2ad2c40f342a54e..e94da2a3f8d4c5a68d83ff1e7c2970fb21ed9ba7 100644 (file)
@@ -449,12 +449,6 @@ ECPG: into_clauseINTOOptTempTableName block
                        $$= cat2_str(mm_strdup("into"), $2);
                    }
    | ecpg_into { $$ = EMPTY; }
-ECPG: table_refselect_with_parensopt_alias_clause addon
-   if ($2 == NULL)
-       mmerror(PARSE_ERROR, ET_ERROR, "subquery in FROM must have an alias");
-ECPG: table_refLATERAL_Pselect_with_parensopt_alias_clause addon
-   if ($3 == NULL)
-       mmerror(PARSE_ERROR, ET_ERROR, "subquery in FROM must have an alias");
 ECPG: TypenameSimpleTypenameopt_array_bounds block
    {   $$ = cat2_str($1, $2.str); }
 ECPG: TypenameSETOFSimpleTypenameopt_array_bounds block
index 45c75eecc5f2e836aa56b117d8b6092e9a7ae595..63d26d44fc3b3017445fc3acf5bf1e70516a51bf 100644 (file)
@@ -30,6 +30,12 @@ SELECT * FROM ((SELECT 1 AS x)) ss;
  1
 (1 row)
 
+SELECT * FROM ((SELECT 1 AS x)), ((SELECT * FROM ((SELECT 2 AS y))));
+ x | y 
+---+---
+ 1 | 2
+(1 row)
+
 (SELECT 2) UNION SELECT 2;
  ?column? 
 ----------
@@ -196,6 +202,69 @@ SELECT f1 AS "Correlated Field"
                 3
 (5 rows)
 
+-- Subselects without aliases
+SELECT count FROM (SELECT COUNT(DISTINCT name) FROM road);
+ count 
+-------
+  2911
+(1 row)
+
+SELECT COUNT(*) FROM (SELECT DISTINCT name FROM road);
+ count 
+-------
+  2911
+(1 row)
+
+SELECT * FROM (SELECT * FROM int4_tbl), (VALUES (123456)) WHERE f1 = column1;
+   f1   | column1 
+--------+---------
+ 123456 |  123456
+(1 row)
+
+CREATE VIEW view_unnamed_ss AS
+SELECT * FROM (SELECT * FROM (SELECT abs(f1) AS a1 FROM int4_tbl)),
+              (SELECT * FROM int8_tbl)
+  WHERE a1 < 10 AND q1 > a1 ORDER BY q1, q2;
+SELECT * FROM view_unnamed_ss;
+ a1 |        q1        |        q2         
+----+------------------+-------------------
+  0 |              123 |               456
+  0 |              123 |  4567890123456789
+  0 | 4567890123456789 | -4567890123456789
+  0 | 4567890123456789 |               123
+  0 | 4567890123456789 |  4567890123456789
+(5 rows)
+
+\sv view_unnamed_ss
+CREATE OR REPLACE VIEW public.view_unnamed_ss AS
+ SELECT unnamed_subquery.a1,
+    unnamed_subquery_1.q1,
+    unnamed_subquery_1.q2
+   FROM ( SELECT unnamed_subquery_2.a1
+           FROM ( SELECT abs(int4_tbl.f1) AS a1
+                   FROM int4_tbl) unnamed_subquery_2) unnamed_subquery,
+    ( SELECT int8_tbl.q1,
+            int8_tbl.q2
+           FROM int8_tbl) unnamed_subquery_1
+  WHERE unnamed_subquery.a1 < 10 AND unnamed_subquery_1.q1 > unnamed_subquery.a1
+  ORDER BY unnamed_subquery_1.q1, unnamed_subquery_1.q2
+DROP VIEW view_unnamed_ss;
+-- Test matching of locking clause to correct alias
+CREATE VIEW view_unnamed_ss_locking AS
+SELECT * FROM (SELECT * FROM int4_tbl), int8_tbl AS unnamed_subquery
+  WHERE f1 = q1
+  FOR UPDATE OF unnamed_subquery;
+\sv view_unnamed_ss_locking
+CREATE OR REPLACE VIEW public.view_unnamed_ss_locking AS
+ SELECT unnamed_subquery.f1,
+    unnamed_subquery_1.q1,
+    unnamed_subquery_1.q2
+   FROM ( SELECT int4_tbl.f1
+           FROM int4_tbl) unnamed_subquery,
+    int8_tbl unnamed_subquery_1
+  WHERE unnamed_subquery.f1 = unnamed_subquery_1.q1
+ FOR UPDATE OF unnamed_subquery_1
+DROP VIEW view_unnamed_ss_locking;
 --
 -- Use some existing tables in the regression test
 --
index 94ba91f5bb35f65a582f52a0b115368795aa07d7..40276708c99268559f0518aa1080d42d2bb98e52 100644 (file)
@@ -13,6 +13,8 @@ SELECT 1 AS zero WHERE 1 IN (SELECT 2);
 SELECT * FROM (SELECT 1 AS x) ss;
 SELECT * FROM ((SELECT 1 AS x)) ss;
 
+SELECT * FROM ((SELECT 1 AS x)), ((SELECT * FROM ((SELECT 2 AS y))));
+
 (SELECT 2) UNION SELECT 2;
 ((SELECT 2)) UNION SELECT 2;
 
@@ -80,6 +82,35 @@ SELECT f1 AS "Correlated Field"
   WHERE (f1, f2) IN (SELECT f2, CAST(f3 AS int4) FROM SUBSELECT_TBL
                      WHERE f3 IS NOT NULL);
 
+-- Subselects without aliases
+
+SELECT count FROM (SELECT COUNT(DISTINCT name) FROM road);
+SELECT COUNT(*) FROM (SELECT DISTINCT name FROM road);
+
+SELECT * FROM (SELECT * FROM int4_tbl), (VALUES (123456)) WHERE f1 = column1;
+
+CREATE VIEW view_unnamed_ss AS
+SELECT * FROM (SELECT * FROM (SELECT abs(f1) AS a1 FROM int4_tbl)),
+              (SELECT * FROM int8_tbl)
+  WHERE a1 < 10 AND q1 > a1 ORDER BY q1, q2;
+
+SELECT * FROM view_unnamed_ss;
+
+\sv view_unnamed_ss
+
+DROP VIEW view_unnamed_ss;
+
+-- Test matching of locking clause to correct alias
+
+CREATE VIEW view_unnamed_ss_locking AS
+SELECT * FROM (SELECT * FROM int4_tbl), int8_tbl AS unnamed_subquery
+  WHERE f1 = q1
+  FOR UPDATE OF unnamed_subquery;
+
+\sv view_unnamed_ss_locking
+
+DROP VIEW view_unnamed_ss_locking;
+
 --
 -- Use some existing tables in the regression test
 --