]> granicus.if.org Git - postgresql/commitdiff
Fix pg_dump's handling of circular dependencies in views. REL9_5_STABLE github/REL9_5_STABLE
authorTom Lane <tgl@sss.pgh.pa.us>
Sat, 26 Oct 2019 21:37:19 +0000 (17:37 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sat, 26 Oct 2019 21:37:19 +0000 (17:37 -0400)
This is a back-patch of the v10 commit d8c05aff5.  The motivation for
doing this now is that we received a complaint that a view with a
circular dependency is dumped with an extra bogus command "ALTER TABLE
ONLY myview REPLICA IDENTITY NOTHING", because pg_dump forgets that it's
a view not a table, and the relreplident value stored for a view is that.
So you'll get an error message during restore even if not using --clean;
this would break pg_upgrade for example.  While that could be handled
with a one-line patch, it seems better to back-patch d8c05aff5, since that
produces cleaner more future-proof output and fixes an additional bug.

Per gripe from Alex Williams.  Back-patch to 9.4-9.6 (even if 9.3 were
still in support, it hasn't got REPLICA IDENTITY so no bug).

Discussion: https://postgr.es/m/NFqxoEi7-8Rw9OW0f-GwHcjvS2I4YQXov4g9OoWv3i7lVOZdLWkAWl9jQQqwEaUq6WV0vdobromhW82e8y5I0_59yZTXcZnXsrmFuldlmZc=@protonmail.com

Original commit message follows:

pg_dump's traditional solution for breaking a circular dependency involving
a view was to create the view with CREATE TABLE and then later issue CREATE
RULE "_RETURN" ... to convert the table to a view, relying on the backend's
very very ancient code that supports making views that way.  We've wanted
to get rid of that kluge for a long time, but the thing that finally
motivates doing something about it is the recognition that this method
fails with the --clean option, because it leads to issuing DROP RULE
"_RETURN" followed by DROP TABLE --- and the backend won't let you drop a
view's _RETURN rule.

Instead, let's break circular dependencies by initially creating the view
using CREATE VIEW AS SELECT NULL::columntype AS columnname, ... (so that
it has the right column names and types to support external references,
but no dependencies beyond the column data types), and then later dumping
the ON SELECT rule using the spelling CREATE OR REPLACE VIEW.  This method
wasn't available when this code was originally written, but it's been
possible since PG 7.3, so it seems fine to start relying on it now.

To solve the --clean problem, make the dropStmt for an ON SELECT rule
be CREATE OR REPLACE VIEW with the same dummy target list as above.
In this way, during the DROP phase, we first reduce the view to have
no extra dependencies, and then we can drop it entirely when we've
gotten rid of whatever had a circular dependency on it.

(Note: this should work adequately well with the --if-exists option, since
the CREATE OR REPLACE VIEW will go through whether the view exists or not.
It could fail if the view exists with a conflicting column set, but we
don't really support --clean against a non-matching database anyway.)

This allows cleaning up some other kluges inside pg_dump, notably that
we don't need a notion of reloptions attached to a rule anymore.

Although this is a bug fix, commit to HEAD only for now.  The problem's
existed for a long time and we've had relatively few complaints, so it
doesn't really seem worth taking risks to fix it in the back branches.
We might revisit that choice if no problems emerge.

Discussion: <19092.1479325184@sss.pgh.pa.us>

src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dump.h
src/bin/pg_dump/pg_dump_sort.c

index 6c1a211399ca55c6216d8763b7ee79c7ea467c22..c9fcb0231dd164191a2ebb1febfce4e4e70b0da0 100644 (file)
@@ -5363,7 +5363,7 @@ getTables(Archive *fout, int *numTables)
                else
                        selectDumpableTable(&tblinfo[i], dopt);
                tblinfo[i].interesting = tblinfo[i].dobj.dump;
-
+               tblinfo[i].dummy_view = false;  /* might get set during sort */
                tblinfo[i].postponed_def = false;               /* might get set during sort */
 
                /*
@@ -6165,16 +6165,6 @@ getRules(Archive *fout, int *numRules)
                }
                else
                        ruleinfo[i].separate = true;
-
-               /*
-                * If we're forced to break a dependency loop by dumping a view as a
-                * table and separate _RETURN rule, we'll move the view's reloptions
-                * to the rule.  (This is necessary because tables and views have
-                * different valid reloptions, so we can't apply the options until the
-                * backend knows it's a view.)  Otherwise the rule's reloptions stay
-                * NULL.
-                */
-               ruleinfo[i].reloptions = NULL;
        }
 
        PQclear(res);
@@ -13746,6 +13736,50 @@ createViewAsClause(Archive *fout, TableInfo *tbinfo)
        return result;
 }
 
+/*
+ * Create a dummy AS clause for a view.  This is used when the real view
+ * definition has to be postponed because of circular dependencies.
+ * We must duplicate the view's external properties -- column names and types
+ * (including collation) -- so that it works for subsequent references.
+ *
+ * This returns a new buffer which must be freed by the caller.
+ */
+static PQExpBuffer
+createDummyViewAsClause(Archive *fout, TableInfo *tbinfo)
+{
+       PQExpBuffer result = createPQExpBuffer();
+       int                     j;
+
+       appendPQExpBufferStr(result, "SELECT");
+
+       for (j = 0; j < tbinfo->numatts; j++)
+       {
+               if (j > 0)
+                       appendPQExpBufferChar(result, ',');
+               appendPQExpBufferStr(result, "\n    ");
+
+               appendPQExpBuffer(result, "NULL::%s", tbinfo->atttypnames[j]);
+
+               /*
+                * Must add collation if not default for the type, because CREATE OR
+                * REPLACE VIEW won't change it
+                */
+               if (OidIsValid(tbinfo->attcollation[j]))
+               {
+                       CollInfo   *coll;
+
+                       coll = findCollationByOid(tbinfo->attcollation[j]);
+                       if (coll)
+                               appendPQExpBuffer(result, " COLLATE %s",
+                                                                 fmtQualifiedDumpable(coll));
+               }
+
+               appendPQExpBuffer(result, " AS %s", fmtId(tbinfo->attnames[j]));
+       }
+
+       return result;
+}
+
 /*
  * dumpTableSchema
  *       write the declaration (not data) of one user-defined table or view
@@ -13780,6 +13814,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
        {
                PQExpBuffer result;
 
+               /*
+                * Note: keep this code in sync with the is_view case in dumpRule()
+                */
+
                reltypename = "VIEW";
 
                appendPQExpBuffer(delq, "DROP VIEW %s;\n", qualrelname);
@@ -13790,17 +13828,22 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 
                appendPQExpBuffer(q, "CREATE VIEW %s", qualrelname);
 
-               if (nonemptyReloptions(tbinfo->reloptions))
+               if (tbinfo->dummy_view)
+                       result = createDummyViewAsClause(fout, tbinfo);
+               else
                {
-                       appendPQExpBufferStr(q, " WITH (");
-                       fmtReloptionsArray(fout, q, tbinfo->reloptions, "");
-                       appendPQExpBufferChar(q, ')');
+                       if (nonemptyReloptions(tbinfo->reloptions))
+                       {
+                               appendPQExpBufferStr(q, " WITH (");
+                               fmtReloptionsArray(fout, q, tbinfo->reloptions, "");
+                               appendPQExpBufferChar(q, ')');
+                       }
+                       result = createViewAsClause(fout, tbinfo);
                }
-               result = createViewAsClause(fout, tbinfo);
                appendPQExpBuffer(q, " AS\n%s", result->data);
                destroyPQExpBuffer(result);
 
-               if (tbinfo->checkoption != NULL)
+               if (tbinfo->checkoption != NULL && !tbinfo->dummy_view)
                        appendPQExpBuffer(q, "\n  WITH %s CHECK OPTION", tbinfo->checkoption);
                appendPQExpBufferStr(q, ";\n");
        }
