Skip to content
Open
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
59 changes: 59 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
# Run test/install/load.sql (extension install) COMMITTED, once, before the
# main pgTAP suite, via pgxntool's test/install feature. Set explicitly
# (rather than left to auto-detect) so an accidentally emptied test/install/
# is a hard build error instead of silently falling back to "disabled".
# Must be set before `include pgxntool/base.mk` below -- base.mk reads it
# while parsing.
PGXNTOOL_ENABLE_TEST_INSTALL = yes

# TEST_LOAD_SOURCE selects how test/install/load.sql installs extension_drop:
# - fresh (default): CREATE EXTENSION extension_drop (current version).
# - update: CREATE EXTENSION at TEST_UPDATE_FROM, then ALTER EXTENSION
# UPDATE -- to TEST_UPDATE_TO if set, otherwise to the current version.
# Running the SAME suite/expected output against the result asserts
# update behaves identically to a fresh install. NOTE: extension_drop has
# never had a real second released version (PGXN's only listing is
# 0.1.x from 2017, predating the current SQL entirely -- see HISTORY.asc
# and RELEASE.md), so TEST_UPDATE_FROM has no safe default; this mode is
# wired up and structurally ready, but there is nothing real to update
# FROM yet, and so no CI leg exercises it in this repo today.
# - existing: the extension is ALREADY installed (a real pg_upgrade, or an
# ALTER EXTENSION UPDATE done outside the suite). load.sql does not
# touch it; it only asserts presence + current version. Pair with
# CONTRIB_TESTDB=<db> and EXTRA_REGRESS_OPTS=--use-existing to point
# pg_regress at that database instead of a throwaway one.
#
# Propagated to load.sql as a GUC: pg_regress doesn't forward make variables,
# but the psql processes it spawns inherit the environment, so PGOPTIONS
# reaches load.sql. Exported UNCONDITIONALLY so load.sql can read it without
# missing_ok and fail loudly if it didn't propagate, rather than silently
# defaulting to the wrong mode. The mode is also validated here at
# make-parse-time, so a typo like `TEST_LOAD_SOURCE=fresh ` or
# `TEST_LOAD_SOURCE=typo` fails immediately instead of quietly running the
# default.
TEST_LOAD_SOURCE ?= fresh
ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update existing),)
$(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)')
endif

# update-mode version range (load.sql only reads these in update mode).
# Empty TEST_UPDATE_TO means "update to the current default_version". There
# is no safe default for TEST_UPDATE_FROM (see above) -- require it
# explicitly rather than pointing it at a version that doesn't exist.
TEST_UPDATE_FROM ?=
TEST_UPDATE_TO ?=
ifeq ($(TEST_LOAD_SOURCE),update)
ifeq ($(strip $(TEST_UPDATE_FROM)),)
$(error TEST_UPDATE_FROM must be set when TEST_LOAD_SOURCE=update -- extension_drop has no prior released version yet to default it to)
endif
endif

export PGOPTIONS := $(PGOPTIONS) -c extension_drop.test_load_mode=$(TEST_LOAD_SOURCE) -c extension_drop.test_update_from=$(TEST_UPDATE_FROM) -c extension_drop.test_update_to=$(TEST_UPDATE_TO)

# make test-update == make test TEST_LOAD_SOURCE=update. Must recurse (a
# fresh $(MAKE)) rather than depend on `test`, so the parse-time
# TEST_LOAD_SOURCE conditional above re-evaluates with update set.
.PHONY: test-update
test-update:
$(MAKE) test TEST_LOAD_SOURCE=update

include pgxntool/base.mk

# Explicit rather than relying on auto-detect (which enables this whenever
Expand Down
32 changes: 8 additions & 24 deletions test/deps.sql
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
-- IF NOT EXISTS will emit NOTICEs, which is annoying
SET client_min_messages = WARNING;

-- Add any test dependency statements here
-- Note: pgTap is loaded by setup.sql

-- Re-enable notices
SET client_min_messages = NOTICE;
/*
* extension_drop itself used to be (re)installed here, per test file. It's
* now installed ONCE, COMMITTED, by test/install/load.sql (pgxntool's
* test/install feature) before this suite runs at all -- this file no
* longer touches it. test/sql/schema.sql is the one test that actually
* drops/recreates the extension itself (that's what it's testing); every
* other test file just uses the extension load.sql already installed.
*/

\set TT extension_drop_test_table
CREATE TEMP TABLE :TT (i int);

