| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| -- |
| -- Extension upgrade regression test |
| -- |
| -- This test validates the upgrade template (age--<VER>--y.y.y.sql) by: |
| -- 1. Installing AGE at the current version (built from the initial |
| -- version-bump commit's SQL — the "day-one release" state) |
| -- 2. Creating three graphs with multiple labels, edges, GIN indexes, |
| -- and numeric properties that serve as integrity checksums |
| -- 3. Upgrading to a synthetic "next" version via the stamped template |
| -- 4. Verifying all data, structure, and checksums survived the upgrade |
| -- |
| -- The Makefile builds: |
| -- age--<CURR>.sql from the initial version-bump commit (git history) |
| -- age--<NEXT>.sql from current HEAD's sql/sql_files |
| -- age--<CURR>--<NEXT>.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. |
| -- |
| |
| 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 cleanly re-create for this test. |
| SELECT drop_graph(name, true) FROM ag_graph ORDER BY name; |
| DROP EXTENSION age; |
| |
| -- Step 2: Verify we have multiple installable versions. |
| SELECT count(*) > 1 AS has_upgrade_path |
| FROM pg_available_extension_versions WHERE name = 'age'; |
| |
| -- Step 3: Install AGE at the default version (the initial release SQL). |
| CREATE EXTENSION age; |
| 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 to the synthetic next version via the stamped template. |
| DO $$ |
| DECLARE next_ver text; |
| BEGIN |
| SELECT version INTO next_ver |
| FROM pg_available_extension_versions |
| WHERE name = 'age' AND version <> ( |
| SELECT default_version FROM pg_available_extensions WHERE name = 'age' |
| ) |
| ORDER BY string_to_array(version, '.')::int[] DESC |
| LIMIT 1; |
| |
| IF next_ver IS NULL THEN |
| RAISE EXCEPTION 'No next version available for upgrade test'; |
| END IF; |
| |
| EXECUTE format('ALTER EXTENSION age UPDATE TO %L', next_ver); |
| END; |
| $$; |
| |
| -- Step 7: Confirm version changed. |
| SELECT installed_version <> default_version AS upgraded_past_default |
| 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); |
| DROP EXTENSION age; |
| CREATE EXTENSION age; |
| |
| -- Step 12: Remove synthetic upgrade test files from the extension directory. |
| \! sh ./regress/age_upgrade_cleanup.sh |