Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 366 additions & 2 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,76 @@ testdeps: $(wildcard test/*/*.sql) $(wildcard test/*.sql) # Be careful not to in
# sql/count_nulls--stable.sql) - a pgxntool bug, filed as
# Postgres-Extensions/pgxntool#48.
DATA += sql/count_nulls--1.0.0.sql

# Same gap, for the oldest version we still ship a full install script for
# (0.9.6): test/deps.sql's 'update' load mode and bin/test_existing both
# need `CREATE EXTENSION count_nulls VERSION '0.9.6'` to work, and it's
# neither the CURRENT version file nor an upgrade script, so base.mk's DATA
# wildcard misses it too.
DATA += sql/count_nulls--0.9.6.sql

# TEST_LOAD_SOURCE selects how test/deps.sql installs the extension for the
# WHOLE test suite:
# - fresh (default): CREATE EXTENSION count_nulls (current version).
# - update: CREATE EXTENSION at the oldest version we still ship a full
# install script for (0.9.6), then ALTER EXTENSION UPDATE to current.
# - existing: the extension is already installed (a real `pg_upgrade` run,
# or an out-of-band update) - deps.sql only asserts it's present and
# current, it does not drop/create/update anything. Meant to be run with
# CONTRIB_TESTDB=<db> EXTRA_REGRESS_OPTS=--use-existing
# PGXNTOOL_ENABLE_TEST_BUILD=no against a real database, not via a make
# wrapper here (see bin/test_existing and the pg_upgrade CI job).
# Running the SAME suite with the SAME expected output against the updated/
# upgraded database proves it behaves identically to a fresh install.
#
# "update" (this) is extension-level (ALTER EXTENSION UPDATE); "upgrade" is
# cluster-level (pg_upgrade) - 'existing' is how that axis is exercised.
# Don't conflate the two in variable names or comments.
#
# The mode is signalled to test/deps.sql via the count_nulls.test_load_mode
# placeholder GUC. pg_regress does not forward make variables, but the psql
# processes it spawns inherit the environment, so PGOPTIONS reaches deps.sql.
# It's exported unconditionally so deps.sql can read it without missing_ok
# and fail loudly if it didn't propagate, instead of silently defaulting to
# fresh and running the wrong suite.
TEST_LOAD_SOURCE ?= fresh
ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update existing),)
$(error TEST_LOAD_SOURCE must be 'fresh', 'update' or 'existing', got '$(TEST_LOAD_SOURCE)')
endif
export PGOPTIONS := $(PGOPTIONS) -c count_nulls.test_load_mode=$(TEST_LOAD_SOURCE)

# TEST_SCHEMA selects which schema test/deps.sql installs count_nulls into,
# for the WHOLE test run (every test file uses the SAME schema in a given
# run - previously each test file hardcoded its own literal schema name as a
# stand-in for real schema-qualification coverage, which only ever tested
# two fixed, always-lowercase names).
#
# Empty (the default): don't target any schema at all. The extension
# installs wherever the session's own default search_path already resolves
# (ordinarily 'public'), exercising the plain, untouched default-install
# code path - the "without a schema" leg.
#
# Non-empty: explicitly CREATE SCHEMA/SET search_path to that name before
# installing - the "with a schema" leg. In particular, a schema whose name
# requires SQL identifier quoting (mixed case - unquoted would silently fold
# to lowercase) exercises the suite's %I schema-qualification, not just its
# literal test data. Locally: `make test TEST_SCHEMA=Quoted`.
#
# Both legs matter and both run in CI - "without" and "with" are genuinely
# different code paths (one never touches search_path, the other explicitly
# targets a schema), not one being a redundant special case of the other.
#
# Propagated the same way as TEST_LOAD_SOURCE: via the count_nulls.test_schema
# GUC, exported unconditionally through PGOPTIONS. Empty is a valid,
# deliberate value here (not an error) - deps.sql reads it without
# missing_ok, so a truly unset GUC (the pipeline failing to propagate at all)
# still fails loudly, while an explicitly empty one means "no schema".
TEST_SCHEMA ?=
export PGOPTIONS := $(PGOPTIONS) -c count_nulls.test_schema=$(TEST_SCHEMA)

# Convenience wrapper: `make test-update` == `make test TEST_LOAD_SOURCE=update`.
# Must recurse (a fresh $(MAKE)) rather than depend on `test`, so the
# parse-time TEST_LOAD_SOURCE conditional above re-evaluates with update set.
.PHONY: test-update
test-update:
$(MAKE) test TEST_LOAD_SOURCE=update
103 changes: 103 additions & 0 deletions bin/compare_fresh_vs_update
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
#
# compare_fresh_vs_update - structurally compare every object the count_nulls
# extension owns between a FRESH install (CREATE EXTENSION at current) and an
# UPDATED one (CREATE EXTENSION at FROM_VERSION, then ALTER EXTENSION
# UPDATE), in the same schema. A fixed pgTAP expected-output suite (see
# test/deps.sql's 'update' mode) only proves the specific behaviors it
# happens to assert still work - it does not prove an update script left
# object definitions/comments/ACLs BYTE-FOR-BYTE identical to a fresh
# install of the same version. This catches divergence classes a fixed
# suite doesn't already know to test for.
#
# Modeled on the manual technique used in Postgres-Extensions/cat_tools#46,
# which found a real bug this way (a pre-0.2.2 update path left
# `EXECUTE PROCEDURE` hardcoded in a trigger body that fresh installs had
# already updated to `EXECUTE FUNCTION`). Unlike that PR, this is committed,
# reusable tooling rather than a one-off manual diff.
#
# What's compared, per object the extension owns (discovered live via
# pg_depend - see extension_members(), not a hardcoded object list, so a
# newly added function is automatically covered without editing this
# script): pg_get_functiondef() (full definition: schema, args, body,
# volatility, strictness - everything), its comment (obj_description), and
# its ACL (proacl). count_nulls currently ships only functions/triggers, no
# views - the doc this is modeled on also compares pg_get_viewdef/type
# labels/extension membership for extensions that have those; add a query
# for the relevant catalog (pg_class for views, pg_type for types, ...) the
# same way if count_nulls ever grows one.
#
# USAGE: bin/compare_fresh_vs_update [SCHEMA] [FROM_VERSION]
# SCHEMA - schema both installs target (default: unqualified, same
# as TEST_SCHEMA empty - see the Makefile). Both installs
# use the SAME schema, since the point is comparing object
# definitions, not exercising schema-qualification (that's
# TEST_SCHEMA's job in the regular suite).
# FROM_VERSION - the update origin (default: 0.9.6, the oldest version
# count_nulls still ships a full install script for).
#
# Exits nonzero (and prints a real diff) on ANY difference. Scratch
# databases are dropped on exit regardless of outcome.
set -euo pipefail

cd "$(dirname "$(readlink -f "$0")")/.."

schema=${1:-}
from_version=${2:-0.9.6}

fresh_db=compare_fresh_vs_update_fresh
update_db=compare_fresh_vs_update_updated
fresh_snapshot=$(mktemp)
update_snapshot=$(mktemp)

cleanup() {
dropdb --if-exists "$fresh_db"
dropdb --if-exists "$update_db"
rm -f "$fresh_snapshot" "$update_snapshot"
}
trap cleanup EXIT

# extension_members(): every object pg_depend records as owned by the
# count_nulls extension (deptype 'e'), restricted to pg_proc for now (see
# the header comment on extending this). Ordered by name/args so the two
# snapshots line up for a textual diff regardless of OID assignment order,
# which differs between a fresh install and an update.
query() {
cat <<'SQL'
SELECT
'-- ' || p.oid::regprocedure::text || E'\n'
|| pg_get_functiondef(p.oid) || E'\n'
|| '-- comment: ' || coalesce(obj_description(p.oid, 'pg_proc'), '(none)') || E'\n'
|| '-- acl: ' || coalesce(p.proacl::text, '(default)') || E'\n'
FROM pg_depend d
JOIN pg_extension x ON d.refobjid = x.oid AND x.extname = 'count_nulls'
JOIN pg_proc p ON d.objid = p.oid AND d.classid = 'pg_proc'::regclass
WHERE d.deptype = 'e'
ORDER BY p.proname, p.oid::regprocedure::text;
SQL
}

install_in_schema() {
local db=$1 sql=""
if [ -n "$schema" ]; then
sql="CREATE SCHEMA IF NOT EXISTS \"$schema\"; SET search_path = \"$schema\"; "
fi
echo "$sql"
}

createdb "$fresh_db"
psql -d "$fresh_db" -v ON_ERROR_STOP=1 -c "$(install_in_schema "$fresh_db")CREATE EXTENSION count_nulls"

createdb "$update_db"
psql -d "$update_db" -v ON_ERROR_STOP=1 -c "$(install_in_schema "$update_db")CREATE EXTENSION count_nulls VERSION '$from_version'"
psql -d "$update_db" -v ON_ERROR_STOP=1 -c "SET client_min_messages = WARNING; ALTER EXTENSION count_nulls UPDATE"

psql -d "$fresh_db" -tA -v ON_ERROR_STOP=1 -c "$(query)" > "$fresh_snapshot"
psql -d "$update_db" -tA -v ON_ERROR_STOP=1 -c "$(query)" > "$update_snapshot"

if diff -u "$fresh_snapshot" "$update_snapshot"; then
echo "OK: fresh install and $from_version->current update produce IDENTICAL object definitions/comments/ACLs"
else
echo "FAIL: update path diverges from a fresh install of the same version - see diff above" >&2
exit 1
fi
Loading
Loading