Copilot commented on code in PR #2364:
URL: https://github.com/apache/age/pull/2364#discussion_r3026477668


##########
Makefile:
##########
@@ -19,6 +19,64 @@ MODULE_big = age
 
 age_sql = age--1.7.0.sql
 
+# --- Extension upgrade regression test support ---
+#
+# Validates the upgrade template (age--<VER>--y.y.y.sql) by simulating an
+# extension version upgrade entirely within "make installcheck". The test:
+#
+#   1. Builds the install SQL from the INITIAL version-bump commit (the "from"
+#      version). This is the age--<CURR>.sql used by CREATE EXTENSION age.
+#   2. Builds the install SQL from current HEAD as a synthetic "next" version
+#      (the "to" version). This is age--<NEXT>.sql where NEXT = CURR.minor+1.
+#   3. Stamps the upgrade template with the synthetic version, producing the
+#      upgrade script age--<CURR>--<NEXT>.sql.
+#   4. Temporarily installs both synthetic files into the PG extension 
directory
+#      so that ALTER EXTENSION age UPDATE TO '<NEXT>' can find them.
+#   5. The age_upgrade regression test exercises the full upgrade path: install
+#      at CURR, create data, ALTER EXTENSION UPDATE to NEXT, verify data.
+#   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).
+#
+# The .so (shared library) is always built from current HEAD, so C-level 
changes
+# in the PR are tested by every regression test, not just the upgrade test.
+#
+# Graceful degradation — the upgrade test is silently skipped when:
+#   - No git history (tarball build): AGE_VER_COMMIT is empty.
+#   - No upgrade template: age--<CURR>--y.y.y.sql does not exist.
+#   - A real (git-tracked) upgrade script from <CURR> already exists
+#     (e.g., age--1.7.0--1.8.0.sql is committed): the synthetic test is
+#     redundant because the real script ships with the extension.
+# Current version from age.control (e.g., "1.7.0")
+AGE_CURR_VER := $(shell grep -oP "default_version = '\K[^']+" age.control 
2>/dev/null)

Review Comment:
   `AGE_CURR_VER` is extracted using `grep -oP`, which relies on GNU grep PCRE 
