From ce0e8b5506d525a23f26205ba575af784b749c1f Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:51:30 -0600 Subject: [PATCH] Apply jsonb/hstore transforms in nested contexts (triggers, composites, SETOF) Previously a declared TRANSFORM FOR TYPE applied only to a function's top-level scalar arguments and result. Now it also converts values of that type nested inside rows, in both directions: - FROM SQL: a trigger's $_TD['new']/['old'] columns and composite argument fields arrive as native PHP values (e.g. jsonb/hstore as PHP arrays). - TO SQL: composite/record result fields, RETURNS SETOF / return_next rows, RETURNS TABLE columns, and the 'MODIFY' trigger return are built from native PHP values. Mechanism: the function's declared transform types (protrftypes) are stored on the compiled descriptor for every function (triggers included) and threaded into the tuple builders as an explicit plphp_trans_ctx. FROM-side columns are converted by calling the FromSQL transform directly; TO-side columns run the ToSQL transform to a Datum and render it to text for the existing BuildTupleFromCStrings path (no tuple-mechanism rewrite). Rows read back through SPI are passed a NULL context and stay in text form, matching PL/Ruby. Verified on PostgreSQL 18: new nested-context cases in both hstore_plphp and jsonb_plphp, plus the full plphp suite -- all green (25 / 1 / 1). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 ++ doc/plphp.md | 15 ++++ hstore_plphp/expected/hstore_plphp.out | 102 ++++++++++++++++++++++- hstore_plphp/sql/hstore_plphp.sql | 67 +++++++++++++++ jsonb_plphp/expected/jsonb_plphp.out | 39 ++++++++- jsonb_plphp/sql/jsonb_plphp.sql | 29 ++++++- plphp.c | 89 ++++++++++++-------- plphp_io.c | 108 ++++++++++++++++++++++--- plphp_io.h | 28 ++++++- plphp_spi.c | 5 +- plphp_spi.h | 4 + 11 files changed, 438 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e904aa..784b5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). adds `TRANSFORM FOR TYPE hstore`, mapping an `hstore` to a PHP associative array of string keys to string-or-null values and back (a PHP `null` value becomes an hstore `NULL`). Companion to the existing `jsonb_plphp`. +- **Transforms reach nested contexts.** A declared `jsonb`/`hstore` transform + now also applies to values of that type inside composite argument/result + fields, `RETURNS SETOF`/`return_next` rows, and a trigger's `$_TD` rows + (including the `'MODIFY'` return) — not only as top-level arguments and + results. Rows read back through SPI are still left in text form. - **Structured `pg_raise`.** `pg_raise(level, message [, detail [, hint [, sqlstate]]])` now attaches `DETAIL`, `HINT` and (for `ERROR`) a custom `SQLSTATE`, mirroring PL/pgSQL's `RAISE ... USING`. The fields are readable diff --git a/doc/plphp.md b/doc/plphp.md index 9a5879d..f24e4d6 100644 --- a/doc/plphp.md +++ b/doc/plphp.md @@ -165,6 +165,21 @@ SELECT tags_upper('a=>x, b=>NULL'); -- "A"=>"X", "B"=>NULL Returning something other than an array, or a nested array as a value, is rejected with a clear error (hstore values are text or null). +### Where transforms apply + +A declared transform (`jsonb` or `hstore`) is not limited to top-level +arguments and results. For a function that declares it, the transform also +converts values of that type **inside**: + +- a composite argument's fields, and a composite/record result's fields; +- `RETURNS SETOF` / `RETURNS TABLE` rows emitted with `return_next`; +- a trigger's `$_TD['new']` and `$_TD['old']` columns (and the row returned by + `'MODIFY'`). + +Rows read back through SPI (`spi_exec` / `spi_fetch_row` / cursors) are **not** +transformed — like a value crossing the boundary without a `TRANSFORM` clause, +they arrive in their text form. + ### Domains A domain is handled as its underlying base type: a scalar domain behaves like diff --git a/hstore_plphp/expected/hstore_plphp.out b/hstore_plphp/expected/hstore_plphp.out index bb3b748..00fc879 100644 --- a/hstore_plphp/expected/hstore_plphp.out +++ b/hstore_plphp/expected/hstore_plphp.out @@ -119,8 +119,102 @@ SELECT ht_untransformed('a=>1'::hstore); string (1 row) +-- +-- The transform reaches nested contexts, in both directions: trigger rows, +-- composite arguments and results, and set-returning results. +-- +-- Trigger: $_TD['new'][''] is a PHP array, and 'MODIFY' takes one +-- back. +CREATE TABLE ht_items (id int, attrs hstore); +CREATE FUNCTION ht_normalize() RETURNS trigger +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $a = $_TD['new']['attrs']; + elog('NOTICE', 'attrs is a ' . gettype($a)); + $out = array('checked' => 'yes'); + foreach ($a as $k => $v) $out[strtolower($k)] = $v; + $_TD['new']['attrs'] = $out; + return 'MODIFY'; +$$; +CREATE TRIGGER ht_items_norm BEFORE INSERT ON ht_items + FOR EACH ROW EXECUTE PROCEDURE ht_normalize(); +INSERT INTO ht_items VALUES (1, 'Color=>red, SIZE=>xl'); +NOTICE: attrs is a array +SELECT id, attrs FROM ht_items; + id | attrs +----+------------------------------------------------ + 1 | "size"=>"xl", "color"=>"red", "checked"=>"yes" +(1 row) + +DROP TABLE ht_items; +-- Composite argument with an hstore field arrives as a PHP array. +CREATE TYPE ht_rec AS (id int, meta hstore); +CREATE FUNCTION ht_field(ht_rec) RETURNS text +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $m = $args[0]['meta']; + return is_array($m) ? ($m['k'] ?? '?') : "not-array"; +$$; +SELECT ht_field(ROW(1, 'k=>v')::ht_rec); + ht_field +---------- + v +(1 row) + +-- Composite result with an hstore field is built from a PHP array. +CREATE FUNCTION ht_make(int) RETURNS ht_rec +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + return array('id' => $args[0], 'meta' => array('n' => $args[0], 'ok' => 'y')); +$$; +SELECT * FROM ht_make(7); + id | meta +----+--------------------- + 7 | "n"=>"7", "ok"=>"y" +(1 row) + +-- SETOF hstore via return_next: each row is a PHP array. +CREATE FUNCTION ht_shatter(hstore) RETURNS SETOF hstore +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $h = $args[0]; + ksort($h); + foreach ($h as $k => $v) return_next(array($k => $v)); + return; +$$; +SELECT * FROM ht_shatter('b=>2, a=>1'); + ht_shatter +------------ + "a"=>"1" + "b"=>"2" +(2 rows) + +-- RETURNS TABLE with an hstore column. +CREATE FUNCTION ht_table(int) RETURNS TABLE(n int, tags hstore) +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + for ($i = 1; $i <= $args[0]; $i++) { $n = $i; $tags = array('sq' => $i * $i); return_next(); } + return; +$$; +SELECT * FROM ht_table(3); + n | tags +---+----------- + 1 | "sq"=>"1" + 2 | "sq"=>"4" + 3 | "sq"=>"9" +(3 rows) + +-- Rows read back from SPI are NOT transformed (they cross as text), matching +-- the top-level "no TRANSFORM" behavior. +CREATE FUNCTION ht_via_spi() RETURNS text +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $r = spi_exec("select 'x=>1'::hstore as h"); + $row = spi_fetch_row($r); + return gettype($row['h']); +$$; +SELECT ht_via_spi(); + ht_via_spi +------------ + string +(1 row) + DROP EXTENSION hstore_plphp CASCADE; -NOTICE: drop cascades to 7 other objects +NOTICE: drop cascades to 13 other objects DETAIL: drop cascades to function ht_classes(hstore) drop cascades to function ht_upcase(hstore) drop cascades to function ht_build(integer) @@ -128,3 +222,9 @@ drop cascades to function ht_merge(hstore,hstore) drop cascades to function ht_empty(hstore) drop cascades to function ht_bad() drop cascades to function ht_nested() +drop cascades to function ht_normalize() +drop cascades to function ht_field(ht_rec) +drop cascades to function ht_make(integer) +drop cascades to function ht_shatter(hstore) +drop cascades to function ht_table(integer) +drop cascades to function ht_via_spi() diff --git a/hstore_plphp/sql/hstore_plphp.sql b/hstore_plphp/sql/hstore_plphp.sql index 4298270..6bd60b6 100644 --- a/hstore_plphp/sql/hstore_plphp.sql +++ b/hstore_plphp/sql/hstore_plphp.sql @@ -81,4 +81,71 @@ CREATE FUNCTION ht_untransformed(hstore) RETURNS text LANGUAGE plphp AS $$ $$; SELECT ht_untransformed('a=>1'::hstore); +-- +-- The transform reaches nested contexts, in both directions: trigger rows, +-- composite arguments and results, and set-returning results. +-- + +-- Trigger: $_TD['new'][''] is a PHP array, and 'MODIFY' takes one +-- back. +CREATE TABLE ht_items (id int, attrs hstore); +CREATE FUNCTION ht_normalize() RETURNS trigger +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $a = $_TD['new']['attrs']; + elog('NOTICE', 'attrs is a ' . gettype($a)); + $out = array('checked' => 'yes'); + foreach ($a as $k => $v) $out[strtolower($k)] = $v; + $_TD['new']['attrs'] = $out; + return 'MODIFY'; +$$; +CREATE TRIGGER ht_items_norm BEFORE INSERT ON ht_items + FOR EACH ROW EXECUTE PROCEDURE ht_normalize(); +INSERT INTO ht_items VALUES (1, 'Color=>red, SIZE=>xl'); +SELECT id, attrs FROM ht_items; +DROP TABLE ht_items; + +-- Composite argument with an hstore field arrives as a PHP array. +CREATE TYPE ht_rec AS (id int, meta hstore); +CREATE FUNCTION ht_field(ht_rec) RETURNS text +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $m = $args[0]['meta']; + return is_array($m) ? ($m['k'] ?? '?') : "not-array"; +$$; +SELECT ht_field(ROW(1, 'k=>v')::ht_rec); + +-- Composite result with an hstore field is built from a PHP array. +CREATE FUNCTION ht_make(int) RETURNS ht_rec +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + return array('id' => $args[0], 'meta' => array('n' => $args[0], 'ok' => 'y')); +$$; +SELECT * FROM ht_make(7); + +-- SETOF hstore via return_next: each row is a PHP array. +CREATE FUNCTION ht_shatter(hstore) RETURNS SETOF hstore +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $h = $args[0]; + ksort($h); + foreach ($h as $k => $v) return_next(array($k => $v)); + return; +$$; +SELECT * FROM ht_shatter('b=>2, a=>1'); + +-- RETURNS TABLE with an hstore column. +CREATE FUNCTION ht_table(int) RETURNS TABLE(n int, tags hstore) +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + for ($i = 1; $i <= $args[0]; $i++) { $n = $i; $tags = array('sq' => $i * $i); return_next(); } + return; +$$; +SELECT * FROM ht_table(3); + +-- Rows read back from SPI are NOT transformed (they cross as text), matching +-- the top-level "no TRANSFORM" behavior. +CREATE FUNCTION ht_via_spi() RETURNS text +TRANSFORM FOR TYPE hstore LANGUAGE plphp AS $$ + $r = spi_exec("select 'x=>1'::hstore as h"); + $row = spi_fetch_row($r); + return gettype($row['h']); +$$; +SELECT ht_via_spi(); + DROP EXTENSION hstore_plphp CASCADE; diff --git a/jsonb_plphp/expected/jsonb_plphp.out b/jsonb_plphp/expected/jsonb_plphp.out index 051993e..1fb7823 100644 --- a/jsonb_plphp/expected/jsonb_plphp.out +++ b/jsonb_plphp/expected/jsonb_plphp.out @@ -160,5 +160,42 @@ SELECT badret(); ERROR: cannot transform this PHP value to jsonb DETAIL: PHP type 8 has no jsonb representation. CONTEXT: PL/php function "badret" +-- The transform reaches nested contexts (as for hstore): a trigger sees a +-- jsonb column as a PHP value and can 'MODIFY' it, and a SETOF jsonb function +-- emits native values row by row. +CREATE TABLE jb_events (id int, payload jsonb); +CREATE FUNCTION jb_tag() RETURNS trigger +LANGUAGE plphp TRANSFORM FOR TYPE jsonb AS $$ + $p = $_TD['new']['payload']; + $p['seen'] = true; + $p['n'] = ($p['n'] ?? 0) + 1; + $_TD['new']['payload'] = $p; + return 'MODIFY'; +$$; +CREATE TRIGGER jb_tag_t BEFORE INSERT ON jb_events + FOR EACH ROW EXECUTE PROCEDURE jb_tag(); +INSERT INTO jb_events VALUES (1, '{"n": 4}'); +SELECT id, payload FROM jb_events; + id | payload +----+------------------------ + 1 | {"n": 5, "seen": true} +(1 row) + +DROP TABLE jb_events; +CREATE FUNCTION jb_series(int) RETURNS SETOF jsonb +LANGUAGE plphp TRANSFORM FOR TYPE jsonb AS $$ + for ($i = 1; $i <= $args[0]; $i++) + return_next(array('i' => $i, 'sq' => $i * $i)); + return; +$$; +SELECT * FROM jb_series(3); + jb_series +------------------- + {"i": 1, "sq": 1} + {"i": 2, "sq": 4} + {"i": 3, "sq": 9} +(3 rows) + DROP FUNCTION roundtrip(jsonb), typeinfo(jsonb), makedoc(), makenum(), - redact(jsonb, text), astext(jsonb), badret(); + redact(jsonb, text), astext(jsonb), badret(), + jb_tag(), jb_series(int); diff --git a/jsonb_plphp/sql/jsonb_plphp.sql b/jsonb_plphp/sql/jsonb_plphp.sql index abd5e8a..1c4b706 100644 --- a/jsonb_plphp/sql/jsonb_plphp.sql +++ b/jsonb_plphp/sql/jsonb_plphp.sql @@ -89,5 +89,32 @@ LANGUAGE plphp TRANSFORM FOR TYPE jsonb AS $$ $$; SELECT badret(); +-- The transform reaches nested contexts (as for hstore): a trigger sees a +-- jsonb column as a PHP value and can 'MODIFY' it, and a SETOF jsonb function +-- emits native values row by row. +CREATE TABLE jb_events (id int, payload jsonb); +CREATE FUNCTION jb_tag() RETURNS trigger +LANGUAGE plphp TRANSFORM FOR TYPE jsonb AS $$ + $p = $_TD['new']['payload']; + $p['seen'] = true; + $p['n'] = ($p['n'] ?? 0) + 1; + $_TD['new']['payload'] = $p; + return 'MODIFY'; +$$; +CREATE TRIGGER jb_tag_t BEFORE INSERT ON jb_events + FOR EACH ROW EXECUTE PROCEDURE jb_tag(); +INSERT INTO jb_events VALUES (1, '{"n": 4}'); +SELECT id, payload FROM jb_events; +DROP TABLE jb_events; + +CREATE FUNCTION jb_series(int) RETURNS SETOF jsonb +LANGUAGE plphp TRANSFORM FOR TYPE jsonb AS $$ + for ($i = 1; $i <= $args[0]; $i++) + return_next(array('i' => $i, 'sq' => $i * $i)); + return; +$$; +SELECT * FROM jb_series(3); + DROP FUNCTION roundtrip(jsonb), typeinfo(jsonb), makedoc(), makenum(), - redact(jsonb, text), astext(jsonb), badret(); + redact(jsonb, text), astext(jsonb), badret(), + jb_tag(), jb_series(int); diff --git a/plphp.c b/plphp.c index 8bd6ca6..a41123b 100644 --- a/plphp.c +++ b/plphp.c @@ -188,6 +188,8 @@ typedef struct plphp_proc_desc Oid arg_elemtype[FUNC_MAX_ARGS]; /* element type, or 0 */ Oid arg_transform[FUNC_MAX_ARGS]; /* FromSQL transform fn, or 0 */ Oid ret_transform; /* ToSQL transform fn, or 0 */ + Oid lang_oid; /* prolang, for per-column transform lookup */ + List *trftypes; /* declared TRANSFORM FOR TYPE types, or NIL */ char arg_typtype[FUNC_MAX_ARGS]; char arg_argmode[FUNC_MAX_ARGS]; TupleDesc args_out_tupdesc; @@ -1183,12 +1185,13 @@ plphp_get_function_tupdesc(Oid result_type, Node *rsinfo) * Build the $_TD array for the trigger function. */ static zval * -plphp_trig_build_args(FunctionCallInfo fcinfo) +plphp_trig_build_args(FunctionCallInfo fcinfo, plphp_proc_desc *desc) { TriggerData *tdata; TupleDesc tupdesc; zval *retval; int i; + plphp_trans_ctx tctx = {desc->lang_oid, desc->trftypes}; retval = (zval *) emalloc(sizeof(zval)); array_init(retval); @@ -1221,7 +1224,7 @@ plphp_trig_build_args(FunctionCallInfo fcinfo) { zval *hashref; - hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc); + hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc, &tctx); add_assoc_zval(retval, "new", hashref); efree(hashref); } @@ -1229,7 +1232,7 @@ plphp_trig_build_args(FunctionCallInfo fcinfo) { zval *hashref; - hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc); + hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc, &tctx); add_assoc_zval(retval, "old", hashref); efree(hashref); } @@ -1237,11 +1240,11 @@ plphp_trig_build_args(FunctionCallInfo fcinfo) { zval *hashref; - hashref = plphp_build_tuple_argument(tdata->tg_newtuple, tupdesc); + hashref = plphp_build_tuple_argument(tdata->tg_newtuple, tupdesc, &tctx); add_assoc_zval(retval, "new", hashref); efree(hashref); - hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc); + hashref = plphp_build_tuple_argument(tdata->tg_trigtuple, tupdesc, &tctx); add_assoc_zval(retval, "old", hashref); efree(hashref); } @@ -1302,7 +1305,7 @@ plphp_trigger_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) REPORT_PHP_MEMUSAGE("going to build the trigger arg"); - zTrigData = plphp_trig_build_args(fcinfo); + zTrigData = plphp_trig_build_args(fcinfo, desc); REPORT_PHP_MEMUSAGE("going to call the trigger function"); @@ -1350,7 +1353,8 @@ plphp_trigger_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event) || TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) retval = PointerGetDatum(plphp_modify_tuple(zTrigData, - trigdata)); + trigdata, + &(plphp_trans_ctx){desc->lang_oid, desc->trftypes})); else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -1577,7 +1581,8 @@ plphp_func_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) if (!td) elog(ERROR, "no TupleDesc info available"); - tup = plphp_htup_from_zval(phpret, td); + tup = plphp_htup_from_zval(phpret, td, + &(plphp_trans_ctx){desc->lang_oid, desc->trftypes}); retval = HeapTupleGetDatum(tup); ReleaseTupleDesc(td); } @@ -1672,6 +1677,10 @@ plphp_srf_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) current_tupledesc = CreateTupleDescCopy(tupdesc); current_attinmeta = TupleDescGetAttInMetadata(current_tupledesc); + /* Transform context, so return_next rows honor declared transforms */ + current_trans_ctx.lang_oid = desc->lang_oid; + current_trans_ctx.trftypes = desc->trftypes; + /* * Call the PHP function. The user code normally calls return_next, * which creates and populates the tuplestore. @@ -1696,7 +1705,8 @@ plphp_srf_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) HeapTuple tup; tup = plphp_srf_htup_from_zval(row, current_attinmeta, - current_memcxt); + current_memcxt, + &(plphp_trans_ctx){desc->lang_oid, desc->trftypes}); tuplestore_puttuple(current_tuplestore, tup); heap_freetuple(tup); } @@ -1725,6 +1735,8 @@ plphp_srf_handler(FunctionCallInfo fcinfo, plphp_proc_desc *desc) current_memcxt = NULL; current_tupledesc = NULL; current_attinmeta = NULL; + current_trans_ctx.lang_oid = InvalidOid; + current_trans_ctx.trftypes = NIL; MemoryContextSwitchTo(oldcxt); @@ -1848,6 +1860,31 @@ plphp_compile_function(Oid fnoid, bool is_trigger, bool is_event_trigger) prodesc->trusted = langStruct->lanpltrusted; ReleaseSysCache(langTup); + /* + * TRANSFORM FOR TYPE support: remember the function's declared + * transform types (copied into the descriptor's own context) so values + * of those types can be converted by their transform functions -- not + * only as top-level arguments/results, but also as columns inside + * trigger rows, composites, and set-returning results. Fetched for + * every function, triggers included. + */ + prodesc->lang_oid = procStruct->prolang; + { + Datum protrftypes_datum; + bool trfisnull; + + protrftypes_datum = SysCacheGetAttr(PROCOID, procTup, + Anum_pg_proc_protrftypes, + &trfisnull); + if (!trfisnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(fn_cxt); + + prodesc->trftypes = oid_array_to_list(protrftypes_datum); + MemoryContextSwitchTo(oldcxt); + } + } + /* * Get the required information for input conversion of the return * value, and output conversion of the procedure's arguments. @@ -1998,29 +2035,16 @@ plphp_compile_function(Oid fnoid, bool is_trigger, bool is_event_trigger) aliases = palloc((NAMEDATALEN + 32) * prodesc->n_total_args); /* - * TRANSFORM FOR TYPE support: fetch the function's declared - * transform types so arguments/results of those types can be - * converted by their transform functions (e.g. jsonb_plphp). + * Resolve the top-level argument/result transforms from the + * declared transform types fetched above. */ - { - Datum protrftypes_datum; - bool trfisnull; - List *trftypes = NIL; - - protrftypes_datum = SysCacheGetAttr(PROCOID, procTup, - Anum_pg_proc_protrftypes, - &trfisnull); - if (!trfisnull) - trftypes = oid_array_to_list(protrftypes_datum); - - prodesc->ret_transform = - get_transform_tosql(procStruct->prorettype, - procStruct->prolang, trftypes); - for (i = 0; i < prodesc->n_total_args; i++) - prodesc->arg_transform[i] = - get_transform_fromsql(argtypes[i], - procStruct->prolang, trftypes); - } + prodesc->ret_transform = + get_transform_tosql(procStruct->prorettype, + procStruct->prolang, prodesc->trftypes); + for (i = 0; i < prodesc->n_total_args; i++) + prodesc->arg_transform[i] = + get_transform_fromsql(argtypes[i], + procStruct->prolang, prodesc->trftypes); /* Main argument processing loop. */ for (i = 0; i < prodesc->n_total_args; i++) @@ -2224,6 +2248,7 @@ plphp_func_build_args(plphp_proc_desc *desc, FunctionCallInfo fcinfo) { zval *retval; int i,j; + plphp_trans_ctx tctx = {desc->lang_oid, desc->trftypes}; retval = (zval *) emalloc(sizeof(zval)); array_init(retval); @@ -2284,7 +2309,7 @@ plphp_func_build_args(plphp_proc_desc *desc, FunctionCallInfo fcinfo) tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); /* Build the PHP hash */ - hashref = plphp_build_tuple_argument(&tmptup, tupdesc); + hashref = plphp_build_tuple_argument(&tmptup, tupdesc, &tctx); add_next_index_zval(retval, hashref); efree(hashref); /* Finally release the acquired tupledesc */ diff --git a/plphp_io.c b/plphp_io.c index 52eb60e..27c28c2 100644 --- a/plphp_io.c +++ b/plphp_io.c @@ -29,6 +29,57 @@ static const char *plphp_parse_record_body(const char *p, TupleDesc tupdesc, zval *row); static char *plphp_build_record_literal(zval *val, TupleDesc tupdesc); +/* + * plphp_col_transform + * Return the From/To SQL transform function OID for a column of type + * typid under the given transform context, or InvalidOid when there is + * no context or the type is not one the function declared a transform + * for. Used to convert transform-typed columns inside trigger rows, + * composites and set-returning results (rows read from SPI pass a NULL + * context, so they are left untransformed). + */ +Oid +plphp_col_transform(plphp_trans_ctx *tctx, Oid typid, bool fromsql) +{ + if (tctx == NULL || tctx->trftypes == NIL) + return InvalidOid; + return fromsql + ? get_transform_fromsql(typid, tctx->lang_oid, tctx->trftypes) + : get_transform_tosql(typid, tctx->lang_oid, tctx->trftypes); +} + +/* + * plphp_col_to_cstring + * Convert one column's PHP value to the text form the tuple builders + * (BuildTupleFromCStrings) expect. When the function declared a ToSQL + * transform for the column's type, a non-null value is converted through + * it (e.g. a PHP array -> hstore/jsonb) and then rendered to text for + * re-parsing; otherwise -- and for a NULL/absent value -- the value is + * stringified exactly as before. Result is palloc'd, or NULL. + */ +static char * +plphp_col_to_cstring(zval *val, Oid atttypid, plphp_trans_ctx *tctx) +{ + Oid trf = plphp_col_transform(tctx, atttypid, false); + + if (OidIsValid(trf) && val != NULL) + { + zval *v = val; + + ZVAL_DEREF(v); + if (Z_TYPE_P(v) != IS_NULL) + { + Datum d = OidFunctionCall1(trf, PointerGetDatum(v)); + Oid typoutput; + bool typisvarlena; + + getTypeOutputInfo(atttypid, &typoutput, &typisvarlena); + return OidOutputFunctionCall(typoutput, d); + } + } + return plphp_zval_get_typed_cstring(val, atttypid); +} + /* * plphp_add_row_value * Add one column's text value to a row hash under attname, converting @@ -105,7 +156,7 @@ plphp_zval_from_tuple(HeapTuple tuple, TupleDesc tupdesc) * arrays in form array(1,2,3) as the result of functions with OUT arguments. */ HeapTuple -plphp_htup_from_zval(zval *val, TupleDesc tupdesc) +plphp_htup_from_zval(zval *val, TupleDesc tupdesc, plphp_trans_ctx *tctx) { MemoryContext oldcxt; MemoryContext tmpcxt; @@ -127,8 +178,8 @@ plphp_htup_from_zval(zval *val, TupleDesc tupdesc) char *key = SPI_fname(tupdesc, i + 1); zval *scalarval = plphp_array_get_elem(val, key); - values[i] = plphp_zval_get_typed_cstring(scalarval, - TupleDescAttr(tupdesc, i)->atttypid); + values[i] = plphp_col_to_cstring(scalarval, + TupleDescAttr(tupdesc, i)->atttypid, tctx); /* * Reset the flag if even one of the keys actually exists, * even if it is NULL. @@ -150,8 +201,8 @@ plphp_htup_from_zval(zval *val, TupleDesc tupdesc) { if (i >= tupdesc->natts) break; - values[i] = plphp_zval_get_typed_cstring(element, - TupleDescAttr(tupdesc, i)->atttypid); + values[i] = plphp_col_to_cstring(element, + TupleDescAttr(tupdesc, i)->atttypid, tctx); i++; } ZEND_HASH_FOREACH_END(); @@ -178,7 +229,7 @@ plphp_htup_from_zval(zval *val, TupleDesc tupdesc) */ HeapTuple plphp_srf_htup_from_zval(zval *val, AttInMetadata *attinmeta, - MemoryContext cxt) + MemoryContext cxt, plphp_trans_ctx *tctx) { MemoryContext oldcxt; HeapTuple ret; @@ -206,8 +257,14 @@ plphp_srf_htup_from_zval(zval *val, AttInMetadata *attinmeta, { Form_pg_attribute att = TupleDescAttr(attinmeta->tupdesc, 0); + /* + * A transform-typed single column takes the whole value (e.g. + * RETURNS SETOF hstore, where each row is one map). + */ + if (OidIsValid(plphp_col_transform(tctx, att->atttypid, false))) + values[0] = plphp_col_to_cstring(val, att->atttypid, tctx); /* Is it an array? */ - if (att->attndims != 0 || + else if (att->attndims != 0 || !OidIsValid(get_element_type(att->atttypid))) { zval *element = NULL; @@ -240,8 +297,8 @@ plphp_srf_htup_from_zval(zval *val, AttInMetadata *attinmeta, break; } - values[i] = plphp_zval_get_typed_cstring(element, - TupleDescAttr(attinmeta->tupdesc, i)->atttypid); + values[i] = plphp_col_to_cstring(element, + TupleDescAttr(attinmeta->tupdesc, i)->atttypid, tctx); i++; } ZEND_HASH_FOREACH_END(); @@ -249,13 +306,20 @@ plphp_srf_htup_from_zval(zval *val, AttInMetadata *attinmeta, } else { + Form_pg_attribute att; + /* The passed zval is not an array -- use as the only attribute */ if (attinmeta->tupdesc->natts != 1) ereport(ERROR, (errmsg("returned array does not correspond to " "declared return value"))); - values[0] = plphp_zval_get_cstring(val, true, true); + /* A scalar into a transform-typed single column still transforms. */ + att = TupleDescAttr(attinmeta->tupdesc, 0); + if (OidIsValid(plphp_col_transform(tctx, att->atttypid, false))) + values[0] = plphp_col_to_cstring(val, att->atttypid, tctx); + else + values[0] = plphp_zval_get_cstring(val, true, true); } MemoryContextSwitchTo(oldcxt); @@ -856,7 +920,8 @@ plphp_zval_get_cstring(zval *val, bool do_array, bool null_ok) * Returns a freshly emalloc'd zval; see the ownership note in plphp_io.h. */ zval * -plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc) +plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, + plphp_trans_ctx *tctx) { int i; zval *output; @@ -873,6 +938,7 @@ plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc) for (i = 0; i < tupdesc->natts; i++) { Form_pg_attribute att = TupleDescAttr(tupdesc, i); + Oid trf; /* Ignore dropped attributes */ if (att->attisdropped) @@ -891,6 +957,22 @@ plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc) continue; } + /* + * If this column's type has a FromSQL transform for the function, + * convert it to a native PHP value directly (e.g. jsonb/hstore become + * PHP arrays) instead of going through the text representation. + */ + trf = plphp_col_transform(tctx, att->atttypid, true); + if (OidIsValid(trf)) + { + zval *transformed = + (zval *) DatumGetPointer(OidFunctionCall1(trf, attr_datum)); + + add_assoc_zval(output, attname, transformed); + efree(transformed); + continue; + } + /* * Lookup the attribute type in the syscache for the output function */ @@ -920,7 +1002,7 @@ plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc) * by the caller. */ HeapTuple -plphp_modify_tuple(zval *outdata, TriggerData *tdata) +plphp_modify_tuple(zval *outdata, TriggerData *tdata, plphp_trans_ctx *tctx) { TupleDesc tupdesc; HeapTuple rettuple; @@ -977,7 +1059,7 @@ plphp_modify_tuple(zval *outdata, TriggerData *tdata) elog(ERROR, "$_TD['new'] does not contain attribute \"%s\"", attname); - vals[i] = plphp_zval_get_typed_cstring(el, att->atttypid); + vals[i] = plphp_col_to_cstring(el, att->atttypid, tctx); } /* Return to the original context so that the new tuple will survive */ diff --git a/plphp_io.h b/plphp_io.h index 749f246..7f654ac 100644 --- a/plphp_io.h +++ b/plphp_io.h @@ -10,6 +10,7 @@ #include "access/heapam.h" #include "commands/trigger.h" #include "funcapi.h" +#include "nodes/pg_list.h" /* * These are defined again in php.h, so undef them to avoid some @@ -39,10 +40,27 @@ * plphp_array_get_elem returns a *borrowed* pointer into an existing array * (or NULL); it must not be freed. */ +/* + * Per-call transform context: the function's language and its declared + * TRANSFORM FOR TYPE types. Passed into the tuple builders so that a column + * whose type has a transform is converted through it (in trigger rows, + * composites and set-returning results), in both directions. A NULL context, + * or NIL trftypes, means "no transforms" -- e.g. rows read back from SPI. + */ +typedef struct plphp_trans_ctx +{ + Oid lang_oid; + List *trftypes; +} plphp_trans_ctx; + +/* Resolve a column type's From/To SQL transform under a context, or 0. */ +Oid plphp_col_transform(plphp_trans_ctx *tctx, Oid typid, bool fromsql); + zval *plphp_zval_from_tuple(HeapTuple tuple, TupleDesc tupdesc); -HeapTuple plphp_htup_from_zval(zval *val, TupleDesc tupdesc); +HeapTuple plphp_htup_from_zval(zval *val, TupleDesc tupdesc, + plphp_trans_ctx *tctx); HeapTuple plphp_srf_htup_from_zval(zval *val, AttInMetadata *attinmeta, - MemoryContext cxt); + MemoryContext cxt, plphp_trans_ctx *tctx); char *plphp_convert_to_pg_array(zval *array); char *plphp_convert_to_pg_array_typed(zval *array, Oid elemtype); zval *plphp_convert_from_pg_array(char *input, Oid elemtype); @@ -50,8 +68,10 @@ zval *plphp_convert_from_pg_record(const char *input, TupleDesc tupdesc); char *plphp_zval_get_typed_cstring(zval *val, Oid typid); zval *plphp_array_get_elem(zval* array, char *key); char *plphp_zval_get_cstring(zval *val, bool do_array, bool null_ok); -zval *plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc); -HeapTuple plphp_modify_tuple(zval *outdata, TriggerData *tdata); +zval *plphp_build_tuple_argument(HeapTuple tuple, TupleDesc tupdesc, + plphp_trans_ctx *tctx); +HeapTuple plphp_modify_tuple(zval *outdata, TriggerData *tdata, + plphp_trans_ctx *tctx); #endif /* PLPHP_IO_H */ diff --git a/plphp_spi.c b/plphp_spi.c index 3f941ad..ff56ea7 100644 --- a/plphp_spi.c +++ b/plphp_spi.c @@ -213,6 +213,8 @@ FunctionCallInfo current_fcinfo = NULL; TupleDesc current_tupledesc = NULL; AttInMetadata *current_attinmeta = NULL; MemoryContext current_memcxt = NULL; +/* Transform context of the SRF currently emitting rows via return_next. */ +plphp_trans_ctx current_trans_ctx = {InvalidOid, NIL}; Tuplestorestate *current_tuplestore = NULL; @@ -597,7 +599,8 @@ ZEND_FUNCTION(return_next) oldcxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory); /* Form the tuple */ - tup = plphp_srf_htup_from_zval(param, current_attinmeta, current_memcxt); + tup = plphp_srf_htup_from_zval(param, current_attinmeta, current_memcxt, + ¤t_trans_ctx); /* First call? Create the tuplestore. */ if (!current_tuplestore) diff --git a/plphp_spi.h b/plphp_spi.h index 9397fe7..e5a1fd3 100644 --- a/plphp_spi.h +++ b/plphp_spi.h @@ -19,6 +19,9 @@ #include "Zend/zend_API.h" +/* for plphp_trans_ctx (used by current_trans_ctx below) */ +#include "plphp_io.h" + /* resource type Id for SPIresult */ extern int SPIres_rtype; /* resource type Id for a prepared SPI plan */ @@ -34,6 +37,7 @@ extern TupleDesc current_tupledesc; extern AttInMetadata *current_attinmeta; extern MemoryContext current_memcxt; extern Tuplestorestate *current_tuplestore; +extern plphp_trans_ctx current_trans_ctx; extern HashTable *saved_symbol_table; /*