From 142279ee568fadb58c9ea4fa5695ade9225186fc Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sat, 1 Aug 2026 16:11:44 -0500 Subject: [PATCH 1/2] Phase 4: real pg_upgrade support via a reduced bin/test_existing Adds the pg-upgrade-test CI job: install 0.9.6 on an old PostgreSQL major, plant + prove a dependency guard, binary pg_upgrade to a newer major, ALTER EXTENSION UPDATE the migrated objects, then run the suite against the real upgraded database in existing mode. bin/test_existing is much smaller than the equivalent script would have been pre-test/install: only prepare-old and run-suite are genuinely external-to-pg_regress concerns (a real pg_upgrade binary run isn't something pg_regress can invoke itself), plus a small `update` subcommand for the post-upgrade ALTER EXTENSION UPDATE step. There's no update-scenario subcommand at all - that entire scenario is just `make test-update` now (test/install/load.sql's own 'update' mode, added in phase 3), since an in-place update has no external step to drive. run_suite() gates on plain `make test`, not the old belt-and-suspenders `make test && make verify-results` - pgxntool 2.3.0 (this repo's phase 0) already made `make test` itself exit non-zero on regression failures. Not yet crossed with TEST_SCHEMA - that's the next phase, once both this job and extension-update-test can cross it together. Verified locally against PG17 (prepare-old -> update -> run-suite, without a real pg_upgrade - this container's clusters are persistent shared infra, so the actual binary pg_upgrade leg is left for CI's ephemeral containers, same reasoning as the pg-tle-test work). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 108 ++++++++++++- bin/test_existing | 210 +++++++++++++++++++++++++ bin/test_existing.sql/assert_guard.sql | 35 +++++ bin/test_existing.sql/drop_guard.sql | 11 ++ bin/test_existing.sql/plant_guard.sql | 30 ++++ 5 files changed, 393 insertions(+), 1 deletion(-) create mode 100755 bin/test_existing create mode 100644 bin/test_existing.sql/assert_guard.sql create mode 100644 bin/test_existing.sql/drop_guard.sql create mode 100644 bin/test_existing.sql/plant_guard.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1848af8..2e9a55c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,16 @@ # own job would only duplicate this job's own # per-PG-version container/checkout setup for # no added confidence. +# pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on an OLD +# PostgreSQL major, binary-upgrade the cluster +# to a NEWER major, then update the extension +# to current - proves objects created on an +# old server still work when read on a new +# one. A smaller old_pg/new_pg matrix (not the +# full PG matrix - by far the most expensive +# job here, installing two full PostgreSQL +# majors and running the real pg_upgrade +# binary per leg). # pg-tle-test -- pg_tle DEPLOYMENT: fresh install registered # through AWS pg_tle's database-backed catalog # instead of a filesystem .control file. @@ -202,6 +212,102 @@ jobs: - name: Update 0.9.6 -> current and run the suite run: make verify-results TEST_LOAD_SOURCE=update + # Proves count_nulls survives a BINARY pg_upgrade (in-place catalog + # migration to a newer PostgreSQL major), not just an in-place extension + # update. Installs 0.9.6 on an old cluster, plants a dependency guard, + # binary-pg_upgrades to a newer cluster, updates the extension to current, + # then runs the suite against the REAL migrated objects in existing mode. + # No bridge-update step first: count_nulls has always been pure SQL + # functions with no SELECT-*-over-catalog views, so it has no known + # pg_upgrade-unsafe old version to bridge past. + # + # Deliberately not doing a stepwise every-major-in-sequence climb (one + # cluster walking 10->11->12->...->newest, vs. the single big jumps here): + # that would catch a regression specific to one particular major-to-major + # boundary, which would matter if count_nulls had views/functions touching + # catalog internals, but it doesn't - pure SQL functions over anyarray/ + # json/jsonb, nothing version-sensitive to break at a specific boundary. + # Revisit if count_nulls ever grows something catalog-touching. + # + # Not yet crossed with TEST_SCHEMA (a later phase adds that, once it can + # do so for both this job and the test job's update leg together). + pg-upgrade-test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + old_pg: ["10", "12"] + new_pg: ["18"] + 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 or pg_upgrade + # refuses to run. + INITDB_OPTS: --data-checksums --auth trust + steps: + - name: Start PostgreSQL ${{ matrix.old_pg }} + run: pg-start ${{ matrix.old_pg }} + - name: Recreate old cluster with data checksums enabled + run: | + pg_ctlcluster ${{ matrix.old_pg }} test stop + pg_dropcluster ${{ matrix.old_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 ${{ matrix.old_pg }} test -- $INITDB_OPTS + pg_ctlcluster ${{ matrix.old_pg }} test start + pg_isready -t 30 + - name: Check out the repo + uses: actions/checkout@v4 + - name: Install count_nulls into old cluster + run: make install + - name: Prepare the old cluster (install + dependency guard) + # prepare-old installs count_nulls at 0.9.6, then plants + proves + # the dependency guard, so a later accidental CASCADE drop anywhere + # in this job cannot silently make the eventual existing-mode run + # test a fresh install instead. + run: bin/test_existing prepare-old count_nulls_upgrade "" 0.9.6 + - name: Install PostgreSQL ${{ matrix.new_pg }} + run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} + - name: Install count_nulls 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: | + pg_ctlcluster ${{ matrix.old_pg }} test stop + pg_createcluster -p 5432 ${{ matrix.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/${{ matrix.new_pg }}/bin/pg_upgrade \ + -b /usr/lib/postgresql/${{ matrix.old_pg }}/bin \ + -B /usr/lib/postgresql/${{ matrix.new_pg }}/bin \ + -d /var/lib/postgresql/${{ matrix.old_pg }}/test \ + -D /var/lib/postgresql/${{ matrix.new_pg }}/test \ + -o '-c config_file=/etc/postgresql/${{ matrix.old_pg }}/test/postgresql.conf' \ + -O '-c config_file=/etc/postgresql/${{ matrix.new_pg }}/test/postgresql.conf'" postgres \ + || { find /tmp/pg_upgrade_logs \ + /var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \ + -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } + pg_ctlcluster ${{ matrix.new_pg }} test start + - name: Update the pg_upgraded extension to the current version + # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects + # (the extension binary pg_upgrade just migrated), running the + # 0.9.6->stable update script. + run: bin/test_existing update count_nulls_upgrade + - 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), + # drops the guard, then runs the suite against the REAL pg_upgraded + # + updated database via --use-existing (so pg_regress does not + # drop/recreate it) - a plain fresh `make test` would silently test + # a fresh install instead of the migrated objects. + run: bin/test_existing run-suite count_nulls_upgrade "" + pg-tle-test: needs: [changes] if: needs.changes.outputs.docs_only != 'true' @@ -332,7 +438,7 @@ jobs: # `changes` job on a docs-only push), and fails if any failed or were # cancelled. all-checks-passed: - needs: [changes, lint, test, pg-tle-test] + needs: [changes, lint, test, pg-upgrade-test, pg-tle-test] if: always() runs-on: ubuntu-latest steps: diff --git a/bin/test_existing b/bin/test_existing new file mode 100755 index 0000000..d0c2cb6 --- /dev/null +++ b/bin/test_existing @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# +# Exercise the count_nulls test suite against a REAL database whose extension +# was installed/upgraded OUTSIDE this pg_regress invocation ("existing" mode) +# - specifically, a real binary pg_upgrade run. Unlike an in-place ALTER +# EXTENSION UPDATE (which test/install/load.sql's own 'update' mode handles +# entirely by itself - see `make test-update`), a real pg_upgrade is an +# external binary process pg_regress can't invoke itself, so THAT leg needs +# an external script to drive it. This is a smaller script than it might look +# like it needs to be: only the pieces that genuinely can't live inside a +# single pg_regress invocation are here. +# +# The pg-upgrade-test CI job repeats the same sequence: +# +# prepare-old (install + plant guard) -> [real pg_upgrade binary, in CI] -> +# update (ALTER EXTENSION UPDATE) -> run-suite (assert + run existing-mode) +# +# so it lives here once instead of being duplicated as inline YAML. Not +# CI-only: a developer can run any subcommand locally against a scratch +# database. Modeled on Postgres-Extensions/cat_tools's bin/test_existing. +# Two differences from that script: count_nulls ships no +# SELECT-*-over-catalog views, so it has no known pg_upgrade-unsafe old +# version to bridge past before running pg_upgrade; and its own suite has a +# legitimate (though harmless - always rolled back) DROP EXTENSION test, so +# here the guard is dropped before run-suite instead of surviving through it. +# +# USAGE: bin/test_existing [args] +# +# prepare-old DB SCHEMA INSTALL_VERSION +# Old-cluster prep for pg-upgrade-test: create DB + extension at +# INSTALL_VERSION in SCHEMA, then plant + prove the dependency guard. +# +# update DB [TO_VERSION] +# ALTER EXTENSION count_nulls UPDATE [TO 'TO_VERSION'] (empty => current). +# +# run-suite DB SCHEMA +# Assert the current version, re-prove the guard, drop it, then run the +# suite in existing mode (extension must be at the current version). +# +# Run `bin/test_existing` with no subcommand to print usage. +# +# Why the dependency guard: "existing" mode must run the suite against the +# ACTUAL upgraded objects. If anything silently dropped + reinstalled the +# extension (a stray CASCADE, a logic bug, a bad CI step), the suite would +# test a FRESH install and hide a regression. We plant an object that HARD- +# references a count_nulls member so a non-CASCADE DROP EXTENSION fails, and +# actively PROVE that (see bin/test_existing.sql/assert_guard.sql): if the +# drop unexpectedly succeeds, this script fails CI rather than silently +# passing. +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")")/.." + +# --------------------------------------------------------------------------- +# psql helpers +# --------------------------------------------------------------------------- + +psql_value() { + local db=$1 sql=$2 + psql -d "$db" -tAc "$sql" +} + +psql_do() { + local db=$1 + shift + psql -d "$db" -v ON_ERROR_STOP=1 "$@" +} + +# --------------------------------------------------------------------------- +# Version / guard helpers +# --------------------------------------------------------------------------- + +current_version() { + # EXTENSION_count_nulls_VERSION (the .control file's default_version), NOT + # PGXNVERSION (the PGXN distribution version, from META.in.json) - a + # version-less CREATE EXTENSION/ALTER EXTENSION UPDATE installs whatever + # the control file's default_version says, and count_nulls' is currently + # the 'stable' pseudo-version, not the last real release. Using PGXNVERSION + # here would compare an installed 'stable' against an expected real version + # number and always report a mismatch. See RELEASE.md's note on + # distribution vs. extension versions; the pg-tle-test CI job makes the + # same distinction for the same reason. + make -s print-EXTENSION_count_nulls_VERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p' +} + +installed_version() { + psql_value "$1" \ + "SELECT extversion FROM pg_extension WHERE extname = 'count_nulls'" +} + +# Plant the guard and PROVE it blocks a non-CASCADE drop. Call right after +# CREATE EXTENSION (and before any update/upgrade) so it persists through them. +plant_guard() { + local db=$1 schema=$2 + psql -d "$db" -v ON_ERROR_STOP=1 -v schema="$schema" -f bin/test_existing.sql/plant_guard.sql + assert_drop_blocked "$db" +} + +# The core safeguard self-check: a non-CASCADE DROP EXTENSION MUST fail while +# the guard exists. assert_guard.sql fails loudly (nonzero exit) if the drop +# unexpectedly succeeds, or if the extension/guard are missing afterward. +assert_drop_blocked() { + local db=$1 + psql -d "$db" -v ON_ERROR_STOP=1 -f bin/test_existing.sql/assert_guard.sql + echo "OK: non-CASCADE DROP EXTENSION is blocked in '$db' (dependency guard effective)" +} + +drop_guard() { + local db=$1 + psql -d "$db" -v ON_ERROR_STOP=1 -f bin/test_existing.sql/drop_guard.sql +} + +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: count_nulls in '$db' is '$installed', expected '$expected'" >&2 + exit 1 + fi +} + +update_ext() { + local db=$1 to=${2:-} + # Prepend "TO " only when a target version is given, so a single statement + # covers both cases (empty $to => bare "ALTER EXTENSION ... UPDATE" to + # current). Use `if`, not `&&`: a false test under `set -e` would abort. + if [ -n "$to" ]; then to="TO '$to'"; fi + psql_do "$db" -c "ALTER EXTENSION count_nulls UPDATE $to" +} + +# CREATE EXTENSION count_nulls at VERSION, targeting SCHEMA - unless SCHEMA +# is empty, in which case it's created untouched, wherever the session's +# own default search_path resolves (ordinarily 'public'). A quoted empty +# identifier ("") is a real Postgres syntax error, so this can't just always +# emit `CREATE SCHEMA IF NOT EXISTS "$schema"` - the empty case has to skip +# that entirely, mirroring test/install/load.sql's own :count_nulls_has_schema +# branch. +create_extension_in_schema() { + local db=$1 schema=$2 version=$3 sql="" + if [ -n "$schema" ]; then + sql="CREATE SCHEMA IF NOT EXISTS \"$schema\"; SET search_path = \"$schema\"; " + fi + psql_do "$db" -c "${sql}CREATE EXTENSION count_nulls VERSION '$version'" +} + +# --------------------------------------------------------------------------- +# Subcommand implementations +# --------------------------------------------------------------------------- + +# prepare-old DB SCHEMA INSTALL_VERSION +# Old-cluster preparation for pg-upgrade-test: create the database and the +# extension at INSTALL_VERSION in SCHEMA, then plant + prove the guard. No +# bridge-update step first: count_nulls ships no SELECT-*-over-catalog +# views, so it has no known pg_upgrade-unsafe old version to bridge past. +prepare_old() { + local db=$1 schema=$2 install=$3 + createdb "$db" + create_extension_in_schema "$db" "$schema" "$install" + plant_guard "$db" "$schema" +} + +# Run the pgTAP suite against an already-populated database in existing mode. +# Verifies count_nulls is at the current version, re-proves the guard still +# blocks a drop (i.e. it survived the update/upgrade), drops the guard (see +# the file header for why - count_nulls's own suite legitimately drops the +# extension, harmlessly, inside a transaction that's always rolled back), +# then runs the suite via --use-existing so pg_regress does NOT drop/recreate +# the database. +run_suite() { + local db=$1 schema=$2 + assert_version "$db" current + assert_drop_blocked "$db" + drop_guard "$db" + # In existing mode pg_regress runs against $db via --use-existing and must + # NOT create/drop its own database. `make test` (not just `make + # verify-results`) is a real gate as of pgxntool 2.3.0 - it now exits + # non-zero on regression failures instead of always exiting 0 regardless + # of pg_regress's result (see this repo's pgxntool 2.3.0 bump). + make test TEST_LOAD_SOURCE=existing TEST_SCHEMA="$schema" CONTRIB_TESTDB="$db" EXTRA_REGRESS_OPTS=--use-existing +} + +usage() { + echo "usage: bin/test_existing [args]" >&2 + echo " prepare-old DB SCHEMA INSTALL_VERSION" >&2 + echo " update DB [TO_VERSION]" >&2 + echo " run-suite DB SCHEMA" >&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 + prepare-old) prepare_old "$@" ;; + update) update_ext "$@" ;; + run-suite) run_suite "$@" ;; + *) usage ;; + esac +} + +main "$@" diff --git a/bin/test_existing.sql/assert_guard.sql b/bin/test_existing.sql/assert_guard.sql new file mode 100644 index 0000000..33946cf --- /dev/null +++ b/bin/test_existing.sql/assert_guard.sql @@ -0,0 +1,35 @@ +/* + * Proves the guard planted by plant_guard.sql actually blocks a + * non-CASCADE DROP EXTENSION - prove it, don't assume it. Re-run after + * every step (install, pg_upgrade, post-upgrade ALTER EXTENSION UPDATE): + * the guard disappearing at any point means a CASCADE drop happened + * somewhere upstream, i.e. the "existing" run downstream would actually be + * a silent fresh install. + * + * Usage: psql -v ON_ERROR_STOP=1 -f assert_guard.sql + */ +\set ON_ERROR_STOP on + +DO $$ +BEGIN + DROP EXTENSION count_nulls; + -- Only reached if the drop above unexpectedly succeeded. + RAISE EXCEPTION 'GUARD FAILURE: non-CASCADE DROP EXTENSION count_nulls unexpectedly succeeded'; +EXCEPTION WHEN dependent_objects_still_exist THEN + RAISE NOTICE 'guard held: DROP EXTENSION count_nulls correctly blocked'; +END +$$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'count_nulls') THEN + RAISE EXCEPTION 'GUARD FAILURE: count_nulls extension missing after guard check'; + END IF; + IF NOT EXISTS ( + SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'guard' AND n.nspname = 'count_nulls_drop_guard' + ) THEN + RAISE EXCEPTION 'GUARD FAILURE: count_nulls_drop_guard.guard view missing'; + END IF; +END +$$; diff --git a/bin/test_existing.sql/drop_guard.sql b/bin/test_existing.sql/drop_guard.sql new file mode 100644 index 0000000..744c3b5 --- /dev/null +++ b/bin/test_existing.sql/drop_guard.sql @@ -0,0 +1,11 @@ +/* + * Removes the guard (plant_guard.sql) once its job is done - proving the + * install/pg_upgrade/update steps didn't corrupt the real extension - so it + * doesn't then block the pgTap suite's own DROP EXTENSION test + * (test__shutdown__drop_all, run in a transaction that's rolled back + * regardless, so re-dropping the real extension there is harmless). + * + * Usage: psql -v ON_ERROR_STOP=1 -f drop_guard.sql + */ +\set ON_ERROR_STOP on +DROP SCHEMA count_nulls_drop_guard CASCADE; diff --git a/bin/test_existing.sql/plant_guard.sql b/bin/test_existing.sql/plant_guard.sql new file mode 100644 index 0000000..1c3e84d --- /dev/null +++ b/bin/test_existing.sql/plant_guard.sql @@ -0,0 +1,30 @@ +/* + * Dependency guard: plants an object with a hard pg_depend dependency on a + * stable, never-dropped/redefined extension member (null_count(anyarray), + * unchanged since 0.9.0), so that a non-CASCADE DROP EXTENSION count_nulls + * is blocked. Used by the pg_upgrade CI job to prove a real pg_upgrade/ + * update run didn't silently destroy the extension it's meant to be + * testing (a stray CASCADE drop, a logic bug, a bad CI step would + * otherwise fall through to a silent fresh reinstall and the job would + * still report green). + * + * Usage: psql -v ON_ERROR_STOP=1 -v schema= -f plant_guard.sql + * (empty schema means "wherever null_count already resolves unqualified" - + * i.e. count_nulls was installed without targeting a schema). + */ +\set ON_ERROR_STOP on + +/* + * schema_prefix: either empty, or the quoted schema name followed by a + * literal '.' - so the view definition below is a single statement with a + * plain (unquoted) substitution, rather than branching the whole CREATE + * VIEW on whether a schema was given. quote_ident(), not :"schema" - + * :schema_prefix is pasted as-is (unquoted substitution), so it must + * already be valid, properly-quoted SQL text by the time it lands there. + */ +SELECT CASE WHEN :'schema' <> '' THEN quote_ident(:'schema') || '.' ELSE '' END AS schema_prefix +\gset + +CREATE SCHEMA IF NOT EXISTS count_nulls_drop_guard; +CREATE OR REPLACE VIEW count_nulls_drop_guard.guard AS + SELECT :schema_prefix null_count(NULL::int, NULL::int) AS guarded_member; From bc3c24e11811d8a209b9a7f817eb53a53dd02b1e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Wed, 5 Aug 2026 16:41:23 -0500 Subject: [PATCH 2/2] pg-upgrade-test: run ALTER EXTENSION UPDATE before binary pg_upgrade, not after Reorders prepare-old -> update -> pg_upgrade -> run-suite (was prepare-old -> pg_upgrade -> update -> run-suite). The old order proved pg_upgrade could migrate 0.9.6's frozen objects, then updated afterward - not actionable, since that version already shipped. This job's whole point is proving pg_upgrade correctly migrates the objects count_nulls' CURRENT code creates, which requires updating BEFORE the binary upgrade runs. make install (into the old cluster) already happens earlier in the job, so the current version's update scripts are on disk in time for the moved step. Updates the job's step names/comments and bin/test_existing's own file-header sequence description to match the new order. --- .github/workflows/ci.yml | 65 ++++++++++++++++++++++++---------------- bin/test_existing | 14 ++++++--- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e9a55c..9902b04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,15 +17,18 @@ # per-PG-version container/checkout setup for # no added confidence. # pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on an OLD -# PostgreSQL major, binary-upgrade the cluster -# to a NEWER major, then update the extension -# to current - proves objects created on an -# old server still work when read on a new -# one. A smaller old_pg/new_pg matrix (not the -# full PG matrix - by far the most expensive -# job here, installing two full PostgreSQL -# majors and running the real pg_upgrade -# binary per leg). +# PostgreSQL major, update the extension to +# current (still on the old major), THEN +# binary-upgrade the cluster to a NEWER major - +# proves pg_upgrade correctly migrates the +# objects the extension actually creates +# TODAY, not objects frozen at some past +# version (which would be untestable anyway - +# that old version already shipped). A smaller +# old_pg/new_pg matrix (not the full PG matrix +# - by far the most expensive job here, +# installing two full PostgreSQL majors and +# running the real pg_upgrade binary per leg). # pg-tle-test -- pg_tle DEPLOYMENT: fresh install registered # through AWS pg_tle's database-backed catalog # instead of a filesystem .control file. @@ -213,13 +216,19 @@ jobs: run: make verify-results TEST_LOAD_SOURCE=update # Proves count_nulls survives a BINARY pg_upgrade (in-place catalog - # migration to a newer PostgreSQL major), not just an in-place extension - # update. Installs 0.9.6 on an old cluster, plants a dependency guard, - # binary-pg_upgrades to a newer cluster, updates the extension to current, + # migration to a newer PostgreSQL major). Installs 0.9.6 on an old + # cluster, plants a dependency guard, updates the extension to CURRENT + # (still on the old major), THEN binary-pg_upgrades to a newer cluster, # then runs the suite against the REAL migrated objects in existing mode. - # No bridge-update step first: count_nulls has always been pure SQL - # functions with no SELECT-*-over-catalog views, so it has no known - # pg_upgrade-unsafe old version to bridge past. + # Updating before the binary upgrade (not after) is deliberate: the whole + # point of this job is proving pg_upgrade correctly migrates the objects + # count_nulls' CURRENT code actually creates - migrating 0.9.6's objects + # and updating afterward would instead test whether pg_upgrade can + # migrate a legacy structure frozen in the past, which isn't actionable + # (that version already shipped; nothing to fix if it turned out + # fragile). No bridge-update step first: count_nulls has always been + # pure SQL functions with no SELECT-*-over-catalog views, so it has no + # known pg_upgrade-unsafe old version to bridge past. # # Deliberately not doing a stepwise every-major-in-sequence climb (one # cluster walking 10->11->12->...->newest, vs. the single big jumps here): @@ -268,6 +277,17 @@ jobs: # in this job cannot silently make the eventual existing-mode run # test a fresh install instead. run: bin/test_existing prepare-old count_nulls_upgrade "" 0.9.6 + - name: Update the extension to the current version (still on the old cluster) + # Exercises ALTER EXTENSION UPDATE on the OLD cluster, BEFORE the + # binary pg_upgrade below, running the 0.9.6->stable update script - + # deliberately in this order (not update-after-upgrade): this job + # exists to prove pg_upgrade correctly migrates the objects + # count_nulls' CURRENT code creates, so pg_upgrade must run against + # already-current objects, not 0.9.6 ones. `make install` above + # already installed the current version's update scripts/control + # file into this (old) cluster's sharedir, so they're in place for + # this ALTER EXTENSION UPDATE to use. + run: bin/test_existing update count_nulls_upgrade - name: Install PostgreSQL ${{ matrix.new_pg }} run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} - name: Install count_nulls into new cluster @@ -294,18 +314,13 @@ jobs: /var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \ -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } pg_ctlcluster ${{ matrix.new_pg }} test start - - name: Update the pg_upgraded extension to the current version - # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects - # (the extension binary pg_upgrade just migrated), running the - # 0.9.6->stable update script. - run: bin/test_existing update count_nulls_upgrade - 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), - # drops the guard, then runs the suite against the REAL pg_upgraded - # + updated database via --use-existing (so pg_regress does not - # drop/recreate it) - a plain fresh `make test` would silently test - # a fresh install instead of the migrated objects. + # still blocks a non-CASCADE drop (i.e. it survived both the update + # and pg_upgrade), drops the guard, then runs the suite against the + # REAL pg_upgraded database via --use-existing (so pg_regress does + # not drop/recreate it) - a plain fresh `make test` would silently + # test a fresh install instead of the migrated objects. run: bin/test_existing run-suite count_nulls_upgrade "" pg-tle-test: diff --git a/bin/test_existing b/bin/test_existing index d0c2cb6..f0b12f5 100755 --- a/bin/test_existing +++ b/bin/test_existing @@ -12,11 +12,17 @@ # # The pg-upgrade-test CI job repeats the same sequence: # -# prepare-old (install + plant guard) -> [real pg_upgrade binary, in CI] -> -# update (ALTER EXTENSION UPDATE) -> run-suite (assert + run existing-mode) +# prepare-old (install + plant guard) -> update (ALTER EXTENSION UPDATE, +# on the OLD cluster, before pg_upgrade) -> [real pg_upgrade binary, in +# CI] -> run-suite (assert + run existing-mode) # -# so it lives here once instead of being duplicated as inline YAML. Not -# CI-only: a developer can run any subcommand locally against a scratch +# so it lives here once instead of being duplicated as inline YAML. update +# runs BEFORE the binary pg_upgrade, not after: the point of this job is +# proving pg_upgrade correctly migrates the objects count_nulls' CURRENT +# code creates, so pg_upgrade needs to run against already-current objects, +# not ones still frozen at the old INSTALL_VERSION. +# +# Not CI-only: a developer can run any subcommand locally against a scratch # database. Modeled on Postgres-Extensions/cat_tools's bin/test_existing. # Two differences from that script: count_nulls ships no # SELECT-*-over-catalog views, so it has no known pg_upgrade-unsafe old