Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions doc/plphp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 101 additions & 1 deletion hstore_plphp/expected/hstore_plphp.out
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,112 @@ 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']['<hstore col>'] 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)
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()
67 changes: 67 additions & 0 deletions hstore_plphp/sql/hstore_plphp.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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']['<hstore col>'] 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;
39 changes: 38 additions & 1 deletion jsonb_plphp/expected/jsonb_plphp.out
Original file line number Diff line number Diff line change
Expand Up @@ -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);
29 changes: 28 additions & 1 deletion jsonb_plphp/sql/jsonb_plphp.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Loading
Loading