4

I have a table mapping_transform with a JSONB column content_json containing something like

{
    "meta": {...},
    "mapping": [
        ...,
        {
            "src": "up",
            "dest": "down",
            ...
        },
        ...
    ]
}

I want to add a new JSON entry ("rule_names": [ "some name" ]) to the JSON object matching src = up and dest = down, which would result in

{
    "meta": {...},
    "mapping": [
        ...,
        {
            "src": "up",
            "dest": "down",
            ...,
            "rule_names": [ "some name" ]
        },
        ...
    ]
}

The following query returns the JSON object that meets the filter requirements:

WITH elems AS (SELECT json_array_elements(content_json->'mapping') from mapping_transform)
SELECT * FROM elems WHERE json_array_elements->>'src' = 'up' and json_array_elements->>'dest' = 'down';

-- Alternative

SELECT mt_entry
FROM mapping_transform,
LATERAL jsonb_array_elements(content_json::jsonb->'mapping') mt_entry
WHERE mt_entry->>'src' = 'up' and mt_entry->>'dest' = 'down';

My problem now is that I do not know how to add the new entry to the specific object. I tried something like

WITH elems AS (SELECT json_array_elements(content_json->'mapping') from mapping_transform),
     results SELECT * FROM elems WHERE json_array_elements->>'src' = 'up' and json_array_elements->>'dest' = 'down'
UPDATE mapping_transform
SET content_json = jsonb_set(results, '{"rule_names"}', '["some name"]'); -- this does obviously not work

but that does not execute as results is an unknown column. I also do need to merge the result of the jsonb_set with the rest of the content_json before assigning to content_json, because otherwise it would override the whole content.

How can I update specific deeply nested JSON objects based on filter criteria? If I had a well defined path as to where my object is that I want to update, things would be much easier. But as the target object lies within a JSON array and has an arbitrary position, finding and updating it is much more difficult.

Torsten Scholz
  • 856
  • 1
  • 9
  • 22

2 Answers2

3

If you are familiar with JavaScript you'll be happy to install and use JavaScript procedural language plv8. This extension allows you to modify json values natively, example:

create extension if not exists plv8;

create or replace function update_mapping_v8(data json)
returns json language plv8 as $$
    var len = data['mapping'].length;
    for (var i = 0; i < len; i++) {
        var o = data['mapping'][i];
        if (o.src == 'up' && o.dest == 'down') {
            o.rule_names = 'some name'
        }
    }
    return data;
$$;

update mapping_transform
set content_json = update_mapping_v8(content_json);

For MS Windows users: ready to install Windows binaries.

A plpgsql alternative solution uses jsonb type:

create or replace function update_mapping_plpgsql(data jsonb)
returns json language plpgsql as $$
declare
    r record;
begin
    for r in
        select value, ordinality- 1 as pos
        from jsonb_array_elements(data->'mapping') with ordinality
        where value->>'src' = 'up' and value->>'dest' = 'down'
    loop
        data = jsonb_set(
            data,
            array['mapping', r.pos::text],
            r.value || '{"rule_names": "some name"}'
            );
    end loop;
    return data;
end $$;

update mapping_transform
set content_json = update_mapping_plpgsql(content_json::jsonb);
klin
  • 112,967
  • 15
  • 204
  • 232
1

I build path here: concat('{mapping,',(ord::int-1),'}')::text[] and the rest is pretty same. Please note I join on text=text (because I dont know what is your PK - it is not recommended). left the value to update with, right original:

vao=# with num as (select content_json,val,ord from mapping_transform, json_array_elements(content_json->'mapping') with ordinality as o (val,ord) where val->>'src' = 'up')
select
  jsonb_pretty(
    jsonb_set(t.content_json::jsonb,concat('{mapping,',(ord::int-1),'}')::text[],((t.content_json->'mapping'->(ord::int-1))::jsonb||'{"rule_names":["some name"]}')::jsonb)
  )
, jsonb_pretty(t.content_json::jsonb)
from mapping_transform t
join num on num.content_json::text = t.content_json::text
/* of course join should be on PK, not text representation*/
;
        jsonb_pretty         |        jsonb_pretty
-----------------------------+----------------------------
 {                          +| {                         +
     "meta": {              +|     "meta": {             +
         "a": true          +|         "a": true         +
     },                     +|     },                    +
     "mapping": [           +|     "mapping": [          +
         "a",               +|         "a",              +
         "c",               +|         "c",              +
         {                  +|         {                 +
             "a": 0,        +|             "a": 0,       +
             "src": "up",   +|             "src": "up",  +
             "dest": "down",+|             "dest": "down"+
             "rule_names": [+|         },                +
                 "some name"+|         "b"               +
             ]              +|     ]                     +
         },                 +| }
         "b"                +|
     ]                      +|
 }                           |
 {                          +| {                         +
     "meta": {              +|     "meta": {             +
         "a": true          +|         "a": true         +
     },                     +|     },                    +
     "mapping": [           +|     "mapping": [          +
         "a",               +|         "a",              +
         {                  +|         {                 +
             "a": 0,        +|             "a": 0,       +
             "src": "up",   +|             "src": "up",  +
             "dest": "down",+|             "dest": "down"+
             "rule_names": [+|         },                +
                 "some name"+|         "b"               +
             ]              +|     ]                     +
         },                 +| }
         "b"                +|
     ]                      +|
 }                           |
(2 rows)

and the build:

vao=# create table mapping_transform(content_json jsonb);
CREATE TABLE
vao=#  insert into mapping_transform select '{
 "meta": {
  "a": true
 },
 "mapping": ["a",{
   "src": "up",
   "dest": "down",
   "a": 0
  },
  "b"
 ]
}';
INSERT 0 1
vao=#  insert into mapping_transform select '{
 "meta": {
  "a": true
 },
 "mapping": ["a","c",{
   "src": "up",
   "dest": "down",
   "a": 0
  },
  "b"
 ]
}';
INSERT 0 1
Vao Tsun
  • 47,234
  • 13
  • 100
  • 132
  • The problem is that you assume that the target object is at position 0 in the array (`content_json->'mapping'->0`), but the object can be at any position, see the last sentence of my question. This is why I need some kind of filtering and cannot "go to the object by path". – Torsten Scholz Jul 11 '17 at 14:48
  • ah. I'll have a look some time later – Vao Tsun Jul 11 '17 at 14:50
  • @TorstenScholz updated the answer to build path dynamically – Vao Tsun Jul 11 '17 at 15:31