@@ -15426,6 +15469,7 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
 {
        DumpOptions *dopt = fout->dopt;
        TableInfo  *tbinfo = rinfo->ruletable;
+       bool            is_view;
        PQExpBuffer query;
        PQExpBuffer cmd;
        PQExpBuffer delcmd;
@@ -15445,6 +15489,12 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
        if (!rinfo->separate)
                return;
 
+       /*
+        * If it's an ON SELECT rule, we want to print it as a view definition,
+        * instead of a rule.
+        */
+       is_view = (rinfo->ev_type == '1' && rinfo->is_instead);
+
        query = createPQExpBuffer();
        cmd = createPQExpBuffer();
        delcmd = createPQExpBuffer();
@@ -15452,30 +15502,60 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
 
        qtabname = pg_strdup(fmtId(tbinfo->dobj.name));
 
-       if (fout->remoteVersion >= 70300)
+       if (is_view)
        {
-               appendPQExpBuffer(query,
-                                                 "SELECT pg_catalog.pg_get_ruledef('%u'::pg_catalog.oid) AS definition",
-                                                 rinfo->dobj.catId.oid);
+               PQExpBuffer result;
+
+               /*
+                * We need OR REPLACE here because we'll be replacing a dummy view.
+                * Otherwise this should look largely like the regular view dump code.
+                */
+               appendPQExpBuffer(cmd, "CREATE OR REPLACE VIEW %s",
+                                                 fmtQualifiedDumpable(tbinfo));
+               if (nonemptyReloptions(tbinfo->reloptions))
+               {
+                       appendPQExpBufferStr(cmd, " WITH (");
+                       fmtReloptionsArray(fout, cmd, tbinfo->reloptions, "");
+                       appendPQExpBufferChar(cmd, ')');
+               }
+               result = createViewAsClause(fout, tbinfo);
+               appendPQExpBuffer(cmd, " AS\n%s", result->data);
+               destroyPQExpBuffer(result);
+               if (tbinfo->checkoption != NULL)
+                       appendPQExpBuffer(cmd, "\n  WITH %s CHECK OPTION",
+                                                         tbinfo->checkoption);
+               appendPQExpBufferStr(cmd, ";\n");
        }
        else
        {
-               /* Rule name was unique before 7.3 ... */
-               appendPQExpBuffer(query,
-                                                 "SELECT pg_get_ruledef('%s') AS definition",
-                                                 rinfo->dobj.name);
-       }
+               /* In the rule case, just print pg_get_ruledef's result verbatim */
+               if (fout->remoteVersion >= 70300)
+               {
+                       appendPQExpBuffer(query,
+                                                         "SELECT pg_catalog.pg_get_ruledef('%u'::pg_catalog.oid) AS definition",
+                                                         rinfo->dobj.catId.oid);
+               }
+               else
+               {
+                       /* Rule name was unique before 7.3 ... */
+                       appendPQExpBuffer(query,
+                                                         "SELECT pg_get_ruledef('%s') AS definition",
+                                                         rinfo->dobj.name);
+               }
 
-       res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+               res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
-       if (PQntuples(res) != 1)
-       {
-               write_msg(NULL, "query to get rule \"%s\" for table \"%s\" failed: wrong number of rows returned\n",
-                                 rinfo->dobj.name, tbinfo->dobj.name);
-               exit_nicely(1);
-       }
+               if (PQntuples(res) != 1)
+               {
+                       write_msg(NULL, "query to get rule \"%s\" for table \"%s\" failed: wrong number of rows returned\n",
+                                         rinfo->dobj.name, tbinfo->dobj.name);
+                       exit_nicely(1);
+               }
+
+               printfPQExpBuffer(cmd, "%s\n", PQgetvalue(res, 0, 0));
 
-       printfPQExpBuffer(cmd, "%s\n", PQgetvalue(res, 0, 0));
+               PQclear(res);
+       }
 
        /*
         * Add the command to alter the rules replication firing semantics if it
@@ -15501,22 +15581,29 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
                }
        }
 
-       /*
-        * Apply view's reloptions when its ON SELECT rule is separate.
-        */
-       if (nonemptyReloptions(rinfo->reloptions))
+       if (is_view)
+       {
+               /*
+                * We can't DROP a view's ON SELECT rule.  Instead, use CREATE OR
+                * REPLACE VIEW to replace the rule with something with minimal
+                * dependencies.
+                */
+               PQExpBuffer result;
+
+               appendPQExpBuffer(delcmd, "CREATE OR REPLACE VIEW %s",
+                                                 fmtQualifiedDumpable(tbinfo));
+               result = createDummyViewAsClause(fout, tbinfo);
+               appendPQExpBuffer(delcmd, " AS\n%s;\n", result->data);
+               destroyPQExpBuffer(result);
+       }
+       else
        {
-               appendPQExpBuffer(cmd, "ALTER VIEW %s SET (",
+               appendPQExpBuffer(delcmd, "DROP RULE %s ",
+                                                 fmtId(rinfo->dobj.name));
+               appendPQExpBuffer(delcmd, "ON %s;\n",
                                                  fmtQualifiedDumpable(tbinfo));
-               fmtReloptionsArray(fout, cmd, rinfo->reloptions, "");
-               appendPQExpBufferStr(cmd, ");\n");
        }
 
-       appendPQExpBuffer(delcmd, "DROP RULE %s ",
-                                         fmtId(rinfo->dobj.name));
-       appendPQExpBuffer(delcmd, "ON %s;\n",
-                                         fmtQualifiedDumpable(tbinfo));
-
        appendPQExpBuffer(ruleprefix, "RULE %s ON",
                                          fmtId(rinfo->dobj.name));
 
@@ -15538,8 +15625,6 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
                                tbinfo->rolname,
                                rinfo->dobj.catId, 0, rinfo->dobj.dumpId);
 
