diff --git a/include/spock.h b/include/spock.h index 2d96941f..8ae26ec1 100644 --- a/include/spock.h +++ b/include/spock.h @@ -24,7 +24,7 @@ #include "spock_fe.h" #include "spock_node.h" -#define SPOCK_VERSION "6.0.0-devel" +#define SPOCK_VERSION "6.0.0" #define SPOCK_VERSION_NUM 60000 #define EXTENSION_NAME "spock" diff --git a/sql/spock--5.0.6--5.0.7.sql b/sql/spock--5.0.6--5.0.7.sql new file mode 100644 index 00000000..cb8ceb4b --- /dev/null +++ b/sql/spock--5.0.6--5.0.7.sql @@ -0,0 +1,163 @@ + +/* spock--5.0.6--5.0.7.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION spock UPDATE TO '5.0.7'" to load this file. \quit + +CREATE FUNCTION spock.pause_apply_workers() +RETURNS void VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'spock_pause_apply_workers'; + +CREATE FUNCTION spock.resume_apply_workers() +RETURNS void VOLATILE LANGUAGE c AS 'MODULE_PATHNAME', 'spock_resume_apply_workers'; + +REVOKE EXECUTE ON FUNCTION spock.pause_apply_workers() FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION spock.resume_apply_workers() FROM PUBLIC; + +DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, oid, pg_lsn, int); +DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, oid, pg_lsn, int, bool); +DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, name, pg_lsn, int); +DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, name, pg_lsn, int, bool); +CREATE PROCEDURE spock.wait_for_sync_event( + OUT result bool, + origin_id oid, + lsn pg_lsn, + timeout int DEFAULT 0, + wait_if_disabled bool DEFAULT false +) AS $$ +DECLARE + target_id oid; + start_time timestamptz := clock_timestamp(); + progress_lsn pg_lsn; + sub_is_enabled bool; + sub_slot name; +BEGIN + IF origin_id IS NULL THEN + RAISE EXCEPTION 'Invalid NULL origin_id'; + END IF; + target_id := node_id FROM spock.node_info(); + + -- Upfront existence check is skipped when wait_if_disabled is true because + -- the subscription may not yet exist (e.g. a newly added node whose + -- subscriptions are still initializing). The loop below handles both the + -- not-found and disabled cases gracefully in that mode. + IF NOT wait_if_disabled THEN + SELECT sub_enabled, sub_slot_name INTO sub_is_enabled, sub_slot + FROM spock.subscription + WHERE sub_origin = origin_id AND sub_target = target_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'No subscription found for replication % => %', + origin_id, target_id; + END IF; + END IF; + + WHILE true LOOP + -- Re-check subscription state each iteration. Also re-fetches + -- sub_slot_name so the loop is self-contained when wait_if_disabled + -- is true and the pre-loop check was skipped. + SELECT sub_enabled, sub_slot_name INTO sub_is_enabled, sub_slot + FROM spock.subscription + WHERE sub_origin = origin_id AND sub_target = target_id; + + IF NOT FOUND THEN + IF NOT wait_if_disabled THEN + RAISE EXCEPTION 'No subscription found for replication % => %', + origin_id, target_id; + END IF; + -- Subscription not yet created; fall through to sleep. + ELSIF NOT sub_is_enabled THEN + IF NOT wait_if_disabled THEN + RAISE EXCEPTION 'Subscription % => % has been disabled', + origin_id, target_id; + END IF; + -- Subscription still initializing; fall through to sleep. + ELSE + -- Subscription is enabled; check LSN progress. + -- Uses PostgreSQL's native origin tracking rather than spock.progress + SELECT remote_lsn INTO progress_lsn + FROM pg_replication_origin_status + WHERE external_id = sub_slot; + + IF progress_lsn IS NOT NULL AND progress_lsn >= lsn THEN + result = true; + RETURN; + END IF; + END IF; + + IF timeout <> 0 AND + EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) >= timeout THEN + result := false; + RETURN; + END IF; + + ROLLBACK; + PERFORM pg_sleep(0.2); + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE PROCEDURE spock.wait_for_sync_event( + OUT result bool, + origin name, + lsn pg_lsn, + timeout int DEFAULT 0, + wait_if_disabled bool DEFAULT false +) AS $$ +DECLARE + origin_id oid; +BEGIN + origin_id := node_id FROM spock.node WHERE node_name = origin; + IF origin_id IS NULL THEN + RAISE EXCEPTION 'Origin node ''%'' not found', origin; + END IF; + CALL spock.wait_for_sync_event(result, origin_id, lsn, timeout, wait_if_disabled); +END; +$$ LANGUAGE plpgsql; + +-- spock.sync_event() gained an optional 'transactional' boolean argument +-- (default false). Drop the old zero-arg signature first so the upgrade +-- doesn't leave behind two overloads with overlapping zero-arg resolution. +DROP FUNCTION IF EXISTS spock.sync_event(); +CREATE FUNCTION spock.sync_event(transactional boolean DEFAULT false) +RETURNS pg_lsn RETURNS NULL ON NULL INPUT +AS 'MODULE_PATHNAME', 'spock_create_sync_event' +LANGUAGE C VOLATILE; + +/* + * Correct the declared type of spock.subscription.sub_skip_schema. + * + * The column was added as text in the 5.0.1--5.0.2 upgrade, but the C code + * has always treated it as text[] on both read and write paths + * (strlist_to_textarray on write, DatumGetArrayTypeP on read). The bytes + * already on disk are therefore a valid ArrayType; only the catalog's type + * label is wrong. ALTER TABLE ... ALTER COLUMN TYPE text[] USING ... is + * not viable here: there is no SQL expression that converts "varlena bytes + * the planner believes are text but are in fact ArrayType internal format" + * back into an ArrayType Datum. Relabel the column in place so SQL-level + * access (SELECT, unnest, etc.) works as users expect, without rewriting + * data. + */ +LOCK TABLE spock.subscription IN ACCESS EXCLUSIVE MODE; + +UPDATE pg_catalog.pg_attribute + SET atttypid = 'text[]'::regtype, + attndims = 1 + WHERE attrelid = 'spock.subscription'::regclass + AND attname = 'sub_skip_schema' + AND atttypid = 'text'::regtype; + +/* + * Drop any pg_statistic rows for the column. Stats sampled when the + * column was labelled text encode varlena bytes with text semantics; + * after the relabel the planner would interpret the same stavalues + * arrays as text[], producing nonsense selectivities (and possibly + * crashing on operators that validate ArrayType structure). ANALYZE + * will repopulate as needed. + */ +DELETE FROM pg_catalog.pg_statistic + WHERE starelid = 'spock.subscription'::regclass + AND staattnum = ( + SELECT attnum + FROM pg_catalog.pg_attribute + WHERE attrelid = 'spock.subscription'::regclass + AND attname = 'sub_skip_schema'); diff --git a/sql/spock--5.0.7--5.0.8.sql b/sql/spock--5.0.7--5.0.8.sql new file mode 100644 index 00000000..85b89849 --- /dev/null +++ b/sql/spock--5.0.7--5.0.8.sql @@ -0,0 +1,6 @@ +/* spock--5.0.7--5.0.8.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION spock UPDATE TO '5.0.8'" to load this file. \quit + +-- No schema changes in 5.0.8. diff --git a/sql/spock--5.0.6--6.0.0-devel.sql b/sql/spock--5.0.8--6.0.0.sql similarity index 71% rename from sql/spock--5.0.6--6.0.0-devel.sql rename to sql/spock--5.0.8--6.0.0.sql index f0078a09..59e7e3c0 100644 --- a/sql/spock--5.0.6--6.0.0-devel.sql +++ b/sql/spock--5.0.8--6.0.0.sql @@ -1,12 +1,30 @@ -/* spock--5.0.6--6.0.0-devel.sql */ +/* spock--5.0.8--6.0.0.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION -\echo Use "ALTER EXTENSION spock UPDATE TO '6.0.0-devel'" to load this file. \quit +\echo Use "ALTER EXTENSION spock UPDATE TO '6.0.0'" to load this file. \quit + +-- Drop functions removed from the 6.0.0 fresh install (present since 5.0.0 but no longer needed) +DROP FUNCTION IF EXISTS spock.convert_column_to_int8(regclass, smallint); +DROP FUNCTION IF EXISTS spock.convert_sequence_to_snowflake(regclass); + +-- Add IMMUTABLE PARALLEL SAFE to md5_agg_sfunc (was missing in earlier definitions) +CREATE OR REPLACE FUNCTION spock.md5_agg_sfunc(text, anyelement) + RETURNS text +AS $$ SELECT md5($1 || $2::text) $$ +LANGUAGE sql IMMUTABLE PARALLEL SAFE; + +-- Add named parameters to spock_gen_slot_name (originally created without names in 5.0.0) +CREATE OR REPLACE FUNCTION spock.spock_gen_slot_name( + dbname name, + provider_node name, + subscription name +) RETURNS name +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; DROP VIEW IF EXISTS spock.lag_tracker; DROP TABLE IF EXISTS spock.progress; -DROP FUNCTION IF EXISTS spock.apply_group_progress; CREATE FUNCTION spock.apply_group_progress ( OUT dbid oid, OUT node_id oid, @@ -26,23 +44,10 @@ LANGUAGE c AS 'MODULE_PATHNAME', 'get_apply_group_progress'; -- for internal use only. CREATE VIEW spock.progress AS SELECT * FROM spock.apply_group_progress() - WHERE dbid = ( - SELECT oid FROM pg_database WHERE datname = current_database() - ); + WHERE dbid = ( + SELECT oid FROM pg_database WHERE datname = current_database() + ); -CREATE FUNCTION spock.pause_apply_workers() -RETURNS void -AS 'MODULE_PATHNAME', 'spock_pause_apply_workers' -LANGUAGE C VOLATILE; - -REVOKE ALL ON FUNCTION spock.pause_apply_workers() FROM PUBLIC; - -CREATE FUNCTION spock.resume_apply_workers() -RETURNS void -AS 'MODULE_PATHNAME', 'spock_resume_apply_workers' -LANGUAGE C VOLATILE; - -REVOKE ALL ON FUNCTION spock.resume_apply_workers() FROM PUBLIC; -- Read peer progress (ros.remote_lsn) for all peer subscriptions. -- Called while apply workers are paused and the slot's snapshot is imported. @@ -85,7 +90,7 @@ BEGIN RAISE NOTICE 'SPOCK cswp slot=% v_lsn=%', p_slot_name, v_lsn; - -- Header row: lsn + snapshot only. + -- Header row: lsn only (snapshot managed by C caller). lsn := v_lsn; snapshot := v_snap; RETURN NEXT; @@ -323,116 +328,6 @@ END; $$ LANGUAGE plpgsql STRICT VOLATILE; --- spock.sync_event() gained an optional 'transactional' boolean argument --- (default false). Drop the old zero-arg signature first so the upgrade --- doesn't leave behind two overloads with overlapping zero-arg resolution. -DROP FUNCTION IF EXISTS spock.sync_event(); -CREATE FUNCTION spock.sync_event(transactional boolean DEFAULT false) -RETURNS pg_lsn RETURNS NULL ON NULL INPUT -AS 'MODULE_PATHNAME', 'spock_create_sync_event' -LANGUAGE C VOLATILE; - -DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, oid, pg_lsn, int); -DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, oid, pg_lsn, int, bool); -DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, name, pg_lsn, int); -DROP PROCEDURE IF EXISTS spock.wait_for_sync_event(OUT bool, name, pg_lsn, int, bool); -CREATE PROCEDURE spock.wait_for_sync_event( - OUT result bool, - origin_id oid, - lsn pg_lsn, - timeout int DEFAULT 0, - wait_if_disabled bool DEFAULT false -) AS $$ -DECLARE - target_id oid; - start_time timestamptz := clock_timestamp(); - progress_lsn pg_lsn; - sub_is_enabled bool; - sub_slot name; -BEGIN - IF origin_id IS NULL THEN - RAISE EXCEPTION 'Invalid NULL origin_id'; - END IF; - target_id := node_id FROM spock.node_info(); - - -- Upfront existence check is skipped when wait_if_disabled is true because - -- the subscription may not yet exist (e.g. a newly added node whose - -- subscriptions are still initializing). The loop below handles both the - -- not-found and disabled cases gracefully in that mode. - IF NOT wait_if_disabled THEN - SELECT sub_enabled, sub_slot_name INTO sub_is_enabled, sub_slot - FROM spock.subscription - WHERE sub_origin = origin_id AND sub_target = target_id; - - IF NOT FOUND THEN - RAISE EXCEPTION 'No subscription found for replication % => %', - origin_id, target_id; - END IF; - END IF; - - WHILE true LOOP - -- Re-check subscription state each iteration. Also re-fetches - -- sub_slot_name so the loop is self-contained when wait_if_disabled - -- is true and the pre-loop check was skipped. - SELECT sub_enabled, sub_slot_name INTO sub_is_enabled, sub_slot - FROM spock.subscription - WHERE sub_origin = origin_id AND sub_target = target_id; - - IF NOT FOUND THEN - IF NOT wait_if_disabled THEN - RAISE EXCEPTION 'No subscription found for replication % => %', - origin_id, target_id; - END IF; - -- Subscription not yet created; fall through to sleep. - ELSIF NOT sub_is_enabled THEN - IF NOT wait_if_disabled THEN - RAISE EXCEPTION 'Subscription % => % has been disabled', - origin_id, target_id; - END IF; - -- Subscription still initializing; fall through to sleep. - ELSE - -- Subscription is enabled; check LSN progress. - -- Uses PostgreSQL's native origin tracking rather than spock.progress - SELECT remote_lsn INTO progress_lsn - FROM pg_replication_origin_status - WHERE external_id = sub_slot; - - IF progress_lsn IS NOT NULL AND progress_lsn >= lsn THEN - result = true; - RETURN; - END IF; - END IF; - - IF timeout <> 0 AND - EXTRACT(EPOCH FROM (clock_timestamp() - start_time)) >= timeout THEN - result := false; - RETURN; - END IF; - - ROLLBACK; - PERFORM pg_sleep(0.2); - END LOOP; -END; -$$ LANGUAGE plpgsql; - -CREATE PROCEDURE spock.wait_for_sync_event( - OUT result bool, - origin name, - lsn pg_lsn, - timeout int DEFAULT 0, - wait_if_disabled bool DEFAULT false -) AS $$ -DECLARE - origin_id oid; -BEGIN - origin_id := node_id FROM spock.node WHERE node_name = origin; - IF origin_id IS NULL THEN - RAISE EXCEPTION 'Origin node ''%'' not found', origin; - END IF; - CALL spock.wait_for_sync_event(result, origin_id, lsn, timeout, wait_if_disabled); -END; -$$ LANGUAGE plpgsql; - CREATE FUNCTION spock.sub_alter_options( subscription_name name, options jsonb @@ -440,3 +335,4 @@ CREATE FUNCTION spock.sub_alter_options( RETURNS boolean AS 'MODULE_PATHNAME', 'spock_alter_subscription_options' LANGUAGE C STRICT VOLATILE; + diff --git a/sql/spock--6.0.0-devel.sql b/sql/spock--6.0.0.sql similarity index 100% rename from sql/spock--6.0.0-devel.sql rename to sql/spock--6.0.0.sql diff --git a/tests/regress/expected/init_1.out b/tests/regress/expected/init_1.out index c1d4e1eb..1072b5df 100644 --- a/tests/regress/expected/init_1.out +++ b/tests/regress/expected/init_1.out @@ -43,7 +43,7 @@ CREATE EXTENSION IF NOT EXISTS spock; List of installed extensions Name | Version | Default version | Schema | Description -------+-------------+-----------------+--------+-------------------------------- - spock | 6.0.0-devel | 6.0.0-devel | spock | PostgreSQL Logical Replication + spock | 6.0.0 | 6.0.0 | spock | PostgreSQL Logical Replication (1 row) SELECT * FROM spock.node_create(node_name := 'test_provider', dsn := (SELECT provider_dsn FROM spock_regress_variables()) || ' user=super'); diff --git a/tests/regress/expected/init_fail.out b/tests/regress/expected/init_fail.out index 4d484769..53f73f86 100644 --- a/tests/regress/expected/init_fail.out +++ b/tests/regress/expected/init_fail.out @@ -12,7 +12,7 @@ SET client_min_messages = 'warning'; \set VERBOSITY terse DO $$ BEGIN - CREATE EXTENSION IF NOT EXISTS spock VERSION '6.0.0-devel'; + CREATE EXTENSION IF NOT EXISTS spock VERSION '6.0.0'; END; $$; ALTER EXTENSION spock UPDATE; diff --git a/tests/regress/sql/init_fail.sql b/tests/regress/sql/init_fail.sql index d0f06586..90bd8af7 100644 --- a/tests/regress/sql/init_fail.sql +++ b/tests/regress/sql/init_fail.sql @@ -17,7 +17,7 @@ SET client_min_messages = 'warning'; DO $$ BEGIN - CREATE EXTENSION IF NOT EXISTS spock VERSION '6.0.0-devel'; + CREATE EXTENSION IF NOT EXISTS spock VERSION '6.0.0'; END; $$; ALTER EXTENSION spock UPDATE; diff --git a/tests/tap/schedule b/tests/tap/schedule index a7894dea..94ec8830 100644 --- a/tests/tap/schedule +++ b/tests/tap/schedule @@ -44,3 +44,5 @@ test: 018_forward_origins test: 018_failover_slots test: 019_stale_fd_epoll_after_conn_death test: 022_rmgr_progress_post_checkpoint_crash +# Upgrade schema match test (builds from source, slow): +#test: 018_upgrade_schema_match diff --git a/tests/tap/t/018_upgrade_schema_match.pl b/tests/tap/t/018_upgrade_schema_match.pl new file mode 100644 index 00000000..3578325b --- /dev/null +++ b/tests/tap/t/018_upgrade_schema_match.pl @@ -0,0 +1,457 @@ +use strict; +use warnings; +use Test::More; +use File::Path qw(make_path remove_tree); +use Cwd qw(getcwd); + +use lib '.'; +use lib 't'; +use SpockTest qw(system_or_bail system_maybe wait_for_pg_ready psql_or_bail scalar_query); + +# ============================================================================= +# Test: 018_upgrade_schema_match.pl +# +# Verify that upgrading Spock from 5.0.8 -> 6.0.0 produces a schema that is +# identical to a fresh 6.0.0 installation. +# +# Each Spock version requires PostgreSQL built with that branch's patches: +# origin/v5_STABLE needs patches/18/ from v5_STABLE (e.g. row-filter-check) +# HEAD needs patches/18/ from current branch +# +# Fast path (local dev): set V5_PG_INSTALL and V60_PG_INSTALL env vars to +# point at pre-built PG installations that already include the right Spock. +# The test will skip all builds and go straight to the schema comparison. +# +# Full build path (CI): leave the env vars unset. The test clones the spock +# repo at each branch, builds PG 18 from source with that branch's patches, +# then builds and installs Spock. PG installs are preserved in TEMP_BASE +# for caching across runs (only datadirs and source trees are cleaned up). +# +# Environment variables: +# V5_PG_INSTALL pre-built PG+Spock 5.0.8 dir (default: $TEMP_BASE/pg_v5) +# V60_PG_INSTALL pre-built PG+Spock 6.0.0 dir (default: $TEMP_BASE/pg_v60) +# PG_TAG git tag for PG source (default: REL_18_2) +# PG_REPO git URL for PG source (default: github postgres mirror) +# +# NOTE: This test is intentionally excluded from the default schedule (slow). +# ============================================================================= + +# ───────────────────────────────────────────────────────────────────────────── +# SPOCK_REPO detection (same strategy as 014_rolling_upgrade.pl) +# ───────────────────────────────────────────────────────────────────────────── +my $SPOCK_REPO; +if (-d "/home/pgedge/spock" && -f "/home/pgedge/spock/Makefile") { + $SPOCK_REPO = "/home/pgedge/spock"; +} else { + my $cwd = getcwd(); + if ($cwd =~ m{^(/.+)/tests/tap(?:/t)?$}) { + $SPOCK_REPO = $1; + } else { + $SPOCK_REPO = $cwd; + } +} +die "SPOCK_REPO not found or missing Makefile: $SPOCK_REPO\n" + unless $SPOCK_REPO && -d $SPOCK_REPO && -f "$SPOCK_REPO/Makefile"; + +# ───────────────────────────────────────────────────────────────────────────── +# Configuration +# ───────────────────────────────────────────────────────────────────────────── +my $TEMP_BASE = '/tmp/spock_upgrade_schema_test'; + +# Pre-built PG+Spock installs. When set, the test skips all source builds. +# Local example: V5_PG_INSTALL=/usr/local/pgsql/pg18-5.x +# V60_PG_INSTALL=/usr/local/pgsql/pg18 +my $V5_PG_INSTALL = $ENV{V5_PG_INSTALL} // "$TEMP_BASE/pg_v5"; +my $V60_PG_INSTALL = $ENV{V60_PG_INSTALL} // "$TEMP_BASE/pg_v60"; + +# PostgreSQL source for fresh builds +my $PG_TAG = $ENV{PG_TAG} // "REL_18_2"; +my $PG_REPO = $ENV{PG_REPO} // "https://github.com/postgres/postgres.git"; + +my $V5_BRANCH = "origin/v5_STABLE"; +my $V60_BRANCH = "HEAD"; + +my $V5_BIN = "$V5_PG_INSTALL/bin"; +my $V60_BIN = "$V60_PG_INSTALL/bin"; +my $PG_BIN = $V60_BIN; # client tools (psql, pg_ctl) use v60 binary + +my $DATADIR_UPG = "$TEMP_BASE/datadir_upgraded"; # starts at 5.0.8, upgraded +my $DATADIR_NEW = "$TEMP_BASE/datadir_fresh"; # fresh 6.0.0 install +my $PORT_UPG = 5441; +my $PORT_NEW = 5442; + +my $NODE_UPG = 1; +my $NODE_NEW = 2; +my $DB_NAME = 'regression'; +my $DB_USER = 'regression'; +my $HOST = '127.0.0.1'; + +my $LOG_DIR = $ENV{TESTLOGDIR} // 'logs'; +my $LOG_FILE = $ENV{SPOCKTEST_LOG_FILE} + // "$LOG_DIR/018_upgrade_schema_match.log"; + +make_path($LOG_DIR); +make_path($TEMP_BASE); + +# Convert any uncaught die into BAIL_OUT so prove always sees a proper TAP result. +$SIG{__DIE__} = sub { + return if $^S; # inside eval — let it propagate normally + my $msg = shift; + $msg =~ s/\s+$//; + BAIL_OUT("Fatal: $msg (see $LOG_FILE)"); +}; + +diag("SPOCK_REPO : $SPOCK_REPO"); +diag("V5_PG_INSTALL : $V5_PG_INSTALL"); +diag("V60_PG_INSTALL: $V60_PG_INSTALL"); +diag("PG_TAG : $PG_TAG"); + +# ───────────────────────────────────────────────────────────────────────────── +# Cleanup on exit +# ───────────────────────────────────────────────────────────────────────────── +my @started_datadirs; +END { + for my $dd (@started_datadirs) { + system("$PG_BIN/pg_ctl stop -D '$dd' -m immediate -s 2>/dev/null"); + } + # Remove datadirs; preserve pg installs (for caching) and pg_src (for debugging). + remove_tree($DATADIR_UPG) if -d $DATADIR_UPG; + remove_tree($DATADIR_NEW) if -d $DATADIR_NEW; + remove_tree("$TEMP_BASE/build_v5") if -d "$TEMP_BASE/build_v5"; + remove_tree("$TEMP_BASE/build_v60") if -d "$TEMP_BASE/build_v60"; + $? = 0; # Don't let pg_ctl exit codes propagate +} + +# ───────────────────────────────────────────────────────────────────────────── +# Helpers +# ───────────────────────────────────────────────────────────────────────────── + +sub run_logged { + my @cmd = @_; + open my $log, '>>', $LOG_FILE or die "Cannot open $LOG_FILE: $!"; + my $pid = fork // die "fork failed: $!"; + if ($pid == 0) { + open STDOUT, '>&', $log; + open STDERR, '>&', $log; + exec @cmd or exit 127; + } + waitpid($pid, 0); + return $? >> 8; +} + +sub run_or_bail { + my @cmd = @_; + my $rc = run_logged(@cmd); + BAIL_OUT("Command failed (rc=$rc): @cmd\n (see $LOG_FILE for details)") if $rc; +} + +# Run a multi-line SQL query and return trimmed scalar value +# (SpockTest::psql_or_bail / scalar_query only support single-line -c queries) +sub psql_query { + my ($port, $sql) = @_; + my $tmpfile = "/tmp/spock_018_$$.sql"; + open my $fh, '>', $tmpfile or die "Cannot write $tmpfile: $!"; + print $fh $sql; + close $fh; + open $fh, '-|', "$PG_BIN/psql", '-X', '-t', '-A', + '-p', $port, '-d', $DB_NAME, '-U', $DB_USER, '-f', $tmpfile + or die "Cannot run psql: $!"; + my $out = join '', <$fh>; + close $fh; + unlink $tmpfile; + chomp $out; + $out =~ s/^\s+|\s+$//g; + return $out; +} + +# Ensure a PG + Spock environment is ready at $pg_install_dir. +# +# Fast path: if $pg_install_dir/bin/postgres and spock library both exist, +# return immediately (handles pre-built installs via env vars). +# +# Build path: clone spock at $spock_branch, build PG from $PG_REPO at $PG_TAG +# applying that branch's patches/18/*.diff, then build and install Spock. +sub ensure_version_ready { + my ($spock_branch, $pg_install_dir, $version_name) = @_; + + my $pg_config = "$pg_install_dir/bin/pg_config"; + my $spock_build = "$TEMP_BASE/build_$version_name"; + my $pg_src = "$TEMP_BASE/pg_src_$version_name"; + + # ── Fast path: pre-built PG + Spock already present ── + if (-x "$pg_install_dir/bin/postgres") { + my $pkglibdir = `$pg_config --pkglibdir 2>/dev/null`; chomp $pkglibdir; + if ($pkglibdir && (-f "$pkglibdir/spock.so" || -f "$pkglibdir/spock.dylib")) { + diag("$version_name: pre-built PG+Spock found at $pg_install_dir — skipping builds"); + return 1; + } + } + + # ── Clone spock repo at the requested branch ── + diag("$version_name: cloning spock ($spock_branch) to $spock_build"); + remove_tree($spock_build) if -d $spock_build; + run_or_bail("git", "clone", "--quiet", $SPOCK_REPO, $spock_build); + if ($spock_branch ne 'HEAD') { + run_or_bail("bash", "-c", "cd $spock_build && git checkout --quiet $spock_branch"); + } + + # ── Build PG from source if not already installed ── + if (!-x "$pg_install_dir/bin/postgres") { + diag("$version_name: building PostgreSQL $PG_TAG from source..."); + remove_tree($pg_src) if -d $pg_src; + make_path($pg_install_dir); + + run_or_bail("git", "clone", "--quiet", "--depth=1", + "--branch", $PG_TAG, $PG_REPO, $pg_src); + + my $patches_dir = "$spock_build/patches/18"; + for my $patch (sort glob("$patches_dir/*.diff")) { + my $name = (split '/', $patch)[-1]; + diag(" applying $name"); + run_or_bail("bash", "-c", "cd $pg_src && patch -p1 < $patch"); + } + + diag("$version_name: configure (build log: $LOG_FILE)"); + run_or_bail("bash", "-c", + "cd $pg_src && ./configure --prefix=$pg_install_dir " + . "--without-icu --without-ldap --without-gssapi --without-readline"); + diag("$version_name: make (build log: $LOG_FILE)"); + # Unset MAKEFLAGS/MAKELEVEL inherited from the outer 'make check_prove' + # to prevent them from interfering with PG's own build. + run_or_bail("bash", "-c", "cd $pg_src && unset MAKEFLAGS MAKELEVEL MFLAGS && make"); + run_or_bail("bash", "-c", "cd $pg_src && unset MAKEFLAGS MAKELEVEL MFLAGS && make install"); + diag("$version_name: PostgreSQL installed to $pg_install_dir"); + } + + # ── Build and install Spock ── + diag("$version_name: building Spock..."); + run_or_bail("bash", "-c", "cd $spock_build && unset MAKEFLAGS MAKELEVEL MFLAGS && make PG_CONFIG=$pg_config 2>&1"); + run_or_bail("bash", "-c", "cd $spock_build && unset MAKEFLAGS MAKELEVEL MFLAGS && make install PG_CONFIG=$pg_config 2>&1"); + diag("$version_name: Spock installed to $pg_install_dir"); + return 1; +} + +# Append key=value lines to postgresql.conf +sub pg_conf_append { + my ($datadir, %kv) = @_; + open my $fh, '>>', "$datadir/postgresql.conf" or die $!; + print $fh "$_=$kv{$_}\n" for keys %kv; + close $fh; +} + +sub _start_postgres { + my ($pg_bin, $datadir) = @_; + open my $log, '>>', $LOG_FILE or die $!; + my $pid = fork // die "fork failed: $!"; + if ($pid == 0) { + open STDOUT, '>&', $log; + open STDERR, '>&', $log; + exec "$pg_bin/postgres", '-D', $datadir; + exit 127; + } + push @started_datadirs, $datadir; +} + +# Initialise a data directory and start postgres from $pg_install_dir +sub init_and_start_node { + my ($datadir, $port, $pg_install_dir) = @_; + remove_tree($datadir) if -d $datadir; + + my $pg_bin = "$pg_install_dir/bin"; + run_or_bail("$pg_bin/initdb", '-A', 'trust', '-D', $datadir); + + pg_conf_append($datadir, + port => $port, + listen_addresses => "'*'", + wal_level => 'logical', + track_commit_timestamp => 'on', + shared_preload_libraries => "'spock'", + log_min_messages => 'warning', + ); + + _start_postgres($pg_bin, $datadir); + return wait_for_pg_ready($HOST, $port, $PG_BIN, 30); +} + +sub stop_pg { + my ($datadir) = @_; + run_logged("$PG_BIN/pg_ctl", 'stop', '-D', $datadir, '-m', 'fast', '-w'); + sleep 2; +} + +# ───────────────────────────────────────────────────────────────────────────── +# Schema comparison queries +# ───────────────────────────────────────────────────────────────────────────── + +my $Q_TABLES = <<'SQL'; +SELECT string_agg(table_name, E'\n' ORDER BY table_name) +FROM information_schema.tables +WHERE table_schema = 'spock' AND table_type = 'BASE TABLE' +SQL + +# udt_name catches text vs text[] mismatches (_text = text[]) +my $Q_COLUMNS = <<'SQL'; +SELECT string_agg( + table_name || '.' || column_name || ' ' || udt_name + || CASE WHEN is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END, + E'\n' ORDER BY table_name, ordinal_position) +FROM information_schema.columns +WHERE table_schema = 'spock' +SQL + +my $Q_FUNCTIONS = <<'SQL'; +SELECT string_agg( + proname + || '(' || pg_catalog.pg_get_function_arguments(oid) || ')' + || ':' || prokind::text, + E'\n' ORDER BY proname, pg_catalog.pg_get_function_arguments(oid)) +FROM pg_catalog.pg_proc +WHERE pronamespace = 'spock'::regnamespace +SQL + +my $Q_VIEWS = <<'SQL'; +SELECT string_agg(viewname, E'\n' ORDER BY viewname) +FROM pg_catalog.pg_views +WHERE schemaname = 'spock' +SQL + +my $Q_SEQUENCES = <<'SQL'; +SELECT string_agg(sequence_name, E'\n' ORDER BY sequence_name) +FROM information_schema.sequences +WHERE sequence_schema = 'spock' +SQL + +# View definitions (catches changes to view SQL) +my $Q_VIEW_DEFS = <<'SQL'; +SELECT string_agg(viewname || ':' || regexp_replace(definition, '\s+', ' ', 'g'), + E'\n' ORDER BY viewname) +FROM pg_catalog.pg_views +WHERE schemaname = 'spock' +SQL + +# plpgsql/SQL function bodies (not C — those share the same symbol name) +my $Q_FUNC_BODIES = <<'SQL'; +SELECT string_agg( + proname || '(' || pg_catalog.pg_get_function_arguments(p.oid) || '):' + || regexp_replace(pg_catalog.pg_get_functiondef(p.oid), '\s+', ' ', 'g'), + E'\n' ORDER BY proname, pg_catalog.pg_get_function_arguments(p.oid)) +FROM pg_catalog.pg_proc p +JOIN pg_catalog.pg_language l ON l.oid = p.prolang +WHERE p.pronamespace = 'spock'::regnamespace + AND l.lanname IN ('plpgsql', 'sql') +SQL + +sub compare_category { + my ($label, $upg, $new) = @_; + if ($upg ne $new) { + diag("$label MISMATCH"); + diag(" UPGRADED:\n$upg"); + diag(" FRESH:\n$new"); + } + is($upg, $new, "$label match between upgraded and fresh install"); +} + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 1 – Ensure v5_STABLE environment (PG + Spock 5.0.8) +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 1: Ensuring Spock 5.0.8 environment ($V5_BRANCH)"); +ok(ensure_version_ready($V5_BRANCH, $V5_PG_INSTALL, 'v5'), + "Spock 5.0.8 environment ready"); + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 2 – Ensure current environment (PG + Spock 6.0.0) +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 2: Ensuring Spock 6.0.0 environment ($V60_BRANCH)"); +ok(ensure_version_ready($V60_BRANCH, $V60_PG_INSTALL, 'v60'), + "Spock 6.0.0 environment ready"); + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 3 – Start upgrade node (Spock 5.0.8) +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 3: Initialising upgrade node (Spock 5.0.8)..."); +ok(init_and_start_node($DATADIR_UPG, $PORT_UPG, $V5_PG_INSTALL), + 'Upgrade node started (5.0.8)'); + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 4 – Start fresh node (Spock 6.0.0) +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 4: Initialising fresh node (Spock 6.0.0)..."); +ok(init_and_start_node($DATADIR_NEW, $PORT_NEW, $V60_PG_INSTALL), + 'Fresh node started (6.0.0)'); + +# Create database and user on both nodes +for my $port ($PORT_UPG, $PORT_NEW) { + system_maybe("$PG_BIN/psql", '-p', $port, '-d', 'postgres', + '-c', "CREATE DATABASE $DB_NAME"); + system_maybe("$PG_BIN/psql", '-p', $port, '-d', 'postgres', + '-c', "CREATE USER $DB_USER SUPERUSER"); +} + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 5 – Install extensions +# ───────────────────────────────────────────────────────────────────────────── +psql_or_bail($NODE_UPG, 'CREATE EXTENSION spock'); +my $ver_upg = scalar_query($NODE_UPG, + "SELECT extversion FROM pg_extension WHERE extname = 'spock'"); +diag("Upgrade node extension version after CREATE: $ver_upg"); +like($ver_upg, qr/^5\.0\.8/, 'Upgrade node starts at 5.0.8'); + +psql_or_bail($NODE_NEW, 'CREATE EXTENSION spock'); +my $ver_new = scalar_query($NODE_NEW, + "SELECT extversion FROM pg_extension WHERE extname = 'spock'"); +diag("Fresh node extension version after CREATE: $ver_new"); +like($ver_new, qr/^6\.0\.0/, 'Fresh node installs at 6.0.0'); + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 6 – Upgrade node 1: swap to 6.0.0 postgres binary, ALTER EXTENSION UPDATE +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 6: Stopping upgrade node to swap to Spock 6.0.0 binary..."); +stop_pg($DATADIR_UPG); + +# Restart with the 6.0.0 postgres binary. shared_preload_libraries='spock' +# (bare name) resolves against the new binary's own $libdir — no conf change needed. +_start_postgres($V60_BIN, $DATADIR_UPG); +ok(wait_for_pg_ready($HOST, $PORT_UPG, $PG_BIN, 30), + 'Upgrade node restarted with 6.0.0 binary'); + +psql_or_bail($NODE_UPG, 'ALTER EXTENSION spock UPDATE'); + +my $ver_upg2 = scalar_query($NODE_UPG, + "SELECT extversion FROM pg_extension WHERE extname = 'spock'"); +diag("Upgrade node extension version after ALTER EXTENSION UPDATE: $ver_upg2"); +is($ver_upg2, '6.0.0', 'Upgrade node extension version is now 6.0.0'); + +# ───────────────────────────────────────────────────────────────────────────── +# PHASE 7 – Schema comparison (catalog queries) +# ───────────────────────────────────────────────────────────────────────────── +diag("PHASE 7: Comparing schemas via catalog queries..."); + +compare_category('Tables', + psql_query($PORT_UPG, $Q_TABLES), + psql_query($PORT_NEW, $Q_TABLES)); + +compare_category('Columns (names and types)', + psql_query($PORT_UPG, $Q_COLUMNS), + psql_query($PORT_NEW, $Q_COLUMNS)); + +compare_category('Functions and procedures', + psql_query($PORT_UPG, $Q_FUNCTIONS), + psql_query($PORT_NEW, $Q_FUNCTIONS)); + +compare_category('Views', + psql_query($PORT_UPG, $Q_VIEWS), + psql_query($PORT_NEW, $Q_VIEWS)); + +compare_category('Sequences', + psql_query($PORT_UPG, $Q_SEQUENCES), + psql_query($PORT_NEW, $Q_SEQUENCES)); + +compare_category('View definitions', + psql_query($PORT_UPG, $Q_VIEW_DEFS), + psql_query($PORT_NEW, $Q_VIEW_DEFS)); + +compare_category('plpgsql/SQL function bodies', + psql_query($PORT_UPG, $Q_FUNC_BODIES), + psql_query($PORT_NEW, $Q_FUNC_BODIES)); + +done_testing();