From 3f5893d7f9cb71160b961cbf2d8682ac1c6bb8a0 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:09:47 -0600 Subject: [PATCH 1/6] Add CI and support PHP 8.1 through 8.4 A GitHub Actions matrix builds PL/php and jsonb_plphp and runs the full regression suite on PostgreSQL 11-18 (distro PHP 8.3) plus PHP 8.1, 8.2, and 8.4 (ondrej PPA) against the newest PostgreSQL, for every push to master and every pull request. PHP compatibility fixes found while widening the matrix: - PHP 8.1: php_module_startup still took an additional-modules count; add a version guard. Also #error explicitly below 8.1, where the zend_error_cb signature differs. - PHP 8.4: fgetcsv deprecates omitting $escape; the cookbook's CSV recipe now passes explicit arguments. - The Makefiles now prefer the libphp matching PHP_CONFIG's version when several are installed. Note (INSTALL): some packagings give every libphpX.Y.so the same SONAME (libphp.so), so the backend loads whatever that symlink points at regardless of the version linked -- keep one embed package installed, or point the symlink. The suite passes on PHP 8.1, 8.2, 8.3, and 8.4 (each with the matching runtime); documented range updated from "8.x" to 8.1-8.4. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 13 ++++++ INSTALL | 6 +++ Makefile | 8 +++- README.md | 4 +- doc/cookbook.md | 2 +- doc/plphp.md | 4 +- expected/cookbook.out | 2 +- jsonb_plphp/Makefile | 8 +++- plphp.c | 10 +++++ sql/cookbook.sql | 2 +- 11 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4de1cbb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + name: PG ${{ matrix.pg }} / PHP ${{ matrix.php }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + # Every supported PostgreSQL against the distro PHP... + pg: [11, 12, 13, 14, 15, 16, 17, 18] + php: ["8.3"] + # ...plus the other supported PHP versions against the newest PG. + include: + - { pg: 18, php: "8.1" } + - { pg: 18, php: "8.2" } + - { pg: 18, php: "8.4" } + + steps: + - uses: actions/checkout@v4 + + - name: Add the PGDG apt repository + run: | + sudo install -d /usr/share/postgresql-common/pgdg + sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + + - name: Add the ondrej/php PPA (non-distro PHP versions) + if: matrix.php != '8.3' + run: sudo add-apt-repository -y ppa:ondrej/php + + - name: Install PostgreSQL ${{ matrix.pg }} and PHP ${{ matrix.php }} + run: | + sudo apt-get update + sudo apt-get install -y \ + postgresql-${{ matrix.pg }} postgresql-server-dev-${{ matrix.pg }} \ + php${{ matrix.php }}-dev libphp${{ matrix.php }}-embed php${{ matrix.php }}-xml + php-config${{ matrix.php }} --php-sapis | grep -q embed + + - name: Build and install PL/php + run: | + make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} + sudo make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} install + + - name: Build and install jsonb_plphp + run: | + make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} + sudo make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \ + PHP_CONFIG=php-config${{ matrix.php }} install + + - name: Start the cluster + run: | + sudo pg_ctlcluster ${{ matrix.pg }} main start + pg_lsclusters + + - name: Run the regression tests + run: | + port=$(pg_lsclusters | awk '$1 == "${{ matrix.pg }}" && $2 == "main" { print $3 }') + chmod -R a+rwX . + sudo -u postgres env PGPORT=$port \ + make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck + + - name: Run the jsonb_plphp regression tests + run: | + port=$(pg_lsclusters | awk '$1 == "${{ matrix.pg }}" && $2 == "main" { print $3 }') + sudo -u postgres env PGPORT=$port \ + make -C jsonb_plphp \ + PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck + + - name: Show regression diffs on failure + if: failure() + run: | + cat regression.diffs 2>/dev/null || true + cat jsonb_plphp/regression.diffs 2>/dev/null || true diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc7d5c..6c72670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,24 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). ### Added +- **Continuous integration** — a GitHub Actions matrix building and running + the full regression suite (core + `jsonb_plphp`) on PostgreSQL 11-18 and + PHP 8.1/8.2/8.3/8.4 for every pull request. - **VARIADIC parameters.** A variadic function's collected arguments arrive as a single PHP array, matching every other PL (`VARIADIC "any"` remains unsupported and is rejected with a clear error). Previously any VARIADIC declaration failed. +### Fixed + +- **PHP 8.1, 8.2, and 8.4 compatibility.** PL/php now builds and passes the + full suite on PHP 8.1 through 8.4: a version guard for the + `php_module_startup` signature change (8.2), explicit `fgetcsv` arguments + in the cookbook (8.4 deprecation), and a clear compile error below 8.1. + The Makefiles now prefer the `libphp` matching `PHP_CONFIG`'s version when + several are installed (see INSTALL for a packaging caveat about shared + SONAMEs). + ## [2.2.0] — 2026-07-05 ### Added diff --git a/INSTALL b/INSTALL index 7848e4f..06565ab 100644 --- a/INSTALL +++ b/INSTALL @@ -41,6 +41,12 @@ from PATH. Override them if needed: make PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config PHP_CONFIG=/usr/bin/php-config sudo make PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config install +Note: if several libphpX.Y-embed versions are installed side by side, some +packagings (e.g. deb.sury.org) give them all the same SONAME (libphp.so), so +the backend loads whatever /usr/lib/libphp.so points at -- regardless of the +version linked at build time. Keep a single embed package installed, or +point that symlink at the intended version. + This installs the shared library plphp.so plus the extension control and SQL files into the directories reported by pg_config. diff --git a/Makefile b/Makefile index 7c414b8..17c2122 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,13 @@ DATA = plphp--2.2.sql plphp--2.0--2.1.sql plphp--2.1--2.2.sql PHP_CONFIG ?= php-config PHP_INCLUDES := $(shell $(PHP_CONFIG) --includes) PHP_LIBDIR := $(shell $(PHP_CONFIG) --prefix)/lib -PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) +# Prefer the library matching PHP_CONFIG's version (e.g. libphp8.3.so); with +# several PHP versions installed, a bare "libphp*.so" glob would pick the +# unversioned symlink of whichever version owns it. +PHP_VERSION := $(shell $(PHP_CONFIG) --version | cut -d. -f1-2) +PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard \ + $(PHP_LIBDIR)/libphp$(PHP_VERSION).so /usr/lib/libphp$(PHP_VERSION).so /usr/lib/*/libphp$(PHP_VERSION).so \ + $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) PG_CPPFLAGS = $(PHP_INCLUDES) # Link against the PHP embed library only; its own transitive dependencies diff --git a/README.md b/README.md index 4ed4a16..6e29c2c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Write PostgreSQL functions, triggers, and procedures in PHP.** [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-11_to_18-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/) -[![PHP](https://img.shields.io/badge/PHP-8.x-777BB4?logo=php&logoColor=white)](https://www.php.net/) +[![PHP](https://img.shields.io/badge/PHP-8.1--8.4-777BB4?logo=php&logoColor=white)](https://www.php.net/) [![Tests](https://img.shields.io/badge/tests-17_passing-brightgreen)](sql/) [![License](https://img.shields.io/badge/license-permissive-blue)](#license) @@ -99,7 +99,7 @@ $$; - **PostgreSQL 11 or newer** (tested on 11–18; 18 recommended), with the server development files that provide `pg_config`. -- **PHP 8.x** built with the **embed SAPI** and **without** ZTS (thread safety). +- **PHP 8.1 through 8.4** built with the **embed SAPI** and **without** ZTS (thread safety). On Debian/Ubuntu, install `php8.x-dev` and `libphp8.x-embed`. ## Installation diff --git a/doc/cookbook.md b/doc/cookbook.md index 3f472e0..07e94e9 100644 --- a/doc/cookbook.md +++ b/doc/cookbook.md @@ -216,7 +216,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return; diff --git a/doc/plphp.md b/doc/plphp.md index 7a5d51f..a6acb90 100644 --- a/doc/plphp.md +++ b/doc/plphp.md @@ -4,8 +4,8 @@ PL/php lets you write PostgreSQL functions and triggers in PHP. This document describes the programming interface. For build and install instructions see [`INSTALL`](../INSTALL); for a feature summary see [`README`](../README.md). -Tested on **PostgreSQL 11 through 18** with **PHP 8.3** (embed SAPI, -non-thread-safe). +Tested on **PostgreSQL 11 through 18** with **PHP 8.1 through 8.4** (embed +SAPI, non-thread-safe). - [Enabling the language](#enabling-the-language) - [Writing functions](#writing-functions) diff --git a/expected/cookbook.out b/expected/cookbook.out index 421597d..b43f0b8 100644 --- a/expected/cookbook.out +++ b/expected/cookbook.out @@ -216,7 +216,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return; diff --git a/jsonb_plphp/Makefile b/jsonb_plphp/Makefile index d783b78..4192c0a 100644 --- a/jsonb_plphp/Makefile +++ b/jsonb_plphp/Makefile @@ -16,7 +16,13 @@ DATA = jsonb_plphp--1.0.sql PHP_CONFIG ?= php-config PHP_INCLUDES := $(shell $(PHP_CONFIG) --includes) PHP_LIBDIR := $(shell $(PHP_CONFIG) --prefix)/lib -PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) +# Prefer the library matching PHP_CONFIG's version (e.g. libphp8.3.so); with +# several PHP versions installed, a bare "libphp*.so" glob would pick the +# unversioned symlink of whichever version owns it. +PHP_VERSION := $(shell $(PHP_CONFIG) --version | cut -d. -f1-2) +PHP_LIBNAME := $(patsubst lib%.so,%,$(notdir $(firstword $(wildcard \ + $(PHP_LIBDIR)/libphp$(PHP_VERSION).so /usr/lib/libphp$(PHP_VERSION).so /usr/lib/*/libphp$(PHP_VERSION).so \ + $(PHP_LIBDIR)/libphp*.so /usr/lib/libphp*.so /usr/lib/*/libphp*.so)))) PG_CPPFLAGS = $(PHP_INCLUDES) SHLIB_LINK = -L$(PHP_LIBDIR) -l$(PHP_LIBNAME) $(shell $(PHP_CONFIG) --ldflags) diff --git a/plphp.c b/plphp.c index 010d04c..7854809 100644 --- a/plphp.c +++ b/plphp.c @@ -76,6 +76,10 @@ /* PHP stuff */ #include "php.h" +#if PHP_VERSION_ID < 80100 +#error "PL/php requires PHP 8.1 or newer (zend_error_cb signature)" +#endif + #include "php_variables.h" #include "php_globals.h" #include "zend_hash.h" @@ -450,8 +454,14 @@ plphp_init(void) plphp_sapi_module.phpinfo_as_text = 1; sapi_startup(&plphp_sapi_module); +#if PHP_VERSION_ID >= 80200 if (php_module_startup(&plphp_sapi_module, NULL) == FAILURE) elog(ERROR, "php_module_startup call failed"); +#else + /* PHP 8.1 still took an additional-modules count */ + if (php_module_startup(&plphp_sapi_module, NULL, 0) == FAILURE) + elog(ERROR, "php_module_startup call failed"); +#endif /* php_module_startup changed it, so put it back */ zend_error_cb = plphp_error_cb; diff --git a/sql/cookbook.sql b/sql/cookbook.sql index f13ced7..899126d 100644 --- a/sql/cookbook.sql +++ b/sql/cookbook.sql @@ -156,7 +156,7 @@ LANGUAGE plphp STRICT AS $$ $fh = fopen($args[0], "r"); if ($fh === false) pg_raise('error', "cannot open {$args[0]}"); - while (($rec = fgetcsv($fh)) !== false) + while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false) return_next($rec); fclose($fh); return; From 0775704515f0d9a88cae4a6eb5691661441e4252 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:11:56 -0600 Subject: [PATCH 2/6] CI: create the cluster explicitly The Actions runner image sets create_main_cluster=false, so installing postgresql-N does not create one. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4de1cbb..cb67cf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,8 @@ jobs: - name: Start the cluster run: | + # the runner image disables automatic cluster creation + sudo pg_createcluster ${{ matrix.pg }} main 2>/dev/null || true sudo pg_ctlcluster ${{ matrix.pg }} main start pg_lsclusters From 14ca129a85a3147594c7729abd791d5ef0d4fb34 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:13:46 -0600 Subject: [PATCH 3/6] CI: let the postgres user traverse the workspace path pg_regress runs as postgres, which could not reach the checkout through /home/runner. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb67cf5..9e0a135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ jobs: - name: Run the regression tests run: | port=$(pg_lsclusters | awk '$1 == "${{ matrix.pg }}" && $2 == "main" { print $3 }') + # let the postgres user reach and write the workspace + sudo chmod a+x /home/runner /home/runner/work /home/runner/work/PL-php chmod -R a+rwX . sudo -u postgres env PGPORT=$port \ make PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck From 126629a3e95ea78380764aebe338b3c6ec92eea8 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:18:01 -0600 Subject: [PATCH 4/6] Force the PHP error/opcache settings PL/php depends on The host's php.ini must not decide which errors reach our zend_error_cb or how they are routed: hardcode error_reporting=E_ALL, display_errors/log_errors off, and disable opcache (whose JIT adds nothing for our one-shot eval'd functions). GitHub's runner images ship a PHP with opcache+JIT enabled for CLI, which silently dropped notices and reduced every error message to "fatal error". Co-Authored-By: Claude Fable 5 --- plphp.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plphp.c b/plphp.c index 7854809..0b4b5b6 100644 --- a/plphp.c +++ b/plphp.c @@ -486,6 +486,21 @@ plphp_init(void) INI_HARDCODED("max_execution_time", "0"); INI_HARDCODED("max_input_time", "-1"); + /* + * The host's php.ini must not decide which errors reach our + * zend_error_cb, or how they are routed: force full reporting + * and disable opcache/JIT, which add nothing for our one-shot + * eval'd functions and can interfere with error delivery. + * (GitHub's runner images, for example, ship a PHP configured + * with opcache+JIT enabled for CLI, which broke both notices + * and error messages.) + */ + INI_HARDCODED("error_reporting", "32767"); /* E_ALL */ + INI_HARDCODED("display_errors", "0"); + INI_HARDCODED("log_errors", "0"); + INI_HARDCODED("opcache.enable", "0"); + INI_HARDCODED("opcache.enable_cli", "0"); + /* * Set memory limit to ridiculously high value. This helps the * server not to crash, because the PHP allocator has the really From f0c7d8d2244ea7ba6bdf7bd7ea95903ef4511652 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:21:52 -0600 Subject: [PATCH 5/6] Keep zend_error_cb ours whatever extensions do Extensions loaded from the host's php.ini (xdebug, notably, which the GitHub runner images preconfigure) hook zend_error_cb during startup, swallowing the warnings and notices PL/php forwards to the client. Re-assert our callback after request startup and at the top of every handler invocation (a pointer assignment, effectively free), and set xdebug.mode=off alongside the other hardcoded INI values. Co-Authored-By: Claude Fable 5 --- plphp.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plphp.c b/plphp.c index 0b4b5b6..c74eb30 100644 --- a/plphp.c +++ b/plphp.c @@ -417,9 +417,11 @@ plphp_init_all(void) plphp_init(); /* - * Any other initialization that must be done each time a new - * backend starts -- currently none. + * Loaded PHP extensions can swap zend_error_cb behind our back at any + * point; put ours back before running anything. It is a plain pointer + * assignment, so doing it on every call is free. */ + zend_error_cb = plphp_error_cb; } /* @@ -500,6 +502,7 @@ plphp_init(void) INI_HARDCODED("log_errors", "0"); INI_HARDCODED("opcache.enable", "0"); INI_HARDCODED("opcache.enable_cli", "0"); + INI_HARDCODED("xdebug.mode", "off"); /* * Set memory limit to ridiculously high value. This helps the @@ -529,6 +532,13 @@ plphp_init(void) PG(during_request_startup) = true; + /* + * Extensions loaded from the host's php.ini (xdebug, for one) + * may have hooked zend_error_cb during module or request + * startup; ours must be the one in place. + */ + zend_error_cb = plphp_error_cb; + /* * Register our SPI functions and the procedure cache now that the * request (and its interned-string tables) is fully initialized. From 9c6100af464ea0931d2b7054c042a1fcb976a965 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:14:13 -0600 Subject: [PATCH 6/6] Convert array columns inside rows to PHP arrays An array-typed column used to cross into PHP as its literal text form ("{a,b}") everywhere a whole row converts: $_TD['new']/['old'] in triggers, rows from spi_fetch_row/spi_fetchrow/spi_each, and the fields of composite-type arguments. Route those column values through the same conversion arguments already get, so they arrive as real PHP arrays -- and, since the reverse path (plphp_zval_get_cstring with do_array) already renders PHP arrays back into array literals, assigning an array works too, e.g. modifying $_TD['new']['tags'] before returning MODIFY. This matches PL/Perl, which converts row fields recursively. It is a behavior change for code that string-parsed the literal form, noted in the CHANGELOG. Composite-typed columns inside rows still arrive as text (converting those needs a record parser; possible follow-up). New cases in the arrays test cover SPI rows, cursor rows, triggers (read + append + MODIFY round trip), and composite argument fields. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 9 +++++++ doc/plphp.md | 5 ++++ expected/arrays.out | 59 +++++++++++++++++++++++++++++++++++++++++++++ plphp_io.c | 25 ++++++++++++++++--- sql/arrays.sql | 43 +++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c72670..fec3bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,15 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). unsupported and is rejected with a clear error). Previously any VARIADIC declaration failed. +### Changed + +- **Array columns inside rows are PHP arrays now.** An array-typed column in + `$_TD['new']`/`['old']`, in rows from `spi_fetch_row`/`spi_fetchrow`, or in + a composite argument's fields used to arrive as its literal text form + (`{a,b}`); it now converts to a PHP array, and converts back when assigned + (e.g. trigger `MODIFY`). Code that string-parsed those values should use + the array directly. + ### Fixed - **PHP 8.1, 8.2, and 8.4 compatibility.** PL/php now builds and passes the diff --git a/doc/plphp.md b/doc/plphp.md index a6acb90..bd0a537 100644 --- a/doc/plphp.md +++ b/doc/plphp.md @@ -87,6 +87,11 @@ function. In practice: | boolean | `"t"` / `"f"` string | `true` / `false`, or `"t"`/`"f"` | | arrays (e.g. `int[]`) | PHP array | PHP array | | composite / row / record | associative array | associative array | + +Array-typed *columns* inside rows — in `$_TD['new']`/`['old']`, rows from +`spi_fetch_row`/`spi_fetchrow`, and composite arguments' fields — also arrive +as PHP arrays, and can be assigned back as arrays (e.g. before a trigger +`MODIFY`). | NULL | unset / null | `return;` or `null` | Arrays map naturally, including multidimensional arrays: diff --git a/expected/arrays.out b/expected/arrays.out index 9d46bd4..7d00b9e 100644 --- a/expected/arrays.out +++ b/expected/arrays.out @@ -136,3 +136,62 @@ select bytea_read('\xdeadbeef'); DEADBEEF (1 row) +-- array-typed columns inside rows arrive as PHP arrays (not "{...}" strings): +-- via SPI rows... +create table arrt (id int, tags text[], nums int[]); +insert into arrt values (1, array['red', 'b"lue'], array[10, 20, 30]); +create function arr_in_row() returns text language plphp as $$ + $r = spi_exec("select * from arrt"); + $row = spi_fetch_row($r); + return gettype($row['tags']) . " " . $row['tags'][1] . " " . array_sum($row['nums']); +$$; +select arr_in_row(); + arr_in_row +---------------- + array b"lue 60 +(1 row) + +-- ...via cursor rows... +create function arr_in_cursor() returns int language plphp as $$ + $c = spi_query("select nums from arrt"); + $row = spi_fetchrow($c); + return count($row['nums']); +$$; +select arr_in_cursor(); + arr_in_cursor +--------------- + 3 +(1 row) + +-- ...in $_TD for triggers, and writable back through MODIFY +create function arr_trig() returns trigger language plphp as $$ + pg_raise('notice', 'tags is ' . gettype($_TD['new']['tags']) + . ' with ' . count($_TD['new']['tags']) . ' elements'); + $_TD['new']['tags'][] = 'added'; + return 'MODIFY'; +$$; +create trigger arrt_trg before insert on arrt + for each row execute procedure arr_trig(); +insert into arrt values (2, array['green'], array[1]); +NOTICE: plphp: tags is array with 1 elements +select tags from arrt where id = 2; + tags +--------------- + {green,added} +(1 row) + +drop trigger arrt_trg on arrt; +-- ...and in composite-type arguments +create type with_arr as (label text, vals int[]); +create function arr_in_comp(with_arr) returns int language plphp as $$ + return array_sum($args[0]['vals']); +$$; +select arr_in_comp(row('x', array[5, 6, 7])::with_arr); + arr_in_comp +------------- + 18 +(1 row) + +drop function arr_in_row(), arr_in_cursor(), arr_in_comp(with_arr), arr_trig(); +drop type with_arr; +drop table arrt; diff --git a/plphp_io.c b/plphp_io.c index bf64a65..3d56797 100644 --- a/plphp_io.c +++ b/plphp_io.c @@ -22,6 +22,26 @@ #include "utils/syscache.h" #include "utils/memutils.h" +/* + * plphp_add_row_value + * Add one column's text value to a row hash under attname, converting + * array-typed columns to PHP arrays (like the argument conversion + * does) rather than leaving them as literal "{...}" strings. + */ +static void +plphp_add_row_value(zval *row, char *attname, Oid atttypid, char *value) +{ + if (OidIsValid(get_element_type(atttypid))) + { + zval *arr = plphp_convert_from_pg_array(value); + + add_assoc_zval(row, attname, arr); + efree(arr); + } + else + add_assoc_string(row, attname, value); +} + /* * plphp_zval_from_tuple * Build a PHP hash from a tuple. @@ -46,8 +66,7 @@ plphp_zval_from_tuple(HeapTuple tuple, TupleDesc tupdesc) /* get its value */ if ((attdata = SPI_getvalue(tuple, tupdesc, i + 1)) != NULL) { - /* add_assoc_string copies the string in PHP 7+ */ - add_assoc_string(array, attname, attdata); + plphp_add_row_value(array, attname, att->atttypid, attdata); pfree(attdata); } else @@ -573,7 +592,7 @@ plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc) /* Append the attribute name and the value to the list. */ outputstr = OidOutputFunctionCall(typoutput, attr_datum); - add_assoc_string(output, attname, outputstr); + plphp_add_row_value(output, attname, att->atttypid, outputstr); pfree(outputstr); } diff --git a/sql/arrays.sql b/sql/arrays.sql index 494506c..a551b12 100644 --- a/sql/arrays.sql +++ b/sql/arrays.sql @@ -81,3 +81,46 @@ create function bytea_read(bytea) returns text language plphp as $$ return strtoupper(substr($args[0], 2)); $$; select bytea_read('\xdeadbeef'); + +-- array-typed columns inside rows arrive as PHP arrays (not "{...}" strings): +-- via SPI rows... +create table arrt (id int, tags text[], nums int[]); +insert into arrt values (1, array['red', 'b"lue'], array[10, 20, 30]); +create function arr_in_row() returns text language plphp as $$ + $r = spi_exec("select * from arrt"); + $row = spi_fetch_row($r); + return gettype($row['tags']) . " " . $row['tags'][1] . " " . array_sum($row['nums']); +$$; +select arr_in_row(); + +-- ...via cursor rows... +create function arr_in_cursor() returns int language plphp as $$ + $c = spi_query("select nums from arrt"); + $row = spi_fetchrow($c); + return count($row['nums']); +$$; +select arr_in_cursor(); + +-- ...in $_TD for triggers, and writable back through MODIFY +create function arr_trig() returns trigger language plphp as $$ + pg_raise('notice', 'tags is ' . gettype($_TD['new']['tags']) + . ' with ' . count($_TD['new']['tags']) . ' elements'); + $_TD['new']['tags'][] = 'added'; + return 'MODIFY'; +$$; +create trigger arrt_trg before insert on arrt + for each row execute procedure arr_trig(); +insert into arrt values (2, array['green'], array[1]); +select tags from arrt where id = 2; +drop trigger arrt_trg on arrt; + +-- ...and in composite-type arguments +create type with_arr as (label text, vals int[]); +create function arr_in_comp(with_arr) returns int language plphp as $$ + return array_sum($args[0]['vals']); +$$; +select arr_in_comp(row('x', array[5, 6, 7])::with_arr); + +drop function arr_in_row(), arr_in_cursor(), arr_in_comp(with_arr), arr_trig(); +drop type with_arr; +drop table arrt;