Postgresql - query to generate json

Startup: PostgreSQL 9.6.2

I have data stored in a table that is in the form of a key / value pair. The "key" is actually the path of the json object, each of which is a property. For example, if the key was "cogs", "props1", "value", then the json object would be like this:

{
  "cogs":{
     "props1": {
       "value": 100    
      }
  }
}

      

I would like to somehow restore the json object via SQL query, if possible. Here is a test case:

drop table if exists test_table;
CREATE TABLE test_table
(
    id serial,
    file_id integer NOT NULL,
    key character varying[],
    value character varying,
    status character varying
)
WITH (
    OIDS = FALSE
)
TABLESPACE pg_default;

insert into test_table (file_id, key, value, status)
values (1, '{"cogs","description"}', 'some awesome cog', 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","display"}', 'Giant Cog', null);
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props1","value"}', '100', 'not verified');
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props1","id"}', 26, 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props1","dimensions"}', '{"200", "300"}', null);
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props2","value"}', '200', 'not verified');
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props2","id"}', 27, 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"cogs","props2","dimensions"}', '{"700", "800"}', null);

insert into test_table (file_id, key, value, status)
values (1, '{"widgets","description"}', 'some awesome widget', 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","display"}', 'Giant Widget', null);
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props1","value"}', '100', 'not verified');
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props1","id"}', 28, 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props1","dimensions"}', '{"200", "300"}', null);
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props2","value"}', '200', 'not verified');
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props2","id"}', 29, 'approved');
insert into test_table (file_id, key, value, status)
values (1, '{"widgets","props2","dimensions"}', '{"900", "1000"}', null);

      

The result I'm looking for is in this format:

{
    "cogs": {
        "description": "some awesome cog",
        "display": "Giant Cog",
        "props1": {
            "value": 100,
            "id": 26,
            "dimensions": [200, 300]
        },
        "props2": {
            "value": 200,
            "id": 27,
            "dimensions": [700, 800]
        }
    },
    "widgets": {
        "description": "some awesome widget",
        "display": "Giant Widget",
        "props1": {
            "value": 100,
            "id": 28,
            "dimensions": [200, 300]
        },
        "props2": {
            "value": 200,
            "id": 29,
            "dimensions": [900, 1000]
        }
    }
}

      

Some of the problems I am facing:

  • The value column can contain text, numbers, and an array. For some reason, the server-side code using knex.js stores an array of integers (ie [100,300]) in postgres as the following format: {"100", "300"}. I need to make sure I retrieve it as an array of integers.

  • An attempt to make this dynamic as possible as possible. Maybe a recursive procedure to figure out what the depth of the "key" path exists ... rather than hardcoding values.

  • json_object_agg works well to group properties into a single object. However, it breaks when it reaches zero. So if the column "key" only has two values ​​(ie "Cogs", "description") and I am trying to concatenate an array of length three (ie "Cogs", "props1", "value") it will break if I don't filter only arrays of length 3.

  • Save the entry order. @ Klin's solution below is amazing and gives me 95% of the way. However, I did not mention to keep order ...

+3


source to share


1 answer


A dynamic solution takes some work.

First, we need a function to convert the text array and value to a jsonb object.

create or replace function keys_to_object(keys text[], val text)
returns jsonb language plpgsql as $$
declare
    i int;
    rslt jsonb = to_jsonb(val);
begin
    for i in select generate_subscripts(keys, 1, true) loop
        rslt := jsonb_build_object(keys[i], rslt);
    end loop;
    return rslt;
end $$;

select keys_to_object(array['key', 'subkey', 'subsub'], 'value');

              keys_to_object              
------------------------------------------
 {"key": {"subkey": {"subsub": "value"}}}
(1 row)

      

Next, another function to combine jsonb objects (see Combining JSONB Values ​​in PostgreSQL ).

create or replace function jsonb_merge(a jsonb, b jsonb) 
returns jsonb language sql as $$ 
select 
    jsonb_object_agg(
        coalesce(ka, kb), 
        case 
            when va isnull then vb 
            when vb isnull then va 
            when jsonb_typeof(va) <> 'object' or jsonb_typeof(vb) <> 'object' then vb 
            else jsonb_merge(va, vb) end 
        ) 
    from jsonb_each(a) e1(ka, va) 
    full join jsonb_each(b) e2(kb, vb) on ka = kb 
$$;

select jsonb_merge('{"key": {"subkey1": "value1"}}', '{"key": {"subkey2": "value2"}}');

                     jsonb_merge                     
-----------------------------------------------------
 {"key": {"subkey1": "value1", "subkey2": "value2"}}
(1 row) 

      



Finally, let's create an aggregate based on the above function,

create aggregate jsonb_merge_agg(jsonb)
(
    sfunc = jsonb_merge,
    stype = jsonb
);

      

and we ended up:

select jsonb_pretty(jsonb_merge_agg(keys_to_object(key, translate(value, '{}"', '[]'))))
from test_table;

                 jsonb_pretty                 
----------------------------------------------
 {                                           +
     "cogs": {                               +
         "props1": {                         +
             "id": "26",                     +
             "value": "100",                 +
             "dimensions": "[200, 300]"      +
         },                                  +
         "props2": {                         +
             "id": "27",                     +
             "value": "200",                 +
             "dimensions": "[700, 800]"      +
         },                                  +
         "display": "Giant Cog",             +
         "description": "some awesome cog"   +
     },                                      +
     "widgets": {                            +
         "props1": {                         +
             "id": "28",                     +
             "value": "100",                 +
             "dimensions": "[200, 300]"      +
         },                                  +
         "props2": {                         +
             "id": "29",                     +
             "value": "200",                 +
             "dimensions": "[900, 1000]"     +
         },                                  +
         "display": "Giant Widget",          +
         "description": "some awesome widget"+
     }                                       +
 }
(1 row)

      

+3


source







All Articles