Skip to content

Commit ddcc582

Browse files
committed
Fix pg_dump's handling of circular dependencies in views.
This is a back-patch of the v10 commit d8c05af. 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 d8c05af, 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>
1 parent 7db3651 commit ddcc582

File tree

3 files changed

+137
-62
lines changed

3 files changed

+137
-62
lines changed

src/bin/pg_dump/pg_dump.c

Lines changed: 133 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5019,7 +5019,7 @@ getTables(Archive *fout, int *numTables)
50195019
else
50205020
selectDumpableTable(&tblinfo[i]);
50215021
tblinfo[i].interesting = tblinfo[i].dobj.dump;
5022-
5022+
tblinfo[i].dummy_view = false; /* might get set during sort */
50235023
tblinfo[i].postponed_def = false; /* might get set during sort */
50245024

50255025
/*
@@ -5819,16 +5819,6 @@ getRules(Archive *fout, int *numRules)
58195819
}
58205820
else
58215821
ruleinfo[i].separate = true;
5822-
5823-
/*
5824-
* If we're forced to break a dependency loop by dumping a view as a
5825-
* table and separate _RETURN rule, we'll move the view's reloptions
5826-
* to the rule. (This is necessary because tables and views have
5827-
* different valid reloptions, so we can't apply the options until the
5828-
* backend knows it's a view.) Otherwise the rule's reloptions stay
5829-
* NULL.
5830-
*/
5831-
ruleinfo[i].reloptions = NULL;
58325822
}
58335823

58345824
PQclear(res);
@@ -13057,6 +13047,50 @@ createViewAsClause(Archive *fout, TableInfo *tbinfo)
1305713047
return result;
1305813048
}
1305913049

13050+
/*
13051+
* Create a dummy AS clause for a view. This is used when the real view
13052+
* definition has to be postponed because of circular dependencies.
13053+
* We must duplicate the view's external properties -- column names and types
13054+
* (including collation) -- so that it works for subsequent references.
13055+
*
13056+
* This returns a new buffer which must be freed by the caller.
13057+
*/
13058+
static PQExpBuffer
13059+
createDummyViewAsClause(Archive *fout, TableInfo *tbinfo)
13060+
{
13061+
PQExpBuffer result = createPQExpBuffer();
13062+
int j;
13063+
13064+
appendPQExpBufferStr(result, "SELECT");
13065+
13066+
for (j = 0; j < tbinfo->numatts; j++)
13067+
{
13068+
if (j > 0)
13069+
appendPQExpBufferChar(result, ',');
13070+
appendPQExpBufferStr(result, "\n ");
13071+
13072+
appendPQExpBuffer(result, "NULL::%s", tbinfo->atttypnames[j]);
13073+
13074+
/*
13075+
* Must add collation if not default for the type, because CREATE OR
13076+
* REPLACE VIEW won't change it
13077+
*/
13078+
if (OidIsValid(tbinfo->attcollation[j]))
13079+
{
13080+
CollInfo *coll;
13081+
13082+
coll = findCollationByOid(tbinfo->attcollation[j]);
13083+
if (coll)
13084+
appendPQExpBuffer(result, " COLLATE %s",
13085+
fmtQualifiedDumpable(coll));
13086+
}
13087+
13088+
appendPQExpBuffer(result, " AS %s", fmtId(tbinfo->attnames[j]));
13089+
}
13090+
13091+
return result;
13092+
}
13093+
1306013094
/*
1306113095
* dumpTableSchema
1306213096
* write the declaration (not data) of one user-defined table or view
@@ -13090,6 +13124,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
1309013124
{
1309113125
PQExpBuffer result;
1309213126

13127+
/*
13128+
* Note: keep this code in sync with the is_view case in dumpRule()
13129+
*/
13130+
1309313131
reltypename = "VIEW";
1309413132

1309513133
appendPQExpBuffer(delq, "DROP VIEW %s;\n", qualrelname);
@@ -13100,17 +13138,22 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
1310013138

1310113139
appendPQExpBuffer(q, "CREATE VIEW %s", qualrelname);
1310213140

13103-
if (nonemptyReloptions(tbinfo->reloptions))
13141+
if (tbinfo->dummy_view)
13142+
result = createDummyViewAsClause(fout, tbinfo);
13143+
else
1310413144
{
13105-
appendPQExpBufferStr(q, " WITH (");
13106-
fmtReloptionsArray(fout, q, tbinfo->reloptions, "");
13107-
appendPQExpBufferChar(q, ')');
13145+
if (nonemptyReloptions(tbinfo->reloptions))
13146+
{
13147+
appendPQExpBufferStr(q, " WITH (");
13148+
fmtReloptionsArray(fout, q, tbinfo->reloptions, "");
13149+
appendPQExpBufferChar(q, ')');
13150+
}
13151+
result = createViewAsClause(fout, tbinfo);
1310813152
}
13109-
result = createViewAsClause(fout, tbinfo);
1311013153
appendPQExpBuffer(q, " AS\n%s", result->data);
1311113154
destroyPQExpBuffer(result);
1311213155

