Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions build/transform.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,10 @@ cat > "${PGTLE_FILE}" << HEADER
-- After loading this script, the extension is registered with pg_tle and can
-- be created or dropped via the standard create / drop extension commands.
--
-- This file is self-contained: it works in psql, GUI tools (DBeaver, etc.),
-- JDBC, libpq-direct callers — anywhere that accepts SQL.
-- This file is a psql script: it uses backslash meta-commands (\set
-- ON_ERROR_STOP, \echo), so run it with psql. From other clients (GUI
-- tools, JDBC, direct libpq), call pgtle.install_extension() directly,
-- passing the contents of sql/pgque.sql as the extension body.
--
-- Prerequisites:
-- 1. pg_tle is installed in this database:
Expand Down
8 changes: 8 additions & 0 deletions sql/pgque-additions/lifecycle.sql
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,14 @@ $$ language plpgsql security definer set search_path = pgque, pg_catalog;
create or replace function pgque.uninstall()
returns void as $$
begin
-- Extension installs (pg_tle) must go through the extension machinery:
-- dropping the schema out from under the extension would fail with a
-- confusing dependency error ("extension pgque requires it").
if exists (select 1 from pg_catalog.pg_extension where extname = 'pgque') then
raise exception 'pgque is installed as an extension; run: '
'drop extension pgque cascade; (for pg_tle installs, use '
'sql/pgque-tle-uninstall.sql to also unregister from pg_tle)';
end if;
-- Stop pg_cron jobs before dropping the schema.
if exists (select 1 from pg_extension where extname = 'pg_cron') then
perform pgque.stop();
Expand Down
24 changes: 20 additions & 4 deletions sql/pgque-tle-uninstall.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
-- pgque-tle-uninstall.sql -- Remove PgQue from pg_tle.
-- Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license.
--
-- Drops the pgque extension from this database (if installed) and unregisters
-- pgque from pg_tle's catalog. Roles are NOT dropped because they may still
-- be referenced by other databases on the cluster.
-- Stops scheduler jobs, drops the pgque extension from this database (if
-- installed), and unregisters pgque from pg_tle's catalog. Roles are NOT
-- dropped because they may still be referenced by other databases on the
-- cluster.
--
-- Idempotent: safe to re-run.
--
Expand All @@ -12,7 +13,22 @@

\set ON_ERROR_STOP on

drop extension if exists pgque cascade;
-- Stop scheduler jobs (pg_cron / pg_timetable) before dropping the
-- extension: scheduler jobs are catalog rows, not dependent objects, so
-- drop extension alone would leave them behind, failing forever afterwards.
-- A real stop() failure therefore aborts the uninstall; only "pgque is not
-- installed" errors are tolerated, keeping the script idempotent.
do $$
begin
begin
perform pgque.stop();
exception
when undefined_function or invalid_schema_name then
-- pgque is not installed (or has no stop()); nothing to stop.
null;
end;
drop extension if exists pgque cascade;
end $$;

