supabase-postgres.git / summary / log / commit / refs
commit ad9cfaa31c64bbae7f752180efc92ff54ff27b95
Author: Márton Boros <martonboros@gmail.com>
Commit: GitHub <noreply@github.com>
Date: Fri Sep 04 15:59:37 2026 +0000
feat(nix): expose per-version extension packages, catalog, and update tools (#2317)
* feat(nix): expose per-version extension packages, catalog, and instance tools
Adds passthru.perVersion (version -> single-version derivation) to each
multi-version extension (18 files, one line each) -- the attrset already
existed inline as the buildEnv input, this surfaces it instead of
discarding it. No change to any buildEnv output or extension identity.
New flake-parts module nix/packages/extension-catalog.nix, exposing
(packages + legacyPackages):
- site-extensions-catalog-<major> (15/17/orioledb-17): pg-extensions-
catalog.json mapping { ext -> { extversion -> store path } }, plus
bin/{resolve,update} preset with PG_EXTENSIONS_CATALOG. Assembled at
build time from the wrappers' control files, so keys are real
extversions (pgsql-http nix "1.5.0" -> "1.5") and include
sub-extensions (postgis_topology, plcoffee, ...). unsafeDiscardReferences
keeps the closure to just the JSON (6.8 KiB, not 2.4 GiB).
- site-extensions-versions-<major> (legacyPackages): the generic
single-version packages the catalog points at (lean, no multi-version
buildEnv), so the CI matrix builds and caches them.
- Generic tools, one version each (not per-major): site-extensions-resolve
(manifest json -> store paths), download-nix-store-paths (retry/timeout
substitution, not extension-specific), site-extensions-update (chains
both + nix-env --install --remove-all onto PROFILE, atomic).
Part of PSQL-1530.
* docs(nix): clarify env vs profile terminology in comment
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* bashism
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
* site-extensions-resolve: fail on empty extension name instead of skipping
* download-nix-store-paths: let nix parallelize+retry instead of serial timeout loop
* inline download-nix-store-paths into site-extensions-update
* inline single-use script helper into site-extensions-resolve
* site-extensions-update: require manifest arg, no silent default
* site-extensions-update: rename PROFILE override to NIX_PROFILE
---------
Co-authored-by: Manuel Mendez <708570+mmlb@users.noreply.github.com>
nix/ext/hypopg.nix | 1 +
nix/ext/index_advisor.nix | 1 +
nix/ext/pg-safeupdate.nix | 1 +
nix/ext/pg_cron/default.nix | 1 +
nix/ext/pg_net.nix | 1 +
nix/ext/pg_partman.nix | 1 +
nix/ext/pg_repack.nix | 1 +
nix/ext/pg_tle.nix | 1 +
nix/ext/pgaudit.nix | 1 +
nix/ext/pgmq/default.nix | 1 +
nix/ext/pgroonga/default.nix | 1 +
nix/ext/pgrouting/default.nix | 1 +
nix/ext/pgsodium.nix | 1 +
nix/ext/pgsql-http.nix | 1 +
nix/ext/pgvector.nix | 1 +
nix/ext/plv8/default.nix | 1 +
nix/ext/postgis.nix | 1 +
nix/ext/vault.nix | 1 +
nix/packages/default.nix | 2 +-
nix/packages/extension-catalog.nix | 165 +++++++++++++++++++++++++++++++++++++
20 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/nix/ext/hypopg.nix b/nix/ext/hypopg.nix
index d95a1c7f..9bd49272 100644
--- a/nix/ext/hypopg.nix
+++ b/nix/ext/hypopg.nix
@@ -98,6 +98,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/index_advisor.nix b/nix/ext/index_advisor.nix
index 3130d4bc..58921271 100644
--- a/nix/ext/index_advisor.nix
+++ b/nix/ext/index_advisor.nix
@@ -83,6 +83,7 @@ pkgs.buildEnv {
];
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pg-safeupdate.nix b/nix/ext/pg-safeupdate.nix
index 168772ce..452e9c2c 100644
--- a/nix/ext/pg-safeupdate.nix
+++ b/nix/ext/pg-safeupdate.nix
@@ -83,6 +83,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pg_cron/default.nix b/nix/ext/pg_cron/default.nix
index 1fd3b403..197420c9 100644
--- a/nix/ext/pg_cron/default.nix
+++ b/nix/ext/pg_cron/default.nix
@@ -139,6 +139,7 @@ buildEnv {
};
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit switch-ext-version latestOnly;
diff --git a/nix/ext/pg_net.nix b/nix/ext/pg_net.nix
index 87b994a5..f769bad3 100644
--- a/nix/ext/pg_net.nix
+++ b/nix/ext/pg_net.nix
@@ -145,6 +145,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pg_partman.nix b/nix/ext/pg_partman.nix
index 4c36e765..0c8a4eee 100644
--- a/nix/ext/pg_partman.nix
+++ b/nix/ext/pg_partman.nix
@@ -100,6 +100,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit switch-ext-version libName latestOnly;
diff --git a/nix/ext/pg_repack.nix b/nix/ext/pg_repack.nix
index 1a7500ea..78d97640 100644
--- a/nix/ext/pg_repack.nix
+++ b/nix/ext/pg_repack.nix
@@ -140,6 +140,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pg_tle.nix b/nix/ext/pg_tle.nix
index 45ba6703..9c7ef867 100644
--- a/nix/ext/pg_tle.nix
+++ b/nix/ext/pg_tle.nix
@@ -111,6 +111,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgaudit.nix b/nix/ext/pgaudit.nix
index 74429fa4..8fb727c6 100644
--- a/nix/ext/pgaudit.nix
+++ b/nix/ext/pgaudit.nix
@@ -240,6 +240,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgmq/default.nix b/nix/ext/pgmq/default.nix
index c12531f2..7e2076a2 100644
--- a/nix/ext/pgmq/default.nix
+++ b/nix/ext/pgmq/default.nix
@@ -103,6 +103,7 @@ buildEnv {
pathsToLink = [ "/share/postgresql/extension" ];
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgroonga/default.nix b/nix/ext/pgroonga/default.nix
index 42345bd4..b9c3829a 100644
--- a/nix/ext/pgroonga/default.nix
+++ b/nix/ext/pgroonga/default.nix
@@ -181,6 +181,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgrouting/default.nix b/nix/ext/pgrouting/default.nix
index 87d190cd..cc00281e 100644
--- a/nix/ext/pgrouting/default.nix
+++ b/nix/ext/pgrouting/default.nix
@@ -153,6 +153,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgsodium.nix b/nix/ext/pgsodium.nix
index 5a615a87..b5a2dcb7 100644
--- a/nix/ext/pgsodium.nix
+++ b/nix/ext/pgsodium.nix
@@ -112,6 +112,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgsql-http.nix b/nix/ext/pgsql-http.nix
index 5815f916..885fcc47 100644
--- a/nix/ext/pgsql-http.nix
+++ b/nix/ext/pgsql-http.nix
@@ -113,6 +113,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/pgvector.nix b/nix/ext/pgvector.nix
index 5f613b3d..2db9d0c1 100644
--- a/nix/ext/pgvector.nix
+++ b/nix/ext/pgvector.nix
@@ -96,6 +96,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/plv8/default.nix b/nix/ext/plv8/default.nix
index 93a17434..73199193 100644
--- a/nix/ext/plv8/default.nix
+++ b/nix/ext/plv8/default.nix
@@ -238,6 +238,7 @@ buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/postgis.nix b/nix/ext/postgis.nix
index 99afea6f..3f722d9a 100644
--- a/nix/ext/postgis.nix
+++ b/nix/ext/postgis.nix
@@ -256,6 +256,7 @@ in
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/ext/vault.nix b/nix/ext/vault.nix
index 18ffe0aa..9ab5391b 100644
--- a/nix/ext/vault.nix
+++ b/nix/ext/vault.nix
@@ -99,6 +99,7 @@ pkgs.buildEnv {
'';
passthru = {
+ perVersion = lib.mapAttrs (name: value: build name value.hash) versionsToUse;
versions = versionsBuilt;
numberOfVersions = numberOfVersionsBuilt;
inherit pname latestOnly;
diff --git a/nix/packages/default.nix b/nix/packages/default.nix
index c6f1a293..b7ad2b20 100644
--- a/nix/packages/default.nix
+++ b/nix/packages/default.nix
@@ -4,8 +4,8 @@
./postgres.nix
./postgres-env.nix
./site-env.nix
+ ./extension-catalog.nix
];
-
perSystem =
{
inputs',
diff --git a/nix/packages/extension-catalog.nix b/nix/packages/extension-catalog.nix
new file mode 100644
index 00000000..d81dcba3
--- /dev/null
+++ b/nix/packages/extension-catalog.nix
@@ -0,0 +1,165 @@
+# Multi version extension catalog and update tools.
+{
+ perSystem =
+ {
+ pkgs,
+ lib,
+ self',
+ ...
+ }:
+ let
+ # Mirror a per-version package, adding unversioned control/library names
+ # only where missing — packages that already ship generic names (e.g.
+ # postgis and its `-3` convention) pass through untouched.
+ mkGenericVersion =
+ leanPkg:
+ pkgs.runCommand "${leanPkg.pname}-${leanPkg.version}-generic" { } ''
+ set -euo pipefail
+ shopt -s nullglob
+ mkdir -p "$out/lib" "$out/share/postgresql/extension"
+ ext="$out/share/postgresql/extension"
+
+ for f in ${leanPkg}/lib/*; do ln -s "$f" "$out/lib/$(basename "$f")"; done
+ for f in ${leanPkg}/share/postgresql/extension/*; do ln -s "$f" "$ext/$(basename "$f")"; done
+
+ # <name>.control with default_version, from <name>--<version>.control
+ for cf in "$ext"/*--*.control; do
+ b=$(basename "$cf")
+ n=''${b%%--*}
+ v=''${b#*--}; v=''${v%.control}
+ [[ -e "$ext/$n.control" ]] && continue
+ { echo "default_version = '$v'"; cat "$cf"; } > "$ext/$n.control"
+ done
+
+ # <base>.so -> <base>-<version>.so
+ for f in "$out"/lib/*-[0-9]*; do
+ b=$(basename "$f")
+ suffix=.''${b##*.}
+ core=''${b%.*}
+ base=''${core%-[0-9]*}
+ [[ "$base" != "$core" ]] || continue
+ gen=$base$suffix
+ [[ -e "$out/lib/$gen" ]] || ln -sfn "$b" "$out/lib/$gen"
+ done
+
+ # Sanity check. No generic control is fine only for preload-only
+ # modules (e.g. pg-safeupdate), which still ship a library.
+ generic=0
+ for c in "$ext"/*.control; do
+ case "$(basename "$c")" in
+ *--*) ;;
+ *) generic=$((generic + 1)) ;;
+ esac
+ done
+ libs=$(ls -A "$out/lib" 2>/dev/null | wc -l)
+ if ((generic == 0)) && ((libs == 0)); then
+ echo "extension-catalog: empty wrapper for ${leanPkg.pname} (no control, no lib)" >&2
+ exit 1
+ fi
+ '';
+
+ wrappersFor =
+ extsSet:
+ lib.mapAttrs (_: drv: lib.mapAttrs (_: mkGenericVersion) drv.perVersion) (
+ lib.filterAttrs (_: drv: lib.isDerivation drv && drv ? perVersion) extsSet
+ );
+
+ # Make catalog json from a set of wrappers: { <ext>: { <version>: <path> } }
+ # Keyed on each wrapper's actual default_version, not the nix attr name,
+ # which can differ (pgsql-http "1.5.0" builds extversion "1.5").
+ mkCatalogFile =
+ wrappers:
+ pkgs.runCommand "pg-extensions-catalog"
+ {
+ paths = map toString (lib.concatMap lib.attrValues (lib.attrValues wrappers));
+ nativeBuildInputs = [ pkgs.jq ];
+ # Without this the literal wrapper paths in the JSON would pull
+ # every wrapper's closure into the catalog's runtime closure.
+ __structuredAttrs = true;
+ unsafeDiscardReferences.out = true;
+ }
+ ''
+ set -euo pipefail
+ obj='{}'
+ for path in "''${paths[@]}"; do
+ for ctrl in "$path"/share/postgresql/extension/*.control; do
+ base=$(basename "$ctrl")
+ case "$base" in *--*) continue ;; esac
+ name=''${base%.control}
+ ver=$(sed -n "s/^default_version = '\(.*\)'.*/\1/p" "$ctrl" | head -1)
+ [[ -n "$ver" ]] || { echo "no default_version in $ctrl" >&2; exit 1; }
+ obj=$(printf '%s' "$obj" | jq --arg e "$name" --arg v "$ver" --arg p "$path" '.[$e][$v] = $p')
+ done
+ done
+ mkdir -p "$out/share"
+ printf '%s\n' "$obj" | jq -S . > "$out/share/pg-extensions-catalog.json"
+ '';
+
+ # Multi version extensions as attrsets: { <major> = { <ext> = { <version> = drv; }; }; }
+ perMajor = lib.genAttrs [ "15" "17" "orioledb-17" ] (
+ major: wrappersFor self'.legacyPackages."psql_${major}".exts
+ );
+
+ # Catalog json plus bin/site-extensions-{resolve,update} that use it by default.
+ catalogs = lib.mapAttrs' (
+ major: wrappers:
+ lib.nameValuePair "site-extensions-catalog-${major}" (
+ pkgs.runCommand "site-extensions-catalog" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
+ mkdir -p "$out/share" "$out/bin"
+ ln -s ${mkCatalogFile wrappers}/share/pg-extensions-catalog.json "$out/share/"
+ makeWrapper ${self'.packages.site-extensions-resolve}/bin/site-extensions-resolve \
+ "$out/bin/site-extensions-resolve" \
+ --set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json"
+ makeWrapper ${self'.packages.site-extensions-update}/bin/site-extensions-update \
+ "$out/bin/site-extensions-update" \
+ --set PG_EXTENSIONS_CATALOG "$out/share/pg-extensions-catalog.json"
+ ''
+ )
+ ) perMajor;
+
+ versions = lib.mapAttrs' (
+ major: wrappers:
+ lib.nameValuePair "site-extensions-versions-${major}" (
+ lib.recurseIntoAttrs (lib.mapAttrs (_: lib.recurseIntoAttrs) wrappers)
+ )
+ ) perMajor;
+ in
+ {
+ packages = catalogs // {
+ # Takes manifest json as argument. Format: {<ext>: <version>}.
+ # Prints nix-store paths of the resolved extensions, one per line.
+ # Does not download or install.
+ site-extensions-resolve = pkgs.writeShellApplication {
+ name = "site-extensions-resolve";
+ runtimeInputs = [
+ pkgs.coreutils
+ pkgs.jq
+ ];
+ text = ''
+ : "''${PG_EXTENSIONS_CATALOG:?PG_EXTENSIONS_CATALOG must point at a pg-extensions-catalog.json}"
+ manifest="''${1:?Usage: $0 path-to/pg-extensions.json}"
+ jq -r 'to_entries[] | "\(.key)=\(.value)"' "$manifest" | while IFS='=' read -r name version; do
+ [ -n "$name" ] || { echo "ERROR: empty extension name in $manifest" >&2; exit 1; }
+ jq -er --arg n "$name" --arg v "$version" \
+ '.[$n][$v] // error("\($n)=\($v) not in catalog")' "$PG_EXTENSIONS_CATALOG"
+ done | sort -u
+ '';
+ };
+
+ # Takes manifest json as argument.
+ # Downloads paths and installs them as an env into the profile, replacing all existing ones.
+ site-extensions-update = pkgs.writeShellApplication {
+ name = "site-extensions-update";
+ runtimeInputs = [ self'.packages.site-extensions-resolve ];
+ text = ''
+ manifest="''${1:?Usage: $0 path-to/pg-extensions.json}"
+ profile="''${NIX_PROFILE:-/nix/var/nix/profiles/site-extensions}"
+ readarray -t paths < <(site-extensions-resolve "$manifest")
+ nix-store -r --option stalled-download-timeout 120 "''${paths[@]}" >/dev/null
+ nix-env --profile "$profile" --install "''${paths[@]}" --remove-all
+ '';
+ };
+ };
+ legacyPackages = catalogs // versions;
+ };
+}