mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 13:12:48 +00:00
fix(restore): leave an extension's own objects to the extension
The --empty pre-clean picked its candidates by owner, on the stated
assumption that extension members are superuser-owned and would never be
selected. That holds only when a superuser installed the extension. A role
that installs one itself owns its functions, so they were listed for a
one-by-one DROP that postgres refuses:
cannot drop function vector_in(cstring,oid,integer)
because extension vector requires it
Under ON_ERROR_STOP that ends the whole restore, which is how a discourse
generation - it declares the vector extension - became unreplayable.
Membership now comes from pg_depend rather than from ownership. Each branch
carries its oid and classid so one NOT EXISTS covers all seven instead of
seven separate predicates, and the schema branch gets the same guard because
an extension can own a schema too. Skipping the members is enough: the dump's
CREATE EXTENSION IF NOT EXISTS finds the surviving extension either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,12 @@
|
|||||||
-- would run every DROP in one transaction and exhaust max_locks_per_transaction on
|
-- would run every DROP in one transaction and exhaust max_locks_per_transaction on
|
||||||
-- large schemas (e.g. gitlab). Also drops user-owned non-public schemas so a dump
|
-- large schemas (e.g. gitlab). Also drops user-owned non-public schemas so a dump
|
||||||
-- that CREATE SCHEMAs (e.g. discourse's discourse_functions) does not fail on an
|
-- that CREATE SCHEMAs (e.g. discourse's discourse_functions) does not fail on an
|
||||||
-- already-existing schema. Extension members (pg_trgm's set_limit) are
|
-- already-existing schema. Objects belonging to an extension are skipped: postgres
|
||||||
-- superuser-owned; IF EXISTS absorbs the CASCADE fallout.
|
-- refuses to drop them one by one ("cannot drop function vector_in(...) because
|
||||||
|
-- extension vector requires it"), and the dump's CREATE EXTENSION IF NOT EXISTS
|
||||||
|
-- finds the surviving extension either way. Owning them is not enough to make them
|
||||||
|
-- droppable - an extension a role installed itself is owned by that role, so the
|
||||||
|
-- owner filter alone lets pgvector's members through.
|
||||||
SELECT format('DROP %s IF EXISTS public.%s CASCADE', obj.type, obj.name)
|
SELECT format('DROP %s IF EXISTS public.%s CASCADE', obj.type, obj.name)
|
||||||
FROM (
|
FROM (
|
||||||
SELECT format('%I', c.relname) AS name,
|
SELECT format('%I', c.relname) AS name,
|
||||||
@@ -13,7 +17,8 @@ SELECT format('DROP %s IF EXISTS public.%s CASCADE', obj.type, obj.name)
|
|||||||
WHEN 'm' THEN 'MATERIALIZED VIEW'
|
WHEN 'm' THEN 'MATERIALIZED VIEW'
|
||||||
WHEN 'f' THEN 'FOREIGN TABLE'
|
WHEN 'f' THEN 'FOREIGN TABLE'
|
||||||
ELSE 'TABLE'
|
ELSE 'TABLE'
|
||||||
END AS type
|
END AS type,
|
||||||
|
c.oid AS objid, 'pg_class'::regclass AS classid
|
||||||
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
|
WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p', 'v', 'm', 'f')
|
||||||
AND pg_get_userbyid(c.relowner) = current_user
|
AND pg_get_userbyid(c.relowner) = current_user
|
||||||
@@ -21,17 +26,20 @@ SELECT format('DROP %s IF EXISTS public.%s CASCADE', obj.type, obj.name)
|
|||||||
-- Overloaded functions share a proname; DROP needs the identity
|
-- Overloaded functions share a proname; DROP needs the identity
|
||||||
-- signature or psql aborts with "function name is not unique".
|
-- signature or psql aborts with "function name is not unique".
|
||||||
SELECT format('%I(%s)', p.proname, pg_get_function_identity_arguments(p.oid)) AS name,
|
SELECT format('%I(%s)', p.proname, pg_get_function_identity_arguments(p.oid)) AS name,
|
||||||
CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS type
|
CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS type,
|
||||||
|
p.oid AS objid, 'pg_proc'::regclass AS classid
|
||||||
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||||
WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p', 'w')
|
WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p', 'w')
|
||||||
AND pg_get_userbyid(p.proowner) = current_user
|
AND pg_get_userbyid(p.proowner) = current_user
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('%I', c.relname) AS name, 'SEQUENCE' AS type
|
SELECT format('%I', c.relname) AS name, 'SEQUENCE' AS type,
|
||||||
|
c.oid AS objid, 'pg_class'::regclass AS classid
|
||||||
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
WHERE n.nspname = 'public' AND c.relkind = 'S'
|
WHERE n.nspname = 'public' AND c.relkind = 'S'
|
||||||
AND pg_get_userbyid(c.relowner) = current_user
|
AND pg_get_userbyid(c.relowner) = current_user
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('%I', t.typname) AS name, 'TYPE' AS type
|
SELECT format('%I', t.typname) AS name, 'TYPE' AS type,
|
||||||
|
t.oid AS objid, 'pg_type'::regclass AS classid
|
||||||
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
|
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
WHERE n.nspname = 'public'
|
WHERE n.nspname = 'public'
|
||||||
AND pg_get_userbyid(t.typowner) = current_user
|
AND pg_get_userbyid(t.typowner) = current_user
|
||||||
@@ -40,25 +48,36 @@ SELECT format('DROP %s IF EXISTS public.%s CASCADE', obj.type, obj.name)
|
|||||||
SELECT 1 FROM pg_class c2
|
SELECT 1 FROM pg_class c2
|
||||||
WHERE c2.oid = t.typrelid AND c2.relkind = 'c')))
|
WHERE c2.oid = t.typrelid AND c2.relkind = 'c')))
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('%I', col.collname) AS name, 'COLLATION' AS type
|
SELECT format('%I', col.collname) AS name, 'COLLATION' AS type,
|
||||||
|
col.oid AS objid, 'pg_collation'::regclass AS classid
|
||||||
FROM pg_collation col JOIN pg_namespace n ON n.oid = col.collnamespace
|
FROM pg_collation col JOIN pg_namespace n ON n.oid = col.collnamespace
|
||||||
WHERE n.nspname = 'public'
|
WHERE n.nspname = 'public'
|
||||||
AND pg_get_userbyid(col.collowner) = current_user
|
AND pg_get_userbyid(col.collowner) = current_user
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('%I', ts.cfgname) AS name, 'TEXT SEARCH CONFIGURATION' AS type
|
SELECT format('%I', ts.cfgname) AS name, 'TEXT SEARCH CONFIGURATION' AS type,
|
||||||
|
ts.oid AS objid, 'pg_ts_config'::regclass AS classid
|
||||||
FROM pg_ts_config ts JOIN pg_namespace n ON n.oid = ts.cfgnamespace
|
FROM pg_ts_config ts JOIN pg_namespace n ON n.oid = ts.cfgnamespace
|
||||||
WHERE n.nspname = 'public'
|
WHERE n.nspname = 'public'
|
||||||
AND pg_get_userbyid(ts.cfgowner) = current_user
|
AND pg_get_userbyid(ts.cfgowner) = current_user
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('%I', d.dictname) AS name, 'TEXT SEARCH DICTIONARY' AS type
|
SELECT format('%I', d.dictname) AS name, 'TEXT SEARCH DICTIONARY' AS type,
|
||||||
|
d.oid AS objid, 'pg_ts_dict'::regclass AS classid
|
||||||
FROM pg_ts_dict d JOIN pg_namespace n ON n.oid = d.dictnamespace
|
FROM pg_ts_dict d JOIN pg_namespace n ON n.oid = d.dictnamespace
|
||||||
WHERE n.nspname = 'public'
|
WHERE n.nspname = 'public'
|
||||||
AND pg_get_userbyid(d.dictowner) = current_user
|
AND pg_get_userbyid(d.dictowner) = current_user
|
||||||
) obj
|
) obj
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_depend dep
|
||||||
|
WHERE dep.classid = obj.classid AND dep.objid = obj.objid
|
||||||
|
AND dep.deptype = 'e')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT format('DROP SCHEMA IF EXISTS %I CASCADE', n.nspname)
|
SELECT format('DROP SCHEMA IF EXISTS %I CASCADE', n.nspname)
|
||||||
FROM pg_namespace n
|
FROM pg_namespace n
|
||||||
WHERE NOT starts_with(n.nspname, 'pg_')
|
WHERE NOT starts_with(n.nspname, 'pg_')
|
||||||
AND n.nspname NOT IN ('public', 'information_schema')
|
AND n.nspname NOT IN ('public', 'information_schema')
|
||||||
AND pg_get_userbyid(n.nspowner) = current_user
|
AND pg_get_userbyid(n.nspowner) = current_user
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_depend dep
|
||||||
|
WHERE dep.classid = 'pg_namespace'::regclass AND dep.objid = n.oid
|
||||||
|
AND dep.deptype = 'e')
|
||||||
\gexec
|
\gexec
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from .helpers import (
|
|||||||
# is not unique") and english_stem_nostop reproduces taiga's text search
|
# is not unique") and english_stem_nostop reproduces taiga's text search
|
||||||
# dictionary abort (duplicate pg_ts_dict_dictname_index).
|
# dictionary abort (duplicate pg_ts_dict_dictname_index).
|
||||||
SCENARIO_SQL = (
|
SCENARIO_SQL = (
|
||||||
|
"CREATE EXTENSION pg_trgm;"
|
||||||
"CREATE SCHEMA discourse_functions;"
|
"CREATE SCHEMA discourse_functions;"
|
||||||
"CREATE TABLE discourse_functions.helper (id int);"
|
"CREATE TABLE discourse_functions.helper (id int);"
|
||||||
"INSERT INTO discourse_functions.helper VALUES (1);"
|
"INSERT INTO discourse_functions.helper VALUES (1);"
|
||||||
@@ -166,6 +167,13 @@ class TestE2EPostgresEmptyDropHard(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(self._scalar("SELECT public.f(41) + public.f();"), "42")
|
self.assertEqual(self._scalar("SELECT public.f(41) + public.f();"), "42")
|
||||||
|
|
||||||
|
def test_the_extension_survived_the_preclean(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
self._scalar("SELECT count(*) FROM pg_extension WHERE extname='pg_trgm';"),
|
||||||
|
"1",
|
||||||
|
)
|
||||||
|
self.assertEqual(self._scalar("SELECT similarity('abc','abc')::int;"), "1")
|
||||||
|
|
||||||
def test_text_search_dictionary_restored_once(self) -> None:
|
def test_text_search_dictionary_restored_once(self) -> None:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self._scalar(
|
self._scalar(
|
||||||
|
|||||||
Reference in New Issue
Block a user