CREATE SCHEMA :TEST_SCHEMA;
SET search_path = :TEST_SCHEMA, tap, "$user";

/*
* Now load our extension. We don't use IF NOT EXISTs here because we want an
* error if the extension is already loaded (because we want to ensure we're
* getting the very latest version).
*/
SET client_min_messages = WARNING; -- Squelch notice from CASCADE
DO $$ BEGIN
IF current_setting('server_version_num')::int < 100000 THEN
CREATE EXTENSION IF NOT EXISTS cat_tools;
CREATE EXTENSION extension_drop ;
ELSE
EXECUTE $exec$CREATE EXTENSION extension_drop CASCADE$exec$;
END IF;
END$$;
SET client_min_messages = NOTICE;

-- vi: expandtab ts=2 sw=2
6 changes: 6 additions & 0 deletions test/expected/dependency_guard.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
\set ECHO none
1..3
ok 1 - Non-CASCADE DROP EXTENSION extension_drop is blocked by the dependency guard
ok 2 - extension_drop is still installed after the blocked drop attempt
ok 3 - Dependency guard view is still present after the blocked drop attempt
# TRANSACTION INTENTIONALLY LEFT OPEN!
10 changes: 5 additions & 5 deletions test/expected/schema.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ ok 1 - Create test extension
ok 2 - Test extension exists
ok 3 - Drop test extension
ok 4 - Test extension does not exist
ok 5 - Table _test_ed.extension_drop__commands should exist
ok 5 - Table "_Test_Ed".extension_drop__commands should exist
ok 6 - Drop extension
ok 7 - Create extension in schema _test_ed_2
ok 8 - Table _test_ed_2.extension_drop__commands should exist
ok 9 - Create test extension in _test_ed_2
ok 7 - Create extension in schema _Test_Ed_2
ok 8 - Table "_Test_Ed_2".extension_drop__commands should exist
ok 9 - Create test extension in _Test_Ed_2
ok 10 - extension_drop__update()
ok 11 - Verify extension_drop__get()
ok 12 - Drop schema _test_ed without cascade succeeds
ok 12 - Drop schema _Test_Ed without cascade succeeds
# TRANSACTION INTENTIONALLY LEFT OPEN!
14 changes: 14 additions & 0 deletions test/install/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# pg_regress writes the install step's result here, because the install
# schedule references tests as ../install/<name> -- one directory up from
# both test/expected/ and test/results/, which cancels back out to this same
# directory for both. So load.out is simultaneously "expected" and "actual":
# confirmed by hand (deliberately breaking load.sql's existing-mode assertion
# and seeing pg_regress still report the step "ok" while the real error text
# showed up in this file) that pg_regress can never see a diff for it here,
# regardless of what load.sql actually does. Never track it -- it would just
# be reformatted/overwritten noise on every run, not a real expectation.
load.out
# Precautionary: haven't observed pg_regress emit a *.diff for this
# self-comparing path locally, but if it ever does, it'd be equally
# meaningless to track for the same reason as load.out above.
install.out.diff
162 changes: 162 additions & 0 deletions test/install/load.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
\set ECHO none
/*
* Committed-once installer for the test suite's one real dependency: the
* extension_drop extension itself. (No test roles exist for this extension
* -- see test/deps.sql -- so unlike cat_tools' equivalent load.sql, there is
* nothing role-related to install here.)
*
* pgxntool's test/install feature runs this file COMMITTED, in its own
* pg_regress session, BEFORE the main pgTAP suite, so the extension persists
* into every (rolled-back) test/sql/ file instead of each one re-installing
* it from scratch. test/deps.sql (run per test) no longer creates the
* extension; it only sets the psql variables the suite references.
* test/sql/schema.sql is the one exception: proving the schema-targeting
* pipeline works is its actual job, so it explicitly drops this committed
* install and recreates its own copies in schemas it chooses -- safely,
* since that all happens inside its own rolled-back transaction and never
* escapes that one file.
*
* Three modes, selected by the extension_drop.test_load_mode placeholder
* GUC, which the Makefile's TEST_LOAD_SOURCE block sets via PGOPTIONS
* (fresh is the default):
* - fresh (default): plain CREATE EXTENSION extension_drop (current
* version).
* - update: CREATE EXTENSION at an older version
* (extension_drop.test_update_from) then ALTER EXTENSION UPDATE -- to
* extension_drop.test_update_to when that GUC is non-empty, otherwise to
* the current default_version. NOTE: extension_drop has never had a
* real second released version -- PGXN's only listing (0.1.x, 2017)
* predates the current SQL entirely (see HISTORY.asc/RELEASE.md), so
* there is no version that could legitimately fill
* extension_drop.test_update_from today. This branch is wired up and
* structurally correct (the Makefile refuses to select this mode
* without TEST_UPDATE_FROM set explicitly), but has nothing real to
* update FROM yet, so it exists ready for the day a second version
* ships rather than because it's exercised in CI now.
* - existing: the extension is ALREADY installed (by a real binary
* pg_upgrade, or an ALTER EXTENSION UPDATE performed outside the
* suite). This branch must NOT drop/create/update it -- that would
* destroy exactly what "existing" mode exists to test. It only asserts
* presence + current version.
*
* Unlike cat_tools (whose control file pins schema = 'cat_tools' --
* CREATE EXTENSION always lands in the same place, no choice), extension_drop's
* control file has no schema= line, so CREATE EXTENSION here lands wherever
* the ambient search_path resolves when this file runs -- a fresh psql
* session's default "$user", public, i.e. public in practice. That's a
* deliberate, useful default: it proves nothing in extension_drop's install
* script is hardcoded to a specific schema, the same property
* test/sql/schema.sql proves again explicitly for non-default schemas.
*/
SET client_min_messages = WARNING;