support and will fail on platforms with BSD grep (common on macOS/*BSD). Please 
switch to a portable extraction (e.g., awk/sed parsing of the `default_version` 
line) so `make installcheck` works cross-platform.
   ```suggestion
   AGE_CURR_VER := $(shell awk -F"'" 
'/^[[:space:]]*default_version[[:space:]]*=/ {print $$2; exit}' age.control 
2>/dev/null)
   ```



##########
Makefile:
##########
@@ -157,15 +226,78 @@ src/backend/parser/cypher_parser.bc: 
src/backend/parser/cypher_gram.c src/includ
 src/backend/parser/cypher_keywords.o: src/backend/parser/cypher_gram.c 
src/include/parser/cypher_gram_def.h
 src/backend/parser/cypher_keywords.bc: src/backend/parser/cypher_gram.c 
src/include/parser/cypher_gram_def.h
 
-# Strip PASSEDBYVALUE on 32-bit (SIZEOF_DATUM=4) for graphid pass-by-reference
+# Build the default install SQL (age--<VER>.sql) from the INITIAL version-bump
+# commit in git history. This means CREATE EXTENSION age installs the "day-one
+# release" SQL — the state of sql/ at the moment the version was bumped in
+# age.control. All other regression tests run against this SQL + the current
+# HEAD .so, implicitly validating that the .so is backward-compatible with the
+# initial release SQL.
+#
+# The current HEAD SQL goes into the synthetic next version (age--<NEXT>.sql)
+# which is only reachable via ALTER EXTENSION UPDATE in the upgrade test.
+ifneq ($(AGE_VER_COMMIT),)
+$(age_sql): age.control
+       @echo "Building initial-release install SQL: $@ from commit 
$(AGE_VER_COMMIT)"
+       @for f in $$(git show $(AGE_VER_COMMIT):sql/sql_files 2>/dev/null); do \
+               git show $(AGE_VER_COMMIT):sql/$${f}.sql 2>/dev/null; \
+       done > $@
+ifeq ($(SIZEOF_DATUM),4)
+       @echo "32-bit build: removing PASSEDBYVALUE from graphid type"
+       @sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see 
Makefile)/' $@ > [email protected] && mv [email protected] $@
+endif
+else
+# Fallback: no git history (tarball build) — use current HEAD SQL fragments
 $(age_sql): $(SQLS)
        @cat $(SQLS) > $@
 ifeq ($(SIZEOF_DATUM),4)
        @echo "32-bit build: removing PASSEDBYVALUE from graphid type"
        @sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see 
Makefile)/' $@ > [email protected] && mv [email protected] $@
        @grep -q 'PASSEDBYVALUE removed for 32-bit' $@ || { echo "Error: 
PASSEDBYVALUE replacement failed in $@"; exit 1; }
 endif
+endif
+
+# Build synthetic "next" version install SQL from current HEAD's sql/sql_files.
+# This represents what the extension SQL looks like in the PR being tested.
+ifneq ($(AGE_HAS_UPGRADE_TEST),)
+$(age_next_sql): $(SQLS)
+       @echo "Building synthetic next version install SQL: $@ 
($(AGE_NEXT_VER))"
+       @cat $(SQLS) > $@
+ifeq ($(SIZEOF_DATUM),4)
+       @sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see 
Makefile)/' $@ > [email protected] && mv [email protected] $@
+endif
+
+# Stamp upgrade template as upgrade from current to synthetic next version
+$(age_upgrade_test_sql): $(AGE_UPGRADE_TEMPLATE)
+       @echo "Stamping upgrade template: $< -> $@"
+       @sed -e "s/1\.X\.0/$(AGE_NEXT_VER)/g" -e "s/y\.y\.y/$(AGE_NEXT_VER)/g" 
$< > $@
+endif
 
 src/backend/parser/ag_scanner.c: FLEX_NO_BACKUP=yes
 
+# --- Upgrade test file lifecycle during installcheck ---
+#
+# Problem: The upgrade test needs age--<NEXT>.sql and age--<CURR>--<NEXT>.sql
+# in the PG extension directory for ALTER EXTENSION UPDATE to find them, but
+# we must not leave them installed permanently (they would confuse users).
+#
+# Solution: A Make prerequisite installs them before pg_regress runs, and the
+# test SQL removes them at the end via \! (shell escape in psql). A generated
+# cleanup script (regress/age_upgrade_cleanup.sh) contains the exact absolute
+# paths so the removal works regardless of the working directory. EXTRA_CLEAN
+# also removes them on "make clean" as a safety net.
+#
+# This adds a prerequisite to "installcheck" but does NOT override the PGXS
+# recipe, so there are no Makefile warnings.
+SHAREDIR = $(shell $(PG_CONFIG) --sharedir)
+
 installcheck: export LC_COLLATE=C
+ifneq ($(AGE_HAS_UPGRADE_TEST),)
+.PHONY: _install_upgrade_test_files
+_install_upgrade_test_files: $(age_next_sql) $(age_upgrade_test_sql)  ## 
Build, install synthetic files, generate cleanup script
+       @echo "Installing upgrade test files to $(SHAREDIR)/extension/"
+       @$(INSTALL_DATA) $(age_next_sql) $(age_upgrade_test_sql) 
'$(SHAREDIR)/extension/'
+       @printf '#!/bin/sh\nrm -f "$(SHAREDIR)/extension/$(age_next_sql)" 
"$(SHAREDIR)/extension/$(age_upgrade_test_sql)"\n' > 
$(ag_regress_dir)/age_upgrade_cleanup.sh
+       @chmod +x $(ag_regress_dir)/age_upgrade_cleanup.sh
+

Review Comment:
   The synthetic extension files are installed into `$(SHAREDIR)/extension/` as 
an `installcheck` prerequisite, but cleanup is only performed inside the 
regression SQL at the end of the test. If `age_upgrade` aborts early (or 
`pg_regress` is interrupted), these files can be left behind in the 
installation directory and affect subsequent runs. Consider adding a 
Makefile-level cleanup that runs even on failure (e.g., wrapping the 
`installcheck` recipe with a shell `trap`, or adding a dedicated 
post-installcheck cleanup target invoked unconditionally).
   ```suggestion
   
   # Makefile-level cleanup hook for synthetic upgrade test files.
   # This can be invoked manually or by CI even if age_upgrade.sql did not
   # reach its in-SQL cleanup (e.g., early failure or interruption).
   .PHONY: age_upgrade_cleanup
   age_upgrade_cleanup:
        @if [ -n "$(AGE_HAS_UPGRADE_TEST)" ]; then \
                echo "Cleaning up upgrade test files from 
$(SHAREDIR)/extension/"; \
                rm -f '$(SHAREDIR)/extension/$(age_next_sql)' 
'$(SHAREDIR)/extension/$(age_upgrade_test_sql)'; \
        fi
   ```



##########
regress/sql/age_upgrade.sql:
##########
@@ -0,0 +1,295 @@
+/*
+ * 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)

Review Comment:
   The integrity "checksums" use floating-point sums and the test asserts exact 
textual output (see expected values like `335.14612999999997`). Floating-point 
aggregation can vary slightly across platforms / build settings, which can make 
this regression test flaky. Consider rounding to a fixed scale (or using 
integer/numeric properties) before comparing results.
   ```suggestion
       MATCH (n) WHERE n.val IS NOT NULL RETURN round(sum(n.val), 6)
   $$) AS (company_val_sum_before agtype);
   
   -- network: sum of all score properties
   SELECT * FROM cypher('network', $$
       MATCH (n:User) RETURN round(sum(n.score), 6)
   $$) AS (network_score_sum_before agtype);
   
   -- routes: sum of all latitude values
   SELECT * FROM cypher('routes', $$
       MATCH (c:City) RETURN round(sum(c.lat), 6)
   ```



##########
regress/sql/age_upgrade.sql:
##########
@@ -0,0 +1,295 @@
+/*
+ * 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 tablename IN ('Employee', 'User', 'Post', 'City')

Review Comment:
   The index survival check queries `pg_indexes` only by `tablename` and `LIKE 
'%gin%'`, without restricting schemas. Since other regression tests also create 
tables named `City` (e.g., `cypher_index."City"` in `regress/sql/index.sql`) 
this query can accidentally pick up unrelated indexes if any objects are left 
behind, producing nondeterministic output. Filtering by `schemaname IN 
('company','network','routes')` (or matching expected index name prefixes) 
would make the check more robust.
   ```suggestion
   WHERE schemaname IN ('company', 'network', 'routes')
     AND tablename IN ('Employee', 'User', 'Post', 'City')
   ```



##########
Makefile:
##########
@@ -19,6 +19,64 @@ MODULE_big = age
 
 age_sql = age--1.7.0.sql
 
+# --- Extension upgrade regression test support ---
+#
+# Validates the upgrade template (age--<VER>--y.y.y.sql) by simulating an
+# extension version upgrade entirely within "make installcheck". The test:
+#
+#   1. Builds the install SQL from the INITIAL version-bump commit (the "from"
+#      version). This is the age--<CURR>.sql used by CREATE EXTENSION age.
+#   2. Builds the install SQL from current HEAD as a synthetic "next" version
+#      (the "to" version). This is age--<NEXT>.sql where NEXT = CURR.minor+1.
+#   3. Stamps the upgrade template with the synthetic version, producing the
+#      upgrade script age--<CURR>--<NEXT>.sql.
+#   4. Temporarily installs both synthetic files into the PG extension 
directory
+#      so that ALTER EXTENSION age UPDATE TO '<NEXT>' can find them.
+#   5. The age_upgrade regression test exercises the full upgrade path: install
+#      at CURR, create data, ALTER EXTENSION UPDATE to NEXT, verify data.
+#   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).
+#
+# The .so (shared library) is always built from current HEAD, so C-level 
changes
+# in the PR are tested by every regression test, not just the upgrade test.
+#
+# Graceful degradation — the upgrade test is silently skipped when:
+#   - No git history (tarball build): AGE_VER_COMMIT is empty.
+#   - No upgrade template: age--<CURR>--y.y.y.sql does not exist.
+#   - A real (git-tracked) upgrade script from <CURR> already exists
+#     (e.g., age--1.7.0--1.8.0.sql is committed): the synthetic test is
+#     redundant because the real script ships with the extension.
+# Current version from age.control (e.g., "1.7.0")
+AGE_CURR_VER := $(shell grep -oP "default_version = '\K[^']+" age.control 
2>/dev/null)
+# Git commit that last changed age.control — the "initial release" commit
+AGE_VER_COMMIT := $(shell git log -1 --format=%H -- age.control 2>/dev/null)
+# Synthetic next version: current minor + 1 (e.g., 1.7.0 -> 1.8.0)
+AGE_NEXT_VER := $(shell echo $(AGE_CURR_VER) | awk -F. '{printf "%s.%s.%s", 
$$1, $$2+1, $$3}')

Review Comment:
   `AGE_NEXT_VER` increments only the middle component and keeps the patch 
component unchanged (e.g., `1.7.3 -> 1.8.3`), which is an unusual version 
progression and can generate a non-existent target version. Consider 
normalizing the synthetic "next" version (typically bump minor and reset patch 
to 0) and/or validating that `AGE_CURR_VER` matches an expected `X.Y.Z` pattern 
before computing.
   ```suggestion
   # Synthetic next version: bump minor and reset patch to 0 (e.g., 1.7.0 -> 
1.8.0).
   # Only computed when AGE_CURR_VER matches X.Y.Z; otherwise left empty.
   AGE_NEXT_VER := $(shell echo "$(AGE_CURR_VER)" | \
        awk -F. '/^[0-9]+\.[0-9]+\.[0-9]+$$/ {printf "%s.%s.0", $$1, $$2+1}')
   ```



##########
Makefile:
##########
@@ -157,15 +226,78 @@ src/backend/parser/cypher_parser.bc: 
src/backend/parser/cypher_gram.c src/includ
 src/backend/parser/cypher_keywords.o: src/backend/parser/cypher_gram.c 
src/include/parser/cypher_gram_def.h
 src/backend/parser/cypher_keywords.bc: src/backend/parser/cypher_gram.c 
src/include/parser/cypher_gram_def.h
 
-# Strip PASSEDBYVALUE on 32-bit (SIZEOF_DATUM=4) for graphid pass-by-reference
+# Build the default install SQL (age--<VER>.sql) from the INITIAL version-bump
+# commit in git history. This means CREATE EXTENSION age installs the "day-one
+# release" SQL — the state of sql/ at the moment the version was bumped in
+# age.control. All other regression tests run against this SQL + the current
+# HEAD .so, implicitly validating that the .so is backward-compatible with the
+# initial release SQL.
+#
+# The current HEAD SQL goes into the synthetic next version (age--<NEXT>.sql)
+# which is only reachable via ALTER EXTENSION UPDATE in the upgrade test.
+ifneq ($(AGE_VER_COMMIT),)
+$(age_sql): age.control
+       @echo "Building initial-release install SQL: $@ from commit 
$(AGE_VER_COMMIT)"
+       @for f in $$(git show $(AGE_VER_COMMIT):sql/sql_files 2>/dev/null); do \
+               git show $(AGE_VER_COMMIT):sql/$${f}.sql 2>/dev/null; \
+       done > $@
+ifeq ($(SIZEOF_DATUM),4)
+       @echo "32-bit build: removing PASSEDBYVALUE from graphid type"
+       @sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see 
Makefile)/' $@ > [email protected] && mv [email protected] $@

Review Comment:
   In the git-based `$(age_sql)` generation path, the 32-bit `sed` rewrite of 
`PASSEDBYVALUE` does not verify that a replacement actually occurred (unlike 
the fallback path which greps for the marker and errors if missing). Please add 
the same verification here to avoid silently producing an incorrect install 
script on 32-bit builds.
   ```suggestion
        @sed 's/^  PASSEDBYVALUE,$$/  -- PASSEDBYVALUE removed for 32-bit (see 
Makefile)/' $@ > [email protected] && mv [email protected] $@
        @grep -q 'PASSEDBYVALUE removed for 32-bit' $@ || { echo "Error: 
PASSEDBYVALUE replacement failed in $@"; exit 1; }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to