Print the correct aliases for DML target tables in ruleutils.
authorTom Lane <tgl@sss.pgh.pa.us>
Fri, 17 Feb 2023 21:40:34 +0000 (16:40 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Fri, 17 Feb 2023 21:40:34 +0000 (16:40 -0500)
ruleutils.c blindly printed the user-given alias (or nothing if there
hadn't been one) for the target table of INSERT/UPDATE/DELETE queries.
That works a large percentage of the time, but not always: for queries
appearing in WITH, it's possible that we chose a different alias to
avoid conflict with outer-scope names.  Since the chosen alias would
be used in any Var references to the target table, this'd lead to an
inconsistent printout with consequences such as dump/restore failures.

The correct logic for printing (or not) a relation alias was embedded
in get_from_clause_item.  Factor it out to a separate function so that
we don't need a jointree node to use it.  (Only a limited part of that
function can be reached from these new call sites, but this seems like
the cleanest non-duplicative factorization.)

In passing, I got rid of a redundant "\d+ rules_src" step in rules.sql.

Initial report from Jonathan Katz; thanks to Vignesh C for analysis.
This has been broken for a long time, so back-patch to all supported
branches.

Discussion: https://postgr.es/m/e947fa21-24b2-f922-375a-d4f763ef3e4b@postgresql.org
Discussion: https://postgr.es/m/CALDaNm1MMntjmT_NJGp-Z=xbF02qHGAyuSHfYHias3TqQbPF2w@mail.gmail.com

src/backend/utils/adt/ruleutils.c
src/test/regress/expected/rules.out
src/test/regress/sql/rules.sql

index 9ac42efdbc358adcb0008b7e09aae94484d509ca..6dc117dea8e7b02898b25602279bdd93b54fda7f 100644 (file)
@@ -475,6 +475,8 @@ static void get_from_clause(Query *query, const char *prefix,
                                                        deparse_context *context);
 static void get_from_clause_item(Node *jtnode, Query *query,
                                                                 deparse_context *context);
+static void get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
+                                                 deparse_context *context);
 static void get_column_alias_list(deparse_columns *colinfo,
                                                                  deparse_context *context);
 static void get_from_clause_coldeflist(RangeTblFunction *rtfunc,
@@ -6648,12 +6650,14 @@ get_insert_query_def(Query *query, deparse_context *context,
                context->indentLevel += PRETTYINDENT_STD;
                appendStringInfoChar(buf, ' ');
        }
-       appendStringInfo(buf, "INSERT INTO %s ",
+       appendStringInfo(buf, "INSERT INTO %s",
                                         generate_relation_name(rte->relid, NIL));
-       /* INSERT requires AS keyword for target alias */
-       if (rte->alias != NULL)
-               appendStringInfo(buf, "AS %s ",
-                                                quote_identifier(rte->alias->aliasname));
+
+       /* Print the relation alias, if needed; INSERT requires explicit AS */
+       get_rte_alias(rte, query->resultRelation, true, context);
+
+       /* always want a space here */
+       appendStringInfoChar(buf, ' ');
 
        /*
         * Add the insert-column-names list.  Any indirection decoration needed on
@@ -6835,9 +6839,10 @@ get_update_query_def(Query *query, deparse_context *context,
        appendStringInfo(buf, "UPDATE %s%s",
                                         only_marker(rte),
                                         generate_relation_name(rte->relid, NIL));
-       if (rte->alias != NULL)
-               appendStringInfo(buf, " %s",
-                                                quote_identifier(rte->alias->aliasname));
+
+       /* Print the relation alias, if needed */
+       get_rte_alias(rte, query->resultRelation, false, context);
+
        appendStringInfoString(buf, " SET ");
 
        /* Deparse targetlist */
@@ -7043,9 +7048,9 @@ get_delete_query_def(Query *query, deparse_context *context,
        appendStringInfo(buf, "DELETE FROM %s%s",
                                         only_marker(rte),
                                         generate_relation_name(rte->relid, NIL));
-       if (rte->alias != NULL)
-               appendStringInfo(buf, " %s",
-                                                quote_identifier(rte->alias->aliasname));
+
+       /* Print the relation alias, if needed */
+       get_rte_alias(rte, query->resultRelation, false, context);
 
        /* Add the USING clause if given */
        get_from_clause(query, " USING ", context);
@@ -10887,10 +10892,8 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
        {
                int                     varno = ((RangeTblRef *) jtnode)->rtindex;
                RangeTblEntry *rte = rt_fetch(varno, query->rtable);
-               char       *refname = get_rtable_name(varno, context);
                deparse_columns *colinfo = deparse_columns_fetch(varno, dpns);
                RangeTblFunction *rtfunc1 = NULL;
-               bool            printalias;
 
                if (rte->lateral)
                        appendStringInfoString(buf, "LATERAL ");
@@ -11027,59 +11030,7 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
                }
 
                /* Print the relation alias, if needed */
-               printalias = false;
-               if (rte->alias != NULL)
-               {
-                       /* Always print alias if user provided one */
-                       printalias = true;
-               }
-               else if (colinfo->printaliases)
-               {
-                       /* Always print alias if we need to print column aliases */
-                       printalias = true;
-               }
-               else if (rte->rtekind == RTE_RELATION)
-               {
-                       /*
-                        * No need to print alias if it's same as relation name (this
-                        * would normally be the case, but not if set_rtable_names had to
-                        * resolve a conflict).
-                        */
-                       if (strcmp(refname, get_relation_name(rte->relid)) != 0)
-                               printalias = true;
-               }
-               else if (rte->rtekind == RTE_FUNCTION)
-               {
-                       /*
-                        * For a function RTE, always print alias.  This covers possible
-                        * renaming of the function and/or instability of the
-                        * FigureColname rules for things that aren't simple functions.
-                        * Note we'd need to force it anyway for the columndef list case.
-                        */
-                       printalias = true;
-               }
-               else if (rte->rtekind == RTE_SUBQUERY ||
-                                rte->rtekind == RTE_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)
-               {
-                       /*
-                        * No need to print alias if it's same as CTE name (this would
-                        * normally be the case, but not if set_rtable_names had to
-                        * resolve a conflict).
-                        */
-                       if (strcmp(refname, rte->ctename) != 0)
-                               printalias = true;
-               }
-               if (printalias)
-                       appendStringInfo(buf, " %s", quote_identifier(refname));
+               get_rte_alias(rte, varno, false, context);
 
                /* Print the column definitions or aliases, if needed */
                if (rtfunc1 && rtfunc1->funccolnames != NIL)
@@ -11217,6 +11168,77 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
                         (int) nodeTag(jtnode));
 }
 
+/*
+ * get_rte_alias - print the relation's alias, if needed
+ *
+ * If printed, the alias is preceded by a space, or by " AS " if use_as is true.
+ */
+static void
+get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
+                         deparse_context *context)
+{
+       deparse_namespace *dpns = (deparse_namespace *) linitial(context->namespaces);
+       char       *refname = get_rtable_name(varno, context);
+       deparse_columns *colinfo = deparse_columns_fetch(varno, dpns);
+       bool            printalias = false;
+
+       if (rte->alias != NULL)
+       {
+               /* Always print alias if user provided one */
+               printalias = true;
+       }
+       else if (colinfo->printaliases)
+       {
+               /* Always print alias if we need to print column aliases */
+               printalias = true;
+       }
+       else if (rte->rtekind == RTE_RELATION)
+       {
+               /*
+                * No need to print alias if it's same as relation name (this would
+                * normally be the case, but not if set_rtable_names had to resolve a
+                * conflict).
+                */
+               if (strcmp(refname, get_relation_name(rte->relid)) != 0)
+                       printalias = true;
+       }
+       else if (rte->rtekind == RTE_FUNCTION)
+       {
+               /*
+                * For a function RTE, always print alias.  This covers possible
+                * renaming of the function and/or instability of the FigureColname
+                * rules for things that aren't simple functions.  Note we'd need to
+                * force it anyway for the columndef list case.
+                */
+               printalias = true;
+       }
+       else if (rte->rtekind == RTE_SUBQUERY ||
+                        rte->rtekind == RTE_VALUES)
+       {
+               /*
+                * For a subquery, always print alias.  This makes the output
+                * SQL-spec-compliant, even though we allow such aliases to be omitted
+                * on input.
+                */
+               printalias = true;
+       }
+       else if (rte->rtekind == RTE_CTE)
+       {
+               /*
+                * No need to print alias if it's same as CTE name (this would
+                * normally be the case, but not if set_rtable_names had to resolve a
+                * conflict).
+                */
+               if (strcmp(refname, rte->ctename) != 0)
+                       printalias = true;
+       }
+
+       if (printalias)
+               appendStringInfo(context->buf, "%s%s",
+                                                use_as ? " AS " : " ",
+                                                quote_identifier(refname));
+}
+
 /*
  * get_column_alias_list - print column alias list for an RTE
  *
index 174b725fff13396d0156aedbd7fb759846336886..a3a5a62329eed701f68a4e034ed1205343ab6343 100644 (file)
@@ -2998,28 +2998,21 @@ select * from rules_log;
 (16 rows)
 
 create rule r4 as on delete to rules_src do notify rules_src_deletion;
-\d+ rules_src
-                                 Table "public.rules_src"
- Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
---------+---------+-----------+----------+---------+---------+--------------+-------------
- f1     | integer |           |          |         | plain   |              | 
- f2     | integer |           |          | 0       | plain   |              | 
-Rules:
-    r1 AS
-    ON UPDATE TO rules_src DO  INSERT INTO rules_log (f1, f2, tag, id) VALUES (old.f1,old.f2,'old'::text,DEFAULT), (new.f1,new.f2,'new'::text,DEFAULT)
-    r2 AS
-    ON UPDATE TO rules_src DO  VALUES (old.f1,old.f2,'old'::text), (new.f1,new.f2,'new'::text)
-    r3 AS
-    ON INSERT TO rules_src DO  INSERT INTO rules_log (f1, f2, tag, id) VALUES (NULL::integer,NULL::integer,'-'::text,DEFAULT), (new.f1,new.f2,'new'::text,DEFAULT)
-    r4 AS
-    ON DELETE TO rules_src DO
- NOTIFY rules_src_deletion
-
 --
 -- Ensure an aliased target relation for insert is correctly deparsed.
 --
 create rule r5 as on insert to rules_src do instead insert into rules_log AS trgt SELECT NEW.* RETURNING trgt.f1, trgt.f2;
 create rule r6 as on update to rules_src do instead UPDATE rules_log AS trgt SET tag = 'updated' WHERE trgt.f1 = new.f1;
+--
+-- Check deparse disambiguation of INSERT/UPDATE/DELETE targets.
+--
+create rule r7 as on delete to rules_src do instead
+  with wins as (insert into int4_tbl as trgt values (0) returning *),
+       wupd as (update int4_tbl trgt set f1 = f1+1 returning *),
+       wdel as (delete from int4_tbl trgt where f1 = 0 returning *)
+  insert into rules_log AS trgt select old.* from wins, wupd, wdel
+  returning trgt.f1, trgt.f2;
+-- check display of all rules added above
 \d+ rules_src
                                  Table "public.rules_src"
  Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
@@ -3044,6 +3037,26 @@ Rules:
     r6 AS
     ON UPDATE TO rules_src DO INSTEAD  UPDATE rules_log trgt SET tag = 'updated'::text
   WHERE trgt.f1 = new.f1
+    r7 AS
+    ON DELETE TO rules_src DO INSTEAD  WITH wins AS (
+         INSERT INTO int4_tbl AS trgt_1 (f1)
+          VALUES (0)
+          RETURNING trgt_1.f1
+        ), wupd AS (
+         UPDATE int4_tbl trgt_1 SET f1 = trgt_1.f1 + 1
+          RETURNING trgt_1.f1
+        ), wdel AS (
+         DELETE FROM int4_tbl trgt_1
+          WHERE trgt_1.f1 = 0
+          RETURNING trgt_1.f1
+        )
+ INSERT INTO rules_log AS trgt (f1, f2)  SELECT old.f1,
+            old.f2
+           FROM wins,
+            wupd,
+            wdel
+  RETURNING trgt.f1,
+    trgt.f2
 
 --
 -- Also check multiassignment deparsing.
index 1f858129b84d1bace4f4b3cc4a649b11774d5921..ac1a4ce55430147622d71188d68be3e33498f2e4 100644 (file)
@@ -1015,13 +1015,24 @@ insert into rules_src values(22,23), (33,default);
 select * from rules_src;
 select * from rules_log;
 create rule r4 as on delete to rules_src do notify rules_src_deletion;
-\d+ rules_src
 
 --
 -- Ensure an aliased target relation for insert is correctly deparsed.
 --
 create rule r5 as on insert to rules_src do instead insert into rules_log AS trgt SELECT NEW.* RETURNING trgt.f1, trgt.f2;
 create rule r6 as on update to rules_src do instead UPDATE rules_log AS trgt SET tag = 'updated' WHERE trgt.f1 = new.f1;
+
+--
+-- Check deparse disambiguation of INSERT/UPDATE/DELETE targets.
+--
+create rule r7 as on delete to rules_src do instead
+  with wins as (insert into int4_tbl as trgt values (0) returning *),
+       wupd as (update int4_tbl trgt set f1 = f1+1 returning *),
+       wdel as (delete from int4_tbl trgt where f1 = 0 returning *)
+  insert into rules_log AS trgt select old.* from wins, wupd, wdel
+  returning trgt.f1, trgt.f2;
+
+-- check display of all rules added above
 \d+ rules_src
 
 --