diff --git a/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql b/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql index 19c2620bd5..454b0a9209 100644 --- a/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql +++ b/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql @@ -21,14 +21,16 @@ begin this update is backwards compatible with version 1.4.4 but should be removed once we're on physical backups everywhere */ --- Detach and delete the official function -if extversion = '1.4.4' then - alter extension pgmq drop function pgmq.drop_queue; - drop function pgmq.drop_queue; -else -- 1.5.1+ - alter extension pgmq drop function pgmq.drop_queue(TEXT); - drop function pgmq.drop_queue(TEXT); -end if; + -- detach both historical drop_queue signatures, if present as extension members + if to_regprocedure('pgmq.drop_queue(text)') is not null then + alter extension pgmq drop function pgmq.drop_queue(text); + end if; + if to_regprocedure('pgmq.drop_queue(text, boolean)') is not null then + alter extension pgmq drop function pgmq.drop_queue(text, boolean); + end if; + + drop function if exists pgmq.drop_queue(text); + drop function if exists pgmq.drop_queue(text, boolean); -- Create and reattach the patched function CREATE FUNCTION pgmq.drop_queue(queue_name TEXT) @@ -73,7 +75,7 @@ BEGIN join pg_depend d on c.oid = d.objid join pg_extension e on d.refobjid = e.oid where c.relname = qtable_seq and e.extname = 'pgmq' - ) then + ) then EXECUTE FORMAT( $QUERY$ ALTER EXTENSION pgmq DROP SEQUENCE pgmq.%I @@ -143,11 +145,17 @@ BEGIN END; $func$ LANGUAGE plpgsql; -if extversion = '1.4.4' then - alter extension pgmq add function pgmq.drop_queue; -else -- 1.5.1+ - alter extension pgmq add function pgmq.drop_queue(TEXT); -end if; + alter extension pgmq add function pgmq.drop_queue(text); + +-- compat shim, kept callable with the historical 2-arg signature +CREATE OR REPLACE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN) +RETURNS BOOLEAN AS $shim$ +BEGIN + RETURN pgmq.drop_queue(queue_name); +END; +$shim$ LANGUAGE plpgsql; + + alter extension pgmq add function pgmq.drop_queue(text, boolean); update pg_extension set extowner = 'postgres'::regrole where extname = 'pgmq'; diff --git a/nix/ext/pgmq/0002-detach-drop_queue-shim-before-drop.patch b/nix/ext/pgmq/0002-detach-drop_queue-shim-before-drop.patch new file mode 100644 index 0000000000..c280e5ecab --- /dev/null +++ b/nix/ext/pgmq/0002-detach-drop_queue-shim-before-drop.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Supabase +Date: Wed, 8 Oct 2026 00:00:00 +0000 +Subject: [PATCH] fix: detach drop_queue(text, boolean) before dropping it + +after-create.sql now attaches drop_queue(text, boolean) to the pgmq +extension. A plain DROP FUNCTION IF EXISTS can't remove an extension +member, so ALTER EXTENSION UPDATE would abort here for anyone +upgrading from pgmq 1.4.4. Detach it first if it's currently a member. +--- + pgmq-extension/sql/pgmq--1.4.5--1.5.0.sql | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/pgmq-extension/sql/pgmq--1.4.5--1.5.0.sql b/pgmq-extension/sql/pgmq--1.4.5--1.5.0.sql +--- a/pgmq-extension/sql/pgmq--1.4.5--1.5.0.sql ++++ b/pgmq-extension/sql/pgmq--1.4.5--1.5.0.sql +@@ -104,5 +104,12 @@ END; + $$ LANGUAGE plpgsql; + ++DO $$ ++BEGIN ++ IF to_regprocedure('pgmq.drop_queue(text, boolean)') IS NOT NULL THEN ++ ALTER EXTENSION pgmq DROP FUNCTION pgmq.drop_queue(text, boolean); ++ END IF; ++END $$; ++ + DROP FUNCTION IF EXISTS pgmq.drop_queue(TEXT, BOOLEAN); + + CREATE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN) +-- +2.51.0 diff --git a/nix/ext/pgmq/default.nix b/nix/ext/pgmq/default.nix index b5ee1a5bd3..718975fb8e 100644 --- a/nix/ext/pgmq/default.nix +++ b/nix/ext/pgmq/default.nix @@ -44,6 +44,7 @@ let patches = lib.optionals (version == latestVersion) [ ./0001-fix-replace-drop_queue-function-if-exists.patch + ./0002-detach-drop_queue-shim-before-drop.patch ]; buildPhase = '' diff --git a/nix/ext/tests/pgmq-drop-queue-overload.nix b/nix/ext/tests/pgmq-drop-queue-overload.nix new file mode 100644 index 0000000000..14c9ea271e --- /dev/null +++ b/nix/ext/tests/pgmq-drop-queue-overload.nix @@ -0,0 +1,95 @@ +{ self, pkgs }: +let + pname = "pgmq"; + inherit (pkgs) lib; + system = pkgs.pkgsLinux.stdenv.hostPlatform.system; + testLib = import ./lib.nix { inherit self pkgs; }; + + installedExtension = self.legacyPackages.${system}."psql_15".exts."${pname}"; + versions = installedExtension.versions; +in +pkgs.testers.runNixOSTest { + name = "pgmq-drop-queue-overload"; + nodes.server = + { ... }: + { + imports = [ + (testLib.makeSupabaseTestConfig { + majorVersion = "15"; + }) + ]; + }; + testScript = + { ... }: + let + versionList = lib.concatStringsSep ", " (map (v: ''"${v}"'') versions); + in + '' + versions = [${versionList}] + + def sql(query): + return server.succeed( + "psql -U supabase_admin -d postgres -t -A -F',' -c \"" + query.replace('"', '\\"') + "\"" + ).strip() + + def drop_queue_overloads(): + # owned flag first: pg_get_function_identity_arguments() can itself + # contain a comma ("queue_name text, partitioned boolean"), so put the + # single-char flag first and split on the first comma only. + out = sql( + "select (d.objid is not null), pg_get_function_identity_arguments(p.oid) " + "from pg_proc p " + "left join pg_depend d on d.objid = p.oid and d.deptype = 'e' " + " and d.refobjid = (select oid from pg_extension where extname = 'pgmq') " + "where p.pronamespace = 'pgmq'::regnamespace and p.proname = 'drop_queue' " + "order by 2;" + ) + return [line.split(",", 1) for line in out.splitlines() if line] + + # every calling convention drop_queue has ever supported still works + def check_callers(qname): + sql(f"select pgmq.create('{qname}_a'); select pgmq.drop_queue('{qname}_a');") + sql(f"select pgmq.create('{qname}_b'); select pgmq.drop_queue('{qname}_b', false);") + # WRONG flag on purpose (queue isn't partitioned) - must still + # succeed, safely ignored in favour of pgmq.meta + sql(f"select pgmq.create('{qname}_c'); select pgmq.drop_queue('{qname}_c', true);") + sql( + f"select pgmq.create('{qname}_d'); " + f"select pgmq.drop_queue(queue_name => '{qname}_d', partitioned => true);" + ) + + start_all() + server.wait_for_unit("supabase-db-init.service") + + # fresh install of each pinned version: check the overload split, then the callers + for version in versions: + with subtest(f"install pgmq {version}"): + server.succeed("psql -U supabase_admin -d postgres -c 'DROP EXTENSION IF EXISTS pgmq;'") + server.succeed( + f"psql -U supabase_admin -d postgres -c \"CREATE EXTENSION pgmq WITH VERSION '{version}' CASCADE;\"" + ) + + overloads = drop_queue_overloads() + print(f"[{version}] drop_queue overloads: {overloads}") + assert overloads == [ + ["t", "queue_name text"], + ["t", "queue_name text, partitioned boolean"], + ], ( + f"[{version}] expected both drop_queue(text) and " + f"drop_queue(text, boolean) extension-owned, got: {overloads}" + ) + + check_callers(f"q_{version.replace('.', '_')}") + + # DROP EXTENSION must remove both signatures, leaving nothing behind + # for a later CREATE EXTENSION to collide with + with subtest("DROP EXTENSION leaves no drop_queue behind"): + server.succeed("psql -U supabase_admin -d postgres -c 'DROP EXTENSION pgmq;'") + remaining = sql( + "select count(*) from pg_proc p " + "join pg_namespace n on n.oid = p.pronamespace " + "where n.nspname = 'pgmq' and p.proname = 'drop_queue';" + ) + assert remaining == "0", f"expected no leftover drop_queue, got {remaining} rows" + ''; +}