How to update the nested JSON object according to the filter criteria in Postgres?

advertisements

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.


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);