From 01620adc751a1d0cabefccd3f1fc79237dae5ba1 Mon Sep 17 00:00:00 2001 From: Florents Tselai Date: Mon, 29 Sep 2025 20:12:09 +0300 Subject: [PATCH v2] Add json{b}_translate function Introduce a new function json{b}_translate(json{b}, from text, to text) that recursively replaces string values in json documents. Recursively means that both string values and string array elements are replaced. --- doc/src/sgml/func/func-json.sgml | 26 ++++++++++++++ src/backend/catalog/system_functions.sql | 14 ++++++++ src/backend/utils/adt/json.c | 43 ++++++++++++++++++++++++ src/backend/utils/adt/jsonb.c | 41 ++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 6 ++++ src/test/regress/expected/json.out | 32 ++++++++++++++++++ src/test/regress/expected/jsonb.out | 32 ++++++++++++++++++ src/test/regress/sql/json.sql | 8 +++++ src/test/regress/sql/jsonb.sql | 8 +++++ 9 files changed, 210 insertions(+) diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 91f98a345d4..fa93c0fab3f 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -1845,6 +1845,32 @@ ERROR: value too long for type character(2) t + + + + + json_translate + + json_translate ( jsonb, text, text ) + json + + + + jsonb_translate + + jsonb_translate ( jsonb, text, text ) + jsonb + + + Recursively replaces string values in a document that exactly match + the second argument with the third argument. + + + jsonb_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth') + {"message": "earth", "elements": ["earth", "orange"]} + + + diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..caf9919f8fd 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -621,6 +621,20 @@ LANGUAGE INTERNAL STRICT STABLE PARALLEL SAFE AS 'json_strip_nulls'; +CREATE OR REPLACE FUNCTION + jsonb_translate(target jsonb, from_ text, to_ text) + RETURNS jsonb + LANGUAGE INTERNAL + STRICT STABLE PARALLEL SAFE +AS 'jsonb_translate'; + +CREATE OR REPLACE FUNCTION + json_translate(target json, from_ text, to_ text) + RETURNS json + LANGUAGE INTERNAL + STRICT STABLE PARALLEL SAFE +AS 'json_translate'; + -- default normalization form is NFC, per SQL standard CREATE OR REPLACE FUNCTION "normalize"(text, text DEFAULT 'NFC') diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index e9d370cb3da..f6c078ac304 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -29,6 +29,7 @@ #include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" +#include "utils/varlena.h" /* @@ -1911,3 +1912,45 @@ json_typeof(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text(type)); } + +typedef struct +{ + text *from; + text *to; + Oid collation; +} TranslateState; + +static text * +translate_json_string_action(void *vstate, char *elem_value, int elem_len) +{ + TranslateState *state = (TranslateState *) vstate; + + char *from_str = VARDATA_ANY(state->from); + int from_len = VARSIZE_ANY_EXHDR(state->from); + + if (varstr_cmp(elem_value, elem_len, + from_str, from_len, + state->collation) == 0) + return state->to; + + return cstring_to_text_with_len(elem_value, elem_len); +} + +Datum +json_translate(PG_FUNCTION_ARGS) +{ + text *json = PG_GETARG_TEXT_PP(0); + text *from = PG_GETARG_TEXT_PP(1); + text *to = PG_GETARG_TEXT_PP(2); + text *res; + + TranslateState *state = palloc0(sizeof(TranslateState)); + state->from = from; + state->to = to; + state->collation = PG_GET_COLLATION(); + + res = transform_json_string_values(json, state, + translate_json_string_action); + + PG_RETURN_TEXT_P(res); +} diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index da94d424d61..b0aac326df9 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -24,6 +24,7 @@ #include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" +#include "utils/varlena.h" typedef struct JsonbInState { @@ -2252,3 +2253,43 @@ JsonbUnquote(Jsonb *jb) else return JsonbToCString(NULL, &jb->root, VARSIZE(jb)); } +typedef struct +{ + text *from; + text *to; + Oid collation; +} TranslateState; + +static text * +translate_jsonb_string_action(void *vstate, char *elem_value, int elem_len) +{ + TranslateState *state = (TranslateState *) vstate; + + char *from_str = VARDATA_ANY(state->from); + int from_len = VARSIZE_ANY_EXHDR(state->from); + + if (varstr_cmp(elem_value, elem_len, + from_str, from_len, + state->collation) == 0) + return state->to; + + return cstring_to_text_with_len(elem_value, elem_len); +} + +Datum +jsonb_translate(PG_FUNCTION_ARGS) +{ + Jsonb *jb = PG_GETARG_JSONB_P(0); + text *from = PG_GETARG_TEXT_PP(1); + text *to = PG_GETARG_TEXT_PP(2); + Jsonb *res; + + TranslateState *state = palloc0(sizeof(TranslateState)); + state->from = from; + state->to = to; + state->collation = PG_GET_COLLATION();; + + res = transform_jsonb_string_values(jb, state, translate_jsonb_string_action); + + PG_RETURN_JSONB_P(res); +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 01eba3b5a19..c1d837c0594 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -9473,6 +9473,12 @@ { oid => '3968', descr => 'get the type of a json value', proname => 'json_typeof', prorettype => 'text', proargtypes => 'json', prosrc => 'json_typeof' }, +{ oid => '4175', descr => 'replace recursively json string values', + proname => 'jsonb_translate', prorettype => 'jsonb', proargtypes => 'jsonb text text', + prosrc => 'jsonb_translate' }, +{ oid => '4176', descr => 'replace recursively json string values', + proname => 'json_translate', prorettype => 'json', proargtypes => 'json text text', + prosrc => 'json_translate' }, # uuid { oid => '2952', descr => 'I/O', diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out index 04b478cb468..3be687fb47a 100644 --- a/src/test/regress/expected/json.out +++ b/src/test/regress/expected/json.out @@ -2766,3 +2766,35 @@ select ts_headline('[]'::json, tsquery('aaa & bbb')); [] (1 row) +-- json_translate +select json_translate('{"message": "world"}', 'world', 'earth'); -- basic case + json_translate +--------------------- + {"message":"earth"} +(1 row) + +select json_translate('"hello world"', 'world', 'earth'); -- shouldn't change + json_translate +---------------- + "hello world" +(1 row) + +select json_translate('"hello world"', 'hello world', 'hello earth'); -- this should + json_translate +---------------- + "hello earth" +(1 row) + +select json_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth'); -- in arrays + json_translate +--------------------------------------------------- + {"message":"earth","elements":["earth","orange"]} +(1 row) + +-- should *not* touch keys by default +select json_translate('{"world": "ok"}', 'world', 'earth'); + json_translate +---------------- + {"world":"ok"} +(1 row) + diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5a1eb18aba2..5ad82f5ff3a 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5831,3 +5831,35 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; 12345 (1 row) +-- jsonb_translate +select jsonb_translate('{"message": "world"}', 'world', 'earth'); -- basic case + jsonb_translate +---------------------- + {"message": "earth"} +(1 row) + +select jsonb_translate('"hello world"', 'world', 'earth'); -- shouldn't change + jsonb_translate +----------------- + "hello world" +(1 row) + +select jsonb_translate('"hello world"', 'hello world', 'hello earth'); -- this should + jsonb_translate +----------------- + "hello earth" +(1 row) + +select jsonb_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth'); -- in arrays + jsonb_translate +------------------------------------------------------- + {"message": "earth", "elements": ["earth", "orange"]} +(1 row) + +-- should *not* touch keys by default +select jsonb_translate('{"world": "ok"}', 'world', 'earth'); + jsonb_translate +----------------- + {"world": "ok"} +(1 row) + diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql index e9800b21ffe..7714ae1a99f 100644 --- a/src/test/regress/sql/json.sql +++ b/src/test/regress/sql/json.sql @@ -888,3 +888,11 @@ select ts_headline('english', '{"a": "aaa bbb", "b": {"c": "ccc ddd fff", "c1": select ts_headline('null'::json, tsquery('aaa & bbb')); select ts_headline('{}'::json, tsquery('aaa & bbb')); select ts_headline('[]'::json, tsquery('aaa & bbb')); + +-- json_translate +select json_translate('{"message": "world"}', 'world', 'earth'); -- basic case +select json_translate('"hello world"', 'world', 'earth'); -- shouldn't change +select json_translate('"hello world"', 'hello world', 'hello earth'); -- this should +select json_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth'); -- in arrays +-- should *not* touch keys by default +select json_translate('{"world": "ok"}', 'world', 'earth'); diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 57c11acddfe..03114673883 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1590,3 +1590,11 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::float8; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int2; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int4; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; + +-- jsonb_translate +select jsonb_translate('{"message": "world"}', 'world', 'earth'); -- basic case +select jsonb_translate('"hello world"', 'world', 'earth'); -- shouldn't change +select jsonb_translate('"hello world"', 'hello world', 'hello earth'); -- this should +select jsonb_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth'); -- in arrays +-- should *not* touch keys by default +select jsonb_translate('{"world": "ok"}', 'world', 'earth'); -- 2.49.0