diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc82d51..e3bfe1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,29 @@ jobs: uses: actions/checkout@v7 - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test + # Real update-path leg (0.1.1, extension_drop's last REAL published PGXN + # release, recovered from PGXN's dist archive -- see + # sql/extension_drop--0.1.1.sql -> stable, current source): runs here as + # extra steps in the same already-running container/cluster rather than + # a separate job, since a whole extra job means a whole extra container + # boot + PostgreSQL startup for no isolation benefit -- nothing about + # this leg needs its own environment, unlike e.g. pg_tle's job, which + # genuinely does. + # + # PG11+ only: cat_tools.routine__parse_arg_types_text() (which + # extension_drop's install script calls unconditionally, in both 0.1.1 + # and current) transitively hits cat_tools's _cat_tools.function__drop_temp, + # which runs a hardcoded `DROP ROUTINE` -- syntax that doesn't exist + # before PG11. This affects a FRESH install of current extension_drop on + # PG9.3-10 too, not just this update leg; it's a real, pre-existing + # incompatibility this repo's non-gating CI has never surfaced before + # now (flagged separately, not something to fix here). + - name: "Prove the update path: install 0.1.1, plant guard, ALTER EXTENSION UPDATE, re-prove guard, assert version" + if: matrix.pg >= 11 + run: bin/test_update_path.sh + - name: Run the pgTAP suite in update mode (0.1.1 -> stable) against the result + if: matrix.pg >= 11 + run: make test-update # A single stable check name for use as a required status check in branch # protection. Matrix jobs produce names like "🐘 PostgreSQL 14" that change diff --git a/.gitignore b/.gitignore index 1873c2c..20c7e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,12 @@ control.mk .deps/ # built targets -# Note: Version-specific files (sql/*--*.sql) are now tracked in git and should be committed +# Note: Version-specific files (sql/*--*.sql) are tracked in git and should be +# committed once a version is really released (immutable from then on) -- +# except sql/extension_drop--stable.sql, which is just a mechanical copy of +# sql/extension_drop.sql for as long as default_version is the stable +# pseudo-version, so committing it would just duplicate that file. +sql/extension_drop--stable.sql # Test artifacts results/ diff --git a/HISTORY.asc b/HISTORY.asc index 0cecdda..aed402e 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,5 +1,13 @@ -1.0.0 ------ +STABLE +------ +== Add a real update path from 0.1.1 +Recovered the actual 0.1.1 install script from PGXN's dist archive (it was +never committed to this repo -- only ever generated as a build artifact and +uploaded directly) as `sql/extension_drop--0.1.1.sql`, and added +`sql/extension_drop--0.1.1--stable.sql` to bring an existing 0.1.1 install +up to date. Previously there was no update path at all from the only version +ever actually published to PGXN. + == Drop support for PostgreSQL versions before 12 extension_drop depends on cat_tools, which already requires PostgreSQL 12+. A fresh install of extension_drop was already broken below that diff --git a/META.in.json b/META.in.json index 9d41647..2c9be8f 100644 --- a/META.in.json +++ b/META.in.json @@ -16,7 +16,7 @@ "name": "extension_drop", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "1.0.0", + "version": "stable", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Run custom commands when an extension is dropped.", @@ -41,7 +41,7 @@ "file": "sql/extension_drop.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "1.0.0", + "version": "stable", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Run custom commands when an extension is dropped.", diff --git a/META.json b/META.json index 47db4fe..cd44111 100644 --- a/META.json +++ b/META.json @@ -16,7 +16,7 @@ "name": "extension_drop", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "1.0.0", + "version": "stable", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "Run custom commands when an extension is dropped.", @@ -41,7 +41,7 @@ "file": "sql/extension_drop.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "1.0.0", + "version": "stable", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "Run custom commands when an extension is dropped.", diff --git a/Makefile b/Makefile index 07a0f45..a07e1c3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,91 @@ +# 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. TEST_UPDATE_FROM +# defaults to 0.1.1, extension_drop's last REAL published PGXN release +# (2017) -- its install script was recovered from PGXN's dist archive +# and committed as sql/extension_drop--0.1.1.sql (it was never in this +# repo's git history; see RELEASE.md and HISTORY.asc), with a matching +# update-diff script at sql/extension_drop--0.1.1--stable.sql. Empty +# TEST_UPDATE_TO (the default) means "update to the current +# default_version", which is now the `stable` pseudo-version. +# - 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= 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" (now +# `stable`). TEST_UPDATE_FROM defaults to 0.1.1, the actual recovered compat +# floor -- still overridable (e.g. once a second real release ships) but no +# longer required on every invocation. The guard below just protects against +# someone explicitly blanking it out (TEST_UPDATE_FROM= on the command line). +TEST_UPDATE_FROM ?= 0.1.1 +TEST_UPDATE_TO ?= +ifeq ($(TEST_LOAD_SOURCE),update) + ifeq ($(strip $(TEST_UPDATE_FROM)),) +$(error TEST_UPDATE_FROM must not be blank when TEST_LOAD_SOURCE=update) + 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 +# test/build/*.sql exists) so an accidental deletion of test/build/'s +# contents is a hard error instead of the check silently disappearing. +PGXNTOOL_ENABLE_TEST_BUILD = yes + +# The recovered real 0.1.1 install script (see sql/extension_drop--0.1.1.sql +# and RELEASE.md) is a single-version file for a version that ISN'T the +# current default_version ('stable'), so base.mk's DATA wildcard -- which +# only picks up the CURRENT version file plus two-dash update-diff scripts, +# not other historical single-version install files -- won't ship it on its +# own. Without this, `pgxn install extension_drop` (or any local `make +# install`) would silently stop being able to `CREATE EXTENSION extension_drop +# VERSION '0.1.1'` at all, even though the update-diff script depends on that +# exact file being installed. +# +# NOTE: this used to need an explicit `DATA += sql/extension_drop--0.1.1.sql` +# here (Postgres-Extensions/pgxntool#48) -- pgxntool 2.3.0 already includes +# it in its own generated DATA list now that #48 is fixed upstream, so adding +# it again duplicated the file in DATA and broke `make install` ("will not +# overwrite just-created ... with ..."). Left this comment as a marker in +# case a future pgxntool downgrade or DATA-generation change brings the gap +# back -- verify with `make -s print-DATA` before assuming it's still needed. + testdeps: test_extension test_extension: $(DESTDIR)$datadir)/extension/extension_drop_test.control $(wildcard $(TESTDIR)/*) $(DESTDIR)$datadir)/extension/extension_drop_test.control: diff --git a/bin/test_update_path.sh b/bin/test_update_path.sh new file mode 100755 index 0000000..6110b0b --- /dev/null +++ b/bin/test_update_path.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Exercises the real extension_drop update path: 0.1.1 (the last REAL PGXN +# release, recovered from PGXN's dist archive -- see sql/extension_drop--0.1.1.sql +# and RELEASE.md/HISTORY.asc) -> stable (this repo's current source). +# +# Assumes extension_drop and cat_tools are already built and installed into +# the active PostgreSQL cluster (`make install`, which pulls in the cat_tools +# deps target first) and that psql's ambient connection defaults reach it. +# +# Each step is PROVEN, not assumed -- see advanced-extension-testing.md +# section 4 (the dependency-guard technique) and section 6(d) (dynamic +# version assertions, never hardcoded): +# 1. CREATE EXTENSION extension_drop VERSION '0.1.1' -- installs the +# recovered real historical release. CASCADE (to auto-install +# cat_tools) only exists from PG10 -- pre-PG10 needs cat_tools created +# explicitly first, same branch test/install/load.sql already uses. +# 2. Plant a dependency-guard view and prove a non-CASCADE DROP EXTENSION +# is blocked -- BEFORE the update, proving the guard actually attaches +# to the 0.1.1-era extension_drop__commands table. +# 3. ALTER EXTENSION extension_drop UPDATE -- runs +# sql/extension_drop--0.1.1--stable.sql. +# 4. Re-prove the SAME guard still blocks a non-CASCADE drop -- proves the +# update script didn't touch extension_drop__commands's identity. +# 5. Assert the installed version now matches extension_drop.control's +# default_version, read dynamically from the control file rather than +# hardcoded, with empty-value guards. +set -eu + +cd "$(dirname "$0")/.." + +DB=${1:-extension_drop_update_test} + +dropdb --if-exists "$DB" +createdb "$DB" + +PG10_PLUS=$(psql -tAc "SELECT current_setting('server_version_num')::int >= 100000" -d "$DB") + +if [ "$PG10_PLUS" = "t" ]; then + CREATE_EXTENSION_DROP="CREATE EXTENSION extension_drop VERSION '0.1.1' CASCADE;" +else + CREATE_EXTENSION_DROP="CREATE EXTENSION IF NOT EXISTS cat_tools; +CREATE EXTENSION extension_drop VERSION '0.1.1';" +fi + +psql -v ON_ERROR_STOP=1 -d "$DB" -c " +$CREATE_EXTENSION_DROP + +CREATE SCHEMA extension_drop_drop_guard; +CREATE VIEW extension_drop_drop_guard.guard AS + SELECT NULL::extension_drop__commands AS guarded_member; +" + +assert_guard_blocks_drop() { + label=$1 + if psql -v ON_ERROR_STOP=1 -d "$DB" -c 'DROP EXTENSION extension_drop' >/tmp/guard_drop.out 2>/tmp/guard_drop.err; then + echo "FAIL ($label): DROP EXTENSION extension_drop succeeded -- the dependency guard did not block it" >&2 + exit 1 + fi + if ! grep -q 'cannot drop extension extension_drop because other objects depend on it' /tmp/guard_drop.err; then + echo "FAIL ($label): DROP EXTENSION failed, but not with the expected dependency-guard error:" >&2 + cat /tmp/guard_drop.err >&2 + exit 1 + fi + echo "OK ($label): non-CASCADE DROP EXTENSION extension_drop is blocked by the dependency guard" +} + +assert_guard_blocks_drop "pre-update, at 0.1.1" + +psql -v ON_ERROR_STOP=1 -d "$DB" -c "SET client_min_messages = ERROR; ALTER EXTENSION extension_drop UPDATE" + +assert_guard_blocks_drop "post-update" + +INSTALLED=$(psql -tAc "SELECT extversion FROM pg_extension WHERE extname = 'extension_drop'" -d "$DB" | tr -d '[:space:]') +EXPECTED=$(sed -n "s/^default_version[[:space:]]*=[[:space:]]*'\([^']*\)'.*/\1/p" extension_drop.control | tr -d '[:space:]') + +if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then + echo "FAIL: installed='$INSTALLED' expected='$EXPECTED' (derived from extension_drop.control)" >&2 + exit 1 +fi +echo "OK: extension_drop landed at '$INSTALLED' after update, matching extension_drop.control's default_version" + +dropdb "$DB" + +echo "PASS: 0.1.1 -> $INSTALLED update path verified (install, guard survival, version assertion)." + +# vi: expandtab ts=2 sw=2 diff --git a/extension_drop.control b/extension_drop.control index 64aba38..f129e48 100644 --- a/extension_drop.control +++ b/extension_drop.control @@ -1,4 +1,4 @@ comment = 'Allows specifying commands to run when dropping an extension.' -default_version = '1.0.0' +default_version = 'stable' relocatable = false requires = 'cat_tools' diff --git a/sql/extension_drop--0.1.1--stable.sql b/sql/extension_drop--0.1.1--stable.sql new file mode 100644 index 0000000..547c4ec --- /dev/null +++ b/sql/extension_drop--0.1.1--stable.sql @@ -0,0 +1,60 @@ +/* + * Update path from 0.1.1 (extension_drop's last REAL published PGXN release, + * 2017 -- recovered from PGXN's dist archive as sql/extension_drop--0.1.1.sql + * since it was never committed to this repo's git history; see RELEASE.md + * and HISTORY.asc) to `stable` (this repo's current in-development source). + * + * The only actual behavioral delta between the two, found by diffing the + * recovered 0.1.1 script against current sql/extension_drop.sql, is the + * extension_drop__event_trigger() function body gaining one entry-point + * RAISE DEBUG line (added in the same commit that also fixed a cat_tools + * function rename -- see git history of sql/extension_drop.sql). That's the + * one change here. + * + * Two other differences the diff turned up are deliberately NOT replayed + * here, because neither one changes anything about the objects this + * extension leaves behind after install completes: + * - The client_min_messages save/restore HISTORY.asc's `stable` section + * documents removing: that code only ever ran inside the install + * script's own session, saving/restoring a GUC and dropping its own + * temp table before the script finished -- nothing it did was ever part + * of the extension's persisted state, so an already-installed 0.1.1 has + * nothing left to clean up. + * - cat_tools.function__arg_types_text() being renamed to + * cat_tools.routine__parse_arg_types_text(): that call only happens + * inside the CREATE EXTENSION script's internal __extension_drop.create_function() + * builder, transiently, to compute the argument list text for the + * REVOKE/GRANT statements it executes immediately -- it's never stored + * in any persisted function body. (cat_tools 0.3.0 also keeps the old + * name as a deprecated wrapper, so a fresh `CREATE EXTENSION + * extension_drop VERSION '0.1.1'` still works today for testing this + * very update path.) + */ +CREATE OR REPLACE FUNCTION extension_drop__event_trigger( +) RETURNS event_trigger LANGUAGE plpgsql SET search_path FROM CURRENT AS +$body$ +DECLARE + r extension_drop__commands; +BEGIN + RAISE DEBUG 'extension_drop event trigger entry: tg_event %, tg_tag %', tg_event, tg_tag; + FOR r IN + SELECT c.* + FROM extension_drop__commands c + JOIN pg_event_trigger_dropped_objects() d + ON c.extension_name = d.object_name + AND d.object_type = 'extension' + LOOP + RAISE DEBUG E'extension "%" is being dropped; executing SQL:\n%', r.extension_name, r.sql; + EXECUTE r.sql; + DELETE FROM extension_drop__commands WHERE extension_name = r.extension_name; + END LOOP; + + /* + * Need to do this after the fact since the extensions being dropped have + * already been removed from the catalog by the time this function is called. + */ + PERFORM extension_drop__sanity_assert(); +END +$body$; + +-- vim: sw=2 ts=2 expandtab diff --git a/sql/extension_drop--1.0.0.sql b/sql/extension_drop--0.1.1.sql similarity index 95% rename from sql/extension_drop--1.0.0.sql rename to sql/extension_drop--0.1.1.sql index 3fe71e8..846dcf9 100644 --- a/sql/extension_drop--1.0.0.sql +++ b/sql/extension_drop--0.1.1.sql @@ -1,4 +1,3 @@ -/* DO NOT EDIT - AUTO-GENERATED FILE */ /* * NOTE: All pg_temp objects must be dropped at the end of the script! * Otherwise the eventual DROP CASCADE of pg_temp when the session ends will @@ -7,6 +6,9 @@ */ CREATE SCHEMA __extension_drop; +CREATE TABLE __extension_drop.messages AS SELECT pg_catalog.current_setting('client_min_messages'); +SET client_min_messages = WARNING; + CREATE FUNCTION __extension_drop.exec( sql text ) RETURNS void LANGUAGE plpgsql AS $body$ @@ -36,7 +38,7 @@ CREATE FUNCTION __extension_drop.create_function( , grants text DEFAULT NULL ) RETURNS void LANGUAGE plpgsql AS $body$ DECLARE - c_clean_args text := cat_tools.routine__parse_arg_types_text(args); + c_clean_args text := cat_tools.function__arg_types_text(args); create_template CONSTANT text := $template$ CREATE OR REPLACE FUNCTION %s( @@ -267,7 +269,6 @@ SELECT __extension_drop.create_function( DECLARE r extension_drop__commands; BEGIN - RAISE DEBUG 'extension_drop event trigger entry: tg_event %, tg_tag %', tg_event, tg_tag; FOR r IN SELECT c.* FROM extension_drop__commands c @@ -299,6 +300,10 @@ CREATE EVENT TRIGGER extension_drop /* * Drop "temporary" objects */ +SELECT __extension_drop.exec('SET client_min_messages = ' || current_setting) + FROM __extension_drop.messages +; +DROP TABLE __extension_drop.messages; DROP FUNCTION __extension_drop.create_function( function_name text , args text diff --git a/test/build/build.sql b/test/build/build.sql new file mode 100644 index 0000000..ac3fbb6 --- /dev/null +++ b/test/build/build.sql @@ -0,0 +1,20 @@ +\set ECHO none +BEGIN; +\i test/pgxntool/psql.sql + +CREATE EXTENSION IF NOT EXISTS cat_tools; + +/* + * Suppress NOTICEs from the raw install script itself (e.g. "%TYPE converted + * to ..." with a version-specific source-file LOCATION line) so this file's + * expected output stays stable across PostgreSQL minor versions instead of + * capturing verbose, version-dependent messages. + */ +SET client_min_messages = WARNING; + +\echo +\echo INSTALL +\t +\i sql/extension_drop.sql + +\echo # TRANSACTION INTENTIONALLY LEFT OPEN diff --git a/test/expected/zzz_build.out b/test/build/expected/build.out similarity index 96% rename from test/expected/zzz_build.out rename to test/build/expected/build.out index c66ee3c..44215eb 100644 --- a/test/expected/zzz_build.out +++ b/test/build/expected/build.out @@ -19,6 +19,4 @@ INSTALL - - # TRANSACTION INTENTIONALLY LEFT OPEN diff --git a/test/deps.sql b/test/deps.sql index bc5e06d..f53733f 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -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 diff --git a/test/expected/dependency_guard.out b/test/expected/dependency_guard.out new file mode 100644 index 0000000..5d22292 --- /dev/null +++ b/test/expected/dependency_guard.out @@ -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! diff --git a/test/expected/schema.out b/test/expected/schema.out index 12db609..511254e 100644 --- a/test/expected/schema.out +++ b/test/expected/schema.out @@ -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! diff --git a/test/install/.gitignore b/test/install/.gitignore new file mode 100644 index 0000000..5ec5e32 --- /dev/null +++ b/test/install/.gitignore @@ -0,0 +1,14 @@ +# pg_regress writes the install step's result here, because the install +# schedule references tests as ../install/ -- 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 diff --git a/test/install/load.sql b/test/install/load.sql new file mode 100644 index 0000000..2de3081 --- /dev/null +++ b/test/install/load.sql @@ -0,0 +1,160 @@ +\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. extension_drop.test_update_from defaults + * to 0.1.1, the last REAL published PGXN release (2017) -- its install + * script was recovered from PGXN's dist archive (never previously in + * this repo's git history) as sql/extension_drop--0.1.1.sql, with a + * matching update-diff script at sql/extension_drop--0.1.1--stable.sql + * (see HISTORY.asc/RELEASE.md). This branch is exercised in CI by the + * extension-update-test job. + * - 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 ''". 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 diff --git a/test/sql/dependency_guard.sql b/test/sql/dependency_guard.sql new file mode 100644 index 0000000..9cdec96 --- /dev/null +++ b/test/sql/dependency_guard.sql @@ -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 diff --git a/test/sql/schema.sql b/test/sql/schema.sql index fa70ed4..20c278e 100644 --- a/test/sql/schema.sql +++ b/test/sql/schema.sql @@ -1,8 +1,30 @@ \set ECHO none -\set TEST_SCHEMA _test_ed +\set TEST_SCHEMA _Test_Ed \i test/pgxntool/setup.sql -CREATE SCHEMA _test_ed_2; +/* + * extension_drop is already installed (test/install/load.sql, committed, + * landing wherever the ambient search_path resolves -- public in practice) + * before this suite runs. This file's actual job is proving the + * schema-targeting/quoting pipeline works, so it drops that committed + * install and recreates its own copies in schemas it chooses instead. Safe + * to drop here: this whole file runs inside pgTAP's own rolled-back + * transaction, so load.sql's committed install is back in place for the + * next test file regardless of what happens below. + * + * :TEST_SCHEMA is mixed-case, so every reference to it MUST be + * identifier-quoted (:"TEST_SCHEMA", or %I via format()) -- an unquoted + * reference would silently fold to lowercase and test a different, + * unquoted schema instead of this one, without erroring. That's + * deliberate: it turns a missing-quote bug in the code under test into a + * hard failure instead of a silent pass. + */ +DROP EXTENSION extension_drop; +CREATE SCHEMA :"TEST_SCHEMA"; +CREATE EXTENSION extension_drop SCHEMA :"TEST_SCHEMA"; +SET search_path = :"TEST_SCHEMA", tap, "$user"; + +CREATE SCHEMA "_Test_Ed_2"; SELECT plan( 0 @@ -28,7 +50,7 @@ SELECT lives_ok( , 'Drop extension' ); -\set TEST_SCHEMA_2 _test_ed_2 +\set TEST_SCHEMA_2 _Test_Ed_2 SELECT lives_ok( format( $$CREATE EXTENSION extension_drop SCHEMA %I$$, :'TEST_SCHEMA_2' ) , 'Create extension in schema ' || :'TEST_SCHEMA_2' @@ -44,11 +66,11 @@ SELECT lives_ok( SET search_path = "$user", public, tap; SELECT lives_ok( - $$SELECT _test_ed_2.extension_drop__update('extension_drop_test', 'moo')$$ + format( $$SELECT %I.extension_drop__update('extension_drop_test', 'moo')$$, :'TEST_SCHEMA_2' ) , 'extension_drop__update()' ); SELECT bag_eq( - $$SELECT * FROM _test_ed_2.extension_drop__get('extension_drop_test')$$ + format( $$SELECT * FROM %I.extension_drop__get('extension_drop_test')$$, :'TEST_SCHEMA_2' ) , $$SELECT 'extension_drop_test'::name , 'moo'::text$$ , 'Verify extension_drop__get()' ); diff --git a/test/sql/simple.sql b/test/sql/simple.sql index 721a825..b01269d 100644 --- a/test/sql/simple.sql +++ b/test/sql/simple.sql @@ -1,5 +1,4 @@ \set ECHO none -\set TEST_SCHEMA _test_ed \i test/pgxntool/setup.sql SELECT plan( @@ -48,17 +47,24 @@ SELECT lives_ok( ); /* - * Check search path for add command + * These calls used to be schema-qualified (_test_ed.extension_drop__remove + * etc.) back when this file's own per-test deps.sql install put + * extension_drop in a private schema and then this section intentionally + * moved search_path away from it, to prove a qualified call still worked. + * extension_drop is now installed once, ambiently (in public, see + * test/install/load.sql), by the time this file runs -- 'public' is always + * on search_path regardless of the change below, so there's no longer a + * schema this file controls to qualify against here. Proving + * schema-qualified access explicitly is test/sql/schema.sql's job now. */ --- Intentionally change our search path SET search_path = "$user", public, tap; SELECT lives_ok( - $$SELECT _test_ed.extension_drop__remove('extension_drop_test')$$ + $$SELECT extension_drop__remove('extension_drop_test')$$ , 'Drop extension command' ); SELECT lives_ok( - $$SELECT _test_ed.extension_drop__add('extension_drop_test', 'moo')$$ + $$SELECT extension_drop__add('extension_drop_test', 'moo')$$ , 'Add extension command' ); @@ -70,7 +76,7 @@ SELECT throws_ok( ); SELECT lives_ok( - $$SELECT _test_ed.extension_drop__remove('extension_drop_test')$$ + $$SELECT extension_drop__remove('extension_drop_test')$$ , 'Drop extension command' ); SELECT lives_ok( diff --git a/test/sql/zzz_build.sql b/test/sql/zzz_build.sql deleted file mode 100644 index 354c480..0000000 --- a/test/sql/zzz_build.sql +++ /dev/null @@ -1,12 +0,0 @@ -\set ECHO none -BEGIN; -\i test/pgxntool/psql.sql - -CREATE EXTENSION IF NOT EXISTS cat_tools; - -\echo -\echo INSTALL -\t -\i sql/extension_drop.sql - -\echo # TRANSACTION INTENTIONALLY LEFT OPEN