do $$
begin
Expand Down
14 changes: 12 additions & 2 deletions sql/pgque-tle.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
-- After loading this script, the extension is registered with pg_tle and can
-- be created or dropped via the standard create / drop extension commands.
--
-- This file is self-contained: it works in psql, GUI tools (DBeaver, etc.),
-- JDBC, libpq-direct callers — anywhere that accepts SQL.
-- This file is a psql script: it uses backslash meta-commands (\set
-- ON_ERROR_STOP, \echo), so run it with psql. From other clients (GUI
-- tools, JDBC, direct libpq), call pgtle.install_extension() directly,
-- passing the contents of sql/pgque.sql as the extension body.
--
-- Prerequisites:
-- 1. pg_tle is installed in this database:
Expand Down Expand Up @@ -4641,6 +4643,14 @@ $$ language plpgsql security definer set search_path = pgque, pg_catalog;
create or replace function pgque.uninstall()
returns void as $$
begin
-- Extension installs (pg_tle) must go through the extension machinery:
-- dropping the schema out from under the extension would fail with a
-- confusing dependency error ("extension pgque requires it").
if exists (select 1 from pg_catalog.pg_extension where extname = 'pgque') then
raise exception 'pgque is installed as an extension; run: '
'drop extension pgque cascade; (for pg_tle installs, use '
'sql/pgque-tle-uninstall.sql to also unregister from pg_tle)';
end if;
-- Stop pg_cron jobs before dropping the schema.
if exists (select 1 from pg_extension where extname = 'pg_cron') then
perform pgque.stop();
Expand Down
8 changes: 8 additions & 0 deletions sql/pgque.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4553,6 +4553,14 @@ $$ language plpgsql security definer set search_path = pgque, pg_catalog;
create or replace function pgque.uninstall()
returns void as $$
begin
-- Extension installs (pg_tle) must go through the extension machinery:
-- dropping the schema out from under the extension would fail with a
-- confusing dependency error ("extension pgque requires it").
if exists (select 1 from pg_catalog.pg_extension where extname = 'pgque') then
raise exception 'pgque is installed as an extension; run: '
'drop extension pgque cascade; (for pg_tle installs, use '
'sql/pgque-tle-uninstall.sql to also unregister from pg_tle)';
end if;
-- Stop pg_cron jobs before dropping the schema.
if exists (select 1 from pg_extension where extname = 'pg_cron') then
perform pgque.stop();
Expand Down
22 changes: 16 additions & 6 deletions sql/pgque_uninstall.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
-- pgque_uninstall.sql -- Remove pgque from database
-- Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license.
--
-- Stops scheduler jobs (pg_cron / pg_timetable) before dropping the schema:
-- scheduler jobs are catalog rows, not dependent objects, so dropping the
-- schema alone would leave them behind, failing forever afterwards. A real
-- stop() failure therefore aborts the uninstall; only "pgque is not
-- installed" errors are tolerated, keeping the script idempotent.

do $$ begin
perform pgque.stop();
exception when others then
null;
do $$
begin
begin
perform pgque.stop();
exception
when undefined_function or invalid_schema_name then
-- pgque is not installed (or has no stop()); nothing to stop.
null;
end;
drop schema if exists pgque cascade;
end $$;

drop schema if exists pgque cascade;

-- Roles are database-global and may be shared across databases.
-- Do not drop them automatically here.
3 changes: 3 additions & 0 deletions tests/run_all.sql
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,8 @@
\echo 'Running: test_legacy_next_batch_role_guard'
\i tests/test_legacy_next_batch_role_guard.sql

\echo 'Running: test_uninstall_guard'
\i tests/test_uninstall_guard.sql

\echo ''
\echo '=== ALL TESTS PASSED ==='
23 changes: 23 additions & 0 deletions tests/test_tle_install.sql
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ end $$;
-- regression and acceptance suites running against the pg_tle install path
-- in CI; nothing extra to assert here.

-- pgque.uninstall() must refuse extension installs with a clear pointer to
-- drop extension, instead of failing on the schema drop with a confusing
-- dependency error ("extension pgque requires it").
do $$
begin
begin
perform pgque.uninstall();
raise exception 'sentinel: pgque.uninstall() did not raise';
exception
when raise_exception then
if sqlerrm like 'sentinel:%' then
raise;
end if;
assert sqlerrm like '%drop extension pgque cascade%',
format('uninstall() error must point to drop extension, got: %s', sqlerrm);
end;
assert exists (select 1 from pg_catalog.pg_namespace where nspname = 'pgque'),
'pgque schema must survive the refused uninstall()';
assert exists (select 1 from pg_catalog.pg_extension where extname = 'pgque'),
'pgque extension must survive the refused uninstall()';
raise notice 'PASS: uninstall() refuses extension install, points to drop extension';
end $$;

-- drop extension cascade removes the schema and the extension membership.
drop extension pgque cascade;