/*
* The Makefile always exports extension_drop.test_load_mode via PGOPTIONS.
* Read it WITHOUT missing_ok: if the GUC did not propagate (a break
* anywhere in make -> PGOPTIONS -> env -> psql), current_setting errors here
* and the whole install step fails loudly, instead of silently defaulting
* and running the wrong suite.
*/
SELECT current_setting('extension_drop.test_load_mode') AS extension_drop_test_load_mode
\gset

DO $DO$
BEGIN
IF current_setting('extension_drop.test_load_mode') NOT IN ('fresh', 'update', 'existing') THEN
RAISE EXCEPTION
'extension_drop.test_load_mode must be ''fresh'', ''update'' or ''existing'', got ''%'''
, current_setting('extension_drop.test_load_mode')
;
END IF;
END
$DO$;

SELECT
:'extension_drop_test_load_mode' = 'update' AS extension_drop_mode_update
, :'extension_drop_test_load_mode' = 'existing' AS extension_drop_mode_existing
\gset

\if :extension_drop_mode_existing
/*
* existing mode: do NOT touch the extension. Assert it is installed and at
* the current default_version -- the pg_upgrade / external update the
* database just went through is exactly what the suite is validating, so
* dropping or reinstalling it would defeat the test. Fail loudly on absence
* or mismatch.
*/
DO $DO$
DECLARE
v_installed text := (SELECT extversion FROM pg_extension WHERE extname = 'extension_drop');
v_default text := (SELECT default_version FROM pg_available_extensions WHERE name = 'extension_drop');
BEGIN
IF v_installed IS NULL THEN
RAISE EXCEPTION 'test_load_mode=existing but the extension_drop extension is not installed';
END IF;
IF v_installed IS DISTINCT FROM v_default THEN
RAISE EXCEPTION
'extension_drop is installed at version % but the current default_version is %'
, v_installed, v_default
;
END IF;
END
$DO$;
\else
/*
* fresh / update: (re)install from scratch. Drop-first (CASCADE, matching
* cat_tools' own load.sql) so a re-run on a persistent cluster installs the
* newest build instead of reusing stale objects.
*
* extension_drop requires cat_tools. CASCADE auto-installs it on PG10+;
* event triggers exist from 9.3 but CREATE EXTENSION ... CASCADE was only
* added in PG10, so pre-PG10 needs cat_tools created explicitly first. This
* mirrors the check test/deps.sql used to do per-test before this file took
* over installing the extension. server_version_num is read once into a
* psql variable rather than a runtime DO block, so it can drive \if
* (client-side) branching around the VERSION-qualified CREATE EXTENSION
* calls below without needing psql variables interpolated inside a
* dollar-quoted DO body.
*/
DROP EXTENSION IF EXISTS extension_drop CASCADE;

SELECT current_setting('server_version_num')::int >= 100000 AS extension_drop_pg10_plus
\gset

