diff --git a/nix/ext/pg_cron/default.nix b/nix/ext/pg_cron/default.nix index a823fa450b..2434553329 100644 --- a/nix/ext/pg_cron/default.nix +++ b/nix/ext/pg_cron/default.nix @@ -23,6 +23,25 @@ let supportedVersions; versionsBuilt = if latestOnly then [ latestVersion ] else versions; numberOfVersionsBuilt = builtins.length versionsBuilt; + # Legacy bare extversion strings mapped to their schema-identical canonical + # version. Shared via passthru so the test can't drift from this table. + legacyVersions = { + "1.0" = "1.0.0"; + "1.1" = "1.1.0"; + "1.2" = "1.2.0"; + "1.3" = "1.3.1"; + "1.4" = "1.4.0"; + "1.4-1" = "1.4.2"; + "1.5" = "1.5.2"; + "1.6" = "1.6.4"; + }; + legacyAlignmentMigrations = lib.concatStrings ( + lib.mapAttrsToList (legacy: canonical: '' + cat > $out/share/postgresql/extension/${pname}--${legacy}--${canonical}.sql << 'EOF' + -- Alignment migration: ${legacy} and ${canonical} are schema-identical. + EOF + '') legacyVersions + ); build = version: versionData: stdenv.mkDerivation rec { @@ -57,12 +76,17 @@ let mv $out/share/postgresql/extension/pg_cron--1.0--1.1.sql $out/share/postgresql/extension/pg_cron--1.0.0--1.1.0.sql mv $out/share/postgresql/extension/pg_cron--1.1--1.2.sql $out/share/postgresql/extension/pg_cron--1.1.0--1.2.0.sql mv $out/share/postgresql/extension/pg_cron--1.2--1.3.sql $out/share/postgresql/extension/pg_cron--1.2.0--1.3.1.sql - mv $out/share/postgresql/extension/pg_cron--1.3--1.4.sql $out/share/postgresql/extension/pg_cron--1.3.1--1.4.2.sql + mv $out/share/postgresql/extension/pg_cron--1.3--1.4.sql $out/share/postgresql/extension/pg_cron--1.3.1--1.4.0.sql 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 + cat > $out/share/postgresql/extension/pg_cron--1.4.1--1.4.2.sql << 'EOF' + -- Alignment migration: 1.4.1 and 1.4.2 are schema-identical. + EOF 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 + ${legacyAlignmentMigrations} fi + # Create versioned control file with modified module path sed -e "/^default_version =/d" \ -e "/^schema =/d" \ @@ -120,6 +144,7 @@ buildEnv { numberOfVersions = numberOfVersionsBuilt; inherit switch-ext-version latestOnly; hasBackgroundWorker = true; + inherit legacyVersions; 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..f0d8e37d46 100644 --- a/nix/ext/tests/default.nix +++ b/nix/ext/tests/default.nix @@ -215,6 +215,7 @@ let in '' from pathlib import Path + import json versions = { "15": [${lib.concatStringsSep ", " (map (s: ''"${s}"'') (versions "15"))}], "17": [${lib.concatStringsSep ", " (map (s: ''"${s}"'') (versions "17"))}], @@ -230,6 +231,7 @@ let pg_regress_test_name = "${(installedExtension "15").pgRegressTestName or pname}" ext_schema = "${(installedExtension "15").defaultSchema or "public"}" lib_name = "${(installedExtension "15").libName or pname}" + legacy_versions = json.loads('${builtins.toJSON ((installedExtension "15").legacyVersions or { })}') print(f"Running tests for extension: {lib_name}") ${builtins.readFile ./lib.py} @@ -247,6 +249,10 @@ let '' with subtest("Check upgrade path with postgresql 15"): test.check_upgrade_path("15") + + if legacy_versions: + with subtest("Check legacy extversion strings upgrade to latest with postgresql 15"): + test.check_legacy_versions_upgrade_to_latest("15", legacy_versions) '' else "" @@ -306,6 +312,10 @@ let '' with subtest("Check upgrade path with postgresql 17"): test.check_upgrade_path("17") + + if legacy_versions: + with subtest("Check legacy extversion strings upgrade to latest with postgresql 17"): + test.check_legacy_versions_upgrade_to_latest("17", legacy_versions) '' else "" @@ -334,6 +344,10 @@ let with subtest("Check upgrade path with orioledb 17"): test.check_upgrade_path("orioledb-17") + if legacy_versions: + with subtest("Check legacy extversion strings upgrade to latest with orioledb 17"): + test.check_legacy_versions_upgrade_to_latest("orioledb-17", legacy_versions) + 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..7443802242 100644 --- a/nix/ext/tests/lib.py +++ b/nix/ext/tests/lib.py @@ -133,6 +133,59 @@ def check_upgrade_path(self, pg_version: str): self.drop_extension() self.install_extension(version) + def check_legacy_versions_upgrade_to_latest( + self, pg_version: str, legacy_versions: Mapping[str, str] + ): + """Prove every legacy extversion string upgrades cleanly to latest. + + legacy_versions maps a legacy/bare extversion string (e.g. pg_cron's + "1.6") to the schema-identical canonical version it has an alignment + migration to (e.g. "1.6.4"). Postgres can't install a legacy string + directly -- there's no base script for it, only an alignment edge off + of it -- so for each entry this installs the canonical version fresh, + then patches pg_extension.extversion directly to the legacy string. + This reproduces the actual field state (and is the same manual fix + used to recover a stuck project) rather than a state Postgres would + ever arrive at on its own. + + From there, a bare ALTER EXTENSION UPDATE (no explicit target version) + is run -- matching how the real upgrade-completion path invokes it -- + and the result must land on the latest canonical version. If any + legacy string has no path to latest, this fails loudly, which is + exactly the stranded-project bug this test exists to catch. + + 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") + legacy_versions: Mapping of legacy extversion string to the + canonical version it is schema-identical to + """ + if not legacy_versions: + return + + available = self.versions.get(pg_version, []) + if not available: + raise ValueError( + f"No versions available for PostgreSQL version {pg_version}" + ) + latest = available[-1] + + for legacy_version, canonical_version in legacy_versions.items(): + self.drop_extension() + self.install_extension(canonical_version) + self.run_sql( + f"UPDATE pg_extension SET extversion = '{legacy_version}' " + f"WHERE extname = '{self.extension_name}';" + ) + self.run_sql(f"ALTER EXTENSION {self.extension_name} UPDATE;") + self.assert_version_matches(latest) + + # Restore canonical latest so following subtests have expected state + self.drop_extension() + self.install_extension(latest) + 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.