diff --git a/src/extensions/artifacts/native/moon.yml b/src/extensions/artifacts/native/moon.yml index 5d7e2ad3f..31ebf19d0 100644 --- a/src/extensions/artifacts/native/moon.yml +++ b/src/extensions/artifacts/native/moon.yml @@ -82,6 +82,7 @@ tasks: - "/tools/release/extension-upstream-licenses.mjs" - "/tools/release/linux-abi-baseline.test.mjs" - "/tools/release/materialize-release-symlinks.mjs" + - "/tools/release/native-extension-qualification.mjs" - "/tools/release/extension-artifact-archive-policy.mjs" - "/tools/release/native-extension-asset-index-contract.mjs" - "/tools/release/native-runtime-payload-policy.json" @@ -127,6 +128,7 @@ tasks: - "/tools/release/extension-upstream-licenses.mjs" - "/tools/release/linux-abi-baseline.test.mjs" - "/tools/release/materialize-release-symlinks.mjs" + - "/tools/release/native-extension-qualification.mjs" - "/tools/release/extension-artifact-archive-policy.mjs" - "/tools/release/native-extension-asset-index-contract.mjs" - "/tools/release/native-runtime-payload-policy.json" diff --git a/src/extensions/artifacts/native/tools/package-release-assets.sh b/src/extensions/artifacts/native/tools/package-release-assets.sh index c7326d964..5bc387322 100755 --- a/src/extensions/artifacts/native/tools/package-release-assets.sh +++ b/src/extensions/artifacts/native/tools/package-release-assets.sh @@ -37,6 +37,7 @@ esac require awk require bun native_extension_runtime_kind="$(bun "$native_asset_index_contract" runtime-kind)" +native_extension_qualification="tools/release/native-extension-qualification.mjs" qualification_only="${OLIPHAUNT_EXTENSION_QUALIFICATION_ONLY:-0}" case "$qualification_only" in 0|1) ;; @@ -595,12 +596,29 @@ make_extension_artifact() { } package_desktop_target() { - local source_runtime embedded_modules runtime binary_contract_runtime + local source_runtime embedded_modules runtime binary_contract_runtime qualification_count build_desktop_extension_runtime source_runtime="$(host_extension_runtime_root)" embedded_modules="$(host_extension_embedded_modules_root)" require_dir "$source_runtime" "$target_id extension runtime" require_dir "$embedded_modules" "$target_id embedded extension modules" + qualification_count="$(bun "$native_extension_qualification" plan \ + --target "$target_id" \ + --selected-sql-names "$build_sql_names" \ + --format count)" + case "$qualification_count" in + ""|*[!0-9]*) fail "native extension qualification planner returned an invalid count: $qualification_count" ;; + esac + if [ "$qualification_count" -gt 0 ]; then + "$observed_phase" \ + --label "qualify declared native extension tests ($qualification_count plan rows)" \ + --log "/tmp/liboliphaunt-release-native-extension-qualification-$target_id.log" \ + -- bun "$native_extension_qualification" run \ + --target "$target_id" \ + --selected-sql-names "$build_sql_names" \ + --runtime "$source_runtime" \ + --format count + fi runtime="$(prepare_extension_release_runtime "$source_runtime")" if [ "$target_id" = "windows-x64-msvc" ]; then tools/dev/bun.sh tools/release/windows-vc-runtime-closure.mjs verify \ diff --git a/src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh b/src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh new file mode 100644 index 000000000..ff587d212 --- /dev/null +++ b/src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)" || { + echo "run-pgxs-installcheck.sh: unable to determine the repository root" >&2 + exit 1 +} + +fail() { + echo "run-pgxs-installcheck.sh: $*" >&2 + exit 1 +} + +require_file() { + if [ ! -f "$1" ] || [ -L "$1" ]; then + fail "missing regular $2 at $1" + fi +} + +runtime="${OLIPHAUNT_EXTENSION_CURRENT_RUNTIME:-}" +sql_name="${OLIPHAUNT_EXTENSION_SQL_NAME:-}" +source_name="${OLIPHAUNT_EXTENSION_SOURCE_NAME:-}" +source_commit="${OLIPHAUNT_EXTENSION_SOURCE_COMMIT:-}" +included_suites="${OLIPHAUNT_EXTENSION_INCLUDED_SUITES:-}" +suite_target_prefix="${OLIPHAUNT_EXTENSION_SUITE_TARGET_PREFIX:-}" +aggregate_suites="${OLIPHAUNT_EXTENSION_AGGREGATE_SUITES:-}" +excluded_suites="${OLIPHAUNT_EXTENSION_EXCLUDED_SUITES:-}" +preload_libraries="${OLIPHAUNT_EXTENSION_SHARED_PRELOAD_LIBRARIES:-}" +test_locale="${OLIPHAUNT_EXTENSION_TEST_LOCALE:-}" + +case "$sql_name" in + ""|*[!a-z0-9_-]*) fail "OLIPHAUNT_EXTENSION_SQL_NAME must be a safe SQL extension name" ;; +esac +case "$source_name" in + ""|*[!a-z0-9_-]*) fail "OLIPHAUNT_EXTENSION_SOURCE_NAME must be a safe source name" ;; +esac +case "$source_commit" in + *[!0-9a-f]*) fail "OLIPHAUNT_EXTENSION_SOURCE_COMMIT must be a full lowercase Git SHA" ;; +esac +[ "${#source_commit}" -eq 40 ] || \ + fail "OLIPHAUNT_EXTENSION_SOURCE_COMMIT must be a full lowercase Git SHA" +[ "$included_suites" = "regress" ] || \ + fail "pgxs-installcheck currently requires included_suites = [\"regress\"]" +case "$suite_target_prefix" in + ""|*[!a-z0-9_-]*) fail "OLIPHAUNT_EXTENSION_SUITE_TARGET_PREFIX must be a safe Make target prefix" ;; +esac +case "$aggregate_suites" in + ""|*[!a-z0-9_,-]*) fail "OLIPHAUNT_EXTENSION_AGGREGATE_SUITES contains an unsafe suite name" ;; +esac +case "$excluded_suites" in + *[!a-z0-9_,-]*) fail "OLIPHAUNT_EXTENSION_EXCLUDED_SUITES contains an unsafe suite name" ;; +esac +case "$preload_libraries" in + *[!a-z0-9_,-]*) fail "OLIPHAUNT_EXTENSION_SHARED_PRELOAD_LIBRARIES contains an unsafe library name" ;; +esac +case "$test_locale" in + ""|*[!A-Za-z0-9._@-]*) fail "OLIPHAUNT_EXTENSION_TEST_LOCALE must be a safe explicit locale" ;; +esac + +[ -n "$runtime" ] || fail "OLIPHAUNT_EXTENSION_CURRENT_RUNTIME must name the exact candidate runtime" +runtime="$(cd "$runtime" 2>/dev/null && pwd)" || fail "runtime is not a directory: $runtime" +for command_name in awk cmp cp git make mkdir mktemp rm rsync sed sort; do + command -v "$command_name" >/dev/null 2>&1 || fail "missing required command: $command_name" +done +for tool in initdb pg_config pg_ctl postgres psql; do + require_file "$runtime/bin/$tool" "runtime tool $tool" +done +require_file "$runtime/lib/postgresql/$sql_name.so" "$sql_name module" +require_file "$runtime/share/postgresql/extension/$sql_name.control" "$sql_name control file" + +checkout="$root/target/oliphaunt-sources/checkouts/$source_name" +if [ ! -d "$checkout" ] || [ -L "$checkout" ] || [ ! -d "$checkout/.git" ] || [ -L "$checkout/.git" ]; then + fail "missing verified $source_name checkout; run the source-fetch-native-runtime dependency" +fi +actual_commit="$(git -C "$checkout" rev-parse --verify 'HEAD^{commit}')" +[ "$actual_commit" = "$source_commit" ] || \ + fail "$source_name checkout is at $actual_commit, expected $source_commit" +[ -z "$(git -C "$checkout" status --porcelain=v1 --untracked-files=all)" ] || \ + fail "$source_name checkout must be clean" + +work_parent="$root/target/extension-upstream-installcheck" +mkdir -p "$work_parent" +if [ ! -d "$work_parent" ] || [ -L "$work_parent" ]; then + fail "upstream test work parent must be a real directory: $work_parent" +fi +work_root="$(mktemp -d "$work_parent/$sql_name.XXXXXX")" +source_dir="$work_root/source" +data_dir="$work_root/data" +socket_dir="$(mktemp -d "/tmp/oliphaunt-$sql_name-pgxs.XXXXXX")" +server_log="$work_root/postgres.log" +preserved_log="/tmp/oliphaunt-$sql_name-upstream-postgres.log" +preserved_diff="/tmp/oliphaunt-$sql_name-upstream-regression.diffs" +server_start_attempted=0 +server_running=0 + +cleanup() { + local status="$?" + local may_remove=1 + trap - EXIT HUP INT TERM + if [ "$server_running" = 1 ] || [ -f "$data_dir/postmaster.pid" ] || \ + { [ "$server_start_attempted" = 1 ] && "$runtime/bin/pg_ctl" status -D "$data_dir" >/dev/null 2>&1; }; then + if ! "$runtime/bin/pg_ctl" stop -D "$data_dir" -m immediate -w >/dev/null 2>&1; then + may_remove=0 + echo "run-pgxs-installcheck.sh: could not confirm server shutdown; preserving $work_root" >&2 + fi + fi + if [ "$status" -ne 0 ]; then + if [ -f "$server_log" ]; then + cp "$server_log" "$preserved_log" 2>/dev/null || true + echo "run-pgxs-installcheck.sh: preserved failing server log at $preserved_log" >&2 + fi + if [ -f "$source_dir/test/regression.diffs" ]; then + cp "$source_dir/test/regression.diffs" "$preserved_diff" 2>/dev/null || true + echo "run-pgxs-installcheck.sh: preserved regression diff at $preserved_diff" >&2 + fi + fi + if [ "$may_remove" = 1 ] && [ ! -f "$data_dir/postmaster.pid" ]; then + rm -rf "$work_root" + rm -rf "$socket_dir" + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +rsync -a --delete --exclude .git/ "$checkout/" "$source_dir/" + +make_database="$work_root/make-database.txt" +make_targets="$work_root/make-test-targets.txt" +declared_targets="$work_root/declared-test-targets.txt" +make_database_result=0 +PATH="$runtime/bin:$PATH" make -C "$source_dir" -qp \ + PG_CONFIG="$runtime/bin/pg_config" >"$make_database" 2>&1 || make_database_result="$?" +case "$make_database_result" in + 0|1) ;; + *) fail "cannot inspect the pinned $source_name Make target database (exit $make_database_result)" ;; +esac +LC_ALL=C awk -F: -v prefix="$suite_target_prefix" \ + 'index($1, prefix) == 1 && $1 ~ /^[a-z][a-z0-9_-]*$/ { print $1 }' \ + "$make_database" | LC_ALL=C sort -u >"$make_targets" +IFS=',' read -r -a declared_target_names <<<"$aggregate_suites,$excluded_suites" +printf '%s\n' "${declared_target_names[@]}" | LC_ALL=C sort -u >"$declared_targets" +if ! cmp -s "$make_targets" "$declared_targets"; then + echo "run-pgxs-installcheck.sh: discovered pinned Make test targets:" >&2 + sed 's/^/ /' "$make_targets" >&2 + echo "run-pgxs-installcheck.sh: declared aggregate/excluded test targets:" >&2 + sed 's/^/ /' "$declared_targets" >&2 + fail "upstream Make test target inventory drifted from the qualification manifest" +fi + +PATH="$runtime/bin:$PATH" "$runtime/bin/initdb" \ + -D "$data_dir" --auth-local=trust --auth-host=reject \ + --locale="$test_locale" --encoding=UTF8 >/dev/null +if [[ "$socket_dir" == *"'"* ]]; then + fail "upstream test socket path must not contain a single quote" +fi +{ + printf "listen_addresses = ''\n" + printf "unix_socket_directories = '%s'\n" "$socket_dir" + printf "fsync = off\n" + printf "full_page_writes = off\n" + if [ -n "$preload_libraries" ]; then + printf "shared_preload_libraries = '%s'\n" "$preload_libraries" + fi +} >>"$data_dir/postgresql.conf" + +server_start_attempted=1 +PATH="$runtime/bin:$PATH" "$runtime/bin/pg_ctl" \ + start -D "$data_dir" -l "$server_log" -w >/dev/null +server_running=1 +server_start_attempted=0 + +echo "running $sql_name upstream suites: $included_suites" +if [ -n "$excluded_suites" ]; then + echo "documented non-PGXS suites excluded from this runner: $excluded_suites" +fi +PATH="$runtime/bin:$PATH" \ + PGHOST="$socket_dir" \ + PGPORT=5432 \ + make -C "$source_dir" \ + PG_CONFIG="$runtime/bin/pg_config" \ + installcheck + +PATH="$runtime/bin:$PATH" "$runtime/bin/pg_ctl" \ + stop -D "$data_dir" -m fast -w >/dev/null +server_running=0 + +echo "$sql_name upstream PGXS installcheck passed at $source_commit" diff --git a/src/extensions/catalog/extensions.source.json b/src/extensions/catalog/extensions.source.json index 83cf220a3..ca1a9af20 100644 --- a/src/extensions/catalog/extensions.source.json +++ b/src/extensions/catalog/extensions.source.json @@ -934,7 +934,7 @@ "control": { "module-pathname": "$libdir/pg_textsearch", "requires": [], - "relocatable": "true" + "relocatable": "false" }, "dependencies": [], "native-dependencies": [], diff --git a/src/extensions/contrib/.release-extension-metadata.json b/src/extensions/contrib/.release-extension-metadata.json new file mode 100644 index 000000000..991d09453 --- /dev/null +++ b/src/extensions/contrib/.release-extension-metadata.json @@ -0,0 +1,1151 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-contrib-pg18" +} diff --git a/src/extensions/contrib/.release-semantic-inputs.json b/src/extensions/contrib/.release-semantic-inputs.json index 2cae8a548..08d4e1d9b 100644 --- a/src/extensions/contrib/.release-semantic-inputs.json +++ b/src/extensions/contrib/.release-semantic-inputs.json @@ -94,6 +94,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -134,7 +176,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -166,7 +208,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -210,18 +252,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -377,5 +407,5 @@ ] } ], - "sha256": "419f620e12df219054374188e11b362780ba9e436f547348c20acd0bf7c29d24" + "sha256": "81c18296e77c1ff93ef309ce6d9933f6147c2c189a13029acbe166ce0feca457" } diff --git a/src/extensions/evidence/matrix.toml b/src/extensions/evidence/matrix.toml index 3d5c7155a..ea20256cf 100644 --- a/src/extensions/evidence/matrix.toml +++ b/src/extensions/evidence/matrix.toml @@ -9,11 +9,15 @@ source-digest-inputs = [ "src/extensions/generated/extensions.build-plan.json", "src/extensions/generated/contrib-build.tsv", "src/extensions/generated/pgxs-build.tsv", + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh", "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "tools/release/native-extension-qualification.mjs", "src/extensions/external/age/source.toml", "src/extensions/external/pg_hashids/source.toml", "src/extensions/external/pg_ivm/source.toml", "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_textsearch/tests/upgrade/source.toml", "src/extensions/external/pg_uuidv7/source.toml", "src/extensions/external/pgtap/source.toml", "src/extensions/external/postgis/dependencies/geos/source.toml", @@ -34,9 +38,12 @@ source-digest-inputs = [ "src/extensions/external/pg_ivm/targets/native-static-registry.toml", "src/extensions/external/pg_ivm/upstream-license-data.json", "src/extensions/external/pg_textsearch/moon.yml", + "src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch", + "src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json", "src/extensions/external/pg_textsearch/recipe.toml", "src/extensions/external/pg_textsearch/targets/native-static-registry.toml", "src/extensions/external/pg_textsearch/tests/smoke.sql", + "src/extensions/external/pg_textsearch/tests/upgrade.sh", "src/extensions/external/pg_textsearch/tests/upstream.toml", "src/extensions/external/pg_textsearch/upstream-license-data.json", "src/extensions/external/pg_uuidv7/moon.yml", diff --git a/src/extensions/external/pg_hashids/.release-extension-metadata.json b/src/extensions/external/pg_hashids/.release-extension-metadata.json new file mode 100644 index 000000000..df17ddb91 --- /dev/null +++ b/src/extensions/external/pg_hashids/.release-extension-metadata.json @@ -0,0 +1,52 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-pg-hashids" +} diff --git a/src/extensions/external/pg_hashids/.release-semantic-inputs.json b/src/extensions/external/pg_hashids/.release-semantic-inputs.json index e10d6f048..d39bf3e26 100644 --- a/src/extensions/external/pg_hashids/.release-semantic-inputs.json +++ b/src/extensions/external/pg_hashids/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "cd9da3b78ef5e8df01dacab7774205aef15270bf3d20dee1718b81bad78c13b1" + "sha256": "3375f6309cc511e0691801c55fda43e33ed398cfd071d56b99dcd995a3769d31" } diff --git a/src/extensions/external/pg_ivm/.release-extension-metadata.json b/src/extensions/external/pg_ivm/.release-extension-metadata.json new file mode 100644 index 000000000..5a857bb81 --- /dev/null +++ b/src/extensions/external/pg_ivm/.release-extension-metadata.json @@ -0,0 +1,52 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-pg-ivm" +} diff --git a/src/extensions/external/pg_ivm/.release-semantic-inputs.json b/src/extensions/external/pg_ivm/.release-semantic-inputs.json index 46bcd81a4..3ed87b380 100644 --- a/src/extensions/external/pg_ivm/.release-semantic-inputs.json +++ b/src/extensions/external/pg_ivm/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "93852eada395596f863f6ff4bf11709c09758e8720e2f92ec0ae315a80ea07ef" + "sha256": "0a6abaa11d66b5a178f7cfb46d780d0bcfaaca534ff5ea26557416b387a181cd" } diff --git a/src/extensions/external/pg_textsearch/.release-extension-metadata.json b/src/extensions/external/pg_textsearch/.release-extension-metadata.json new file mode 100644 index 000000000..78f63e8d6 --- /dev/null +++ b/src/extensions/external/pg_textsearch/.release-extension-metadata.json @@ -0,0 +1,68 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [ + "pg_textsearch" + ], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-pg-textsearch" +} diff --git a/src/extensions/external/pg_textsearch/.release-semantic-inputs.json b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json index aeac4dcf2..a7058e618 100644 --- a/src/extensions/external/pg_textsearch/.release-semantic-inputs.json +++ b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "93af1bc0cc1039d7285d1bfdefeca1b1a959c89bb0c523490711b59601b3c427" + "sha256": "b237873e008c6741e47c8c0e2f3622ec52087a5f0a62e39f2763228f1e2aac29" } diff --git a/src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch b/src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch new file mode 100644 index 000000000..2f098f5ae --- /dev/null +++ b/src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch @@ -0,0 +1,226 @@ +diff --git a/src/access/am.h b/src/access/am.h +--- a/src/access/am.h ++++ b/src/access/am.h +@@ -82,7 +82,7 @@ float8 tp_get_cached_score(void); + /* + * Access method handler + */ +-Datum tp_handler(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tp_handler(PG_FUNCTION_ARGS); + + /* + * Build utilities (am/build.c) +diff --git a/src/memtable/expull.h b/src/memtable/expull.h +--- a/src/memtable/expull.h ++++ b/src/memtable/expull.h +@@ -24,12 +24,16 @@ typedef struct TpExpullBlock + * Single posting entry stored in EXPULL blocks. + * 7 bytes of data; packed to avoid waste in blocks. + */ ++#pragma pack(push, 1) + typedef struct TpExpullEntry + { + uint32 doc_id; /* Segment-local document ID */ + uint16 frequency; /* Term frequency in document */ + uint8 fieldnorm; /* Quantized document length */ +-} __attribute__((packed)) TpExpullEntry; ++} TpExpullEntry; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpExpullEntry) == 7, "TpExpullEntry must remain 7 bytes on Windows"); ++StaticAssertDecl(__alignof(TpExpullEntry) == 1, "TpExpullEntry must remain 1-byte aligned on Windows"); + + #define TP_EXPULL_ENTRY_SIZE sizeof(TpExpullEntry) /* 7 bytes */ + +diff --git a/src/oliphaunt_windows_compat.h b/src/oliphaunt_windows_compat.h +new file mode 100644 +--- /dev/null ++++ b/src/oliphaunt_windows_compat.h +@@ -0,0 +1,7 @@ ++#pragma once ++ ++#ifdef _MSC_VER ++#ifndef __attribute__ ++#define __attribute__(x) ++#endif ++#endif +diff --git a/src/segment/format.h b/src/segment/format.h +--- a/src/segment/format.h ++++ b/src/segment/format.h +@@ -174,13 +174,17 @@ typedef struct TpStringEntry + /* + * V3 legacy dictionary entry - 12 bytes + */ ++#pragma pack(push, 4) + typedef struct TpDictEntryV3 + { + uint32 skip_index_offset; + uint16 block_count; + uint16 reserved; + uint32 doc_freq; +-} __attribute__((aligned(4))) TpDictEntryV3; ++} TpDictEntryV3; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpDictEntryV3) == 12, "TpDictEntryV3 must remain 12 bytes on Windows"); ++StaticAssertDecl(__alignof(TpDictEntryV3) == 4, "TpDictEntryV3 must remain 4-byte aligned on Windows"); + + /* + * Dictionary entry - 16 bytes, block-based storage (V4: uint64 offset) +@@ -195,12 +199,16 @@ typedef struct TpDictEntryV3 + * (x86-64, ARM64) these four bytes read identically as a uint32, + * so existing V4 segments are binary-compatible. + */ ++#pragma pack(push, 8) + typedef struct TpDictEntry + { + uint64 skip_index_offset; /* Offset to TpSkipEntry array for this term */ + uint32 block_count; /* Number of blocks (and skip entries) */ + uint32 doc_freq; /* Document frequency for IDF */ +-} __attribute__((aligned(8))) TpDictEntry; ++} TpDictEntry; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpDictEntry) == 16, "TpDictEntry must remain 16 bytes on Windows"); ++StaticAssertDecl(__alignof(TpDictEntry) == 8, "TpDictEntry must remain 8-byte aligned on Windows"); + + /* + * Block storage constants +@@ -210,6 +218,7 @@ typedef struct TpDictEntry + /* + * V3 legacy skip index entry - 16 bytes per block + */ ++#pragma pack(push, 1) + typedef struct TpSkipEntryV3 + { + uint32 last_doc_id; +@@ -219,5 +228,8 @@ typedef struct TpSkipEntryV3 + uint32 posting_offset; + uint8 flags; + uint8 reserved[3]; +-} __attribute__((packed)) TpSkipEntryV3; ++} TpSkipEntryV3; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpSkipEntryV3) == 16, "TpSkipEntryV3 must remain 16 bytes on Windows"); ++StaticAssertDecl(__alignof(TpSkipEntryV3) == 1, "TpSkipEntryV3 must remain 1-byte aligned on Windows"); + +@@ -224,8 +236,9 @@ typedef struct TpSkipEntryV3 + * Skip index entry - 20 bytes per block (V4: uint64 posting_offset) + * + * Stored separately from posting data for cache efficiency during BMW. + * The skip index is a dense array of these entries, one per block. + */ ++#pragma pack(push, 1) + typedef struct TpSkipEntry + { + uint32 last_doc_id; /* Last segment-local doc ID in block */ +@@ -236,7 +249,10 @@ typedef struct TpSkipEntry + uint64 posting_offset; /* Byte offset from segment start to block data */ + uint8 flags; /* Compression type, etc. */ + uint8 reserved[3]; /* Future use */ +-} __attribute__((packed)) TpSkipEntry; ++} TpSkipEntry; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpSkipEntry) == 20, "TpSkipEntry must remain 20 bytes on Windows"); ++StaticAssertDecl(__alignof(TpSkipEntry) == 1, "TpSkipEntry must remain 1-byte aligned on Windows"); + + /* Skip entry flags */ + #define TP_BLOCK_FLAG_UNCOMPRESSED 0x00 /* Raw doc IDs and frequencies */ +@@ -270,7 +286,11 @@ typedef struct TpBlockPosting + * compact 4-byte doc IDs in posting lists while still being able + * to look up the actual heap tuple. + */ ++#pragma pack(push, 1) + typedef struct TpCtidMapEntry + { + ItemPointerData ctid; /* 6 bytes - heap tuple location */ +-} __attribute__((packed)) TpCtidMapEntry; ++} TpCtidMapEntry; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpCtidMapEntry) == 6, "TpCtidMapEntry must remain 6 bytes on Windows"); ++StaticAssertDecl(__alignof(TpCtidMapEntry) == 1, "TpCtidMapEntry must remain 1-byte aligned on Windows"); +diff --git a/src/segment/segment.h b/src/segment/segment.h +--- a/src/segment/segment.h ++++ b/src/segment/segment.h +@@ -46,12 +46,16 @@ typedef struct TpPostingList + * ctid is invalid and doc_id is used to look up CTID at result extraction. + */ ++#pragma pack(push, 1) + typedef struct TpSegmentPosting + { + ItemPointerData ctid; /* 6 bytes - heap tuple ID (may be invalid) */ + uint32 doc_id; /* 4 bytes - segment-local doc ID */ + uint16 frequency; /* 2 bytes - term frequency */ + uint16 doc_length; /* 2 bytes - document length (from fieldnorm) */ +-} __attribute__((packed)) TpSegmentPosting; ++} TpSegmentPosting; ++#pragma pack(pop) ++StaticAssertDecl(sizeof(TpSegmentPosting) == 14, "TpSegmentPosting must remain 14 bytes on Windows"); ++StaticAssertDecl(__alignof(TpSegmentPosting) == 1, "TpSegmentPosting must remain 1-byte aligned on Windows"); + + /* Version-aware struct size helpers */ + static inline size_t +diff --git a/src/types/query.h b/src/types/query.h +--- a/src/types/query.h ++++ b/src/types/query.h +@@ -47,21 +47,21 @@ typedef struct TpQuery + #define TPQUERY_TEXT_PTR(x) (((TpQuery *)(x))->data) + + /* Function declarations */ +-Datum tpquery_in(PG_FUNCTION_ARGS); +-Datum tpquery_out(PG_FUNCTION_ARGS); +-Datum tpquery_recv(PG_FUNCTION_ARGS); +-Datum tpquery_send(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpquery_in(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpquery_out(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpquery_recv(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpquery_send(PG_FUNCTION_ARGS); + + /* Constructor functions */ +-Datum to_tpquery_text(PG_FUNCTION_ARGS); +-Datum to_tpquery_text_index(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum to_tpquery_text(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum to_tpquery_text_index(PG_FUNCTION_ARGS); + + /* Operator functions */ +-Datum bm25_text_bm25query_score(PG_FUNCTION_ARGS); +-Datum bm25_text_text_score(PG_FUNCTION_ARGS); +-Datum bm25_textarray_bm25query_score(PG_FUNCTION_ARGS); +-Datum bm25_textarray_text_score(PG_FUNCTION_ARGS); +-Datum tpquery_eq(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum bm25_text_bm25query_score(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum bm25_text_text_score(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum bm25_textarray_bm25query_score(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum bm25_textarray_text_score(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpquery_eq(PG_FUNCTION_ARGS); + + /* Utility functions */ + TpQuery *create_tpquery(const char *query_text, Oid index_oid); +diff --git a/src/types/vector.h b/src/types/vector.h +--- a/src/types/vector.h ++++ b/src/types/vector.h +@@ -90,12 +90,12 @@ typedef struct TpVector + MAXALIGN(((TpVector *)(x))->index_name_len + 1))) + + /* Function declarations */ +-Datum tpvector_in(PG_FUNCTION_ARGS); +-Datum tpvector_out(PG_FUNCTION_ARGS); +-Datum tpvector_recv(PG_FUNCTION_ARGS); +-Datum tpvector_send(PG_FUNCTION_ARGS); +-Datum to_tpvector(PG_FUNCTION_ARGS); +-Datum tpvector_eq(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpvector_in(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpvector_out(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpvector_recv(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpvector_send(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum to_tpvector(PG_FUNCTION_ARGS); ++extern PGDLLEXPORT Datum tpvector_eq(PG_FUNCTION_ARGS); + + /* Constructor */ + TpVector *create_tpvector_from_strings( +diff --git a/src/unistd.h b/src/unistd.h +new file mode 100644 +--- /dev/null ++++ b/src/unistd.h +@@ -0,0 +1,4 @@ ++#ifndef OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H ++#define OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H ++ ++#endif diff --git a/src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json b/src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json new file mode 100644 index 000000000..dacad0ad0 --- /dev/null +++ b/src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json @@ -0,0 +1,169 @@ +{ + "schema": "oliphaunt-external-pgxs-windows-recipe-v1", + "sql_name": "pg_textsearch", + "source_commit": "578ff529894992fb9e67cae4c69424e65c84868e", + "default_version": "1.3.1", + "sources": [ + "src/mod.c", + "src/access/handler.c", + "src/access/build.c", + "src/access/build_context.c", + "src/access/build_parallel.c", + "src/access/scan.c", + "src/access/vacuum.c", + "src/memtable/arena.c", + "src/memtable/cache.c", + "src/memtable/cache_source.c", + "src/memtable/chain_source.c", + "src/memtable/chain_walker.c", + "src/memtable/expull.c", + "src/memtable/log.c", + "src/memtable/page.c", + "src/memtable/posting.c", + "src/memtable/scan.c", + "src/memtable/stringtable.c", + "src/segment/segment.c", + "src/segment/dictionary.c", + "src/segment/scan.c", + "src/segment/merge.c", + "src/segment/tombstone.c", + "src/segment/docmap.c", + "src/segment/alive_bitset.c", + "src/segment/compression.c", + "src/segment/fieldnorm.c", + "src/scoring/bmw.c", + "src/scoring/bm25.c", + "src/types/array.c", + "src/types/vector.c", + "src/types/query.c", + "src/index/state.c", + "src/index/registry.c", + "src/index/metapage.c", + "src/index/limit.c", + "src/index/resolve.c", + "src/index/source.c", + "src/planner/hooks.c", + "src/planner/cost.c", + "src/debug/dump.c" + ], + "data_files": [ + "sql/pg_textsearch--1.3.1.sql", + "sql/pg_textsearch--0.0.1--0.0.2.sql", + "sql/pg_textsearch--0.0.2--0.0.3.sql", + "sql/pg_textsearch--0.0.3--0.0.4.sql", + "sql/pg_textsearch--0.0.4--0.0.5.sql", + "sql/pg_textsearch--0.0.5--0.1.0.sql", + "sql/pg_textsearch--0.1.0--0.2.0.sql", + "sql/pg_textsearch--0.2.0--0.3.0.sql", + "sql/pg_textsearch--0.3.0--0.4.0.sql", + "sql/pg_textsearch--0.4.0--0.4.1.sql", + "sql/pg_textsearch--0.4.1--0.4.2.sql", + "sql/pg_textsearch--0.4.2--0.5.0.sql", + "sql/pg_textsearch--0.5.0--0.5.1.sql", + "sql/pg_textsearch--0.5.1--0.6.0.sql", + "sql/pg_textsearch--0.6.0--0.6.1.sql", + "sql/pg_textsearch--0.6.1--1.0.0.sql", + "sql/pg_textsearch--1.0.0--1.1.0.sql", + "sql/pg_textsearch--1.1.0--1.2.0.sql", + "sql/pg_textsearch--1.2.0--1.3.0.sql", + "sql/pg_textsearch--1.3.0--1.3.1.sql", + "pg_textsearch.control" + ], + "compiler_arguments": [ + "/D_CRT_SECURE_NO_WARNINGS" + ], + "local_include_directories": [ + "src" + ], + "force_include_files": [ + "src/oliphaunt_windows_compat.h" + ], + "version_defines": [ + "PG_TEXTSEARCH_VERSION" + ], + "patches": [ + { + "path": "patches/windows-msvc/pg_textsearch-1.3.1.patch", + "sha256": "61f478bde24e4afc3f911abe5e044c7088f41c2abb1646ec79c44a4cbe00252e" + } + ], + "layout_contracts": [ + { + "path": "src/segment/format.h", + "type": "TpDictEntryV3", + "size": 12, + "alignment": 4 + }, + { + "path": "src/segment/format.h", + "type": "TpDictEntry", + "size": 16, + "alignment": 8 + }, + { + "path": "src/segment/format.h", + "type": "TpSkipEntryV3", + "size": 16, + "alignment": 1 + }, + { + "path": "src/segment/format.h", + "type": "TpSkipEntry", + "size": 20, + "alignment": 1 + }, + { + "path": "src/segment/format.h", + "type": "TpCtidMapEntry", + "size": 6, + "alignment": 1 + }, + { + "path": "src/segment/segment.h", + "type": "TpSegmentPosting", + "size": 14, + "alignment": 1 + }, + { + "path": "src/memtable/expull.h", + "type": "TpExpullEntry", + "size": 7, + "alignment": 1 + } + ], + "export_contracts": [ + { + "path": "src/access/am.h", + "symbols": [ + "tp_handler" + ] + }, + { + "path": "src/types/vector.h", + "symbols": [ + "tpvector_in", + "tpvector_out", + "tpvector_recv", + "tpvector_send", + "to_tpvector", + "tpvector_eq" + ] + }, + { + "path": "src/types/query.h", + "symbols": [ + "tpquery_in", + "tpquery_out", + "tpquery_recv", + "tpquery_send", + "to_tpquery_text", + "to_tpquery_text_index", + "bm25_text_bm25query_score", + "bm25_text_text_score", + "bm25_textarray_bm25query_score", + "bm25_textarray_text_score", + "tpquery_eq" + ] + } + ] +} diff --git a/src/extensions/external/pg_textsearch/source.toml b/src/extensions/external/pg_textsearch/source.toml index 9543a3d06..df4e6b4a5 100644 --- a/src/extensions/external/pg_textsearch/source.toml +++ b/src/extensions/external/pg_textsearch/source.toml @@ -1,9 +1,9 @@ name = "pg_textsearch" url = "https://github.com/timescale/pg_textsearch.git" branch = "pinned" -commit = "07936f7cd67f7a183659d3acd459c0a5efc93756" +commit = "578ff529894992fb9e67cae4c69424e65c84868e" [extension-control] sql-name = "pg_textsearch" source-path = "pg_textsearch.control" -default-version = "0.6.1" +default-version = "1.3.1" diff --git a/src/extensions/external/pg_textsearch/targets/native-static-registry.toml b/src/extensions/external/pg_textsearch/targets/native-static-registry.toml index 0c228f487..440a80202 100644 --- a/src/extensions/external/pg_textsearch/targets/native-static-registry.toml +++ b/src/extensions/external/pg_textsearch/targets/native-static-registry.toml @@ -4,4 +4,4 @@ status = "supported" build_kind = "pgxs-static-registry" source_recursive_dirs = ["src"] include_dirs = ["source:src"] -cflags = ['-DPG_TEXTSEARCH_VERSION="0.6.1"'] +cflags = ['-DPG_TEXTSEARCH_VERSION="1.3.1"'] diff --git a/src/extensions/external/pg_textsearch/tests/upgrade.sh b/src/extensions/external/pg_textsearch/tests/upgrade.sh new file mode 100644 index 000000000..96e7a2e4c --- /dev/null +++ b/src/extensions/external/pg_textsearch/tests/upgrade.sh @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)" || { + echo "pg_textsearch-upgrade: unable to determine the repository root" >&2 + exit 1 +} + +fail() { + echo "pg_textsearch-upgrade: $*" >&2 + exit 1 +} + +require_file() { + if [ ! -f "$1" ] || [ -L "$1" ]; then + fail "missing regular $2 at $1" + fi +} + +sql_name="${OLIPHAUNT_EXTENSION_SQL_NAME:-}" +source_name="${OLIPHAUNT_EXTENSION_SOURCE_NAME:-}" +old_version="${OLIPHAUNT_EXTENSION_UPGRADE_FROM_VERSION:-}" +old_commit="${OLIPHAUNT_EXTENSION_SOURCE_COMMIT:-}" +source_control_path="${OLIPHAUNT_EXTENSION_SOURCE_CONTROL_PATH:-}" +source_runtime="${OLIPHAUNT_EXTENSION_CURRENT_RUNTIME:-}" +[ "$sql_name" = "pg_textsearch" ] || fail "the qualification plan must select pg_textsearch" +case "$source_name" in + ""|*[!a-z0-9_-]*) fail "the qualification plan must provide a safe old source name" ;; +esac +case "$old_version" in + ""|*[!0-9A-Za-z._+-]*) fail "the qualification plan must provide a safe old extension version" ;; +esac +case "$old_commit" in + *[!0-9a-f]*) fail "the qualification plan must provide a full lowercase old source commit" ;; +esac +[ "${#old_commit}" -eq 40 ] || fail "the qualification plan must provide a full lowercase old source commit" +case "$source_control_path" in + ""|/*|*\\*|..|../*|*/..|*/../*) + fail "the qualification plan must provide a normalized relative old control path" + ;; +esac +[ -n "$source_runtime" ] || fail "the qualification plan must provide the current native runtime" +source_runtime="$(cd "$source_runtime" 2>/dev/null && pwd)" || fail "current native runtime is not a directory: $source_runtime" + +for command_name in awk cat cp find git grep install make mktemp rm rsync; do + command -v "$command_name" >/dev/null 2>&1 || fail "missing required command: $command_name" +done +for tool in initdb pg_config pg_ctl pg_dump postgres psql; do + require_file "$source_runtime/bin/$tool" "current runtime tool $tool" +done +require_file "$source_runtime/lib/postgresql/pg_textsearch.so" "current pg_textsearch module" +require_file "$source_runtime/share/postgresql/extension/pg_textsearch.control" "current pg_textsearch control file" +current_version="$(awk -F"'" '/^[[:space:]]*default_version[[:space:]]*=/ { print $2 }' \ + "$source_runtime/share/postgresql/extension/pg_textsearch.control")" +[ -n "$current_version" ] || fail "current runtime pg_textsearch control file has no default_version" + +current_sql_files=("$source_runtime"/share/postgresql/extension/pg_textsearch--*.sql) +[ -e "${current_sql_files[0]}" ] || fail "current runtime has no pg_textsearch SQL files" +if ! grep -Eq "^[[:space:]]*default_version[[:space:]]*=[[:space:]]*'$current_version'[[:space:]]*$" \ + "$source_runtime/share/postgresql/extension/pg_textsearch.control"; then + fail "current runtime pg_textsearch control file does not declare $current_version" +fi + +checkout="$root/target/oliphaunt-sources/checkouts/$source_name" +if [ ! -d "$checkout" ] || [ -L "$checkout" ] || [ ! -d "$checkout/.git" ] || [ -L "$checkout/.git" ]; then + fail "missing verified pg_textsearch $old_version source checkout; run the source-fetch-native-runtime dependency" +fi +actual_old_commit="$(git -C "$checkout" rev-parse --verify 'HEAD^{commit}')" +[ "$actual_old_commit" = "$old_commit" ] || \ + fail "pg_textsearch $old_version checkout is at $actual_old_commit, expected $old_commit" +[ -z "$(git -C "$checkout" status --porcelain=v1 --untracked-files=all)" ] || \ + fail "pg_textsearch $old_version checkout must be clean" +require_file "$checkout/$source_control_path" "pinned pg_textsearch control file" +checkout_old_version="$(awk -F"'" '/^[[:space:]]*default_version[[:space:]]*=/ { print $2 }' \ + "$checkout/$source_control_path")" +[ "$checkout_old_version" = "$old_version" ] || \ + fail "upgrade manifest declares $old_version but the pinned source control declares $checkout_old_version" + +work_parent="$root/target/pg-textsearch-upgrade" +mkdir -p "$work_parent" +if [ ! -d "$work_parent" ] || [ -L "$work_parent" ]; then + fail "upgrade work parent must be a real directory: $work_parent" +fi +work_root="$(mktemp -d "$work_parent/run.XXXXXX")" +old_source="$work_root/old-source" +runtime="$work_root/runtime" +data_dir="$work_root/data" +socket_dir="$work_root/socket" +server_log="$work_root/postgres.log" +preserved_log="/tmp/oliphaunt-pg-textsearch-upgrade-postgres.log" +server_start_attempted=0 +server_running=0 + +cleanup() { + local status="$?" + local may_remove=1 + trap - EXIT HUP INT TERM + if [ "$server_running" = 1 ] || [ -f "$data_dir/postmaster.pid" ] || \ + { [ "$server_start_attempted" = 1 ] && "$runtime/bin/pg_ctl" status -D "$data_dir" >/dev/null 2>&1; }; then + if ! "$runtime/bin/pg_ctl" stop -D "$data_dir" -m immediate -w >/dev/null 2>&1; then + may_remove=0 + echo "pg_textsearch-upgrade: could not confirm server shutdown; preserving $work_root" >&2 + fi + fi + if [ "$status" -ne 0 ] && [ -f "$server_log" ]; then + cp "$server_log" "$preserved_log" 2>/dev/null || true + echo "pg_textsearch-upgrade: preserved failing server log at $preserved_log" >&2 + fi + if [ "$may_remove" = 1 ] && [ ! -f "$data_dir/postmaster.pid" ]; then + rm -rf "$work_root" + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +rsync -a --delete --exclude .git/ "$checkout/" "$old_source/" +PATH="$source_runtime/bin:$PATH" make -C "$old_source" PG_CONFIG="$source_runtime/bin/pg_config" -j2 +require_file "$old_source/pg_textsearch.so" "built pg_textsearch $old_version module" + +rsync -a --delete "$source_runtime/" "$runtime/" +extension_dir="$runtime/share/postgresql/extension" +module="$runtime/lib/postgresql/pg_textsearch.so" +find "$extension_dir" -maxdepth 1 -type f -name 'pg_textsearch--*.sql' -delete +install -m 0755 "$old_source/pg_textsearch.so" "$module" +install -m 0644 "$old_source/$source_control_path" "$extension_dir/pg_textsearch.control" +old_sql_files=("$old_source"/sql/pg_textsearch--*.sql) +[ -e "${old_sql_files[0]}" ] || fail "pinned pg_textsearch $old_version source has no SQL files" +install -m 0644 "${old_sql_files[@]}" "$extension_dir/" + +mkdir -p "$socket_dir" +PATH="$runtime/bin:$PATH" "$runtime/bin/initdb" -D "$data_dir" --auth-local=trust --auth-host=reject --no-locale >/dev/null +if [[ "$socket_dir" == *"'"* ]]; then + fail "upgrade socket path must not contain a single quote" +fi +cat >>"$data_dir/postgresql.conf" </dev/null + server_running=1 + server_start_attempted=0 +} + +stop_server() { + PATH="$runtime/bin:$PATH" "$runtime/bin/pg_ctl" stop -D "$data_dir" -m fast -w >/dev/null + server_running=0 + server_start_attempted=0 +} + +psql_db() { + PATH="$runtime/bin:$PATH" "$runtime/bin/psql" -X -v ON_ERROR_STOP=1 -h "$socket_dir" -d "$1" "${@:2}" +} + +start_server +psql_db postgres -c 'CREATE DATABASE pg_textsearch_upgrade' >/dev/null +psql_db pg_textsearch_upgrade -v old_version="$old_version" <<'SQL' +CREATE EXTENSION pg_textsearch VERSION :'old_version'; +CREATE TABLE upgrade_docs(id integer PRIMARY KEY, body text NOT NULL); +INSERT INTO upgrade_docs +SELECT id, + CASE + WHEN id = 1 THEN 'postgres database postgres database query migration' + WHEN id = 3 THEN 'legacyalpha legacyalpha persisted posting' + WHEN id = 4 THEN 'deletionproof deletionproof persisted posting' + WHEN id = 5 THEN 'legacybeta legacybeta persisted posting' + WHEN id = 6 THEN 'deletionproof surviving persisted posting' + WHEN id % 2 = 0 THEN 'postgres database guide' + ELSE 'unrelated document about wildlife' + END +FROM generate_series(1, 256) AS id; +CREATE INDEX upgrade_docs_bm25 ON upgrade_docs USING bm25(body) + WITH (text_config = 'english'); +SELECT bm25_spill_index('upgrade_docs_bm25'); +DO $$ +DECLARE top_id integer; +BEGIN + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('postgres database', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 1 THEN + RAISE EXCEPTION 'old pg_textsearch query returned unexpected top id %', top_id; + END IF; +END $$; +SQL +stop_server + +find "$extension_dir" -maxdepth 1 -type f -name 'pg_textsearch--*.sql' -delete +install -m 0755 "$source_runtime/lib/postgresql/pg_textsearch.so" "$module" +install -m 0644 "$source_runtime/share/postgresql/extension/pg_textsearch.control" "$extension_dir/pg_textsearch.control" +install -m 0644 "${current_sql_files[@]}" "$extension_dir/" + +start_server +psql_db pg_textsearch_upgrade <<'SQL' +DO $$ +DECLARE top_id integer; +BEGIN + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('postgres database', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 1 THEN + RAISE EXCEPTION 'pre-upgrade query with the current library returned unexpected top id %', top_id; + END IF; +END $$; +ALTER EXTENSION pg_textsearch UPDATE; +INSERT INTO upgrade_docs VALUES + (1000, 'upgrade sentinel upgrade sentinel upgrade sentinel migration proof'); +UPDATE upgrade_docs +SET body = 'mutationproof mutationproof updated persisted row' +WHERE id = 2; +DELETE FROM upgrade_docs WHERE id = 4; +SELECT bm25_force_merge('upgrade_docs_bm25'); +SQL + +assert_upgraded_state() { + local database="$1" + local installed_version + psql_db "$database" <<'SQL' +DO $$ +DECLARE + actual_body text; + actual_count bigint; + top_id integer; +BEGIN + SELECT count(*) INTO actual_count FROM upgrade_docs; + IF actual_count <> 256 THEN + RAISE EXCEPTION 'upgraded table contains % rows, expected 256', actual_count; + END IF; + + SELECT body INTO actual_body FROM upgrade_docs WHERE id = 2; + IF actual_body <> 'mutationproof mutationproof updated persisted row' THEN + RAISE EXCEPTION 'updated row 2 has unexpected body %', actual_body; + END IF; + IF EXISTS (SELECT 1 FROM upgrade_docs WHERE id = 4) THEN + RAISE EXCEPTION 'deleted row 4 is still visible'; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('postgres database', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 1 THEN + RAISE EXCEPTION 'persisted pre-upgrade query returned unexpected top id %', top_id; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('legacyalpha', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 3 THEN + RAISE EXCEPTION 'persisted legacyalpha posting returned unexpected top id %', top_id; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('legacybeta', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 5 THEN + RAISE EXCEPTION 'persisted legacybeta posting returned unexpected top id %', top_id; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('mutationproof', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 2 THEN + RAISE EXCEPTION 'updated posting returned unexpected top id %', top_id; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('upgrade sentinel', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 1000 THEN + RAISE EXCEPTION 'new posting returned unexpected top id %', top_id; + END IF; + + SELECT id INTO top_id + FROM upgrade_docs + ORDER BY body <@> to_bm25query('deletionproof', 'upgrade_docs_bm25') + LIMIT 1; + IF top_id <> 6 THEN + RAISE EXCEPTION 'deleted posting remained ahead of survivor; top id is %', top_id; + END IF; +END $$; +SQL + installed_version="$(psql_db "$database" -Atc \ + "SELECT extversion FROM pg_extension WHERE extname = 'pg_textsearch'")" + [ "$installed_version" = "$current_version" ] || \ + fail "$database pg_textsearch version is $installed_version, expected $current_version" +} + +assert_upgraded_state pg_textsearch_upgrade +stop_server +start_server +assert_upgraded_state pg_textsearch_upgrade + +PATH="$runtime/bin:$PATH" "$runtime/bin/pg_dump" \ + -h "$socket_dir" --format=plain --no-owner --no-privileges \ + -d pg_textsearch_upgrade >"$work_root/upgrade.sql" +psql_db postgres -c 'CREATE DATABASE pg_textsearch_upgrade_restore' >/dev/null +psql_db pg_textsearch_upgrade_restore -f "$work_root/upgrade.sql" >/dev/null +assert_upgraded_state pg_textsearch_upgrade_restore +stop_server + +echo "pg_textsearch upgrade qualification passed: $old_version ($old_commit) -> $current_version" diff --git a/src/extensions/external/pg_textsearch/tests/upgrade/source.toml b/src/extensions/external/pg_textsearch/tests/upgrade/source.toml new file mode 100644 index 000000000..8df6d744a --- /dev/null +++ b/src/extensions/external/pg_textsearch/tests/upgrade/source.toml @@ -0,0 +1,14 @@ +name = "pg_textsearch_upgrade_from" +url = "https://github.com/timescale/pg_textsearch.git" +branch = "pinned" +commit = "07936f7cd67f7a183659d3acd459c0a5efc93756" + +[extension-control] +sql-name = "pg_textsearch" +source-path = "pg_textsearch.control" +default-version = "0.6.1" + +[qualification] +schema = "oliphaunt-extension-upgrade-qualification-v1" +targets = ["linux-x64-gnu"] +runner = "tests/upgrade.sh" diff --git a/src/extensions/external/pg_textsearch/tests/upstream.toml b/src/extensions/external/pg_textsearch/tests/upstream.toml index 5fe1e901e..7670c1583 100644 --- a/src/extensions/external/pg_textsearch/tests/upstream.toml +++ b/src/extensions/external/pg_textsearch/tests/upstream.toml @@ -1,6 +1,26 @@ schema = "oliphaunt-extension-upstream-tests-v1" runner = "pgxs-installcheck" -status = "candidate" -reason = "pg_textsearch is currently covered by Oliphaunt direct/server/restart/dump smoke evidence. Upstream PGXS installcheck should be wired before using the full upstream suite as release evidence." -included_suites = ["pg_textsearch"] -excluded_suites = [] +status = "required" +reason = "Exact-candidate Linux qualification runs the complete upstream REGRESS list with PGXS installcheck. Non-PGXS shell, concurrency, stress, recovery, and replication targets require separate long-running or multi-node harnesses and are explicitly excluded here." +targets = ["linux-x64-gnu"] +locale = "C.UTF-8" +included_suites = ["regress"] +suite_target_prefix = "test-" +aggregate_suites = [ + "test-all", + "test-local", + "test-shell", +] +excluded_suites = [ + "test-cic", + "test-concurrency", + "test-logical-replication", + "test-multi-index", + "test-recovery", + "test-reindex", + "test-replication", + "test-replication-extended", + "test-segment", + "test-stress", +] +shared_preload_libraries = ["pg_textsearch"] diff --git a/src/extensions/external/pg_textsearch/upstream-license-data.json b/src/extensions/external/pg_textsearch/upstream-license-data.json index b2547591d..b8b181c4e 100644 --- a/src/extensions/external/pg_textsearch/upstream-license-data.json +++ b/src/extensions/external/pg_textsearch/upstream-license-data.json @@ -7,7 +7,7 @@ "kind": "git", "url": "https://github.com/timescale/pg_textsearch.git", "branch": "pinned", - "commit": "07936f7cd67f7a183659d3acd459c0a5efc93756" + "commit": "578ff529894992fb9e67cae4c69424e65c84868e" } ], "extension": { @@ -19,7 +19,7 @@ "destination": "share/licenses/pg_textsearch/LICENSE", "role": "license", "spdx": "PostgreSQL", - "license_url": "https://github.com/timescale/pg_textsearch/blob/07936f7cd67f7a183659d3acd459c0a5efc93756/LICENSE", + "license_url": "https://github.com/timescale/pg_textsearch/blob/578ff529894992fb9e67cae4c69424e65c84868e/LICENSE", "sha256": "d33de21a123ce25b41722a5d10750984cb9c844c4d9b01add9e1b31f3ff452e5" }, { @@ -28,7 +28,7 @@ "destination": "share/licenses/pg_textsearch/NOTICE", "role": "notice", "spdx": "PostgreSQL", - "license_url": "https://github.com/timescale/pg_textsearch/blob/07936f7cd67f7a183659d3acd459c0a5efc93756/NOTICE", + "license_url": "https://github.com/timescale/pg_textsearch/blob/578ff529894992fb9e67cae4c69424e65c84868e/NOTICE", "sha256": "ff70cf4336c579957368a71c6b6b66ee8954011deef2b3d2c7a11f931080851d" } ] diff --git a/src/extensions/external/pg_uuidv7/.release-extension-metadata.json b/src/extensions/external/pg_uuidv7/.release-extension-metadata.json new file mode 100644 index 000000000..a1c8ed075 --- /dev/null +++ b/src/extensions/external/pg_uuidv7/.release-extension-metadata.json @@ -0,0 +1,52 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-pg-uuidv7" +} diff --git a/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json index 36aa011eb..d17245789 100644 --- a/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json +++ b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "582f1d4797443f6cda96d94f8f3a6c1bf66bcd4778d3f8518b937233aab11e2e" + "sha256": "cfae1422b57a924edfe481d2ed2375afd4fe9831df5bf7a6a65475918a3091da" } diff --git a/src/extensions/external/pgtap/.release-extension-metadata.json b/src/extensions/external/pgtap/.release-extension-metadata.json new file mode 100644 index 000000000..deae74b49 --- /dev/null +++ b/src/extensions/external/pgtap/.release-extension-metadata.json @@ -0,0 +1,73 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-pgtap" +} diff --git a/src/extensions/external/pgtap/.release-semantic-inputs.json b/src/extensions/external/pgtap/.release-semantic-inputs.json index 8e6eb89a2..cd22e9720 100644 --- a/src/extensions/external/pgtap/.release-semantic-inputs.json +++ b/src/extensions/external/pgtap/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "5bd705d032c08abd0cee58e21f15836ee67f512822017e61ce4bea347a8c6031" + "sha256": "6cbfdc1b0fb71fc994da6a454f6cbc7fb5e34d9438667fe2bfa76d5718a95223" } diff --git a/src/extensions/external/postgis/.release-extension-metadata.json b/src/extensions/external/postgis/.release-extension-metadata.json new file mode 100644 index 000000000..e847bd61f --- /dev/null +++ b/src/extensions/external/postgis/.release-extension-metadata.json @@ -0,0 +1,105 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-postgis" +} diff --git a/src/extensions/external/postgis/.release-semantic-inputs.json b/src/extensions/external/postgis/.release-semantic-inputs.json index fd32cd81e..5b528bc72 100644 --- a/src/extensions/external/postgis/.release-semantic-inputs.json +++ b/src/extensions/external/postgis/.release-semantic-inputs.json @@ -61,7 +61,7 @@ ] }, { - "id": "native-postgis-carrier-producers", + "id": "native-extension-carrier-producers", "paths": [ "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", @@ -69,8 +69,7 @@ "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", - "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", - "src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh" + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" ], "inputs": [ { @@ -99,8 +98,16 @@ }, { "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", - "sha256": "3ecdda83cf6ed8df45852ead088dc5baa415b07e302824b0531a4ff839cbb8c6" - }, + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, + { + "id": "native-postgis-carrier-producer", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh" + ], + "inputs": [ { "path": "src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh", "sha256": "3542a4b4c4efe80fe664eb8f2421b992675f88e01e2f19a30856ff98cb711fb1" @@ -147,7 +154,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -179,7 +186,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -223,18 +230,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -390,5 +385,5 @@ ] } ], - "sha256": "a8d8102915899d7308025b7c9fd5401f64e18c347d2d53aed9c083c7b001c2a4" + "sha256": "0c134a10edb9294fb93f34c9ea6102d8482ef0ad9b3e749cef11a2d4b723e92f" } diff --git a/src/extensions/external/vector/.release-extension-metadata.json b/src/extensions/external/vector/.release-extension-metadata.json new file mode 100644 index 000000000..4af4675ec --- /dev/null +++ b/src/extensions/external/vector/.release-extension-metadata.json @@ -0,0 +1,52 @@ +{ + "consumer": "release-product", + "extensions": [ + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ], + "release-product": "oliphaunt-extension-vector" +} diff --git a/src/extensions/external/vector/.release-semantic-inputs.json b/src/extensions/external/vector/.release-semantic-inputs.json index baf87a34a..b078e6f86 100644 --- a/src/extensions/external/vector/.release-semantic-inputs.json +++ b/src/extensions/external/vector/.release-semantic-inputs.json @@ -60,6 +60,48 @@ } ] }, + { + "id": "native-extension-carrier-producers", + "paths": [ + "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1" + ], + "inputs": [ + { + "path": "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", + "sha256": "d9900d8fa7db46d698cf366e1d09b227f5fab4be2707be0a9c83c9fb8354fc12" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", + "sha256": "203fea01484138fcb07481b979b707f0aab19ed2134dea5781d0ccd094b908dc" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh", + "sha256": "bfbd6056f74c4069d6aa0819a775410e084b60bbb22c5c923c3a6ac9da4cf307" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh", + "sha256": "71acd0774c4ac9d0d32e7f0c229bd9777e57083ca270fab495f3f126a033b1d9" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", + "sha256": "d9ded61bf55c42197f84115940c0546f2801876b1562084b17cae80b41aeae68" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", + "sha256": "b53b165f9942a080ecfe84af9f2bffd41c194af79657ab09fa1018c156af20ca" + }, + { + "path": "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", + "sha256": "821b468642d48a23e85d4617c8e134d9d1061c3008bcaa0f45d97f90bd59bd33" + } + ] + }, { "id": "npm-extension-contract", "paths": [ @@ -100,7 +142,7 @@ }, { "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", - "sha256": "3bba3ae4486549321911a68c2e6e6ecd2d1c22640edc8253ec5079aaeb3fe657" + "sha256": "0db0679fb0a746df979bdf493c5e9a60af36d8b0982654a3a9190da04fa23e8e" }, { "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", @@ -132,7 +174,7 @@ }, { "path": "tools/release/build-extension-ci-artifacts.mjs", - "sha256": "4aa4332d70814a0fdada16207df7ade2a18405da0bde92a364307dc76ab241e5" + "sha256": "e9b7d7594de6c66c2bf46ca0438a183de0110067d5d1b9ed6d0a2b1428ac7bcd" }, { "path": "tools/release/extension-artifact-archive-policy.mjs", @@ -176,18 +218,6 @@ } ] }, - { - "id": "extension-kotlin-runtime-catalog", - "paths": [ - "src/extensions/generated/sdk/kotlin.json" - ], - "inputs": [ - { - "path": "src/extensions/generated/sdk/kotlin.json", - "sha256": "e6a713ed46ca57edfc92855317865114a54c8b415736759668294cc9c592713b" - } - ] - }, { "id": "maven-runtime-extension-carrier-producer", "paths": [ @@ -343,5 +373,5 @@ ] } ], - "sha256": "f97b62f53ca3ddf959883c4f057a4a077b08a9f0529169768bc426468a8b5d27" + "sha256": "2970ac315c21db3e00e971372b4a550159f8a6425d0e0fb3817ee26bea0913f6" } diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 80431e38d..527d1c510 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -1347,7 +1347,7 @@ "current-source-status": "requires-exact-candidate-ci", "kind": "exact-sha-ci" }, - "source-digest": "sha256:d30cde83f40642f68c2dd7f2b1fe1bf1a9ca05122a7a6232fca42d624a06fed2", + "source-digest": "sha256:dc2b7c716729069022a04bc9e2f92d596ba135918f3120c3e659bc86c90f736b", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", @@ -1358,11 +1358,15 @@ "src/extensions/generated/extensions.build-plan.json", "src/extensions/generated/contrib-build.tsv", "src/extensions/generated/pgxs-build.tsv", + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh", "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "tools/release/native-extension-qualification.mjs", "src/extensions/external/age/source.toml", "src/extensions/external/pg_hashids/source.toml", "src/extensions/external/pg_ivm/source.toml", "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_textsearch/tests/upgrade/source.toml", "src/extensions/external/pg_uuidv7/source.toml", "src/extensions/external/pgtap/source.toml", "src/extensions/external/postgis/dependencies/geos/source.toml", @@ -1383,9 +1387,12 @@ "src/extensions/external/pg_ivm/targets/native-static-registry.toml", "src/extensions/external/pg_ivm/upstream-license-data.json", "src/extensions/external/pg_textsearch/moon.yml", + "src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch", + "src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json", "src/extensions/external/pg_textsearch/recipe.toml", "src/extensions/external/pg_textsearch/targets/native-static-registry.toml", "src/extensions/external/pg_textsearch/tests/smoke.sql", + "src/extensions/external/pg_textsearch/tests/upgrade.sh", "src/extensions/external/pg_textsearch/tests/upstream.toml", "src/extensions/external/pg_textsearch/upstream-license-data.json", "src/extensions/external/pg_uuidv7/moon.yml", diff --git a/src/extensions/generated/docs/extensions.json b/src/extensions/generated/docs/extensions.json index 0f3e37d36..05f874802 100644 --- a/src/extensions/generated/docs/extensions.json +++ b/src/extensions/generated/docs/extensions.json @@ -814,7 +814,7 @@ "native": null, "wasix": null }, - "version": "0.6.1" + "version": "1.3.1" }, { "activation": "CREATE EXTENSION", diff --git a/src/extensions/generated/extensions.catalog.json b/src/extensions/generated/extensions.catalog.json index 0da538986..c77e32825 100644 --- a/src/extensions/generated/extensions.catalog.json +++ b/src/extensions/generated/extensions.catalog.json @@ -1347,10 +1347,10 @@ "bundle-size": 55062, "control-file": "target/oliphaunt-sources/checkouts/pg_textsearch/pg_textsearch.control", "control": { - "default-version": "0.6.1", + "default-version": "1.3.1", "module-pathname": "$libdir/pg_textsearch", "requires": [], - "relocatable": "true" + "relocatable": "false" }, "dependencies": [], "native-dependencies": [], diff --git a/src/extensions/generated/mobile/static-extensions.tsv b/src/extensions/generated/mobile/static-extensions.tsv index e2e9f5000..438aa3975 100644 --- a/src/extensions/generated/mobile/static-extensions.tsv +++ b/src/extensions/generated/mobile/static-extensions.tsv @@ -23,7 +23,7 @@ pg_freespacemap pg_freespacemap contrib contrib/pg_freespacemap pg_hashids pg_hashids external target/oliphaunt-sources/checkouts/pg_hashids pg_ivm pg_ivm external target/oliphaunt-sources/checkouts/pg_ivm createas.c,matview.c,pg_ivm.c,ruleutils.c,subselect.c pg_surgery pg_surgery contrib contrib/pg_surgery -pg_textsearch pg_textsearch external target/oliphaunt-sources/checkouts/pg_textsearch source:src -DPG_TEXTSEARCH_VERSION="0.6.1" src +pg_textsearch pg_textsearch external target/oliphaunt-sources/checkouts/pg_textsearch source:src -DPG_TEXTSEARCH_VERSION="1.3.1" src pg_trgm pg_trgm contrib contrib/pg_trgm pg_uuidv7 pg_uuidv7 external target/oliphaunt-sources/checkouts/pg_uuidv7 pg_visibility pg_visibility contrib contrib/pg_visibility diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 15acd1f43..1459b6e37 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -32,6 +32,7 @@ THIRD_PARTY_ROOT = ROOT / "src/sources/third-party" EXTERNAL_ROOT = ROOT / "src/extensions/external" EXTENSION_ENVELOPE_FILENAMES = { + ".release-extension-metadata.json", ".release-semantic-inputs.json", "CHANGELOG.md", "VERSION", @@ -39,6 +40,7 @@ "publication-blocker.toml", "release.toml", } +RELEASE_EXTENSION_METADATA_BASENAME = ".release-extension-metadata.json" GENERATED_SDKS = { "rust": ROOT / "src/extensions/generated/sdk/rust.json", "swift": ROOT / "src/extensions/generated/sdk/swift.json", @@ -125,7 +127,10 @@ "src/extensions/generated/extensions.build-plan.json", "src/extensions/generated/contrib-build.tsv", "src/extensions/generated/pgxs-build.tsv", + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh", "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "tools/release/native-extension-qualification.mjs", ] ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") @@ -324,6 +329,30 @@ def extension_metadata_by_sql_name() -> dict[str, dict]: return {str(row["sqlName"]): row for row in extension_metadata_rows()} +@lru_cache(maxsize=1) +def exact_extension_product_paths() -> dict[str, Path]: + products: dict[str, Path] = {} + for row in release_graph_rows("product-configs"): + kind = row.get("kind") + if kind not in {"exact-extension-artifact", "exact-extension-bundle"}: + continue + product = row.get("id") + product_path = row.get("path") + if not isinstance(product, str) or not product: + fail("release graph product-configs exact extension row must define id") + if not isinstance(product_path, str) or not product_path: + fail(f"release graph product-configs {product} must define path") + path = ROOT / product_path + if not path.is_dir(): + fail(f"release graph product-configs {product} path is not a directory: {product_path}") + if product in products: + fail(f"release graph product-configs returned duplicate product {product}") + products[product] = path + if not products: + fail("release graph product-configs returned no exact extension products") + return products + + def rel(path: Path) -> str: try: return path.relative_to(ROOT).as_posix() @@ -1166,7 +1195,7 @@ def target_native_support_modules(sql_name: str, target: str) -> list[dict]: return modules -def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: +def generated_extension_metadata(catalog: dict, consumer: str) -> dict: rows = [] release_metadata = extension_metadata_by_sql_name() public_sql_names = { @@ -1221,7 +1250,7 @@ def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: "source-kind": extension.get("source-kind"), "archive": promotion.get("archive") or "", } - if sdk == "react-native": + if consumer == "react-native": row["ios-static-dependencies"] = mobile_static_dependencies( sql_name, "ios_dependencies" ) @@ -1232,7 +1261,7 @@ def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: ).hexdigest() return { "format-version": 1, - "consumer": sdk, + "consumer": consumer, "extension-catalog-sha256": catalog_sha256, "generated-from": [ {"name": "extension-catalog", "path": rel(CATALOG)}, @@ -1242,6 +1271,36 @@ def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: } +def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: + return generated_extension_metadata(catalog, sdk) + + +def generated_release_extension_metadata(catalog: dict) -> dict[Path, dict]: + product_paths = exact_extension_product_paths() + rows_by_product: dict[str, list[dict]] = {product: [] for product in product_paths} + release_metadata = generated_extension_metadata(catalog, "release-product") + for row in release_metadata.get("extensions", []): + product = row.get("release-product") + if product not in rows_by_product: + fail(f"release extension metadata row has unknown release product {product}") + rows_by_product[str(product)].append(row) + + generated: dict[Path, dict] = {} + for product, product_path in sorted(product_paths.items()): + rows = rows_by_product[product] + if not rows: + fail(f"exact extension release product {product} has no public extension metadata rows") + rows.sort(key=lambda row: (str(row["sql-name"]), str(row["id"]))) + generated[product_path / RELEASE_EXTENSION_METADATA_BASENAME] = { + "format-version": 1, + "consumer": "release-product", + "release-product": product, + "generated-from": release_metadata["generated-from"], + "extensions": rows, + } + return generated + + def generated_typescript_extension_module(metadata: dict) -> str: include_ios_static_dependencies = metadata.get("consumer") == "react-native" @@ -2055,6 +2114,18 @@ def validate_generated_sdk_metadata(catalog: dict, build_plan: dict, write: bool write, ) validate_generated_file(GENERATED_KOTLIN_SDK_METADATA, kotlin_metadata, write) + expected_release_metadata = generated_release_extension_metadata(catalog) + existing_release_metadata = set(ROOT.glob(f"src/extensions/**/{RELEASE_EXTENSION_METADATA_BASENAME}")) + unexpected_release_metadata = existing_release_metadata - set(expected_release_metadata) + if unexpected_release_metadata: + if write: + for path in sorted(unexpected_release_metadata, key=rel): + path.unlink() + else: + paths = ", ".join(rel(path) for path in sorted(unexpected_release_metadata, key=rel)) + fail(f"unexpected generated release extension metadata: {paths}; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") + for path, metadata in expected_release_metadata.items(): + validate_generated_file(path, metadata, write) validate_generated_text_file( GENERATED_KOTLIN_GRADLE_PLUGIN_CATALOG, generated_kotlin_gradle_plugin_catalog(kotlin_metadata), @@ -2557,6 +2628,7 @@ def run_xtask_check() -> None: def self_test() -> None: digest_inputs = set(source_digest_inputs()) for path in [ + "src/extensions/external/vector/.release-extension-metadata.json", "src/extensions/external/vector/.release-semantic-inputs.json", "src/extensions/external/vector/VERSION", "src/extensions/external/vector/CHANGELOG.md", diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 b/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 index d3aca38c9..875b9540f 100644 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 @@ -69,6 +69,7 @@ $SnowballStopwordFiles = @( $VcRuntimeClosureTool = Join-Path $RepoRoot "tools/release/windows-vc-runtime-closure.mjs" $Stamp = Join-Path $OutDir "oliphaunt-windows.inputs.sha256" $ExternalCheckoutRoot = Join-Path $RepoRoot "target/oliphaunt-sources/checkouts" +$ExternalExtensionRoot = Join-Path $RepoRoot "src/extensions/external" $OpenSslSourceManifest = Join-Path $RepoRoot "src/sources/third-party/shared/openssl.toml" $PgxsBuildPlan = Join-Path $RepoRoot "src/extensions/generated/pgxs-build.tsv" $PortableUuidDir = Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/portable-uuid" @@ -522,6 +523,17 @@ function Get-DesiredHash { $parts.Add("source-input:$source=$(Get-FileSha256 $source)") } } + foreach ($recipeRoot in @( + Get-ChildItem -LiteralPath $ExternalExtensionRoot -Directory | + ForEach-Object { Join-Path $_.FullName "patches/windows-msvc" } | + Where-Object { Test-Path -LiteralPath $_ -PathType Container } | + Sort-Object + )) { + foreach ($input in Get-ChildItem -LiteralPath $recipeRoot -Recurse -File | Sort-Object FullName) { + $relativeInput = $input.FullName.Substring($RepoRoot.Length + 1).Replace("\", "/") + $parts.Add("external-windows-input:$relativeInput=$(Get-FileSha256 $input.FullName)") + } + } $bytes = [System.Text.Encoding]::UTF8.GetBytes(($parts -join "`n") + "`n") $sha = [System.Security.Cryptography.SHA256]::Create() try { @@ -2005,211 +2017,383 @@ function Add-UuidOsspMesonProducer { @("/I$portableUuidInclude", "/DHAVE_UUID_E2FS=1", "/DHAVE_UUID_UUID_H=1") } -function Get-PgTextsearchMakefileList([string]$ExtensionDir, [string]$Variable) { - $makefile = Join-Path $ExtensionDir "Makefile" - if (-not (Test-Path -LiteralPath $makefile -PathType Leaf)) { - Fail "pg_textsearch checkout is missing its authoritative Makefile: $makefile" +function Get-ExternalWindowsRecipeStringList( + [object]$Recipe, + [string]$PropertyName, + [string]$RecipePath, + [switch]$AllowEmpty +) { + $property = $Recipe.PSObject.Properties[$PropertyName] + if ($null -eq $property -or -not ($property.Value -is [System.Array])) { + Fail "$RecipePath must declare $PropertyName as an array" } - - $values = New-Object System.Collections.Generic.List[string] - $found = $false - $collecting = $false - $variablePattern = "^$([regex]::Escape($Variable))\s*=\s*(.*)$" - foreach ($line in Get-Content -Path $makefile) { - $fragment = $null - if (-not $collecting) { - if ($line -notmatch $variablePattern) { - continue - } - $found = $true - $collecting = $true - $fragment = $Matches[1].Trim() - } else { - $fragment = $line.Trim() + $values = @($property.Value) + if (-not $AllowEmpty -and $values.Count -eq 0) { + Fail "$RecipePath $PropertyName must not be empty" + } + $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($value in $values) { + if (-not ($value -is [string]) -or [string]::IsNullOrWhiteSpace($value)) { + Fail "$RecipePath $PropertyName must contain only non-empty strings" } - - $continues = $fragment.EndsWith("\") - if ($continues) { - $fragment = $fragment.Substring(0, $fragment.Length - 1).TrimEnd() + if (-not $seen.Add($value)) { + Fail "$RecipePath $PropertyName contains duplicate value '$value'" } - if ($fragment) { - foreach ($value in ($fragment -split "\s+")) { - if ($value) { - $values.Add($value) | Out-Null - } - } + } + return $values +} + +function Assert-ExternalWindowsRecipeProperties( + [object]$Value, + [string[]]$Allowed, + [string[]]$Required, + [string]$Label +) { + if ($null -eq $Value -or $null -eq $Value.PSObject) { + Fail "$Label must be an object" + } + $allowedNames = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::Ordinal + ) + foreach ($name in $Allowed) { + [void]$allowedNames.Add($name) + } + foreach ($property in $Value.PSObject.Properties) { + if (-not $allowedNames.Contains($property.Name)) { + Fail "$Label contains unsupported property '$($property.Name)'" } - if (-not $continues) { - break + } + foreach ($name in $Required) { + if ($null -eq $Value.PSObject.Properties[$name]) { + Fail "$Label is missing required property '$name'" } } +} - if (-not $found) { - Fail "pg_textsearch Makefile is missing its $Variable assignment" +function Convert-ExternalWindowsRecipePositiveInt([object]$Value, [string]$Label) { + $integerTypes = @( + [byte], [sbyte], [int16], [uint16], [int], [uint32], [long], [ulong] + ) + if ($null -eq $Value -or $integerTypes -notcontains $Value.GetType()) { + Fail "$Label must be an integer" } - @($values) + try { + $parsed = [System.Convert]::ToInt32($Value, [System.Globalization.CultureInfo]::InvariantCulture) + } catch { + Fail "$Label must fit in a 32-bit integer" + } + if ($parsed -le 0) { + Fail "$Label must be positive" + } + return $parsed } -function Assert-PgTextsearchWindowsPgxsManifest( - [string]$ExtensionDir, - [string[]]$Sources, - [string[]]$DataFiles +function Resolve-ExternalWindowsRecipePath( + [string]$Root, + [string]$RelativePath, + [string]$Label, + [switch]$RequireLeaf, + [switch]$RequireDirectory ) { - $expectedSources = @( - Get-PgTextsearchMakefileList $ExtensionDir "OBJS" | - ForEach-Object { - if (-not $_.EndsWith(".o", [System.StringComparison]::Ordinal)) { - Fail "pg_textsearch Makefile OBJS contains a non-object entry: $_" - } - $_.Substring(0, $_.Length - 2) + ".c" + if ([string]::IsNullOrWhiteSpace($RelativePath) -or + [System.IO.Path]::IsPathRooted($RelativePath) -or + $RelativePath.Contains("\") -or + $RelativePath -notmatch '^[A-Za-z0-9_./-]+$') { + Fail "$Label must be a safe forward-slash relative path, got '$RelativePath'" + } + $segments = @($RelativePath -split "/") + if ($segments.Count -eq 0 -or $segments -contains "" -or $segments -contains "." -or $segments -contains "..") { + Fail "$Label must not contain empty, current-directory, or parent-directory segments: '$RelativePath'" + } + + $rootPath = [System.IO.Path]::GetFullPath($Root).TrimEnd([char[]]@('\', '/')) + $resolved = [System.IO.Path]::GetFullPath((Join-Path $rootPath $RelativePath)) + $prefix = $rootPath + [System.IO.Path]::DirectorySeparatorChar + if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + Fail "$Label escapes its declared root: '$RelativePath'" + } + + $current = $rootPath + foreach ($segment in $segments) { + $current = Join-Path $current $segment + if (Test-Path -LiteralPath $current) { + $item = Get-Item -Force -LiteralPath $current + if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + Fail "$Label traverses a reparse point: '$RelativePath'" } - ) - $expectedDataFiles = @( - @(Get-PgTextsearchMakefileList $ExtensionDir "DATA") + - @("pg_textsearch.control") - ) - if (($Sources -join "`n") -ne ($expectedSources -join "`n")) { - Fail "pg_textsearch Windows sources differ from the pinned upstream Makefile: expected $($expectedSources -join ', '); got $($Sources -join ', ')" + } } - if (($DataFiles -join "`n") -ne ($expectedDataFiles -join "`n")) { - Fail "pg_textsearch Windows SQL payload differs from the pinned upstream Makefile: expected $($expectedDataFiles -join ', '); got $($DataFiles -join ', ')" + if ($RequireLeaf -and -not (Test-Path -LiteralPath $resolved -PathType Leaf)) { + Fail "$Label is missing its declared file: '$RelativePath'" } - foreach ($relativePath in @($Sources) + @($DataFiles)) { - $path = Join-Path $ExtensionDir $relativePath - if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { - Fail "pg_textsearch Windows input declared by the pinned Makefile is missing: $relativePath" - } + if ($RequireDirectory -and -not (Test-Path -LiteralPath $resolved -PathType Container)) { + Fail "$Label is missing its declared directory: '$RelativePath'" } + return $resolved } -function Get-PgTextsearchWindowsVersionDefine([string]$ExtensionDir) { - $control = Join-Path $ExtensionDir "pg_textsearch.control" - if (-not (Test-Path -LiteralPath $control -PathType Leaf)) { - Fail "pg_textsearch checkout is missing its control file: $control" +function Get-ExternalPgxsWindowsRecipe([string]$SqlName, [string]$CheckoutName) { + $productRoot = Join-Path $ExternalExtensionRoot $SqlName + $recipePath = Join-Path $productRoot "patches/windows-msvc/recipe.json" + if (-not (Test-Path -LiteralPath $recipePath -PathType Leaf)) { + return $null + } + try { + $recipe = Get-Content -Raw -LiteralPath $recipePath | ConvertFrom-Json + } catch { + Fail "cannot parse external Windows recipe ${recipePath}: $($_.Exception.Message)" + } + $recipeProperties = @( + "schema", + "sql_name", + "source_commit", + "default_version", + "sources", + "data_files", + "compiler_arguments", + "local_include_directories", + "force_include_files", + "version_defines", + "patches", + "layout_contracts", + "export_contracts" + ) + Assert-ExternalWindowsRecipeProperties $recipe $recipeProperties $recipeProperties $recipePath + foreach ($propertyName in @("schema", "sql_name", "source_commit", "default_version")) { + $property = $recipe.PSObject.Properties[$propertyName] + if ($null -eq $property -or -not ($property.Value -is [string]) -or [string]::IsNullOrWhiteSpace($property.Value)) { + Fail "$recipePath must declare non-empty string $propertyName" + } + } + if ($recipe.schema -cne "oliphaunt-external-pgxs-windows-recipe-v1") { + Fail "$recipePath has unsupported schema '$($recipe.schema)'" + } + if ($recipe.sql_name -cne $SqlName) { + Fail "$recipePath sql_name '$($recipe.sql_name)' does not match '$SqlName'" + } + if ($recipe.source_commit -notmatch '^[0-9a-f]{40}$') { + Fail "$recipePath source_commit must be a lowercase 40-character Git SHA" + } + if ($recipe.default_version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { + Fail "$recipePath default_version '$($recipe.default_version)' is unsupported" } - $controlText = Get-Content -Raw -Path $control - $versionMatches = [regex]::Matches( - $controlText, - "(?m)^\s*default_version\s*=\s*'([^']+)'\s*$" + $sourceManifest = Join-Path $productRoot "source.toml" + if (-not (Test-Path -LiteralPath $sourceManifest -PathType Leaf)) { + Fail "$recipePath product source manifest is missing: $sourceManifest" + } + $sourceManifestText = Get-Content -Raw -LiteralPath $sourceManifest + $sourceCommitMatches = [regex]::Matches( + $sourceManifestText, + '(?m)^commit\s*=\s*"([0-9a-f]{40})"\s*$' ) - if ($versionMatches.Count -ne 1) { - Fail "pg_textsearch control must declare exactly one single-quoted default_version; found $($versionMatches.Count)" + if ($sourceCommitMatches.Count -ne 1 -or + $sourceCommitMatches[0].Groups[1].Value -cne $recipe.source_commit) { + Fail "$recipePath source_commit must exactly match the canonical source.toml" + } + + $checkout = External-Checkout $CheckoutName + if (-not (Test-Path -LiteralPath $checkout -PathType Container)) { + Fail "$recipePath source checkout is missing: $checkout" + } + $global:LASTEXITCODE = 0 + $checkoutCommit = (& git -C $checkout rev-parse HEAD).Trim() + if ($LASTEXITCODE -ne 0 -or $checkoutCommit -cne $recipe.source_commit) { + Fail "$recipePath requires source commit $($recipe.source_commit), got '$checkoutCommit'" + } + $global:LASTEXITCODE = 0 + $checkoutStatus = @(& git -C $checkout status --porcelain=v1 --untracked-files=all) + if ($LASTEXITCODE -ne 0 -or $checkoutStatus.Count -ne 0) { + Fail "$recipePath requires a clean exact source checkout before staging" + } + + $controlPath = Resolve-ExternalWindowsRecipePath ` + $checkout "$SqlName.control" "$recipePath control file" -RequireLeaf + $controlText = Get-Content -Raw -LiteralPath $controlPath + $versionMatches = [regex]::Matches($controlText, "(?m)^\s*default_version\s*=\s*'([^']+)'\s*$") + if ($versionMatches.Count -ne 1 -or $versionMatches[0].Groups[1].Value -cne $recipe.default_version) { + Fail "$recipePath default_version must exactly match the pinned $SqlName.control" + } + + foreach ($relativePath in @(Get-ExternalWindowsRecipeStringList $recipe "sources" $recipePath)) { + [void](Resolve-ExternalWindowsRecipePath $checkout $relativePath "$recipePath source" -RequireLeaf) + } + foreach ($relativePath in @(Get-ExternalWindowsRecipeStringList $recipe "data_files" $recipePath)) { + [void](Resolve-ExternalWindowsRecipePath $checkout $relativePath "$recipePath data file" -RequireLeaf) + } + foreach ($relativePath in @(Get-ExternalWindowsRecipeStringList $recipe "local_include_directories" $recipePath)) { + [void](Resolve-ExternalWindowsRecipePath $checkout $relativePath "$recipePath include directory" -RequireDirectory) + } + [void](Get-ExternalWindowsRecipeStringList $recipe "compiler_arguments" $recipePath -AllowEmpty) + [void](Get-ExternalWindowsRecipeStringList $recipe "force_include_files" $recipePath -AllowEmpty) + foreach ($define in @(Get-ExternalWindowsRecipeStringList $recipe "version_defines" $recipePath -AllowEmpty)) { + if ($define -notmatch '^[A-Z][A-Z0-9_]*$') { + Fail "$recipePath version_defines contains invalid C macro '$define'" + } + } + + if (-not ($recipe.patches -is [System.Array])) { + Fail "$recipePath patches must be an array" } - $version = $versionMatches[0].Groups[1].Value - if ($version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { - Fail "pg_textsearch control has unsupported default_version '$version'" + $patches = @($recipe.patches) + if ($patches.Count -eq 0) { + Fail "$recipePath patches must not be empty" + } + $seenPatches = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($patch in $patches) { + Assert-ExternalWindowsRecipeProperties ` + $patch @("path", "sha256") @("path", "sha256") "$recipePath patch entry" + if (-not ($patch.path -is [string]) -or -not ($patch.sha256 -is [string])) { + Fail "$recipePath patches must declare string path and sha256 values" + } + if (-not $seenPatches.Add($patch.path)) { + Fail "$recipePath contains duplicate patch '$($patch.path)'" + } + if ($patch.sha256 -notmatch '^[0-9a-f]{64}$') { + Fail "$recipePath patch $($patch.path) has an invalid SHA-256" + } + $patchPath = Resolve-ExternalWindowsRecipePath ` + $productRoot $patch.path "$recipePath patch" -RequireLeaf + $actualPatchSha = Get-FileSha256 $patchPath + if ($actualPatchSha -cne $patch.sha256) { + Fail "$recipePath patch $($patch.path) expected SHA-256 $($patch.sha256), got $actualPatchSha" + } } - $makefile = Join-Path $ExtensionDir "Makefile" - $makefileText = Get-Content -Raw -Path $makefile - if (-not $makefileText.Contains('-DPG_TEXTSEARCH_VERSION=\"$(EXTVERSION)\"')) { - Fail "pg_textsearch Makefile no longer defines PG_TEXTSEARCH_VERSION from EXTVERSION" + if (-not ($recipe.layout_contracts -is [System.Array])) { + Fail "$recipePath layout_contracts must be an array" + } + $layouts = @($recipe.layout_contracts) + if ($layouts.Count -eq 0) { + Fail "$recipePath layout_contracts must not be empty" + } + $seenLayouts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($layout in $layouts) { + Assert-ExternalWindowsRecipeProperties ` + $layout @("path", "type", "size", "alignment") @("path", "type", "size", "alignment") ` + "$recipePath layout contract" + if (-not ($layout.path -is [string]) -or + -not ($layout.type -is [string]) -or $layout.type -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { + Fail "$recipePath layout_contracts entries must declare a path and C type" + } + $layout.size = Convert-ExternalWindowsRecipePositiveInt ` + $layout.size "$recipePath $($layout.type) size" + $layout.alignment = Convert-ExternalWindowsRecipePositiveInt ` + $layout.alignment "$recipePath $($layout.type) alignment" + [void](Resolve-ExternalWindowsRecipePath $checkout $layout.path "$recipePath layout source" -RequireLeaf) + $layoutKey = "$($layout.path):$($layout.type)" + if (-not $seenLayouts.Add($layoutKey)) { + Fail "$recipePath contains duplicate layout contract '$layoutKey'" + } } - # Keep the embedded quotes in one Meson argument. Meson/Ninja preserves - # them for cl.exe, so the macro expands to a C string literal. - "/DPG_TEXTSEARCH_VERSION=`"$version`"" + if (-not ($recipe.export_contracts -is [System.Array])) { + Fail "$recipePath export_contracts must be an array" + } + $seenExportPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($contract in @($recipe.export_contracts)) { + Assert-ExternalWindowsRecipeProperties ` + $contract @("path", "symbols") @("path", "symbols") "$recipePath export contract" + if (-not ($contract.path -is [string])) { + Fail "$recipePath export_contracts entries must declare path" + } + [void](Resolve-ExternalWindowsRecipePath $checkout $contract.path "$recipePath export source" -RequireLeaf) + if (-not $seenExportPaths.Add($contract.path)) { + Fail "$recipePath contains duplicate export contract path '$($contract.path)'" + } + foreach ($symbol in @(Get-ExternalWindowsRecipeStringList $contract "symbols" "$recipePath export contract $($contract.path)")) { + if ($symbol -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { + Fail "$recipePath export contract contains invalid C symbol '$symbol'" + } + } + } + return $recipe } -function Patch-PgTextsearchWindowsTypeLayout( - [string]$Text, - [string]$TypeName, - [string]$Attribute, - [int]$Pack, - [int]$ExpectedSize, - [int]$ExpectedAlignment +function Apply-ExternalPgxsWindowsRecipe( + [string]$SqlName, + [string]$ExtensionDir, + [object]$Recipe ) { - $escapedTypeName = [regex]::Escape($TypeName) - $escapedAttribute = [regex]::Escape($Attribute) - $declarationPattern = "(?m)^typedef struct $escapedTypeName\r?$" - $closingPattern = "(?m)^} __attribute__\(\($escapedAttribute\)\) $escapedTypeName;\r?$" - $declarationMatches = [regex]::Matches($Text, $declarationPattern) - $closingMatches = [regex]::Matches($Text, $closingPattern) - if ($declarationMatches.Count -ne 1 -or $closingMatches.Count -ne 1) { - Fail "pg_textsearch $TypeName layout changed upstream; expected one declaration and one __attribute__(($Attribute)) closing, found $($declarationMatches.Count) and $($closingMatches.Count)" - } - - $newline = if ($Text.Contains("`r`n")) { "`r`n" } else { "`n" } - $declaration = "#ifdef _MSC_VER${newline}#pragma pack(push, $Pack)${newline}#endif${newline}typedef struct $TypeName" - $closing = "} $TypeName;${newline}#ifdef _MSC_VER${newline}#pragma pack(pop)${newline}StaticAssertDecl(sizeof($TypeName) == $ExpectedSize, `"$TypeName must remain $ExpectedSize bytes on Windows`");${newline}StaticAssertDecl(__alignof($TypeName) == $ExpectedAlignment, `"$TypeName must remain $ExpectedAlignment-byte aligned on Windows`");${newline}#endif" - $Text = [regex]::Replace($Text, $declarationPattern, $declaration) - $Text = [regex]::Replace($Text, $closingPattern, $closing) - $Text -} - -function Patch-PgTextsearchWindowsSource([string]$ExtensionDir) { - $compat = Join-Path $ExtensionDir "src/oliphaunt_windows_compat.h" - Set-Content -Path $compat -Encoding UTF8 -Value @" -#ifdef _MSC_VER -#ifndef __attribute__ -#define __attribute__(x) -#endif -#endif -"@ - Set-Content -Path (Join-Path $ExtensionDir "src/unistd.h") -Encoding UTF8 -Value @" -#ifndef OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H -#define OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H -#endif -"@ - $segmentHeader = Join-Path $ExtensionDir "src/segment/segment.h" - $text = Get-Content -Raw -Path $segmentHeader - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpDictEntryV3" "aligned(4)" 4 12 4 - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpDictEntry" "aligned(8)" 8 16 8 - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpSegmentPosting" "packed" 1 14 1 - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpSkipEntryV3" "packed" 1 16 1 - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpSkipEntry" "packed" 1 20 1 - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpCtidMapEntry" "packed" 1 6 1 - Set-Content -Path $segmentHeader -Encoding UTF8 -Value $text - - $expullHeader = Join-Path $ExtensionDir "src/memtable/expull.h" - $text = Get-Content -Raw -Path $expullHeader - $text = Patch-PgTextsearchWindowsTypeLayout $text "TpExpullEntry" "packed" 1 7 1 - Set-Content -Path $expullHeader -Encoding UTF8 -Value $text - - $amHeader = Join-Path $ExtensionDir "src/am/am.h" - $text = Get-Content -Raw -Path $amHeader - $original = "Datum tp_handler(PG_FUNCTION_ARGS);" - $replacement = "extern PGDLLEXPORT Datum tp_handler(PG_FUNCTION_ARGS);" - if (-not $text.Contains($original)) { - Fail "pg_textsearch am.h is missing expected tp_handler declaration" - } - $text = $text.Replace($original, $replacement) - Set-Content -Path $amHeader -Encoding UTF8 -Value $text - - $vectorHeader = Join-Path $ExtensionDir "src/types/vector.h" - $text = Get-Content -Raw -Path $vectorHeader - foreach ($functionName in @("tpvector_in", "tpvector_out", "tpvector_recv", "tpvector_send", "to_tpvector", "tpvector_eq")) { - $original = "Datum $($functionName)(PG_FUNCTION_ARGS);" - $replacement = "extern PGDLLEXPORT Datum $($functionName)(PG_FUNCTION_ARGS);" - if (-not $text.Contains($original)) { - Fail "pg_textsearch vector.h is missing expected $functionName declaration" - } - $text = $text.Replace($original, $replacement) - } - Set-Content -Path $vectorHeader -Encoding UTF8 -Value $text - - $queryHeader = Join-Path $ExtensionDir "src/types/query.h" - $text = Get-Content -Raw -Path $queryHeader - foreach ($functionName in @( - "tpquery_in", - "tpquery_out", - "tpquery_recv", - "tpquery_send", - "to_tpquery_text", - "to_tpquery_text_index", - "bm25_text_bm25query_score", - "bm25_text_text_score", - "tpquery_eq" - )) { - $original = "Datum $($functionName)(PG_FUNCTION_ARGS);" - $replacement = "extern PGDLLEXPORT Datum $($functionName)(PG_FUNCTION_ARGS);" - if (-not $text.Contains($original)) { - Fail "pg_textsearch query.h is missing expected $functionName declaration" + $productRoot = Join-Path $ExternalExtensionRoot $SqlName + $recipePath = Join-Path $productRoot "patches/windows-msvc/recipe.json" + $patchCeiling = Split-Path -Parent ([System.IO.Path]::GetFullPath($ExtensionDir)) + $previousGitCeiling = [Environment]::GetEnvironmentVariable( + "GIT_CEILING_DIRECTORIES", + [EnvironmentVariableTarget]::Process + ) + try { + # The staging tree may live below an enclosing repository (the default work root + # is under target). Bound discovery so git apply operates as a patch utility rooted + # at ExtensionDir; otherwise Git filters every path against the worktree prefix. + [Environment]::SetEnvironmentVariable( + "GIT_CEILING_DIRECTORIES", + $patchCeiling, + [EnvironmentVariableTarget]::Process + ) + foreach ($patch in @($Recipe.patches)) { + $patchPath = Resolve-ExternalWindowsRecipePath ` + $productRoot $patch.path "$recipePath patch" -RequireLeaf + $global:LASTEXITCODE = 0 + & git -C $ExtensionDir apply --check --whitespace=error-all $patchPath + if ($LASTEXITCODE -ne 0) { + Fail "$recipePath patch $($patch.path) does not apply exactly to source commit $($Recipe.source_commit)" + } + $global:LASTEXITCODE = 0 + & git -C $ExtensionDir apply --whitespace=error-all $patchPath + if ($LASTEXITCODE -ne 0) { + Fail "$recipePath patch $($patch.path) failed to apply" + } + } + } finally { + [Environment]::SetEnvironmentVariable( + "GIT_CEILING_DIRECTORIES", + $previousGitCeiling, + [EnvironmentVariableTarget]::Process + ) + } + + foreach ($relativePath in @($Recipe.sources) + @($Recipe.data_files) + @($Recipe.force_include_files)) { + [void](Resolve-ExternalWindowsRecipePath $ExtensionDir $relativePath "$recipePath staged input" -RequireLeaf) + } + foreach ($layout in @($Recipe.layout_contracts)) { + $layoutPath = Resolve-ExternalWindowsRecipePath ` + $ExtensionDir $layout.path "$recipePath staged layout" -RequireLeaf + $text = Get-Content -Raw -LiteralPath $layoutPath + $sizeMarker = "StaticAssertDecl(sizeof($($layout.type)) == $($layout.size)," + $alignmentMarker = "StaticAssertDecl(__alignof($($layout.type)) == $($layout.alignment)," + if ([regex]::Matches($text, [regex]::Escape($sizeMarker)).Count -ne 1 -or + [regex]::Matches($text, [regex]::Escape($alignmentMarker)).Count -ne 1) { + Fail "$recipePath staged layout $($layout.type) does not contain its unique size/alignment assertions" + } + } + foreach ($contract in @($Recipe.export_contracts)) { + $exportPath = Resolve-ExternalWindowsRecipePath ` + $ExtensionDir $contract.path "$recipePath staged export" -RequireLeaf + $text = Get-Content -Raw -LiteralPath $exportPath + foreach ($symbol in @($contract.symbols)) { + $marker = "extern PGDLLEXPORT Datum $symbol(PG_FUNCTION_ARGS);" + if ([regex]::Matches($text, [regex]::Escape($marker)).Count -ne 1) { + Fail "$recipePath staged export $symbol is missing or duplicated in $($contract.path)" + } } - $text = $text.Replace($original, $replacement) } - Set-Content -Path $queryHeader -Encoding UTF8 -Value $text +} + +function Get-ExternalPgxsWindowsRecipeCArgs([string]$ExtensionDir, [object]$Recipe) { + $arguments = New-Object System.Collections.Generic.List[string] + foreach ($argument in @($Recipe.compiler_arguments)) { + $arguments.Add($argument) | Out-Null + } + foreach ($define in @($Recipe.version_defines)) { + $arguments.Add("/D$define=`"$($Recipe.default_version)`"") | Out-Null + } + foreach ($relativePath in @($Recipe.force_include_files)) { + $includePath = Resolve-ExternalWindowsRecipePath ` + $ExtensionDir $relativePath "external Windows force-include" -RequireLeaf + $arguments.Add("/FI$(Meson-Path $includePath)") | Out-Null + } + return @($arguments) } function Patch-PgUuidv7WindowsSource([string]$ExtensionDir) { @@ -2254,29 +2438,46 @@ function Add-ExternalPgxsMesonProducer( [string[]]$Sources, [string[]]$DataFiles, [string[]]$CArgs = @(), - [string[]]$LocalIncludeDirs = @() + [string[]]$LocalIncludeDirs = @(), + [object]$WindowsRecipe = $null ) { if (-not (NativeExtension-Selected $SqlName)) { return } $destination = Join-Path $OliphauntContribDir $Subdir Copy-SourceTree (External-Checkout $CheckoutName) $destination + if ($null -ne $WindowsRecipe) { + Apply-ExternalPgxsWindowsRecipe $SqlName $destination $WindowsRecipe + $CArgs = @($CArgs) + @(Get-ExternalPgxsWindowsRecipeCArgs $destination $WindowsRecipe) + $LocalIncludeDirs = @($LocalIncludeDirs) + @($WindowsRecipe.local_include_directories) + } if ($SqlName -eq "pg_uuidv7") { Patch-PgUuidv7WindowsSource $destination } - if ($SqlName -eq "pg_textsearch") { - Assert-PgTextsearchWindowsPgxsManifest $destination $Sources $DataFiles - Patch-PgTextsearchWindowsSource $destination - $compatHeader = Meson-Path (Join-Path $destination "src/oliphaunt_windows_compat.h") - $versionDefine = Get-PgTextsearchWindowsVersionDefine $destination - $CArgs = @($CArgs) + @($versionDefine, "/FI$compatHeader") - } if ($SqlName -eq "vector") { Copy-Item -Force (Join-Path $destination "sql/vector.sql") (Join-Path $destination "sql/vector--0.8.2.sql") } Write-OliphauntMesonModule $Subdir $ModuleName $Sources $DataFiles $CArgs @() $LocalIncludeDirs } +function Add-ExternalPgxsMesonProducerFromWindowsRecipe( + [string]$SqlName, + [string]$CheckoutName, + [string]$Subdir, + [string]$ModuleName +) { + if (-not (NativeExtension-Selected $SqlName)) { + return + } + $recipe = Get-ExternalPgxsWindowsRecipe $SqlName $CheckoutName + if ($null -eq $recipe) { + Fail "$SqlName must own patches/windows-msvc/recipe.json for its recipe-backed Windows producer" + } + Add-ExternalPgxsMesonProducer ` + $SqlName $CheckoutName $Subdir $ModuleName ` + @($recipe.sources) @($recipe.data_files) @() @() $recipe +} + function Add-ExternalPgxsMesonProducers { Add-ExternalPgxsMesonProducer ` "pg_hashids" "pg_hashids" "pg_hashids" "pg_hashids" ` @@ -2317,62 +2518,8 @@ function Add-ExternalPgxsMesonProducers { "sql/pg_uuidv7--1.7.sql", "pg_uuidv7.control" ) - Add-ExternalPgxsMesonProducer ` - "pg_textsearch" "pg_textsearch" "pg_textsearch" "pg_textsearch" ` - @( - "src/mod.c", - "src/source.c", - "src/am/handler.c", - "src/am/build.c", - "src/am/build_context.c", - "src/am/build_parallel.c", - "src/am/scan.c", - "src/am/vacuum.c", - "src/memtable/arena.c", - "src/memtable/expull.c", - "src/memtable/memtable.c", - "src/memtable/posting.c", - "src/memtable/stringtable.c", - "src/memtable/scan.c", - "src/memtable/source.c", - "src/segment/segment.c", - "src/segment/dictionary.c", - "src/segment/scan.c", - "src/segment/merge.c", - "src/segment/docmap.c", - "src/segment/compression.c", - "src/query/bmw.c", - "src/query/score.c", - "src/types/vector.c", - "src/types/query.c", - "src/state/state.c", - "src/state/registry.c", - "src/state/metapage.c", - "src/state/limit.c", - "src/planner/hooks.c", - "src/planner/cost.c", - "src/debug/dump.c" - ) ` - @( - "sql/pg_textsearch--0.6.1.sql", - "sql/pg_textsearch--0.0.1--0.0.2.sql", - "sql/pg_textsearch--0.0.2--0.0.3.sql", - "sql/pg_textsearch--0.0.3--0.0.4.sql", - "sql/pg_textsearch--0.0.4--0.0.5.sql", - "sql/pg_textsearch--0.0.5--0.1.0.sql", - "sql/pg_textsearch--0.1.0--0.2.0.sql", - "sql/pg_textsearch--0.2.0--0.3.0.sql", - "sql/pg_textsearch--0.3.0--0.4.0.sql", - "sql/pg_textsearch--0.4.0--0.4.1.sql", - "sql/pg_textsearch--0.4.1--0.4.2.sql", - "sql/pg_textsearch--0.4.2--0.5.0.sql", - "sql/pg_textsearch--0.5.0--0.6.1.sql", - "sql/pg_textsearch--0.5.1--0.6.1.sql", - "sql/pg_textsearch--0.6.0--0.6.1.sql", - "pg_textsearch.control" - ) ` - @("/D_CRT_SECURE_NO_WARNINGS") ` - @("src") + Add-ExternalPgxsMesonProducerFromWindowsRecipe ` + "pg_textsearch" "pg_textsearch" "pg_textsearch" "pg_textsearch" Add-ExternalPgxsMesonProducer ` "vector" "pgvector" "vector" "vector" ` @( diff --git a/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json b/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json index 65323c692..b1216db11 100644 --- a/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json +++ b/src/runtimes/liboliphaunt/wasix/.release-semantic-inputs.json @@ -154,7 +154,7 @@ }, { "path": "tools/xtask/src/asset_fingerprint.rs", - "sha256": "e7eb15342326c2480fdf991b2de271114ac2d336aff7ac81291c9fe4aa15dad6" + "sha256": "c7ddffef00f3d31e39952d943880895aa14ec4d76bfb352020abe717675cbcb4" }, { "path": "tools/xtask/src/asset_manifest.rs", @@ -212,11 +212,11 @@ }, { "path": "src/extensions/generated/docs/extension-evidence.json", - "sha256": "1b9ae33dc2ca239985c4cd5c1571e2669f2ea905acf45b6d81e4e31261e7f7bb" + "sha256": "14cbf48d651ccdc86f489abdd93176a49344abb924ccb55346494fef29d0d99f" }, { "path": "src/extensions/generated/docs/extensions.json", - "sha256": "e0741112aeb1d1a187ba3fda06a06302026048eceef08b389f2e55a94343cd69" + "sha256": "ab083ce76e08fea7a631d672783f888bc305a839f300a023c7a5c48b3402e7c3" }, { "path": "src/extensions/generated/extensions.build-plan.json", @@ -224,11 +224,11 @@ }, { "path": "src/extensions/generated/extensions.catalog.json", - "sha256": "79ba39ba702d247fd975089cc812fbb4c3624ebd325f0fe1f6255dd575efddb7" + "sha256": "36171c63bfb7f346c220c6a8da8cf162628c2de59b8b9134b1d3821feb05d21c" }, { "path": "src/extensions/generated/mobile/static-extensions.tsv", - "sha256": "4bff991f8fce3c66f1da9428fd84861c4700a0e7c744cce51c1c5837f0cafd5d" + "sha256": "62af4ffadb40d76168614093cb961613678c87eb9ce88268845364862e05043c" }, { "path": "src/extensions/generated/mobile/static-registry.json", @@ -377,5 +377,5 @@ ] } ], - "sha256": "763a5c1635efc9c5275c839eb4870379d19077cb6426f89cc690e9ae1890bbad" + "sha256": "57025f4fbeb23412b831c21f92414332fd8028faa5f58936b03edf77517277e8" } diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index e02e39f54..3948818f7 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -178f0d74e7367e8dfe4eb44c32fb7ed62f0c17f30001b56b6d7f83617f11b5c5 +e6714b54eb106625b42bb1011619a3002e6e46c2ee7dfdd93a18f117315647c4 diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/wasix-dl.exports b/src/runtimes/liboliphaunt/wasix/assets/generated/wasix-dl.exports index a30064b67..51258b399 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/wasix-dl.exports +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/wasix-dl.exports @@ -1,3 +1,4 @@ +AbortCurrentTransaction AcquireRewriteLocks AggCheckCallContext AllocSetContextCreateInternal @@ -33,11 +34,13 @@ CallerFInfoFunctionCall2 CatalogTupleDelete CatalogTupleInsert CatalogTupleUpdate +ChangeVarNodes CheckFunctionValidatorAccess CheckIndexCompatible CheckTableNotInUse CheckXidAlive CommandCounterIncrement +CommitTransactionCommand ConditionVariableBroadcast ConditionVariableCancelSleep ConditionVariableInit @@ -97,6 +100,8 @@ EvictUnpinnedBuffer ExecDropSingleTupleTableSlot ExecInitExpr ExecInitExprWithParams +ExecPrepareQual +ExecStoreBufferHeapTuple ExecStoreVirtualTuple ExecuteTruncateGuts ExecutorEnd @@ -121,8 +126,8 @@ ExprEvalPushStep ExtendBufferedRel Float8GetDatum FlushErrorState -FlushOneBuffer FlushRelationBuffers +FormIndexDatum FreeAccessStrategy FreeCachedExpression FreeExecutorState @@ -166,6 +171,7 @@ GetTopFullTransactionId GetTransactionSnapshot GetUserId GetUserIdAndSecContext +GetXLogInsertRecPtr GetXLogReplayRecPtr GinDataLeafPageGetItems HeapTupleGetUpdateXid @@ -182,12 +188,14 @@ InstrEndLoop Int64GetDatum InterruptPending InvalidObjectAddress +IsAbortedTransactionBlockState IsTransactionState IsValidJsonNumber ItemPointerCompare ItemPointerEquals JsonbValueToJsonb LWLockAcquire +LWLockConditionalAcquire LWLockHeldByMe LWLockInitialize LWLockNewTrancheId @@ -264,6 +272,7 @@ ReThrowError ReadBuffer ReadBufferExtended ReadMultiXactIdRange +ReadNextFullTransactionId ReadNextMultiXactId RecentXmin RecordFreeIndexPage @@ -356,8 +365,11 @@ SharedFileSetInit ShmemInitStruct SnapshotAnyData SplitIdentifierString +StartTransactionCommand +SysCacheGetAttr SysCacheGetAttrNotNull SystemFuncName +TTSOpsBufferHeapTuple TTSOpsMinimalTuple TTSOpsVirtual TopMemoryContext @@ -451,6 +463,7 @@ atexit atof atoi be_lo_unlink +before_shmem_exit bit_in bitcmp biteq @@ -532,6 +545,7 @@ construct_md_array contain_aggs_of_level contain_mutable_functions contain_nonstrict_functions +contain_var_clause convert_network_to_scalar convert_tuples_by_position copyObjectImpl @@ -583,8 +597,6 @@ dshash_detach dshash_find dshash_find_or_insert dshash_get_hash_table_handle -dshash_memcmp -dshash_memhash dshash_release_lock dshash_seq_init dshash_seq_next @@ -945,7 +957,6 @@ pg_detoast_datum_packed pg_detoast_datum_slice pg_do_encoding_conversion pg_encoding_max_length -pg_fprintf pg_get_indexdef_columns_extended pg_get_querydef pg_get_shmem_pagesize @@ -953,6 +964,7 @@ pg_global_prng_state pg_is_ascii pg_ltoa pg_mb2wchar_with_len +pg_mblen pg_mblen_cstr pg_mblen_range pg_mblen_unbounded @@ -1022,6 +1034,7 @@ pre_format_elog_string process_shared_preload_libraries_in_progress psprintf pstrdup +pull_var_clause pull_varattnos pull_varnos pull_vars_of_level @@ -1044,7 +1057,6 @@ realloc recordDependencyOn recordDependencyOnExpr regclassin -regconfigout register_ENR register_reloptions_validator relation_close @@ -1170,7 +1182,6 @@ timestamp_mi timetz_cmp tm2timestamp to_hex32 -to_tsvector to_tsvector_byid toast_close_indexes toast_open_indexes @@ -1181,6 +1192,7 @@ transformDistinctClause transformExpr transformRelOptions transformStmt +try_index_open tsearch_readline tsearch_readline_begin tsearch_readline_end diff --git a/tools/graph/synthetic/ci-affected.toml b/tools/graph/synthetic/ci-affected.toml index 6a03cf4b7..596da0ee7 100644 --- a/tools/graph/synthetic/ci-affected.toml +++ b/tools/graph/synthetic/ci-affected.toml @@ -192,6 +192,18 @@ required_jobs = [ "node-direct", ] +[cases.js_exact_candidate_extension_scenarios_fixture] +path = "tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs" +required_jobs = [ + "broker-runtime", + "extension-artifacts-native", + "js-sdk-exact-candidate-consumer", + "js-sdk-package", + "liboliphaunt-native-desktop", + "liboliphaunt-native-ios", + "node-direct", +] + [cases.js_exact_candidate_procsignal_fixture] path = "tools/release/fixtures/js-exact-candidate-procsignal.mjs" required_jobs = [ diff --git a/tools/release/build-extension-ci-artifacts.mjs b/tools/release/build-extension-ci-artifacts.mjs index e3c5e1a2b..db19887f6 100644 --- a/tools/release/build-extension-ci-artifacts.mjs +++ b/tools/release/build-extension-ci-artifacts.mjs @@ -37,6 +37,7 @@ import { extensionMetadata, extensionSourceIdentity, extensionSqlNames, + releaseMetadata, } from "./release-artifact-targets.mjs"; import { swiftExtensionCarrierAssetName, @@ -46,6 +47,11 @@ import { AOT_TARGET_TRIPLES } from "./wasix-cargo-artifact-contract.mjs"; import { assertCanonicalWasixAotManifest } from "./wasix-aot-manifest.mjs"; const PREFIX = "build-extension-ci-artifacts.mjs"; +const RELEASE_EXTENSION_METADATA_BASENAME = ".release-extension-metadata.json"; +const RELEASE_EXTENSION_METADATA_SOURCES = [ + { name: "extension-catalog", path: "src/extensions/generated/extensions.catalog.json" }, + { name: "extension-evidence", path: "src/extensions/generated/docs/extension-evidence.json" }, +]; function fail(message) { console.error(`${PREFIX}: ${message}`); @@ -64,14 +70,60 @@ function extensionProducts() { return exactExtensionProducts(PREFIX); } -function generatedExtensionRow(sqlName) { - const metadata = path.join(ROOT, "src/extensions/generated/sdk/kotlin.json"); - const data = JSON.parse(readFileSync(metadata, "utf8")); - const row = (data.extensions ?? []).find((item) => item && item["sql-name"] === sqlName); - if (!row) { - fail(`generated extension metadata has no row for ${sqlName}`); +export function readProductReleaseExtensionMetadata(file, { product, sqlNames }) { + const context = rel(file); + let data; + try { + data = JSON.parse(readFileSync(file, "utf8")); + } catch (error) { + throw new Error(`${context} is not readable JSON: ${error.message}`); + } + if ( + data?.["format-version"] !== 1 + || data.consumer !== "release-product" + || data["release-product"] !== product + || !Array.isArray(data.extensions) + ) { + throw new Error(`${context} must be the format-version 1 release-product contract for ${product}`); + } + if (JSON.stringify(data["generated-from"]) !== JSON.stringify(RELEASE_EXTENSION_METADATA_SOURCES)) { + throw new Error(`${context} must be generated directly from the canonical extension catalog and evidence`); + } + + const rows = new Map(); + for (const [index, row] of data.extensions.entries()) { + const sqlName = row?.["sql-name"]; + if (typeof sqlName !== "string" || sqlName.length === 0) { + throw new Error(`${context} extension row ${index} must define a SQL name`); + } + if (row["release-product"] !== product) { + throw new Error(`${context} ${sqlName}.release-product must be ${product}`); + } + if (rows.has(sqlName)) { + throw new Error(`${context} repeats SQL extension ${sqlName}`); + } + rows.set(sqlName, row); + } + + const actualSqlNames = [...rows.keys()]; + if (JSON.stringify(actualSqlNames) !== JSON.stringify(sqlNames)) { + throw new Error( + `${context} must contain exactly the ordered release members for ${product}: expected ${sqlNames.join(", ")}, got ${actualSqlNames.join(", ")}`, + ); + } + return rows; +} + +export function productReleaseExtensionMetadata(product, sqlNames = extensionSqlNames(product, PREFIX)) { + const packageRoot = resolveRepoPath(releaseMetadata(product, PREFIX).packagePath, { + label: `${product} package path`, + }); + const metadata = path.join(packageRoot, RELEASE_EXTENSION_METADATA_BASENAME); + try { + return readProductReleaseExtensionMetadata(metadata, { product, sqlNames }); + } catch (error) { + fail(error.message); } - return row; } function stringList(value, label) { @@ -472,11 +524,11 @@ function publicMemberAsset(asset) { function stageMember(product, sqlName, version, productRoot, { destinationDir, bundle, + extensionRow, requireNative, requireWasix, requireNativeTargets, }) { - const extensionRow = generatedExtensionRow(sqlName); const assets = []; let iosRegistration = null; for (const row of nativeAssetsFor(sqlName, { product, required: requireNative })) { @@ -779,6 +831,7 @@ async function stageProduct(product, { outputRoot, requireNative, requireWasix, fail(`unknown exact-extension product ${product}; expected one of: ${[...known].sort(compareText).join(", ")}`); } const sqlNames = extensionSqlNames(product, PREFIX); + const releaseExtensionMetadata = productReleaseExtensionMetadata(product, sqlNames); const version = await currentProductVersion(product, PREFIX); const productRoot = path.join(outputRoot, product); const assetDir = path.join(productRoot, "release-assets"); @@ -788,6 +841,7 @@ async function stageProduct(product, { outputRoot, requireNative, requireWasix, const members = sqlNames.map((sqlName) => stageMember(product, sqlName, version, productRoot, { destinationDir: bundle ? path.join(productRoot, "member-assets", sqlName) : assetDir, bundle, + extensionRow: releaseExtensionMetadata.get(sqlName), requireNative, requireWasix, requireNativeTargets, diff --git a/tools/release/build-extension-ci-artifacts.test.mjs b/tools/release/build-extension-ci-artifacts.test.mjs new file mode 100644 index 000000000..0457a7031 --- /dev/null +++ b/tools/release/build-extension-ci-artifacts.test.mjs @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, test } from "node:test"; + +import { + productReleaseExtensionMetadata, + readProductReleaseExtensionMetadata, +} from "./build-extension-ci-artifacts.mjs"; + +const roots = []; +const GENERATED_FROM = [ + { name: "extension-catalog", path: "src/extensions/generated/extensions.catalog.json" }, + { name: "extension-evidence", path: "src/extensions/generated/docs/extension-evidence.json" }, +]; + +function fixtureMetadata(overrides = {}) { + const root = mkdtempSync(path.join(tmpdir(), "oliphaunt-release-extension-metadata-")); + roots.push(root); + const file = path.join(root, ".release-extension-metadata.json"); + writeFileSync(file, `${JSON.stringify({ + "format-version": 1, + consumer: "release-product", + "release-product": "oliphaunt-extension-example", + "generated-from": GENERATED_FROM, + extensions: [ + { + id: "example", + "sql-name": "example", + "release-product": "oliphaunt-extension-example", + }, + ], + ...overrides, + }, null, 2)}\n`); + return file; +} + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true }); +}); + +test("release assembly resolves the product-local neutral extension contract", () => { + const rows = productReleaseExtensionMetadata("oliphaunt-extension-pg-textsearch"); + assert.deepEqual([...rows.keys()], ["pg_textsearch"]); + assert.equal(rows.get("pg_textsearch")["release-product"], "oliphaunt-extension-pg-textsearch"); +}); + +test("release extension metadata rejects SDK ownership and mismatched product membership", () => { + const sdkOwned = fixtureMetadata({ + "generated-from": [ + { name: "kotlin-extension-catalog", path: "src/extensions/generated/sdk/kotlin.json" }, + ], + }); + assert.throws( + () => readProductReleaseExtensionMetadata(sdkOwned, { + product: "oliphaunt-extension-example", + sqlNames: ["example"], + }), + /generated directly from the canonical extension catalog and evidence/u, + ); + + const wrongMembers = fixtureMetadata({ + extensions: [ + { + id: "other", + "sql-name": "other", + "release-product": "oliphaunt-extension-example", + }, + ], + }); + assert.throws( + () => readProductReleaseExtensionMetadata(wrongMembers, { + product: "oliphaunt-extension-example", + sqlNames: ["example"], + }), + /must contain exactly the ordered release members/u, + ); +}); diff --git a/tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs b/tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs new file mode 100644 index 000000000..c5caeede4 --- /dev/null +++ b/tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs @@ -0,0 +1,117 @@ +import assert from "node:assert/strict"; + +const PG_TEXTSEARCH_TABLE = "exact_candidate_pg_textsearch_english"; +const PG_TEXTSEARCH_INDEX = "exact_candidate_pg_textsearch_english_bm25"; + +function resultText(result, column) { + assert.equal(result?.rows?.length ?? 1, 1, `${column} query must return exactly one row`); + return result.getText(0, column); +} + +async function pgTextsearchTopId(database, terms) { + const result = await database.query( + `SELECT id::text AS id FROM ${PG_TEXTSEARCH_TABLE} ` + + `ORDER BY body <@> to_bm25query('${terms}', '${PG_TEXTSEARCH_INDEX}') LIMIT 1`, + ); + return resultText(result, "id"); +} + +async function assertPgTextsearchState(database, writeTerms) { + const existingTopId = await pgTextsearchTopId(database, "running database"); + assert.equal(existingTopId, "1", "pg_textsearch must preserve the original indexed row"); + + const updatedTopId = await pgTextsearchTopId(database, "updated migration target"); + assert.equal(updatedTopId, "2", "pg_textsearch must index an updated row"); + + const sentinelTopId = await pgTextsearchTopId(database, "merge sentinel proof"); + assert.equal(sentinelTopId, "1000", "pg_textsearch must preserve the post-index sentinel row"); + + const postOpenWriteTopId = await pgTextsearchTopId(database, writeTerms); + assert.equal(postOpenWriteTopId, "1001", "pg_textsearch must index a post-open write"); + + const deletedTopId = await pgTextsearchTopId(database, "deleted wildlife marker"); + assert.equal( + deletedTopId, + "4", + "pg_textsearch must discard the higher-scoring deleted posting and return its survivor", + ); + + return { + sqlName: "pg_textsearch", + scenario: "bm25-mutation-merge-persistence", + existingTopId, + updatedTopId, + sentinelTopId, + postOpenWriteTopId, + deletionSurvivorTopId: deletedTopId, + }; +} + +export async function verifyCoreEnglishTextSearch(database) { + const result = await database.query( + "SELECT CASE WHEN " + + "to_tsvector('pg_catalog.english', 'the quick foxes running') " + + "@@ to_tsquery('pg_catalog.english', 'run & fox') " + + "THEN 'english-snowball-ok' ELSE 'english-snowball-failed' END AS value", + ); + assert.equal( + resultText(result, "value"), + "english-snowball-ok", + "every runtime mode must load PostgreSQL core dict_snowball and its English stopword data", + ); +} + +export async function verifyPgTextsearchEnglishBm25(database, phase) { + assert.ok( + phase === "produce" || phase === "verify-restored", + `unsupported pg_textsearch exact-candidate phase ${phase}`, + ); + + if (phase === "produce") { + await database.query( + `CREATE TABLE ${PG_TEXTSEARCH_TABLE} (id bigint PRIMARY KEY, body text NOT NULL)`, + ); + await database.query( + `INSERT INTO ${PG_TEXTSEARCH_TABLE} (id, body) VALUES ` + + "(1, 'PostgreSQL databases support reliable runners'), " + + "(2, 'An unrelated document about walking'), " + + "(3, 'deleted wildlife marker deleted wildlife marker deleted wildlife marker'), " + + "(4, 'deleted wildlife marker')", + ); + await database.query( + `CREATE INDEX ${PG_TEXTSEARCH_INDEX} ` + + `ON ${PG_TEXTSEARCH_TABLE} USING bm25 (body) ` + + "WITH (text_config = 'pg_catalog.english')", + ); + await database.query( + `INSERT INTO ${PG_TEXTSEARCH_TABLE} (id, body) VALUES ` + + "(1000, 'merge sentinel proof merge sentinel proof'), " + + "(1001, 'post activation write marker')", + ); + await database.query( + `UPDATE ${PG_TEXTSEARCH_TABLE} ` + + "SET body = 'updated migration target updated migration target' WHERE id = 2", + ); + await database.query(`DELETE FROM ${PG_TEXTSEARCH_TABLE} WHERE id = 3`); + await database.query(`SELECT bm25_force_merge('${PG_TEXTSEARCH_INDEX}')`); + return assertPgTextsearchState(database, "post activation write marker"); + } + + const persisted = await assertPgTextsearchState(database, "post activation write marker"); + await database.query( + `UPDATE ${PG_TEXTSEARCH_TABLE} ` + + "SET body = 'post restore write marker' WHERE id = 1001", + ); + await database.query(`SELECT bm25_force_merge('${PG_TEXTSEARCH_INDEX}')`); + const afterWrite = await assertPgTextsearchState(database, "post restore write marker"); + assert.deepEqual(afterWrite, persisted); + return afterWrite; +} + +export async function verifyExtensionFunctionality(database, extensions, phase) { + const functional = []; + if (extensions.some((extension) => extension.sqlName === "pg_textsearch")) { + functional.push(await verifyPgTextsearchEnglishBm25(database, phase)); + } + return functional; +} diff --git a/tools/release/fixtures/js-exact-candidate-runtime.mjs b/tools/release/fixtures/js-exact-candidate-runtime.mjs index 7acd2d9ad..fe580720a 100644 --- a/tools/release/fixtures/js-exact-candidate-runtime.mjs +++ b/tools/release/fixtures/js-exact-candidate-runtime.mjs @@ -9,6 +9,10 @@ import { verifyNativeDirectProcSignalSurvival, withNativeDirectExtensionSignalIsolation, } from "./js-exact-candidate-procsignal.mjs"; +import { + verifyCoreEnglishTextSearch, + verifyExtensionFunctionality, +} from "./js-exact-candidate-extension-scenarios.mjs"; const OVERRIDE_ENV = [ "LIBOLIPHAUNT_PATH", @@ -72,62 +76,12 @@ function quoteIdentifier(value) { return `"${value.replaceAll('"', '""')}"`; } -async function verifyCoreEnglishTextSearch(database) { - const result = await database.query( - "SELECT CASE WHEN " - + "to_tsvector('pg_catalog.english', 'the quick foxes running') " - + "@@ to_tsquery('pg_catalog.english', 'run & fox') " - + "THEN 'english-snowball-ok' ELSE 'english-snowball-failed' END AS value", - ); - assert.equal( - result.getText(0, "value"), - "english-snowball-ok", - "every runtime mode must load PostgreSQL core dict_snowball and its English stopword data", - ); -} - -async function verifyPgTextsearchEnglishBm25(database) { - await database.query("DROP TABLE IF EXISTS exact_candidate_pg_textsearch_english"); - await database.query( - "CREATE TABLE exact_candidate_pg_textsearch_english (id bigint PRIMARY KEY, body text NOT NULL)", - ); - await database.query( - "INSERT INTO exact_candidate_pg_textsearch_english (id, body) VALUES " - + "(1, 'PostgreSQL databases support reliable runners'), " - + "(2, 'An unrelated document about walking')", - ); - await database.query( - "CREATE INDEX exact_candidate_pg_textsearch_english_bm25 " - + "ON exact_candidate_pg_textsearch_english USING bm25 (body) " - + "WITH (text_config = 'pg_catalog.english')", - ); - const result = await database.query( - "SELECT id::text AS id FROM exact_candidate_pg_textsearch_english " - + "ORDER BY body <@> to_bm25query(" - + "'running database', 'exact_candidate_pg_textsearch_english_bm25') LIMIT 1", - ); - assert.equal(result.getText(0, "id"), "1", "pg_textsearch English BM25 must stem and rank the matching row"); - await database.query("DROP TABLE exact_candidate_pg_textsearch_english"); - return { - sqlName: "pg_textsearch", - scenario: "nonempty-english-bm25-create-and-query", - topId: "1", - }; -} - -async function verifyExtensionFunctionality(database, extensions) { - const functional = []; - if (extensions.some((extension) => extension.sqlName === "pg_textsearch")) { - functional.push(await verifyPgTextsearchEnglishBm25(database)); - } - return functional; -} - async function activateAndVerifyExtensions( database, extensions, checkpoint, procSignalSentinel, + phase, ) { if (extensions.length === 0) return { activated: [], catalog: [], functional: [], loaded: [] }; const loaded = []; @@ -160,7 +114,7 @@ async function activateAndVerifyExtensions( const catalog = result.rows.map((row) => row.text(0)); assert.deepEqual(catalog, expectedCatalog, "the database extension catalog must match the exact promoted set"); await checkpoint("extension-catalog-verified", { count: catalog.length }); - const functional = await verifyExtensionFunctionality(database, extensions); + const functional = await verifyExtensionFunctionality(database, extensions, phase); await checkpoint("extension-functionality-verified", { count: functional.length }); return { activated: extensions.map((extension) => extension.sqlName).sort(), @@ -268,6 +222,7 @@ async function main() { extensions, checkpoint, procSignalSentinel, + phase, ); assert.deepEqual(extensionProof.activated, state.extensionProof.activated); assert.deepEqual(extensionProof.catalog, state.extensionProof.catalog); @@ -344,6 +299,7 @@ async function main() { extensions, checkpoint, procSignalSentinel, + phase, ); await checkpoint("capabilities-before"); diff --git a/tools/release/js-exact-candidate-consumer.mjs b/tools/release/js-exact-candidate-consumer.mjs index 19c7bf41c..1b6260fed 100644 --- a/tools/release/js-exact-candidate-consumer.mjs +++ b/tools/release/js-exact-candidate-consumer.mjs @@ -72,6 +72,7 @@ export const WINDOWS_STANDARD_USER_CONTROL_READ_FILES = Object.freeze([ "tools/release/rust-build-script-sha256.mjs", "src/sdks/js/src/native/extension-contract.ts", "tools/release/fixtures/js-exact-candidate-runtime.mjs", + "tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs", "tools/release/fixtures/js-exact-candidate-procsignal.mjs", "tools/release/fixtures/js-exact-candidate-prepare-deno-runtime.mjs", "tools/release/fixtures/js-exact-candidate-jsr.mjs", @@ -81,6 +82,10 @@ export const WINDOWS_STANDARD_USER_CONTROL_READ_FILES = Object.freeze([ ]); const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const RUNTIME_FIXTURE = path.join(ROOT, "tools/release/fixtures/js-exact-candidate-runtime.mjs"); +const EXTENSION_SCENARIOS_FIXTURE = path.join( + ROOT, + "tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs", +); const PROCSIGNAL_FIXTURE = path.join( ROOT, "tools/release/fixtures/js-exact-candidate-procsignal.mjs", @@ -2884,6 +2889,18 @@ export function exactCandidateJsrPortableCommand(fixture) { }; } +export function stageExactCandidateRuntimeFixtures(consumerRoot) { + const runtimeFixture = path.join(consumerRoot, "exact-candidate-runtime.mjs"); + for (const [source, name] of [ + [RUNTIME_FIXTURE, "exact-candidate-runtime.mjs"], + [EXTENSION_SCENARIOS_FIXTURE, "js-exact-candidate-extension-scenarios.mjs"], + [PROCSIGNAL_FIXTURE, "js-exact-candidate-procsignal.mjs"], + ]) { + copyFileSync(source, path.join(consumerRoot, name)); + } + return runtimeFixture; +} + export function validateExactCandidateDenoPreparationReceipt(receipt, extensionCount, candidate) { if ( receipt?.schemaVersion !== 1 @@ -3273,12 +3290,7 @@ function main(argv) { expectedPackages: contract.packages, }); - const runtimeFixture = path.join(consumerRoot, "exact-candidate-runtime.mjs"); - copyFileSync(RUNTIME_FIXTURE, runtimeFixture); - copyFileSync( - PROCSIGNAL_FIXTURE, - path.join(consumerRoot, "js-exact-candidate-procsignal.mjs"), - ); + const runtimeFixture = stageExactCandidateRuntimeFixtures(consumerRoot); const extensionContractPath = path.join(consumerRoot, "exact-extension-contract.json"); const extensionContract = { schemaVersion: 1, diff --git a/tools/release/js-exact-candidate-consumer.test.mjs b/tools/release/js-exact-candidate-consumer.test.mjs index 8e37f2458..59f2e29c6 100644 --- a/tools/release/js-exact-candidate-consumer.test.mjs +++ b/tools/release/js-exact-candidate-consumer.test.mjs @@ -6,6 +6,7 @@ import { mkdirSync, mkdtempSync, readFileSync, + readdirSync, renameSync, rmSync, symlinkSync, @@ -47,6 +48,7 @@ import { removeExactCandidateRunRoot, runExactCandidateCommandToFileWithTimeout, runExactCandidateCommandWithTimeout, + stageExactCandidateRuntimeFixtures, validateExactCandidateDenoPreparationReceipt, validateIosExtensionCandidateInputs, stopVerdaccio, @@ -75,32 +77,109 @@ import { renderPlanWithSelection, selectedExtensionProductsForPlan, } from "../graph/ci_plan.mjs"; +import { + verifyCoreEnglishTextSearch, + verifyPgTextsearchEnglishBm25, +} from "./fixtures/js-exact-candidate-extension-scenarios.mjs"; const scratch = []; const RELEASE_GRAPH_TIMEOUT = { timeout: 300_000 }; const posixTest = process.platform === "win32" ? test.skip : test; -test("exact JavaScript candidate proves pg_textsearch English Snowball indexing", () => { - const runtimeFixture = readFileSync( - path.join(ROOT, "tools/release/fixtures/js-exact-candidate-runtime.mjs"), - "utf8", +function queryResult(value, column) { + return { + rows: [{}], + getText(row, requestedColumn) { + expect(row).toBe(0); + expect(requestedColumn).toBe(column); + return value; + }, + }; +} + +function pgTextsearchScenarioDatabase() { + const queries = []; + return { + queries, + async query(sql) { + queries.push(sql); + if (sql.includes("running database")) return queryResult("1", "id"); + if (sql.includes("updated migration target")) return queryResult("2", "id"); + if (sql.includes("merge sentinel proof")) return queryResult("1000", "id"); + if (sql.includes("post activation write marker")) return queryResult("1001", "id"); + if (sql.includes("post restore write marker")) return queryResult("1001", "id"); + if (sql.includes("deleted wildlife marker")) return queryResult("4", "id"); + return { rows: [] }; + }, + }; +} + +test("exact JavaScript candidate preserves pg_textsearch mutations through restore", async () => { + const source = pgTextsearchScenarioDatabase(); + const sourceProof = await verifyPgTextsearchEnglishBm25(source, "produce"); + expect(sourceProof).toEqual({ + sqlName: "pg_textsearch", + scenario: "bm25-mutation-merge-persistence", + existingTopId: "1", + updatedTopId: "2", + sentinelTopId: "1000", + postOpenWriteTopId: "1001", + deletionSurvivorTopId: "4", + }); + expect(source.queries.some((sql) => sql.startsWith("UPDATE "))).toBe(true); + expect(source.queries.some((sql) => sql.startsWith("DELETE FROM "))).toBe(true); + expect(source.queries).toContain( + "SELECT bm25_force_merge('exact_candidate_pg_textsearch_english_bm25')", + ); + + const restored = pgTextsearchScenarioDatabase(); + const restoredProof = await verifyPgTextsearchEnglishBm25(restored, "verify-restored"); + expect(restoredProof).toEqual(sourceProof); + expect(restored.queries.some((sql) => sql.startsWith("CREATE TABLE"))).toBe(false); + expect(restored.queries.some((sql) => sql.includes("SET body = 'post restore write marker'"))).toBe(true); + expect(restored.queries).toContain( + "SELECT bm25_force_merge('exact_candidate_pg_textsearch_english_bm25')", ); - expect(runtimeFixture).toContain("verifyPgTextsearchEnglishBm25"); - expect(runtimeFixture).toContain("PostgreSQL databases support reliable runners"); - expect(runtimeFixture).toContain("WITH (text_config = 'pg_catalog.english')"); - expect(runtimeFixture).toContain("nonempty-english-bm25-create-and-query"); }); -test("every exact JavaScript runtime mode proves core English Snowball text search", () => { - const runtimeFixture = readFileSync( - path.join(ROOT, "tools/release/fixtures/js-exact-candidate-runtime.mjs"), - "utf8", +test("every exact JavaScript runtime mode proves core English Snowball text search", async () => { + const queries = []; + await verifyCoreEnglishTextSearch({ + async query(sql) { + queries.push(sql); + return queryResult("english-snowball-ok", "value"); + }, + }); + expect(queries).toEqual([ + "SELECT CASE WHEN to_tsvector('pg_catalog.english', 'the quick foxes running') " + + "@@ to_tsquery('pg_catalog.english', 'run & fox') " + + "THEN 'english-snowball-ok' ELSE 'english-snowball-failed' END AS value", + ]); +}); + +test("stages the complete isolated exact-candidate runtime fixture closure", () => { + const root = mkdtempSync(path.join(ROOT, "target/js-exact-runtime-fixtures-")); + scratch.push(root); + const runtimeFixture = stageExactCandidateRuntimeFixtures(root); + expect(runtimeFixture).toBe(path.join(root, "exact-candidate-runtime.mjs")); + expect(readdirSync(root).sort()).toEqual([ + "exact-candidate-runtime.mjs", + "js-exact-candidate-extension-scenarios.mjs", + "js-exact-candidate-procsignal.mjs", + ]); + + const runtimeSource = readFileSync(runtimeFixture, "utf8"); + const relativeImports = Array.from( + runtimeSource.matchAll(/from "[.]\/([^"\n]+)";/gu), + (match) => match[1], ); - expect(runtimeFixture).toContain("verifyCoreEnglishTextSearch"); - expect(runtimeFixture).toContain("to_tsvector('pg_catalog.english', 'the quick foxes running')"); - expect(runtimeFixture).toContain("to_tsquery('pg_catalog.english', 'run & fox')"); - expect(runtimeFixture).toContain("english-snowball-ok"); - expect(runtimeFixture.match(/await verifyCoreEnglishTextSearch\(/gu)).toHaveLength(2); + expect(relativeImports.sort()).toEqual([ + "js-exact-candidate-extension-scenarios.mjs", + "js-exact-candidate-procsignal.mjs", + ]); + for (const imported of relativeImports) { + expect(existsSync(path.join(root, imported))).toBe(true); + } }); afterEach(() => { diff --git a/tools/release/js-exact-candidate-procsignal.test.mjs b/tools/release/js-exact-candidate-procsignal.test.mjs index d0b0128db..616e6fe7b 100644 --- a/tools/release/js-exact-candidate-procsignal.test.mjs +++ b/tools/release/js-exact-candidate-procsignal.test.mjs @@ -1,15 +1,14 @@ import { expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; -import { fileURLToPath } from "node:url"; import { installNativeDirectProcSignalSentinel, verifyNativeDirectProcSignalSurvival, withNativeDirectExtensionSignalIsolation, } from "./fixtures/js-exact-candidate-procsignal.mjs"; - -const RELEASE_ROOT = path.dirname(fileURLToPath(import.meta.url)); +import { stageExactCandidateRuntimeFixtures } from "./js-exact-candidate-consumer.mjs"; function result(value) { return { @@ -267,34 +266,28 @@ test("removes the host listener when installation evidence cannot be recorded", }); test("stages the ProcSignal helper beside the copied exact-candidate runtime", () => { - const runtimeSource = readFileSync( - path.join(RELEASE_ROOT, "fixtures/js-exact-candidate-runtime.mjs"), - "utf8", - ); - const consumerSource = readFileSync( - path.join(RELEASE_ROOT, "js-exact-candidate-consumer.mjs"), - "utf8", - ); + const root = mkdtempSync(path.join(tmpdir(), "oliphaunt-exact-candidate-procsignal-")); + const runtimeFixture = stageExactCandidateRuntimeFixtures(root); + const runtimeSource = readFileSync(runtimeFixture, "utf8"); - expect(runtimeSource).toContain( - 'from "./js-exact-candidate-procsignal.mjs";', - ); - const installs = Array.from( - runtimeSource.matchAll(/installNativeDirectProcSignalSentinel\(/gu), - (match) => match.index, - ); - const opens = Array.from( - runtimeSource.matchAll(/await Oliphaunt\.open\(/gu), - (match) => match.index, - ); - expect(installs).toHaveLength(2); - expect(opens).toHaveLength(2); - expect(installs[0]).toBeLessThan(opens[0]); - expect(installs[1]).toBeLessThan(opens[1]); - expect(consumerSource).toContain( - '"tools/release/fixtures/js-exact-candidate-procsignal.mjs"', - ); - expect(consumerSource).toContain( - 'path.join(consumerRoot, "js-exact-candidate-procsignal.mjs")', - ); + try { + expect(runtimeSource).toContain( + 'from "./js-exact-candidate-procsignal.mjs";', + ); + expect(existsSync(path.join(root, "js-exact-candidate-procsignal.mjs"))).toBe(true); + const installs = Array.from( + runtimeSource.matchAll(/installNativeDirectProcSignalSentinel\(/gu), + (match) => match.index, + ); + const opens = Array.from( + runtimeSource.matchAll(/await Oliphaunt\.open\(/gu), + (match) => match.index, + ); + expect(installs).toHaveLength(2); + expect(opens).toHaveLength(2); + expect(installs[0]).toBeLessThan(opens[0]); + expect(installs[1]).toBeLessThan(opens[1]); + } finally { + rmSync(root, { recursive: true, force: true }); + } }); diff --git a/tools/release/moon.yml b/tools/release/moon.yml index 1f226f356..8e484297c 100644 --- a/tools/release/moon.yml +++ b/tools/release/moon.yml @@ -66,6 +66,7 @@ tasks: - "/src/sdks/js/src/native/extension-contract.ts" - "/tools/release/run-windows-standard-user-exact-candidate.ps1" - "/tools/release/fixtures/js-exact-candidate-runtime.mjs" + - "/tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs" - "/tools/release/fixtures/js-exact-candidate-procsignal.mjs" - "/tools/release/fixtures/js-exact-candidate-prepare-deno-runtime.mjs" - "/tools/release/fixtures/js-exact-candidate-jsr.mjs" diff --git a/tools/release/native-extension-qualification.mjs b/tools/release/native-extension-qualification.mjs new file mode 100644 index 000000000..1abb3d3d5 --- /dev/null +++ b/tools/release/native-extension-qualification.mjs @@ -0,0 +1,365 @@ +#!/usr/bin/env bun + +import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const TOOL = "native-extension-qualification.mjs"; +const DEFAULT_ROOT = path.resolve(import.meta.dir, "../.."); +const UPGRADE_SCHEMA = "oliphaunt-extension-upgrade-qualification-v1"; +const UPSTREAM_SCHEMA = "oliphaunt-extension-upstream-tests-v1"; +const FULL_GIT_SHA = /^[0-9a-f]{40}$/u; +const SAFE_NAME = /^[a-z][a-z0-9_-]*$/u; +const SAFE_LOCALE = /^[A-Za-z0-9._@-]+$/u; +const SAFE_TARGET = /^[A-Za-z0-9._-]+$/u; +const SAFE_VERSION = /^[0-9A-Za-z][0-9A-Za-z._+-]*$/u; + +function fail(message) { + throw new Error(`${TOOL}: ${message}`); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function relative(root, file) { + return path.relative(root, file).split(path.sep).join("/"); +} + +function readRegularText(root, file) { + const label = relative(root, file); + let stat; + try { + stat = lstatSync(file); + } catch (cause) { + fail(`${label} cannot be inspected: ${cause.message}`); + } + if (!stat.isFile() || stat.isSymbolicLink()) { + fail(`${label} must be a regular non-symlink file`); + } + return readFileSync(file, "utf8"); +} + +function readToml(root, file) { + try { + return Bun.TOML.parse(readRegularText(root, file)); + } catch (cause) { + fail(`${relative(root, file)} is not valid TOML: ${cause.message}`); + } +} + +function nonEmptyString(value, label, pattern = undefined) { + if (typeof value !== "string" || value.length === 0 || value.trim() !== value) { + fail(`${label} must be a non-empty trimmed string`); + } + if (pattern !== undefined && !pattern.test(value)) { + fail(`${label} has invalid value ${JSON.stringify(value)}`); + } + return value; +} + +function exactStringList(value, label, { allowEmpty = false, pattern = undefined } = {}) { + if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) { + fail(`${label} must be ${allowEmpty ? "a" : "a non-empty"} string list`); + } + const items = value.map((item, index) => nonEmptyString(item, `${label}[${index}]`, pattern)); + const canonical = [...new Set(items)].sort(compareText); + if (JSON.stringify(items) !== JSON.stringify(canonical)) { + fail(`${label} must be sorted and unique`); + } + return items; +} + +function repositoryRelativeFile(root, extensionRoot, value, label) { + const relativeFile = nonEmptyString(value, label); + if (path.isAbsolute(relativeFile) || relativeFile.includes("\\")) { + fail(`${label} must be a repository-relative POSIX path`); + } + const file = path.resolve(extensionRoot, relativeFile); + const extensionPrefix = `${path.resolve(extensionRoot)}${path.sep}`; + if (!file.startsWith(extensionPrefix)) { + fail(`${label} must remain beneath ${relative(root, extensionRoot)}`); + } + readRegularText(root, file); + return relative(root, file); +} + +function safeRelativePath(value, label) { + const relativePath = nonEmptyString(value, label); + if ( + path.isAbsolute(relativePath) + || relativePath.includes("\\") + || path.posix.normalize(relativePath) !== relativePath + || relativePath === ".." + || relativePath.startsWith("../") + ) { + fail(`${label} must be a normalized relative POSIX path`); + } + return relativePath; +} + +function extensionIdentity(root, extensionRoot) { + const file = path.join(extensionRoot, "source.toml"); + const source = readToml(root, file); + const control = source["extension-control"]; + if (control === null || typeof control !== "object" || Array.isArray(control)) { + fail(`${relative(root, file)} must define [extension-control]`); + } + return Object.freeze({ + sqlName: nonEmptyString(control["sql-name"], `${relative(root, file)} extension-control.sql-name`, SAFE_NAME), + sourceName: nonEmptyString(source.name, `${relative(root, file)} name`, SAFE_NAME), + sourceCommit: nonEmptyString(source.commit, `${relative(root, file)} commit`, FULL_GIT_SHA), + }); +} + +function selectedNames(value) { + const names = typeof value === "string" ? value.split(",").filter(Boolean) : value; + if (!Array.isArray(names)) fail("selectedSqlNames must be a CSV string or string list"); + const selected = new Set(); + for (const [index, name] of names.entries()) { + selected.add(nonEmptyString(name, `selectedSqlNames[${index}]`, SAFE_NAME)); + } + return selected; +} + +function upgradePlanRow(root, extensionRoot, identity, target) { + const manifestFile = path.join(extensionRoot, "tests/upgrade/source.toml"); + if (!existsSync(manifestFile)) return undefined; + const source = readToml(root, manifestFile); + const qualification = source.qualification; + if (qualification === null || typeof qualification !== "object" || Array.isArray(qualification)) { + fail(`${relative(root, manifestFile)} must define [qualification]`); + } + if (qualification.schema !== UPGRADE_SCHEMA) { + fail(`${relative(root, manifestFile)} qualification.schema must be ${UPGRADE_SCHEMA}`); + } + const control = source["extension-control"]; + if (control === null || typeof control !== "object" || Array.isArray(control)) { + fail(`${relative(root, manifestFile)} must define [extension-control]`); + } + const sqlName = nonEmptyString( + control["sql-name"], + `${relative(root, manifestFile)} extension-control.sql-name`, + SAFE_NAME, + ); + if (sqlName !== identity.sqlName) { + fail(`${relative(root, manifestFile)} extension-control.sql-name must equal ${identity.sqlName}`); + } + const targets = exactStringList( + qualification.targets, + `${relative(root, manifestFile)} qualification.targets`, + { pattern: SAFE_TARGET }, + ); + if (!targets.includes(target)) return undefined; + const fromVersion = nonEmptyString( + control["default-version"], + `${relative(root, manifestFile)} extension-control.default-version`, + SAFE_VERSION, + ); + const sourceControlPath = safeRelativePath( + control["source-path"], + `${relative(root, manifestFile)} extension-control.source-path`, + ); + const sourceName = nonEmptyString(source.name, `${relative(root, manifestFile)} name`, SAFE_NAME); + const sourceCommit = nonEmptyString(source.commit, `${relative(root, manifestFile)} commit`, FULL_GIT_SHA); + const runner = repositoryRelativeFile( + root, + extensionRoot, + qualification.runner, + `${relative(root, manifestFile)} qualification.runner`, + ); + return Object.freeze({ + kind: "upgrade", + sqlName, + target, + runner, + sourceName, + sourceCommit, + sourceControlPath, + fromVersion, + manifest: relative(root, manifestFile), + }); +} + +function upstreamPlanRow(root, extensionRoot, identity, target) { + const manifestFile = path.join(extensionRoot, "tests/upstream.toml"); + if (!existsSync(manifestFile)) return undefined; + const manifest = readToml(root, manifestFile); + if (manifest.schema !== UPSTREAM_SCHEMA) { + fail(`${relative(root, manifestFile)} schema must be ${UPSTREAM_SCHEMA}`); + } + const status = nonEmptyString(manifest.status, `${relative(root, manifestFile)} status`); + if (status !== "required") return undefined; + const targets = exactStringList(manifest.targets, `${relative(root, manifestFile)} targets`, { + pattern: SAFE_TARGET, + }); + if (!targets.includes(target)) return undefined; + const runner = nonEmptyString(manifest.runner, `${relative(root, manifestFile)} runner`, SAFE_NAME); + if (runner !== "pgxs-installcheck") { + fail(`${relative(root, manifestFile)} required runner ${runner} is unsupported`); + } + const includedSuites = exactStringList( + manifest.included_suites, + `${relative(root, manifestFile)} included_suites`, + { pattern: SAFE_NAME }, + ); + const excludedSuites = exactStringList( + manifest.excluded_suites, + `${relative(root, manifestFile)} excluded_suites`, + { allowEmpty: true, pattern: SAFE_NAME }, + ); + const suiteTargetPrefix = nonEmptyString( + manifest.suite_target_prefix, + `${relative(root, manifestFile)} suite_target_prefix`, + SAFE_NAME, + ); + const aggregateSuites = exactStringList( + manifest.aggregate_suites, + `${relative(root, manifestFile)} aggregate_suites`, + { pattern: SAFE_NAME }, + ); + const declaredMakeSuites = [...aggregateSuites, ...excludedSuites]; + if (declaredMakeSuites.some((suite) => !suite.startsWith(suiteTargetPrefix))) { + fail(`${relative(root, manifestFile)} aggregate/excluded suites must start with ${suiteTargetPrefix}`); + } + if (new Set(declaredMakeSuites).size !== declaredMakeSuites.length) { + fail(`${relative(root, manifestFile)} aggregate_suites and excluded_suites must not overlap`); + } + const preloadLibraries = exactStringList( + manifest.shared_preload_libraries ?? [], + `${relative(root, manifestFile)} shared_preload_libraries`, + { allowEmpty: true, pattern: SAFE_NAME }, + ); + const locale = nonEmptyString(manifest.locale, `${relative(root, manifestFile)} locale`, SAFE_LOCALE); + return Object.freeze({ + kind: "upstream", + sqlName: identity.sqlName, + target, + runner, + sourceName: identity.sourceName, + sourceCommit: identity.sourceCommit, + locale, + includedSuites: Object.freeze(includedSuites), + suiteTargetPrefix, + aggregateSuites: Object.freeze(aggregateSuites), + excludedSuites: Object.freeze(excludedSuites), + preloadLibraries: Object.freeze(preloadLibraries), + manifest: relative(root, manifestFile), + }); +} + +export function nativeExtensionQualificationPlan({ + root = DEFAULT_ROOT, + target, + selectedSqlNames, +} = {}) { + const checkedTarget = nonEmptyString(target, "target", SAFE_TARGET); + const selected = selectedNames(selectedSqlNames); + if (selected.size === 0) return []; + const externalRoot = path.join(root, "src/extensions/external"); + const rows = []; + for (const entry of readdirSync(externalRoot, { withFileTypes: true }) + .filter((candidate) => candidate.isDirectory()) + .sort((left, right) => compareText(left.name, right.name))) { + const extensionRoot = path.join(externalRoot, entry.name); + if (!existsSync(path.join(extensionRoot, "source.toml"))) continue; + const identity = extensionIdentity(root, extensionRoot); + if (!selected.has(identity.sqlName)) continue; + const upgrade = upgradePlanRow(root, extensionRoot, identity, checkedTarget); + const upstream = upstreamPlanRow(root, extensionRoot, identity, checkedTarget); + if (upgrade !== undefined) rows.push(upgrade); + if (upstream !== undefined) rows.push(upstream); + } + return Object.freeze(rows.sort((left, right) => + compareText(`${left.sqlName}\0${left.kind}`, `${right.sqlName}\0${right.kind}`))); +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (command !== "plan" && command !== "run") { + fail("usage: native-extension-qualification.mjs --target TARGET --selected-sql-names CSV [--runtime DIR] [--format json|count]"); + } + const options = { command, format: "json" }; + for (let index = 0; index < rest.length; index += 2) { + const flag = rest[index]; + const value = rest[index + 1]; + if (value === undefined) fail(`${flag} requires a value`); + if (flag === "--target") options.target = value; + else if (flag === "--selected-sql-names") options.selectedSqlNames = value; + else if (flag === "--runtime") options.runtime = value; + else if (flag === "--format") options.format = value; + else fail(`unknown argument ${flag}`); + } + if (options.format !== "json" && options.format !== "count") { + fail("--format must be json or count"); + } + if (command === "run" && options.runtime === undefined) fail("run requires --runtime"); + return options; +} + +function runProcess(command, args, { cwd, env, label }) { + process.stdout.write(`==> ${label}\n`); + const result = spawnSync(command, args, { cwd, env, stdio: "inherit" }); + if (result.error !== undefined) fail(`${label}: ${result.error.message}`); + if (result.status !== 0) fail(`${label} failed with exit code ${result.status ?? "unknown"}`); +} + +function runPlan(root, rows, runtime) { + const runtimeRoot = path.resolve(runtime); + const runtimeStat = lstatSync(runtimeRoot); + if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()) { + fail(`runtime must be a real directory: ${runtime}`); + } + for (const row of rows) { + const commonEnv = { + ...process.env, + OLIPHAUNT_EXTENSION_CURRENT_RUNTIME: runtimeRoot, + OLIPHAUNT_EXTENSION_SQL_NAME: row.sqlName, + OLIPHAUNT_EXTENSION_SOURCE_NAME: row.sourceName, + OLIPHAUNT_EXTENSION_SOURCE_COMMIT: row.sourceCommit, + }; + if (row.kind === "upgrade") { + runProcess("bash", [path.join(root, row.runner)], { + cwd: root, + env: { + ...commonEnv, + OLIPHAUNT_EXTENSION_UPGRADE_FROM_VERSION: row.fromVersion, + OLIPHAUNT_EXTENSION_SOURCE_CONTROL_PATH: row.sourceControlPath, + }, + label: `${row.sqlName} ${row.fromVersion} pinned-version upgrade`, + }); + continue; + } + runProcess("bash", [path.join(root, "src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh")], { + cwd: root, + env: { + ...commonEnv, + OLIPHAUNT_EXTENSION_INCLUDED_SUITES: row.includedSuites.join(","), + OLIPHAUNT_EXTENSION_SUITE_TARGET_PREFIX: row.suiteTargetPrefix, + OLIPHAUNT_EXTENSION_AGGREGATE_SUITES: row.aggregateSuites.join(","), + OLIPHAUNT_EXTENSION_EXCLUDED_SUITES: row.excludedSuites.join(","), + OLIPHAUNT_EXTENSION_TEST_LOCALE: row.locale, + OLIPHAUNT_EXTENSION_SHARED_PRELOAD_LIBRARIES: row.preloadLibraries.join(","), + }, + label: `${row.sqlName} exact-candidate upstream PGXS installcheck`, + }); + } +} + +function main(argv) { + const options = parseArgs(argv); + const rows = nativeExtensionQualificationPlan(options); + if (options.command === "run") runPlan(DEFAULT_ROOT, rows, options.runtime); + if (options.format === "count") process.stdout.write(`${rows.length}\n`); + else process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + try { + main(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); + } +} diff --git a/tools/release/pg-textsearch-upgrade-qualification.test.mjs b/tools/release/pg-textsearch-upgrade-qualification.test.mjs new file mode 100644 index 000000000..53ba0363a --- /dev/null +++ b/tools/release/pg-textsearch-upgrade-qualification.test.mjs @@ -0,0 +1,174 @@ +#!/usr/bin/env bun + +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { nativeExtensionQualificationPlan } from "./native-extension-qualification.mjs"; + +function writeFixture(root, relative, contents) { + const file = path.join(root, relative); + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, contents.trimStart()); +} + +function qualificationFixture(t, { runner = "tests/upgrade.sh", sqlName = "fixture_search" } = {}) { + const root = mkdtempSync(path.join(tmpdir(), "oliphaunt-native-extension-qualification-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + writeFixture(root, "src/extensions/external/fixture_search/source.toml", ` +name = "fixture_search" +commit = "1111111111111111111111111111111111111111" + +[extension-control] +sql-name = "fixture_search" +default-version = "2.0.0" +`); + writeFixture(root, "src/extensions/external/fixture_search/tests/upgrade.sh", "#!/usr/bin/env bash\nexit 0\n"); + writeFixture(root, "src/extensions/external/fixture_search/tests/upgrade/source.toml", ` +name = "fixture_search_upgrade_1_0_0" +url = "https://example.invalid/fixture-search.git" +branch = "v1.0.0" +commit = "2222222222222222222222222222222222222222" + +[extension-control] +sql-name = "${sqlName}" +source-path = "fixture_search.control" +default-version = "1.0.0" + +[qualification] +schema = "oliphaunt-extension-upgrade-qualification-v1" +targets = ["linux-x64-gnu"] +runner = "${runner}" +`); + writeFixture(root, "src/extensions/external/fixture_search/tests/upstream.toml", ` +schema = "oliphaunt-extension-upstream-tests-v1" +runner = "pgxs-installcheck" +status = "required" +reason = "The complete PGXS regression suite is required for this fixture." +targets = ["linux-x64-gnu"] +locale = "C.UTF-8" +included_suites = ["regress"] +suite_target_prefix = "test-" +aggregate_suites = ["test-all", "test-shell"] +excluded_suites = ["test-replication"] +shared_preload_libraries = ["fixture_search"] +`); + return root; +} + +test("the live plan derives the complete pg_textsearch Linux qualification", () => { + const rows = nativeExtensionQualificationPlan({ + target: "linux-x64-gnu", + selectedSqlNames: "pg_textsearch", + }); + assert.deepEqual(rows, [ + { + kind: "upgrade", + sqlName: "pg_textsearch", + target: "linux-x64-gnu", + runner: "src/extensions/external/pg_textsearch/tests/upgrade.sh", + sourceName: "pg_textsearch_upgrade_from", + sourceCommit: "07936f7cd67f7a183659d3acd459c0a5efc93756", + sourceControlPath: "pg_textsearch.control", + fromVersion: "0.6.1", + manifest: "src/extensions/external/pg_textsearch/tests/upgrade/source.toml", + }, + { + kind: "upstream", + sqlName: "pg_textsearch", + target: "linux-x64-gnu", + runner: "pgxs-installcheck", + sourceName: "pg_textsearch", + sourceCommit: "578ff529894992fb9e67cae4c69424e65c84868e", + locale: "C.UTF-8", + includedSuites: ["regress"], + suiteTargetPrefix: "test-", + aggregateSuites: ["test-all", "test-local", "test-shell"], + excludedSuites: [ + "test-cic", + "test-concurrency", + "test-logical-replication", + "test-multi-index", + "test-recovery", + "test-reindex", + "test-replication", + "test-replication-extended", + "test-segment", + "test-stress", + ], + preloadLibraries: ["pg_textsearch"], + manifest: "src/extensions/external/pg_textsearch/tests/upstream.toml", + }, + ]); +}); + +test("the live plan is selected by extension and declared target", () => { + assert.deepEqual(nativeExtensionQualificationPlan({ + target: "windows-x64-msvc", + selectedSqlNames: "pg_textsearch", + }), []); + assert.deepEqual(nativeExtensionQualificationPlan({ + target: "linux-x64-gnu", + selectedSqlNames: "vector", + }), []); +}); + +test("the planner parses generic upgrade and upstream manifests", (t) => { + const root = qualificationFixture(t); + assert.deepEqual(nativeExtensionQualificationPlan({ + root, + target: "linux-x64-gnu", + selectedSqlNames: ["fixture_search"], + }), [ + { + kind: "upgrade", + sqlName: "fixture_search", + target: "linux-x64-gnu", + runner: "src/extensions/external/fixture_search/tests/upgrade.sh", + sourceName: "fixture_search_upgrade_1_0_0", + sourceCommit: "2222222222222222222222222222222222222222", + sourceControlPath: "fixture_search.control", + fromVersion: "1.0.0", + manifest: "src/extensions/external/fixture_search/tests/upgrade/source.toml", + }, + { + kind: "upstream", + sqlName: "fixture_search", + target: "linux-x64-gnu", + runner: "pgxs-installcheck", + sourceName: "fixture_search", + sourceCommit: "1111111111111111111111111111111111111111", + locale: "C.UTF-8", + includedSuites: ["regress"], + suiteTargetPrefix: "test-", + aggregateSuites: ["test-all", "test-shell"], + excludedSuites: ["test-replication"], + preloadLibraries: ["fixture_search"], + manifest: "src/extensions/external/fixture_search/tests/upstream.toml", + }, + ]); +}); + +test("the planner rejects transition identity drift and escaping runners", (t) => { + const identityRoot = qualificationFixture(t, { sqlName: "another_extension" }); + assert.throws( + () => nativeExtensionQualificationPlan({ + root: identityRoot, + target: "linux-x64-gnu", + selectedSqlNames: "fixture_search", + }), + /extension-control[.]sql-name must equal fixture_search/u, + ); + + const runnerRoot = qualificationFixture(t, { runner: "../../outside.sh" }); + assert.throws( + () => nativeExtensionQualificationPlan({ + root: runnerRoot, + target: "linux-x64-gnu", + selectedSqlNames: "fixture_search", + }), + /qualification[.]runner must remain beneath/u, + ); +}); diff --git a/tools/release/pg-textsearch-windows-producer.test.mjs b/tools/release/pg-textsearch-windows-producer.test.mjs new file mode 100644 index 000000000..0401fdf9a --- /dev/null +++ b/tools/release/pg-textsearch-windows-producer.test.mjs @@ -0,0 +1,287 @@ +#!/usr/bin/env bun +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { spawnSync } from "../test/fd-backed-spawn-sync.mjs"; +import { + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + realpathSync, + rmSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dir, "../.."); +const productRoot = path.join(root, "src/extensions/external/pg_textsearch"); +const recipePath = path.join(productRoot, "patches/windows-msvc/recipe.json"); +const sourceManifestPath = path.join(productRoot, "source.toml"); +const checkout = path.join(root, "target/oliphaunt-sources/checkouts/pg_textsearch"); +const producerPath = path.join( + root, + "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", +); +const recipe = JSON.parse(readFileSync(recipePath, "utf8")); +const sourceManifest = Bun.TOML.parse(readFileSync(sourceManifestPath, "utf8")); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + ...options, + }); + assert.equal( + result.status, + 0, + `${command} ${args.join(" ")} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, + ); + return result.stdout.trim(); +} + +function sha256(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function contained(rootPath, relativePath, label) { + assert.equal(typeof relativePath, "string", `${label} must be a string`); + assert.match(relativePath, /^[A-Za-z0-9_./-]+$/u, `${label} alphabet`); + assert.equal(relativePath.includes("\\"), false, `${label} must use forward slashes`); + assert.equal(path.posix.isAbsolute(relativePath), false, `${label} POSIX root`); + assert.equal(path.win32.isAbsolute(relativePath), false, `${label} Windows root`); + const segments = relativePath.split("/"); + assert.equal( + segments.some((segment) => segment === "" || segment === "." || segment === ".."), + false, + `${label} traversal segment`, + ); + const resolvedRoot = path.resolve(rootPath); + const resolved = path.resolve(resolvedRoot, ...segments); + assert.equal( + resolved.startsWith(`${resolvedRoot}${path.sep}`), + true, + `${label} must remain checkout-contained`, + ); + return resolved; +} + +function occurrences(text, literal) { + return text.split(literal).length - 1; +} + +test("pg_textsearch owns a content-addressed exact-pin Windows recipe", () => { + assert.deepEqual(Object.keys(recipe).sort(), [ + "compiler_arguments", + "data_files", + "default_version", + "export_contracts", + "force_include_files", + "layout_contracts", + "local_include_directories", + "patches", + "schema", + "source_commit", + "sources", + "sql_name", + "version_defines", + ]); + assert.equal(recipe.schema, "oliphaunt-external-pgxs-windows-recipe-v1"); + assert.equal(recipe.sql_name, "pg_textsearch"); + assert.equal(recipe.source_commit, sourceManifest.commit); + assert.equal(recipe.default_version, sourceManifest["extension-control"]["default-version"]); + assert.match(recipe.source_commit, /^[0-9a-f]{40}$/u); + assert.ok(recipe.sources.length > 0); + assert.ok(recipe.data_files.length > 0); + assert.ok(recipe.layout_contracts.length > 0); + assert.ok(recipe.export_contracts.length > 0); + assert.deepEqual( + Object.fromEntries( + recipe.layout_contracts + .map((contract) => [contract.type, [contract.size, contract.alignment]]) + .sort(([left], [right]) => left.localeCompare(right)), + ), + { + TpCtidMapEntry: [6, 1], + TpDictEntry: [16, 8], + TpDictEntryV3: [12, 4], + TpExpullEntry: [7, 1], + TpSegmentPosting: [14, 1], + TpSkipEntry: [20, 1], + TpSkipEntryV3: [16, 1], + }, + ); + + for (const [property, base] of [ + ["sources", checkout], + ["data_files", checkout], + ["local_include_directories", checkout], + ["force_include_files", checkout], + ]) { + assert.equal(new Set(recipe[property]).size, recipe[property].length, `${property} uniqueness`); + for (const entry of recipe[property]) contained(base, entry, `${property} entry`); + } + for (const patch of recipe.patches) { + assert.deepEqual(Object.keys(patch).sort(), ["path", "sha256"]); + const patchPath = contained(productRoot, patch.path, "patch path"); + assert.equal(sha256(patchPath), patch.sha256, `${patch.path} digest`); + } + for (const contract of recipe.layout_contracts) { + assert.deepEqual(Object.keys(contract).sort(), ["alignment", "path", "size", "type"]); + contained(checkout, contract.path, "layout contract path"); + } + for (const contract of recipe.export_contracts) { + assert.deepEqual(Object.keys(contract).sort(), ["path", "symbols"]); + contained(checkout, contract.path, "contract path"); + } + for (const hostile of ["../outside.c", "/rooted.c", "C:\\rooted.c", "src//empty.c", "src/./dot.c"]) { + assert.throws(() => contained(checkout, hostile, "hostile fixture")); + } +}); + +test("shared producer consumes the generic recipe without pg_textsearch source rewriting", () => { + const producer = readFileSync(producerPath, "utf8"); + assert.match(producer, /function Apply-ExternalPgxsWindowsRecipe\(/u); + assert.match(producer, /git -C \$ExtensionDir apply --check --whitespace=error-all/u); + assert.match( + producer, + /\$previousGitCeiling = \[Environment\]::GetEnvironmentVariable\(\s*"GIT_CEILING_DIRECTORIES",\s*\[EnvironmentVariableTarget\]::Process\s*\)/u, + ); + assert.match(producer, /"GIT_CEILING_DIRECTORIES",\s*\$patchCeiling,\s*\[EnvironmentVariableTarget\]::Process/u); + assert.match( + producer, + /finally\s*\{\s*\[Environment\]::SetEnvironmentVariable\(\s*"GIT_CEILING_DIRECTORIES",\s*\$previousGitCeiling,\s*\[EnvironmentVariableTarget\]::Process\s*\)\s*\}/u, + ); + assert.match(producer, /external-windows-input:/u); + assert.match(producer, /Resolve-ExternalWindowsRecipePath/u); + assert.match(producer, /Add-ExternalPgxsMesonProducerFromWindowsRecipe/u); + assert.doesNotMatch(producer, /function Patch-PgTextsearchWindows/u); + assert.doesNotMatch(producer, /function Get-PgTextsearchMakefileList/u); + assert.doesNotMatch(producer, /if \(\$SqlName -eq "pg_textsearch"\)/u); + assert.doesNotMatch(producer, /Set-Content[^\n]+oliphaunt_windows_compat/u); +}); + +test( + "the real recipe applies to a clean archive of the exact checkout", + { skip: !existsSync(path.join(checkout, ".git")) && "exact pinned checkout is not materialized" }, + () => { + const checkoutCommit = run("git", ["-C", checkout, "rev-parse", "HEAD"]); + assert.equal(checkoutCommit, recipe.source_commit); + assert.equal( + run("git", ["-C", checkout, "status", "--porcelain=v1", "--untracked-files=all"]), + "", + "the canonical exact checkout must be clean before staging", + ); + + const scratch = mkdtempSync(path.join(tmpdir(), "oliphaunt-pg-textsearch-windows-")); + const archive = path.join(scratch, "source.tar"); + const staged = path.join(scratch, "source"); + mkdirSync(staged); + try { + run("git", ["-C", scratch, "init", "-q"]); + run("git", ["-C", checkout, "archive", "--format=tar", `--output=${archive}`, recipe.source_commit]); + run("tar", ["-xf", archive, "-C", staged]); + assert.equal( + realpathSync(run("git", ["-C", staged, "rev-parse", "--show-toplevel"])), + realpathSync(scratch), + "fixture must reproduce an enclosing worktree around the staged source", + ); + + const control = readFileSync(path.join(staged, "pg_textsearch.control"), "utf8"); + const version = /^\s*default_version\s*=\s*'([^']+)'\s*$/mu.exec(control)?.[1]; + assert.equal(version, recipe.default_version); + + const patchEnvironment = { ...process.env, GIT_CEILING_DIRECTORIES: scratch }; + for (const patch of recipe.patches) { + const patchPath = contained(productRoot, patch.path, "patch path"); + run( + "git", + ["-C", staged, "apply", "--check", "--whitespace=error-all", patchPath], + { env: patchEnvironment }, + ); + run( + "git", + ["-C", staged, "apply", "--whitespace=error-all", patchPath], + { env: patchEnvironment }, + ); + } + + for (const relativePath of [ + ...recipe.sources, + ...recipe.data_files, + ...recipe.force_include_files, + ]) { + assert.equal(existsSync(contained(staged, relativePath, "staged input")), true, relativePath); + } + + for (const contract of recipe.layout_contracts) { + const header = readFileSync(contained(staged, contract.path, "layout path"), "utf8"); + const declaration = `typedef struct ${contract.type}\n{`; + const closing = `} ${contract.type};`; + const push = `#pragma pack(push, ${contract.alignment})`; + const pop = "#pragma pack(pop)"; + const size = `StaticAssertDecl(sizeof(${contract.type}) == ${contract.size},`; + const alignment = `StaticAssertDecl(__alignof(${contract.type}) == ${contract.alignment},`; + for (const marker of [declaration, closing, size, alignment]) { + assert.equal(occurrences(header, marker), 1, `${contract.type} marker ${marker}`); + } + const declarationIndex = header.indexOf(declaration); + const closingIndex = header.indexOf(closing, declarationIndex); + const pushIndex = header.lastIndexOf(push, declarationIndex); + const popIndex = header.indexOf(pop, closingIndex); + assert.ok(pushIndex >= 0 && pushIndex < declarationIndex, `${contract.type} pack push`); + assert.ok(declarationIndex < closingIndex, `${contract.type} declaration`); + assert.ok(closingIndex < popIndex, `${contract.type} pack pop`); + assert.ok(popIndex < header.indexOf(size), `${contract.type} size assertion`); + assert.ok(header.indexOf(size) < header.indexOf(alignment), `${contract.type} alignment assertion`); + } + for (const headerPath of new Set(recipe.layout_contracts.map(({ path: value }) => value))) { + const header = readFileSync(contained(staged, headerPath, "layout path"), "utf8"); + assert.equal( + occurrences(header, "#pragma pack(push,"), + occurrences(header, "#pragma pack(pop)"), + `${headerPath} pack stack must remain balanced`, + ); + } + + for (const contract of recipe.export_contracts) { + const header = readFileSync(contained(staged, contract.path, "export path"), "utf8"); + for (const symbol of contract.symbols) { + const declaration = `extern PGDLLEXPORT Datum ${symbol}(PG_FUNCTION_ARGS);`; + assert.equal(occurrences(header, declaration), 1, `${symbol} export declaration`); + } + } + + const makeContract = run( + "make", + [ + "-s", + "-C", + staged, + "-f", + "Makefile", + "--eval=.PHONY: oliphaunt-print-windows-contract\noliphaunt-print-windows-contract:\n\t@printf '%s\\n' 'OBJS=$(OBJS)' 'DATA=$(DATA)'", + "oliphaunt-print-windows-contract", + ], + { env: { ...process.env, PG_CONFIG: "false" } }, + ); + const values = Object.fromEntries( + makeContract.split("\n").map((line) => { + const equals = line.indexOf("="); + return [line.slice(0, equals), line.slice(equals + 1).trim().split(/\s+/u)]; + }), + ); + assert.deepEqual( + values.OBJS.map((object) => `${object.slice(0, -2)}.c`), + recipe.sources, + "recipe sources must equal GNU make's evaluation of the pinned OBJS contract", + ); + assert.deepEqual( + [...values.DATA, "pg_textsearch.control"], + recipe.data_files, + "recipe data files must equal GNU make's evaluation of the pinned DATA contract", + ); + } finally { + rmSync(scratch, { recursive: true, force: true }); + } + }, +); diff --git a/tools/release/release-semantic-inputs.test.mjs b/tools/release/release-semantic-inputs.test.mjs index bfbc451ea..5a43eb5ac 100644 --- a/tools/release/release-semantic-inputs.test.mjs +++ b/tools/release/release-semantic-inputs.test.mjs @@ -207,6 +207,8 @@ test("real shared shipped-byte inputs have exact declarative product owners", () ], ["src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", extensionProducts], ["src/extensions/artifacts/wasix/tools/package-release-assets.mjs", extensionProducts], + ["src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", extensionProducts], + ["src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh", ["oliphaunt-extension-postgis"]], ["tools/release/bounded-gunzip-to-file.mjs", extensionProducts], ["tools/release/build-extension-ci-artifacts.mjs", extensionProducts], ["tools/release/extension-artifact-inventory.mjs", extensionProducts], @@ -233,7 +235,7 @@ test("real shared shipped-byte inputs have exact declarative product owners", () ["src/extensions/generated/extensions.catalog.json", ["liboliphaunt-wasix"]], [ "src/extensions/generated/sdk/kotlin.json", - ["liboliphaunt-wasix", ...extensionProducts], + ["liboliphaunt-wasix"], ], [ "src/extensions/generated/sdk/swift.json", @@ -393,6 +395,35 @@ test("product-local upstream license data preserves independent extension releas } }); +test("product-local generated extension metadata preserves independent extension releases", () => { + const cases = [ + [ + "src/extensions/contrib", + "oliphaunt-extension-contrib-pg18", + ["liboliphaunt-native", "liboliphaunt-wasix", "oliphaunt-extension-contrib-pg18"], + ], + ["src/extensions/external/pg_hashids", "oliphaunt-extension-pg-hashids"], + ["src/extensions/external/pg_ivm", "oliphaunt-extension-pg-ivm"], + ["src/extensions/external/pg_textsearch", "oliphaunt-extension-pg-textsearch"], + ["src/extensions/external/pg_uuidv7", "oliphaunt-extension-pg-uuidv7"], + ["src/extensions/external/pgtap", "oliphaunt-extension-pgtap"], + ["src/extensions/external/postgis", "oliphaunt-extension-postgis"], + ["src/extensions/external/vector", "oliphaunt-extension-vector"], + ]; + for (const [directory, product, releaseProducts = [product]] of cases) { + const candidate = `${directory}/.release-extension-metadata.json`; + assert.deepEqual( + releaseSemanticProductsForPath(manifest, candidate, { prefix: "release-semantic-inputs.test" }), + [], + `${candidate} is product-local and must not be duplicated in the shared semantic manifest`, + ); + const plan = buildPlan(graph, [candidate], "release-semantic-inputs.test"); + assert.deepEqual(plan.semanticInputProducts, [], candidate); + assert.deepEqual(plan.directProducts, [product], candidate); + assert.deepEqual(plan.releaseProducts, releaseProducts, candidate); + } +}); + test("the native runtime-resource Rust byte path has focused ownership", () => { const nativeProduct = "liboliphaunt-native"; const semanticOwners = (candidate) => @@ -515,6 +546,7 @@ test("extension artifact tool inventory explicitly separates byte producers from "src/extensions/artifacts/native/tools/check-release-artifacts.sh", "src/extensions/artifacts/native/tools/run-observed-phase.sh", "src/extensions/artifacts/native/tools/run-observed-phase.test.sh", + "src/extensions/artifacts/native/tools/run-pgxs-installcheck.sh", ]; const inventory = [ ...repositoryFiles("src/extensions/artifacts/native/tools"), diff --git a/tools/release/release-semantic-inputs.toml b/tools/release/release-semantic-inputs.toml index e002d0c51..c66be2be7 100644 --- a/tools/release/release-semantic-inputs.toml +++ b/tools/release/release-semantic-inputs.toml @@ -108,7 +108,7 @@ paths = [ products = ["liboliphaunt-native", "liboliphaunt-wasix"] [[rules]] -id = "native-postgis-carrier-producers" +id = "native-extension-carrier-producers" paths = [ "src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", @@ -117,8 +117,12 @@ paths = [ "src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", "src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1", - "src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh", ] +product_kinds = ["exact-extension-artifact", "exact-extension-bundle"] + +[[rules]] +id = "native-postgis-carrier-producer" +paths = ["src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh"] products = ["oliphaunt-extension-postgis"] [[rules]] @@ -219,7 +223,6 @@ products = ["liboliphaunt-wasix"] id = "extension-kotlin-runtime-catalog" paths = ["src/extensions/generated/sdk/kotlin.json"] products = ["liboliphaunt-wasix"] -product_kinds = ["exact-extension-artifact", "exact-extension-bundle"] [[rules]] id = "swift-extension-owner-catalog" diff --git a/tools/release/run-windows-standard-user-exact-candidate.ps1 b/tools/release/run-windows-standard-user-exact-candidate.ps1 index 999d1f62d..37d9de261 100644 --- a/tools/release/run-windows-standard-user-exact-candidate.ps1 +++ b/tools/release/run-windows-standard-user-exact-candidate.ps1 @@ -45,6 +45,7 @@ $RepositoryConsumerControlReadRelativePaths = @( "tools/release/rust-build-script-sha256.mjs", "src/sdks/js/src/native/extension-contract.ts", "tools/release/fixtures/js-exact-candidate-runtime.mjs", + "tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs", "tools/release/fixtures/js-exact-candidate-procsignal.mjs", "tools/release/fixtures/js-exact-candidate-prepare-deno-runtime.mjs", "tools/release/fixtures/js-exact-candidate-jsr.mjs", diff --git a/tools/release/windows-exact-candidate-command.test.mjs b/tools/release/windows-exact-candidate-command.test.mjs index 91ec854b2..289ce774c 100644 --- a/tools/release/windows-exact-candidate-command.test.mjs +++ b/tools/release/windows-exact-candidate-command.test.mjs @@ -43,6 +43,7 @@ const EXPECTED_WINDOWS_STANDARD_USER_CONTROL_READ_FILES = [ "tools/release/rust-build-script-sha256.mjs", "src/sdks/js/src/native/extension-contract.ts", "tools/release/fixtures/js-exact-candidate-runtime.mjs", + "tools/release/fixtures/js-exact-candidate-extension-scenarios.mjs", "tools/release/fixtures/js-exact-candidate-procsignal.mjs", "tools/release/fixtures/js-exact-candidate-prepare-deno-runtime.mjs", "tools/release/fixtures/js-exact-candidate-jsr.mjs", diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index f97007a86..e21267693 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -303,6 +303,7 @@ mod asset_fingerprint_tests { #[test] fn release_envelope_files_do_not_invalidate_binary_assets() { for file in [ + "src/extensions/external/vector/.release-extension-metadata.json", "src/extensions/external/vector/.release-semantic-inputs.json", "src/extensions/external/vector/CHANGELOG.md", "src/extensions/external/vector/VERSION", @@ -345,6 +346,26 @@ mod asset_fingerprint_tests { } } + #[test] + fn extension_target_specific_and_qualification_inputs_do_not_invalidate_binary_assets() { + for file in [ + "src/extensions/external/pg_textsearch/tests/upgrade.sh", + "src/extensions/external/pg_textsearch/tests/upgrade/source.toml", + "src/extensions/external/pg_textsearch/tests/upstream.toml", + "src/extensions/external/pg_textsearch/patches/windows-msvc/recipe.json", + "src/extensions/external/pg_textsearch/patches/windows-msvc/pg_textsearch-1.3.1.patch", + ] { + assert!(!is_asset_binary_semantic_input(file), "{file}"); + } + for file in [ + "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_textsearch/recipe.toml", + "src/extensions/external/pg_textsearch/patches/0001-wasix.patch", + ] { + assert!(is_asset_binary_semantic_input(file), "{file}"); + } + } + #[test] fn cargo_manifest_normalization_only_masks_the_product_version() { let manifest = "[package]\nname = \"producer\"\nversion = \"1.2.3\"\n\n[dependencies]\nserde = \"1.0\"\n"; diff --git a/tools/xtask/src/asset_fingerprint.rs b/tools/xtask/src/asset_fingerprint.rs index 21aef75a5..854b14d5a 100644 --- a/tools/xtask/src/asset_fingerprint.rs +++ b/tools/xtask/src/asset_fingerprint.rs @@ -83,6 +83,12 @@ pub(crate) fn is_asset_binary_semantic_input(file: &str) -> bool { if file.starts_with("src/sources/toolchains/") && file != "src/sources/toolchains/wasix.toml" { return false; } + if file.starts_with("src/extensions/") && file.split('/').any(|segment| segment == "tests") { + return false; + } + if file.starts_with("src/extensions/") && file.contains("/patches/windows-msvc/") { + return false; + } if file.contains("/testdata/") || file.ends_with(".test.sh") || file.ends_with(".test.mjs") { return false; } @@ -94,7 +100,8 @@ pub(crate) fn is_asset_binary_semantic_input(file: &str) -> bool { !matches!( name, - ".release-semantic-inputs.json" + ".release-extension-metadata.json" + | ".release-semantic-inputs.json" | "CHANGELOG.md" | "VERSION" | "artifacts.toml"