Home > OS >  How to get around lack of nested aggregate calls to generate JSON from Postgres
How to get around lack of nested aggregate calls to generate JSON from Postgres

Time:10-30

I'm trying to produce GeoJSON from a query in Postgres 13.1:

SELECT json_build_object(
               'type', 'FeatureCollection',
               'features', json_agg(
                       json_build_object(
                               'type', 'Feature',
                               'geometry', ST_AsGeoJSON(geom)::json,
                               'properties', json_build_object(
                                   -- list of fields
                                       'id', hardinfra.id,
--                                    need json_object_agg here
                                       'protections', json_build_object(
                                               p.ptype,
                                               i.pscore
                                           ),
--                                    need json_object_agg here
                                       'responses', json_build_object(
                                               ep.etype,
                                               er.response
                                           ),
                                       'category', c.category
                                   )
                           )
                   )
           )
FROM hardinfra
         JOIN expertresponse er ON hardinfra.id = er.infra_id
         JOIN expert ep ON ep.id = er.expert_id
         JOIN infraprotection i ON hardinfra.id = i.infra_id
         JOIN protection p ON p.id = i.protection_id
         JOIN category c on c.id = hardinfra.category_id
;

However protections and responses should be key-value collections built up from the many-to-many relationships, usually using json_object_agg (see join conditions). How can I get around the lack of nested aggregation calls to produce these?

This is what the GeoJSON output should look like:

Note that the "protections" and "responses" properties can be of arbitrary length as they're coming from the related tables.

{
  "features": [
    {
      "geometry": {
        "coordinates": [
          -6.147880554,
          53.383533145
        ],
        "type": "Point"
      },
      "properties": {
        "id": 90,
        "protections": {
          "Foo": 1,
          "Bar": 2,
          "Baz": 3
        },
        "responses": {
          "A": "response A",
          "B": "response B",
          "C": "response C"
        }
      },
      "type": "Feature"
    }
  ],
  "type": "FeatureCollection"
}

The tables are set up as follows (You'll need PostGIS, or substitute a plaintext field – that aspect isn't important here):

create table hardinfra (
    id serial CONSTRAINT hardinfra_pkey PRIMARY KEY,
    geom geometry(Geometry, 4326)
    );

create table expert
(
    id serial
        constraint expert_pkey
            primary key,
    etype varchar(50)
);

create table expertresponse
(
    infra_id  integer not null
        constraint expertresponse_infra_id_fkey
            references hardinfra
            on delete cascade,
    expert_id integer not null
        constraint expertresponse_expert_id_fkey
            references expert
            on delete cascade,
    response  varchar(280) not null,
    constraint expertresponse_pkey
        primary key (infra_id, expert_id)
);

create table protection
(
    id serial
        constraint protection_pkey
            primary key,
    ptype varchar(200)
);

create table infraprotection
(
    infra_id integer not null
        constraint infraprotection_infra_id_fkey
            references hardinfra
            on delete cascade,
    protection_id integer not null
        constraint infraprotection_protection_id_fkey
            references protection
            on delete cascade,
    pscore integer   default 0 not null,
    constraint infraprotection_pkey
        primary key (infra_id, protection_id)
);

CodePudding user response:

As stated by @a_horse_with_no_name you can't build nested aggregations in just one step, you can try this instead, assuming that there is only one category_id per id in table hardinfra and one category per id in table category :

SELECT json_build_object
           ( 'type', 'FeatureCollection'
           , 'features'
           , json_agg( json_build_object 
                           ( 'type', 'Feature'
                           , 'geometry', ST_AsGeoJSON(geom)::json
                           , 'properties'
                           , json_build_object
                                 ( 'id', hi.id
                                 , 'protections', p.protection_list
                                 , 'responses', r.response_list
                                 , 'category', c.category
                                 )
                           )
                     )
           )
FROM hardinfra AS hi
INNER JOIN
(   SELECT i.infra_id
         , json_build_object ('protections'
                             , json_object_agg(p.ptype, i.pscore)
                             ) AS protection_list
      FROM infraprotection AS i 
      JOIN protection AS p
        ON p.id = i.protection_id
) AS p
ON hi.id = p.infra_id
INNER JOIN
(   SELECT er.infra_id
         , json_build_object ('responses'
                             , json_object_agg(ep.etype, er.response)
                             ) AS response_list
      FROM expertresponse AS er
      JOIN expert AS ep
        ON ep.id = er.expert_id
) AS r
ON hi.id = r.infra_id
INNER JOIN category c
ON c.id = hi.category_id ;

PS : in your json example, there is no "category" key/value ?

  • Related