13113-
if (tbinfo->checkoption != NULL)
13156+
if (tbinfo->checkoption != NULL && !tbinfo->dummy_view)
1311413157
appendPQExpBuffer(q, "\n WITH %s CHECK OPTION", tbinfo->checkoption);
1311513158
appendPQExpBufferStr(q, ";\n");
1311613159
}
@@ -14721,6 +14764,7 @@ static void
1472114764
dumpRule(Archive *fout, RuleInfo *rinfo)
1472214765
{
1472314766
TableInfo *tbinfo = rinfo->ruletable;
14767+
bool is_view;
1472414768
PQExpBuffer query;
1472514769
PQExpBuffer cmd;
1472614770
PQExpBuffer delcmd;
@@ -14740,37 +14784,73 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
1474014784
if (!rinfo->separate)
1474114785
return;
1474214786

14787+
/*
14788+
* If it's an ON SELECT rule, we want to print it as a view definition,
14789+
* instead of a rule.
14790+
*/
14791+
is_view = (rinfo->ev_type == '1' && rinfo->is_instead);
14792+
1474314793
query = createPQExpBuffer();
1474414794
cmd = createPQExpBuffer();
1474514795
delcmd = createPQExpBuffer();
1474614796
ruleprefix = createPQExpBuffer();
1474714797

1474814798
qtabname = pg_strdup(fmtId(tbinfo->dobj.name));
1474914799

14750-
if (fout->remoteVersion >= 70300)
14800+
if (is_view)
1475114801
{
14752-
appendPQExpBuffer(query,
14753-
"SELECT pg_catalog.pg_get_ruledef('%u'::pg_catalog.oid) AS definition",
14754-
rinfo->dobj.catId.oid);
14802+
PQExpBuffer result;
14803+
14804+
/*
14805+
* We need OR REPLACE here because we'll be replacing a dummy view.
14806+
* Otherwise this should look largely like the regular view dump code.
14807+
*/
14808+
appendPQExpBuffer(cmd, "CREATE OR REPLACE VIEW %s",
14809+
fmtQualifiedDumpable(tbinfo));
14810+
if (nonemptyReloptions(tbinfo->reloptions))
14811+
{
14812+
appendPQExpBufferStr(cmd, " WITH (");
14813+
fmtReloptionsArray(fout, cmd, tbinfo->reloptions, "");
14814+
appendPQExpBufferChar(cmd, ')');
14815+
}
14816+
result = createViewAsClause(fout, tbinfo);
14817+
appendPQExpBuffer(cmd, " AS\n%s", result->data);
14818+
destroyPQExpBuffer(result);
14819+
if (tbinfo->checkoption != NULL)
14820+
appendPQExpBuffer(cmd, "\n WITH %s CHECK OPTION",
14821+
tbinfo->checkoption);
14822+
appendPQExpBufferStr(cmd, ";\n");
1475514823
}
1475614824
else
1475714825
{
14758-
/* Rule name was unique before 7.3 ... */
14759-
appendPQExpBuffer(query,
14760-
"SELECT pg_get_ruledef('%s') AS definition",
14761-
rinfo->dobj.name);
14762-
}
14826+
/* In the rule case, just print pg_get_ruledef's result verbatim */
14827+
if (fout->remoteVersion >= 70300)
14828+
{
14829+
appendPQExpBuffer(query,
14830+
"SELECT pg_catalog.pg_get_ruledef('%u'::pg_catalog.oid) AS definition",
14831+
rinfo->dobj.catId.oid);
14832+
}
14833+
else
14834+
{
14835+
/* Rule name was unique before 7.3 ... */
14836+
appendPQExpBuffer(query,
14837+
"SELECT pg_get_ruledef('%s') AS definition",
14838+
rinfo->dobj.name);
14839+
}
1476314840

14764-
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
14841+
res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
1476514842

14766-
if (PQntuples(res) != 1)
14767-
{
14768-
write_msg(NULL, "query to get rule \"%s\" for table \"%s\" failed: wrong number of rows returned\n",
14769-
rinfo->dobj.name, tbinfo->dobj.name);
14770-
exit_nicely(1);
14771-
}
14843+
if (PQntuples(res) != 1)
14844+
{
14845+
write_msg(NULL, "query to get rule \"%s\" for table \"%s\" failed: wrong number of rows returned\n",
14846+
rinfo->dobj.name, tbinfo->dobj.name);
14847+
exit_nicely(1);
14848+
}
14849+
14850+
printfPQExpBuffer(cmd, "%s\n", PQgetvalue(res, 0, 0));
1477214851

14773-
printfPQExpBuffer(cmd, "%s\n", PQgetvalue(res, 0, 0));
14852+
PQclear(res);
14853+
}
1477414854

