Skip to content

Commit 90c33eb

Browse files
authored
Add extension upgrade template regression test (#2364)
Note: This PR was created with AI tools and a human. Add a version-agnostic regression test (age_upgrade) that validates the upgrade template (age--<VER>--y.y.y.sql) works correctly by simulating a full extension version upgrade within "make installcheck". Add full upgrade scripts to the install path (DATA) in the Makefile, excluding template upgrade files. This enables the install to copy all version upgrade files into the PG AGE install. This is needed for ALTER EXTENSION Adjusted installcheck.yaml to allow git commit history for this test. Makefile infrastructure: - Build the install SQL (age--<CURR>.sql) from the initial version-bump commit in git history, so CREATE EXTENSION installs "day-one" SQL while the .so comes from current HEAD — implicitly testing backward compat. - Build a synthetic "next" version (age--<NEXT>.sql) from HEAD and stamp the upgrade template to produce age--<CURR>--<NEXT>.sql. - Add an installcheck prerequisite that temporarily installs both synthetic files into the PG extension directory; a generated cleanup script removes them at the end of the test via \! shell escape. EXTRA_CLEAN catches stragglers on "make clean". - Skip the test automatically when: (a) no git history (tarball builds), (b) no upgrade template exists, or (c) a real upgrade script from the current version is already committed (detected via git ls-files).o Regression test (regress/sql/age_upgrade.sql): - Creates 3 graphs (company, network, routes) with 8 vertex labels, 8 edge labels, 23 vertices, 28 edges, and 4 GIN indexes. - Records integrity checksums (agtype sums), vertex/edge counts, and label counts before the upgrade; repeats all checks after ALTER EXTENSION UPDATE to the synthetic next version. - Verifies structural queries: VLE management chains, circular follow chains, flight distances with edge properties. - Verifies all 4 GIN indexes survive the upgrade via pg_indexes. - Uses ORDER BY on all multi-row queries for deterministic output. - Returns agtype natively (no ::numeric casts) for portability. - Avoids version-dependent output (checks boolean IS NOT NULL instead of printing the version string). - Uses JOIN-based label counts to avoid NULL comparison bugs with the internal _ag_catalog graph. - Cleans up all 3 graphs and restores the default AGE version. modified: Makefile new file: regress/expected/age_upgrade.out new file: regress/sql/age_upgrade.sql modified: .github/workflows/installcheck.yaml
1 parent 23146a4 commit 90c33eb

4 files changed

Lines changed: 875 additions & 2 deletions

File tree

.github/workflows/installcheck.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
make PG_CONFIG=$HOME/pg18/bin/pg_config install -j$(nproc) > /dev/null
4343
4444
- uses: actions/checkout@v3
45+
with:
46+
fetch-depth: 75
4547

4648
- name: Build AGE
4749
id: build

Makefile

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,64 @@ MODULE_big = age
1919

2020
age_sql = age--1.7.0.sql
2121

22+
# --- Extension upgrade regression test support ---
23+
#
24+
# Validates the upgrade template (age--<VER>--y.y.y.sql) by simulating an
25+
# extension version upgrade entirely within "make installcheck". The test:
26+
#
27+
# 1. Builds the install SQL from the INITIAL version-bump commit (the "from"
28+
# version). This is the age--<CURR>.sql used by CREATE EXTENSION age.
29+
# 2. Builds the install SQL from current HEAD as a synthetic "next" version
30+
# (the "to" version). This is age--<NEXT>.sql where NEXT = CURR.minor+1.
31+
# 3. Stamps the upgrade template with the synthetic version, producing the
32+
# upgrade script age--<CURR>--<NEXT>.sql.
33+
# 4. Temporarily installs both synthetic files into the PG extension directory
34+
# so that ALTER EXTENSION age UPDATE TO '<NEXT>' can find them.
35+
# 5. The age_upgrade regression test exercises the full upgrade path: install
36+
# at CURR, create data, ALTER EXTENSION UPDATE to NEXT, verify data.
37+
# 6. The test SQL cleans up the synthetic files via a generated shell script.
38+
#
39+
# This forces developers to keep the upgrade template in sync: any SQL object
40+
# added after the version-bump commit must also appear in the template, or the
41+
# upgrade test will fail (the object will be missing after ALTER EXTENSION UPDATE).
42+
#
43+
# The .so (shared library) is always built from current HEAD, so C-level changes
44+
# in the PR are tested by every regression test, not just the upgrade test.
45+
#
46+
# Graceful degradation — the upgrade test is silently skipped when:
47+
# - No git history (tarball build): AGE_VER_COMMIT is empty.
48+
# - No upgrade template: age--<CURR>--y.y.y.sql does not exist.
49+
# - A real (git-tracked) upgrade script from <CURR> already exists
50+
# (e.g., age--1.7.0--1.8.0.sql is committed): the synthetic test is
51+
# redundant because the real script ships with the extension.
52+
# Current version from age.control (e.g., "1.7.0")
53+
AGE_CURR_VER := $(shell awk -F"'" '/default_version/ {print $$2}' age.control 2>/dev/null)
54+
# Git commit that last changed age.control — the "initial release" commit
55+
AGE_VER_COMMIT := $(shell git log -1 --format=%H -- age.control 2>/dev/null)
56+
# Synthetic next version: current minor + 1 (e.g., 1.7.0 -> 1.8.0)
57+
AGE_NEXT_VER := $(shell echo $(AGE_CURR_VER) | awk -F. '{printf "%s.%s.%s", $$1, $$2+1, $$3}')
58+
# The upgrade template file (e.g., age--1.7.0--y.y.y.sql); empty if not present
59+
AGE_UPGRADE_TEMPLATE := $(wildcard age--$(AGE_CURR_VER)--y.y.y.sql)
60+
61+
# Synthetic filenames — these are NOT installed permanently; they are temporarily
62+
# placed in $(SHAREDIR)/extension/ during installcheck and removed after.
63+
age_next_sql = $(if $(AGE_NEXT_VER),age--$(AGE_NEXT_VER).sql)
64+
age_upgrade_test_sql = $(if $(AGE_NEXT_VER),age--$(AGE_CURR_VER)--$(AGE_NEXT_VER).sql)
65+
66+
# Real (git-tracked, non-template) upgrade scripts FROM the current version.
67+
# If any exist (e.g., age--1.7.0--1.8.0.sql is committed), the synthetic
68+
# upgrade test is redundant because a real upgrade path already ships.
69+
# Uses git ls-files so untracked synthetic files are NOT matched.
70+
AGE_REAL_UPGRADE := $(shell git ls-files 'age--$(AGE_CURR_VER)--*.sql' 2>/dev/null | grep -v 'y\.y\.y')
71+
72+
# Non-empty when ALL of these hold:
73+
# 1. Git history is available (AGE_VER_COMMIT non-empty)
74+
# 2. The upgrade template exists (AGE_UPGRADE_TEMPLATE non-empty)
75+
# 3. No real upgrade script from current version exists (AGE_REAL_UPGRADE empty)
76+
# When a real upgrade script ships, the test is skipped — the real script
77+
# supersedes the synthetic one and has its own validation path.
78+
AGE_HAS_UPGRADE_TEST = $(and $(AGE_VER_COMMIT),$(AGE_UPGRADE_TEMPLATE),$(if $(AGE_REAL_UPGRADE),,yes))
79+
2280
OBJS = src/backend/age.o \
2381
src/backend/catalog/ag_catalog.o \
2482
src/backend/catalog/ag_graph.o \
@@ -83,6 +141,10 @@ SQLS := $(addsuffix .sql,$(SQLS))
83141

84142
DATA_built = $(age_sql)
85143

144+
# Git-tracked upgrade scripts shipped with the extension (e.g., age--1.6.0--1.7.0.sql).
145+
# Excludes the upgrade template (y.y.y) and the synthetic stamped test file.
146+
DATA = $(filter-out age--%-y.y.y.sql $(age_upgrade_test_sql),$(wildcard age--*--*.sql))
147+
86148
# sorted in dependency order
87149
REGRESS = scan \
88150
graphid \
@@ -119,6 +181,13 @@ ifneq ($(EXTRA_TESTS),)
119181
REGRESS += $(EXTRA_TESTS)
120182
endif
121183

184+
# Extension upgrade test — included when git history is available, the upgrade
185+
# template exists, and no real upgrade script from the current version is
186+
# committed. Runs between "security" and "drop" in test order.
187+
ifneq ($(AGE_HAS_UPGRADE_TEST),)
188+
REGRESS += age_upgrade
189+
endif
190+
122191
REGRESS += drop
123192

124193
srcdir=`pwd`
@@ -127,7 +196,7 @@ ag_regress_dir = $(srcdir)/regress
127196
REGRESS_OPTS = --load-extension=age --inputdir=$(ag_regress_dir) --outputdir=$(ag_regress_dir) --temp-instance=$(ag_regress_dir)/instance --port=61958 --encoding=UTF-8 --temp-config $(ag_regress_dir)/age_regression.conf
128197

129198
ag_regress_out = instance/ log/ results/ regression.*
130-
EXTRA_CLEAN = $(addprefix $(ag_regress_dir)/, $(ag_regress_out)) src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h src/include/parser/cypher_kwlist_d.h $(all_age_sql)
199+
EXTRA_CLEAN = $(addprefix $(ag_regress_dir)/, $(ag_regress_out)) src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h src/include/parser/cypher_kwlist_d.h $(all_age_sql) $(age_next_sql) $(age_upgrade_test_sql) $(ag_regress_dir)/age_upgrade_cleanup.sh
131200

132201
GEN_KEYWORDLIST = $(PERL) -I ./tools/ ./tools/gen_keywordlist.pl
133202
GEN_KEYWORDLIST_DEPS = ./tools/gen_keywordlist.pl tools/PerfectHash.pm
@@ -157,15 +226,78 @@ src/backend/parser/cypher_parser.bc: src/backend/parser/cypher_gram.c src/includ
157226
src/backend/parser/cypher_keywords.o: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
158227
src/backend/parser/cypher_keywords.bc: src/backend/parser/cypher_gram.c src/include/parser/cypher_gram_def.h
159228

160-
# Strip PASSEDBYVALUE on 32-bit (SIZEOF_DATUM=4) for graphid pass-by-reference
229+
# Build the default install SQL (age--<VER>.sql) from the INITIAL version-bump
230+
# commit in git history. This means CREATE EXTENSION age installs the "day-one
231+
# release" SQL — the state of sql/ at the moment the version was bumped in
232+
# age.control. All other regression tests run against this SQL + the current
233+
# HEAD .so, implicitly validating that the .so is backward-compatible with the
234+
# initial release SQL.
235+
#
236+
# The current HEAD SQL goes into the synthetic next version (age--<NEXT>.sql)
237+
# which is only reachable via ALTER EXTENSION UPDATE in the upgrade test.
238+
ifneq ($(AGE_VER_COMMIT),)
239+
$(age_sql): age.control
240+
@echo "Building initial-release install SQL: $@ from commit $(AGE_VER_COMMIT)"
241+
@for f in $$(git show $(AGE_VER_COMMIT):sql/sql_files 2>/dev/null); do \
242+
git show $(AGE_VER_COMMIT):sql/$${f}.sql 2>/dev/null; \
243+
done > $@
244+
ifeq ($(SIZEOF_DATUM),4)
245+
@echo "32-bit build: removing PASSEDBYVALUE from graphid type"
246+
@sed 's/^ PASSEDBYVALUE,$$/ -- PASSEDBYVALUE removed for 32-bit (see Makefile)/' $@ > $@.tmp && mv $@.tmp $@
247+
endif
248+
else
249+
# Fallback: no git history (tarball build) — use current HEAD SQL fragments
161250
$(age_sql): $(SQLS)
162251
@cat $(SQLS) > $@
163252
ifeq ($(SIZEOF_DATUM),4)
164253
@echo "32-bit build: removing PASSEDBYVALUE from graphid type"
165254
@sed 's/^ PASSEDBYVALUE,$$/ -- PASSEDBYVALUE removed for 32-bit (see Makefile)/' $@ > $@.tmp && mv $@.tmp $@
166255
@grep -q 'PASSEDBYVALUE removed for 32-bit' $@ || { echo "Error: PASSEDBYVALUE replacement failed in $@"; exit 1; }
167256
endif
257+
endif
258+
259+
# Build synthetic "next" version install SQL from current HEAD's sql/sql_files.
260+
# This represents what the extension SQL looks like in the PR being tested.
261+
ifneq ($(AGE_HAS_UPGRADE_TEST),)
262+
$(age_next_sql): $(SQLS)
263+
@echo "Building synthetic next version install SQL: $@ ($(AGE_NEXT_VER))"
264+
@cat $(SQLS) > $@
265+
ifeq ($(SIZEOF_DATUM),4)
266+
@sed 's/^ PASSEDBYVALUE,$$/ -- PASSEDBYVALUE removed for 32-bit (see Makefile)/' $@ > $@.tmp && mv $@.tmp $@
267+
endif
268+
269+
# Stamp upgrade template as upgrade from current to synthetic next version
270+
$(age_upgrade_test_sql): $(AGE_UPGRADE_TEMPLATE)
271+
@echo "Stamping upgrade template: $< -> $@"
272+
@sed -e "s/1\.X\.0/$(AGE_NEXT_VER)/g" -e "s/y\.y\.y/$(AGE_NEXT_VER)/g" $< > $@
273+
endif
168274

169275
src/backend/parser/ag_scanner.c: FLEX_NO_BACKUP=yes
170276

277+
# --- Upgrade test file lifecycle during installcheck ---
278+
#
279+
# Problem: The upgrade test needs age--<NEXT>.sql and age--<CURR>--<NEXT>.sql
280+
# in the PG extension directory for ALTER EXTENSION UPDATE to find them, but
281+
# we must not leave them installed permanently (they would confuse users).
282+
#
283+
# Solution: A Make prerequisite installs them before pg_regress runs, and the
284+
# test SQL removes them at the end via \! (shell escape in psql). A generated
285+
# cleanup script (regress/age_upgrade_cleanup.sh) contains the exact absolute
286+
# paths so the removal works regardless of the working directory. EXTRA_CLEAN
287+
# also removes them on "make clean" as a safety net.
288+
#
289+
# This adds a prerequisite to "installcheck" but does NOT override the PGXS
290+
# recipe, so there are no Makefile warnings.
291+
SHAREDIR = $(shell $(PG_CONFIG) --sharedir)
292+
171293
installcheck: export LC_COLLATE=C
294+
ifneq ($(AGE_HAS_UPGRADE_TEST),)
295+
.PHONY: _install_upgrade_test_files
296+
_install_upgrade_test_files: $(age_next_sql) $(age_upgrade_test_sql) ## Build, install synthetic files, generate cleanup script
297+
@echo "Installing upgrade test files to $(SHAREDIR)/extension/"
298+
@$(INSTALL_DATA) $(age_next_sql) $(age_upgrade_test_sql) '$(SHAREDIR)/extension/'
299+
@printf '#!/bin/sh\nrm -f "$(SHAREDIR)/extension/$(age_next_sql)" "$(SHAREDIR)/extension/$(age_upgrade_test_sql)"\n' > $(ag_regress_dir)/age_upgrade_cleanup.sh
300+
@chmod +x $(ag_regress_dir)/age_upgrade_cleanup.sh
301+
302+
installcheck: _install_upgrade_test_files
303+
endif

0 commit comments

Comments
 (0)