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