Expand Down
113 changes: 113 additions & 0 deletions tests/test_uninstall_guard.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
-- test_uninstall_guard.sql -- Uninstall scripts must stop scheduler jobs first
-- Copyright 2026 Nikolay Samokhvalov. Apache-2.0 license.
--
-- Scheduler jobs (pg_cron, pg_timetable) are catalog rows, not dependent
-- objects: dropping the pgque schema or extension does not remove them, so
-- both uninstall scripts must call pgque.stop() before dropping, and a real
-- stop() failure must abort the uninstall instead of being swallowed.
--
-- These tests swap pgque.stop() for instrumented fakes; the real definition
-- is saved first and restored at the end, so the rest of the suite is
-- unaffected. Runs without pg_cron / pg_tle. The TLE uninstall sub-tests
-- only run against a plain (non-extension) install; see the gate below.
--
-- Run from the repo root: the \i commands below resolve relative to cwd.

-- Save the real pgque.stop() so it can be restored at the end.
create table pg_temp.saved_stop as
select pg_catalog.pg_get_functiondef('pgque.stop()'::pg_catalog.regprocedure) as def;

-- Test 1 (C6): a real pgque.stop() failure must abort sql/pgque_uninstall.sql
-- before the schema drop (no silent "when others" swallowing).
create or replace function pgque.stop()
returns void as $$
begin
raise exception 'simulated stop() failure (test_uninstall_guard)';
end;
$$ language plpgsql;

\set ON_ERROR_STOP off
\i sql/pgque_uninstall.sql
\set ON_ERROR_STOP on

do $$
begin
assert exists (select 1 from pg_catalog.pg_namespace where nspname = 'pgque'),
'pgque schema must survive when pgque.stop() raises during uninstall';
raise notice 'PASS: pgque_uninstall.sql aborts before drop when stop() fails';
end $$;

-- Tests 2 and 3 execute sql/pgque-tle-uninstall.sql, which (correctly)
-- drops the pgque extension -- taking the whole schema with it -- and
-- unregisters pgque from pg_tle. Against an extension install (the pg_tle
-- CI job) that would destroy the install mid-suite, so the script must not
-- run at all there: skip both sub-tests. The extension path of the script
-- is covered by tests/test_tle_install.sql.
select exists (select 1 from pg_catalog.pg_extension where extname = 'pgque') as pgque_is_extension
\gset

\if :pgque_is_extension

\echo 'SKIP: pgque is installed as an extension; TLE uninstall sub-tests need a plain install'

\else

-- Test 2 (C4): sql/pgque-tle-uninstall.sql must call pgque.stop() before
-- drop extension, so pg_cron / pg_timetable jobs do not outlive the schema.
create table pg_temp.tle_stop_called (called bool);

create or replace function pgque.stop()
returns void as $$
begin
insert into pg_temp.tle_stop_called values (true);
end;
$$ language plpgsql;

\i sql/pgque-tle-uninstall.sql

do $$
begin
assert exists (select 1 from pg_temp.tle_stop_called),
'pgque-tle-uninstall.sql must call pgque.stop() before drop extension';
assert exists (select 1 from pg_catalog.pg_namespace where nspname = 'pgque'),
'plain (non-extension) install must survive the TLE uninstall script';
raise notice 'PASS: pgque-tle-uninstall.sql calls stop() before drop extension';
end $$;

-- Test 3 (C4/C6): a real stop() failure must abort the TLE uninstall script
-- before drop extension as well.
create or replace function pgque.stop()
returns void as $$
begin
raise exception 'simulated stop() failure (test_uninstall_guard)';
end;
$$ language plpgsql;

\set ON_ERROR_STOP off
\i sql/pgque-tle-uninstall.sql
\set ON_ERROR_STOP on

do $$
begin
assert exists (select 1 from pg_catalog.pg_namespace where nspname = 'pgque'),
'pgque schema must survive when stop() raises during TLE uninstall';
raise notice 'PASS: pgque-tle-uninstall.sql aborts before drop when stop() fails';
end $$;

drop table pg_temp.tle_stop_called;

\endif

-- Restore the real pgque.stop() and verify the restoration.
do $$
declare
v_def text;
begin
select def into v_def from pg_temp.saved_stop;
execute v_def;
assert pg_catalog.pg_get_functiondef('pgque.stop()'::pg_catalog.regprocedure) = v_def,
'pgque.stop() must be restored to its original definition';
raise notice 'PASS: original pgque.stop() restored';
end $$;

drop table pg_temp.saved_stop;
Loading