diff --git a/ansible/vars.yml b/ansible/vars.yml index c0ab29ef87..0e554be618 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -10,9 +10,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.6.0.099-orioledb" - postgres17: "17.6.1.142" - postgres15: "15.14.1.142" + postgresorioledb-17: "17.6.0.089-orioledb-pgcron-13" + postgres17: "17.6.1.132-pgcron-13" + postgres15: "15.14.1.132-pgcron-13" # Non Postgres Extensions pgbouncer_release: 1.25.1 diff --git a/ebssurrogate/scripts/surrogate-bootstrap-nix.sh b/ebssurrogate/scripts/surrogate-bootstrap-nix.sh index 47f3014d13..6d4c4b2497 100755 --- a/ebssurrogate/scripts/surrogate-bootstrap-nix.sh +++ b/ebssurrogate/scripts/surrogate-bootstrap-nix.sh @@ -273,7 +273,7 @@ EOF if [ "${ARCH}" = "amd64" ]; then debootstrap --arch ${ARCH} --variant=minbase "$UBUNTU_VERSION" /mnt "http://${REGION}.ec2.archive.ubuntu.com/ubuntu" else - debootstrap --arch ${ARCH} --variant=minbase "$UBUNTU_VERSION" /mnt "http://${REGION}.clouds.ports.ubuntu.com/ubuntu-ports" + debootstrap --arch ${ARCH} --variant=minbase "$UBUNTU_VERSION" /mnt "http://ports.ubuntu.com/ubuntu-ports" fi sed -i "s/REGION/${REGION}/g" /tmp/sources.list diff --git a/nix/ext/pg_cron/default.nix b/nix/ext/pg_cron/default.nix index a823fa450b..8bc14e7c82 100644 --- a/nix/ext/pg_cron/default.nix +++ b/nix/ext/pg_cron/default.nix @@ -61,8 +61,32 @@ let mv $out/share/postgresql/extension/pg_cron--1.4--1.4-1.sql $out/share/postgresql/extension/pg_cron--1.4.0--1.4.1.sql mv $out/share/postgresql/extension/pg_cron--1.4-1--1.5.sql $out/share/postgresql/extension/pg_cron--1.4.2--1.5.2.sql mv $out/share/postgresql/extension/pg_cron--1.5--1.6.sql $out/share/postgresql/extension/pg_cron--1.5.2--1.6.4.sql + ln -s pg_cron--1.0.0--1.1.0.sql $out/share/postgresql/extension/pg_cron--1.0--1.1.sql + ln -s pg_cron--1.0.0--1.1.0.sql $out/share/postgresql/extension/pg_cron--1.0.0--1.1.sql + ln -s pg_cron--1.1.0--1.2.0.sql $out/share/postgresql/extension/pg_cron--1.1--1.2.sql + ln -s pg_cron--1.1.0--1.2.0.sql $out/share/postgresql/extension/pg_cron--1.1.0--1.2.sql + ln -s pg_cron--1.1.0--1.2.0.sql $out/share/postgresql/extension/pg_cron--1.1--1.2.0.sql + ln -s pg_cron--1.2.0--1.3.1.sql $out/share/postgresql/extension/pg_cron--1.2--1.3.sql + ln -s pg_cron--1.2.0--1.3.1.sql $out/share/postgresql/extension/pg_cron--1.2.0--1.3.sql + ln -s pg_cron--1.2.0--1.3.1.sql $out/share/postgresql/extension/pg_cron--1.2--1.3.1.sql + ln -s pg_cron--1.3.1--1.4.2.sql $out/share/postgresql/extension/pg_cron--1.3--1.4.sql + ln -s pg_cron--1.3.1--1.4.2.sql $out/share/postgresql/extension/pg_cron--1.3.1--1.4.sql + ln -s pg_cron--1.3.1--1.4.2.sql $out/share/postgresql/extension/pg_cron--1.3--1.4.2.sql + ln -s pg_cron--1.4.0--1.4.1.sql $out/share/postgresql/extension/pg_cron--1.4--1.4-1.sql + ln -s pg_cron--1.4.2--1.5.2.sql $out/share/postgresql/extension/pg_cron--1.4-1--1.5.sql + ln -s pg_cron--1.4.2--1.5.2.sql $out/share/postgresql/extension/pg_cron--1.4.2--1.5.sql + ln -s pg_cron--1.4.2--1.5.2.sql $out/share/postgresql/extension/pg_cron--1.4-1--1.5.2.sql + ln -s pg_cron--1.5.2--1.6.4.sql $out/share/postgresql/extension/pg_cron--1.5--1.6.sql + ln -s pg_cron--1.5.2--1.6.4.sql $out/share/postgresql/extension/pg_cron--1.5.2--1.6.sql + ln -s pg_cron--1.5.2--1.6.4.sql $out/share/postgresql/extension/pg_cron--1.5--1.6.4.sql + cat > $out/share/postgresql/extension/pg_cron--1.6--1.6.4.sql << 'EOF' + -- Version alignment migration + -- Both 1.6 and 1.6.4 are actually the same version (1.6.4) + -- This file exists only to allow smooth transition from the old naming scheme + EOF fi + # Create versioned control file with modified module path sed -e "/^default_version =/d" \ -e "/^schema =/d" \ @@ -120,6 +144,11 @@ buildEnv { numberOfVersions = numberOfVersionsBuilt; inherit switch-ext-version latestOnly; hasBackgroundWorker = true; + # pg_cron ships symlinked alias scripts and an empty 1.6 -> 1.6.4 + # alignment migration to cover legacy/non-canonical extversion strings. + # Opt in to scanning and exercising every upgrade edge, not just the + # canonical chain from versions.json. + testAllUpgradeEdges = true; defaultSettings = { shared_preload_libraries = [ "pg_cron" ]; "cron.database_name" = "postgres"; diff --git a/nix/ext/tests/default.nix b/nix/ext/tests/default.nix index 79415c20fe..6b9c23a0f1 100644 --- a/nix/ext/tests/default.nix +++ b/nix/ext/tests/default.nix @@ -230,6 +230,12 @@ let pg_regress_test_name = "${(installedExtension "15").pgRegressTestName or pname}" ext_schema = "${(installedExtension "15").defaultSchema or "public"}" lib_name = "${(installedExtension "15").libName or pname}" + test_all_upgrade_edges = ${ + if (installedExtension "15").testAllUpgradeEdges or false then "True" else "False" + } + ext_dir_15 = "${psql_15}/share/postgresql/extension" + ext_dir_17 = "${psql_17}/share/postgresql/extension" + ext_dir_orioledb = "${orioledb_17}/share/postgresql/extension" print(f"Running tests for extension: {lib_name}") ${builtins.readFile ./lib.py} @@ -247,6 +253,10 @@ let '' with subtest("Check upgrade path with postgresql 15"): test.check_upgrade_path("15") + + if test_all_upgrade_edges: + with subtest("Check all upgrade edges (incl. legacy aliases) with postgresql 15"): + test.check_all_upgrade_edges("15", ext_dir_15) '' else "" @@ -306,6 +316,10 @@ let '' with subtest("Check upgrade path with postgresql 17"): test.check_upgrade_path("17") + + if test_all_upgrade_edges: + with subtest("Check all upgrade edges (incl. legacy aliases) with postgresql 17"): + test.check_all_upgrade_edges("17", ext_dir_17) '' else "" @@ -334,6 +348,10 @@ let with subtest("Check upgrade path with orioledb 17"): test.check_upgrade_path("orioledb-17") + if test_all_upgrade_edges: + with subtest("Check all upgrade edges (incl. legacy aliases) with orioledb 17"): + test.check_all_upgrade_edges("orioledb-17", ext_dir_orioledb) + with subtest("Check pg_regress with orioledb 17 after installing the last version"): test.check_pg_regress(Path("${orioledb_17}/lib/pgxs/src/test/regress/pg_regress"), "orioledb-17", pg_regress_test_name) '' diff --git a/nix/ext/tests/lib.py b/nix/ext/tests/lib.py index 26bd42d49c..f702abbdd0 100644 --- a/nix/ext/tests/lib.py +++ b/nix/ext/tests/lib.py @@ -133,6 +133,135 @@ def check_upgrade_path(self, pg_version: str): self.drop_extension() self.install_extension(version) + def discover_upgrade_scripts(self, extension_dir: str): + """Discover the install scripts and upgrade edges in extension_dir. + + Lists all ``--*.sql`` scripts, including symlinked alias scripts + (without resolving them), and classifies each: + + - ``--V.sql`` is a base install script for version ``V``. + - ``--A--B.sql`` is an upgrade edge from ``A`` to ``B``. + + Version strings use single dashes and the separator between versions is + a double dash, so splitting the filename's version portion on ``--`` is + unambiguous (``1.4-1`` stays intact). + + Args: + extension_dir: Directory holding the extension's SQL scripts + + Returns: + Tuple of (set of base versions, sorted list of (from, to) edges) + """ + listing = self.vm.succeed( + f"ls -1 {extension_dir}/{self.extension_name}--*.sql 2>/dev/null || true" + ).strip() + prefix = f"{self.extension_name}--" + suffix = ".sql" + bases = set() + edges = set() + for line in listing.splitlines(): + name = Path(line).name + if not (name.startswith(prefix) and name.endswith(suffix)): + continue + core = name[len(prefix) : -len(suffix)] + parts = core.split("--") + if len(parts) == 1 and parts[0]: + bases.add(parts[0]) + elif len(parts) == 2 and parts[0] and parts[1]: + edges.add((parts[0], parts[1])) + return bases, sorted(edges) + + @staticmethod + def _reachable_versions(bases, edges): + """Versions installable via ``CREATE EXTENSION ... VERSION 'V'``. + + Postgres can only create a version it can reach from a base install + script by following upgrade edges. A version that only ever appears as + an edge *source* (e.g. pg_cron ``1.0``, which has no base script and no + incoming edge in this packaging) is not installable on a fresh cluster. + """ + reachable = set(bases) + changed = True + while changed: + changed = False + for frm, to in edges: + if frm in reachable and to not in reachable: + reachable.add(to) + changed = True + return reachable + + def check_all_upgrade_edges(self, pg_version: str, extension_dir: str): + """Prove every real project state upgrades cleanly to latest. + + Runs two passes over the upgrade-script graph discovered on disk + (including symlinked alias scripts and empty version-alignment + migrations such as the pg_cron 1.6 -> 1.6.4 no-op, which the canonical + check_upgrade_path never reaches because they are absent from + versions.json): + + 1. Per-edge body coverage. For each ``--A--B.sql`` whose source + ``A`` is reachable, create A from scratch and ``UPDATE TO 'B'``, + asserting the result. This guarantees every individual alias and + alignment script actually executes at least once; a shortest-path + upgrade could otherwise route around a broken one. + + 2. Full upgrade to latest from every reachable version. For every + version a real project could actually sit at -- canonical *or* + legacy-alias form -- create it from scratch and run a single + ``UPDATE TO ''``, asserting it lands on the latest + canonical version. This is the customer-faithful guarantee: any + existing project, whatever extversion string it reports, must reach + latest, with Postgres traversing alias edges as needed. If any + reachable version cannot reach latest, this pass fails loudly -- + which is exactly the stranded-project bug we want to catch. + + A version is "reachable" if a fresh cluster can come to rest at it: it + has a base install script or an incoming upgrade edge. Versions that + are neither (pg_cron ``1.0`` and ``1.4.0``: no base script, no incoming + edge) are not real project states -- no database, fresh or upgraded, + can ever sit at them -- so neither pass tests them. They lose no script + coverage either: each is a symlink to, or shares its body with, a + reachable alias edge that pass 1 runs (``1.0--1.1`` aliases + ``1.0.0--1.1.0``; ``1.4.0--1.4.1`` shares its body with the reachable + ``1.4--1.4-1`` alias). They exist only to keep the version graph + connected for legacy extversion strings, a structural property of the + package rather than an upgrade this test needs to exercise. + + The extension is restored to the latest canonical version on exit so + the subtests that follow see the expected state. + + Args: + pg_version: PostgreSQL version under test (e.g. "15") + extension_dir: Directory holding the extension's SQL scripts + """ + bases, edges = self.discover_upgrade_scripts(extension_dir) + if not edges: + return + + available = self.versions.get(pg_version, []) + latest = available[-1] if available else None + + reachable = self._reachable_versions(bases, edges) + testable = [edge for edge in edges if edge[0] in reachable] + + # Pass 1: every reachable upgrade script executes at least once. + for from_version, to_version in testable: + self.drop_extension() + self.install_extension(from_version) + self.update_extension(to_version) + + # Pass 2: every reachable version upgrades all the way to latest. + if latest is not None: + for from_version in sorted(v for v in reachable if v != latest): + self.drop_extension() + self.install_extension(from_version) + self.update_extension(latest) + + # Restore canonical latest so following subtests have expected state + self.drop_extension() + if available: + self.install_extension(available[-1]) + def check_install_last_version(self, pg_version: str) -> str: """Test if the install of the last version of the extension works for a given PostgreSQL version. diff --git a/scripts/90-cleanup.sh b/scripts/90-cleanup.sh index d221a9f275..2426db489f 100644 --- a/scripts/90-cleanup.sh +++ b/scripts/90-cleanup.sh @@ -4,7 +4,8 @@ # © 2021 DigitalOcean LLC. # This code is licensed under Apache 2.0 license (see LICENSE.md for details) -set -ex +set -o errexit +trap 'status=$?; echo "[90-cleanup] failed at line ${LINENO}: ${BASH_COMMAND} (exit ${status})" >&2' ERR # Ensure /tmp exists and has the proper permissions before # checking for security updates