Further fixes for CREATE TABLE LIKE: cope with self-referential FKs.
authorTom Lane <[email protected]>
Thu, 19 Nov 2020 20:03:17 +0000 (15:03 -0500)
committerTom Lane <[email protected]>
Thu, 19 Nov 2020 20:03:17 +0000 (15:03 -0500)
Commit 502898192 was too careless about the order of execution of the
additional ALTER TABLE operations generated by expandTableLikeClause.
It just stuck them all at the end, which seems okay for most purposes.
But it falls down in the case where LIKE is importing a primary key
or unique index and the outer CREATE TABLE includes a FOREIGN KEY
constraint that needs to depend on that index.  Weird as that is,
it used to work, so we ought to keep it working.

To fix, make parse_utilcmd.c insert LIKE clauses between index-creation
and FK-creation commands in the transformed list of commands, and change
utility.c so that the commands generated by expandTableLikeClause are
executed immediately not at the end.  One could imagine scenarios where
this wouldn't work either; but currently expandTableLikeClause only
makes column default expressions, CHECK constraints, and indexes, and
this ordering seems fine for those.

Per bug #16730 from Sofoklis Papasofokli.  Like the previous patch,
back-patch to all supported branches.

Discussion: https://postgr.es/m/16730-b902f7e6e0276b30@postgresql.org

src/backend/parser/parse_utilcmd.c
src/backend/tcop/utility.c
src/test/regress/expected/create_table_like.out
src/test/regress/sql/create_table_like.sql

index 78aa87da0d063caffb1a45b6f487600fe0ad8ab1..6102a19c3e7cd6e94b3ee20a02067de0661c07df 100644 (file)
@@ -78,6 +78,7 @@ typedef struct
    List       *ckconstraints;  /* CHECK constraints */
    List       *fkconstraints;  /* FOREIGN KEY constraints */
    List       *ixconstraints;  /* index-creating constraints */
+   List       *likeclauses;    /* LIKE clauses that need post-processing */
    List       *blist;          /* "before list" of things to do before
                                 * creating the table */
    List       *alist;          /* "after list" of things to do after creating
@@ -221,6 +222,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
    cxt.ckconstraints = NIL;
    cxt.fkconstraints = NIL;
    cxt.ixconstraints = NIL;
+   cxt.likeclauses = NIL;
    cxt.blist = NIL;
    cxt.alist = NIL;
    cxt.pkey = NULL;
@@ -285,6 +287,20 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
     */
    transformIndexConstraints(&cxt);
 
+   /*
+    * Re-consideration of LIKE clauses should happen after creation of
+    * indexes, but before creation of foreign keys.  This order is critical
+    * because a LIKE clause may attempt to create a primary key.  If there's
+    * also a pkey in the main CREATE TABLE list, creation of that will not
+    * check for a duplicate at runtime (since index_check_primary_key()
+    * expects that we rejected dups here).  Creation of the LIKE-generated
+    * pkey behaves like ALTER TABLE ADD, so it will check, but obviously that
+    * only works if it happens second.  On the other hand, we want to make
+    * pkeys before foreign key constraints, in case the user tries to make a
+    * self-referential FK.
+    */
+   cxt.alist = list_concat(cxt.alist, cxt.likeclauses);
+
    /*
     * Postprocess foreign-key constraints.
     */
@@ -695,7 +711,7 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
  * Change the LIKE <srctable> portion of a CREATE TABLE statement into
  * column definitions that recreate the user defined column portions of
  * <srctable>.  Also, if there are any LIKE options that we can't fully
- * process at this point, add the TableLikeClause to cxt->alist, which
+ * process at this point, add the TableLikeClause to cxt->likeclauses, which
  * will cause utility.c to call expandTableLikeClause() after the new
  * table has been created.
  */
@@ -862,13 +878,13 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
     * We cannot yet deal with CHECK constraints or indexes, since we don't
     * yet know what column numbers the copied columns will have in the
     * finished table.  If any of those options are specified, add the LIKE
-    * clause to cxt->alist so that expandTableLikeClause will be called after
-    * we do know that.
+    * clause to cxt->likeclauses so that expandTableLikeClause will be called
+    * after we do know that.
     */
    if (table_like_clause->options &
        (CREATE_TABLE_LIKE_CONSTRAINTS |
         CREATE_TABLE_LIKE_INDEXES))
