From ab23426d66018cb5f156e900dc817eff96f28ed4 Mon Sep 17 00:00:00 2001 From: John Gemignani Date: Mon, 20 Apr 2026 18:08:03 -0700 Subject: [PATCH] Fix upgrade test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix upgrade test: replace data-integrity checks with catalog comparison 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) 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 catalog comparison approach: 1. Snapshot ag_catalog's functions (pg_proc) and relations (pg_class) from the fresh install (the --load-extension=age default version). 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 function or relation 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. This approach: - Catches the actual failure mode: incomplete upgrade templates. - Uses only plain SQL catalog queries — no cypher, no .so cache issues. - Works reliably across all PostgreSQL versions. - Reports the exact missing/extra object name in the diff output. - Is simpler: 130 lines of SQL vs 306, no graph data setup needed. Makefile: updated step 5 comment to reflect catalog comparison approach. All 33 regression tests pass. modified: Makefile modified: regress/expected/age_upgrade.out modified: regress/sql/age_upgrade.sql --- Makefile | 5 +- regress/expected/age_upgrade.out | 467 ++++++------------------------- regress/sql/age_upgrade.sql | 326 ++++++--------------- 3 files changed, 184 insertions(+), 614 deletions(-) diff --git a/Makefile b/Makefile index 5405665d8..6e02c17af 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,9 @@ 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 (pg_proc, pg_class) to detect missing or extra 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 diff --git a/regress/expected/age_upgrade.out b/regress/expected/age_upgrade.out index 7c3da0150..3babad71c 100644 --- a/regress/expected/age_upgrade.out +++ b/regress/expected/age_upgrade.out @@ -17,36 +17,43 @@ * 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 function or relation exists in the fresh install but is missing +-- after upgrade, the template is incomplete — it failed to create that +-- object. This catches the case where a developer adds a new SQL function +-- 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 --- --- All version discovery is dynamic — no hardcoded versions anywhere. --- This test is version-agnostic and works on any branch for any version. +-- The test compares functions (pg_proc) and relations (pg_class) in the +-- ag_catalog schema. 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. +-- Step 1: Clean up any graphs left by prior tests. SELECT drop_graph(name, true) FROM ag_graph ORDER BY name; drop_graph ------------ (0 rows) +-- Step 2: Snapshot the fresh install catalog — functions in ag_catalog. +CREATE TEMP TABLE _fresh_funcs AS +SELECT proname::text, + pg_get_function_identity_arguments(oid) AS args +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; +-- Step 3: Snapshot the fresh install catalog — relations in ag_catalog. +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') +ORDER BY relname; +-- Step 4: Drop AGE entirely. DROP EXTENSION age; --- Step 2: Verify we have multiple installable versions. +-- Step 5: 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 +61,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 6: Install AGE at the synthetic initial version. DO $$ DECLARE init_ver text; BEGIN @@ -66,204 +73,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 7: Upgrade to the current (default) version via the stamped template. DO $$ DECLARE curr_ver text; BEGIN @@ -273,11 +86,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 8: 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 +97,76 @@ 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) - -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) - -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) - -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) - -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) - -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 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) - --- 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) - --- 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 9: Snapshot the upgraded catalog — functions. +CREATE TEMP TABLE _upgraded_funcs AS +SELECT proname::text, + pg_get_function_identity_arguments(oid) AS args +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; +-- Step 10: Snapshot the upgraded catalog — 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') +ORDER BY relname; +-- Step 11: Functions in fresh install but MISSING after upgrade. +-- Any rows here mean the upgrade template forgot to CREATE that function. +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) --- 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 12: Functions in upgraded but NOT in fresh install. +-- Any rows here mean the template creates something the fresh install doesn't. +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) --- 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 ------------- - -(1 row) +-- Step 13: Relations in fresh install but MISSING after upgrade. +SELECT f.relname 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 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 14: Relations in upgraded but NOT in fresh install. +SELECT u.relname 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 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 15: Summary — counts must match between fresh and upgraded. +SELECT + (SELECT count(*) FROM _fresh_funcs) AS fresh_funcs, + (SELECT count(*) FROM _upgraded_funcs) AS upgraded_funcs, + (SELECT count(*) FROM _fresh_rels) AS fresh_rels, + (SELECT count(*) FROM _upgraded_rels) AS upgraded_rels; + fresh_funcs | upgraded_funcs | fresh_rels | upgraded_rels +-------------+----------------+------------+--------------- + 354 | 354 | 2 | 2 (1 row) +-- Step 16: Cleanup — drop temp tables, restore AGE at default version. +DROP TABLE _fresh_funcs, _upgraded_funcs, _fresh_rels, _upgraded_rels; DROP EXTENSION age; CREATE EXTENSION age; --- Step 12: Remove synthetic upgrade test files from the extension directory. +-- Step 17: 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..35c7866ef 100644 --- a/regress/sql/age_upgrade.sql +++ b/regress/sql/age_upgrade.sql @@ -18,39 +18,49 @@ */ -- --- 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 function or relation exists in the fresh install but is missing +-- after upgrade, the template is incomplete — it failed to create that +-- object. This catches the case where a developer adds a new SQL function +-- 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 --- --- All version discovery is dynamic — no hardcoded versions anywhere. --- This test is version-agnostic and works on any branch for any version. +-- The test compares functions (pg_proc) and relations (pg_class) in the +-- ag_catalog schema. 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. +-- Step 1: Clean up any graphs left by prior tests. SELECT drop_graph(name, true) FROM ag_graph ORDER BY name; + +-- Step 2: Snapshot the fresh install catalog — functions in ag_catalog. +CREATE TEMP TABLE _fresh_funcs AS +SELECT proname::text, + pg_get_function_identity_arguments(oid) AS args +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; + +-- Step 3: Snapshot the fresh install catalog — relations in ag_catalog. +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') +ORDER BY relname; + +-- Step 4: Drop AGE entirely. DROP EXTENSION age; --- Step 2: Verify we have multiple installable versions. +-- Step 5: 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 6: Install AGE at the synthetic initial version. DO $$ DECLARE init_ver text; BEGIN @@ -62,154 +72,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 7: Upgrade to the current (default) version via the stamped template. DO $$ DECLARE curr_ver text; BEGIN @@ -219,88 +86,71 @@ 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 8: 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); +-- Step 9: Snapshot the upgraded catalog — functions. +CREATE TEMP TABLE _upgraded_funcs AS +SELECT proname::text, + pg_get_function_identity_arguments(oid) AS args +FROM pg_proc +WHERE pronamespace = 'ag_catalog'::regnamespace +ORDER BY proname, args; + +-- Step 10: Snapshot the upgraded catalog — 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') +ORDER BY relname; + +-- Step 11: Functions in fresh install but MISSING after upgrade. +-- Any rows here mean the upgrade template forgot to CREATE that function. +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 12: Functions in upgraded but NOT in fresh install. +-- Any rows here mean the template creates something the fresh install doesn't. +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 13: Relations in fresh install but MISSING after upgrade. +SELECT f.relname AS missing_relation +FROM _fresh_rels f +LEFT JOIN _upgraded_rels u USING (relname, relkind) +WHERE u.relname IS NULL +ORDER BY 1; + +-- Step 14: Relations in upgraded but NOT in fresh install. +SELECT u.relname AS extra_relation +FROM _upgraded_rels u +LEFT JOIN _fresh_rels f USING (relname, relkind) +WHERE f.relname IS NULL +ORDER BY 1; + +-- Step 15: Summary — counts must match between fresh and upgraded. +SELECT + (SELECT count(*) FROM _fresh_funcs) AS fresh_funcs, + (SELECT count(*) FROM _upgraded_funcs) AS upgraded_funcs, + (SELECT count(*) FROM _fresh_rels) AS fresh_rels, + (SELECT count(*) FROM _upgraded_rels) AS upgraded_rels; + +-- Step 16: Cleanup — drop temp tables, restore AGE at default version. +DROP TABLE _fresh_funcs, _upgraded_funcs, _fresh_rels, _upgraded_rels; DROP EXTENSION age; CREATE EXTENSION age; --- Step 12: Remove synthetic upgrade test files from the extension directory. +-- Step 17: Remove synthetic upgrade test files from the extension directory. \! sh ./regress/age_upgrade_cleanup.sh