2

I have a table in Postgres which stores a tree structure. Each node has a jsonb field: params_diff:

CREATE TABLE tree (id INT, parent_id INT, params_diff JSONB);
INSERT INTO tree VALUES
  (1, NULL, '{ "some_key": "some value" }'::jsonb)
, (2, 1,    '{ "some_key": "other value", "other_key": "smth" }'::jsonb)
, (3, 2,    '{ "other_key": "smth else" }'::jsonb);

The thing I need is to select a node by id with additional generated params field which contains the result of merging all params_diff from the whole parents chain:

SELECT tree.*, /* some magic here */ AS params FROM tree WHERE id = 3;

 id | parent_id |        params_diff         |                        params
----+-----------+----------------------------+-------------------------------------------------------
  3 |         2 | {"other_key": "smth else"} | {"some_key": "other value", "other_key": "smth else"}
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
fey
  • 1,289
  • 1
  • 10
  • 20
  • It's unclear how to "merge parents chain" exactly. Only take the "last" (furthest away from the root) value for each key to form a list? Sort order? – Erwin Brandstetter Feb 22 '15 at 00:10

1 Answers1

3

Generally, a recursive CTE can do the job. Example:

We just need a more magic to decompose, process and re-assemble the JSON result. I am assuming from your example, that you want each key once only, with the first value in the search path (bottom-up):

WITH RECURSIVE cte AS (
   SELECT id, parent_id, params_diff, 1 AS lvl
   FROM   tree
   WHERE  id = 3

   UNION ALL
   SELECT t.id, t.parent_id, t.params_diff, c.lvl + 1
   FROM   cte  c
   JOIN   tree t ON t.id = c.parent_id
   )
SELECT id, parent_id, params_diff
    , (SELECT json_object(array_agg(key   ORDER BY lvl)
                        , array_agg(value ORDER BY lvl))::jsonb
        FROM  (
           SELECT key, value
           FROM (
                SELECT DISTINCT ON (key)
                       p.key, p.value, c.lvl
                FROM   cte c, jsonb_each_text(c.params_diff) p
                ORDER  BY p.key, c.lvl
                ) sub1
           ORDER  BY lvl
           ) sub2
       ) AS params

FROM   cte
WHERE  id = 3;

How?

  1. Walk the tree with a classic recursive CTE.
  2. Create a derived table with all keys and values with jsonb_each_text() in a LATERAL JOIN, remember the level in the search path (lvl).
  3. Use DISTINCT ON to get the "first" (lowest lvl) value for each key. Details:
  4. Sort and aggregate resulting keys and values and feed the arrays to json_object() to build the final params value.

SQL Fiddle (only as far as pg 9.3 can go with json instead of jsonb).

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • This function works but it casts all the values to strings (including nested objects and arrays). Is there a way to make it keep the value type and also merge nested objects? – fey Mar 16 '15 at 18:33
  • Ok, I managed that myself. Basically I changed `json_object` call to `json_object_agg(key, value)` and `jsonb_each_text` to `jsonb_each(c.params_diff)`. – fey Mar 16 '15 at 20:04