-       PQclear(res);
-
        free(tag);
        destroyPQExpBuffer(query);
        destroyPQExpBuffer(cmd);
index 27f72e75bd31df09c2675162c7f8ffbd99de419d..748da5d59c2360184012211cffa4df7d5208c38f 100644 (file)
@@ -226,6 +226,7 @@ typedef struct _tableInfo
        int                     relpages;               /* table's size in pages (from pg_class) */
 
        bool            interesting;    /* true if need to collect more data */
+       bool            dummy_view;             /* view's real definition must be postponed */
        bool            postponed_def;  /* matview must be postponed into post-data */
 
        /*
@@ -303,8 +304,6 @@ typedef struct _ruleInfo
        char            ev_enabled;
        bool            separate;               /* TRUE if must dump as separate item */
        /* separate is always true for non-ON SELECT rules */
-       char       *reloptions;         /* options specified by WITH (...) */
-       /* reloptions is only set if we need to dump the options with the rule */
 } RuleInfo;
 
 typedef struct _triggerInfo
index 48bc6685ca6d5c02c551e444d62b33ed11f33ac0..121b26c19a4f4873da9541cbcf02ef4d502f0c6e 100644 (file)
@@ -874,6 +874,7 @@ repairViewRuleLoop(DumpableObject *viewobj,
 {
        /* remove rule's dependency on view */
        removeObjectDependency(ruleobj, viewobj->dumpId);
+       /* flags on the two objects are already set correctly for this case */
 }
 
 /*
@@ -893,27 +894,17 @@ repairViewRuleMultiLoop(DumpableObject *viewobj,
 {
        TableInfo  *viewinfo = (TableInfo *) viewobj;
        RuleInfo   *ruleinfo = (RuleInfo *) ruleobj;
-       int                     i;
 
        /* remove view's dependency on rule */
        removeObjectDependency(viewobj, ruleobj->dumpId);
-       /* pretend view is a plain table and dump it that way */
-       viewinfo->relkind = 'r';        /* RELKIND_RELATION */
+       /* mark view to be printed with a dummy definition */
+       viewinfo->dummy_view = true;
        /* mark rule as needing its own dump */
        ruleinfo->separate = true;
-       /* move any reloptions from view to rule */
-       if (viewinfo->reloptions)
-       {
-               ruleinfo->reloptions = viewinfo->reloptions;
-               viewinfo->reloptions = NULL;
-       }
        /* put back rule's dependency on view */
        addObjectDependency(ruleobj, viewobj->dumpId);
        /* now that rule is separate, it must be post-data */
        addObjectDependency(ruleobj, postDataBoundId);
-       /* also, any triggers on the view must be dumped after the rule */
-       for (i = 0; i < viewinfo->numTriggers; i++)
-               addObjectDependency(&(viewinfo->triggers[i].dobj), ruleobj->dumpId);
 }
 
 /*