From d7cdd7a520e19e81020c771d3beea3e3ec68d05e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:10:49 -0500 Subject: [PATCH 1/8] Replace Travis with GitHub Actions CI Adds `.github/workflows/ci.yml`: a changes job that skips the test matrix on docs-only pushes and derives the supported PostgreSQL major list from one pair of constants, a per-major test job in the `pgxn/pgxn-tools` container that runs `make verify-results`, and a single `all-checks-passed` job for branch protection. Removes the Travis configuration (`.travis.yml`, `pg-travis-test.sh`), which tested only PostgreSQL 9.5 and 9.6. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 132 +++++++++++++++++++++++++++++++++++++++ .travis.yml | 9 --- pg-travis-test.sh | 29 --------- 3 files changed, 132 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 pg-travis-test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b0ed7ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,132 @@ +name: CI +on: + push: + branches: + - master + pull_request: +env: + PGUSER: postgres +jobs: + # Cheap gate that lets the test matrix below skip itself on commits that + # touch only docs. Runs on every push/pull_request unconditionally (no + # paths-ignore on the workflow itself) -- a workflow-level paths-ignore + # would skip this job too on a docs-only push, and the required + # all-checks-passed check would then never report and get stuck Pending in + # branch protection. + # + # Also derives the supported-PostgreSQL-major list the test job's matrix + # consumes, from a single pair of constants below, so adding or dropping a + # major is a one-line edit here instead of touching the matrix directly. + changes: + name: 🔍 Detect changes & derive PG matrix + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.diff.outputs.docs_only }} + supported_pg: ${{ steps.pg.outputs.supported_pg }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + # Full history needed so BASE and HEAD below are both reachable + # for `git diff`. + fetch-depth: 0 + - name: Compute per-push changed files + id: diff + run: | + # Fail-safe FIRST, before anything else runs: any early exit below + # (an unusable BASE/HEAD, a failed git diff) leaves this in place, + # so the test matrix only ever gets skipped after actually proving + # the push is docs-only. + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + 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" + + # A missing HEAD, or an all-zeros BASE (a new branch's first push, + # where GitHub reports no prior commit), means no real diff can be + # computed -- leave the fail-safe in place. + if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE" "$HEAD") || exit 0 + [ -z "$CHANGED" ] && exit 0 + + 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" + - name: Derive the supported-PostgreSQL-major list + id: pg + run: | + # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To + # add or drop a major, edit only the two constants below; the test + # job's matrix derives its version list from them. Do NOT hardcode + # a supported major directly in a job matrix. + # + # NEWEST -- highest PostgreSQL major tested. + # CURRENT_FLOOR -- oldest major supported. + NEWEST=18 + CURRENT_FLOOR=12 + + supported=$(seq "$NEWEST" -1 "$CURRENT_FLOOR") + + # Emit a JSON array for the test job's matrix to consume via + # fromJSON. + json=$(printf '%s\n' $supported | paste -sd, - | sed 's/^/[/; s/$/]/') + echo "supported_pg=$json" >> "$GITHUB_OUTPUT" + + test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + # Supported majors, from the single source in the changes job. + pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} + name: 🐘 PostgreSQL ${{ matrix.pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + steps: + - name: Start PostgreSQL ${{ matrix.pg }} + run: pg-start ${{ matrix.pg }} + - name: Check out the repo + uses: actions/checkout@v4 + - name: Test on PostgreSQL ${{ matrix.pg }} + # verify-results runs the same TEST_DEPS chain as `make test`, then + # inspects the pgTAP results (PGXNTOOL_ENABLE_VERIFY_RESULTS defaults + # to yes, PGXNTOOL_VERIFY_RESULTS_MODE to pgtap) and exits non-zero on + # failure. + run: make verify-results + + # A single stable check name for use as a required status check in branch + # protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14" + # which would all need to be listed individually and updated whenever the + # matrix changes. This job passes if all others passed or were skipped + # (e.g. test, on a docs-only push), and fails if any failed or were + # cancelled. + all-checks-passed: + needs: [changes, test] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all jobs passed or were skipped + run: | + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more jobs failed or were cancelled" + exit 1 + fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0a8d3ea..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: c -before_install: - - wget https://gist.github.com/petere/5893799/raw/apt.postgresql.org.sh - - sudo sh ./apt.postgresql.org.sh - - sudo sh -c "echo deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs 2>/dev/null)-pgdg main $PGVERSION >> /etc/apt/sources.list.d/pgdg.list" -env: - - PGVERSION=9.6 - - PGVERSION=9.5 -script: bash ./pg-travis-test.sh diff --git a/pg-travis-test.sh b/pg-travis-test.sh deleted file mode 100644 index 8ce45c1..0000000 --- a/pg-travis-test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -# Based on https://gist.github.com/petere/6023944 - -set -eux - -sudo apt-get update - -packages="python-setuptools postgresql-$PGVERSION postgresql-server-dev-$PGVERSION postgresql-common" - -# bug: http://www.postgresql.org/message-id/20130508192711.GA9243@msgid.df7cb.de -sudo update-alternatives --remove-all postmaster.1.gz - -# stop all existing instances (because of https://github.com/travis-ci/travis-cookbooks/pull/221) -sudo service postgresql stop -# and make sure they don't come back -echo 'exit 0' | sudo tee /etc/init.d/postgresql -sudo chmod a+x /etc/init.d/postgresql - -sudo apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install $packages - -sudo easy_install pgxnclient - -PGPORT=55435 -sudo pg_createcluster --locale=C --start $PGVERSION test -p $PGPORT -- -A trust -# TODO: have base.mk support dynamic sudo -sudo PGPORT=$PGPORT PGUSER=postgres PG_CONFIG=/usr/lib/postgresql/$PGVERSION/bin/pg_config make test - -[ ! -e test/regression.diffs ] From 57dd2403a09331e5b3ddda5c55f90844b903138c Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:25:24 -0500 Subject: [PATCH 2/8] Derive the PostgreSQL major list from NEWEST/FLOOR plus a 9.x tail Matches the final trunklet CI workflow: supported range is PostgreSQL 9.5 through 18, with 9.6 and 9.5 listed explicitly because `seq` can't express them. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ed7ce..9ad8d18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,17 +74,24 @@ jobs: - name: Derive the supported-PostgreSQL-major list id: pg run: | - # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To - # add or drop a major, edit only the two constants below; the test - # job's matrix derives its version list from them. Do NOT hardcode - # a supported major directly in a job matrix. + # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To add + # or drop a major, edit ONLY the constants and the 9.x tail below; the + # test job's matrix derives from them (adding the newest major is a + # one-line NEWEST bump). Do NOT hardcode a supported major in a job + # matrix. # - # NEWEST -- highest PostgreSQL major tested. - # CURRENT_FLOOR -- oldest major supported. + # NEWEST -- highest PostgreSQL major tested. + # FLOOR -- oldest integer major (10+) supported. Supported + # versions older than that are the 9.x tail below. The + # overall floor is 9.5, which trunklet requires (see + # META.in.json). NEWEST=18 - CURRENT_FLOOR=12 + FLOOR=10 - supported=$(seq "$NEWEST" -1 "$CURRENT_FLOOR") + # seq can't express 9.x, so those majors are listed explicitly. When + # 9.x support is dropped, delete the tail and make this a plain + # `seq "$NEWEST" -1 "$FLOOR"` (FLOOR then becomes the overall floor). + supported="$(seq "$NEWEST" -1 "$FLOOR") 9.6 9.5" # Emit a JSON array for the test job's matrix to consume via # fromJSON. From ea509d13da6e2082d35a28a9fa4f34a098163b87 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:55:43 -0500 Subject: [PATCH 3/8] Raise the supported PostgreSQL floor to 12 extension_drop installs cat_tools >= 0.2.1, which now resolves to cat_tools 0.3.0; that release requires PostgreSQL 12 and fails to install on older majors. Set the PostgreSQL build prereq to 12.0.0, make the CI matrix a plain range from NEWEST down to FLOOR=12, and drop the test setup code that only ran before PostgreSQL 10. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 20 +++++++------------- META.in.json | 4 ++-- META.json | 4 ++-- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ad8d18..3fd49cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,23 +75,17 @@ jobs: id: pg run: | # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To add - # or drop a major, edit ONLY the constants and the 9.x tail below; the - # test job's matrix derives from them (adding the newest major is a - # one-line NEWEST bump). Do NOT hardcode a supported major in a job - # matrix. + # or drop a major, edit ONLY the constants below; the test job's + # matrix derives from them (adding the newest major is a one-line + # NEWEST bump). Do NOT hardcode a supported major in a job matrix. # # NEWEST -- highest PostgreSQL major tested. - # FLOOR -- oldest integer major (10+) supported. Supported - # versions older than that are the 9.x tail below. The - # overall floor is 9.5, which trunklet requires (see - # META.in.json). + # FLOOR -- oldest PostgreSQL major supported; keep it in sync + # with the PostgreSQL build prereq in META.in.json. NEWEST=18 - FLOOR=10 + FLOOR=12 - # seq can't express 9.x, so those majors are listed explicitly. When - # 9.x support is dropped, delete the tail and make this a plain - # `seq "$NEWEST" -1 "$FLOOR"` (FLOOR then becomes the overall floor). - supported="$(seq "$NEWEST" -1 "$FLOOR") 9.6 9.5" + supported="$(seq "$NEWEST" -1 "$FLOOR")" # Emit a JSON array for the test job's matrix to consume via # fromJSON. diff --git a/META.in.json b/META.in.json index 67e359c..c948485 100644 --- a/META.in.json +++ b/META.in.json @@ -73,14 +73,14 @@ "X_comment": "Valid keys for 'prereqs' are configure, build, runtime, or test.", "X_comment": "Valid keys for 'requires' are requires, recommends, suggests, or conflicts.", "X_comment": "Requires JSONB, which was released in 9.4", - "X_comment": "trunklet currently requires 9.5", + "X_comment": "PostgreSQL 12: extension_drop installs cat_tools >= 0.2.1, which resolves to cat_tools 0.3.0, and that requires 12", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { "build": { "requires": { - "PostgreSQL": "9.5.0", + "PostgreSQL": "12.0.0", "extension_drop": ">= 0.1.1", "trunklet": ">= 0.3.3" } diff --git a/META.json b/META.json index 4475eb6..6b80a95 100644 --- a/META.json +++ b/META.json @@ -73,14 +73,14 @@ "X_comment": "Valid keys for 'prereqs' are configure, build, runtime, or test.", "X_comment": "Valid keys for 'requires' are requires, recommends, suggests, or conflicts.", "X_comment": "Requires JSONB, which was released in 9.4", - "X_comment": "trunklet currently requires 9.5", + "X_comment": "PostgreSQL 12: extension_drop installs cat_tools >= 0.2.1, which resolves to cat_tools 0.3.0, and that requires 12", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { "build": { "requires": { - "PostgreSQL": "9.5.0", + "PostgreSQL": "12.0.0", "extension_drop": ">= 0.1.1", "trunklet": ">= 0.3.3" } From 6912fb1b8a5fbfcfd9dde1c22aa1aceceb48f5c2 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:12:43 -0500 Subject: [PATCH 4/8] Test the update from 0.2.0 alongside the fresh install `make test-update` (TEST_LOAD_SOURCE=update) runs the same suite with the same expected output, but pg_temp._create() installs 0.2.0 and runs ALTER EXTENSION UPDATE instead of installing the current version. It creates extension_drop first, since 0.2.0 did not require it. CI runs it as a second step in each test job. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 9 ++++++--- Makefile | 22 ++++++++++++++++++++++ test/deps.sql | 21 ++++++++++++++++++++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fd49cb..f47a50b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,8 @@ jobs: # all-checks-passed check would then never report and get stuck Pending in # branch protection. # - # Also derives the supported-PostgreSQL-major list the test job's matrix - # consumes, from a single pair of constants below, so adding or dropping a - # major is a one-line edit here instead of touching the matrix directly. + # Also derives the supported-PostgreSQL-major list for the test job's matrix + # (see the step below). changes: name: 🔍 Detect changes & derive PG matrix runs-on: ubuntu-latest @@ -113,6 +112,10 @@ jobs: # to yes, PGXNTOOL_VERIFY_RESULTS_MODE to pgtap) and exits non-zero on # failure. run: make verify-results + - name: Test the update from 0.2.0 on PostgreSQL ${{ matrix.pg }} + # Same suite and expected output, but each test installs 0.2.0 and runs + # ALTER EXTENSION UPDATE (see TEST_LOAD_SOURCE in the Makefile). + run: make verify-results TEST_LOAD_SOURCE=update # A single stable check name for use as a required status check in branch # protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14" diff --git a/Makefile b/Makefile index 5b353bf..3750560 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,27 @@ include pgxntool/base.mk +# TEST_LOAD_SOURCE selects how pg_temp._create() (test/deps.sql) installs the +# extension: +# - fresh (default): CREATE EXTENSION at the current version. +# - update: CREATE EXTENSION at TEST_UPDATE_FROM, then ALTER EXTENSION UPDATE +# to the current version. The suite and its expected output are the same, +# so a pass means the updated install behaves like a fresh one. +# The settings reach test/deps.sql as placeholder GUCs via PGOPTIONS, which the +# psql processes pg_regress spawns inherit. They are exported unconditionally so +# deps.sql can read them without missing_ok and fail if they didn't propagate. +TEST_LOAD_SOURCE ?= fresh +ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update),) +$(error TEST_LOAD_SOURCE must be 'fresh' or 'update', got '$(TEST_LOAD_SOURCE)') +endif +TEST_UPDATE_FROM ?= 0.2.0 +export PGOPTIONS := $(PGOPTIONS) -c trunklet_format.test_load_mode=$(TEST_LOAD_SOURCE) -c trunklet_format.test_update_from=$(TEST_UPDATE_FROM) + +# Recurses rather than depending on `test` so the TEST_LOAD_SOURCE check above +# re-evaluates with update set. +.PHONY: test-update +test-update: + $(MAKE) test TEST_LOAD_SOURCE=update + # TODO: Remove this after merging pgxntool 0.2.1+ testdeps: $(TEST_SQL_FILES) $(TEST_SOURCE_FILES) diff --git a/test/deps.sql b/test/deps.sql index f9c6a6b..8e8fe7a 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -9,9 +9,28 @@ CREATE OR REPLACE FUNCTION pg_temp._create() RETURNS void LANGUAGE plpgsql SET client_min_messages = WARNING -- Necessary due to CASCADE AS $body$ +DECLARE + c_mode CONSTANT text := current_setting('trunklet_format.test_load_mode'); -- Set by the Makefile BEGIN -- No IF NOT EXISTS because we'll be confused if we're not loading the new stuff - CREATE EXTENSION "trunklet-format" CASCADE; + IF c_mode = 'fresh' THEN + CREATE EXTENSION "trunklet-format" CASCADE; + ELSIF c_mode = 'update' THEN + -- 0.2.0 did not require extension_drop, so updating from it needs it created first + CREATE EXTENSION IF NOT EXISTS extension_drop CASCADE; + EXECUTE format( + 'CREATE EXTENSION "trunklet-format" VERSION %L CASCADE' + , current_setting('trunklet_format.test_update_from') + ); + ALTER EXTENSION "trunklet-format" UPDATE; + IF (SELECT extversion FROM pg_extension WHERE extname = 'trunklet-format') + IS DISTINCT FROM (SELECT default_version FROM pg_available_extensions WHERE name = 'trunklet-format') + THEN + RAISE EXCEPTION 'ALTER EXTENSION UPDATE did not reach the default version'; + END IF; + ELSE + RAISE EXCEPTION 'unknown trunklet_format.test_load_mode "%"', c_mode; + END IF; END $body$; From 3c0b6fe83d63e303bbd71629598a3bde24319756 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:12:43 -0500 Subject: [PATCH 5/8] Note the PostgreSQL 12 floor in HISTORY and drop stale comments Co-Authored-By: Claude Opus 5.5 --- HISTORY.asc | 3 +++ META.in.json | 2 +- META.json | 2 +- test/sql/base.sql | 4 ++-- test/sql/build.sql | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/HISTORY.asc b/HISTORY.asc index 1c3e53b..5fd44c0 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,5 +1,8 @@ STABLE ------ +=== Require PostgreSQL 12 or newer +extension_drop pulls in cat_tools, and cat_tools 0.3.0 and newer require PostgreSQL 12. + === Fix bug where extension could not be dropped The extension now requires extension_drop and trunklet 0.3.3 or later, neither of which 0.2.0 required. Updating from trunklet-format 0.2.0 (PostgreSQL 12 or later) requires trunklet >= 0.3.3: diff --git a/META.in.json b/META.in.json index c948485..1b131bf 100644 --- a/META.in.json +++ b/META.in.json @@ -73,7 +73,7 @@ "X_comment": "Valid keys for 'prereqs' are configure, build, runtime, or test.", "X_comment": "Valid keys for 'requires' are requires, recommends, suggests, or conflicts.", "X_comment": "Requires JSONB, which was released in 9.4", - "X_comment": "PostgreSQL 12: extension_drop installs cat_tools >= 0.2.1, which resolves to cat_tools 0.3.0, and that requires 12", + "X_comment": "PostgreSQL 12: extension_drop pulls in cat_tools, and cat_tools >= 0.3.0 requires PostgreSQL 12", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { diff --git a/META.json b/META.json index 6b80a95..4d286b8 100644 --- a/META.json +++ b/META.json @@ -73,7 +73,7 @@ "X_comment": "Valid keys for 'prereqs' are configure, build, runtime, or test.", "X_comment": "Valid keys for 'requires' are requires, recommends, suggests, or conflicts.", "X_comment": "Requires JSONB, which was released in 9.4", - "X_comment": "PostgreSQL 12: extension_drop installs cat_tools >= 0.2.1, which resolves to cat_tools 0.3.0, and that requires 12", + "X_comment": "PostgreSQL 12: extension_drop pulls in cat_tools, and cat_tools >= 0.3.0 requires PostgreSQL 12", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { diff --git a/test/sql/base.sql b/test/sql/base.sql index 2da40a4..fd1ce07 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -125,7 +125,7 @@ SELECT throws_ok( , '22004' -- null_value_not_allowed , 'SQL identifier format option ("I") not allowed with optional parameters' ); --- This is meant to ensure the DETAIL is correct, but it doesn't work on travis :( +-- Disabled check that the DETAIL is correct /* SAVEPOINT a; \unset ON_ERROR_STOP @@ -157,7 +157,7 @@ SELECT throws_ok( ORDER BY sn, opn ) specifier_test ; --- This is meant to ensure the DETAIL is correct, but it doesn't work on travis :( +-- Disabled check that the DETAIL is correct /* SAVEPOINT a; \unset ON_ERROR_STOP diff --git a/test/sql/build.sql b/test/sql/build.sql index 86bf8da..dc8f78a 100644 --- a/test/sql/build.sql +++ b/test/sql/build.sql @@ -4,7 +4,7 @@ BEGIN; \i test/deps.sql --- This is just an easy way to get the dependencies pulled in on 10.0+ +-- This is just an easy way to get the dependencies pulled in \set saved_verbosity :VERBOSITY \set VERBOSITY terse SELECT pg_temp._create(); From ae265031d4325f69f483ae9e45f222abbaa46bca Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:15:37 -0500 Subject: [PATCH 6/8] Keep the update-from version only in the Makefile; README floor to 12 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 +++--- README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f47a50b..839c722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,9 +112,9 @@ jobs: # to yes, PGXNTOOL_VERIFY_RESULTS_MODE to pgtap) and exits non-zero on # failure. run: make verify-results - - name: Test the update from 0.2.0 on PostgreSQL ${{ matrix.pg }} - # Same suite and expected output, but each test installs 0.2.0 and runs - # ALTER EXTENSION UPDATE (see TEST_LOAD_SOURCE in the Makefile). + - name: Test the update path on PostgreSQL ${{ matrix.pg }} + # Same suite and expected output, but each test installs the Makefile's + # TEST_UPDATE_FROM version and runs ALTER EXTENSION UPDATE. run: make verify-results TEST_LOAD_SOURCE=update # A single stable check name for use as a required status check in branch diff --git a/README.md b/README.md index 9db7b39..ecfc893 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You need to run the test suite using a super user, such as the default make installcheck PGUSER=postgres Once trunklet-format is installed, you can add it to a database. If you're running -PostgreSQL 9.1.0 or greater, it's a simple as connecting to a database as a +PostgreSQL 12 or greater, it's a simple as connecting to a database as a super user and running: CREATE EXTENSION "trunklet-format"; From 8dc22b5eea7a98460948740bd73e9ca78feefef0 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 26 Oct 2026 16:32:15 -0500 Subject: [PATCH 7/8] Re-enable DETAIL checks as pgTAP assertions Replace the commented-out psql-based DETAIL checks in base.sql with a pg_temp helper that captures PG_EXCEPTION_DETAIL, asserted with is(). Co-Authored-By: Claude Sonnet 5.5 --- test/expected/base.out | 77 ++++++++++++++++++++++-------------------- test/sql/base.sql | 59 +++++++++++++++++--------------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/test/expected/base.out b/test/expected/base.out index d77098f..1f3fbe9 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -25,19 +25,19 @@ ok 14 - test optional handling with L and ok 15 - test optional handling with L and text ok 16 - test optional handling with L and true ok 17 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -ok 18 - threw Unexpected character "" in format specifier "" -ok 19 - threw Unexpected character "" in format specifier "O" -ok 20 - threw Unexpected character "a" in format specifier "a" -ok 21 - threw Unexpected character "a" in format specifier "Oa" -ok 22 - threw Unexpected character "S" in format specifier "S" -ok 23 - threw Unexpected character "S" in format specifier "OS" -ok 24 - threw Unexpected character "i" in format specifier "i" -ok 25 - threw Unexpected character "i" in format specifier "Oi" -ok 26 - threw Unexpected character "l" in format specifier "l" -ok 27 - threw Unexpected character "l" in format specifier "Ol" -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' +ok 18 - DETAIL for optional SQL identifier error +ok 19 - threw Unexpected character "" in format specifier "" +ok 20 - threw Unexpected character "" in format specifier "O" +ok 21 - threw Unexpected character "a" in format specifier "a" +ok 22 - threw Unexpected character "a" in format specifier "Oa" +ok 23 - threw Unexpected character "S" in format specifier "S" +ok 24 - threw Unexpected character "S" in format specifier "OS" +ok 25 - threw Unexpected character "i" in format specifier "i" +ok 26 - threw Unexpected character "i" in format specifier "Oi" +ok 27 - threw Unexpected character "l" in format specifier "l" +ok 28 - threw Unexpected character "l" in format specifier "Ol" +ok 29 - DETAIL for unexpected format character in 'more%Moo%OSmore' +ok 30 - DETAIL for unexpected format character in 'more%Moo%lmore' ok 31 - Should throw exception like 'parameter "mia" not found' ok 32 - Should throw exception like 'parameter "mia" not found' ok 33 - Should throw exception like 'parameter "mia" not found' @@ -59,28 +59,31 @@ ok 48 - Should throw exception like 'parameter "mia" not found' ok 49 - Should throw exception like 'parameter "mia" not found' ok 50 - Should throw exception like 'parameter "mia" not found' ok 51 - Should throw exception like 'parameter "mia" not found' -ok 52 - Test extract_paramaters() for single string -ok 53 - Test extract_paramaters() for number -ok 54 - Test extract_paramaters() for multiple -ok 55 - Test extract_paramaters() for num & bogus -ok 56 - Test extract_paramaters() for bogus -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 59 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 60 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 63 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 64 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 67 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 70 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -ok 73 - threw object is not supported as a parameter type -ok 74 - threw array is not supported as a parameter type -1..74 +ok 52 - Should throw exception like 'parameter "mia" not found' +ok 53 - Should throw exception like 'parameter "mia" not found' +ok 54 - Should throw exception like 'parameter "mia" not found' +ok 55 - Test extract_paramaters() for single string +ok 56 - Test extract_paramaters() for number +ok 57 - Test extract_paramaters() for multiple +ok 58 - Test extract_paramaters() for num & bogus +ok 59 - Test extract_paramaters() for bogus +ok 60 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') +ok 61 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') +ok 62 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') +ok 63 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') +ok 64 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) +ok 65 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) +ok 66 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) +ok 67 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) +ok 68 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" +ok 69 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" +ok 70 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" +ok 71 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" +ok 72 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" +ok 73 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" +ok 74 - threw object is not supported as a parameter type +ok 75 - threw array is not supported as a parameter type +ok 76 - threw object is not supported as a parameter type +ok 77 - threw array is not supported as a parameter type +1..77 # TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/sql/base.sql b/test/sql/base.sql index fd1ce07..9634803 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -125,18 +125,27 @@ SELECT throws_ok( , '22004' -- null_value_not_allowed , 'SQL identifier format option ("I") not allowed with optional parameters' ); --- Disabled check that the DETAIL is correct -/* -SAVEPOINT a; -\unset ON_ERROR_STOP -\set VERBOSITY default -\echo THIS ERROR IS OK! -SELECT 'DETAIL: parameter "test parameter" at template position ' || strpos( 'some stuff%test parameter%OIsome more stuff', '%' ); -SELECT trunklet.process_language( 'format', 'some stuff%test parameter%OIsome more stuff', NULL::json ); -ROLLBACK TO a; -\set ON_ERROR_STOP 1 -\set VERBOSITY VERBOSE -*/ +CREATE FUNCTION pg_temp.error_detail(sql text) RETURNS text LANGUAGE plpgsql AS $$ +DECLARE + v_detail text; +BEGIN + EXECUTE sql; + RETURN NULL; +EXCEPTION WHEN others THEN + GET STACKED DIAGNOSTICS v_detail = PG_EXCEPTION_DETAIL; + RETURN v_detail; +END +$$; + +SELECT is( + pg_temp.error_detail( format( + $$SELECT trunklet.process_language( 'format', %L, NULL::json )$$ + , t + ) ) + , 'parameter "test parameter" at template position ' || strpos( t, '%' ) + , 'DETAIL for optional SQL identifier error' +) + FROM (SELECT 'some stuff%test parameter%OIsome more stuff'::text AS t) x; SELECT throws_ok( format( @@ -157,22 +166,16 @@ SELECT throws_ok( ORDER BY sn, opn ) specifier_test ; --- Disabled check that the DETAIL is correct -/* -SAVEPOINT a; -\unset ON_ERROR_STOP -\set VERBOSITY default -\echo THIS ERROR IS OK! -SELECT 'DETAIL: parameter "Moo" at template position ' || strpos( 'more%Moo%OSmore', '%' ); -SELECT trunklet.process_language('format', 'more%Moo%OSmore', NULL::json); -ROLLBACK TO a; -\echo THIS ERROR IS OK! -SELECT 'DETAIL: parameter "Moo" at template position ' || strpos( 'more%Moo%OSmore', '%' ); -SELECT trunklet.process_language('format', 'more%Moo%lmore', NULL::json); -ROLLBACK TO a; -\set ON_ERROR_STOP 1 -\set VERBOSITY VERBOSE -*/ +SELECT is( + pg_temp.error_detail( format( + $$SELECT trunklet.process_language( 'format', %L, NULL::json )$$ + , t + ) ) + , 'parameter "Moo" at template position ' || strpos( t, '%' ) + , format( 'DETAIL for unexpected format character in %L', t ) +) + FROM unnest('{more%Moo%OSmore,more%Moo%lmore}'::text[]) WITH ORDINALITY u(t, n) + ORDER BY n; SELECT throws_like( format( From a9f969ffaf6278d0cd67084e4433ac94efca88a1 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 26 Oct 2026 16:51:21 -0500 Subject: [PATCH 8/8] Simplify DETAIL assertions: helper takes the template, single is() call Co-Authored-By: Claude Sonnet 5.5 --- test/expected/base.out | 26 +++++++++++----------- test/sql/base.sql | 49 ++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/test/expected/base.out b/test/expected/base.out index 1f3fbe9..f545b8c 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -25,19 +25,19 @@ ok 14 - test optional handling with L and ok 15 - test optional handling with L and text ok 16 - test optional handling with L and true ok 17 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -ok 18 - DETAIL for optional SQL identifier error -ok 19 - threw Unexpected character "" in format specifier "" -ok 20 - threw Unexpected character "" in format specifier "O" -ok 21 - threw Unexpected character "a" in format specifier "a" -ok 22 - threw Unexpected character "a" in format specifier "Oa" -ok 23 - threw Unexpected character "S" in format specifier "S" -ok 24 - threw Unexpected character "S" in format specifier "OS" -ok 25 - threw Unexpected character "i" in format specifier "i" -ok 26 - threw Unexpected character "i" in format specifier "Oi" -ok 27 - threw Unexpected character "l" in format specifier "l" -ok 28 - threw Unexpected character "l" in format specifier "Ol" -ok 29 - DETAIL for unexpected format character in 'more%Moo%OSmore' -ok 30 - DETAIL for unexpected format character in 'more%Moo%lmore' +ok 18 - threw Unexpected character "" in format specifier "" +ok 19 - threw Unexpected character "" in format specifier "O" +ok 20 - threw Unexpected character "a" in format specifier "a" +ok 21 - threw Unexpected character "a" in format specifier "Oa" +ok 22 - threw Unexpected character "S" in format specifier "S" +ok 23 - threw Unexpected character "S" in format specifier "OS" +ok 24 - threw Unexpected character "i" in format specifier "i" +ok 25 - threw Unexpected character "i" in format specifier "Oi" +ok 26 - threw Unexpected character "l" in format specifier "l" +ok 27 - threw Unexpected character "l" in format specifier "Ol" +ok 28 - DETAIL for 'some stuff%test parameter%OIsome more stuff' +ok 29 - DETAIL for 'more%Moo%OSmore' +ok 30 - DETAIL for 'more%Moo%lmore' ok 31 - Should throw exception like 'parameter "mia" not found' ok 32 - Should throw exception like 'parameter "mia" not found' ok 33 - Should throw exception like 'parameter "mia" not found' diff --git a/test/sql/base.sql b/test/sql/base.sql index 9634803..4bcfdf5 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -13,6 +13,19 @@ CREATE TABLE process( -- Protect against infinite loops SET statement_timeout = '2 s'; SELECT no_plan(); +-- Returns the DETAIL of the error raised by processing the template, or NULL if none. +CREATE FUNCTION pg_temp.error_detail(template text) RETURNS text LANGUAGE plpgsql AS $$ +DECLARE + v_detail text; +BEGIN + PERFORM trunklet.process_language( 'format', template, NULL::json ); + RETURN NULL; +EXCEPTION WHEN others THEN + GET STACKED DIAGNOSTICS v_detail = PG_EXCEPTION_DETAIL; + RETURN v_detail; +END +$$; + SELECT is( trunklet.process_language( 'format' @@ -125,27 +138,6 @@ SELECT throws_ok( , '22004' -- null_value_not_allowed , 'SQL identifier format option ("I") not allowed with optional parameters' ); -CREATE FUNCTION pg_temp.error_detail(sql text) RETURNS text LANGUAGE plpgsql AS $$ -DECLARE - v_detail text; -BEGIN - EXECUTE sql; - RETURN NULL; -EXCEPTION WHEN others THEN - GET STACKED DIAGNOSTICS v_detail = PG_EXCEPTION_DETAIL; - RETURN v_detail; -END -$$; - -SELECT is( - pg_temp.error_detail( format( - $$SELECT trunklet.process_language( 'format', %L, NULL::json )$$ - , t - ) ) - , 'parameter "test parameter" at template position ' || strpos( t, '%' ) - , 'DETAIL for optional SQL identifier error' -) - FROM (SELECT 'some stuff%test parameter%OIsome more stuff'::text AS t) x; SELECT throws_ok( format( @@ -167,14 +159,15 @@ SELECT throws_ok( ) specifier_test ; SELECT is( - pg_temp.error_detail( format( - $$SELECT trunklet.process_language( 'format', %L, NULL::json )$$ - , t - ) ) - , 'parameter "Moo" at template position ' || strpos( t, '%' ) - , format( 'DETAIL for unexpected format character in %L', t ) + pg_temp.error_detail(t) + , 'parameter "' || parameter || '" at template position ' || strpos( t, '%' ) + , format( 'DETAIL for %L', t ) ) - FROM unnest('{more%Moo%OSmore,more%Moo%lmore}'::text[]) WITH ORDINALITY u(t, n) + FROM (VALUES + (1, 'some stuff%test parameter%OIsome more stuff', 'test parameter') + , (2, 'more%Moo%OSmore', 'Moo') + , (3, 'more%Moo%lmore', 'Moo') + ) v(n, t, parameter) ORDER BY n; SELECT throws_like(