diff --git a/backup/queries_functions.go b/backup/queries_functions.go index 98c90461..0da1c806 100644 --- a/backup/queries_functions.go +++ b/backup/queries_functions.go @@ -879,6 +879,15 @@ func (fs ForeignServer) FQN() string { func GetForeignServers(connectionPool *dbconn.DBConn) []ForeignServer { results := make([]ForeignServer, 0) + // A foreign server that belongs to an extension (pg_depend deptype 'e') is + // normally skipped, since CREATE EXTENSION is expected to recreate it. + // However, gpbackup still dumps the user's foreign tables, which are not + // extension members. When such a table depends on an extension-member + // server -- e.g. a migration tool registered the server under the FDW + // extension with ALTER EXTENSION ... ADD SERVER -- skipping the server + // leaves the dumped foreign table unrestorable ("server ... does not + // exist"). Also dump an extension-member server when a to-be-dumped foreign + // table depends on it, so the backup is self-contained. query := fmt.Sprintf(` SELECT fs.oid, quote_ident(fs.srvname) AS name, @@ -890,8 +899,16 @@ func GetForeignServers(connectionPool *dbconn.DBConn) []ForeignServer { FROM pg_options_to_table(fs.srvoptions) ORDER BY option_name), ', ') AS options FROM pg_foreign_server fs LEFT JOIN pg_foreign_data_wrapper fdw ON fdw.oid = srvfdw - WHERE fs.oid >= %d AND %s - ORDER BY fs.oid`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause("fs")) + WHERE fs.oid >= %d + AND (%s + OR fs.oid IN ( + SELECT ft.ftserver + FROM pg_foreign_table ft + JOIN pg_class c ON c.oid = ft.ftrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE %s AND %s)) + ORDER BY fs.oid`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause("fs"), + relationAndSchemaFilterClause(), ExtensionFilterClause("c")) err := connectionPool.Select(&results, query) gplog.FatalOnError(err) diff --git a/integration/predata_functions_queries_test.go b/integration/predata_functions_queries_test.go index 26e4fb83..2066def5 100644 --- a/integration/predata_functions_queries_test.go +++ b/integration/predata_functions_queries_test.go @@ -997,6 +997,30 @@ LANGUAGE SQL`) Expect(resultServers).To(HaveLen(1)) structmatcher.ExpectStructsToMatchExcluding(&expectedServer, &resultServers[0], "Oid") }) + It("dumps an extension-member server a foreign table depends on, but not an unreferenced one", func() { + // A server registered under an extension (e.g. by a migration tool + // via ALTER EXTENSION ... ADD SERVER) is normally filtered out. It + // must still be dumped when a non-extension foreign table depends on + // it, otherwise that dumped foreign table cannot be restored. A + // server no dumped foreign table references stays filtered. + testhelper.AssertQueryRuns(connectionPool, "CREATE SERVER referencedserver FOREIGN DATA WRAPPER foreigndatawrapper") + testhelper.AssertQueryRuns(connectionPool, "CREATE FOREIGN TABLE public.ref_ftable (i int) SERVER referencedserver") + defer testhelper.AssertQueryRuns(connectionPool, "DROP FOREIGN TABLE public.ref_ftable") + testhelper.AssertQueryRuns(connectionPool, "ALTER EXTENSION plpgsql ADD SERVER referencedserver") + defer testhelper.AssertQueryRuns(connectionPool, "ALTER EXTENSION plpgsql DROP SERVER referencedserver") + testhelper.AssertQueryRuns(connectionPool, "CREATE SERVER orphanserver FOREIGN DATA WRAPPER foreigndatawrapper") + testhelper.AssertQueryRuns(connectionPool, "ALTER EXTENSION plpgsql ADD SERVER orphanserver") + defer testhelper.AssertQueryRuns(connectionPool, "ALTER EXTENSION plpgsql DROP SERVER orphanserver") + + resultServers := backup.GetForeignServers(connectionPool) + + serverNames := make([]string, len(resultServers)) + for i, server := range resultServers { + serverNames[i] = server.Name + } + Expect(serverNames).To(ContainElement("referencedserver")) + Expect(serverNames).ToNot(ContainElement("orphanserver")) + }) }) Describe("GetUserMappings", func() { BeforeEach(func() {