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] 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;