1477514855
/*
1477614856
* Add the command to alter the rules replication firing semantics if it
@@ -14796,22 +14876,29 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
1479614876
}
1479714877
}
1479814878

14799-
/*
14800-
* Apply view's reloptions when its ON SELECT rule is separate.
14801-
*/
14802-
if (nonemptyReloptions(rinfo->reloptions))
14879+
if (is_view)
14880+
{
14881+
/*
14882+
* We can't DROP a view's ON SELECT rule. Instead, use CREATE OR
14883+
* REPLACE VIEW to replace the rule with something with minimal
14884+
* dependencies.
14885+
*/
14886+
PQExpBuffer result;
14887+
14888+
appendPQExpBuffer(delcmd, "CREATE OR REPLACE VIEW %s",
14889+
fmtQualifiedDumpable(tbinfo));
14890+
result = createDummyViewAsClause(fout, tbinfo);
14891+
appendPQExpBuffer(delcmd, " AS\n%s;\n", result->data);
14892+
destroyPQExpBuffer(result);
14893+
}
14894+
else
1480314895
{
14804-
appendPQExpBuffer(cmd, "ALTER VIEW %s SET (",
14896+
appendPQExpBuffer(delcmd, "DROP RULE %s ",
14897+
fmtId(rinfo->dobj.name));
14898+
appendPQExpBuffer(delcmd, "ON %s;\n",
1480514899
fmtQualifiedDumpable(tbinfo));
14806-
fmtReloptionsArray(fout, cmd, rinfo->reloptions, "");
14807-
appendPQExpBufferStr(cmd, ");\n");
1480814900
}
1480914901

14810-
appendPQExpBuffer(delcmd, "DROP RULE %s ",
14811-
fmtId(rinfo->dobj.name));
14812-
appendPQExpBuffer(delcmd, "ON %s;\n",
14813-
fmtQualifiedDumpable(tbinfo));
14814-
1481514902
appendPQExpBuffer(ruleprefix, "RULE %s ON",
1481614903
fmtId(rinfo->dobj.name));
1481714904

@@ -14833,8 +14920,6 @@ dumpRule(Archive *fout, RuleInfo *rinfo)
1483314920
tbinfo->rolname,
1483414921
rinfo->dobj.catId, 0, rinfo->dobj.dumpId);
1483514922

14836-
PQclear(res);
14837-
1483814923
free(tag);
1483914924
destroyPQExpBuffer(query);
1484014925
destroyPQExpBuffer(cmd);

src/bin/pg_dump/pg_dump.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ typedef struct _tableInfo
258258
int relpages; /* table's size in pages (from pg_class) */
259259

260260
bool interesting; /* true if need to collect more data */
261+
bool dummy_view; /* view's real definition must be postponed */
261262
bool postponed_def; /* matview must be postponed into post-data */
262263

263264
/*
@@ -335,8 +336,6 @@ typedef struct _ruleInfo
335336
char ev_enabled;
336337
bool separate; /* TRUE if must dump as separate item */
337338
/* separate is always true for non-ON SELECT rules */
338-
char *reloptions; /* options specified by WITH (...) */
339-
/* reloptions is only set if we need to dump the options with the rule */
340339
} RuleInfo;
341340

342341
typedef struct _triggerInfo

src/bin/pg_dump/pg_dump_sort.c

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ repairViewRuleLoop(DumpableObject *viewobj,
868868
{
869869
/* remove rule's dependency on view */
870870
removeObjectDependency(ruleobj, viewobj->dumpId);
871+
/* flags on the two objects are already set correctly for this case */
871872
}
872873

873874
/*
@@ -887,27 +888,17 @@ repairViewRuleMultiLoop(DumpableObject *viewobj,
887888
{
888889
TableInfo *viewinfo = (TableInfo *) viewobj;
889890
RuleInfo *ruleinfo = (RuleInfo *) ruleobj;
890-
int i;
891891

892892
/* remove view's dependency on rule */
893893
removeObjectDependency(viewobj, ruleobj->dumpId);
894-
/* pretend view is a plain table and dump it that way */
895-
viewinfo->relkind = 'r'; /* RELKIND_RELATION */
894+
/* mark view to be printed with a dummy definition */
895+
viewinfo->dummy_view = true;
896896
/* mark rule as needing its own dump */
897897
ruleinfo->separate = true;
898-
/* move any reloptions from view to rule */
899-
if (viewinfo->reloptions)
900-
{
901-
ruleinfo->reloptions = viewinfo->reloptions;
902-
viewinfo->reloptions = NULL;
903-
}
904898
/* put back rule's dependency on view */
905899
addObjectDependency(ruleobj, viewobj->dumpId);
906900
/* now that rule is separate, it must be post-data */
907901
addObjectDependency(ruleobj, postDataBoundId);
908-
/* also, any triggers on the view must be dumped after the rule */
909-
for (i = 0; i < viewinfo->numTriggers; i++)
910-
addObjectDependency(&(viewinfo->triggers[i].dobj), ruleobj->dumpId);
911902
}
912903

913904
/*

0 commit comments

Comments
 (0)