-       cxt->alist = lappend(cxt->alist, table_like_clause);
+       cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
 
    /*
     * Close the parent rel, but keep our AccessShareLock on it until xact
@@ -1991,7 +2007,7 @@ transformFKConstraints(CreateStmtContext *cxt,
     * Note: the ADD CONSTRAINT command must also execute after any index
     * creation commands.  Thus, this should run after
     * transformIndexConstraints, so that the CREATE INDEX commands are
-    * already in cxt->alist.
+    * already in cxt->alist.  See also the handling of cxt->likeclauses.
     */
    if (!isAddConstraint)
    {
@@ -2500,6 +2516,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
    cxt.ckconstraints = NIL;
    cxt.fkconstraints = NIL;
    cxt.ixconstraints = NIL;
+   cxt.likeclauses = NIL;
    cxt.blist = NIL;
    cxt.alist = NIL;
    cxt.pkey = NULL;
index 3a26a81f1694bbdc061872bf6554e3f3186158f7..36ad86828d32269caf5a908699d146dc15f61dc7 100644 (file)
@@ -948,17 +948,22 @@ ProcessUtilitySlow(Node *parsetree,
            case T_CreateForeignTableStmt:
                {
                    List       *stmts;
-                   ListCell   *l;
                    RangeVar   *table_rv = NULL;
 
                    /* Run parse analysis ... */
                    stmts = transformCreateStmt((CreateStmt *) parsetree,
                                                queryString);
 
-                   /* ... and do it */
-                   foreach(l, stmts)
+                   /*
+                    * ... and do it.  We can't use foreach() because we may
+                    * modify the list midway through, so pick off the
+                    * elements one at a time, the hard way.
+                    */
+                   while (stmts != NIL)
                    {
-                       Node       *stmt = (Node *) lfirst(l);
+                       Node       *stmt = (Node *) linitial(stmts);
+
+                       stmts = list_delete_first(stmts);
 
                        if (IsA(stmt, CreateStmt))
                        {
@@ -1022,8 +1027,8 @@ ProcessUtilitySlow(Node *parsetree,
                            /*
                             * Do delayed processing of LIKE options.  This
                             * will result in additional sub-statements for us
-                            * to process.  We can just tack those onto the
-                            * to-do list.
+                            * to process.  Those should get done before any
+                            * remaining actions, so prepend them to "stmts".
                             */
                            TableLikeClause *like = (TableLikeClause *) stmt;
                            List       *morestmts;
@@ -1031,14 +1036,7 @@ ProcessUtilitySlow(Node *parsetree,
                            Assert(table_rv != NULL);
 
                            morestmts = expandTableLikeClause(table_rv, like);
-                           stmts = list_concat(stmts, morestmts);
-
-                           /*
-                            * We don't need a CCI now, besides which the "l"
-                            * list pointer is now possibly invalid, so just
-                            * skip the CCI test below.
-                            */
-                           continue;
+                           stmts = list_concat(morestmts, stmts);
                        }
                        else
                        {
@@ -1056,7 +1054,7 @@ ProcessUtilitySlow(Node *parsetree,
                        }
 
                        /* Need CCI between commands */
-                       if (lnext(l) != NULL)
+                       if (stmts != NIL)
                            CommandCounterIncrement();
                    }
 
index eef56037160579ce8a8f3aeb97d5e749c2fed307..c72591ee159e8a965e84fad0fa9e95b287a349b8 100644 (file)
@@ -114,6 +114,22 @@ INSERT INTO inhg (xx, yy, x) VALUES ('foo', 10, 15); -- should fail
 ERROR:  duplicate key value violates unique constraint "inhg_x_key"
 DETAIL:  Key (x)=(15) already exists.
 DROP TABLE inhg;
+DROP TABLE inhz;
+/* Use primary key imported by LIKE for self-referential FK constraint */
+CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
+\d inhz
+    Table "public.inhz"
+ Column | Type | Modifiers 
+--------+------+-----------
+ x      | text | 
+ xx     | text | not null
+Indexes:
+    "inhz_pkey" PRIMARY KEY, btree (xx)
+Foreign-key constraints:
+    "inhz_x_fkey" FOREIGN KEY (x) REFERENCES inhz(xx)
+Referenced by:
+    TABLE "inhz" CONSTRAINT "inhz_x_fkey" FOREIGN KEY (x) REFERENCES inhz(xx)
+
 DROP TABLE inhz;
 -- including storage and comments
 CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
index 5a5b204251d6e2fe5730a585184349548813a9ed..a7d4a669f5f840b96d653a29f04682d2e0edc6d9 100644 (file)
@@ -66,6 +66,11 @@ INSERT INTO inhg (xx, yy, x) VALUES ('foo', 10, 15); -- should fail
 DROP TABLE inhg;
 DROP TABLE inhz;
 
+/* Use primary key imported by LIKE for self-referential FK constraint */
+CREATE TABLE inhz (x text REFERENCES inhz, LIKE inhx INCLUDING INDEXES);
+\d inhz
+DROP TABLE inhz;
+
 -- including storage and comments
 CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) PRIMARY KEY, b text);
 CREATE INDEX ctlt1_b_key ON ctlt1 (b);