diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4aaec9..2e175f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,176 @@ +# =========================================================================== +# Test strategy +# +# count_nulls can be arrived at several ways, each of which can break +# differently, so each is exercised by its own job below: +# +# test -- FRESH install: CREATE EXTENSION at the current +# version, across every supported PostgreSQL +# major x schema leg (empty/none and Quoted - +# see TEST_SCHEMA in the Makefile). +# extension-update-test -- IN-PLACE update: CREATE EXTENSION at 0.9.6 +# (the oldest version we still ship a full +# install script for) then ALTER EXTENSION +# UPDATE (same PostgreSQL, no pg_upgrade), same +# PG x schema matrix as test. +# 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 x schema matrix (not the +# full PG matrix - this job is by far the most +# expensive, installing two full PostgreSQL +# majors and running the real pg_upgrade binary +# per leg). +# +# All three matrices cross PostgreSQL major with the TEST_SCHEMA axis: +# without a schema specified at all (empty - CREATE EXTENSION lands wherever +# the session's own default search_path resolves) and with one explicitly +# targeted, using a name that requires SQL identifier quoting. Neither leg is +# redundant with the other - see the Makefile's TEST_SCHEMA comment. +# +# `changes` is a cheap gate that lets all three heavy jobs above skip +# themselves on doc-only pushes, and also derives the shared PostgreSQL-major +# list those jobs consume from a single set of constants. `all-checks-passed` +# is the single stable required-status-check name. +# =========================================================================== name: CI on: [push, pull_request] jobs: + # Cheap gate that lets the heavy jobs below skip themselves 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 doc-only pushes and get stuck Pending in branch + # protection. + # + # Also derives, from a SINGLE set of constants, the supported-PostgreSQL- + # major list the test / extension-update-test jobs consume (see the + # "Derive ..." step): every job that cares which majors are supported reads + # the SAME list, so they can't silently drift onto different sets, and + # adding a new major is a one-line change here instead of an edit in + # several jobs. + changes: + name: 🔍 Detect docs-only 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 to running the full matrix: default docs_only to false + # immediately, before anything below has a chance to compute or + # fail. Writing the same GITHUB_OUTPUT key twice is fine (the last + # write wins), so the only way this step ends with docs_only=true + # is by genuinely proving it further down - never by skipping past + # an edge case with a default. + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + 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" + + # A missing HEAD, 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. docs_only is already false from above; + # just stop here 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__) + + DOCS_ONLY=true + if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then + DOCS_ONLY=false + else + while IFS= read -r f; do + if ! [[ "$f" =~ \.(md|asc)$ ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED" + fi + + echo "changed files:" + echo "$CHANGED" + echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" + + - name: Derive the supported-PostgreSQL-major list + id: pg + run: | + # A dozen-odd lines to replace what looks like a handful of version + # references, but it buys CONSISTENCY: both the fresh-install + # `test` matrix and the `extension-update-test` matrix derive their + # PostgreSQL set from this ONE source, so they cannot silently + # drift onto different lists. Adding a new major is a one-line + # NEWEST bump here, not an edit in N places. + # + # Only one floor is needed here: 0.9.6 (the oldest version + # count_nulls still ships a full install script for) is pure SQL + # over anyarray/json/jsonb with no catalog-version sensitivity, so + # it installs on every PostgreSQL major count_nulls supports - + # there's no separate legacy-only floor to carve out. + NEWEST=18 + FLOOR=10 + + supported=$(seq "$NEWEST" -1 "$FLOOR") + + # Emit a JSON array from a list of ints, for the job matrices to + # consume with fromJSON (GitHub evaluates a literal dollar-brace + # expression even inside a run block, so none is written here). + json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; } + + echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT" + + # Baseline: fresh CREATE EXTENSION, across the PG matrix AND a schema + # matrix (TEST_SCHEMA, picked up from the environment by test/deps.sql via + # the count_nulls.test_schema GUC - see the Makefile). Empty ('') runs + # WITHOUT specifying a schema at all - CREATE EXTENSION with no targeting, + # landing wherever the session's own default search_path resolves. + # 'Quoted' runs WITH one explicitly specified, using a name that requires + # SQL identifier quoting (mixed case - unquoted would fold to lowercase), + # exercising the suite's %I schema-qualification rather than just its + # literal test data. Both legs matter; neither is redundant with the + # other (see the Makefile's TEST_SCHEMA comment). test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' strategy: matrix: - pg: [17, 16, 15, 14, 13, 12, 11, 10] - name: 🐘 PostgreSQL ${{ matrix.pg }} + # From the single source in the changes job. + pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} + schema: ["", Quoted] + name: 🐘 PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) runs-on: ubuntu-latest container: pgxn/pgxn-tools + env: + TEST_SCHEMA: ${{ matrix.schema }} steps: - name: Start PostgreSQL ${{ matrix.pg }} run: pg-start ${{ matrix.pg }} @@ -15,3 +178,181 @@ jobs: uses: actions/checkout@v4 - name: Test on PostgreSQL ${{ matrix.pg }} run: pg-build-test + + # Proves the in-place extension update path: CREATE EXTENSION at the + # oldest version we still ship a full install script for (0.9.6), then + # ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL), then run the + # FULL suite against the updated database (same expected output as a + # fresh install of the same schema, so an updated DB must behave + # identically). Complements pg-upgrade-test, which covers the + # cross-major-version binary upgrade instead. + extension-update-test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + # From the single source in the changes job. + pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} + schema: ["", Quoted] + name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) + 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: Install count_nulls + run: make install + - name: Update 0.9.6 -> current and run the suite (existing mode) + # update-scenario creates a real database at 0.9.6, plants + proves + # the dependency guard, ALTER EXTENSION UPDATEs to the current + # version, and runs the suite against that updated database in + # existing mode (asserting the version and that the guard still + # blocked a drop, before dropping it itself - see bin/test_existing). + run: bin/test_existing update-scenario count_nulls_update "${{ matrix.schema }}" 0.9.6 + + # 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. + # The two big-jump legs below already cover the axis that matters here. + # Revisit if count_nulls ever grows something catalog-touching. + pg-upgrade-test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + include: + - old_pg: "10" + new_pg: "18" + schema: "" + - old_pg: "10" + new_pg: "18" + schema: Quoted + - old_pg: "12" + new_pg: "18" + schema: "" + - old_pg: "12" + new_pg: "18" + schema: Quoted + name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) + 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 in the leg's schema, + # 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 "${{ matrix.schema }}" 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->1.0.0 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 "${{ matrix.schema }}" + + # A single stable check name for use as a required status check in branch + # protection rules. Matrix jobs produce check names like + # "🐘 PostgreSQL 14 (schema none)" 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. the heavy jobs gated off by the + # `changes` job on a docs-only push), and fails if any failed or were + # cancelled. + all-checks-passed: + needs: [changes, test, extension-update-test, pg-upgrade-test] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify all jobs are listed in needs + # Ensures this job won't silently ignore a newly-added job that was + # omitted from the needs list above. + run: | + DEFINED=$(python3 -c " + import yaml + with open('.github/workflows/ci.yml') as f: + w = yaml.safe_load(f) + print('\n'.join(sorted(j for j in w['jobs'] if j != 'all-checks-passed'))) + ") + NEEDED=$(echo '${{ toJson(needs) }}' | python3 -c " + import json, sys + print('\n'.join(sorted(json.load(sys.stdin)))) + ") + if [ "$DEFINED" != "$NEEDED" ]; then + echo "Some jobs are missing from all-checks-passed needs:" + diff <(echo "$DEFINED") <(echo "$NEEDED") + exit 1 + fi + - 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 +# vi: expandtab ts=2 sw=2 diff --git a/Makefile b/Makefile index e57011a..c8f6467 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,77 @@ include pgxntool/base.mk # Temporary hack testdeps: $(wildcard test/*/*.sql) $(wildcard test/*.sql) # Be careful not to include directories in this + +# Install the oldest historical version's full install script so the update +# test in test/build/upgrade.sql and test/deps.sql's update mode can +# CREATE EXTENSION count_nulls VERSION '0.9.6'. +# Not covered by base.mk's DATA wildcard, which only picks up upgrade scripts +# (sql/*--*--*.sql) and the current version file - a pgxntool bug, filed as +# Postgres-Extensions/pgxntool#48. +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= EXTRA_REGRESS_OPTS=--use-existing +# PGXNTOOL_ENABLE_TEST_BUILD=no against a real database, not via a make +# wrapper here (see 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 diff --git a/bin/test_existing b/bin/test_existing new file mode 100755 index 0000000..8955336 --- /dev/null +++ b/bin/test_existing @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# +# Exercise the count_nulls test suite against a REAL database whose extension +# was installed/updated/pg_upgraded OUTSIDE the suite ("existing" mode). Both +# the pg-upgrade-test and extension-update-test jobs in +# .github/workflows/ci.yml repeat the same sequence: +# +# install -> plant dependency guard -> update/upgrade -> assert version +# -> drop the guard -> 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. 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] +# +# plant-guard DB SCHEMA +# Plant + prove the dependency guard (extension must already exist). +# +# update DB [TO_VERSION] +# ALTER EXTENSION count_nulls UPDATE [TO 'TO_VERSION'] (empty => current). +# +# 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 guard. +# +# 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). +# +# update-scenario DB SCHEMA FROM_VERSION +# Create DB + extension at FROM_VERSION in SCHEMA, plant the guard, +# update to current, then run-suite against that real updated database. +# +# Run `bin/test_existing` with no subcommand to print usage. +# +# Why the dependency guard: "existing" mode must run the suite against the +# ACTUAL upgraded/updated 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 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 +# --------------------------------------------------------------------------- + +# Capture the single-value (-tAc) output of a query against a database. +psql_value() { + local db=$1 sql=$2 + psql -d "$db" -tAc "$sql" +} + +# Run SQL that must succeed, aborting the whole script on any error. +psql_do() { + local db=$1 + shift + psql -d "$db" -v ON_ERROR_STOP=1 "$@" +} + +# --------------------------------------------------------------------------- +# Version / guard helpers +# --------------------------------------------------------------------------- + +current_version() { + make -s print-PGXNVERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p' +} + +installed_version() { + psql_value "$1" \ + "SELECT extversion FROM pg_extension WHERE extname = 'count_nulls'" +} + +guard_present() { + test "$(psql_value "$1" \ + "SELECT count(*) FROM pg_views WHERE schemaname = 'count_nulls_drop_guard' AND viewname = 'guard'")" = 1 +} + +# 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/deps.sql's :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" +} + +# update-scenario DB SCHEMA FROM_VERSION +# Full extension-update flow that runs the existing-mode suite: create the +# DB and extension at FROM_VERSION in SCHEMA, plant + prove the guard, +# update to the current version, then run the suite against that real +# updated database. +update_scenario() { + local db=$1 schema=$2 from=$3 + createdb "$db" + create_extension_in_schema "$db" "$schema" "$from" + plant_guard "$db" "$schema" + update_ext "$db" + run_suite "$db" "$schema" +} + +# Run the pgTAP suite against an already-populated database in existing mode. +# Verifies the extension 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. Two consequences drive the make args: + # 1. PGXNTOOL_ENABLE_TEST_BUILD=no: base.mk auto-enables the test-build + # sanity check (test/build/upgrade.sql), adding it as a `test` + # prerequisite. test-build spawns a recursive `installcheck` that + # INHERITS this call's --use-existing (a command-line var propagates + # to sub-makes) but targets a fresh `regression` DB it cannot create + # under --use-existing, so it dies. test-build is a fresh-install + # smoke check already run by the fresh `test` job on every PG version + # - it adds nothing here. + # 2. `make verify-results` (not just `make test`) is the real pass/fail + # gate: base.mk's `.IGNORE: installcheck` means `make test` prints + # regression.diffs but exits 0 regardless. verify-results depends on + # `test`, so it must carry the SAME existing-mode overrides or it + # would re-run FRESH (against a new throwaway DB) instead of + # verifying THIS existing database. + local existing_args="TEST_LOAD_SOURCE=existing TEST_SCHEMA=$schema CONTRIB_TESTDB=$db EXTRA_REGRESS_OPTS=--use-existing PGXNTOOL_ENABLE_TEST_BUILD=no" + make test $existing_args + make verify-results $existing_args +} + +usage() { + echo "usage: bin/test_existing [args]" >&2 + echo " plant-guard DB SCHEMA" >&2 + echo " update DB [TO_VERSION]" >&2 + echo " prepare-old DB SCHEMA INSTALL_VERSION" >&2 + echo " run-suite DB SCHEMA" >&2 + echo " update-scenario DB SCHEMA FROM_VERSION" >&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 "$@" ;; + update) update_ext "$@" ;; + prepare-old) prepare_old "$@" ;; + run-suite) run_suite "$@" ;; + update-scenario) update_scenario "$@" ;; + *) 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..5b3dd92 --- /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. the extension 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; diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..2752857 --- /dev/null +++ b/test/README.md @@ -0,0 +1,93 @@ +# count_nulls test suite + +This suite is structured differently from most pgTAP-based extension tests: +rather than each `test/sql/*.sql` file writing its own independent +assertions, `core/functions.sql` defines a shared library of `test__*` +functions (pgTAP's `runtests()` naming convention) that test files `\i` and +then invoke via `runtests()`. See the layout below for how the pieces fit +together, and the TODO note at the top of `core/functions.sql` for the +tradeoffs of that choice. + +## Layout + +- `deps.sql` — loaded by every test file (via `load.sql` -> + `pgxntool/setup.sql` -> `deps.sql`). Installs the extension according to + two independent GUC-driven switches (below), or asserts it's already + installed. +- `core/functions.sql` — a shared helper, `\i`'d by `sql/extension_tests.sql`. + Defines `ncs()` (discovers, live, which schema the extension is actually + installed in - never trusts a hardcoded/passed-in value) plus a battery of + `test__*` functions (pgTAP's `runtests()` naming convention) covering + function definitions, immutability/strictness, and behavior across + `anyarray`/`json`/`jsonb` and both trigger functions. +- `sql/extension_tests.sql` — `\i`'s `core/functions.sql`, adds two more + `test__*` functions of its own (`test__check_ncs`, asserting the extension + landed where expected; `test__shutdown__drop_all`, asserting it can be + cleanly dropped), then runs everything via `runtests()`. +- `build/upgrade.sql` — pgxntool's `test-build` feature: a fast, cheap smoke + check (install 0.9.6, `ALTER EXTENSION UPDATE`, rolled back) that runs + automatically before the main suite on every `make test`. Catches a broken + update script immediately, before the heavier CI jobs even start. + +## The two independent mode switches + +Both are make vars, propagated the same way: `make var` -> `PGOPTIONS -c +=` -> `current_setting()` in `deps.sql` (`pg_regress` doesn't +forward make vars, but the `psql` it spawns inherits the environment). + +**`TEST_LOAD_SOURCE`** (`count_nulls.test_load_mode`) — *how* the extension +got there: +- `fresh` (default): `CREATE EXTENSION count_nulls` at current. +- `update`: installs `0.9.6`, then `ALTER EXTENSION UPDATE`. `make + test-update` wraps this. +- `existing`: extension already installed (a real `pg_upgrade`, or an + out-of-band update via `bin/test_existing`) - asserts present/current, + touches nothing. + +**`TEST_SCHEMA`** (`count_nulls.test_schema`) — *where* it lands: +- empty (default): no `CREATE SCHEMA`/`SET search_path` at all - lands + wherever the session's own ambient search_path resolves. +- non-empty: explicitly targets that schema. `TEST_SCHEMA=Quoted` locally + exercises a name requiring SQL identifier quoting. + +These are independent axes; the CI matrix (`.github/workflows/ci.yml`, see +its top-of-file summary comment) crosses both with the PostgreSQL version. + +## Why multiple expected-output files per test + +`extension_tests.sql`'s assertions build SQL text through `%I`-qualified +`format()` calls using `ncs()`'s result (`core/functions.sql`) - e.g. a +`CREATE TRIGGER ... EXECUTE PROCEDURE tap.null_count_trigger(...)` line's +exact text depends on which schema `ncs()` found. `pg_regress` requires an +EXACT text match against `expected/.out`, so a run that lands the +extension somewhere else produces a *correct but textually different* +result - not a failure to paper over, a second valid baseline to capture. +`pg_regress` natively supports this: it tries `expected/.out`, then +`expected/_1.out`, `_2.out`, ... in turn, and passes if ANY one +matches (see `PGXNTOOL_ENABLE_TEST_BUILD`/pg_regress docs for the general +mechanism - this isn't pgxntool-specific). + +Concretely, three real scenarios currently produce different (each +individually correct) text for `extension_tests`, so there are three files: + +| File | Scenario | Where the extension lands, and why | +|---|---|---| +| `extension_tests.out` (default) | `TEST_SCHEMA` empty, run in-suite (`make test`) | `tap` - this suite's own pgTAP setup (`pgxntool/test/pgxntool/tap_setup.sql`) already puts its own schema first on `search_path` *before* `deps.sql` runs, so an untargeted `CREATE EXTENSION` lands there. | +| `extension_tests_1.out` | `TEST_SCHEMA=Quoted` (any entry point) | `Quoted` - explicitly targeted, so it's wherever it was told to go, regardless of entry point. | +| `extension_tests_2.out` | `TEST_SCHEMA` empty, run via `bin/test_existing` (the `extension-update-test`/`pg-upgrade-test` CI jobs) | `public` - `bin/test_existing` connects with a bare, standalone `psql` session that never runs `tap_setup.sql`, so an untargeted `CREATE EXTENSION` lands in Postgres's own real default instead. | + +If you add a new scenario that changes this text again (a new TEST_SCHEMA +value, a new entry point), capture its actual `test/results/.out` (a +real run, zero raw `^not ok` lines) as the next `_N.out`, don't hand-author +one. + +## Regenerating expected output + +Never hand-edit files under `expected/`. Regenerate via `make results` +(guarded by `make verify-results`, which refuses to copy while +`regression.diffs` shows real failures - use `PGXNTOOL_ENABLE_VERIFY_RESULTS=no` +to bypass that guard for a run you've already reviewed and know is a +legitimate, intentional change, not a way to skip reviewing the diff). +`make results` only ever writes the unsuffixed default; alternates +(`_1.out`, `_2.out`, ...) have to be copied by hand from a real +`test/results/.out` for that scenario. diff --git a/test/build/expected/upgrade.out b/test/build/expected/upgrade.out new file mode 100644 index 0000000..25fdbb1 --- /dev/null +++ b/test/build/expected/upgrade.out @@ -0,0 +1 @@ +\set ECHO none diff --git a/test/build/upgrade.sql b/test/build/upgrade.sql new file mode 100644 index 0000000..35d2c29 --- /dev/null +++ b/test/build/upgrade.sql @@ -0,0 +1,25 @@ +\set ECHO none +\i test/pgxntool/psql.sql +\t + +/* + * Fast, cheap smoke check: install the oldest version we still ship a full + * script for and ALTER EXTENSION UPDATE to current, rolled back so it has + * no side effects. sql/count_nulls--0.9.6.sql is that oldest full install + * script (0.9.0/0.9.2/0.9.5 only exist as upgrade diffs, not standalone + * installs). + * + * pgxntool's test-build feature runs this automatically as part of every + * plain `make test` - i.e. on every PG major in the fresh-install CI + * matrix, before the pgTAP suite even starts. It only proves the update + * SQL runs without erroring, nothing about behavior, so it's not a + * substitute for test/deps.sql's 'update' load mode (which reruns the + * whole suite against the updated database - see the extension-update-test + * CI job) or the real binary-pg_upgrade coverage (pg-upgrade-test). Its + * value is failing fast and cheaply on a broken update script, across + * every supported PostgreSQL major, before those heavier jobs run at all. + */ +BEGIN; +CREATE EXTENSION count_nulls VERSION '0.9.6'; +ALTER EXTENSION count_nulls UPDATE; +ROLLBACK; diff --git a/test/core/functions.sql b/test/core/functions.sql index 482526c..5c13006 100644 --- a/test/core/functions.sql +++ b/test/core/functions.sql @@ -1,3 +1,29 @@ +/* + * TODO, next time this file is substantially touched - reconsider two + * things, not urgent now but worth revisiting rather than forgetting: + * + * 1. test/install (pgxntool's committed-once installer, see + * Postgres-Extensions/cat_tools's test/install/load.sql): cat_tools uses + * it to centralize its fresh/update/existing load-mode switching in ONE + * place, run once, rather than count_nulls's current test/deps.sql + * (loaded per test file, reinstalling every time). That's a real, + * independent argument for test/install regardless of the schema + * question - count_nulls just hasn't needed to revisit it. + * + * 2. The debuggability cost of this file's loop-driven, ncs()-indirected + * design isn't really about automated TAP output (which mostly localizes + * failures fine) - it's that a developer can trivially `psql`, `\i` a + * traditional, self-contained test file, and interactively poke at the + * resulting state; doing the same with this file's generated names and + * shared ncs() lookup is much less direct. That's the actual reason + * count_nulls originally had separate, deliberately-independent smoke + * test files alongside this shared helper (sanity.sql was the last one; + * removed once its behavioral coverage turned out to be a strict subset + * of test__functionality's, but the debuggability angle it existed for + * is real and would be worth an intentional replacement, not just an + * absence, if this file's indirection ever becomes a genuine problem). + */ + CREATE SCHEMA _null_count_test; -- See bottom as well! @@ -219,6 +245,12 @@ BEGIN END $body$; -SET SEARCH_PATH = _null_count_test, tap, :schema; +/* + * Deliberately does NOT restore :"schema" (the schema count_nulls is + * installed into) onto search_path here - whether the caller wants it there + * (testing unqualified/on-search-path use) or not (testing fully-qualified/ + * off-search-path use) differs per test file, so each caller sets its own + * search_path explicitly after \i'ing this file. + */ -- vi: expandtab sw=2 ts=2 diff --git a/test/deps.sql b/test/deps.sql index 9705f9f..b86701a 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -1,8 +1,115 @@ -- Add any test dependency statements here --- IF NOT EXISTS will emit NOTICEs, which is annoying + +/* + * Mode selection: 'fresh' installs the current version directly; 'update' + * installs the oldest version we still ship a full script for (0.9.6) and + * runs ALTER EXTENSION UPDATE; 'existing' asserts the extension is already + * installed (a real `pg_upgrade` leg, or an out-of-band update) and touches + * nothing. Since every test file loads this via test/load.sql, running the + * suite in each mode (make test / make test-update / make test + * TEST_LOAD_SOURCE=existing) exercises the SAME tests against a fresh vs. + * updated vs. really-upgraded install, with the same expected output either + * way. + * + * "update" here is extension-level (ALTER EXTENSION UPDATE); it is not + * "upgrade" (cluster-level pg_upgrade) - 'existing' is how that axis is + * exercised, via a real pg_upgrade run outside this test suite (see the + * pg_upgrade CI job). + * + * The Makefile always exports this GUC, so we read it without missing_ok: + * if it failed to propagate we want a loud error here, not a silent + * fall-through to 'fresh'. + */ +SELECT current_setting('count_nulls.test_load_mode') AS count_nulls_test_load_mode +\gset + +DO $$ +BEGIN + IF current_setting('count_nulls.test_load_mode') NOT IN ('fresh', 'update', 'existing') THEN + RAISE EXCEPTION + 'count_nulls.test_load_mode must be ''fresh'', ''update'' or ''existing'', got ''%''' + , current_setting('count_nulls.test_load_mode') + ; + END IF; +END +$$; + +SELECT :'count_nulls_test_load_mode' = 'update' AS count_nulls_update_mode +\gset +SELECT :'count_nulls_test_load_mode' = 'existing' AS count_nulls_existing_mode +\gset + +/* + * TEST_SCHEMA (the count_nulls.test_schema GUC, set via the Makefile): + * which schema the extension is installed into, for the WHOLE test run - + * the SAME schema for every test file, not a different literal hardcoded + * per file. Empty (the default) means "don't target any schema at all" - + * the extension lands wherever the session's own default search_path + * already resolves. Non-empty explicitly creates and targets that schema, + * including a schema whose name requires SQL identifier quoting (mixed + * case - unquoted would fold to lowercase), to exercise test files' %I + * schema-qualification rather than just their literal test data. Both the + * empty and non-empty cases are exercised in CI - genuinely different code + * paths, not one a redundant special case of the other. + * + * Read with :"schema" (quoted-identifier form) wherever used as a bare + * identifier below and in the test files, since the quoting-required case + * is exactly what the non-empty case exists to exercise. + * + * Read without missing_ok, same reasoning as count_nulls.test_load_mode + * above: a genuinely unpropagated GUC must fail loudly, not be + * indistinguishable from a deliberately empty one. + */ +SELECT current_setting('count_nulls.test_schema') AS schema +\gset +SELECT :'schema' <> '' AS count_nulls_has_schema +\gset + +\if :count_nulls_has_schema +/* + * :"schema" is created/searched in every mode that has one, including + * 'existing' (even though 'existing' mode never installs the extension + * into it): test files (e.g. test__shutdown__drop_all's + * `DROP SCHEMA :"schema"`) expect it to exist regardless of mode, and an + * empty schema is harmless. IF NOT EXISTS emits NOTICEs, which is annoying. + */ SET client_min_messages = WARNING; -CREATE SCHEMA IF NOT EXISTS :schema; -SET search_path = :schema; +CREATE SCHEMA IF NOT EXISTS :"schema"; +SET search_path = :"schema"; SET client_min_messages = NOTICE; +\endif +\if :count_nulls_existing_mode +/* + * Extension is already installed (real pg_upgrade, or an out-of-band + * update) - assert it's present and current, but do NOT drop/create/update + * it. The CI job driving this mode is responsible for having installed the + * real extension matching this run's TEST_SCHEMA (or the default location, + * if empty) before getting here, so the rest of the suite sees the same + * schema regardless of mode. + */ +DO $$ +DECLARE + v_installed text := (SELECT extversion FROM pg_extension WHERE extname = 'count_nulls'); + v_default text := (SELECT default_version FROM pg_available_extensions WHERE name = 'count_nulls'); +BEGIN + IF v_installed IS NULL THEN + RAISE EXCEPTION 'count_nulls.test_load_mode=existing but count_nulls is not installed'; + END IF; + IF v_installed IS DISTINCT FROM v_default THEN + RAISE EXCEPTION 'count_nulls installed at % but default_version is %', v_installed, v_default; + END IF; +END +$$; +\elif :count_nulls_update_mode +CREATE EXTENSION count_nulls VERSION '0.9.6'; +/* + * Suppress the "already installed, no update" NOTICE class of messages any + * update script might emit. + */ +SET client_min_messages = WARNING; +ALTER EXTENSION count_nulls UPDATE; +SET client_min_messages = NOTICE; +\else CREATE EXTENSION count_nulls; +\endif diff --git a/test/expected/extension_tests.out b/test/expected/extension_tests.out index 3a22c24..6860ae6 100644 --- a/test/expected/extension_tests.out +++ b/test/expected/extension_tests.out @@ -1,87 +1,81 @@ \set ECHO none # Subtest: _null_count_test.test__check_ncs() ok 1 - ok 2 - schema schema_to_load_count_nulls should not be in search path + ok 2 # SKIP extension landed in a schema functions.sql always keeps on search_path - not a meaningful check here 1..2 ok 1 - _null_count_test.test__check_ncs # Subtest: _null_count_test.test__definition() - ok 1 - ensure null_count({anyarray}) is not in search_path - ok 2 - Function schema_to_load_count_nulls.null_count(anyarray) should return integer - ok 3 - Function schema_to_load_count_nulls.null_count(anyarray) should not be strict - ok 4 - Function schema_to_load_count_nulls.null_count(anyarray) should be IMMUTABLE - ok 5 - ensure null_count({json}) is not in search_path - ok 6 - Function schema_to_load_count_nulls.null_count(json) should return integer - ok 7 - Function schema_to_load_count_nulls.null_count(json) should not be strict - ok 8 - Function schema_to_load_count_nulls.null_count(json) should be IMMUTABLE - ok 9 - ensure null_count({jsonb}) is not in search_path - ok 10 - Function schema_to_load_count_nulls.null_count(jsonb) should return integer - ok 11 - Function schema_to_load_count_nulls.null_count(jsonb) should not be strict - ok 12 - Function schema_to_load_count_nulls.null_count(jsonb) should be IMMUTABLE - ok 13 - ensure not_null_count({anyarray}) is not in search_path - ok 14 - Function schema_to_load_count_nulls.not_null_count(anyarray) should return integer - ok 15 - Function schema_to_load_count_nulls.not_null_count(anyarray) should not be strict - ok 16 - Function schema_to_load_count_nulls.not_null_count(anyarray) should be IMMUTABLE - ok 17 - ensure not_null_count({json}) is not in search_path - ok 18 - Function schema_to_load_count_nulls.not_null_count(json) should return integer - ok 19 - Function schema_to_load_count_nulls.not_null_count(json) should not be strict - ok 20 - Function schema_to_load_count_nulls.not_null_count(json) should be IMMUTABLE - ok 21 - ensure not_null_count({jsonb}) is not in search_path - ok 22 - Function schema_to_load_count_nulls.not_null_count(jsonb) should return integer - ok 23 - Function schema_to_load_count_nulls.not_null_count(jsonb) should not be strict - ok 24 - Function schema_to_load_count_nulls.not_null_count(jsonb) should be IMMUTABLE - ok 25 - Function schema_to_load_count_nulls.null_count_trigger() should return trigger - ok 26 - Function schema_to_load_count_nulls.null_count_trigger() should not be strict - ok 27 - Function schema_to_load_count_nulls.null_count_trigger() should be IMMUTABLE - ok 28 - Function schema_to_load_count_nulls.not_null_count_trigger() should return trigger - ok 29 - Function schema_to_load_count_nulls.not_null_count_trigger() should not be strict - ok 30 - Function schema_to_load_count_nulls.not_null_count_trigger() should be IMMUTABLE - 1..30 + ok 1 - Function tap.null_count(anyarray) should return integer + ok 2 - Function tap.null_count(anyarray) should not be strict + ok 3 - Function tap.null_count(anyarray) should be IMMUTABLE + ok 4 - Function tap.null_count(json) should return integer + ok 5 - Function tap.null_count(json) should not be strict + ok 6 - Function tap.null_count(json) should be IMMUTABLE + ok 7 - Function tap.null_count(jsonb) should return integer + ok 8 - Function tap.null_count(jsonb) should not be strict + ok 9 - Function tap.null_count(jsonb) should be IMMUTABLE + ok 10 - Function tap.not_null_count(anyarray) should return integer + ok 11 - Function tap.not_null_count(anyarray) should not be strict + ok 12 - Function tap.not_null_count(anyarray) should be IMMUTABLE + ok 13 - Function tap.not_null_count(json) should return integer + ok 14 - Function tap.not_null_count(json) should not be strict + ok 15 - Function tap.not_null_count(json) should be IMMUTABLE + ok 16 - Function tap.not_null_count(jsonb) should return integer + ok 17 - Function tap.not_null_count(jsonb) should not be strict + ok 18 - Function tap.not_null_count(jsonb) should be IMMUTABLE + ok 19 - Function tap.null_count_trigger() should return trigger + ok 20 - Function tap.null_count_trigger() should not be strict + ok 21 - Function tap.null_count_trigger() should be IMMUTABLE + ok 22 - Function tap.not_null_count_trigger() should return trigger + ok 23 - Function tap.not_null_count_trigger() should not be strict + ok 24 - Function tap.not_null_count_trigger() should be IMMUTABLE + 1..24 ok 2 - _null_count_test.test__definition # Subtest: _null_count_test.test__functionality() - ok 1 - Test schema_to_load_count_nulls.null_count(a, b, c) - ok 2 - Test schema_to_load_count_nulls.null_count(json) - ok 3 - Test schema_to_load_count_nulls.null_count(jsonb) - ok 4 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger( NULL ) - ok 5 - Test schema_to_load_count_nulls.not_null_count_trigger( NULL ) + ok 1 - Test tap.null_count(a, b, c) + ok 2 - Test tap.null_count(json) + ok 3 - Test tap.null_count(jsonb) + ok 4 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger( NULL ) + ok 5 - Test tap.not_null_count_trigger( NULL ) ok 6 - DROP TRIGGER "test trigger" - ok 7 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger( ) - ok 8 - Test schema_to_load_count_nulls.not_null_count_trigger( ) + ok 7 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger( ) + ok 8 - Test tap.not_null_count_trigger( ) ok 9 - DROP TRIGGER "test trigger" - ok 10 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger( NULL ) - ok 11 - Test schema_to_load_count_nulls.null_count_trigger( NULL ) + ok 10 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger( NULL ) + ok 11 - Test tap.null_count_trigger( NULL ) ok 12 - DROP TRIGGER "test trigger" - ok 13 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger( ) - ok 14 - Test schema_to_load_count_nulls.null_count_trigger( ) + ok 13 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger( ) + ok 14 - Test tap.null_count_trigger( ) ok 15 - DROP TRIGGER "test trigger" - ok 16 - CREATE TRIGGER "null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger(1, 'error_message') + ok 16 - CREATE TRIGGER "null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger(1, 'error_message') ok 17 - Test "null_BEFORE_error_message" ok 18 - DROP TRIGGER "null_BEFORE_error_message" - ok 19 - CREATE TRIGGER "null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger(1, 'error_message') + ok 19 - CREATE TRIGGER "null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger(1, 'error_message') ok 20 - Test "null_AFTER_error_message" ok 21 - DROP TRIGGER "null_AFTER_error_message" - ok 22 - CREATE TRIGGER "not_null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger(1, 'error_message') + ok 22 - CREATE TRIGGER "not_null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger(1, 'error_message') ok 23 - Test "not_null_BEFORE_error_message" ok 24 - DROP TRIGGER "not_null_BEFORE_error_message" - ok 25 - CREATE TRIGGER "not_null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger(1, 'error_message') + ok 25 - CREATE TRIGGER "not_null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger(1, 'error_message') ok 26 - Test "not_null_AFTER_error_message" ok 27 - DROP TRIGGER "not_null_AFTER_error_message" - ok 28 - CREATE TRIGGER "null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger(1, NULL) + ok 28 - CREATE TRIGGER "null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger(1, NULL) ok 29 - Test "null_BEFORE_" ok 30 - DROP TRIGGER "null_BEFORE_" - ok 31 - CREATE TRIGGER "null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.null_count_trigger(1, NULL) + ok 31 - CREATE TRIGGER "null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.null_count_trigger(1, NULL) ok 32 - Test "null_AFTER_" ok 33 - DROP TRIGGER "null_AFTER_" - ok 34 - CREATE TRIGGER "not_null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger(1, NULL) + ok 34 - CREATE TRIGGER "not_null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger(1, NULL) ok 35 - Test "not_null_BEFORE_" ok 36 - DROP TRIGGER "not_null_BEFORE_" - ok 37 - CREATE TRIGGER "not_null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE schema_to_load_count_nulls.not_null_count_trigger(1, NULL) + ok 37 - CREATE TRIGGER "not_null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE tap.not_null_count_trigger(1, NULL) ok 38 - Test "not_null_AFTER_" ok 39 - DROP TRIGGER "not_null_AFTER_" 1..39 ok 3 - _null_count_test.test__functionality # Subtest: _null_count_test.test__shutdown__drop_all() ok 1 - ok 2 + ok 2 # SKIP TEST_SCHEMA is empty - no dedicated schema to drop 1..2 ok 4 - _null_count_test.test__shutdown__drop_all 1..4 diff --git a/test/expected/extension_tests_1.out b/test/expected/extension_tests_1.out new file mode 100644 index 0000000..7466a35 --- /dev/null +++ b/test/expected/extension_tests_1.out @@ -0,0 +1,87 @@ +\set ECHO none +# Subtest: _null_count_test.test__check_ncs() + ok 1 + ok 2 - schema "Quoted" should not be in search path + 1..2 +ok 1 - _null_count_test.test__check_ncs +# Subtest: _null_count_test.test__definition() + ok 1 - ensure null_count({anyarray}) is not in search_path + ok 2 - Function "Quoted".null_count(anyarray) should return integer + ok 3 - Function "Quoted".null_count(anyarray) should not be strict + ok 4 - Function "Quoted".null_count(anyarray) should be IMMUTABLE + ok 5 - ensure null_count({json}) is not in search_path + ok 6 - Function "Quoted".null_count(json) should return integer + ok 7 - Function "Quoted".null_count(json) should not be strict + ok 8 - Function "Quoted".null_count(json) should be IMMUTABLE + ok 9 - ensure null_count({jsonb}) is not in search_path + ok 10 - Function "Quoted".null_count(jsonb) should return integer + ok 11 - Function "Quoted".null_count(jsonb) should not be strict + ok 12 - Function "Quoted".null_count(jsonb) should be IMMUTABLE + ok 13 - ensure not_null_count({anyarray}) is not in search_path + ok 14 - Function "Quoted".not_null_count(anyarray) should return integer + ok 15 - Function "Quoted".not_null_count(anyarray) should not be strict + ok 16 - Function "Quoted".not_null_count(anyarray) should be IMMUTABLE + ok 17 - ensure not_null_count({json}) is not in search_path + ok 18 - Function "Quoted".not_null_count(json) should return integer + ok 19 - Function "Quoted".not_null_count(json) should not be strict + ok 20 - Function "Quoted".not_null_count(json) should be IMMUTABLE + ok 21 - ensure not_null_count({jsonb}) is not in search_path + ok 22 - Function "Quoted".not_null_count(jsonb) should return integer + ok 23 - Function "Quoted".not_null_count(jsonb) should not be strict + ok 24 - Function "Quoted".not_null_count(jsonb) should be IMMUTABLE + ok 25 - Function "Quoted".null_count_trigger() should return trigger + ok 26 - Function "Quoted".null_count_trigger() should not be strict + ok 27 - Function "Quoted".null_count_trigger() should be IMMUTABLE + ok 28 - Function "Quoted".not_null_count_trigger() should return trigger + ok 29 - Function "Quoted".not_null_count_trigger() should not be strict + ok 30 - Function "Quoted".not_null_count_trigger() should be IMMUTABLE + 1..30 +ok 2 - _null_count_test.test__definition +# Subtest: _null_count_test.test__functionality() + ok 1 - Test "Quoted".null_count(a, b, c) + ok 2 - Test "Quoted".null_count(json) + ok 3 - Test "Quoted".null_count(jsonb) + ok 4 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger( NULL ) + ok 5 - Test "Quoted".not_null_count_trigger( NULL ) + ok 6 - DROP TRIGGER "test trigger" + ok 7 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger( ) + ok 8 - Test "Quoted".not_null_count_trigger( ) + ok 9 - DROP TRIGGER "test trigger" + ok 10 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger( NULL ) + ok 11 - Test "Quoted".null_count_trigger( NULL ) + ok 12 - DROP TRIGGER "test trigger" + ok 13 - CREATE TRIGGER "test trigger" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger( ) + ok 14 - Test "Quoted".null_count_trigger( ) + ok 15 - DROP TRIGGER "test trigger" + ok 16 - CREATE TRIGGER "null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger(1, 'error_message') + ok 17 - Test "null_BEFORE_error_message" + ok 18 - DROP TRIGGER "null_BEFORE_error_message" + ok 19 - CREATE TRIGGER "null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger(1, 'error_message') + ok 20 - Test "null_AFTER_error_message" + ok 21 - DROP TRIGGER "null_AFTER_error_message" + ok 22 - CREATE TRIGGER "not_null_BEFORE_error_message" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger(1, 'error_message') + ok 23 - Test "not_null_BEFORE_error_message" + ok 24 - DROP TRIGGER "not_null_BEFORE_error_message" + ok 25 - CREATE TRIGGER "not_null_AFTER_error_message" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger(1, 'error_message') + ok 26 - Test "not_null_AFTER_error_message" + ok 27 - DROP TRIGGER "not_null_AFTER_error_message" + ok 28 - CREATE TRIGGER "null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger(1, NULL) + ok 29 - Test "null_BEFORE_" + ok 30 - DROP TRIGGER "null_BEFORE_" + ok 31 - CREATE TRIGGER "null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".null_count_trigger(1, NULL) + ok 32 - Test "null_AFTER_" + ok 33 - DROP TRIGGER "null_AFTER_" + ok 34 - CREATE TRIGGER "not_null_BEFORE_" BEFORE INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger(1, NULL) + ok 35 - Test "not_null_BEFORE_" + ok 36 - DROP TRIGGER "not_null_BEFORE_" + ok 37 - CREATE TRIGGER "not_null_AFTER_" AFTER INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE "Quoted".not_null_count_trigger(1, NULL) + ok 38 - Test "not_null_AFTER_" + ok 39 - DROP TRIGGER "not_null_AFTER_" + 1..39 +ok 3 - _null_count_test.test__functionality +# Subtest: _null_count_test.test__shutdown__drop_all() + ok 1 + ok 2 + 1..2 +ok 4 - _null_count_test.test__shutdown__drop_all +1..4 diff --git a/test/expected/simple.out b/test/expected/extension_tests_2.out similarity index 57% rename from test/expected/simple.out rename to test/expected/extension_tests_2.out index ea8bad2..744cda8 100644 --- a/test/expected/simple.out +++ b/test/expected/extension_tests_2.out @@ -1,31 +1,42 @@ \set ECHO none +# Subtest: _null_count_test.test__check_ncs() + ok 1 + ok 2 - schema public should not be in search path + 1..2 +ok 1 - _null_count_test.test__check_ncs # Subtest: _null_count_test.test__definition() - ok 1 - Function public.null_count(anyarray) should return integer - ok 2 - Function public.null_count(anyarray) should not be strict - ok 3 - Function public.null_count(anyarray) should be IMMUTABLE - ok 4 - Function public.null_count(json) should return integer - ok 5 - Function public.null_count(json) should not be strict - ok 6 - Function public.null_count(json) should be IMMUTABLE - ok 7 - Function public.null_count(jsonb) should return integer - ok 8 - Function public.null_count(jsonb) should not be strict - ok 9 - Function public.null_count(jsonb) should be IMMUTABLE - ok 10 - Function public.not_null_count(anyarray) should return integer - ok 11 - Function public.not_null_count(anyarray) should not be strict - ok 12 - Function public.not_null_count(anyarray) should be IMMUTABLE - ok 13 - Function public.not_null_count(json) should return integer - ok 14 - Function public.not_null_count(json) should not be strict - ok 15 - Function public.not_null_count(json) should be IMMUTABLE - ok 16 - Function public.not_null_count(jsonb) should return integer - ok 17 - Function public.not_null_count(jsonb) should not be strict - ok 18 - Function public.not_null_count(jsonb) should be IMMUTABLE - ok 19 - Function public.null_count_trigger() should return trigger - ok 20 - Function public.null_count_trigger() should not be strict - ok 21 - Function public.null_count_trigger() should be IMMUTABLE - ok 22 - Function public.not_null_count_trigger() should return trigger - ok 23 - Function public.not_null_count_trigger() should not be strict - ok 24 - Function public.not_null_count_trigger() should be IMMUTABLE - 1..24 -ok 1 - _null_count_test.test__definition + ok 1 - ensure null_count({anyarray}) is not in search_path + ok 2 - Function public.null_count(anyarray) should return integer + ok 3 - Function public.null_count(anyarray) should not be strict + ok 4 - Function public.null_count(anyarray) should be IMMUTABLE + ok 5 - ensure null_count({json}) is not in search_path + ok 6 - Function public.null_count(json) should return integer + ok 7 - Function public.null_count(json) should not be strict + ok 8 - Function public.null_count(json) should be IMMUTABLE + ok 9 - ensure null_count({jsonb}) is not in search_path + ok 10 - Function public.null_count(jsonb) should return integer + ok 11 - Function public.null_count(jsonb) should not be strict + ok 12 - Function public.null_count(jsonb) should be IMMUTABLE + ok 13 - ensure not_null_count({anyarray}) is not in search_path + ok 14 - Function public.not_null_count(anyarray) should return integer + ok 15 - Function public.not_null_count(anyarray) should not be strict + ok 16 - Function public.not_null_count(anyarray) should be IMMUTABLE + ok 17 - ensure not_null_count({json}) is not in search_path + ok 18 - Function public.not_null_count(json) should return integer + ok 19 - Function public.not_null_count(json) should not be strict + ok 20 - Function public.not_null_count(json) should be IMMUTABLE + ok 21 - ensure not_null_count({jsonb}) is not in search_path + ok 22 - Function public.not_null_count(jsonb) should return integer + ok 23 - Function public.not_null_count(jsonb) should not be strict + ok 24 - Function public.not_null_count(jsonb) should be IMMUTABLE + ok 25 - Function public.null_count_trigger() should return trigger + ok 26 - Function public.null_count_trigger() should not be strict + ok 27 - Function public.null_count_trigger() should be IMMUTABLE + ok 28 - Function public.not_null_count_trigger() should return trigger + ok 29 - Function public.not_null_count_trigger() should not be strict + ok 30 - Function public.not_null_count_trigger() should be IMMUTABLE + 1..30 +ok 2 - _null_count_test.test__definition # Subtest: _null_count_test.test__functionality() ok 1 - Test public.null_count(a, b, c) ok 2 - Test public.null_count(json) @@ -67,5 +78,10 @@ ok 1 - _null_count_test.test__definition ok 38 - Test "not_null_AFTER_" ok 39 - DROP TRIGGER "not_null_AFTER_" 1..39 -ok 2 - _null_count_test.test__functionality -1..2 +ok 3 - _null_count_test.test__functionality +# Subtest: _null_count_test.test__shutdown__drop_all() + ok 1 + ok 2 # SKIP TEST_SCHEMA is empty - no dedicated schema to drop + 1..2 +ok 4 - _null_count_test.test__shutdown__drop_all +1..4 diff --git a/test/expected/sanity.out b/test/expected/sanity.out deleted file mode 100644 index ae40eed..0000000 --- a/test/expected/sanity.out +++ /dev/null @@ -1,12 +0,0 @@ -\set ECHO none - null_count ------------- - 1 -(1 row) - - null_count ------------- - 1 -(1 row) - -TRANSACTION INTENTIONALLY LEFT OPEN diff --git a/test/sql/extension_tests.sql b/test/sql/extension_tests.sql index ba88447..23ee783 100644 --- a/test/sql/extension_tests.sql +++ b/test/sql/extension_tests.sql @@ -1,41 +1,92 @@ \set ECHO none -\set schema schema_to_load_count_nulls \i test/load.sql -\set schema public \i test/core/functions.sql ---CREATE OR REPLACE FUNCTION ncs() RETURNS name LANGUAGE sql IMMUTABLE AS $$SELECT 'schema_to_load_count_nulls'::name$$; +/* + * This file leaves search_path as functions.sql set it + * (_null_count_test, tap) - with an explicit TEST_SCHEMA, that keeps the + * extension's schema off search_path, so every check below only passes if + * functions.sql's %I-qualified calls (via ncs()) are actually correct, + * never relying on the extension's own schema being reachable unqualified. + * When TEST_SCHEMA is empty, the extension happens to land in 'tap' itself + * (wherever the ambient search_path - already tap, public at this point, + * via pgTap's own setup - resolves at CREATE EXTENSION time), which IS on + * search_path; test__check_ncs accounts for that below rather than + * asserting something that wouldn't hold. + */ -CREATE FUNCTION _null_count_test.test__check_ncs -() RETURNS SETOF text LANGUAGE plpgsql AS $body$ +/* + * schema_hint DEFAULT NULLIF(:'schema', '')::name, not current_setting() + * inside the function body: psql does not interpolate variables inside + * dollar-quoted bodies, but a parameter DEFAULT is plain top-level SQL, so + * :'schema' (test/deps.sql's psql variable, still set from this session's + * \gset) substitutes there normally. NULLIF turns the empty-TEST_SCHEMA + * case into NULL, and runtests() calls every test__* function with no + * arguments, so it always gets this default. + */ +CREATE FUNCTION _null_count_test.test__check_ncs( + schema_hint name DEFAULT NULLIF(:'schema', '')::name +) RETURNS SETOF text LANGUAGE plpgsql AS $body$ DECLARE - s CONSTANT name = 'schema_to_load_count_nulls'; + /* + * When TEST_SCHEMA is non-empty we know exactly where the extension + * should be, so compare ncs() against that known value - a real + * assertion. When it's empty (schema_hint is NULL), there's no fixed + * expectation (it lands wherever the ambient search_path resolves at + * CREATE EXTENSION time, which depends on what ran before deps.sql - + * not something this test should hardcode), so fall back to ncs() + * itself: a no-op comparison that still exercises the call, without + * asserting a location this file has no business assuming. + */ + s CONSTANT name = COALESCE(schema_hint, ncs()); BEGIN RETURN NEXT is( ncs() , s ); - RETURN NEXT is( - current_schemas(true) @> array[s] - , false - --, format('schema %I should not be in search path (%s)', s, current_schemas(true)) - , format('schema %I should not be in search path', s) --, current_schemas(true)) - ); + /* + * "Not on search path" only means anything when s isn't one of the + * schemas functions.sql itself always puts there (_null_count_test, + * tap) - which happens exactly when TEST_SCHEMA is empty and the + * extension coincidentally lands in 'tap' (pgTap's own schema, always + * on search_path here). That's not a bug to work around, just a + * scenario this particular check can't say anything meaningful about. + */ + IF s IN ('_null_count_test', 'tap') THEN + RETURN NEXT skip('extension landed in a schema functions.sql always keeps on search_path - not a meaningful check here'); + ELSE + RETURN NEXT is( + current_schemas(true) @> array[s] + , false + , format('schema %I should not be in search path', s) + ); + END IF; END $body$; -CREATE FUNCTION _null_count_test.test__shutdown__drop_all -() RETURNS SETOF text LANGUAGE plpgsql AS $body$ +CREATE FUNCTION _null_count_test.test__shutdown__drop_all( + schema_hint name DEFAULT NULLIF(:'schema', '')::name +) RETURNS SETOF text LANGUAGE plpgsql AS $body$ BEGIN RETURN NEXT lives_ok( $$DROP EXTENSION count_nulls$$ ); - RETURN NEXT lives_ok( - $$DROP SCHEMA schema_to_load_count_nulls$$ - ); + /* + * Only try to drop a schema when TEST_SCHEMA actually created one - + * when it's empty (schema_hint is NULL), the extension lives wherever + * it landed on its own (see test/deps.sql), which this file has no + * business dropping. + */ + IF schema_hint IS NOT NULL THEN + RETURN NEXT lives_ok( + format('DROP SCHEMA %I', schema_hint) + ); + ELSE + RETURN NEXT skip('TEST_SCHEMA is empty - no dedicated schema to drop'); + END IF; END $body$; diff --git a/test/sql/sanity.sql b/test/sql/sanity.sql deleted file mode 100644 index 6c9ea2f..0000000 --- a/test/sql/sanity.sql +++ /dev/null @@ -1,11 +0,0 @@ -\set ECHO none -\set VERBOSITY verbose - -BEGIN; -CREATE EXTENSION count_nulls; - --- Remember that JSON only accepts 'null' -SELECT null_count('{"a": null}'::jsonb); -SELECT null_count(1,NULL); - -\echo TRANSACTION INTENTIONALLY LEFT OPEN diff --git a/test/sql/simple.sql b/test/sql/simple.sql deleted file mode 100644 index 9406729..0000000 --- a/test/sql/simple.sql +++ /dev/null @@ -1,11 +0,0 @@ -\set ECHO none - -\set schema public - -\i test/load.sql - -\i test/core/functions.sql - ---SET client_min_messages = debug; - -SELECT * FROM runtests( '_null_count_test'::name );