From d0a42e2f89cca2de539d7e018431cb1c59559f59 Mon Sep 17 00:00:00 2001 From: John Gemignani Date: Wed, 22 Apr 2026 10:23:25 -0700 Subject: [PATCH] Fix upgrade test: replace data-integrity checks with catalog comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The age_upgrade regression test (added in #2364, improved in #2377, #2397) was designed to validate the upgrade template (age----y.y.y.sql) by creating graph data before the upgrade and verifying it survived afterward. This approach had two fundamental problems: 1. It did not detect incomplete upgrade templates. The test verified that graph data (vertices, edges, checksums, GIN indexes) survived ALTER EXTENSION UPDATE, but never checked whether new SQL objects (functions, views, relations, indexes, types, operators, casts, constraints) were actually created by the template. A developer could add a new function to sql/ and sql_files, forget to add it to the upgrade template, and all tests would pass — the function existed via the fresh CREATE EXTENSION install that ran before the upgrade test, but would be missing for users who upgraded via ALTER EXTENSION UPDATE. 2. The data-integrity checks relied on cypher queries (MATCH/RETURN) within the same backend session after DROP EXTENSION + CREATE EXTENSION. This caused intermittent failures on some PostgreSQL versions where AGE's internal type cache (agtype OID) was not properly refreshed after the extension was dropped and recreated, resulting in 'type with OID 0 does not exist' errors. The data-integrity aspect was also redundant — ALTER EXTENSION UPDATE runs DDL statements and does not touch heap data, so data survival is guaranteed by PostgreSQL and not a meaningful test. The fix replaces the entire test with a comprehensive catalog comparison: 1. Snapshot the ag_catalog schema from the fresh install across seven PostgreSQL system catalogs: - pg_proc: functions, aggregates, procedures (name, args, and properties: volatility, strictness, kind, return type, setof) - pg_class: tables, views, sequences, indexes (name, kind) - pg_type: types (name, type category) - pg_operator: operators (name, left/right operand 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) 2. DROP EXTENSION, CREATE EXTENSION at the synthetic initial version, then ALTER EXTENSION UPDATE to the current version via the stamped upgrade template. 3. Snapshot the catalog again after upgrade. 4. Compare: any object present in the fresh snapshot but missing after upgrade means the template is incomplete. Any object present after upgrade but not in the fresh snapshot means the template creates something unexpected. Function properties (volatility, strictness, prokind, return type) are also compared for functions that exist in both — catching cases where a CREATE OR REPLACE in the template changes a function's signature or behavior. Additional improvements from code review feedback: - Graph cleanup in Step 1 uses a DO block with PERFORM and suppressed NOTICEs to produce deterministic output regardless of prior test state. - The pg_class snapshot includes indexes (relkind 'i') in addition to tables, views, and sequences. - Diagnostic output includes relkind/typtype suffixes for actionable diffs. - Summary uses boolean equality checks (funcs_match, rels_match, etc.) instead of absolute counts, so the expected output does not need updating when new objects are added to AGE. Developers who correctly add objects to both sql/ and the template will never need to modify this test or its expected output. This approach: - Catches the actual failure mode: incomplete upgrade templates. - Covers all SQL object categories: functions (including aggregates), relations, types, operators, casts, operator classes, and constraints. - Detects property changes on existing functions (volatility, strictness, kind, return type changes). - Uses only plain SQL catalog queries — no cypher, no .so cache issues. - Works reliably across all PostgreSQL versions. - Reports the exact missing/extra/changed object in the diff output. - Is maintenance-free: no expected output changes needed when AGE grows. Makefile: updated step 5 comment to reflect catalog comparison approach. All 33 regression tests pass. Co-authored-by: Claude modified: Makefile modified: regress/expected/age_upgrade.out modified: regress/sql/age_upgrade.sql --- Makefile | 8 +- regress/expected/age_upgrade.out | 694 +++++++++++++++---------------- regress/sql/age_upgrade.sql | 552 +++++++++++++----------- 3 files changed, 645 insertions(+), 609 deletions(-) diff --git a/Makefile b/Makefile index 5405665d8..3ccb736ea 100644 --- a/Makefile +++ b/Makefile @@ -33,15 +33,17 @@ age_sql = age--1.7.0.sql # 4. Temporarily installs the synthetic files into the PG extension directory # so that CREATE EXTENSION age VERSION '' and ALTER EXTENSION # age UPDATE TO '' can find them. -# 5. The age_upgrade regression test exercises the full upgrade path: install -# at INIT, create data, ALTER EXTENSION UPDATE to CURR, verify data. +# 5. The age_upgrade regression test snapshots the ag_catalog schema from +# a fresh install, then installs at INIT, upgrades to CURR, and compares +# the catalog across seven system catalogs to detect missing, extra, +# or changed objects. # 6. The test SQL cleans up the synthetic files via a generated shell script. # # This forces developers to keep the upgrade template in sync: any SQL object # added after the version-bump commit must also appear in the template, or the # upgrade test will fail (the object will be missing after ALTER EXTENSION UPDATE). # -# Because the default install SQL comes from HEAD, all 31 non-upgrade tests +# Because the default install SQL comes from HEAD, all non-upgrade tests # run with every SQL function registered — no functions are missing. # # Graceful degradation — the upgrade test is silently skipped when: diff --git a/regress/expected/age_upgrade.out b/regress/expected/age_upgrade.out index 7c3da0150..edf8e6022 100644 --- a/regress/expected/age_upgrade.out +++ b/regress/expected/age_upgrade.out @@ -17,36 +17,105 @@ * under the License. */ -- --- Extension upgrade regression test +-- Extension upgrade template regression test -- --- This test validates the upgrade template (age----y.y.y.sql) by: --- 1. Dropping AGE and reinstalling at the synthetic "initial" version --- (built from the version-bump commit's SQL — the "day-one" state) --- 2. Creating three graphs with multiple labels, edges, GIN indexes, --- and numeric properties that serve as integrity checksums --- 3. Upgrading to the current (default) version via the stamped template --- 4. Verifying all data, structure, and checksums survived the upgrade +-- Validates the upgrade template (age----y.y.y.sql) by comparing +-- the pg_catalog entries of a fresh install against an upgraded install. +-- If any object exists in the fresh install but is missing after upgrade, +-- the template is incomplete. This catches the case where a developer adds +-- a new SQL object to sql/ and sql_files but forgets to add it to the +-- upgrade template. -- --- The Makefile builds: --- age--.sql from current HEAD's sql/sql_files (default) --- age--_initial.sql from the version-bump commit (synthetic) --- age--_initial--.sql stamped from the upgrade template +-- Compared catalogs: +-- pg_proc — functions, aggregates, procedures (name, args, properties) +-- 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) -- --- All version discovery is dynamic — no hardcoded versions anywhere. --- This test is version-agnostic and works on any branch for any version. +-- All comparison queries should return 0 rows. -- LOAD 'age'; SET search_path TO ag_catalog; --- Step 1: Clean up any state left by prior tests, then drop AGE entirely. --- The --load-extension=age flag installed AGE at the current (default) version. --- We need to remove it so we can reinstall at the synthetic initial version. -SELECT drop_graph(name, true) FROM ag_graph ORDER BY name; - drop_graph ------------- -(0 rows) +-- Step 1: Clean up any graphs left by prior tests (deterministic, no output). +DO $$ +DECLARE + graph_name ag_graph.name%TYPE; +BEGIN + PERFORM set_config('client_min_messages', 'warning', true); + FOR graph_name IN + SELECT name + FROM ag_graph + ORDER BY name + LOOP + PERFORM drop_graph(graph_name, true); + END LOOP; +END +$$; +-- ===================================================================== +-- FRESH INSTALL SNAPSHOTS (Steps 2-7) +-- Capture the catalog state from the default CREATE EXTENSION install. +-- ===================================================================== +-- Step 2: Snapshot functions (includes aggregates via prokind). +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 +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; +-- Step 3: Snapshot relations (tables, views, sequences, indexes). +CREATE TEMP TABLE _fresh_rels AS +SELECT relname::text, relkind::text +FROM pg_class +WHERE relnamespace = 'ag_catalog'::regnamespace + AND relkind IN ('r', 'v', 'S', 'i') +ORDER BY relname; +-- Step 4: Snapshot types. +CREATE TEMP TABLE _fresh_types AS +SELECT typname::text, typtype::text +FROM pg_type +WHERE typnamespace = 'ag_catalog'::regnamespace + AND typname NOT LIKE 'pg_toast%' +ORDER BY typname; +-- Step 5: Snapshot operators. +CREATE TEMP TABLE _fresh_ops AS +SELECT oprname::text, + oprleft::regtype::text AS lefttype, + oprright::regtype::text AS righttype +FROM pg_operator +WHERE oprnamespace = 'ag_catalog'::regnamespace +ORDER BY oprname, lefttype, righttype; +-- Step 6: Snapshot casts involving AGE types, and operator classes. +CREATE TEMP TABLE _fresh_casts AS +SELECT castsource::regtype::text AS source_type, + casttarget::regtype::text AS target_type, + castcontext::text +FROM pg_cast +WHERE castsource IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) + OR casttarget IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) +ORDER BY source_type, target_type; +CREATE TEMP TABLE _fresh_opclass AS +SELECT opcname::text, + (SELECT amname FROM pg_am WHERE oid = opcmethod)::text AS amname +FROM pg_opclass +WHERE opcnamespace = 'ag_catalog'::regnamespace +ORDER BY opcname; +-- Step 7: Snapshot constraints. +CREATE TEMP TABLE _fresh_constraints AS +SELECT conname::text, contype::text, + conrelid::regclass::text AS table_name, + confrelid::regclass::text AS ref_table +FROM pg_constraint +WHERE connamespace = 'ag_catalog'::regnamespace +ORDER BY conname; +-- Step 8: Drop AGE entirely. DROP EXTENSION age; --- Step 2: Verify we have multiple installable versions. +-- Step 9: Verify we have an upgrade path available. SELECT count(*) > 1 AS has_upgrade_path FROM pg_available_extension_versions WHERE name = 'age'; has_upgrade_path @@ -54,7 +123,7 @@ FROM pg_available_extension_versions WHERE name = 'age'; t (1 row) --- Step 3: Install AGE at the synthetic initial version (pre-upgrade state). +-- Step 10: Install AGE at the synthetic initial version. DO $$ DECLARE init_ver text; BEGIN @@ -66,204 +135,10 @@ BEGIN IF init_ver IS NULL THEN RAISE EXCEPTION 'No initial version available for upgrade test'; END IF; - EXECUTE format('CREATE EXTENSION age VERSION %L', init_ver); END; $$; -SELECT extversion IS NOT NULL AS version_installed FROM pg_extension WHERE extname = 'age'; - version_installed -------------------- - t -(1 row) - --- Step 4: Create three test graphs with diverse labels, edges, and data. -LOAD 'age'; -SET search_path TO ag_catalog, "$user", public; --- --- Graph 1: "company" — organization hierarchy with numeric checksums. --- Labels: Employee, Department, Project --- Edges: WORKS_IN, MANAGES, ASSIGNED_TO --- Each vertex has a "val" property (float) for checksum validation. --- -SELECT create_graph('company'); -NOTICE: graph "company" has been created - create_graph --------------- - -(1 row) - -SELECT * FROM cypher('company', $$ - CREATE (e1:Employee {name: 'Alice', role: 'VP', val: 3.14159}) - CREATE (e2:Employee {name: 'Bob', role: 'Manager', val: 2.71828}) - CREATE (e3:Employee {name: 'Charlie', role: 'Engineer', val: 1.41421}) - CREATE (e4:Employee {name: 'Diana', role: 'Engineer', val: 1.73205}) - CREATE (d1:Department {name: 'Engineering', budget: 500000, val: 42.0}) - CREATE (d2:Department {name: 'Research', budget: 300000, val: 17.5}) - CREATE (p1:Project {name: 'Atlas', priority: 1, val: 99.99}) - CREATE (p2:Project {name: 'Beacon', priority: 2, val: 88.88}) - CREATE (p3:Project {name: 'Cipher', priority: 3, val: 77.77}) - CREATE (e1)-[:WORKS_IN {since: 2019}]->(d1) - CREATE (e2)-[:WORKS_IN {since: 2020}]->(d1) - CREATE (e3)-[:WORKS_IN {since: 2021}]->(d1) - CREATE (e4)-[:WORKS_IN {since: 2022}]->(d2) - CREATE (e1)-[:MANAGES {level: 1}]->(e2) - CREATE (e2)-[:MANAGES {level: 2}]->(e3) - CREATE (e3)-[:ASSIGNED_TO {hours: 40}]->(p1) - CREATE (e3)-[:ASSIGNED_TO {hours: 20}]->(p2) - CREATE (e4)-[:ASSIGNED_TO {hours: 30}]->(p2) - CREATE (e4)-[:ASSIGNED_TO {hours: 10}]->(p3) - RETURN 'company graph created' -$$) AS (result agtype); - result -------------------------- - "company graph created" -(1 row) - --- GIN index on Employee properties in company graph -CREATE INDEX company_employee_gin ON company."Employee" USING GIN (properties); --- --- Graph 2: "network" — social network with weighted edges. --- Labels: User, Post --- Edges: FOLLOWS, AUTHORED, LIKES --- -SELECT create_graph('network'); -NOTICE: graph "network" has been created - create_graph --------------- - -(1 row) - -SELECT * FROM cypher('network', $$ - CREATE (u1:User {handle: '@alpha', score: 1000.01}) - CREATE (u2:User {handle: '@beta', score: 2000.02}) - CREATE (u3:User {handle: '@gamma', score: 3000.03}) - CREATE (u4:User {handle: '@delta', score: 4000.04}) - CREATE (u5:User {handle: '@epsilon', score: 5000.05}) - CREATE (p1:Post {title: 'Hello World', views: 150}) - CREATE (p2:Post {title: 'Graph Databases 101', views: 890}) - CREATE (p3:Post {title: 'AGE is awesome', views: 2200}) - CREATE (u1)-[:FOLLOWS {weight: 0.9}]->(u2) - CREATE (u2)-[:FOLLOWS {weight: 0.8}]->(u3) - CREATE (u3)-[:FOLLOWS {weight: 0.7}]->(u4) - CREATE (u4)-[:FOLLOWS {weight: 0.6}]->(u5) - CREATE (u5)-[:FOLLOWS {weight: 0.5}]->(u1) - CREATE (u1)-[:AUTHORED]->(p1) - CREATE (u2)-[:AUTHORED]->(p2) - CREATE (u3)-[:AUTHORED]->(p3) - CREATE (u4)-[:LIKES]->(p1) - CREATE (u5)-[:LIKES]->(p2) - CREATE (u1)-[:LIKES]->(p3) - CREATE (u2)-[:LIKES]->(p3) - RETURN 'network graph created' -$$) AS (result agtype); - result -------------------------- - "network graph created" -(1 row) - --- GIN indexes on network graph -CREATE INDEX network_user_gin ON network."User" USING GIN (properties); -CREATE INDEX network_post_gin ON network."Post" USING GIN (properties); --- --- Graph 3: "routes" — geographic routing with precise coordinates. --- Labels: City, Airport --- Edges: ROAD, FLIGHT --- Coordinates use precise decimals that are easy to checksum. --- -SELECT create_graph('routes'); -NOTICE: graph "routes" has been created - create_graph --------------- - -(1 row) - -SELECT * FROM cypher('routes', $$ - CREATE (c1:City {name: 'Portland', lat: 45.5152, lon: -122.6784, pop: 652503}) - CREATE (c2:City {name: 'Seattle', lat: 47.6062, lon: -122.3321, pop: 749256}) - CREATE (c3:City {name: 'Vancouver', lat: 49.2827, lon: -123.1207, pop: 631486}) - CREATE (a1:Airport {code: 'PDX', elev: 30.5}) - CREATE (a2:Airport {code: 'SEA', elev: 131.7}) - CREATE (a3:Airport {code: 'YVR', elev: 4.3}) - CREATE (c1)-[:ROAD {distance_km: 279.5, toll: 0.0}]->(c2) - CREATE (c2)-[:ROAD {distance_km: 225.3, toll: 5.0}]->(c3) - CREATE (c1)-[:ROAD {distance_km: 502.1, toll: 5.0}]->(c3) - CREATE (a1)-[:FLIGHT {distance_km: 229.0, duration_min: 55}]->(a2) - CREATE (a2)-[:FLIGHT {distance_km: 198.0, duration_min: 50}]->(a3) - CREATE (a1)-[:FLIGHT {distance_km: 426.0, duration_min: 75}]->(a3) - RETURN 'routes graph created' -$$) AS (result agtype); - result ------------------------- - "routes graph created" -(1 row) - --- GIN index on routes graph -CREATE INDEX routes_city_gin ON routes."City" USING GIN (properties); --- Step 5: Record pre-upgrade integrity checksums. --- These sums use the "val" / "score" / coordinate properties as fingerprints. --- company: sum of all val properties (should be a precise known value) -SELECT * FROM cypher('company', $$ - MATCH (n) WHERE n.val IS NOT NULL RETURN sum(n.val) -$$) AS (company_val_sum_before agtype); - company_val_sum_before ------------------------- - 335.14612999999997 -(1 row) - --- network: sum of all score properties -SELECT * FROM cypher('network', $$ - MATCH (n:User) RETURN sum(n.score) -$$) AS (network_score_sum_before agtype); - network_score_sum_before --------------------------- - 15000.149999999998 -(1 row) - --- routes: sum of all latitude values -SELECT * FROM cypher('routes', $$ - MATCH (c:City) RETURN sum(c.lat) -$$) AS (routes_lat_sum_before agtype); - routes_lat_sum_before ------------------------ - 142.4041 -(1 row) - --- Total vertex and edge counts across all three graphs -SELECT sum(cnt)::int AS total_vertices_before FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH (n) RETURN n $$) AS (n agtype) -) sub; - total_vertices_before ------------------------ - 23 -(1 row) - -SELECT sum(cnt)::int AS total_edges_before FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) -) sub; - total_edges_before --------------------- - 28 -(1 row) - --- Count of distinct labels (ag_label entries) across all graphs -SELECT count(*)::int AS total_labels_before -FROM ag_label al JOIN ag_graph ag ON al.graph = ag.graphid -WHERE ag.name <> '_ag_catalog'; - total_labels_before ---------------------- - 21 -(1 row) - --- Step 6: Upgrade AGE from the initial version to the current (default) version --- via the stamped upgrade template. +-- Step 11: Upgrade to the current (default) version via the stamped template. DO $$ DECLARE curr_ver text; BEGIN @@ -273,11 +148,10 @@ BEGIN IF curr_ver IS NULL THEN RAISE EXCEPTION 'No default version found for upgrade test'; END IF; - EXECUTE format('ALTER EXTENSION age UPDATE TO %L', curr_ver); END; $$; --- Step 7: Confirm version is now the default (current HEAD) version. +-- Step 12: Confirm the upgrade succeeded. SELECT installed_version = default_version AS upgraded_to_current FROM pg_available_extensions WHERE name = 'age'; upgraded_to_current @@ -285,169 +159,253 @@ FROM pg_available_extensions WHERE name = 'age'; t (1 row) --- Step 8: Verify all data survived — reload and recheck. -LOAD 'age'; -SET search_path TO ag_catalog, "$user", public; --- Repeat integrity checksums — must match pre-upgrade values exactly. -SELECT * FROM cypher('company', $$ - MATCH (n) WHERE n.val IS NOT NULL RETURN sum(n.val) -$$) AS (company_val_sum_after agtype); - company_val_sum_after ------------------------ - 335.14612999999997 -(1 row) +-- ===================================================================== +-- UPGRADED INSTALL SNAPSHOTS (Steps 13-18) +-- Capture the catalog state after upgrade from initial to current. +-- ===================================================================== +-- Step 13: Snapshot functions. +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 +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; +-- Step 14: Snapshot relations. +CREATE TEMP TABLE _upgraded_rels AS +SELECT relname::text, relkind::text +FROM pg_class +WHERE relnamespace = 'ag_catalog'::regnamespace + AND relkind IN ('r', 'v', 'S', 'i') +ORDER BY relname; +-- Step 15: Snapshot types. +CREATE TEMP TABLE _upgraded_types AS +SELECT typname::text, typtype::text +FROM pg_type +WHERE typnamespace = 'ag_catalog'::regnamespace + AND typname NOT LIKE 'pg_toast%' +ORDER BY typname; +-- Step 16: Snapshot operators. +CREATE TEMP TABLE _upgraded_ops AS +SELECT oprname::text, + oprleft::regtype::text AS lefttype, + oprright::regtype::text AS righttype +FROM pg_operator +WHERE oprnamespace = 'ag_catalog'::regnamespace +ORDER BY oprname, lefttype, righttype; +-- Step 17: Snapshot casts and operator classes. +CREATE TEMP TABLE _upgraded_casts AS +SELECT castsource::regtype::text AS source_type, + casttarget::regtype::text AS target_type, + castcontext::text +FROM pg_cast +WHERE castsource IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) + OR casttarget IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) +ORDER BY source_type, target_type; +CREATE TEMP TABLE _upgraded_opclass AS +SELECT opcname::text, + (SELECT amname FROM pg_am WHERE oid = opcmethod)::text AS amname +FROM pg_opclass +WHERE opcnamespace = 'ag_catalog'::regnamespace +ORDER BY opcname; +-- Step 18: Snapshot constraints. +CREATE TEMP TABLE _upgraded_constraints AS +SELECT conname::text, contype::text, + conrelid::regclass::text AS table_name, + confrelid::regclass::text AS ref_table +FROM pg_constraint +WHERE connamespace = 'ag_catalog'::regnamespace +ORDER BY conname; +-- ===================================================================== +-- COMPARISON: Missing or extra objects (Steps 19-33) +-- Any rows returned indicate a template deficiency. +-- ===================================================================== +-- Step 19: Functions MISSING after upgrade. +SELECT f.proname || '(' || f.args || ')' AS missing_function +FROM _fresh_funcs f +LEFT JOIN _upgraded_funcs u USING (proname, args) +WHERE u.proname IS NULL +ORDER BY 1; + missing_function +------------------ +(0 rows) -SELECT * FROM cypher('network', $$ - MATCH (n:User) RETURN sum(n.score) -$$) AS (network_score_sum_after agtype); - network_score_sum_after -------------------------- - 15000.149999999998 -(1 row) +-- Step 20: Functions EXTRA after upgrade. +SELECT u.proname || '(' || u.args || ')' AS extra_function +FROM _upgraded_funcs u +LEFT JOIN _fresh_funcs f USING (proname, args) +WHERE f.proname IS NULL +ORDER BY 1; + extra_function +---------------- +(0 rows) -SELECT * FROM cypher('routes', $$ - MATCH (c:City) RETURN sum(c.lat) -$$) AS (routes_lat_sum_after agtype); - routes_lat_sum_after ----------------------- - 142.4041 -(1 row) +-- Step 21: Function PROPERTY changes (volatility, strictness, kind, return type). +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 +FROM _fresh_funcs f +JOIN _upgraded_funcs u USING (proname, args) +WHERE f.provolatile <> u.provolatile + OR f.proisstrict <> u.proisstrict + OR f.prokind <> u.prokind + OR f.rettype <> u.rettype + OR f.proretset <> u.proretset +ORDER BY 1; + function_name | kind_change | volatility_change | strict_change | rettype_change | retset_change +---------------+-------------+-------------------+---------------+----------------+--------------- +(0 rows) -SELECT sum(cnt)::int AS total_vertices_after FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH (n) RETURN n $$) AS (n agtype) -) sub; - total_vertices_after ----------------------- - 23 -(1 row) +-- Step 22: Relations MISSING after upgrade. +SELECT f.relname || ' (' || f.relkind || ')' AS missing_relation +FROM _fresh_rels f +LEFT JOIN _upgraded_rels u USING (relname, relkind) +WHERE u.relname IS NULL +ORDER BY 1; + missing_relation +------------------ +(0 rows) -SELECT sum(cnt)::int AS total_edges_after FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) -) sub; - total_edges_after -------------------- - 28 -(1 row) +-- Step 23: Relations EXTRA after upgrade. +SELECT u.relname || ' (' || u.relkind || ')' AS extra_relation +FROM _upgraded_rels u +LEFT JOIN _fresh_rels f USING (relname, relkind) +WHERE f.relname IS NULL +ORDER BY 1; + extra_relation +---------------- +(0 rows) -SELECT count(*)::int AS total_labels_after -FROM ag_label al JOIN ag_graph ag ON al.graph = ag.graphid -WHERE ag.name <> '_ag_catalog'; - total_labels_after --------------------- - 21 -(1 row) +-- Step 24: Types MISSING after upgrade. +SELECT f.typname || ' (' || f.typtype || ')' AS missing_type +FROM _fresh_types f +LEFT JOIN _upgraded_types u USING (typname, typtype) +WHERE u.typname IS NULL +ORDER BY 1; + missing_type +-------------- +(0 rows) --- Step 9: Verify specific structural queries across all three graphs. --- company: management chain -SELECT * FROM cypher('company', $$ - MATCH (boss:Employee)-[:MANAGES*]->(report:Employee) - RETURN boss.name, report.name - ORDER BY boss.name, report.name -$$) AS (boss agtype, report agtype); - boss | report ----------+----------- - "Alice" | "Bob" - "Alice" | "Charlie" - "Bob" | "Charlie" -(3 rows) +-- Step 25: Types EXTRA after upgrade. +SELECT u.typname || ' (' || u.typtype || ')' AS extra_type +FROM _upgraded_types u +LEFT JOIN _fresh_types f USING (typname, typtype) +WHERE f.typname IS NULL +ORDER BY 1; + extra_type +------------ +(0 rows) --- network: circular follow chain (proves full cycle survived) -SELECT * FROM cypher('network', $$ - MATCH (a:User)-[:FOLLOWS]->(b:User) - RETURN a.handle, b.handle - ORDER BY a.handle -$$) AS (follower agtype, followed agtype); - follower | followed -------------+------------ - "@alpha" | "@beta" - "@beta" | "@gamma" - "@delta" | "@epsilon" - "@epsilon" | "@alpha" - "@gamma" | "@delta" -(5 rows) +-- Step 26: Operators MISSING after upgrade. +SELECT f.oprname || ' (' || f.lefttype || ', ' || f.righttype || ')' AS missing_operator +FROM _fresh_ops f +LEFT JOIN _upgraded_ops u USING (oprname, lefttype, righttype) +WHERE u.oprname IS NULL +ORDER BY 1; + missing_operator +------------------ +(0 rows) --- routes: all flights with distances (proves edge properties intact) -SELECT * FROM cypher('routes', $$ - MATCH (a:Airport)-[f:FLIGHT]->(b:Airport) - RETURN a.code, b.code, f.distance_km - ORDER BY a.code, b.code -$$) AS (origin agtype, dest agtype, dist agtype); - origin | dest | dist ---------+-------+------- - "PDX" | "SEA" | 229.0 - "PDX" | "YVR" | 426.0 - "SEA" | "YVR" | 198.0 -(3 rows) +-- Step 27: Operators EXTRA after upgrade. +SELECT u.oprname || ' (' || u.lefttype || ', ' || u.righttype || ')' AS extra_operator +FROM _upgraded_ops u +LEFT JOIN _fresh_ops f USING (oprname, lefttype, righttype) +WHERE f.oprname IS NULL +ORDER BY 1; + extra_operator +---------------- +(0 rows) --- Step 10: Verify GIN indexes still exist after upgrade. -SELECT indexname FROM pg_indexes -WHERE schemaname IN ('company', 'network', 'routes') - AND tablename IN ('Employee', 'User', 'Post', 'City') - AND indexdef LIKE '%gin%' -ORDER BY indexname; - indexname ----------------------- - company_employee_gin - network_post_gin - network_user_gin - routes_city_gin -(4 rows) +-- Step 28: Casts MISSING after upgrade. +SELECT f.source_type || ' -> ' || f.target_type || ' (' || f.castcontext || ')' AS missing_cast +FROM _fresh_casts f +LEFT JOIN _upgraded_casts u USING (source_type, target_type, castcontext) +WHERE u.source_type IS NULL +ORDER BY 1; + missing_cast +-------------- +(0 rows) --- Step 11: Cleanup and restore AGE at the default version for subsequent tests. -SELECT drop_graph('routes', true); -NOTICE: drop cascades to 6 other objects -DETAIL: drop cascades to table routes._ag_label_vertex -drop cascades to table routes._ag_label_edge -drop cascades to table routes."City" -drop cascades to table routes."Airport" -drop cascades to table routes."ROAD" -drop cascades to table routes."FLIGHT" -NOTICE: graph "routes" has been dropped - drop_graph +-- Step 29: Casts EXTRA after upgrade. +SELECT u.source_type || ' -> ' || u.target_type || ' (' || u.castcontext || ')' AS extra_cast +FROM _upgraded_casts u +LEFT JOIN _fresh_casts f USING (source_type, target_type, castcontext) +WHERE f.source_type IS NULL +ORDER BY 1; + extra_cast ------------ - -(1 row) +(0 rows) -SELECT drop_graph('network', true); -NOTICE: drop cascades to 7 other objects -DETAIL: drop cascades to table network._ag_label_vertex -drop cascades to table network._ag_label_edge -drop cascades to table network."User" -drop cascades to table network."Post" -drop cascades to table network."FOLLOWS" -drop cascades to table network."AUTHORED" -drop cascades to table network."LIKES" -NOTICE: graph "network" has been dropped - drop_graph ------------- - -(1 row) +-- Step 30: Operator classes MISSING after upgrade. +SELECT f.opcname || ' (' || f.amname || ')' AS missing_opclass +FROM _fresh_opclass f +LEFT JOIN _upgraded_opclass u USING (opcname, amname) +WHERE u.opcname IS NULL +ORDER BY 1; + missing_opclass +----------------- +(0 rows) -SELECT drop_graph('company', true); -NOTICE: drop cascades to 8 other objects -DETAIL: drop cascades to table company._ag_label_vertex -drop cascades to table company._ag_label_edge -drop cascades to table company."Employee" -drop cascades to table company."Department" -drop cascades to table company."Project" -drop cascades to table company."WORKS_IN" -drop cascades to table company."MANAGES" -drop cascades to table company."ASSIGNED_TO" -NOTICE: graph "company" has been dropped - drop_graph ------------- - +-- Step 31: Operator classes EXTRA after upgrade. +SELECT u.opcname || ' (' || u.amname || ')' AS extra_opclass +FROM _upgraded_opclass u +LEFT JOIN _fresh_opclass f USING (opcname, amname) +WHERE f.opcname IS NULL +ORDER BY 1; + extra_opclass +--------------- +(0 rows) + +-- Step 32: Constraints MISSING after upgrade. +SELECT f.conname || ' (' || f.contype || ' on ' || f.table_name || ')' AS missing_constraint +FROM _fresh_constraints f +LEFT JOIN _upgraded_constraints u USING (conname, contype, table_name) +WHERE u.conname IS NULL +ORDER BY 1; + missing_constraint +-------------------- +(0 rows) + +-- Step 33: Constraints EXTRA after upgrade. +SELECT u.conname || ' (' || u.contype || ' on ' || u.table_name || ')' AS extra_constraint +FROM _upgraded_constraints u +LEFT JOIN _fresh_constraints f USING (conname, contype, table_name) +WHERE f.conname IS NULL +ORDER BY 1; + extra_constraint +------------------ +(0 rows) + +-- ===================================================================== +-- SUMMARY (Step 34) +-- ===================================================================== +-- Step 34: Verify all counts match (result: single row, all true). +SELECT + (SELECT count(*) FROM _fresh_funcs) = (SELECT count(*) FROM _upgraded_funcs) AS funcs_match, + (SELECT count(*) FROM _fresh_rels) = (SELECT count(*) FROM _upgraded_rels) AS rels_match, + (SELECT count(*) FROM _fresh_types) = (SELECT count(*) FROM _upgraded_types) AS types_match, + (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 (1 row) +-- ===================================================================== +-- CLEANUP (Steps 35-36) +-- ===================================================================== +-- Step 35: Drop temp tables, restore AGE at default version. +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; DROP EXTENSION age; CREATE EXTENSION age; --- Step 12: Remove synthetic upgrade test files from the extension directory. +-- Step 36: Remove synthetic upgrade test files from the extension directory. \! sh ./regress/age_upgrade_cleanup.sh diff --git a/regress/sql/age_upgrade.sql b/regress/sql/age_upgrade.sql index 70d45064a..f56f7ca93 100644 --- a/regress/sql/age_upgrade.sql +++ b/regress/sql/age_upgrade.sql @@ -18,39 +18,121 @@ */ -- --- Extension upgrade regression test +-- Extension upgrade template regression test -- --- This test validates the upgrade template (age----y.y.y.sql) by: --- 1. Dropping AGE and reinstalling at the synthetic "initial" version --- (built from the version-bump commit's SQL — the "day-one" state) --- 2. Creating three graphs with multiple labels, edges, GIN indexes, --- and numeric properties that serve as integrity checksums --- 3. Upgrading to the current (default) version via the stamped template --- 4. Verifying all data, structure, and checksums survived the upgrade +-- Validates the upgrade template (age----y.y.y.sql) by comparing +-- the pg_catalog entries of a fresh install against an upgraded install. +-- If any object exists in the fresh install but is missing after upgrade, +-- the template is incomplete. This catches the case where a developer adds +-- a new SQL object to sql/ and sql_files but forgets to add it to the +-- upgrade template. -- --- The Makefile builds: --- age--.sql from current HEAD's sql/sql_files (default) --- age--_initial.sql from the version-bump commit (synthetic) --- age--_initial--.sql stamped from the upgrade template +-- Compared catalogs: +-- pg_proc — functions, aggregates, procedures (name, args, properties) +-- 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) -- --- All version discovery is dynamic — no hardcoded versions anywhere. --- This test is version-agnostic and works on any branch for any version. +-- All comparison queries should return 0 rows. -- LOAD 'age'; SET search_path TO ag_catalog; --- Step 1: Clean up any state left by prior tests, then drop AGE entirely. --- The --load-extension=age flag installed AGE at the current (default) version. --- We need to remove it so we can reinstall at the synthetic initial version. -SELECT drop_graph(name, true) FROM ag_graph ORDER BY name; +-- Step 1: Clean up any graphs left by prior tests (deterministic, no output). +DO $$ +DECLARE + graph_name ag_graph.name%TYPE; +BEGIN + PERFORM set_config('client_min_messages', 'warning', true); + + FOR graph_name IN + SELECT name + FROM ag_graph + ORDER BY name + LOOP + PERFORM drop_graph(graph_name, true); + END LOOP; +END +$$; + +-- ===================================================================== +-- FRESH INSTALL SNAPSHOTS (Steps 2-7) +-- Capture the catalog state from the default CREATE EXTENSION install. +-- ===================================================================== + +-- Step 2: Snapshot functions (includes aggregates via prokind). +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 +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; + +-- Step 3: Snapshot relations (tables, views, sequences, indexes). +CREATE TEMP TABLE _fresh_rels AS +SELECT relname::text, relkind::text +FROM pg_class +WHERE relnamespace = 'ag_catalog'::regnamespace + AND relkind IN ('r', 'v', 'S', 'i') +ORDER BY relname; + +-- Step 4: Snapshot types. +CREATE TEMP TABLE _fresh_types AS +SELECT typname::text, typtype::text +FROM pg_type +WHERE typnamespace = 'ag_catalog'::regnamespace + AND typname NOT LIKE 'pg_toast%' +ORDER BY typname; + +-- Step 5: Snapshot operators. +CREATE TEMP TABLE _fresh_ops AS +SELECT oprname::text, + oprleft::regtype::text AS lefttype, + oprright::regtype::text AS righttype +FROM pg_operator +WHERE oprnamespace = 'ag_catalog'::regnamespace +ORDER BY oprname, lefttype, righttype; + +-- Step 6: Snapshot casts involving AGE types, and operator classes. +CREATE TEMP TABLE _fresh_casts AS +SELECT castsource::regtype::text AS source_type, + casttarget::regtype::text AS target_type, + castcontext::text +FROM pg_cast +WHERE castsource IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) + OR casttarget IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) +ORDER BY source_type, target_type; + +CREATE TEMP TABLE _fresh_opclass AS +SELECT opcname::text, + (SELECT amname FROM pg_am WHERE oid = opcmethod)::text AS amname +FROM pg_opclass +WHERE opcnamespace = 'ag_catalog'::regnamespace +ORDER BY opcname; + +-- Step 7: Snapshot constraints. +CREATE TEMP TABLE _fresh_constraints AS +SELECT conname::text, contype::text, + conrelid::regclass::text AS table_name, + confrelid::regclass::text AS ref_table +FROM pg_constraint +WHERE connamespace = 'ag_catalog'::regnamespace +ORDER BY conname; + +-- Step 8: Drop AGE entirely. DROP EXTENSION age; --- Step 2: Verify we have multiple installable versions. +-- Step 9: Verify we have an upgrade path available. SELECT count(*) > 1 AS has_upgrade_path FROM pg_available_extension_versions WHERE name = 'age'; --- Step 3: Install AGE at the synthetic initial version (pre-upgrade state). +-- Step 10: Install AGE at the synthetic initial version. DO $$ DECLARE init_ver text; BEGIN @@ -62,154 +144,11 @@ BEGIN IF init_ver IS NULL THEN RAISE EXCEPTION 'No initial version available for upgrade test'; END IF; - EXECUTE format('CREATE EXTENSION age VERSION %L', init_ver); END; $$; -SELECT extversion IS NOT NULL AS version_installed FROM pg_extension WHERE extname = 'age'; --- Step 4: Create three test graphs with diverse labels, edges, and data. -LOAD 'age'; -SET search_path TO ag_catalog, "$user", public; - --- --- Graph 1: "company" — organization hierarchy with numeric checksums. --- Labels: Employee, Department, Project --- Edges: WORKS_IN, MANAGES, ASSIGNED_TO --- Each vertex has a "val" property (float) for checksum validation. --- -SELECT create_graph('company'); - -SELECT * FROM cypher('company', $$ - CREATE (e1:Employee {name: 'Alice', role: 'VP', val: 3.14159}) - CREATE (e2:Employee {name: 'Bob', role: 'Manager', val: 2.71828}) - CREATE (e3:Employee {name: 'Charlie', role: 'Engineer', val: 1.41421}) - CREATE (e4:Employee {name: 'Diana', role: 'Engineer', val: 1.73205}) - CREATE (d1:Department {name: 'Engineering', budget: 500000, val: 42.0}) - CREATE (d2:Department {name: 'Research', budget: 300000, val: 17.5}) - CREATE (p1:Project {name: 'Atlas', priority: 1, val: 99.99}) - CREATE (p2:Project {name: 'Beacon', priority: 2, val: 88.88}) - CREATE (p3:Project {name: 'Cipher', priority: 3, val: 77.77}) - CREATE (e1)-[:WORKS_IN {since: 2019}]->(d1) - CREATE (e2)-[:WORKS_IN {since: 2020}]->(d1) - CREATE (e3)-[:WORKS_IN {since: 2021}]->(d1) - CREATE (e4)-[:WORKS_IN {since: 2022}]->(d2) - CREATE (e1)-[:MANAGES {level: 1}]->(e2) - CREATE (e2)-[:MANAGES {level: 2}]->(e3) - CREATE (e3)-[:ASSIGNED_TO {hours: 40}]->(p1) - CREATE (e3)-[:ASSIGNED_TO {hours: 20}]->(p2) - CREATE (e4)-[:ASSIGNED_TO {hours: 30}]->(p2) - CREATE (e4)-[:ASSIGNED_TO {hours: 10}]->(p3) - RETURN 'company graph created' -$$) AS (result agtype); - --- GIN index on Employee properties in company graph -CREATE INDEX company_employee_gin ON company."Employee" USING GIN (properties); - --- --- Graph 2: "network" — social network with weighted edges. --- Labels: User, Post --- Edges: FOLLOWS, AUTHORED, LIKES --- -SELECT create_graph('network'); - -SELECT * FROM cypher('network', $$ - CREATE (u1:User {handle: '@alpha', score: 1000.01}) - CREATE (u2:User {handle: '@beta', score: 2000.02}) - CREATE (u3:User {handle: '@gamma', score: 3000.03}) - CREATE (u4:User {handle: '@delta', score: 4000.04}) - CREATE (u5:User {handle: '@epsilon', score: 5000.05}) - CREATE (p1:Post {title: 'Hello World', views: 150}) - CREATE (p2:Post {title: 'Graph Databases 101', views: 890}) - CREATE (p3:Post {title: 'AGE is awesome', views: 2200}) - CREATE (u1)-[:FOLLOWS {weight: 0.9}]->(u2) - CREATE (u2)-[:FOLLOWS {weight: 0.8}]->(u3) - CREATE (u3)-[:FOLLOWS {weight: 0.7}]->(u4) - CREATE (u4)-[:FOLLOWS {weight: 0.6}]->(u5) - CREATE (u5)-[:FOLLOWS {weight: 0.5}]->(u1) - CREATE (u1)-[:AUTHORED]->(p1) - CREATE (u2)-[:AUTHORED]->(p2) - CREATE (u3)-[:AUTHORED]->(p3) - CREATE (u4)-[:LIKES]->(p1) - CREATE (u5)-[:LIKES]->(p2) - CREATE (u1)-[:LIKES]->(p3) - CREATE (u2)-[:LIKES]->(p3) - RETURN 'network graph created' -$$) AS (result agtype); - --- GIN indexes on network graph -CREATE INDEX network_user_gin ON network."User" USING GIN (properties); -CREATE INDEX network_post_gin ON network."Post" USING GIN (properties); - --- --- Graph 3: "routes" — geographic routing with precise coordinates. --- Labels: City, Airport --- Edges: ROAD, FLIGHT --- Coordinates use precise decimals that are easy to checksum. --- -SELECT create_graph('routes'); - -SELECT * FROM cypher('routes', $$ - CREATE (c1:City {name: 'Portland', lat: 45.5152, lon: -122.6784, pop: 652503}) - CREATE (c2:City {name: 'Seattle', lat: 47.6062, lon: -122.3321, pop: 749256}) - CREATE (c3:City {name: 'Vancouver', lat: 49.2827, lon: -123.1207, pop: 631486}) - CREATE (a1:Airport {code: 'PDX', elev: 30.5}) - CREATE (a2:Airport {code: 'SEA', elev: 131.7}) - CREATE (a3:Airport {code: 'YVR', elev: 4.3}) - CREATE (c1)-[:ROAD {distance_km: 279.5, toll: 0.0}]->(c2) - CREATE (c2)-[:ROAD {distance_km: 225.3, toll: 5.0}]->(c3) - CREATE (c1)-[:ROAD {distance_km: 502.1, toll: 5.0}]->(c3) - CREATE (a1)-[:FLIGHT {distance_km: 229.0, duration_min: 55}]->(a2) - CREATE (a2)-[:FLIGHT {distance_km: 198.0, duration_min: 50}]->(a3) - CREATE (a1)-[:FLIGHT {distance_km: 426.0, duration_min: 75}]->(a3) - RETURN 'routes graph created' -$$) AS (result agtype); - --- GIN index on routes graph -CREATE INDEX routes_city_gin ON routes."City" USING GIN (properties); - --- Step 5: Record pre-upgrade integrity checksums. --- These sums use the "val" / "score" / coordinate properties as fingerprints. - --- company: sum of all val properties (should be a precise known value) -SELECT * FROM cypher('company', $$ - MATCH (n) WHERE n.val IS NOT NULL RETURN sum(n.val) -$$) AS (company_val_sum_before agtype); - --- network: sum of all score properties -SELECT * FROM cypher('network', $$ - MATCH (n:User) RETURN sum(n.score) -$$) AS (network_score_sum_before agtype); - --- routes: sum of all latitude values -SELECT * FROM cypher('routes', $$ - MATCH (c:City) RETURN sum(c.lat) -$$) AS (routes_lat_sum_before agtype); - --- Total vertex and edge counts across all three graphs -SELECT sum(cnt)::int AS total_vertices_before FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH (n) RETURN n $$) AS (n agtype) -) sub; - -SELECT sum(cnt)::int AS total_edges_before FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) -) sub; - --- Count of distinct labels (ag_label entries) across all graphs -SELECT count(*)::int AS total_labels_before -FROM ag_label al JOIN ag_graph ag ON al.graph = ag.graphid -WHERE ag.name <> '_ag_catalog'; - --- Step 6: Upgrade AGE from the initial version to the current (default) version --- via the stamped upgrade template. +-- Step 11: Upgrade to the current (default) version via the stamped template. DO $$ DECLARE curr_ver text; BEGIN @@ -219,88 +158,225 @@ BEGIN IF curr_ver IS NULL THEN RAISE EXCEPTION 'No default version found for upgrade test'; END IF; - EXECUTE format('ALTER EXTENSION age UPDATE TO %L', curr_ver); END; $$; --- Step 7: Confirm version is now the default (current HEAD) version. +-- Step 12: Confirm the upgrade succeeded. SELECT installed_version = default_version AS upgraded_to_current FROM pg_available_extensions WHERE name = 'age'; --- Step 8: Verify all data survived — reload and recheck. -LOAD 'age'; -SET search_path TO ag_catalog, "$user", public; - --- Repeat integrity checksums — must match pre-upgrade values exactly. -SELECT * FROM cypher('company', $$ - MATCH (n) WHERE n.val IS NOT NULL RETURN sum(n.val) -$$) AS (company_val_sum_after agtype); - -SELECT * FROM cypher('network', $$ - MATCH (n:User) RETURN sum(n.score) -$$) AS (network_score_sum_after agtype); - -SELECT * FROM cypher('routes', $$ - MATCH (c:City) RETURN sum(c.lat) -$$) AS (routes_lat_sum_after agtype); - -SELECT sum(cnt)::int AS total_vertices_after FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH (n) RETURN n $$) AS (n agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH (n) RETURN n $$) AS (n agtype) -) sub; - -SELECT sum(cnt)::int AS total_edges_after FROM ( - SELECT count(*) AS cnt FROM cypher('company', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('network', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) - UNION ALL - SELECT count(*) FROM cypher('routes', $$ MATCH ()-[e]->() RETURN e $$) AS (e agtype) -) sub; - -SELECT count(*)::int AS total_labels_after -FROM ag_label al JOIN ag_graph ag ON al.graph = ag.graphid -WHERE ag.name <> '_ag_catalog'; - --- Step 9: Verify specific structural queries across all three graphs. - --- company: management chain -SELECT * FROM cypher('company', $$ - MATCH (boss:Employee)-[:MANAGES*]->(report:Employee) - RETURN boss.name, report.name - ORDER BY boss.name, report.name -$$) AS (boss agtype, report agtype); - --- network: circular follow chain (proves full cycle survived) -SELECT * FROM cypher('network', $$ - MATCH (a:User)-[:FOLLOWS]->(b:User) - RETURN a.handle, b.handle - ORDER BY a.handle -$$) AS (follower agtype, followed agtype); - --- routes: all flights with distances (proves edge properties intact) -SELECT * FROM cypher('routes', $$ - MATCH (a:Airport)-[f:FLIGHT]->(b:Airport) - RETURN a.code, b.code, f.distance_km - ORDER BY a.code, b.code -$$) AS (origin agtype, dest agtype, dist agtype); - --- Step 10: Verify GIN indexes still exist after upgrade. -SELECT indexname FROM pg_indexes -WHERE schemaname IN ('company', 'network', 'routes') - AND tablename IN ('Employee', 'User', 'Post', 'City') - AND indexdef LIKE '%gin%' -ORDER BY indexname; - --- Step 11: Cleanup and restore AGE at the default version for subsequent tests. -SELECT drop_graph('routes', true); -SELECT drop_graph('network', true); -SELECT drop_graph('company', true); +-- ===================================================================== +-- UPGRADED INSTALL SNAPSHOTS (Steps 13-18) +-- Capture the catalog state after upgrade from initial to current. +-- ===================================================================== + +-- Step 13: Snapshot functions. +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 +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; + +-- Step 14: Snapshot relations. +CREATE TEMP TABLE _upgraded_rels AS +SELECT relname::text, relkind::text +FROM pg_class +WHERE relnamespace = 'ag_catalog'::regnamespace + AND relkind IN ('r', 'v', 'S', 'i') +ORDER BY relname; + +-- Step 15: Snapshot types. +CREATE TEMP TABLE _upgraded_types AS +SELECT typname::text, typtype::text +FROM pg_type +WHERE typnamespace = 'ag_catalog'::regnamespace + AND typname NOT LIKE 'pg_toast%' +ORDER BY typname; + +-- Step 16: Snapshot operators. +CREATE TEMP TABLE _upgraded_ops AS +SELECT oprname::text, + oprleft::regtype::text AS lefttype, + oprright::regtype::text AS righttype +FROM pg_operator +WHERE oprnamespace = 'ag_catalog'::regnamespace +ORDER BY oprname, lefttype, righttype; + +-- Step 17: Snapshot casts and operator classes. +CREATE TEMP TABLE _upgraded_casts AS +SELECT castsource::regtype::text AS source_type, + casttarget::regtype::text AS target_type, + castcontext::text +FROM pg_cast +WHERE castsource IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) + OR casttarget IN (SELECT oid FROM pg_type WHERE typnamespace = 'ag_catalog'::regnamespace) +ORDER BY source_type, target_type; + +CREATE TEMP TABLE _upgraded_opclass AS +SELECT opcname::text, + (SELECT amname FROM pg_am WHERE oid = opcmethod)::text AS amname +FROM pg_opclass +WHERE opcnamespace = 'ag_catalog'::regnamespace +ORDER BY opcname; + + +-- Step 18: Snapshot constraints. +CREATE TEMP TABLE _upgraded_constraints AS +SELECT conname::text, contype::text, + conrelid::regclass::text AS table_name, + confrelid::regclass::text AS ref_table +FROM pg_constraint +WHERE connamespace = 'ag_catalog'::regnamespace +ORDER BY conname; + +-- ===================================================================== +-- COMPARISON: Missing or extra objects (Steps 19-33) +-- Any rows returned indicate a template deficiency. +-- ===================================================================== + +-- Step 19: Functions MISSING after upgrade. +SELECT f.proname || '(' || f.args || ')' AS missing_function +FROM _fresh_funcs f +LEFT JOIN _upgraded_funcs u USING (proname, args) +WHERE u.proname IS NULL +ORDER BY 1; + +-- Step 20: Functions EXTRA after upgrade. +SELECT u.proname || '(' || u.args || ')' AS extra_function +FROM _upgraded_funcs u +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). +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 +FROM _fresh_funcs f +JOIN _upgraded_funcs u USING (proname, args) +WHERE f.provolatile <> u.provolatile + OR f.proisstrict <> u.proisstrict + OR f.prokind <> u.prokind + OR f.rettype <> u.rettype + OR f.proretset <> u.proretset +ORDER BY 1; + +-- Step 22: Relations MISSING after upgrade. +SELECT f.relname || ' (' || f.relkind || ')' AS missing_relation +FROM _fresh_rels f +LEFT JOIN _upgraded_rels u USING (relname, relkind) +WHERE u.relname IS NULL +ORDER BY 1; + +-- Step 23: Relations EXTRA after upgrade. +SELECT u.relname || ' (' || u.relkind || ')' AS extra_relation +FROM _upgraded_rels u +LEFT JOIN _fresh_rels f USING (relname, relkind) +WHERE f.relname IS NULL +ORDER BY 1; + +-- Step 24: Types MISSING after upgrade. +SELECT f.typname || ' (' || f.typtype || ')' AS missing_type +FROM _fresh_types f +LEFT JOIN _upgraded_types u USING (typname, typtype) +WHERE u.typname IS NULL +ORDER BY 1; + +-- Step 25: Types EXTRA after upgrade. +SELECT u.typname || ' (' || u.typtype || ')' AS extra_type +FROM _upgraded_types u +LEFT JOIN _fresh_types f USING (typname, typtype) +WHERE f.typname IS NULL +ORDER BY 1; + +-- Step 26: Operators MISSING after upgrade. +SELECT f.oprname || ' (' || f.lefttype || ', ' || f.righttype || ')' AS missing_operator +FROM _fresh_ops f +LEFT JOIN _upgraded_ops u USING (oprname, lefttype, righttype) +WHERE u.oprname IS NULL +ORDER BY 1; + +-- Step 27: Operators EXTRA after upgrade. +SELECT u.oprname || ' (' || u.lefttype || ', ' || u.righttype || ')' AS extra_operator +FROM _upgraded_ops u +LEFT JOIN _fresh_ops f USING (oprname, lefttype, righttype) +WHERE f.oprname IS NULL +ORDER BY 1; + +-- Step 28: Casts MISSING after upgrade. +SELECT f.source_type || ' -> ' || f.target_type || ' (' || f.castcontext || ')' AS missing_cast +FROM _fresh_casts f +LEFT JOIN _upgraded_casts u USING (source_type, target_type, castcontext) +WHERE u.source_type IS NULL +ORDER BY 1; + +-- Step 29: Casts EXTRA after upgrade. +SELECT u.source_type || ' -> ' || u.target_type || ' (' || u.castcontext || ')' AS extra_cast +FROM _upgraded_casts u +LEFT JOIN _fresh_casts f USING (source_type, target_type, castcontext) +WHERE f.source_type IS NULL +ORDER BY 1; + +-- Step 30: Operator classes MISSING after upgrade. +SELECT f.opcname || ' (' || f.amname || ')' AS missing_opclass +FROM _fresh_opclass f +LEFT JOIN _upgraded_opclass u USING (opcname, amname) +WHERE u.opcname IS NULL +ORDER BY 1; + +-- Step 31: Operator classes EXTRA after upgrade. +SELECT u.opcname || ' (' || u.amname || ')' AS extra_opclass +FROM _upgraded_opclass u +LEFT JOIN _fresh_opclass f USING (opcname, amname) +WHERE f.opcname IS NULL +ORDER BY 1; + +-- Step 32: Constraints MISSING after upgrade. +SELECT f.conname || ' (' || f.contype || ' on ' || f.table_name || ')' AS missing_constraint +FROM _fresh_constraints f +LEFT JOIN _upgraded_constraints u USING (conname, contype, table_name) +WHERE u.conname IS NULL +ORDER BY 1; + +-- Step 33: Constraints EXTRA after upgrade. +SELECT u.conname || ' (' || u.contype || ' on ' || u.table_name || ')' AS extra_constraint +FROM _upgraded_constraints u +LEFT JOIN _fresh_constraints f USING (conname, contype, table_name) +WHERE f.conname IS NULL +ORDER BY 1; + +-- ===================================================================== +-- SUMMARY (Step 34) +-- ===================================================================== + +-- Step 34: Verify all counts match (result: single row, all true). +SELECT + (SELECT count(*) FROM _fresh_funcs) = (SELECT count(*) FROM _upgraded_funcs) AS funcs_match, + (SELECT count(*) FROM _fresh_rels) = (SELECT count(*) FROM _upgraded_rels) AS rels_match, + (SELECT count(*) FROM _fresh_types) = (SELECT count(*) FROM _upgraded_types) AS types_match, + (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; + +-- ===================================================================== +-- CLEANUP (Steps 35-36) +-- ===================================================================== + +-- Step 35: Drop temp tables, restore AGE at default version. +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; DROP EXTENSION age; CREATE EXTENSION age; --- Step 12: Remove synthetic upgrade test files from the extension directory. +-- Step 36: Remove synthetic upgrade test files from the extension directory. \! sh ./regress/age_upgrade_cleanup.sh