From fd158dd162f7bbc886742028d24a8a8b146d714e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Tue, 4 Aug 2026 15:37:08 -0500 Subject: [PATCH] Add binary pg_upgrade CI job with dependency-guard-protected existing-mode suite Adds a pg-upgrade-test job (3 legs: 12->13, 16->17, 12->17) that installs extension_drop on an old cluster, plants a dependency guard, performs a real binary pg_upgrade, and runs the suite in TEST_LOAD_SOURCE=existing mode against the migrated database, with a dynamic version assertion and a docs-only cost gate for the new heavy job. Modeled on Postgres-Extensions/cat_tools's bin/test_existing and .github/scripts/pg_upgrade_cluster, substantially simplified: extension_drop has only ever shipped one real version, so no bridge leg or update-scenario machinery is needed. Verified end to end locally (PG12 -> PG17) before committing, which also surfaced a real conflict between a persistent dependency guard and test/sql/schema.sql's unconditional non-cascade drop, now handled by excluding schema.sql/zzz_build.sql from the existing-mode REGRESS list (both are already proven by the fresh-install `test` job). Co-Authored-By: Claude Sonnet 5 --- .github/scripts/pg_upgrade_cluster | 94 ++++++++++ .github/workflows/ci.yml | 193 ++++++++++++++++++- bin/test_existing | 292 +++++++++++++++++++++++++++++ 3 files changed, 571 insertions(+), 8 deletions(-) create mode 100755 .github/scripts/pg_upgrade_cluster create mode 100755 bin/test_existing diff --git a/.github/scripts/pg_upgrade_cluster b/.github/scripts/pg_upgrade_cluster new file mode 100755 index 0000000..8c6eea7 --- /dev/null +++ b/.github/scripts/pg_upgrade_cluster @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# +# pg_upgrade_cluster - Shared binary pg_upgrade mechanics for CI. Used by the +# pg-upgrade-test job in .github/workflows/ci.yml. This part of the flow has +# nothing extension-specific about it (it's pure pg_ctlcluster/pg_createcluster/ +# pg_upgrade plumbing against the pgxn-tools image's "test" cluster convention), +# so it lives here as its own committed script rather than inline YAML. +# Modeled near-verbatim on Postgres-Extensions/cat_tools's own +# .github/scripts/pg_upgrade_cluster (see that repo's ci.yml pg-upgrade-test job). +# +# Lives under .github/, not bin/: unlike bin/test_existing (which a developer +# can run locally against a scratch database), this is CI-mechanics-only -- +# pg_ctlcluster/pg_createcluster/pg_upgrade against the pgxn-tools image's +# "test" cluster convention isn't a locally-runnable workflow outside that +# container image. +# +# USAGE: .github/scripts/pg_upgrade_cluster [args] +# +# recreate-old PG_VERSION +# pg-start's default "test" cluster doesn't have data checksums +# enabled, but binary pg_upgrade requires the old and new clusters to +# have MATCHING checksum/auth settings (see $INITDB_OPTS below) -- +# stop, drop, and recreate the "test" cluster for PG_VERSION with them, +# then start it and wait for readiness. +# +# upgrade OLD_PG NEW_PG +# Stop the old cluster, create the new cluster's "test" cluster (same +# $INITDB_OPTS), binary pg_upgrade OLD_PG -> NEW_PG, then start the new +# cluster and wait for readiness. On pg_upgrade failure, dumps its logs +# (PG17+ writes them to $new_datadir/pg_upgrade_output.d/; older +# versions write to CWD -- both are searched) before failing. +# +# $INITDB_OPTS (env var, required): initdb options both clusters must share so +# pg_upgrade sees consistent settings on old and new (e.g. +# "--data-checksums --auth trust"). Deliberately left unquoted at each use +# site so its (space-separated) options word-split into separate arguments. +set -euo pipefail + +: "${INITDB_OPTS:?INITDB_OPTS must be set}" + +recreate_old() { + local pg=$1 + pg_ctlcluster "$pg" test stop + pg_dropcluster "$pg" test + # -p 5432: pg_createcluster assigns the next available port, which may not + # be 5432 after pg-start has claimed and released it. Force 5432 so + # subsequent psql/createdb calls connect without -p. + pg_createcluster -p 5432 "$pg" test -- $INITDB_OPTS + pg_ctlcluster "$pg" test start + pg_isready -t 30 +} + +upgrade() { + local old_pg=$1 new_pg=$2 + pg_ctlcluster "$old_pg" test stop + pg_createcluster -p 5432 "$new_pg" test -- $INITDB_OPTS + # PG17+ writes logs to $new_datadir/pg_upgrade_output.d/; older versions + # write to CWD. Search both on failure. + mkdir -p /tmp/pg_upgrade_logs + chown postgres:postgres /tmp/pg_upgrade_logs + su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/$new_pg/bin/pg_upgrade \ + -b /usr/lib/postgresql/$old_pg/bin \ + -B /usr/lib/postgresql/$new_pg/bin \ + -d /var/lib/postgresql/$old_pg/test \ + -D /var/lib/postgresql/$new_pg/test \ + -o '-c config_file=/etc/postgresql/$old_pg/test/postgresql.conf' \ + -O '-c config_file=/etc/postgresql/$new_pg/test/postgresql.conf'" postgres \ + || { find /tmp/pg_upgrade_logs \ + "/var/lib/postgresql/$new_pg/test/pg_upgrade_output.d" \ + -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } + pg_ctlcluster "$new_pg" test start + pg_isready -t 30 +} + +usage() { + echo "usage: .github/scripts/pg_upgrade_cluster [args]" >&2 + echo " recreate-old PG_VERSION" >&2 + echo " upgrade OLD_PG NEW_PG" >&2 + exit 2 +} + +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + recreate-old) recreate_old "$@" ;; + upgrade) upgrade "$@" ;; + *) usage ;; + esac +} + +main "$@" + +# vi: expandtab ts=2 sw=2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aad0d23..1512a55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,118 @@ name: CI -# Scope push to master only. With a bare `on: [push, pull_request]`, a commit to -# a branch that has an open PR triggers CI twice (once for push, once for -# pull_request) for the same SHA -- wasteful, and a flaky run can show red next -# to an identical green one. PR branches now run only via pull_request; master -# (post-merge) runs via push. +# Test strategy +# +# `test` -- fresh install (`pg-build-test`) across every PostgreSQL major +# extension_drop's own SQL claims to support (see META.json). +# +# `pg-upgrade-test` -- binary pg_upgrade legs: install the CURRENT +# extension_drop version on an OLD cluster, plant a dependency guard, binary +# pg_upgrade straight to a NEWER major, then run the suite in existing mode +# against the real migrated objects (see bin/test_existing). No update step +# and no bridge leg: extension_drop has only ever shipped one real version +# (1.0.0 -- see HISTORY.asc/RELEASE.md; the only PGXN listing, 0.1.x from +# 2017, predates the current SQL entirely), so extversion never changes +# across a leg and there is no known pg_upgrade-unsafe old version to bridge +# from (see ~/advanced-extension-testing.md ยง6c's own guidance not to build +# that preemptively). Legs' old_pg floor is 12, NOT extension_drop's own +# claimed 9.3 floor above: `make install` unconditionally builds cat_tools +# from Postgres-Extensions/cat_tools's `master` (see the Makefile's +# `cat_tools` target comment -- PGXN's published cat_tools is a stale 2017 +# release extension_drop can't use), and that current cat_tools requires +# PostgreSQL >= 12 for a fresh install (its own META.json, +# build.requires.PostgreSQL). Below PG12, `make install` cannot complete at +# all today, independent of anything this job does -- consistent with the +# already-known pre-existing old-PG failures on the plain `test` job (see +# this branch's own history: "Revert ci.yml pg-build-test switch: +# pre-existing failures on old PG predate this branch"). +# +# Scope: push only runs on master (post-merge); PR commits are covered by +# pull_request -- avoids double-running CI for the same commit. on: push: branches: [master] pull_request: +concurrency: + # A superseded push's pg_upgrade matrix (several binary pg_upgrades) is + # pure waste once a newer push on the same ref supersedes it. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: + # Cheap gate that lets the heavy pg-upgrade-test job below skip itself on + # commits that touch only docs. Must run on every push/pull_request (no + # paths-ignore on the workflow itself) -- otherwise the required + # all-checks-passed check would never report on a docs-only push and get + # stuck Pending in branch protection (~/advanced-extension-testing.md ยง6f). + # Deliberately NOT gating the pre-existing `test` job on this: `test` is + # already cheap (a single `pg-build-test` per PG major), so there is + # nothing costly to save by skipping it too -- only pg-upgrade-test (several + # real binary pg_upgrades per push) is worth gating. + changes: + name: ๐Ÿ” Detect docs-only changes + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.diff.outputs.docs_only }} + steps: + - name: Check out the repo + uses: actions/checkout@v5 + with: + # Full history so BASE and HEAD are both reachable for `git diff`. + fetch-depth: 0 + - name: Compute per-push changed files + id: diff + run: | + if [ "${{ github.event_name }}" = "pull_request" ] && \ + [ "${{ github.event.action }}" = "synchronize" ] && \ + [ -n "${{ github.event.before }}" ]; then + # A push to an already-open PR: before/after give the true + # per-push diff, same as for a branch push. + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + elif [ "${{ github.event_name }}" = "pull_request" ]; then + # First run for this PR (opened/reopened/etc, or synchronize + # without a usable before): fall back to the whole base...head + # diff. + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + fi + + echo "base=$BASE" + echo "head=$HEAD" + + # Fail-safe is the literal FIRST thing written to GITHUB_OUTPUT, so + # any early exit or error further down (a bad BASE/HEAD, a failed + # git diff) leaves docs_only=false in place rather than silently + # falling through to "skip the heavy job". + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + # A missing HEAD/BASE, or an all-zeros BASE (e.g. a new branch's + # first push, where GitHub reports no prior commit), means we + # can't compute a real diff -- run the full matrix rather than + # risk skipping tests. + if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) + + if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then + exit 0 + fi + + DOCS_ONLY=true + while IFS= read -r f; do + if ! [[ "$f" =~ \.(md|asc)$ ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED" + + echo "changed files:" + echo "$CHANGED" + echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" + test: strategy: matrix: @@ -24,13 +128,86 @@ jobs: - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test + # Proves extension_drop survives a BINARY pg_upgrade (in-place catalog + # migration to a newer PostgreSQL major), not just a fresh install. Each + # leg: install the CURRENT extension_drop version on an OLD cluster, plant + # + prove a dependency guard, binary pg_upgrade STRAIGHT to a NEWER major, + # then run the suite against the REAL migrated objects in "existing" mode + # (bin/test_existing). No update-to-current step: extension_drop has only + # ever shipped one real version, so extversion is already current on both + # sides of every leg (see bin/test_existing's own header comment for the + # full reasoning, including why it has no bridge/update-scenario machinery + # unlike its cat_tools model). + # + # Modest matrix: two adjacent-major legs (one near the floor, one near the + # ceiling) plus one full-span leg (floor -> newest). The floor is 12, not + # extension_drop's own claimed 9.3 -- see the top-of-file comment. + pg-upgrade-test: + # Gated behind the cheap jobs: this matrix is expensive (multiple binary + # pg_upgrades), and every leg would fail anyway on a baseline already + # broken by a failing fresh-install test. success() is required + # explicitly once a job's `if:` references anything -- GitHub only + # assumes success() as a default when no `if:` is written at all. + needs: [changes, test] + if: success() && needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + include: + - old_pg: "12" + new_pg: "13" + - old_pg: "16" + new_pg: "17" + - old_pg: "12" + new_pg: "17" + name: ๐Ÿ”„ Binary pg_upgrade ${{ matrix.old_pg }} โ†’ ${{ matrix.new_pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + env: + # Both clusters must use the same initdb options so pg_upgrade sees + # consistent settings (checksums, auth) on old and new clusters. + INITDB_OPTS: --data-checksums --auth trust + steps: + - name: Start PostgreSQL ${{ matrix.old_pg }} + run: pg-start ${{ matrix.old_pg }} + - name: Check out the repo + uses: actions/checkout@v5 + - name: Install rsync + run: apt-get install -y rsync + - name: Recreate old cluster with data checksums enabled + run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }} + - name: Install extension_drop (+ cat_tools) into old cluster + run: make install + - name: Prepare the old cluster (install + dependency guard) + # prepare creates the database, installs extension_drop CASCADE + # (auto-installing cat_tools) at the current version, then plants + + # proves the dependency guard so the later existing-mode run cannot + # silently drop+reinstall and test a fresh install instead. + run: bin/test_existing prepare extension_drop_upgrade + - name: Install PostgreSQL ${{ matrix.new_pg }} + run: apt-get install -y postgresql-${{ matrix.new_pg }} + - name: Install extension_drop (+ cat_tools) into new cluster + # PG_CONFIG must be specified explicitly: at this point both old and + # new PostgreSQL are installed, and the default pg_config on PATH may + # not be the new version's. + run: make install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config + - name: Stop old cluster, binary pg_upgrade to PostgreSQL ${{ matrix.new_pg }}, start new cluster + run: .github/scripts/pg_upgrade_cluster upgrade ${{ matrix.old_pg }} ${{ matrix.new_pg }} + - name: Run the suite against the pg_upgraded database (existing mode) + # run-suite asserts the version, re-proves the dependency guard + # still blocks a non-CASCADE drop (i.e. it survived pg_upgrade), then + # runs the existing-mode-curated suite (see bin/test_existing) against + # the REAL pg_upgraded database via --use-existing -- a plain fresh + # `make test` would silently test a fresh install instead of the + # migrated objects. + run: bin/test_existing run-suite extension_drop_upgrade + # A single stable check name for use as a required status check in branch # protection. Matrix jobs produce names like "๐Ÿ˜ PostgreSQL 14" that change # with the matrix; this aggregates them into one. It passes if every needed - # job succeeded or was skipped (e.g. a docs-only push with paths-ignore) and - # fails if any failed or were cancelled. + # job succeeded or was skipped (e.g. a docs-only push skipping + # pg-upgrade-test) and fails if any failed or were cancelled. all-checks-passed: - needs: [test] + needs: [changes, test, pg-upgrade-test] if: always() runs-on: ubuntu-latest steps: diff --git a/bin/test_existing b/bin/test_existing new file mode 100755 index 0000000..d491a13 --- /dev/null +++ b/bin/test_existing @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +# +# Exercise the extension_drop test suite against a REAL database whose +# extension was installed and then binary pg_upgraded OUTSIDE the suite +# ("existing" mode, test/install/load.sql). The pg-upgrade-test job in +# .github/workflows/ci.yml repeats the same sequence for every old_pg->new_pg +# leg: +# +# install -> plant dependency guard -> binary pg_upgrade -> assert version +# -> run the suite in existing mode +# +# so it lives here once instead of being duplicated as inline YAML. It is NOT +# CI-only: a developer can run any subcommand locally against a scratch +# database (e.g. the manual PG12->PG17 walkthrough this script was verified +# against before being wired into CI). +# +# Modeled on Postgres-Extensions/cat_tools's own bin/test_existing (see +# https://github.com/Postgres-Extensions/cat_tools/pull/16), adapted down to +# what extension_drop actually needs. Notably SMALLER than cat_tools's +# version: extension_drop has only ever shipped one real version (1.0.0 -- +# see HISTORY.asc/RELEASE.md; the 0.1.x PGXN listing predates the current SQL +# entirely), so there is no old version to install, no bridge-update leg, and +# no post-upgrade "update to current" step -- extversion is already current +# on both sides of every leg. That also means none of cat_tools's +# update-scenario/update-check/update-check-version/diff-fresh subcommands +# apply here (there is nothing to update FROM yet); this script only has +# plant-guard, prepare and run-suite. +# +# USAGE: bin/test_existing [args] +# +# plant-guard DB +# Plant + prove the dependency guard against an already-installed +# extension_drop in DB. +# +# prepare DB +# Old-cluster prep for pg-upgrade-test: create DB, CREATE EXTENSION +# extension_drop CASCADE (auto-installs cat_tools) at the current +# version, then plant + prove the dependency guard. +# +# run-suite DB +# Assert DB's extension_drop is at the current version, re-prove the +# dependency guard still blocks a non-CASCADE drop (i.e. it survived +# pg_upgrade), then run the suite in existing mode +# (TEST_LOAD_SOURCE=existing, --use-existing) against DB, and re-prove +# the guard survived the suite run too. +# +# Run `bin/test_existing` with no subcommand to print usage. +# +# Why the dependency guard: "existing" mode must run the suite against the +# ACTUAL pg_upgraded objects. If anything silently dropped + reinstalled the +# extension, the suite would test a FRESH install and hide a regression. We +# plant an object that HARD-references an extension_drop member so a +# non-CASCADE DROP EXTENSION fails, and actively PROVE that here (see +# plant_guard): if the drop unexpectedly succeeds, this script fails CI. See +# also test/sql/dependency_guard.sql, which proves the same mechanism as a +# pgTAP test (a separate, rolled-back proof -- see EXISTING_MODE_EXCLUDE below +# for why this script's guard uses a DIFFERENT schema name than that test's). +# +# EXISTING_MODE_EXCLUDE (below): test/sql/schema.sql's whole job is proving +# the schema-targeting/quoting install pipeline -- it does this by freely +# DROPping and recreating extension_drop (non-CASCADE) in several schemas, +# which is fundamentally incompatible with a real, persistent dependency +# guard: the very first statement in that file (a plain +# `DROP EXTENSION extension_drop;`) would fail against a guarded database, +# not because of a bug, but because the guard is doing exactly its job. +# Confirmed hitting this running the flow locally (PG12->PG17): schema.sql's +# first statement failed with the guard's own 2BP01 error, and +# test/sql/zzz_build.sql (which CASCADE-drops extension_drop to test the raw +# install script in isolation) still passes but adds a +# "drop cascades to view ..." NOTICE that isn't in the checked-in expected +# output for a run with no guard present. Neither is a regression in either +# test file -- both already run, and are already proven, by the regular fresh +# `test` job on every PostgreSQL major. Existing mode's job is different: +# prove the SURVIVING install still works and wasn't silently reinstalled, +# which dependency_guard.sql + simple.sql already cover. So both are excluded +# from the existing-mode REGRESS list here, not papered over. +set -euo pipefail + +# Run from the repository root (where `make` works and test paths resolve), +# regardless of the caller's cwd. bin/ sits directly under the repo root, so +# its parent is the root. readlink -f resolves any path the script was +# invoked through. +cd "$(dirname "$(readlink -f "$0")")/.." + +# A view whose output column has extension_drop's own row type creates a +# pg_depend edge to that extension member, so a non-CASCADE DROP EXTENSION +# cannot succeed. extension_drop__commands is the one state table every other +# object in this extension revolves around (get/add/remove/update, and the +# event trigger, all key off it) -- referencing its row type means the guard +# needs no updating even if a future release adds a column to it. Uses a +# DIFFERENT schema than test/sql/dependency_guard.sql's own +# extension_drop_drop_guard (see the EXISTING_MODE_EXCLUDE comment above) so +# the two never collide when both exist in the same database. +GUARD_SCHEMA=extension_drop_ci_guard +GUARD_VIEW=guard + +# See the top-of-file comment: schema.sql and zzz_build.sql are excluded from +# the existing-mode suite run, not because they're broken, but because their +# own jobs are inherently incompatible with (schema.sql) or produce expected- +# output-irrelevant noise against (zzz_build.sql) a real, persistent +# dependency guard. Both already run, and are already proven, by the regular +# fresh `test` job on every PostgreSQL major. +EXISTING_MODE_EXCLUDE="schema zzz_build" + +# --------------------------------------------------------------------------- +# psql helpers +# --------------------------------------------------------------------------- + +# Capture the single-value (-tAc) output of a query against a database. Used +# for the many "read one value back out" calls below so the psql flags live +# in one place. +psql_value() { + local db=$1 sql=$2 + psql -d "$db" -tAc "$sql" +} + +# Run SQL that must succeed, aborting the whole script on any error +# (ON_ERROR_STOP). Extra args pass through to psql, so callers use either +# `psql_do DB -c '...'` or a heredoc (`psql_do DB </dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p' +} + +installed_version() { + psql_value "$1" \ + "SELECT extversion FROM pg_extension WHERE extname = 'extension_drop'" +} + +guard_present() { + test "$(psql_value "$1" \ + "SELECT count(*) FROM pg_views WHERE schemaname = '$GUARD_SCHEMA' AND viewname = '$GUARD_VIEW'")" = 1 +} + +# Plant the guard and PROVE it blocks a non-CASCADE drop. Call right after +# CREATE EXTENSION (and before pg_upgrade) so it persists through it. +plant_guard() { + local db=$1 + psql_do "$db" </dev/null 2>&1; then + echo "FAIL: DROP EXTENSION extension_drop (non-CASCADE) unexpectedly SUCCEEDED in '$db' -- dependency guard is ineffective" >&2 + exit 1 + fi + guard_present "$db" \ + || { echo "FAIL: dependency guard missing from '$db' after the drop attempt" >&2; exit 1; } + test -n "$(installed_version "$db")" \ + || { echo "FAIL: extension_drop extension missing from '$db' after the drop attempt" >&2; exit 1; } + echo "OK: non-CASCADE DROP EXTENSION is blocked in '$db' (dependency guard effective)" +} + +# Empty-value guards on BOTH sides: an empty `installed`/`expected` must fail +# loudly, not silently pass via `"" != ""` being false. +assert_version() { + local db=$1 expected=$2 installed + [ "$expected" = current ] && expected=$(current_version) + installed=$(installed_version "$db") + echo "version check '$db': installed='$installed' expected='$expected'" + if [ -z "$installed" ] || [ -z "$expected" ] || [ "$installed" != "$expected" ]; then + echo "FAIL: extension_drop in '$db' is '$installed', expected '$expected'" >&2 + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Subcommand implementations +# --------------------------------------------------------------------------- + +# prepare DB +# Old-cluster preparation for pg-upgrade-test: create the database, CREATE +# EXTENSION extension_drop CASCADE (extension_drop requires cat_tools; +# CASCADE auto-installs it, matching test/install/load.sql's own PG10+ +# branch), then plant + prove the dependency guard. There is no old version +# to install and no bridge to a safe version -- extension_drop has never +# shipped a pg_upgrade-unsafe release (its only prior PGXN listing, 0.1.x +# from 2017, predates the current SQL entirely), so every leg simply +# installs the CURRENT version fresh on the old cluster. +prepare() { + local db=$1 + createdb "$db" + psql_do "$db" -c "CREATE EXTENSION extension_drop CASCADE" + plant_guard "$db" +} + +# Compute the existing-mode REGRESS list: every test/sql/*.sql basename +# except EXISTING_MODE_EXCLUDE (see the top-of-file comment for why). Derived +# from the actual directory contents (not a hardcoded full list) so a future +# new test/sql file is automatically included in existing-mode runs unless +# someone deliberately adds it to EXISTING_MODE_EXCLUDE. +existing_mode_regress() { + local f base + for f in test/sql/*.sql; do + base=$(basename "$f" .sql) + case " $EXISTING_MODE_EXCLUDE " in + *" $base "*) continue ;; + esac + echo "$base" + done +} + +# Run the pgTAP suite against an already-populated database in existing mode. +# Verifies extension_drop is at the current version, re-proves the guard +# still blocks a drop (i.e. it survived pg_upgrade), runs the (curated, see +# existing_mode_regress) suite via --use-existing so pg_regress does NOT +# drop/recreate the database, then confirms the guard is still present (a +# CASCADE drop+reinstall would have removed it). +run_suite() { + local db=$1 + assert_version "$db" current + assert_drop_blocked "$db" + local regress + regress=$(existing_mode_regress | tr '\n' ' ') + # In existing mode pg_regress runs against $db via --use-existing and must + # NOT create/drop its own database. Two consequences drive the make args + # below (see ~/advanced-extension-testing.md ยง5, and pgxntool base.mk): + # 1. PGXNTOOL_ENABLE_TEST_BUILD=no: harmless here (this repo has no + # test/build/ directory, so it already auto-detects to "no"), kept + # explicit to match the documented existing-mode invocation and guard + # against a future test/build/ addition breaking this flow silently. + # 2. verify-results depends on `test`, so it re-runs the suite; it must + # carry the SAME existing-mode overrides (including REGRESS) or it + # would re-run the FULL suite (hitting the schema.sql/zzz_build.sql + # conflict above) instead of verifying THIS existing database. + # An ARRAY, not a flat string: REGRESS's value is itself multiple + # space-separated test names, so passing it through a single unquoted + # variable (like cat_tools's own $existing_args, which never needed a + # multi-word override) would word-split REGRESS=dependency_guard from + # simple and hand `make` a bogus "simple" goal instead of a REGRESS value + # -- confirmed hitting exactly this ("No rule to make target 'simple'") + # before switching to an array here. + local existing_args=( + "TEST_LOAD_SOURCE=existing" + "CONTRIB_TESTDB=$db" + "EXTRA_REGRESS_OPTS=--use-existing" + "PGXNTOOL_ENABLE_TEST_BUILD=no" + "REGRESS=$regress" + ) + make test "${existing_args[@]}" + make verify-results "${existing_args[@]}" + # Post-suite guard assertion: a CASCADE drop+reinstall during the run would + # have removed the guard view, meaning the suite tested a fresh install, + # not $db. + guard_present "$db" \ + || { echo "FAIL: dependency guard vanished during the suite run on '$db' -- extension was dropped+reinstalled (CASCADE)?" >&2; exit 1; } +} + +usage() { + echo "usage: bin/test_existing [args]" >&2 + echo " plant-guard DB" >&2 + echo " prepare DB" >&2 + echo " run-suite DB" >&2 + exit 2 +} + +# Explicit subcommand dispatch on $1. Defined first for readability; INVOKED +# at the very bottom, after every helper it calls is defined (bash resolves +# calls at runtime, so main() appearing first is fine). +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + plant-guard) plant_guard "$@" ;; + prepare) prepare "$@" ;; + run-suite) run_suite "$@" ;; + *) usage ;; + esac +} + +main "$@" + +# vi: expandtab ts=2 sw=2