Add jsonb_translate(jsonb, from, to)
This is a spur of the moment patch really,
but while going through the process of translating some json data from
Greek to English I found myself wishing for $subject.
Inspired by the Unix tr utility.
Here's a working v1
If others find it useful as well,
I'd go ahead with a json implementation
And an additional parameter to make in-arrays replacement optional.
Attachments:
v1-0001-Add-jsonb_translate-function.patchapplication/x-patch; name=v1-0001-Add-jsonb_translate-function.patchDownload
From c292e634a465fa178a87e711c7b47668afd3091e Mon Sep 17 00:00:00 2001
From: Florents Tselai <florents.tselai@gmail.com>
Date: Sat, 27 Sep 2025 20:02:44 +0300
Subject: [PATCH v1] Add jsonb_translate function
Introduce a new function jsonb_translate(jsonb, from text, to text) that recursively replaces string values in json
documents.
---
doc/src/sgml/func/func-json.sgml | 19 ++++++++++
src/backend/catalog/system_functions.sql | 7 ++++
src/backend/utils/adt/jsonb.c | 45 ++++++++++++++++++++++++
src/include/catalog/pg_proc.dat | 3 ++
src/test/regress/expected/jsonb.out | 32 +++++++++++++++++
src/test/regress/sql/jsonb.sql | 8 +++++
6 files changed, 114 insertions(+)
diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml
index 91f98a345d4..469877dd8c5 100644
--- a/doc/src/sgml/func/func-json.sgml
+++ b/doc/src/sgml/func/func-json.sgml
@@ -1845,6 +1845,25 @@ ERROR: value too long for type character(2)
<returnvalue>t</returnvalue>
</para></entry>
</row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>jsonb_translate</primary>
+ </indexterm>
+ <function>jsonb_translate</function> ( <type>jsonb</type>, <type>text</type>, <type>text</type> )
+ <returnvalue>jsonb</returnvalue>
+ </para>
+ <para>
+ Recursively replaces string values in a JSONB document that exactly match
+ the second argument with the third argument.
+ </para>
+ <para>
+ <literal>jsonb_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth')</literal>
+ <returnvalue>{"message": "earth", "elements": ["earth", "orange"]}</returnvalue>
+ </para>
+ </entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..4f2eb18f713 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -621,6 +621,13 @@ 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';
+
-- default normalization form is NFC, per SQL standard
CREATE OR REPLACE FUNCTION
"normalize"(text, text DEFAULT 'NFC')
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index da94d424d61..33b55b67ce9 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,47 @@ JsonbUnquote(Jsonb *jb)
else
return JsonbToCString(NULL, &jb->root, VARSIZE(jb));
}
+typedef struct
+{
+ text *from;
+ text *to;
+ Oid collation;
+ bool with_keys;
+} 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);
+ bool with_keys = PG_GETARG_BOOL(3);
+ Oid collation = PG_GET_COLLATION();
+ Jsonb *res;
+
+ TranslateState *state = palloc0(sizeof(TranslateState));
+ state->from = from;
+ state->to = to;
+ state->collation = collation;
+ state->with_keys = with_keys;
+
+ 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..91f4f70da7f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9473,6 +9473,9 @@
{ 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' },
# uuid
{ oid => '2952', descr => 'I/O',
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/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
Hi Florents,
Thanks for the patch. I once had the same pain on a similar task, I had to create a PL/SQL function at the time.
I haven’t read the code change yet, but I think the function name jsonb_translate() sounds to generic. To make the name more meaningful, I would suggest a few candidates: jsonb_replace_text(), or jsonb_replace_value(), or jsonb_deep_replace().
Also, I want to understand why do you decide to support only whole word matching?
```
evantest=# select jsonb_translate('{"message": "world"}', 'wor', 'earth');
jsonb_translate
----------------------
{"message": "world"}
(1 row)
```
With this patch, partial match will not result in a replacement.
On Sep 28, 2025, at 01:16, Florents Tselai <florents.tselai@gmail.com> wrote:
This is a spur of the moment patch really,
but while going through the process of translating some json data from Greek to English I found myself wishing for $subject.
Inspired by the Unix tr utility.Here's a working v1
If others find it useful as well,
I'd go ahead with a json implementation
And an additional parameter to make in-arrays replacement optional.
<v1-0001-Add-jsonb_translate-function.patch>
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
Thanks for taking the time Evan
On Sun, Sep 28, 2025, 12:34 Chao Li <li.evan.chao@gmail.com> wrote:
Hi Florents,
Thanks for the patch. I once had the same pain on a similar task, I had to
create a PL/SQL function at the time.I haven’t read the code change yet, but I think the function name
jsonb_translate() sounds to generic. To make the name more meaningful, I
would suggest a few candidates: jsonb_replace_text(), or
jsonb_replace_value(), or jsonb_deep_replace().Also, I want to understand why do you decide to support only whole word
matching?```
evantest=# select jsonb_translate('{"message": "world"}', 'wor', 'earth');
jsonb_translate
----------------------
{"message": "world"}
(1 row)
```With this patch, partial match will not result in a replacement.
That is on purpose. My use case for this is to replace categorical/enum
values scattered deep inside the json structure.
Hence the name translate which usually means mapping from one key space
to another.
Partial replacement wasn't the case for me, and most importantly I guess I
could achieve the same by casting to text replacing and casting back to
jsonb.
Show quoted text
On Sep 28, 2025, at 01:16, Florents Tselai <florents.tselai@gmail.com>
wrote:This is a spur of the moment patch really,
but while going through the process of translating some json data from
Greek to English I found myself wishing for $subject.
Inspired by the Unix tr utility.Here's a working v1
If others find it useful as well,
I'd go ahead with a json implementation
And an additional parameter to make in-arrays replacement optional.<v1-0001-Add-jsonb_translate-function.patch>
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
ne 28. 9. 2025 v 12:11 odesílatel Florents Tselai <florents.tselai@gmail.com>
napsal:
Thanks for taking the time Evan
On Sun, Sep 28, 2025, 12:34 Chao Li <li.evan.chao@gmail.com> wrote:
Hi Florents,
Thanks for the patch. I once had the same pain on a similar task, I had
to create a PL/SQL function at the time.I haven’t read the code change yet, but I think the function name
jsonb_translate() sounds to generic. To make the name more meaningful, I
would suggest a few candidates: jsonb_replace_text(), or
jsonb_replace_value(), or jsonb_deep_replace().Also, I want to understand why do you decide to support only whole word
matching?```
evantest=# select jsonb_translate('{"message": "world"}', 'wor', 'earth');
jsonb_translate
----------------------
{"message": "world"}
(1 row)
```With this patch, partial match will not result in a replacement.
That is on purpose. My use case for this is to replace categorical/enum
values scattered deep inside the json structure.
Hence the name translate which usually means mapping from one key space
to another.Partial replacement wasn't the case for me, and most importantly I guess I
could achieve the same by casting to text replacing and casting back to
jsonb.
Cannot be better to use JsonPath for specification what should be replaced?
Regards
Pavel
Show quoted text
On Sep 28, 2025, at 01:16, Florents Tselai <florents.tselai@gmail.com>
wrote:This is a spur of the moment patch really,
but while going through the process of translating some json data from
Greek to English I found myself wishing for $subject.
Inspired by the Unix tr utility.Here's a working v1
If others find it useful as well,
I'd go ahead with a json implementation
And an additional parameter to make in-arrays replacement optional.
<v1-0001-Add-jsonb_translate-function.patch>Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On 28 Sep 2025, at 2:26 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
ne 28. 9. 2025 v 12:11 odesílatel Florents Tselai <florents.tselai@gmail.com <mailto:florents.tselai@gmail.com>> napsal:
Thanks for taking the time Evan
On Sun, Sep 28, 2025, 12:34 Chao Li <li.evan.chao@gmail.com <mailto:li.evan.chao@gmail.com>> wrote:
Hi Florents,
Thanks for the patch. I once had the same pain on a similar task, I had to create a PL/SQL function at the time.
I haven’t read the code change yet, but I think the function name jsonb_translate() sounds to generic. To make the name more meaningful, I would suggest a few candidates: jsonb_replace_text(), or jsonb_replace_value(), or jsonb_deep_replace().
Also, I want to understand why do you decide to support only whole word matching?
```
evantest=# select jsonb_translate('{"message": "world"}', 'wor', 'earth');
jsonb_translate
----------------------
{"message": "world"}
(1 row)
```With this patch, partial match will not result in a replacement.
That is on purpose. My use case for this is to replace categorical/enum values scattered deep inside the json structure.
Hence the name translate which usually means mapping from one key space to another.Partial replacement wasn't the case for me, and most importantly I guess I could achieve the same by casting to text replacing and casting back to jsonb.
Cannot be better to use JsonPath for specification what should be replaced?
Fair point.
The main purpose of this patch is to provide a recursive, global replacement across all values and arrays,
which is not as straightforward to express in JSONPath today.
I understand that some may find this too case-specific, so I’m just leaving it out there for consideration.
That said, I believe it can be quite useful in domains where documents carry many tags or labels that need to be translated or normalized consistently.
On Mon, Sep 29, 2025 at 3:34 PM Florents Tselai <florents.tselai@gmail.com>
wrote:
On 28 Sep 2025, at 2:26 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:
ne 28. 9. 2025 v 12:11 odesílatel Florents Tselai <
florents.tselai@gmail.com> napsal:Thanks for taking the time Evan
On Sun, Sep 28, 2025, 12:34 Chao Li <li.evan.chao@gmail.com> wrote:
Hi Florents,
Thanks for the patch. I once had the same pain on a similar task, I had
to create a PL/SQL function at the time.I haven’t read the code change yet, but I think the function name
jsonb_translate() sounds to generic. To make the name more meaningful, I
would suggest a few candidates: jsonb_replace_text(), or
jsonb_replace_value(), or jsonb_deep_replace().Also, I want to understand why do you decide to support only whole word
matching?```
evantest=# select jsonb_translate('{"message": "world"}', 'wor',
'earth');
jsonb_translate
----------------------
{"message": "world"}
(1 row)
```With this patch, partial match will not result in a replacement.
That is on purpose. My use case for this is to replace categorical/enum
values scattered deep inside the json structure.
Hence the name translate which usually means mapping from one key space
to another.Partial replacement wasn't the case for me, and most importantly I guess
I could achieve the same by casting to text replacing and casting back to
jsonb.Cannot be better to use JsonPath for specification what should be replaced?
Fair point.
The main purpose of this patch is to provide a recursive, global
replacement across all values and arrays,
which is not as straightforward to express in JSONPath today.
I understand that some may find this too case-specific, so I’m just
leaving it out there for consideration.
That said, I believe it can be quite useful in domains where documents
carry many tags or labels that need to be translated or normalized
consistently.
Here's a v2 with a json_translate implementation for consideration
Attachments:
v2-0001-Add-json-b-_translate-function.patchapplication/octet-stream; name=v2-0001-Add-json-b-_translate-function.patchDownload
From 01620adc751a1d0cabefccd3f1fc79237dae5ba1 Mon Sep 17 00:00:00 2001
From: Florents Tselai <florents.tselai@gmail.com>
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)
<returnvalue>t</returnvalue>
</para></entry>
</row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>json_translate</primary>
+ </indexterm>
+ <function>json_translate</function> ( <type>jsonb</type>, <type>text</type>, <type>text</type> )
+ <returnvalue>json</returnvalue>
+ </para>
+ <para role="func_signature">
+ <indexterm>
+ <primary>jsonb_translate</primary>
+ </indexterm>
+ <function>jsonb_translate</function> ( <type>jsonb</type>, <type>text</type>, <type>text</type> )
+ <returnvalue>jsonb</returnvalue>
+ </para>
+ <para>
+ Recursively replaces string values in a document that exactly match
+ the second argument with the third argument.
+ </para>
+ <para>
+ <literal>jsonb_translate('{"message": "world", "elements": ["world", "orange"]}', 'world', 'earth')</literal>
+ <returnvalue>{"message": "earth", "elements": ["earth", "orange"]}</returnvalue>
+ </para>
+ </entry>
+ </row>
</tbody>
</tgroup>
</table>
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
On 2025-09-27 Sa 1:16 PM, Florents Tselai wrote:
This is a spur of the moment patch really,
but while going through the process of translating some json data from
Greek to English I found myself wishing for $subject.
Inspired by the Unix tr utility.Here's a working v1
If others find it useful as well,
I'd go ahead with a json implementation
And an additional parameter to make in-arrays replacement optional.
It's not clear to me that this is a good idea. Blindly replacing every
instance of a single string regardless of context seems at best a niche
use. Is there any reason not to make this an extension?
I haven't dug deeply into the patch, but I don't understand why you're
putting an entry for this in system_functions.sql.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On 29.09.25 14:34, Florents Tselai wrote:
Cannot be better to use JsonPath for specification what should be
replaced?Fair point.
The main purpose of this patch is to provide a recursive, global
replacement across all values and arrays,
which is not as straightforward to express in JSONPath today.
I understand that some may find this too case-specific, so I’m just
leaving it out there for consideration.
That said, I believe it can be quite useful in domains where documents
carry many tags or labels that need to be translated or normalized
consistently.
Oracle has a json_transform function, which has also been added to the
SQL standard draft recently. That appears to be more in the direction
that Pavel is suggesting. Maybe someone wants to take a stab at it.
(In that case, having both a json_transform and a json_translate might
be confusing.)