\if :extension_drop_mode_update
SELECT current_setting('extension_drop.test_update_from') AS extension_drop_test_update_from \gset
SELECT current_setting('extension_drop.test_update_to') AS extension_drop_test_update_to \gset
/*
* Build the optional target clause once so a SINGLE ALTER EXTENSION covers
* both cases: an empty test_update_to yields '' (update to the current
* default_version -- the widest path); a non-empty value yields
* "TO '<v>'". format(%L) quotes the version literal safely.
*/
SELECT CASE WHEN :'extension_drop_test_update_to' = '' THEN ''
ELSE format('TO %L', :'extension_drop_test_update_to') END
AS extension_drop_update_to_clause \gset

\if :extension_drop_pg10_plus
CREATE EXTENSION extension_drop VERSION :'extension_drop_test_update_from' CASCADE;
\else
CREATE EXTENSION IF NOT EXISTS cat_tools;
CREATE EXTENSION extension_drop VERSION :'extension_drop_test_update_from';
\endif

/*
* Suppress the deprecation NOTICEs an update script might emit.
*/
SET client_min_messages = ERROR;
ALTER EXTENSION extension_drop UPDATE :extension_drop_update_to_clause;
SET client_min_messages = WARNING;
\else
\if :extension_drop_pg10_plus
CREATE EXTENSION extension_drop CASCADE;
\else
CREATE EXTENSION IF NOT EXISTS cat_tools;
CREATE EXTENSION extension_drop;
\endif
\endif
-- end \if :extension_drop_mode_update (fresh vs. update install branch)
\endif
-- end \if :extension_drop_mode_existing (existing mode skips the whole (re)install block)

-- vi: expandtab ts=2 sw=2
66 changes: 66 additions & 0 deletions test/sql/dependency_guard.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
\set ECHO none
\i test/pgxntool/setup.sql

/*
* Dependency-guard proof. This protects a future "existing" mode CI run
* (extension_drop already installed by a real pg_upgrade, or an ALTER
* EXTENSION UPDATE done outside the suite -- see test/install/load.sql and
* the Makefile's TEST_LOAD_SOURCE machinery): nothing today stops an
* accidental CASCADE drop, a stray CI step, or a logic bug from silently
* destroying the real updated/upgraded objects that mode exists to
* validate -- after which the suite would quietly pass again against a
* fresh reinstall instead of the thing it was supposed to check.
*
* The fix is a view with a HARD pg_depend dependency on a stable
* extension_drop member: something the extension only ever extends, never
* drops or redefines. extension_drop__commands is exactly that -- it's the
* one state table every other object in this extension revolves around
* (get/add/remove/update, the sanity checks, and the event trigger all key
* off it); getting rid of it or changing its identity would be a rewrite of
* the whole extension, not a routine update. Referencing its row type
* (rather than a specific column) means the guard doesn't need updating
* even if a future release adds a column to it. extension_drop has no
* enums (unlike cat_tools' own guard, which types on an enum grown via ADD
* VALUE) -- a stable table's row type serves the same purpose here.
*
* This test PROVES the guard works instead of assuming the SQL is correct:
* it attempts the actual non-CASCADE DROP EXTENSION and asserts it fails,
* then asserts both the extension and the guard view are still present
* afterward. Everything here runs inside pgTAP's own rolled-back
* transaction, so the guard schema/view never leaks into any other test
* file.
*/
CREATE SCHEMA extension_drop_drop_guard;
CREATE VIEW extension_drop_drop_guard.guard AS
SELECT NULL::extension_drop__commands AS guarded_member;

SELECT plan(
0
+ 1 -- non-CASCADE drop is blocked
+ 1 -- extension_drop is still installed
+ 1 -- guard view still present
);

/*
* 2BP01 = dependent_objects_still_exist: the standard error DROP ... RESTRICT
* (the implicit default for DROP EXTENSION) raises when another object
* depends on something the extension owns. throws_ok's 3-arg overload is
* (sql, message, description), not (sql, sqlstate, description) -- passing
* just the sqlstate there matches message text literally instead of
* checking the code, so the sqlstate AND the real message both need to be
* given explicitly (4-arg form) to actually check the error class.
*/
SELECT throws_ok(
$$DROP EXTENSION extension_drop$$
, '2BP01'
, 'cannot drop extension extension_drop because other objects depend on it'
, 'Non-CASCADE DROP EXTENSION extension_drop is blocked by the dependency guard'
);

SELECT has_extension('extension_drop', 'extension_drop is still installed after the blocked drop attempt');

SELECT has_view('extension_drop_drop_guard', 'guard', 'Dependency guard view is still present after the blocked drop attempt');

\i test/pgxntool/finish.sql

-- vi: expandtab ts=2 sw=2
Loading