From 91a831367bb50b2fd52db44d47fe9b090c842f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 23 Jun 2025 11:49:15 +0200 Subject: [PATCH 1/6] feat: support multiple versions of the http extension Build multiple versions of the http extension on different PostgreSQL versions. Add test for the extensions and their upgrade on PostgreSQL 15 and 17. --- nix/ext/pgsql-http.nix | 118 ++++++++++++++++++++++++------- nix/ext/tests/http.nix | 155 +++++++++++++++++++++++++++++++++++++++++ nix/ext/versions.json | 25 +++++-- 3 files changed, 269 insertions(+), 29 deletions(-) create mode 100644 nix/ext/tests/http.nix diff --git a/nix/ext/pgsql-http.nix b/nix/ext/pgsql-http.nix index 9f4bae789..6b9394e55 100644 --- a/nix/ext/pgsql-http.nix +++ b/nix/ext/pgsql-http.nix @@ -1,39 +1,109 @@ { + pkgs, lib, stdenv, fetchFromGitHub, - curl, postgresql, + curl, }: +let + pname = "http"; -stdenv.mkDerivation rec { - pname = "pgsql-http"; - version = "1.6.1"; + # Load version configuration from external file + allVersions = (builtins.fromJSON (builtins.readFile ./versions.json)).${pname}; - buildInputs = [ - curl - postgresql - ]; + # Filter versions compatible with current PostgreSQL version + supportedVersions = lib.filterAttrs ( + _: value: builtins.elem (lib.versions.major postgresql.version) value.postgresql + ) allVersions; - src = fetchFromGitHub { - owner = "pramsey"; - repo = pname; - rev = "refs/tags/v${version}"; - hash = "sha256-C8eqi0q1dnshUAZjIsZFwa5FTYc7vmATF3vv2CReWPM="; - }; + # Derived version information + versions = lib.naturalSort (lib.attrNames supportedVersions); + latestVersion = lib.last versions; + numberOfVersions = builtins.length versions; + packages = builtins.attrValues ( + lib.mapAttrs (name: value: build name value.hash) supportedVersions + ); + + # Build function for individual versions + build = + version: hash: + stdenv.mkDerivation rec { + inherit pname version; + + buildInputs = [ + curl + postgresql + ]; + + src = fetchFromGitHub { + owner = "pramsey"; + repo = "pgsql-http"; + rev = "refs/tags/v${version}"; + inherit hash; + }; + + installPhase = '' + runHook preInstall - installPhase = '' - mkdir -p $out/{lib,share/postgresql/extension} + mkdir -p $out/{lib,share/postgresql/extension} + + # Install versioned library + install -Dm755 ${pname}${postgresql.dlSuffix} $out/lib/${pname}--${version}${postgresql.dlSuffix} + + cp ${pname}--${version}.sql $out/share/postgresql/extension/${pname}--${version}.sql + + # Create versioned control file with modified module path + sed -e "/^default_version =/d" \ + -e "s|^module_pathname = .*|module_pathname = '\$libdir/${pname}'|" \ + ${pname}.control > $out/share/postgresql/extension/${pname}--${version}.control + + # For the latest version, create default control file and symlink and copy SQL upgrade scripts + if [[ "${version}" == "${latestVersion}" ]]; then + { + echo "default_version = '${version}'" + cat $out/share/postgresql/extension/${pname}--${version}.control + } > $out/share/postgresql/extension/${pname}.control + ln -sfn ${pname}--${latestVersion}${postgresql.dlSuffix} $out/lib/${pname}${postgresql.dlSuffix} + cp *.sql $out/share/postgresql/extension + fi + + runHook postInstall + ''; + + meta = with lib; { + description = "HTTP client for Postgres"; + homepage = "https://github.com/pramsey/${pname}"; + inherit (postgresql.meta) platforms; + license = licenses.postgresql; + }; + }; +in +pkgs.buildEnv { + name = pname; + paths = packages; + + pathsToLink = [ + "/lib" + "/share/postgresql/extension" + ]; + postBuild = '' + # Verify all expected library files are present + expectedFiles=${toString (numberOfVersions + 1)} + actualFiles=$(ls -A $out/lib/${pname}*${postgresql.dlSuffix} | wc -l) - cp *${postgresql.dlSuffix} $out/lib - cp *.sql $out/share/postgresql/extension - cp *.control $out/share/postgresql/extension + if [[ "$actualFiles" != "$expectedFiles" ]]; then + echo "Error: Expected $expectedFiles library files, found $actualFiles" + echo "Files found:" + ls -la $out/lib/${pname}*${postgresql.dlSuffix} || true + exit 1 + fi ''; - meta = with lib; { - description = "HTTP client for Postgres"; - homepage = "https://github.com/pramsey/${pname}"; - platforms = postgresql.meta.platforms; - license = licenses.postgresql; + passthru = { + inherit versions numberOfVersions; + pname = "${pname}-all"; + version = + "multi-" + lib.concatStringsSep "-" (map (v: lib.replaceStrings [ "." ] [ "-" ] v) versions); }; } diff --git a/nix/ext/tests/http.nix b/nix/ext/tests/http.nix new file mode 100644 index 000000000..aa2130fa4 --- /dev/null +++ b/nix/ext/tests/http.nix @@ -0,0 +1,155 @@ +{ self, pkgs }: +let + pname = "http"; + inherit (pkgs) lib; + installedExtension = + postgresMajorVersion: self.packages.${pkgs.system}."psql_${postgresMajorVersion}/exts/${pname}-all"; + versions = postgresqlMajorVersion: (installedExtension postgresqlMajorVersion).versions; + postgresqlWithExtension = + postgresql: + let + majorVersion = lib.versions.major postgresql.version; + pkg = pkgs.buildEnv { + name = "postgresql-${majorVersion}-${pname}"; + paths = [ + postgresql + postgresql.lib + (installedExtension majorVersion) + ]; + passthru = { + inherit (postgresql) version psqlSchema; + lib = pkg; + withPackages = _: pkg; + }; + nativeBuildInputs = [ pkgs.makeWrapper ]; + pathsToLink = [ + "/" + "/bin" + "/lib" + ]; + postBuild = '' + wrapProgram $out/bin/postgres --set NIX_PGLIBDIR $out/lib + wrapProgram $out/bin/pg_ctl --set NIX_PGLIBDIR $out/lib + wrapProgram $out/bin/pg_upgrade --set NIX_PGLIBDIR $out/lib + ''; + }; + in + pkg; +in +self.inputs.nixpkgs.lib.nixos.runTest { + name = pname; + hostPkgs = pkgs; + nodes.server = + { config, ... }: + { + virtualisation = { + forwardPorts = [ + { + from = "host"; + host.port = 13022; + guest.port = 22; + } + ]; + }; + services.openssh = { + enable = true; + }; + + services.postgresql = { + enable = true; + package = postgresqlWithExtension self.packages.${pkgs.system}.postgresql_15; + }; + + specialisation.postgresql17.configuration = { + services.postgresql = { + package = lib.mkForce (postgresqlWithExtension self.packages.${pkgs.system}.postgresql_17); + }; + + systemd.services.postgresql-migrate = { + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "postgres"; + Group = "postgres"; + StateDirectory = "postgresql"; + WorkingDirectory = "${builtins.dirOf config.services.postgresql.dataDir}"; + }; + script = + let + oldPostgresql = postgresqlWithExtension self.packages.${pkgs.system}.postgresql_15; + newPostgresql = postgresqlWithExtension self.packages.${pkgs.system}.postgresql_17; + oldDataDir = "${builtins.dirOf config.services.postgresql.dataDir}/${oldPostgresql.psqlSchema}"; + newDataDir = "${builtins.dirOf config.services.postgresql.dataDir}/${newPostgresql.psqlSchema}"; + in + '' + if [[ ! -d ${newDataDir} ]]; then + install -d -m 0700 -o postgres -g postgres "${newDataDir}" + ${newPostgresql}/bin/initdb -D "${newDataDir}" + ${newPostgresql}/bin/pg_upgrade --old-datadir "${oldDataDir}" --new-datadir "${newDataDir}" \ + --old-bindir "${oldPostgresql}/bin" --new-bindir "${newPostgresql}/bin" + else + echo "${newDataDir} already exists" + fi + ''; + }; + + systemd.services.postgresql = { + after = [ "postgresql-migrate.service" ]; + requires = [ "postgresql-migrate.service" ]; + }; + }; + + }; + testScript = + { nodes, ... }: + let + pg17-configuration = "${nodes.server.system.build.toplevel}/specialisation/postgresql17"; + in + '' + versions = { + "15": [${lib.concatStringsSep ", " (map (s: ''"${s}"'') (versions "15"))}], + "17": [${lib.concatStringsSep ", " (map (s: ''"${s}"'') (versions "17"))}], + } + + def run_sql(query): + return server.succeed(f"""sudo -u postgres psql -t -A -F\",\" -c \"{query}\" """).strip() + + def check_upgrade_path(pg_version): + with subtest("Check ${pname} upgrade path"): + firstVersion = versions[pg_version][0] + server.succeed("sudo -u postgres psql -c 'DROP EXTENSION IF EXISTS ${pname};'") + run_sql(f"""CREATE EXTENSION ${pname} WITH VERSION '{firstVersion}' CASCADE;""") + installed_version = run_sql(r"""SELECT extversion FROM pg_extension WHERE extname = '${pname}';""") + assert installed_version == firstVersion, f"Expected ${pname} version {firstVersion}, but found {installed_version}" + for version in versions[pg_version][1:]: + run_sql(f"""ALTER EXTENSION ${pname} UPDATE TO '{version}';""") + installed_version = run_sql(r"""SELECT extversion FROM pg_extension WHERE extname = '${pname}';""") + assert installed_version == version, f"Expected ${pname} version {version}, but found {installed_version}" + + start_all() + + server.wait_for_unit("multi-user.target") + server.wait_for_unit("postgresql.service") + + check_upgrade_path("15") + + with subtest("Check ${pname} latest extension version"): + server.succeed("sudo -u postgres psql -c 'DROP EXTENSION ${pname};'") + server.succeed("sudo -u postgres psql -c 'CREATE EXTENSION ${pname} CASCADE;'") + installed_extensions=run_sql(r"""SELECT extname, extversion FROM pg_extension;""") + latestVersion = versions["15"][-1] + assert f"${pname},{latestVersion}" in installed_extensions + + with subtest("switch to postgresql 17"): + server.succeed( + "${pg17-configuration}/bin/switch-to-configuration test >&2" + ) + + with subtest("Check ${pname} latest extension version after upgrade"): + installed_extensions=run_sql(r"""SELECT extname, extversion FROM pg_extension;""") + latestVersion = versions["17"][-1] + assert f"${pname},{latestVersion}" in installed_extensions + + check_upgrade_path("17") + ''; +} diff --git a/nix/ext/versions.json b/nix/ext/versions.json index 7fc75a7f8..53666e1cd 100644 --- a/nix/ext/versions.json +++ b/nix/ext/versions.json @@ -1,12 +1,17 @@ { - "index_advisor": { - "0.2.0": { + "http": { + "1.5": { + "postgresql": [ + "15" + ], + "hash": "sha256-+N/CXm4arRgvhglanfvO0FNOBUWV5RL8mn/9FpNvcjY=" + }, + "1.6": { "postgresql": [ "15", - "17", - "orioledb-17" + "17" ], - "hash": "sha256-G0eQk2bY5CNPMeokN/nb05g03CuiplRf902YXFVQFbs=" + "hash": "sha256-C8eqi0q1dnshUAZjIsZFwa5FTYc7vmATF3vv2CReWPM=" } }, "hypopg": { @@ -24,6 +29,16 @@ "hash": "sha256-88uKPSnITRZ2VkelI56jZ9GWazG/Rn39QlyHKJKSKMM=" } }, + "index_advisor": { + "0.2.0": { + "postgresql": [ + "15", + "17", + "orioledb-17" + ], + "hash": "sha256-G0eQk2bY5CNPMeokN/nb05g03CuiplRf902YXFVQFbs=" + } + }, "pg_cron": { "1.3.1": { "postgresql": [ From c9388de0567c237cf11284d65da9d6a6097979ca Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 24 Sep 2025 13:41:02 -0400 Subject: [PATCH 2/6] feat: http multiversion extension and pg_regress tests --- nix/ext/tests/http.nix | 1 - nix/ext/versions.json | 3 +- nix/tests/expected/http.out | 105 ++++++++++++++++++++++++++++++++ nix/tests/expected/z_17_rum.out | 41 +++++++++++++ nix/tests/sql/http.sql | 65 ++++++++++++++++++++ nix/tests/sql/z_17_rum.sql | 40 ++++++++++++ 6 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 nix/tests/expected/http.out create mode 100644 nix/tests/expected/z_17_rum.out create mode 100644 nix/tests/sql/http.sql create mode 100644 nix/tests/sql/z_17_rum.sql diff --git a/nix/ext/tests/http.nix b/nix/ext/tests/http.nix index aa2130fa4..100fdcd00 100644 --- a/nix/ext/tests/http.nix +++ b/nix/ext/tests/http.nix @@ -98,7 +98,6 @@ self.inputs.nixpkgs.lib.nixos.runTest { requires = [ "postgresql-migrate.service" ]; }; }; - }; testScript = { nodes, ... }: diff --git a/nix/ext/versions.json b/nix/ext/versions.json index 53666e1cd..435028206 100644 --- a/nix/ext/versions.json +++ b/nix/ext/versions.json @@ -9,7 +9,8 @@ "1.6": { "postgresql": [ "15", - "17" + "17", + "orioledb-17" ], "hash": "sha256-C8eqi0q1dnshUAZjIsZFwa5FTYc7vmATF3vv2CReWPM=" } diff --git a/nix/tests/expected/http.out b/nix/tests/expected/http.out new file mode 100644 index 000000000..1335e9736 --- /dev/null +++ b/nix/tests/expected/http.out @@ -0,0 +1,105 @@ +-- Test for http extension +-- Basic HTTP functionality tests +-- Test basic HTTP GET request +SELECT status FROM http_get('http://httpbingo.org/get'); + status +-------- + 200 +(1 row) + +-- Test HTTP GET with headers +SELECT status, content_type +FROM http(( + 'GET', + 'http://httpbingo.org/headers', + ARRAY[http_header('User-Agent', 'pg_http_test')], + NULL, + NULL +)::http_request); + status | content_type +--------+--------------------------------- + 200 | application/json; charset=utf-8 +(1 row) + +-- Test HTTP POST request with JSON body +SELECT status FROM http_post( + 'http://httpbingo.org/post', + '{"test": "data"}', + 'application/json' +); + status +-------- + 200 +(1 row) + +-- Test HTTP PUT request +SELECT status FROM http_put( + 'http://httpbingo.org/put', + '{"update": "data"}', + 'application/json' +); + status +-------- + 200 +(1 row) + +-- Test HTTP DELETE request +SELECT status FROM http_delete('http://httpbingo.org/delete'); + status +-------- + 200 +(1 row) + +-- Test HTTP PATCH request +SELECT status FROM http_patch( + 'http://httpbingo.org/patch', + '{"patch": "data"}', + 'application/json' +); + status +-------- + 200 +(1 row) + +-- Test HTTP HEAD request +SELECT status FROM http_head('http://httpbingo.org/get'); + status +-------- + 200 +(1 row) + +-- Test response headers parsing +WITH response AS ( + SELECT * FROM http_get('http://httpbingo.org/response-headers?Content-Type=text/plain') +) +SELECT + status, + content_type, + headers IS NOT NULL as has_headers +FROM response; + status | content_type | has_headers +--------+--------------+------------- + 200 | text/plain | t +(1 row) + +-- Test timeout handling (using a delay endpoint) +-- This should complete successfully with reasonable timeout +SELECT status FROM http(( + 'GET', + 'http://httpbingo.org/delay/1', + ARRAY[]::http_header[], + 'application/json', + 2000 -- 2 second timeout +)::http_request); + status +-------- + 200 +(1 row) + +-- Test URL encoding +SELECT status FROM http_get('http://httpbingo.org/anything?param=value%20with%20spaces&another=123'); + status +-------- + 200 +(1 row) + diff --git a/nix/tests/expected/z_17_rum.out b/nix/tests/expected/z_17_rum.out new file mode 100644 index 000000000..1296befa7 --- /dev/null +++ b/nix/tests/expected/z_17_rum.out @@ -0,0 +1,41 @@ +/* +This extension is excluded from oriole-17 because it uses an unsupported index type +*/ +create schema v; +create table v.test_rum( + t text, + a tsvector +); +create trigger tsvectorupdate + before update or insert on v.test_rum + for each row + execute procedure + tsvector_update_trigger( + 'a', + 'pg_catalog.english', + 't' + ); +insert into v.test_rum(t) +values + ('the situation is most beautiful'), + ('it is a beautiful'), + ('it looks like a beautiful place'); +create index rumidx on v.test_rum using rum (a rum_tsvector_ops); +select + t, + round(a <=> to_tsquery('english', 'beautiful | place')) as rank +from + v.test_rum +where + a @@ to_tsquery('english', 'beautiful | place') +order by + a <=> to_tsquery('english', 'beautiful | place'); + t | rank +---------------------------------+------ + it looks like a beautiful place | 8 + the situation is most beautiful | 16 + it is a beautiful | 16 +(3 rows) + +drop schema v cascade; +NOTICE: drop cascades to table v.test_rum diff --git a/nix/tests/sql/http.sql b/nix/tests/sql/http.sql new file mode 100644 index 000000000..f7bd2d8e8 --- /dev/null +++ b/nix/tests/sql/http.sql @@ -0,0 +1,65 @@ +-- Test for http extension +-- Basic HTTP functionality tests + +-- Test basic HTTP GET request +SELECT status FROM http_get('http://httpbingo.org/get'); + +-- Test HTTP GET with headers +SELECT status, content_type +FROM http(( + 'GET', + 'http://httpbingo.org/headers', + ARRAY[http_header('User-Agent', 'pg_http_test')], + NULL, + NULL +)::http_request); + +-- Test HTTP POST request with JSON body +SELECT status FROM http_post( + 'http://httpbingo.org/post', + '{"test": "data"}', + 'application/json' +); + +-- Test HTTP PUT request +SELECT status FROM http_put( + 'http://httpbingo.org/put', + '{"update": "data"}', + 'application/json' +); + +-- Test HTTP DELETE request +SELECT status FROM http_delete('http://httpbingo.org/delete'); + +-- Test HTTP PATCH request +SELECT status FROM http_patch( + 'http://httpbingo.org/patch', + '{"patch": "data"}', + 'application/json' +); + +-- Test HTTP HEAD request +SELECT status FROM http_head('http://httpbingo.org/get'); + +-- Test response headers parsing +WITH response AS ( + SELECT * FROM http_get('http://httpbingo.org/response-headers?Content-Type=text/plain') +) +SELECT + status, + content_type, + headers IS NOT NULL as has_headers +FROM response; + +-- Test timeout handling (using a delay endpoint) +-- This should complete successfully with reasonable timeout +SELECT status FROM http(( + 'GET', + 'http://httpbingo.org/delay/1', + ARRAY[]::http_header[], + 'application/json', + 2000 -- 2 second timeout +)::http_request); + +-- Test URL encoding +SELECT status FROM http_get('http://httpbingo.org/anything?param=value%20with%20spaces&another=123'); diff --git a/nix/tests/sql/z_17_rum.sql b/nix/tests/sql/z_17_rum.sql new file mode 100644 index 000000000..6ae945975 --- /dev/null +++ b/nix/tests/sql/z_17_rum.sql @@ -0,0 +1,40 @@ +/* +This extension is excluded from oriole-17 because it uses an unsupported index type +*/ +create schema v; + +create table v.test_rum( + t text, + a tsvector +); + +create trigger tsvectorupdate + before update or insert on v.test_rum + for each row + execute procedure + tsvector_update_trigger( + 'a', + 'pg_catalog.english', + 't' + ); + +insert into v.test_rum(t) +values + ('the situation is most beautiful'), + ('it is a beautiful'), + ('it looks like a beautiful place'); + +create index rumidx on v.test_rum using rum (a rum_tsvector_ops); + +select + t, + round(a <=> to_tsquery('english', 'beautiful | place')) as rank +from + v.test_rum +where + a @@ to_tsquery('english', 'beautiful | place') +order by + a <=> to_tsquery('english', 'beautiful | place'); + + +drop schema v cascade; From 825c6ea3259d3a278431b2969c608223529acb8b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 24 Sep 2025 13:49:51 -0400 Subject: [PATCH 3/6] chore: bump to test --- ansible/vars.yml | 6 +++--- nix/ext/versions.json | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ansible/vars.yml b/ansible/vars.yml index e7e15a78d..e5b7f1c86 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -9,9 +9,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.5.1.029" - postgres17: "17.6.1.008" - postgres15: "15.14.1.008" + postgresorioledb-17: "17.5.1.028-orioledb-http-1" + postgres17: "17.6.1.007-http-1" + postgres15: "15.14.1.007-http-1" # Non Postgres Extensions pgbouncer_release: "1.19.0" diff --git a/nix/ext/versions.json b/nix/ext/versions.json index 435028206..53666e1cd 100644 --- a/nix/ext/versions.json +++ b/nix/ext/versions.json @@ -9,8 +9,7 @@ "1.6": { "postgresql": [ "15", - "17", - "orioledb-17" + "17" ], "hash": "sha256-C8eqi0q1dnshUAZjIsZFwa5FTYc7vmATF3vv2CReWPM=" } From a3d3fd5cc2d66d411339700e29b6238d195231d4 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 24 Sep 2025 14:41:27 -0400 Subject: [PATCH 4/6] tests: local mock http server for http regress tests --- nix/checks.nix | 49 +++++++ nix/packages/default.nix | 1 + nix/packages/http-mock-server.nix | 35 +++++ nix/tests/expected/http.out | 20 +-- nix/tests/http-mock-server.py | 222 ++++++++++++++++++++++++++++++ nix/tests/sql/http.sql | 20 +-- 6 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 nix/packages/http-mock-server.nix create mode 100644 nix/tests/http-mock-server.py diff --git a/nix/checks.nix b/nix/checks.nix index d6d3aa59f..8e4520665 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -163,11 +163,60 @@ which getkey-script supabase-groonga + python3 + netcat ]; } '' set -e + # Start HTTP mock server for http extension tests using portable locking + HTTP_MOCK_PORT=8889 + PID_FILE="/tmp/http-mock-server-$HTTP_MOCK_PORT.pid" + + # Function to start mock server with simple lock mechanism + start_mock_server() { + # Try to acquire lock by creating PID file atomically + if (set -C; echo $$ > "$PID_FILE") 2>/dev/null; then + # We got the lock, start the server + echo "Starting HTTP mock server on port $HTTP_MOCK_PORT for tests..." + ${pkgs.python3}/bin/python3 ${./tests/http-mock-server.py} $HTTP_MOCK_PORT & + HTTP_MOCK_PID=$! + echo $HTTP_MOCK_PID > "$PID_FILE" + + # Clean up on exit + trap "kill $HTTP_MOCK_PID 2>/dev/null || true; rm -f '$PID_FILE'" EXIT + + # Wait for server to be ready + sleep 2 + + # Keep this process alive to maintain the lock + wait $HTTP_MOCK_PID + rm -f "$PID_FILE" + else + # Lock exists, check if process is still running + if [ -f "$PID_FILE" ]; then + existing_pid=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then + echo "HTTP mock server already running with PID $existing_pid, reusing it..." + return 0 + else + echo "Stale PID file found, removing and retrying..." + rm -f "$PID_FILE" + sleep 1 + start_mock_server # Retry once + return $? + fi + fi + fi + } + + # Start the server in background + start_mock_server & + + # Give server time to start + sleep 3 + #First we need to create a generic pg cluster for pgtap tests and run those export GRN_PLUGINS_DIR=${pkgs.supabase-groonga}/lib/groonga/plugins PGTAP_CLUSTER=$(mktemp -d) diff --git a/nix/packages/default.nix b/nix/packages/default.nix index fca05a10a..f297c8359 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -35,6 +35,7 @@ dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; docs = pkgs.callPackage ./docs.nix { }; supabase-groonga = pkgs.callPackage ./groonga { }; + http-mock-server = pkgs.callPackage ./http-mock-server.nix { }; local-infra-bootstrap = pkgs.callPackage ./local-infra-bootstrap.nix { }; migrate-tool = pkgs.callPackage ./migrate-tool.nix { psql_15 = self'.packages."psql_15/bin"; }; overlayfs-on-package = pkgs.callPackage ./overlayfs-on-package.nix { }; diff --git a/nix/packages/http-mock-server.nix b/nix/packages/http-mock-server.nix new file mode 100644 index 000000000..67a4af520 --- /dev/null +++ b/nix/packages/http-mock-server.nix @@ -0,0 +1,35 @@ +{ + pkgs, + lib, + stdenv, +}: + +stdenv.mkDerivation { + pname = "http-mock-server"; + version = "1.0.0"; + + src = ../tests/http-mock-server.py; + + nativeBuildInputs = with pkgs; [ + python3 + makeWrapper + ]; + + dontUnpack = true; + + installPhase = '' + mkdir -p $out/bin + cp $src $out/bin/http-mock-server.py + chmod +x $out/bin/http-mock-server.py + + # Create a wrapper script + makeWrapper ${pkgs.python3}/bin/python3 $out/bin/http-mock-server \ + --add-flags "$out/bin/http-mock-server.py" + ''; + + meta = with lib; { + description = "Simple HTTP mock server for testing"; + license = licenses.mit; + platforms = platforms.all; + }; +} diff --git a/nix/tests/expected/http.out b/nix/tests/expected/http.out index 1335e9736..a16d057a7 100644 --- a/nix/tests/expected/http.out +++ b/nix/tests/expected/http.out @@ -1,7 +1,7 @@ -- Test for http extension -- Basic HTTP functionality tests -- Test basic HTTP GET request -SELECT status FROM http_get('http://httpbingo.org/get'); +SELECT status FROM http_get('http://localhost:8889/get'); status -------- 200 @@ -11,7 +11,7 @@ SELECT status FROM http_get('http://httpbingo.org/get'); SELECT status, content_type FROM http(( 'GET', - 'http://httpbingo.org/headers', + 'http://localhost:8889/headers', ARRAY[http_header('User-Agent', 'pg_http_test')], NULL, NULL @@ -23,7 +23,7 @@ FROM http(( -- Test HTTP POST request with JSON body SELECT status FROM http_post( - 'http://httpbingo.org/post', + 'http://localhost:8889/post', '{"test": "data"}', 'application/json' ); @@ -34,7 +34,7 @@ SELECT status FROM http_post( -- Test HTTP PUT request SELECT status FROM http_put( - 'http://httpbingo.org/put', + 'http://localhost:8889/put', '{"update": "data"}', 'application/json' ); @@ -44,7 +44,7 @@ SELECT status FROM http_put( (1 row) -- Test HTTP DELETE request -SELECT status FROM http_delete('http://httpbingo.org/delete'); +SELECT status FROM http_delete('http://localhost:8889/delete'); status -------- 200 @@ -52,7 +52,7 @@ SELECT status FROM http_delete('http://httpbingo.org/delete'); -- Test HTTP PATCH request SELECT status FROM http_patch( - 'http://httpbingo.org/patch', + 'http://localhost:8889/patch', '{"patch": "data"}', 'application/json' ); @@ -62,7 +62,7 @@ SELECT status FROM http_patch( (1 row) -- Test HTTP HEAD request -SELECT status FROM http_head('http://httpbingo.org/get'); +SELECT status FROM http_head('http://localhost:8889/get'); status -------- 200 @@ -70,7 +70,7 @@ SELECT status FROM http_head('http://httpbingo.org/get'); -- Test response headers parsing WITH response AS ( - SELECT * FROM http_get('http://httpbingo.org/response-headers?Content-Type=text/plain') + SELECT * FROM http_get('http://localhost:8889/response-headers?Content-Type=text/plain') ) SELECT status, @@ -86,7 +86,7 @@ FROM response; -- This should complete successfully with reasonable timeout SELECT status FROM http(( 'GET', - 'http://httpbingo.org/delay/1', + 'http://localhost:8889/delay/1', ARRAY[]::http_header[], 'application/json', 2000 -- 2 second timeout @@ -97,7 +97,7 @@ SELECT status FROM http(( (1 row) -- Test URL encoding -SELECT status FROM http_get('http://httpbingo.org/anything?param=value%20with%20spaces&another=123'); +SELECT status FROM http_get('http://localhost:8889/anything?param=value%20with%20spaces&another=123'); status -------- 200 diff --git a/nix/tests/http-mock-server.py b/nix/tests/http-mock-server.py new file mode 100644 index 000000000..f3e08ece1 --- /dev/null +++ b/nix/tests/http-mock-server.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Simple HTTP mock server for testing pg_http extension offline. +Mimics basic endpoints similar to httpbingo/postman-echo services. +""" + +from http.server import HTTPServer, BaseHTTPRequestHandler +import json +import urllib.parse +import time + + +class MockHTTPHandler(BaseHTTPRequestHandler): + def _send_json_response(self, status_code=200, data=None): + """Send a JSON response""" + self.send_response(status_code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + response_data = data or {} + self.wfile.write(json.dumps(response_data).encode("utf-8")) + + def _send_text_response( + self, status_code=200, content="", content_type="text/plain" + ): + """Send a text response""" + self.send_response(status_code) + self.send_header("Content-Type", content_type) + self.end_headers() + self.wfile.write(content.encode("utf-8")) + + def _get_request_info(self): + """Get request information""" + parsed_path = urllib.parse.urlparse(self.path) + query_params = urllib.parse.parse_qs(parsed_path.query) + + # Read body if present + content_length = int(self.headers.get("Content-Length", 0)) + body = ( + self.rfile.read(content_length).decode("utf-8") + if content_length > 0 + else "" + ) + + return { + "method": self.command, + "url": self.path, + "path": parsed_path.path, + "query": query_params, + "headers": dict(self.headers), + "body": body, + } + + def do_GET(self): + """Handle GET requests""" + request_info = self._get_request_info() + path = request_info["path"] + + if path == "/get": + response = { + "args": request_info["query"], + "headers": request_info["headers"], + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + } + self._send_json_response(200, response) + + elif path == "/headers": + response = {"headers": request_info["headers"]} + self._send_json_response(200, response) + + elif path == "/response-headers": + # Check if Content-Type is specified in query params + query_params = request_info["query"] + if "Content-Type" in query_params: + content_type = query_params["Content-Type"][0] + self._send_text_response( + 200, "Response with custom content type", content_type + ) + else: + self._send_json_response(200, {"message": "response-headers endpoint"}) + + elif path.startswith("/delay/"): + # Extract delay time from path + try: + delay = int(path.split("/delay/")[1]) + time.sleep(min(delay, 5)) # Cap at 5 seconds + self._send_json_response(200, {"delay": delay}) + except (ValueError, IndexError): + self._send_json_response(400, {"error": "Invalid delay value"}) + + elif path == "/anything" or path.startswith("/anything"): + response = { + "method": "GET", + "args": request_info["query"], + "headers": request_info["headers"], + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + "data": "", + "json": None, + } + self._send_json_response(200, response) + + else: + self._send_json_response(404, {"error": "Not found"}) + + def do_POST(self): + """Handle POST requests""" + request_info = self._get_request_info() + path = request_info["path"] + + if path == "/post": + response = { + "args": request_info["query"], + "data": request_info["body"], + "headers": request_info["headers"], + "json": None, + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + } + + # Try to parse JSON if content-type is json + if "application/json" in request_info["headers"].get("Content-Type", ""): + try: + response["json"] = json.loads(request_info["body"]) + except json.JSONDecodeError: + pass + + self._send_json_response(200, response) + else: + self._send_json_response(404, {"error": "Not found"}) + + def do_PUT(self): + """Handle PUT requests""" + request_info = self._get_request_info() + path = request_info["path"] + + if path == "/put": + response = { + "args": request_info["query"], + "data": request_info["body"], + "headers": request_info["headers"], + "json": None, + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + } + + # Try to parse JSON if content-type is json + if "application/json" in request_info["headers"].get("Content-Type", ""): + try: + response["json"] = json.loads(request_info["body"]) + except json.JSONDecodeError: + pass + + self._send_json_response(200, response) + else: + self._send_json_response(404, {"error": "Not found"}) + + def do_DELETE(self): + """Handle DELETE requests""" + request_info = self._get_request_info() + path = request_info["path"] + + if path == "/delete": + response = { + "args": request_info["query"], + "headers": request_info["headers"], + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + } + self._send_json_response(200, response) + else: + self._send_json_response(404, {"error": "Not found"}) + + def do_PATCH(self): + """Handle PATCH requests""" + request_info = self._get_request_info() + path = request_info["path"] + + if path == "/patch": + response = { + "args": request_info["query"], + "data": request_info["body"], + "headers": request_info["headers"], + "json": None, + "url": f"http://{self.headers.get('Host', 'localhost:8080')}{self.path}", + } + + # Try to parse JSON if content-type is json + if "application/json" in request_info["headers"].get("Content-Type", ""): + try: + response["json"] = json.loads(request_info["body"]) + except json.JSONDecodeError: + pass + + self._send_json_response(200, response) + else: + self._send_json_response(404, {"error": "Not found"}) + + def do_HEAD(self): + """Handle HEAD requests""" + path = urllib.parse.urlparse(self.path).path + + if path == "/get": + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + """Suppress default logging""" + pass + + +def run_server(port=8080): + """Run the mock HTTP server""" + server = HTTPServer(("0.0.0.0", port), MockHTTPHandler) + print(f"Mock HTTP server running on port {port}") + server.serve_forever() + + +if __name__ == "__main__": + import sys + + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + run_server(port) diff --git a/nix/tests/sql/http.sql b/nix/tests/sql/http.sql index f7bd2d8e8..e4efacfc3 100644 --- a/nix/tests/sql/http.sql +++ b/nix/tests/sql/http.sql @@ -2,13 +2,13 @@ -- Basic HTTP functionality tests -- Test basic HTTP GET request -SELECT status FROM http_get('http://httpbingo.org/get'); +SELECT status FROM http_get('http://localhost:8889/get'); -- Test HTTP GET with headers SELECT status, content_type FROM http(( 'GET', - 'http://httpbingo.org/headers', + 'http://localhost:8889/headers', ARRAY[http_header('User-Agent', 'pg_http_test')], NULL, NULL @@ -16,34 +16,34 @@ FROM http(( -- Test HTTP POST request with JSON body SELECT status FROM http_post( - 'http://httpbingo.org/post', + 'http://localhost:8889/post', '{"test": "data"}', 'application/json' ); -- Test HTTP PUT request SELECT status FROM http_put( - 'http://httpbingo.org/put', + 'http://localhost:8889/put', '{"update": "data"}', 'application/json' ); -- Test HTTP DELETE request -SELECT status FROM http_delete('http://httpbingo.org/delete'); +SELECT status FROM http_delete('http://localhost:8889/delete'); -- Test HTTP PATCH request SELECT status FROM http_patch( - 'http://httpbingo.org/patch', + 'http://localhost:8889/patch', '{"patch": "data"}', 'application/json' ); -- Test HTTP HEAD request -SELECT status FROM http_head('http://httpbingo.org/get'); +SELECT status FROM http_head('http://localhost:8889/get'); -- Test response headers parsing WITH response AS ( - SELECT * FROM http_get('http://httpbingo.org/response-headers?Content-Type=text/plain') + SELECT * FROM http_get('http://localhost:8889/response-headers?Content-Type=text/plain') ) SELECT status, @@ -55,11 +55,11 @@ FROM response; -- This should complete successfully with reasonable timeout SELECT status FROM http(( 'GET', - 'http://httpbingo.org/delay/1', + 'http://localhost:8889/delay/1', ARRAY[]::http_header[], 'application/json', 2000 -- 2 second timeout )::http_request); -- Test URL encoding -SELECT status FROM http_get('http://httpbingo.org/anything?param=value%20with%20spaces&another=123'); +SELECT status FROM http_get('http://localhost:8889/anything?param=value%20with%20spaces&another=123'); From 5fa9e50ada00ea458fffba9ed9f15a96bfe9281f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 24 Sep 2025 15:30:00 -0400 Subject: [PATCH 5/6] fix: a better way to handle http server for tests --- nix/checks.nix | 77 +++++++++++++++++------------------ nix/tests/expected/http.out | 20 ++++----- nix/tests/http-mock-server.py | 47 ++++++++++++++++++--- nix/tests/sql/http.sql | 20 ++++----- 4 files changed, 99 insertions(+), 65 deletions(-) diff --git a/nix/checks.nix b/nix/checks.nix index 8e4520665..74b50a84f 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -170,52 +170,35 @@ '' set -e - # Start HTTP mock server for http extension tests using portable locking - HTTP_MOCK_PORT=8889 - PID_FILE="/tmp/http-mock-server-$HTTP_MOCK_PORT.pid" + # Start HTTP mock server for http extension tests + # Use a build-specific directory for coordination + BUILD_TMP=$(mktemp -d) + HTTP_MOCK_PORT_FILE="$BUILD_TMP/http-mock-port" - # Function to start mock server with simple lock mechanism - start_mock_server() { - # Try to acquire lock by creating PID file atomically - if (set -C; echo $$ > "$PID_FILE") 2>/dev/null; then - # We got the lock, start the server - echo "Starting HTTP mock server on port $HTTP_MOCK_PORT for tests..." - ${pkgs.python3}/bin/python3 ${./tests/http-mock-server.py} $HTTP_MOCK_PORT & - HTTP_MOCK_PID=$! - echo $HTTP_MOCK_PID > "$PID_FILE" + echo "Starting HTTP mock server (will find free port)..." + HTTP_MOCK_PORT_FILE="$HTTP_MOCK_PORT_FILE" ${pkgs.python3}/bin/python3 ${./tests/http-mock-server.py} & + HTTP_MOCK_PID=$! - # Clean up on exit - trap "kill $HTTP_MOCK_PID 2>/dev/null || true; rm -f '$PID_FILE'" EXIT + # Clean up on exit + trap "kill $HTTP_MOCK_PID 2>/dev/null || true; rm -rf '$BUILD_TMP'" EXIT - # Wait for server to be ready - sleep 2 - - # Keep this process alive to maintain the lock - wait $HTTP_MOCK_PID - rm -f "$PID_FILE" - else - # Lock exists, check if process is still running - if [ -f "$PID_FILE" ]; then - existing_pid=$(cat "$PID_FILE" 2>/dev/null || echo "") - if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then - echo "HTTP mock server already running with PID $existing_pid, reusing it..." - return 0 - else - echo "Stale PID file found, removing and retrying..." - rm -f "$PID_FILE" - sleep 1 - start_mock_server # Retry once - return $? - fi - fi + # Wait for server to start and write port file + for i in {1..10}; do + if [ -f "$HTTP_MOCK_PORT_FILE" ]; then + HTTP_MOCK_PORT=$(cat "$HTTP_MOCK_PORT_FILE") + echo "HTTP mock server started on port $HTTP_MOCK_PORT" + break fi - } + sleep 1 + done - # Start the server in background - start_mock_server & + if [ ! -f "$HTTP_MOCK_PORT_FILE" ]; then + echo "Failed to start HTTP mock server" + exit 1 + fi - # Give server time to start - sleep 3 + # Export the port for use in SQL tests + export HTTP_MOCK_PORT #First we need to create a generic pg cluster for pgtap tests and run those export GRN_PLUGINS_DIR=${pkgs.supabase-groonga}/lib/groonga/plugins @@ -277,6 +260,13 @@ pg_ctl -D "$PGTAP_CLUSTER" stop exit 1 fi + + # Create a table to store test configuration + psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -c " + CREATE TABLE IF NOT EXISTS test_config (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO test_config (key, value) VALUES ('http_mock_port', '$HTTP_MOCK_PORT') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + " SORTED_DIR=$(mktemp -d) for t in $(printf "%s\n" ${builtins.concatStringsSep " " sortedTestList}); do psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -f "${./tests/sql}/$t.sql" || true @@ -310,6 +300,13 @@ exit 1 fi + # Create a table to store test configuration for pg_regress tests + psql -p ${pgPort} -h ${self.supabase.defaults.host} --no-password --username=supabase_admin -d postgres -c " + CREATE TABLE IF NOT EXISTS test_config (key TEXT PRIMARY KEY, value TEXT); + INSERT INTO test_config (key, value) VALUES ('http_mock_port', '$HTTP_MOCK_PORT') + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + " + mkdir -p $out/regression_output if ! pg_regress \ --use-existing \ diff --git a/nix/tests/expected/http.out b/nix/tests/expected/http.out index a16d057a7..d83488006 100644 --- a/nix/tests/expected/http.out +++ b/nix/tests/expected/http.out @@ -1,7 +1,7 @@ -- Test for http extension -- Basic HTTP functionality tests -- Test basic HTTP GET request -SELECT status FROM http_get('http://localhost:8889/get'); +SELECT status FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/get'); status -------- 200 @@ -11,7 +11,7 @@ SELECT status FROM http_get('http://localhost:8889/get'); SELECT status, content_type FROM http(( 'GET', - 'http://localhost:8889/headers', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/headers', ARRAY[http_header('User-Agent', 'pg_http_test')], NULL, NULL @@ -23,7 +23,7 @@ FROM http(( -- Test HTTP POST request with JSON body SELECT status FROM http_post( - 'http://localhost:8889/post', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/post', '{"test": "data"}', 'application/json' ); @@ -34,7 +34,7 @@ SELECT status FROM http_post( -- Test HTTP PUT request SELECT status FROM http_put( - 'http://localhost:8889/put', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/put', '{"update": "data"}', 'application/json' ); @@ -44,7 +44,7 @@ SELECT status FROM http_put( (1 row) -- Test HTTP DELETE request -SELECT status FROM http_delete('http://localhost:8889/delete'); +SELECT status FROM http_delete('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/delete'); status -------- 200 @@ -52,7 +52,7 @@ SELECT status FROM http_delete('http://localhost:8889/delete'); -- Test HTTP PATCH request SELECT status FROM http_patch( - 'http://localhost:8889/patch', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/patch', '{"patch": "data"}', 'application/json' ); @@ -62,7 +62,7 @@ SELECT status FROM http_patch( (1 row) -- Test HTTP HEAD request -SELECT status FROM http_head('http://localhost:8889/get'); +SELECT status FROM http_head('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/get'); status -------- 200 @@ -70,7 +70,7 @@ SELECT status FROM http_head('http://localhost:8889/get'); -- Test response headers parsing WITH response AS ( - SELECT * FROM http_get('http://localhost:8889/response-headers?Content-Type=text/plain') + SELECT * FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/response-headers?Content-Type=text/plain') ) SELECT status, @@ -86,7 +86,7 @@ FROM response; -- This should complete successfully with reasonable timeout SELECT status FROM http(( 'GET', - 'http://localhost:8889/delay/1', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/delay/1', ARRAY[]::http_header[], 'application/json', 2000 -- 2 second timeout @@ -97,7 +97,7 @@ SELECT status FROM http(( (1 row) -- Test URL encoding -SELECT status FROM http_get('http://localhost:8889/anything?param=value%20with%20spaces&another=123'); +SELECT status FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/anything?param=value%20with%20spaces&another=123'); status -------- 200 diff --git a/nix/tests/http-mock-server.py b/nix/tests/http-mock-server.py index f3e08ece1..fedeb40ad 100644 --- a/nix/tests/http-mock-server.py +++ b/nix/tests/http-mock-server.py @@ -208,15 +208,52 @@ def log_message(self, format, *args): pass -def run_server(port=8080): +def find_free_port(start_port=8880, end_port=8899): + """Find a free port within the given range""" + import socket + + for port in range(start_port, end_port + 1): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("0.0.0.0", port)) + return port + except OSError: + continue + + raise RuntimeError(f"No free port found in range {start_port}-{end_port}") + + +def run_server(port=None): """Run the mock HTTP server""" - server = HTTPServer(("0.0.0.0", port), MockHTTPHandler) - print(f"Mock HTTP server running on port {port}") - server.serve_forever() + if port is None: + port = find_free_port() + + try: + server = HTTPServer(("0.0.0.0", port), MockHTTPHandler) + print(f"Mock HTTP server running on port {port}") + + # Write port to a file that can be read by the test environment + import os + + port_file = os.environ.get("HTTP_MOCK_PORT_FILE", "/tmp/http-mock-port") + try: + with open(port_file, "w") as f: + f.write(str(port)) + except: + pass # Ignore if we can't write the port file + + server.serve_forever() + except OSError as e: + if port is not None: + # If specific port was requested but failed, try to find a free one + print(f"Port {port} not available, finding free port...") + run_server(None) + else: + raise e if __name__ == "__main__": import sys - port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + port = int(sys.argv[1]) if len(sys.argv) > 1 else None run_server(port) diff --git a/nix/tests/sql/http.sql b/nix/tests/sql/http.sql index e4efacfc3..df80feb52 100644 --- a/nix/tests/sql/http.sql +++ b/nix/tests/sql/http.sql @@ -2,13 +2,13 @@ -- Basic HTTP functionality tests -- Test basic HTTP GET request -SELECT status FROM http_get('http://localhost:8889/get'); +SELECT status FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/get'); -- Test HTTP GET with headers SELECT status, content_type FROM http(( 'GET', - 'http://localhost:8889/headers', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/headers', ARRAY[http_header('User-Agent', 'pg_http_test')], NULL, NULL @@ -16,34 +16,34 @@ FROM http(( -- Test HTTP POST request with JSON body SELECT status FROM http_post( - 'http://localhost:8889/post', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/post', '{"test": "data"}', 'application/json' ); -- Test HTTP PUT request SELECT status FROM http_put( - 'http://localhost:8889/put', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/put', '{"update": "data"}', 'application/json' ); -- Test HTTP DELETE request -SELECT status FROM http_delete('http://localhost:8889/delete'); +SELECT status FROM http_delete('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/delete'); -- Test HTTP PATCH request SELECT status FROM http_patch( - 'http://localhost:8889/patch', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/patch', '{"patch": "data"}', 'application/json' ); -- Test HTTP HEAD request -SELECT status FROM http_head('http://localhost:8889/get'); +SELECT status FROM http_head('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/get'); -- Test response headers parsing WITH response AS ( - SELECT * FROM http_get('http://localhost:8889/response-headers?Content-Type=text/plain') + SELECT * FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/response-headers?Content-Type=text/plain') ) SELECT status, @@ -55,11 +55,11 @@ FROM response; -- This should complete successfully with reasonable timeout SELECT status FROM http(( 'GET', - 'http://localhost:8889/delay/1', + 'http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/delay/1', ARRAY[]::http_header[], 'application/json', 2000 -- 2 second timeout )::http_request); -- Test URL encoding -SELECT status FROM http_get('http://localhost:8889/anything?param=value%20with%20spaces&another=123'); +SELECT status FROM http_get('http://localhost:' || (SELECT value FROM test_config WHERE key = 'http_mock_port') || '/anything?param=value%20with%20spaces&another=123'); From df1426e56fb6903047a73f390d3d2775c5194094 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 25 Sep 2025 15:29:28 -0400 Subject: [PATCH 6/6] chore: bump to release --- ansible/vars.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ansible/vars.yml b/ansible/vars.yml index e5b7f1c86..0fd92cab1 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -1,3 +1,4 @@ +--- supabase_internal: true ebssurrogate_mode: true async_mode: true @@ -5,33 +6,33 @@ async_mode: true postgres_major: - "15" - "17" - - "orioledb-17" + - orioledb-17 # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.5.1.028-orioledb-http-1" - postgres17: "17.6.1.007-http-1" - postgres15: "15.14.1.007-http-1" + postgresorioledb-17: 17.5.1.030-orioledb + postgres17: 17.6.1.009 + postgres15: 15.14.1.009 # Non Postgres Extensions -pgbouncer_release: "1.19.0" +pgbouncer_release: 1.19.0 pgbouncer_release_checksum: sha256:af0b05e97d0e1fd9ad45fe00ea6d2a934c63075f67f7e2ccef2ca59e3d8ce682 # The checksum can be found under "Assets", in the GitHub release page for each version. # The binaries used are: ubuntu-aarch64 and linux-static. # https://github.com/PostgREST/postgrest/releases -postgrest_release: "13.0.5" +postgrest_release: 13.0.5 postgrest_arm_release_checksum: sha256:7b4eafdaf76bc43b57f603109d460a838f89f949adccd02f452ca339f9a0a0d4 postgrest_x86_release_checksum: sha256:05be2bd48abee6c1691fc7c5d005023466c6989e41a4fc7d1302b8212adb88b5 gotrue_release: 2.179.0 gotrue_release_checksum: sha1:e985fce00b2720b747e6a04420910015c4967121 -aws_cli_release: "2.23.11" +aws_cli_release: 2.23.11 salt_minion_version: 3007 -golang_version: "1.22.11" +golang_version: 1.22.11 golang_version_checksum: arm64: sha256:0fc88d966d33896384fbde56e9a8d80a305dc17a9f48f1832e061724b1719991 amd64: sha256:9ebfcab26801fa4cf0627c6439db7a4da4d3c6766142a3dd83508240e4f21031 @@ -52,10 +53,10 @@ postgres_exporter_release_checksum: arm64: sha256:29ba62d538b92d39952afe12ee2e1f4401250d678ff4b354ff2752f4321c87a0 amd64: sha256:cb89fc5bf4485fb554e0d640d9684fae143a4b2d5fa443009bd29c59f9129e84 -adminapi_release: 0.92.1 -adminmgr_release: 0.32.1 +adminapi_release: "0.92.1" +adminmgr_release: "0.32.1" supabase_admin_agent_release: 1.4.38 supabase_admin_agent_splay: 30 -vector_x86_deb: "https://packages.timber.io/vector/0.48.X/vector_0.48.0-1_amd64.deb" -vector_arm_deb: "https://packages.timber.io/vector/0.48.X/vector_0.48.0-1_arm64.deb" +vector_x86_deb: https://packages.timber.io/vector/0.48.X/vector_0.48.0-1_amd64.deb +vector_arm_deb: https://packages.timber.io/vector/0.48.X/vector_0.48.0-1_arm64.deb