-
-
Save villelahdenvuo/5b1fbbf3f22853475bba14f8833faada to your computer and use it in GitHub Desktop.
CREATE OR REPLACE FUNCTION jsonb_set_deep(target jsonb, path text[], val jsonb) | |
RETURNS jsonb AS $$ | |
DECLARE | |
k text; | |
p text[]; | |
BEGIN | |
-- Create missing objects in the path. | |
FOREACH k IN ARRAY path LOOP | |
p := p || k; | |
IF (target #> p IS NULL) THEN | |
target := jsonb_set(target, p, '{}'::jsonb); | |
END IF; | |
END LOOP; | |
-- Set the value like normal. | |
RETURN jsonb_set(target, path, val); | |
END; | |
$$ LANGUAGE plpgsql; |
For those wanting to avoid creating a new PostgreSQL function, there is a way you can emulate the same sort of behavior within the query itself, using the ||
operator (as seen here).
Example:
update "myTable" set "myField" = jsonb_set(
'{"mySubField": {}}' || "myField",
'{mySubField,newEntryKey}',
'newEntryValue'
) where "id" = 'myRowID' returning *
If your target path is deeper than that, it's probably best to stop providing jsonb_set
with deep-paths, instead using a nested approach like so:
update "myTable" set "myField" = jsonb_set(
COALESCE("myField", '{}'),
'{"depth1"}',
jsonb_set(
COALESCE("myField"->'depth1', '{}'),
'{"depth2"}',
jsonb_set(
COALESCE("myField"->'depth1'->'depth2', '{}'),
'{"depth3"}',
jsonb_set(
COALESCE("myField"->'depth1'->'depth2'->'depth3', '{}'),
'{"depth4"}',
'"newValue"'
)
)
)
) where "id" = 'myRowID' returning *
Or compacted:
update "myTable" set "myField" =
jsonb_set(COALESCE("myField", '{}'), '{"depth1"}',
jsonb_set(COALESCE("myField"->'depth1', '{}'), '{"depth2"}',
jsonb_set(COALESCE("myField"->'depth1'->'depth2', '{}'), '{"depth3"}',
jsonb_set(COALESCE("myField"->'depth1'->'depth2'->'depth3', '{}'), '{"depth4"}',
'"newValue"'
)))) where "id" = 'myRowID' returning *
Of course, this is somewhat of a pain if you're writing the query by hand; but it's useful to know about, in cases where you don't want to (or aren't able to) add a native PostgreSQL function to the database.
EDIT: In place of:
jsonb_set(XXX, '{"depth1"}', XXX)
... you can instead do:
jsonb_set(XXX, array['depth1'], XXX)
Which I prefer actually, since it has one fewer set of quotes, and works with query parameters, eg:
UPDATE "myTable" SET "myField" =
jsonb_set(COALESCE("myField", '{}'), array[$1::text],
jsonb_set(COALESCE("myField"->$2::text, '{}'), array[$3::text],
jsonb_set(COALESCE("myField"->$4::text->$5::text, '{}'), array[$6::text],
jsonb_set(COALESCE("myField"->$7::text->$8::text->$9::text, '{}'), array[$10::text],
$11::jsonb
)))) WHERE id = $12::text
For those who may dislike the deeply-nested nature of the calls above, here is an alternative:
update "myTable" set "myField" = jsonb_set(
CASE
WHEN "myField" IS NULL THEN '{"depth1": {"depth2": {"depth3": {}}}}'
WHEN "myField"->'depth1' IS NULL THEN jsonb_set("myField", array['depth1'], '{"depth2": {"depth3": {}}}')
WHEN "myField"->'depth1'->'depth2' IS NULL THEN jsonb_set("myField", array['depth1','depth2'], '{"depth3": {}}')
WHEN "myField"->'depth1'->'depth2'->'depth3' IS NULL THEN jsonb_set("myField", array['depth1','depth2','depth3'], '{}')
ELSE "myField"
END,
array['depth1','depth2','depth3','depth4'],
'"newValue"'
) where "id" = 'myRowID' returning *
Following @Venryx solution, I have created a function (in Kotlin) that creates this jsonb_set_deep statement:
https://gist.github.com/GuyKomari/d81ab763fc38bf88595f1cc29e5566ac
Version that handles conditions:
- if the target is initially null,
- if path is null (insert to root)
CREATE OR REPLACE FUNCTION jsonb_set_deep(target jsonb, path text[], val jsonb)
RETURNS jsonb AS $$
DECLARE
k text;
p text[];
BEGIN
IF (path = '{}') THEN
RETURN val;
ELSE
IF (target IS NULL) THEN
target = '{}'::jsonb;
END IF;
FOREACH k IN ARRAY path LOOP
p := p || k;
IF (target #> p IS NULL) THEN
target := jsonb_set(target, p, '{}'::jsonb);
ELSE
target := jsonb_set(target, p, target #> p);
END IF;
END LOOP;
-- Set the value like normal.
RETURN jsonb_set(target, path, val);
END IF;
END;
$$ LANGUAGE plpgsql;
It was pretty useful to include the ability to concatenate or merge objects (the ||
operator). Without that, the calling code is pretty verbose. This one should support arrays and act more like json_set. I'm not sure about older versions like pg9 but there are some short example SQLs included below.
/*
https://gist.github.com/villelahdenvuo/5b1fbbf3f22853475bba14f8833faada
Handles conditions:
* Accept array index in path
* Concatenation (using the || operator) when target path matches a type in the "concat" array parameter like ARRAY['array', 'object', 'string'] (array append, object merge, strings into an array of strings, etc.).
* If target is null, return null (like jsonb_set)
* If path is null return null (like jsonb_set)
* If path is empty return target (like jsonb_set)
* If concat is null return null
*/
CREATE FUNCTION jsonb_set_deep(
target jsonb, path text[], val jsonb, concat text[] = ARRAY['object']
)
RETURNS jsonb AS $$
DECLARE
k text;
p text[];
i int = 1;
BEGIN
IF target IS NULL OR path IS NULL OR concat IS NULL THEN
RETURN NULL; -- strict
END IF;
FOREACH k IN ARRAY path LOOP
p = p || k;
IF target #> p IS NULL THEN
IF path[i + 1] ~ '^[0-9]+$' THEN
target = jsonb_set(target, p, '[]'::jsonb);
ELSE
target = jsonb_set(target, p, '{}'::jsonb);
END IF;
END IF;
i = i + 1;
END LOOP;
IF jsonb_typeof(target #> path) = ANY(concat) THEN
RETURN jsonb_set(target, path, target #> path || val);
ELSE
RETURN jsonb_set(target, path, val);
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
/*
Accept array index in path
concat is true: {"a":{"b":1},"c":[1,2,{"d":3}]}
concat is false: {"a":{"b":1},"c":[2,{"d":3}]}
*/
SELECT jsonb_set_deep(
'{"a": {"b": 1}, "c":[1]}'::jsonb, ARRAY['c']::TEXT[],
'[2, {"d": 3}]'::jsonb,
ARRAY['object', 'array']
);
-- {"a": {"b": 1}, "c": ["abc", "def"]}
SELECT jsonb_set_deep(
'{"a": {"b": 1}, "c": "abc"}'::jsonb,
ARRAY['c']::TEXT[],
'"def"'::jsonb,
ARRAY['string']
);
/*
If target is null, return null (like jsonb_set)
?column?|?column?|
--------+--------+
true |true |
*/
SELECT jsonb_set(null, array['a']::text[], '1'::jsonb) IS NULL,
jsonb_set_deep(null, array['a']::text[], '1'::jsonb) IS NULL;
/*
If path is null return null (like jsonb_set)
?column?|?column?|
--------+--------+
true |true |
*/
SELECT jsonb_set('{}'::jsonb, null::text[], '1'::jsonb) IS NULL,
jsonb_set_deep('{}'::jsonb, null::text[], '1'::jsonb) IS NULL;
/*
If path is empty return target (like jsonb_set)
jsonb_set|jsonb_set_deep|
---------+--------------+
{"a": 1} |{"a": 1} |
*/
SELECT jsonb_set('{"a": 1}'::jsonb, array[]::text[], '{"a": 2}'::jsonb),
jsonb_set_deep('{"a": 1}'::jsonb, array[]::text[], '{"a": 2}'::jsonb);
/*
If concat is null return null
?column?|
--------+
true |
*/
SELECT jsonb_set_deep('{}'::jsonb, array['a']::text[], '1'::jsonb, NULL) IS NULL;
This doesn't work on postgrest 9.6 if the target is null originally, here is a version that does: