This is an automated email from the ASF dual-hosted git repository.
muhammadshoaib pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git
The following commit(s) were added to refs/heads/master by this push:
new a1b749af Fix upgrade test: allow function removal (#2422)
a1b749af is described below
commit a1b749af383dfdf0958ba6c1531b99aeab9207f5
Author: John Gemignani <[email protected]>
AuthorDate: Tue May 5 14:02:19 2026 -0700
Fix upgrade test: allow function removal (#2422)
Fix upgrade test: allow function removal and detect more deficiencies.
The age_upgrade regression test (added in #2364, refined in #2377, #2397,
install and a synthetic-initial -> current upgrade. Three gaps surfaced
in practice:
1. Function removal forced permanent C stubs.
The synthetic '_initial' install is built from a fixed historical
commit. CREATE EXTENSION resolves every CREATE FUNCTION ... AS
'$libdir/age', '<symbol>' via dlsym at install time when
check_function_bodies is on (the default). If a developer retires a
C entry point in HEAD's age.so, step 10 aborts with "could not find
function ... in file age.so" -- even though the immediately-following
ALTER EXTENSION UPDATE would DROP that SQL declaration. The only way
to keep the test green was to leave a permanent error-raising stub
in age.so, and to remember to add a DROP to the upgrade template.
2. Modifications were under-detected.
The function-property-change query did not compare probin or prosrc,
so a C function whose symbol was renamed in the upgrade template, or
a SQL/plpgsql function whose body changed in either path, slipped
through.
3. Extension membership was not checked.
A template that CREATEs an object but never ALTER EXTENSION ADDs it
leaves a row in pg_proc/pg_class but no pg_depend deptype='e' link.
pg_dump --extension would diverge, but the existing per-catalog diff
queries all returned 0 rows.
Changes (regress/sql/age_upgrade.sql + regress/expected/age_upgrade.out):
* Step 10 wraps the synthetic CREATE EXTENSION in
SET check_function_bodies = off; ... RESET check_function_bodies;
Symbol resolution is deferred to call time. Step 11's ALTER EXTENSION
UPDATE then DROPs any retired functions before any plan can call them.
Step 35's fresh CREATE EXTENSION runs at the GUC default, so HEAD's
sql/ <-> HEAD's age.so consistency is still enforced on the production
install path.
* Steps 2 and 13 add probin and prosrc to the function snapshot.
Step 21 reports probin and prosrc divergences alongside the existing
property-change columns.
* Steps 7b and 18b add an extension-membership snapshot from
pg_depend deptype='e' filtered to the AGE extension OID. Every member
is labeled by stable identity (regprocedure, regtype, regoperator,
opfname+strategy+types, etc.), never by raw OID, so OID drift between
fresh and upgrade installs cannot produce false positives. Steps 33a
and 33b report MISSING / EXTRA members. Step 34 adds extmembers_match
to the summary row.
* Section-header step ranges updated to include the new sub-steps.
The change is fully self-contained: only regress/sql/age_upgrade.sql and
regress/expected/age_upgrade.out are modified. No production C, SQL,
build, or test files are touched. All 34 regression tests pass on a
clean tree.
Mutation-tested with 8 cases against the unmutated tree: baseline pass;
remove-function-with-DROP pass (no stub needed); remove-function-forget-
DROP fail; add-function-with-CREATE pass; add-function-forget-CREATE
fail; volatility-change-no-template fail; volatility-change-with-CREATE-
OR-REPLACE pass; C-symbol-rename-no-template fail. All eight expected
outcomes observed.
All 34 regression tests pass.
Co-authored-by: Claude <[email protected]>
modified: regress/expected/age_upgrade.out
modified: regress/sql/age_upgrade.sql
---
regress/expected/age_upgrade.out | 208 +++++++++++++++++++++++++++++++++++----
regress/sql/age_upgrade.sql | 194 +++++++++++++++++++++++++++++++++---
2 files changed, 367 insertions(+), 35 deletions(-)
diff --git a/regress/expected/age_upgrade.out b/regress/expected/age_upgrade.out
index edf8e602..446e0eb7 100644
--- a/regress/expected/age_upgrade.out
+++ b/regress/expected/age_upgrade.out
@@ -27,16 +27,30 @@
-- upgrade template.
--
-- Compared catalogs:
--- pg_proc — functions, aggregates, procedures (name, args, properties)
+-- pg_proc — functions, aggregates, procedures (name, args, properties
+-- including probin/prosrc to catch C-symbol renames and
+-- SQL-body changes)
-- pg_class — tables, views, sequences, indexes (name, kind)
-- pg_type — types (name, type category)
-- pg_operator — operators (name, left/right types)
-- pg_cast — casts involving AGE types (source, target, context)
-- pg_opclass — operator classes (name, access method)
-- pg_constraint — constraints (name, type, table, referenced table)
+-- pg_depend — extension membership (every AGE-owned object must be
+-- linked back to the extension via deptype='e'; catches
+-- a missing ALTER EXTENSION age ADD ... in the template)
--
-- All comparison queries should return 0 rows.
--
+-- Note on synthetic-initial install (step 10): the synthetic '*_initial'
+-- snapshot is built from a fixed historical commit, so its CREATE FUNCTION
+-- statements may reference C symbols that have since been removed from the
+-- current age.so. Step 10 disables check_function_bodies so that dlsym is
+-- deferred to call time; the immediately-following ALTER EXTENSION UPDATE
+-- (step 11) DROPs any such retired functions before any plan can call them.
+-- This lets developers cleanly remove deprecated C entry points without
+-- needing to keep error-raising stubs in age.so.
+--
LOAD 'age';
SET search_path TO ag_catalog;
-- Step 1: Clean up any graphs left by prior tests (deterministic, no output).
@@ -56,15 +70,23 @@ BEGIN
END
$$;
-- =====================================================================
--- FRESH INSTALL SNAPSHOTS (Steps 2-7)
+-- FRESH INSTALL SNAPSHOTS (Steps 2-7b)
-- Capture the catalog state from the default CREATE EXTENSION install.
-- =====================================================================
-- Step 2: Snapshot functions (includes aggregates via prokind).
+-- probin/prosrc capture the binding to the implementation:
+-- * LANGUAGE c : probin = '$libdir/age', prosrc = C symbol name
+-- (a renamed/retargeted symbol shows up here)
+-- * LANGUAGE sql/plpgsql: probin = NULL, prosrc = function body text
+-- (a body change in the upgrade template shows up
here)
+-- * LANGUAGE internal : probin = NULL, prosrc = builtin name
CREATE TEMP TABLE _fresh_funcs AS
SELECT proname::text,
pg_get_function_identity_arguments(oid) AS args,
provolatile::text, proisstrict::text, prokind::text,
- prorettype::regtype::text AS rettype, proretset::text
+ prorettype::regtype::text AS rettype, proretset::text,
+ COALESCE(probin, '') AS probin,
+ COALESCE(prosrc, '') AS prosrc
FROM pg_proc
WHERE pronamespace = 'ag_catalog'::regnamespace
ORDER BY proname, args;
@@ -113,6 +135,57 @@ SELECT conname::text, contype::text,
FROM pg_constraint
WHERE connamespace = 'ag_catalog'::regnamespace
ORDER BY conname;
+-- Step 7b: Snapshot extension membership (pg_depend deptype='e').
+-- Every object that CREATE EXTENSION owns has a row in pg_depend linking
+-- it to the extension. The upgrade template must produce the same set:
+-- if it CREATEs an object but forgets to ALTER EXTENSION ADD it, the
+-- catalog row exists (so funcs_match/rels_match would pass) but the
+-- pg_depend link is absent and pg_dump --extension would diverge.
+CREATE TEMP TABLE _fresh_extmembers AS
+SELECT
+ CASE d.classid
+ WHEN 'pg_proc'::regclass
+ THEN 'function: ' || d.objid::regprocedure::text
+ WHEN 'pg_type'::regclass
+ THEN 'type: ' || d.objid::regtype::text
+ WHEN 'pg_class'::regclass
+ THEN 'relation: ' || d.objid::regclass::text
+ || ' (' || (SELECT relkind::text FROM pg_class WHERE oid =
d.objid) || ')'
+ WHEN 'pg_operator'::regclass
+ THEN 'operator: ' || d.objid::regoperator::text
+ WHEN 'pg_cast'::regclass
+ THEN 'cast: ' || (SELECT castsource::regtype::text || ' -> '
|| casttarget::regtype::text
+ FROM pg_cast WHERE oid = d.objid)
+ WHEN 'pg_opclass'::regclass
+ THEN 'opclass: ' || (SELECT opcname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opcmethod) || ')'
+ FROM pg_opclass WHERE oid = d.objid)
+ WHEN 'pg_constraint'::regclass
+ THEN 'constraint: ' || (SELECT conname || ' on ' ||
conrelid::regclass::text
+ FROM pg_constraint WHERE oid = d.objid)
+ WHEN 'pg_opfamily'::regclass
+ THEN 'opfamily: ' || (SELECT opfname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opfmethod) || ')'
+ FROM pg_opfamily WHERE oid = d.objid)
+ WHEN 'pg_amop'::regclass
+ THEN 'amop: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = a.amopfamily)
+ || ' [strategy ' || a.amopstrategy ||
'] '
+ || a.amoplefttype::regtype::text || ','
+ || a.amoprighttype::regtype::text || '
op '
+ || a.amopopr::regoperator::text
+ FROM pg_amop a WHERE a.oid = d.objid)
+ WHEN 'pg_amproc'::regclass
+ THEN 'amproc: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = p.amprocfamily)
+ || ' [proc ' || p.amprocnum || '] '
+ || p.amproclefttype::regtype::text ||
','
+ || p.amprocrighttype::regtype::text ||
' fn '
+ || p.amproc::regprocedure::text
+ FROM pg_amproc p WHERE p.oid = d.objid)
+ ELSE 'unhandled[' || d.classid::regclass::text || ']'
+ END AS member
+FROM pg_depend d
+WHERE d.deptype = 'e'
+ AND d.refclassid = 'pg_extension'::regclass
+ AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'age')
+ORDER BY 1;
-- Step 8: Drop AGE entirely.
DROP EXTENSION age;
-- Step 9: Verify we have an upgrade path available.
@@ -124,6 +197,19 @@ FROM pg_available_extension_versions WHERE name = 'age';
(1 row)
-- Step 10: Install AGE at the synthetic initial version.
+--
+-- Disable check_function_bodies for this CREATE only. The synthetic
+-- '*_initial' SQL is pulled from a fixed historical commit and may
+-- declare C functions whose symbols have since been removed from the
+-- current age.so. With check_function_bodies=on, PostgreSQL would dlsym
+-- each such symbol at CREATE FUNCTION time and abort. Deferring the
+-- symbol probe to call time is safe because step 11 (ALTER EXTENSION
+-- UPDATE) immediately runs the upgrade template, which DROPs any
+-- removed-in-HEAD functions before the test (or any user) can call them.
+-- The fresh CREATE EXTENSION at step 35 keeps the GUC at its default,
+-- so any inconsistency between HEAD's SQL and HEAD's age.so is still
+-- caught at install time on the production code path.
+SET check_function_bodies = off;
DO $$
DECLARE init_ver text;
BEGIN
@@ -138,6 +224,7 @@ BEGIN
EXECUTE format('CREATE EXTENSION age VERSION %L', init_ver);
END;
$$;
+RESET check_function_bodies;
-- Step 11: Upgrade to the current (default) version via the stamped template.
DO $$
DECLARE curr_ver text;
@@ -160,15 +247,17 @@ FROM pg_available_extensions WHERE name = 'age';
(1 row)
-- =====================================================================
--- UPGRADED INSTALL SNAPSHOTS (Steps 13-18)
+-- UPGRADED INSTALL SNAPSHOTS (Steps 13-18b)
-- Capture the catalog state after upgrade from initial to current.
-- =====================================================================
--- Step 13: Snapshot functions.
+-- Step 13: Snapshot functions (probin/prosrc included; see step 2).
CREATE TEMP TABLE _upgraded_funcs AS
SELECT proname::text,
pg_get_function_identity_arguments(oid) AS args,
provolatile::text, proisstrict::text, prokind::text,
- prorettype::regtype::text AS rettype, proretset::text
+ prorettype::regtype::text AS rettype, proretset::text,
+ COALESCE(probin, '') AS probin,
+ COALESCE(prosrc, '') AS prosrc
FROM pg_proc
WHERE pronamespace = 'ag_catalog'::regnamespace
ORDER BY proname, args;
@@ -217,8 +306,54 @@ SELECT conname::text, contype::text,
FROM pg_constraint
WHERE connamespace = 'ag_catalog'::regnamespace
ORDER BY conname;
+-- Step 18b: Snapshot extension membership after upgrade (see step 7b).
+CREATE TEMP TABLE _upgraded_extmembers AS
+SELECT
+ CASE d.classid
+ WHEN 'pg_proc'::regclass
+ THEN 'function: ' || d.objid::regprocedure::text
+ WHEN 'pg_type'::regclass
+ THEN 'type: ' || d.objid::regtype::text
+ WHEN 'pg_class'::regclass
+ THEN 'relation: ' || d.objid::regclass::text
+ || ' (' || (SELECT relkind::text FROM pg_class WHERE oid =
d.objid) || ')'
+ WHEN 'pg_operator'::regclass
+ THEN 'operator: ' || d.objid::regoperator::text
+ WHEN 'pg_cast'::regclass
+ THEN 'cast: ' || (SELECT castsource::regtype::text || ' -> '
|| casttarget::regtype::text
+ FROM pg_cast WHERE oid = d.objid)
+ WHEN 'pg_opclass'::regclass
+ THEN 'opclass: ' || (SELECT opcname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opcmethod) || ')'
+ FROM pg_opclass WHERE oid = d.objid)
+ WHEN 'pg_constraint'::regclass
+ THEN 'constraint: ' || (SELECT conname || ' on ' ||
conrelid::regclass::text
+ FROM pg_constraint WHERE oid = d.objid)
+ WHEN 'pg_opfamily'::regclass
+ THEN 'opfamily: ' || (SELECT opfname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opfmethod) || ')'
+ FROM pg_opfamily WHERE oid = d.objid)
+ WHEN 'pg_amop'::regclass
+ THEN 'amop: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = a.amopfamily)
+ || ' [strategy ' || a.amopstrategy ||
'] '
+ || a.amoplefttype::regtype::text || ','
+ || a.amoprighttype::regtype::text || '
op '
+ || a.amopopr::regoperator::text
+ FROM pg_amop a WHERE a.oid = d.objid)
+ WHEN 'pg_amproc'::regclass
+ THEN 'amproc: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = p.amprocfamily)
+ || ' [proc ' || p.amprocnum || '] '
+ || p.amproclefttype::regtype::text ||
','
+ || p.amprocrighttype::regtype::text ||
' fn '
+ || p.amproc::regprocedure::text
+ FROM pg_amproc p WHERE p.oid = d.objid)
+ ELSE 'unhandled[' || d.classid::regclass::text || ']'
+ END AS member
+FROM pg_depend d
+WHERE d.deptype = 'e'
+ AND d.refclassid = 'pg_extension'::regclass
+ AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'age')
+ORDER BY 1;
-- =====================================================================
--- COMPARISON: Missing or extra objects (Steps 19-33)
+-- COMPARISON: Missing or extra objects (Steps 19-33b)
-- Any rows returned indicate a template deficiency.
-- =====================================================================
-- Step 19: Functions MISSING after upgrade.
@@ -241,13 +376,20 @@ ORDER BY 1;
----------------
(0 rows)
--- Step 21: Function PROPERTY changes (volatility, strictness, kind, return
type).
+-- Step 21: Function PROPERTY changes
+-- (kind, volatility, strictness, return type, return-set, binding,
+-- body/symbol). The probin/prosrc check catches:
+-- * a C function whose symbol was renamed in the upgrade template
+-- * a SQL/plpgsql function whose body was changed in either path
+-- * a language change between the fresh and upgrade installs.
SELECT f.proname || '(' || f.args || ')' AS function_name,
- CASE WHEN f.prokind <> u.prokind THEN 'prokind: ' || f.prokind
|| '->' || u.prokind END AS kind_change,
- CASE WHEN f.provolatile<> u.provolatile THEN 'volatile: ' ||
f.provolatile|| '->' || u.provolatile END AS volatility_change,
- CASE WHEN f.proisstrict<> u.proisstrict THEN 'strict: ' ||
f.proisstrict|| '->' || u.proisstrict END AS strict_change,
- CASE WHEN f.rettype <> u.rettype THEN 'rettype: ' || f.rettype
|| '->' || u.rettype END AS rettype_change,
- CASE WHEN f.proretset <> u.proretset THEN 'retset: ' ||
f.proretset || '->' || u.proretset END AS retset_change
+ CASE WHEN f.prokind <> u.prokind THEN 'prokind: ' ||
f.prokind || '->' || u.prokind END AS kind_change,
+ CASE WHEN f.provolatile <> u.provolatile THEN 'volatile: ' ||
f.provolatile || '->' || u.provolatile END AS volatility_change,
+ CASE WHEN f.proisstrict <> u.proisstrict THEN 'strict: ' ||
f.proisstrict || '->' || u.proisstrict END AS strict_change,
+ CASE WHEN f.rettype <> u.rettype THEN 'rettype: ' ||
f.rettype || '->' || u.rettype END AS rettype_change,
+ CASE WHEN f.proretset <> u.proretset THEN 'retset: ' ||
f.proretset || '->' || u.proretset END AS retset_change,
+ CASE WHEN f.probin <> u.probin THEN 'probin: ' ||
f.probin || '->' || u.probin END AS probin_change,
+ CASE WHEN f.prosrc <> u.prosrc THEN 'prosrc changed' END
AS prosrc_change
FROM _fresh_funcs f
JOIN _upgraded_funcs u USING (proname, args)
WHERE f.provolatile <> u.provolatile
@@ -255,9 +397,11 @@ WHERE f.provolatile <> u.provolatile
OR f.prokind <> u.prokind
OR f.rettype <> u.rettype
OR f.proretset <> u.proretset
+ OR f.probin <> u.probin
+ OR f.prosrc <> u.prosrc
ORDER BY 1;
- function_name | kind_change | volatility_change | strict_change |
rettype_change | retset_change
----------------+-------------+-------------------+---------------+----------------+---------------
+ function_name | kind_change | volatility_change | strict_change |
rettype_change | retset_change | probin_change | prosrc_change
+---------------+-------------+-------------------+---------------+----------------+---------------+---------------+---------------
(0 rows)
-- Step 22: Relations MISSING after upgrade.
@@ -380,6 +524,28 @@ ORDER BY 1;
------------------
(0 rows)
+-- Step 33a: Extension members MISSING after upgrade
+-- (object exists in pg_proc/pg_class/etc. but is not linked to the AGE
+-- extension via pg_depend, i.e. ALTER EXTENSION age ADD ... was forgotten).
+SELECT f.member AS missing_extension_member
+FROM _fresh_extmembers f
+LEFT JOIN _upgraded_extmembers u USING (member)
+WHERE u.member IS NULL
+ORDER BY 1;
+ missing_extension_member
+--------------------------
+(0 rows)
+
+-- Step 33b: Extension members EXTRA after upgrade.
+SELECT u.member AS extra_extension_member
+FROM _upgraded_extmembers u
+LEFT JOIN _fresh_extmembers f USING (member)
+WHERE f.member IS NULL
+ORDER BY 1;
+ extra_extension_member
+------------------------
+(0 rows)
+
-- =====================================================================
-- SUMMARY (Step 34)
-- =====================================================================
@@ -391,10 +557,11 @@ SELECT
(SELECT count(*) FROM _fresh_ops) = (SELECT count(*) FROM
_upgraded_ops) AS ops_match,
(SELECT count(*) FROM _fresh_casts) = (SELECT count(*) FROM
_upgraded_casts) AS casts_match,
(SELECT count(*) FROM _fresh_opclass) = (SELECT count(*) FROM
_upgraded_opclass) AS opclass_match,
- (SELECT count(*) FROM _fresh_constraints) = (SELECT count(*) FROM
_upgraded_constraints) AS constraints_match;
- funcs_match | rels_match | types_match | ops_match | casts_match |
opclass_match | constraints_match
--------------+------------+-------------+-----------+-------------+---------------+-------------------
- t | t | t | t | t | t
| t
+ (SELECT count(*) FROM _fresh_constraints) = (SELECT count(*) FROM
_upgraded_constraints) AS constraints_match,
+ (SELECT count(*) FROM _fresh_extmembers) = (SELECT count(*) FROM
_upgraded_extmembers) AS extmembers_match;
+ funcs_match | rels_match | types_match | ops_match | casts_match |
opclass_match | constraints_match | extmembers_match
+-------------+------------+-------------+-----------+-------------+---------------+-------------------+------------------
+ t | t | t | t | t | t
| t | t
(1 row)
-- =====================================================================
@@ -404,7 +571,8 @@ SELECT
DROP TABLE _fresh_funcs, _upgraded_funcs, _fresh_rels, _upgraded_rels,
_fresh_types, _upgraded_types, _fresh_ops, _upgraded_ops,
_fresh_casts, _upgraded_casts, _fresh_opclass, _upgraded_opclass,
- _fresh_constraints, _upgraded_constraints;
+ _fresh_constraints, _upgraded_constraints,
+ _fresh_extmembers, _upgraded_extmembers;
DROP EXTENSION age;
CREATE EXTENSION age;
-- Step 36: Remove synthetic upgrade test files from the extension directory.
diff --git a/regress/sql/age_upgrade.sql b/regress/sql/age_upgrade.sql
index f56f7ca9..98ce3e21 100644
--- a/regress/sql/age_upgrade.sql
+++ b/regress/sql/age_upgrade.sql
@@ -28,16 +28,30 @@
-- upgrade template.
--
-- Compared catalogs:
--- pg_proc — functions, aggregates, procedures (name, args, properties)
+-- pg_proc — functions, aggregates, procedures (name, args, properties
+-- including probin/prosrc to catch C-symbol renames and
+-- SQL-body changes)
-- pg_class — tables, views, sequences, indexes (name, kind)
-- pg_type — types (name, type category)
-- pg_operator — operators (name, left/right types)
-- pg_cast — casts involving AGE types (source, target, context)
-- pg_opclass — operator classes (name, access method)
-- pg_constraint — constraints (name, type, table, referenced table)
+-- pg_depend — extension membership (every AGE-owned object must be
+-- linked back to the extension via deptype='e'; catches
+-- a missing ALTER EXTENSION age ADD ... in the template)
--
-- All comparison queries should return 0 rows.
--
+-- Note on synthetic-initial install (step 10): the synthetic '*_initial'
+-- snapshot is built from a fixed historical commit, so its CREATE FUNCTION
+-- statements may reference C symbols that have since been removed from the
+-- current age.so. Step 10 disables check_function_bodies so that dlsym is
+-- deferred to call time; the immediately-following ALTER EXTENSION UPDATE
+-- (step 11) DROPs any such retired functions before any plan can call them.
+-- This lets developers cleanly remove deprecated C entry points without
+-- needing to keep error-raising stubs in age.so.
+--
LOAD 'age';
SET search_path TO ag_catalog;
@@ -60,16 +74,24 @@ END
$$;
-- =====================================================================
--- FRESH INSTALL SNAPSHOTS (Steps 2-7)
+-- FRESH INSTALL SNAPSHOTS (Steps 2-7b)
-- Capture the catalog state from the default CREATE EXTENSION install.
-- =====================================================================
-- Step 2: Snapshot functions (includes aggregates via prokind).
+-- probin/prosrc capture the binding to the implementation:
+-- * LANGUAGE c : probin = '$libdir/age', prosrc = C symbol name
+-- (a renamed/retargeted symbol shows up here)
+-- * LANGUAGE sql/plpgsql: probin = NULL, prosrc = function body text
+-- (a body change in the upgrade template shows up
here)
+-- * LANGUAGE internal : probin = NULL, prosrc = builtin name
CREATE TEMP TABLE _fresh_funcs AS
SELECT proname::text,
pg_get_function_identity_arguments(oid) AS args,
provolatile::text, proisstrict::text, prokind::text,
- prorettype::regtype::text AS rettype, proretset::text
+ prorettype::regtype::text AS rettype, proretset::text,
+ COALESCE(probin, '') AS probin,
+ COALESCE(prosrc, '') AS prosrc
FROM pg_proc
WHERE pronamespace = 'ag_catalog'::regnamespace
ORDER BY proname, args;
@@ -125,6 +147,58 @@ FROM pg_constraint
WHERE connamespace = 'ag_catalog'::regnamespace
ORDER BY conname;
+-- Step 7b: Snapshot extension membership (pg_depend deptype='e').
+-- Every object that CREATE EXTENSION owns has a row in pg_depend linking
+-- it to the extension. The upgrade template must produce the same set:
+-- if it CREATEs an object but forgets to ALTER EXTENSION ADD it, the
+-- catalog row exists (so funcs_match/rels_match would pass) but the
+-- pg_depend link is absent and pg_dump --extension would diverge.
+CREATE TEMP TABLE _fresh_extmembers AS
+SELECT
+ CASE d.classid
+ WHEN 'pg_proc'::regclass
+ THEN 'function: ' || d.objid::regprocedure::text
+ WHEN 'pg_type'::regclass
+ THEN 'type: ' || d.objid::regtype::text
+ WHEN 'pg_class'::regclass
+ THEN 'relation: ' || d.objid::regclass::text
+ || ' (' || (SELECT relkind::text FROM pg_class WHERE oid =
d.objid) || ')'
+ WHEN 'pg_operator'::regclass
+ THEN 'operator: ' || d.objid::regoperator::text
+ WHEN 'pg_cast'::regclass
+ THEN 'cast: ' || (SELECT castsource::regtype::text || ' -> '
|| casttarget::regtype::text
+ FROM pg_cast WHERE oid = d.objid)
+ WHEN 'pg_opclass'::regclass
+ THEN 'opclass: ' || (SELECT opcname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opcmethod) || ')'
+ FROM pg_opclass WHERE oid = d.objid)
+ WHEN 'pg_constraint'::regclass
+ THEN 'constraint: ' || (SELECT conname || ' on ' ||
conrelid::regclass::text
+ FROM pg_constraint WHERE oid = d.objid)
+ WHEN 'pg_opfamily'::regclass
+ THEN 'opfamily: ' || (SELECT opfname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opfmethod) || ')'
+ FROM pg_opfamily WHERE oid = d.objid)
+ WHEN 'pg_amop'::regclass
+ THEN 'amop: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = a.amopfamily)
+ || ' [strategy ' || a.amopstrategy ||
'] '
+ || a.amoplefttype::regtype::text || ','
+ || a.amoprighttype::regtype::text || '
op '
+ || a.amopopr::regoperator::text
+ FROM pg_amop a WHERE a.oid = d.objid)
+ WHEN 'pg_amproc'::regclass
+ THEN 'amproc: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = p.amprocfamily)
+ || ' [proc ' || p.amprocnum || '] '
+ || p.amproclefttype::regtype::text ||
','
+ || p.amprocrighttype::regtype::text ||
' fn '
+ || p.amproc::regprocedure::text
+ FROM pg_amproc p WHERE p.oid = d.objid)
+ ELSE 'unhandled[' || d.classid::regclass::text || ']'
+ END AS member
+FROM pg_depend d
+WHERE d.deptype = 'e'
+ AND d.refclassid = 'pg_extension'::regclass
+ AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'age')
+ORDER BY 1;
+
-- Step 8: Drop AGE entirely.
DROP EXTENSION age;
@@ -133,6 +207,19 @@ SELECT count(*) > 1 AS has_upgrade_path
FROM pg_available_extension_versions WHERE name = 'age';
-- Step 10: Install AGE at the synthetic initial version.
+--
+-- Disable check_function_bodies for this CREATE only. The synthetic
+-- '*_initial' SQL is pulled from a fixed historical commit and may
+-- declare C functions whose symbols have since been removed from the
+-- current age.so. With check_function_bodies=on, PostgreSQL would dlsym
+-- each such symbol at CREATE FUNCTION time and abort. Deferring the
+-- symbol probe to call time is safe because step 11 (ALTER EXTENSION
+-- UPDATE) immediately runs the upgrade template, which DROPs any
+-- removed-in-HEAD functions before the test (or any user) can call them.
+-- The fresh CREATE EXTENSION at step 35 keeps the GUC at its default,
+-- so any inconsistency between HEAD's SQL and HEAD's age.so is still
+-- caught at install time on the production code path.
+SET check_function_bodies = off;
DO $$
DECLARE init_ver text;
BEGIN
@@ -147,6 +234,7 @@ BEGIN
EXECUTE format('CREATE EXTENSION age VERSION %L', init_ver);
END;
$$;
+RESET check_function_bodies;
-- Step 11: Upgrade to the current (default) version via the stamped template.
DO $$
@@ -167,16 +255,18 @@ SELECT installed_version = default_version AS
upgraded_to_current
FROM pg_available_extensions WHERE name = 'age';
-- =====================================================================
--- UPGRADED INSTALL SNAPSHOTS (Steps 13-18)
+-- UPGRADED INSTALL SNAPSHOTS (Steps 13-18b)
-- Capture the catalog state after upgrade from initial to current.
-- =====================================================================
--- Step 13: Snapshot functions.
+-- Step 13: Snapshot functions (probin/prosrc included; see step 2).
CREATE TEMP TABLE _upgraded_funcs AS
SELECT proname::text,
pg_get_function_identity_arguments(oid) AS args,
provolatile::text, proisstrict::text, prokind::text,
- prorettype::regtype::text AS rettype, proretset::text
+ prorettype::regtype::text AS rettype, proretset::text,
+ COALESCE(probin, '') AS probin,
+ COALESCE(prosrc, '') AS prosrc
FROM pg_proc
WHERE pronamespace = 'ag_catalog'::regnamespace
ORDER BY proname, args;
@@ -233,8 +323,55 @@ FROM pg_constraint
WHERE connamespace = 'ag_catalog'::regnamespace
ORDER BY conname;
+-- Step 18b: Snapshot extension membership after upgrade (see step 7b).
+CREATE TEMP TABLE _upgraded_extmembers AS
+SELECT
+ CASE d.classid
+ WHEN 'pg_proc'::regclass
+ THEN 'function: ' || d.objid::regprocedure::text
+ WHEN 'pg_type'::regclass
+ THEN 'type: ' || d.objid::regtype::text
+ WHEN 'pg_class'::regclass
+ THEN 'relation: ' || d.objid::regclass::text
+ || ' (' || (SELECT relkind::text FROM pg_class WHERE oid =
d.objid) || ')'
+ WHEN 'pg_operator'::regclass
+ THEN 'operator: ' || d.objid::regoperator::text
+ WHEN 'pg_cast'::regclass
+ THEN 'cast: ' || (SELECT castsource::regtype::text || ' -> '
|| casttarget::regtype::text
+ FROM pg_cast WHERE oid = d.objid)
+ WHEN 'pg_opclass'::regclass
+ THEN 'opclass: ' || (SELECT opcname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opcmethod) || ')'
+ FROM pg_opclass WHERE oid = d.objid)
+ WHEN 'pg_constraint'::regclass
+ THEN 'constraint: ' || (SELECT conname || ' on ' ||
conrelid::regclass::text
+ FROM pg_constraint WHERE oid = d.objid)
+ WHEN 'pg_opfamily'::regclass
+ THEN 'opfamily: ' || (SELECT opfname || ' (' || (SELECT amname
FROM pg_am WHERE oid = opfmethod) || ')'
+ FROM pg_opfamily WHERE oid = d.objid)
+ WHEN 'pg_amop'::regclass
+ THEN 'amop: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = a.amopfamily)
+ || ' [strategy ' || a.amopstrategy ||
'] '
+ || a.amoplefttype::regtype::text || ','
+ || a.amoprighttype::regtype::text || '
op '
+ || a.amopopr::regoperator::text
+ FROM pg_amop a WHERE a.oid = d.objid)
+ WHEN 'pg_amproc'::regclass
+ THEN 'amproc: ' || (SELECT (SELECT opfname FROM pg_opfamily
WHERE oid = p.amprocfamily)
+ || ' [proc ' || p.amprocnum || '] '
+ || p.amproclefttype::regtype::text ||
','
+ || p.amprocrighttype::regtype::text ||
' fn '
+ || p.amproc::regprocedure::text
+ FROM pg_amproc p WHERE p.oid = d.objid)
+ ELSE 'unhandled[' || d.classid::regclass::text || ']'
+ END AS member
+FROM pg_depend d
+WHERE d.deptype = 'e'
+ AND d.refclassid = 'pg_extension'::regclass
+ AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'age')
+ORDER BY 1;
+
-- =====================================================================
--- COMPARISON: Missing or extra objects (Steps 19-33)
+-- COMPARISON: Missing or extra objects (Steps 19-33b)
-- Any rows returned indicate a template deficiency.
-- =====================================================================
@@ -252,13 +389,20 @@ LEFT JOIN _fresh_funcs f USING (proname, args)
WHERE f.proname IS NULL
ORDER BY 1;
--- Step 21: Function PROPERTY changes (volatility, strictness, kind, return
type).
+-- Step 21: Function PROPERTY changes
+-- (kind, volatility, strictness, return type, return-set, binding,
+-- body/symbol). The probin/prosrc check catches:
+-- * a C function whose symbol was renamed in the upgrade template
+-- * a SQL/plpgsql function whose body was changed in either path
+-- * a language change between the fresh and upgrade installs.
SELECT f.proname || '(' || f.args || ')' AS function_name,
- CASE WHEN f.prokind <> u.prokind THEN 'prokind: ' || f.prokind
|| '->' || u.prokind END AS kind_change,
- CASE WHEN f.provolatile<> u.provolatile THEN 'volatile: ' ||
f.provolatile|| '->' || u.provolatile END AS volatility_change,
- CASE WHEN f.proisstrict<> u.proisstrict THEN 'strict: ' ||
f.proisstrict|| '->' || u.proisstrict END AS strict_change,
- CASE WHEN f.rettype <> u.rettype THEN 'rettype: ' || f.rettype
|| '->' || u.rettype END AS rettype_change,
- CASE WHEN f.proretset <> u.proretset THEN 'retset: ' ||
f.proretset || '->' || u.proretset END AS retset_change
+ CASE WHEN f.prokind <> u.prokind THEN 'prokind: ' ||
f.prokind || '->' || u.prokind END AS kind_change,
+ CASE WHEN f.provolatile <> u.provolatile THEN 'volatile: ' ||
f.provolatile || '->' || u.provolatile END AS volatility_change,
+ CASE WHEN f.proisstrict <> u.proisstrict THEN 'strict: ' ||
f.proisstrict || '->' || u.proisstrict END AS strict_change,
+ CASE WHEN f.rettype <> u.rettype THEN 'rettype: ' ||
f.rettype || '->' || u.rettype END AS rettype_change,
+ CASE WHEN f.proretset <> u.proretset THEN 'retset: ' ||
f.proretset || '->' || u.proretset END AS retset_change,
+ CASE WHEN f.probin <> u.probin THEN 'probin: ' ||
f.probin || '->' || u.probin END AS probin_change,
+ CASE WHEN f.prosrc <> u.prosrc THEN 'prosrc changed' END
AS prosrc_change
FROM _fresh_funcs f
JOIN _upgraded_funcs u USING (proname, args)
WHERE f.provolatile <> u.provolatile
@@ -266,6 +410,8 @@ WHERE f.provolatile <> u.provolatile
OR f.prokind <> u.prokind
OR f.rettype <> u.rettype
OR f.proretset <> u.proretset
+ OR f.probin <> u.probin
+ OR f.prosrc <> u.prosrc
ORDER BY 1;
-- Step 22: Relations MISSING after upgrade.
@@ -352,6 +498,22 @@ LEFT JOIN _fresh_constraints f USING (conname, contype,
table_name)
WHERE f.conname IS NULL
ORDER BY 1;
+-- Step 33a: Extension members MISSING after upgrade
+-- (object exists in pg_proc/pg_class/etc. but is not linked to the AGE
+-- extension via pg_depend, i.e. ALTER EXTENSION age ADD ... was forgotten).
+SELECT f.member AS missing_extension_member
+FROM _fresh_extmembers f
+LEFT JOIN _upgraded_extmembers u USING (member)
+WHERE u.member IS NULL
+ORDER BY 1;
+
+-- Step 33b: Extension members EXTRA after upgrade.
+SELECT u.member AS extra_extension_member
+FROM _upgraded_extmembers u
+LEFT JOIN _fresh_extmembers f USING (member)
+WHERE f.member IS NULL
+ORDER BY 1;
+
-- =====================================================================
-- SUMMARY (Step 34)
-- =====================================================================
@@ -364,7 +526,8 @@ SELECT
(SELECT count(*) FROM _fresh_ops) = (SELECT count(*) FROM
_upgraded_ops) AS ops_match,
(SELECT count(*) FROM _fresh_casts) = (SELECT count(*) FROM
_upgraded_casts) AS casts_match,
(SELECT count(*) FROM _fresh_opclass) = (SELECT count(*) FROM
_upgraded_opclass) AS opclass_match,
- (SELECT count(*) FROM _fresh_constraints) = (SELECT count(*) FROM
_upgraded_constraints) AS constraints_match;
+ (SELECT count(*) FROM _fresh_constraints) = (SELECT count(*) FROM
_upgraded_constraints) AS constraints_match,
+ (SELECT count(*) FROM _fresh_extmembers) = (SELECT count(*) FROM
_upgraded_extmembers) AS extmembers_match;
-- =====================================================================
-- CLEANUP (Steps 35-36)
@@ -374,7 +537,8 @@ SELECT
DROP TABLE _fresh_funcs, _upgraded_funcs, _fresh_rels, _upgraded_rels,
_fresh_types, _upgraded_types, _fresh_ops, _upgraded_ops,
_fresh_casts, _upgraded_casts, _fresh_opclass, _upgraded_opclass,
- _fresh_constraints, _upgraded_constraints;
+ _fresh_constraints, _upgraded_constraints,
+ _fresh_extmembers, _upgraded_extmembers;
DROP EXTENSION age;
CREATE EXTENSION age;