How to Add Multiple JSON_BUILD_OBJECT entries to a JSON_AGG - sql

I am using PostgreSQL 9.4 and I have a requirement to have an 'addresses' array which contains closed JSON objects for different types of address (residential and correspondence). The structure should look like this:
[
{
"addresses": [
{
"addressLine1": "string",
"type": "residential"
},
{
"addressLine1": "string",
"type": "correspondence"
}
],
"lastName": "string"
}
]
...and here's some example data to illustrate the desired result:
[
{
"addresses": [
{
"addressLine1": "54 ASHFIELD PADDOCK",
"type": "residential"
},
{
"addressLine1": "135 MERRION HILL",
"type": "correspondence"
}
],
"lastName": "WRIGHT"
},
{
"addresses": [
{
"addressLine1": "13 BOAKES GROVE",
"type": "residential"
},
{
"addressLine1": "46 BEACONSFIELD GRANGE",
"type": "correspondence"
}
],
"lastName": "DOHERTY"
}
]
This is where I've gotten to with my SQL:
SELECT
json_agg(
json_build_object('addresses',(SELECT json_agg(json_build_object('addressLine1',c2.address_line_1,
'type',c2.address_type
)
)
FROM my_customer_table c2
WHERE c2.person_id=c.person_id
),
'addresses',(SELECT json_agg(json_build_object('addressLine1',c2.corr_address_line_1,
'type',c2.corr_address_type
)
)
FROM my_customer_table c2
WHERE c2.person_id=c.person_id
),
'lastName',c.surname
)
) AS customer_json
FROM
my_customer_table c
WHERE
c.corr_address_type IS NOT NULL /*exclude customers without correspondence addresses*/
...and this runs, however it repeats the 'addresses' object twice and has an array for each address variant, not around the overall array.
What I stupidly thought would work, is the following:
SELECT
json_agg(
json_build_object('addresses',(SELECT json_agg(json_build_object('addressLine1',c2.address_line_1,
'type',c2.address_type
),
json_build_object('addressLine1',c2.corr_address_line_1,
'type',c2.corr_address_type
)
)
FROM my_customer_table c2
WHERE c2.person_id=c.person_id
),
'lastName',c.surname
)
) AS customer_json
FROM
my_customer_table c
WHERE
c.corr_address_type IS NOT NULL /*exclude customers without correspondence addresses*/
...however this throws an error:
"ERROR: function json_agg(json, json) does not exist.
LINE 3: json_build_object('addresses',(SELECT json_agg(json_build_o...
HINT: No function matches the given name and argument types. You might need to add explicit type casts."
I've Googled this, however no posts found seem to relate to the same kind of result I'm trying to get.
Does anyone know if it's possible to have multiple JSON_BUILD_OBJECT entries inside of an array?

A colleague has found a solution to this. It's totally different to the way I was approaching it, but works nicely. Here's the working code:
SELECT
json_agg(subquery.customer_json) AS customer_json
FROM
(
SELECT
row_to_json(t) AS customer_json
FROM (
SELECT
(
SELECT array_to_json(array_agg(addresses_union))
FROM (
SELECT
c.address_line_1 AS "addressLine1",
c.address_type as type
UNION ALL
SELECT
c.corr_address_Line_1 AS "addressLine1",
c.corr_address_type as type
) AS addresses_union
) as addresses,
c.surname AS "lastName"
FROM
my_customer_table c
WHERE
c.corr_address_type IS NOT NULL /*exclude customers without correspondence address*/
) t
) subquery

Related

Querying over PostgreSQL JSONB column

I have a table "blobs" with a column "metadata" in jsonb data-type,
Example:
{
"total_count": 2,
"items": [
{
"name": "somename",
"metadata": {
"metas": [
{
"id": "11258",
"score": 6.1,
"status": "active",
"published_at": "2019-04-20T00:29:00",
"nvd_modified_at": "2022-04-06T18:07:00"
},
{
"id": "9251",
"score": 5.1,
"status": "active",
"published_at": "2018-01-18T23:29:00",
"nvd_modified_at": "2021-01-08T12:15:00"
}
]
}
]
}
I want to identify statuses in the "metas" array that match with certain, given strings. I have tried the following so far but without results:
SELECT * FROM blobs
WHERE metadata is not null AND
(
SELECT count(*) FROM jsonb_array_elements(metadata->'metas') AS cn
WHERE cn->>'status' IN ('active','reported')
) > 0;
It would also be sufficient if I could compare the string with "status" in the first array object.
I am using PostgreSQL 9.6.24
for some clarity I usually break code into series of WITH statements. My idea for your problem would be to use json path (https://www.postgresql.org/docs/12/functions-json.html#FUNCTIONS-SQLJSON-PATH) and function jsonb_path_query.
Below code gives a list of counts, I will leave the rest to you, to get final data.
I've added ID column just to have something to join on. Otherwise join on metadata.
Also, note additional " in where condition. Left join in blob_ext is there just to have null value if metadata is not present or that path does not work.
with blob as (
select row_number() over()"id", * from (VALUES
(
'{
"total_count": 2,
"items": [
{
"name": "somename",
"metadata": {
"metas": [
{
"id": "11258",
"score": 6.1,
"status": "active",
"published_at": "2019-04-20T00:29:00",
"nvd_modified_at": "2022-04-06T18:07:00"
},
{
"id": "9251",
"score": 5.1,
"status": "active",
"published_at": "2018-01-18T23:29:00",
"nvd_modified_at": "2021-01-08T12:15:00"
}
]
}
}
]}'::jsonb),
(null::jsonb)) b(metadata)
)
, blob_ext as (
select bb.*, blob_sts.status
from blob bb
left join (
select
bb2.id,
jsonb_path_query (bb2.metadata::jsonb, '$.items[*].metadata.metas[*].status'::jsonpath)::character varying "status"
FROM blob bb2
) as blob_sts ON
blob_sts.id = bb.id
)
select bbe.id, count(*) cnt, bbe.metadata
from blob_ext bbe
where bbe.status in ('"active"', '"reported"')
group by bbe.id, bbe.metadata;
A way is to peel one layer at a time with jsonb_extract_path() and jsonb_array_elements():
with cte_items as (
select id,
metadata,
jsonb_extract_path(jx.value,'metadata','metas') as metas
from blobs,
lateral jsonb_array_elements(jsonb_extract_path(metadata,'items')) as jx),
cte_metas as (
select id,
metadata,
jsonb_extract_path_text(s.value,'status') as status
from cte_items,
lateral jsonb_array_elements(metas) s)
select distinct
id,
metadata
from cte_metas
where status in ('active','reported');

Average of numeric values in Postgres JSON column

I have a jsonb column with the following structure:
{
"key1": {
"type": "...",
"label": "...",
"variables": [
{
"label": "Height",
"value": 131315.9289,
"variable": "myVar1"
},
{
"label": "Width",
"value": 61085.7525,
"variable": "myVar2"
}
]
},
}
I want to query for the average height across all rows. The top-level key values are unknown, so I have something like this:
select id,
avg((latVars ->> 'value')::numeric) as avg
from "MyTable",
jsonb_array_elements((my_json_field->jsonb_object_keys(my_json_field)->>'variables')::jsonb) as latVars
where my_json_field is not null
group by id;
It's throwing the following error:
ERROR: set-returning functions must appear at top level of FROM
Moving the jsonb_array_elements function above MyTable in the FROM clause doesn't work.
I'm following the basic advice found in this SO answer to no avail.
Any advice?
jsonb_array_elements is not relevant until my_json_field is a json array at the top level.
You can use instead the jsonb_path_query function based on the jsonpath language if postgres >= 12 :
select id
, avg(v.value :: numeric) as avg
from "MyTable"
, jsonb_path_query(my_json_field, '$.*.variables[*] ? (#.label == "Height").value') AS v(value)
where my_json_field is not null
group by id;

JSON database table query

I have JSON table with some objects and I am trying to query the amount value in the object
{
"authorizations": [
{
"id": "d50",
"type": "passed",
"amount": 100,
"fortId": 5050,
"status": "GENERATED",
"voided": false,
"cardNumber": 3973,
"expireDate": null,
"description": "Success",
"customerCode": "858585",
"paymentMethod": "cash",
"changeDatetime": null,
"createDatetime": 000000000,
"reservationCode": "202020DD",
"authorizationCode": "D8787"
},
{
"id": "d50",
"type": "passed",
"amount": 100,
"fortId": 5050,
"status": "GENERATED",
"voided": false,
"cardNumber": 3973,
"expireDate": null,
"description": "Success",
"customerCode": "858585",
"paymentMethod": "cash",
"changeDatetime": null,
"createDatetime": 000000000,
"reservationCode": "202020DD",
"authorizationCode": "D8787"
}
],
}
I have tried the following four options, but none of these give me the value of the object:
SELECT info #> 'authorizations:[{amount}]'
FROM idv.reservations;
SELECT info -> 'authorizations:[{amount}]'
FROM idv.reservations;
info -> ''authorizations' ->> 'amount'
FROM idv.reservations
select (json_array_elements(info->'authorizations')->'amount')::int from idv.reservations
note I am using DBeaver
If you want one row per object contained in the "authorizations" JSON array, with the corresponding amount, you can use a lateral join and jsonb_array_elements():
select r.*, (x.obj ->> 'amount')::int as amount
from reservations r
cross join lateral jsonb_array_elements(r.info -> 'authorizations') x(obj)
We can also extract all amounts at once and put them in an array, like so:
select r.*,
jsonb_path_query_array(r.info, '$.authorizations[*].amount') as amounts
from reservations r
Demo on DB Fiddlde

SQL Error while trying to extract nested json data

I am running an SQL query to extract nested JSON data.
SELECT watsonResponse.responseId,
watsonResponse.chatId,
d.*
FROM watson_response_table watsonResponse
CROSS JOIN LATERAL (
SELECT d2.*
FROM jsonb_array_elements(watsonResponse.watsonresponse_output) AS d(events)
CROSS JOIN LATERAL (
SELECT d2.events ->> 'data' AS watsonResponse_ouput_data
, d2.events ->> 'text' AS watsonResponse_output_text
, d2.events ->> 'uiActionCode' AS watsonResponse_output_uiActionCode
FROM jsonb_array_elements(d.events) AS d2(events)
) d2
WHERE d.events ->> 'uiActionCode' = 'TextWithButton'
) d;
It fails with message SQL Error [22023]: ERROR: cannot extract elements from an object
I am using PostgresSQL 11+. Here is what the JSON looks like,
[
{
"text": [
"Some text!"
],
"uiActionCode": "textOnly"
},
{
"data": {
"type": "options",
"options": [
{ "label": "test", "value": "testvalue" },
{ "label": "test2", "value": "testvalue2" },
{
"label": "test3",
"value": "testQuestion?"
}
]
},
"text": ["testQuestion2?"],
"uiActionCode": "TextWithButton"
}
]
If I am following this correctly, one level of unnesting is sufficient. You can then use the JSON accessors to get the results you want:
SELECT
r.responseId,
r.chatId,
d.events ->> 'uiActionCode' AS output_uiActionCode,
d.events -> 'text' ->> 0 AS output_text,
d.events -> 'data' AS output_data,
FROM watson_response_table watsonResponse r
CROSS JOIN LATERAL jsonb_array_elements(r.watsonresponse_output) AS d(events)
WHERE d.events ->> 'uiActionCode' = 'TextWithButton'
Note that there is an important difference between accessors -> and ->>. The former returns an object, while the latter returns a text value. You need to carefully pick the correct operator according to what needs to be done for each field.

How to parse multiple geojson feature types and write to SQL Geography column

This is the follow up question from here.
Given: A geojson file with 3 different geometry types (Point, Polygon, Line) and multiple properties (AssetType, GeoID, etc.), write each geometry type and its corresponding properties to a single Azure SQL record which uses a Geography-type column.
Work so far: I have separate SQL statements that will push the properties alone, then push the Polygon alone into a Geography-type column. But this results in the properties in one record and the geography in another (the graphic doesn't exactly match the script below, illustrative purposes only).
Questions: How should the scripts be combined to write both properties and geometry to the same record? How should the script account for Point, Polygon and Line geometry types as they are encountered in the geojson file?
Any help would be greatly appreciated.
-- Create the table
IF OBJECT_ID('dbo.fixedAssets', 'U') IS NOT NULL
DROP TABLE dbo.fixedAssets
GO
CREATE TABLE dbo.fixedAssets
(
Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
GeoId [NVARCHAR] (50),
AssetType [NVARCHAR](255),
Status [NVARCHAR](255),
Desc [NVARCHAR](255),
GeoType [NVARCHAR](255),
GeoCoord [GEOGRAPHY]
)
GO
-- Create sample geojson
DECLARE #JsonSample nvarchar(max) =
'{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"geoID": "001",
"geoType": "Residential",
"geoStatus": "Active",
"geoDesc" : "Cool place"
},
"geometry": {
"type": "Point",
"coordinates": [
-122.38563537597656,
47.57368844862113
]
}
},
{
"type": "Feature",
"properties": {
"geoID": "004",
"geoType": "Commercial Prod",
"geoStatus": "Active",
"geoDesc" : "Don't go here"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-122.33181953430176,
47.57808903904011
],
[
-122.32658386230469,
47.57808903904011
],
[
-122.32658386230469,
47.58214188724162
],
[
-122.33181953430176,
47.58214188724162
],
[
-122.33181953430176,
47.57808903904011
]
]
]
}
},
{
"type": "Feature",
"properties": {
"geoID": "007",
"geoType": "Road",
"geoStatus": "Active",
"geoDesc" : "To nowhere"
},
"geometry": {
"type": "LineString",
"coordinates": [
[
-122.39275932312012,
47.583994513354966
],
[
-122.40022659301758,
47.57889963377935
],
[
-122.39275932312012,
47.57768373696443
],
[
-122.40031242370604,
47.57473072714337
]
]
}
}
]
}';
--This works for inserting properties into their own records
INSERT INTO dbo.fixedAssets_DEV (GeoId, AssetType, Status, Desc, GeoType)
SELECT
GeoId,
AssetType,
Status,
Desc,
GeoType
FROM
OPENJSON(#JsonSample, '$.features')
WITH (
GeoId [NVARCHAR] (50) '$.properties.geoId',
AssetType [NVARCHAR](300) '$.properties.Type',
Status [NVARCHAR](300) '$.properties.Status',
Desc [NVARCHAR](300) '$.properties.Desc',
GeoType [NVARCHAR](300) '$.geometry.type'
)
GO
-- This works for inserting Polygon into Geography column, but its writing all features to one record instead of each record.
INSERT INTO dbo.fixedAssets (GeoCoord)
SELECT
geography::STPolyFromText('POLYGON ((' + STRING_AGG(CAST(Long + ' ' + Lat as varchar(max)), ',') + '))',4326).ReorientObject() AS fGeofenceCoord
FROM
(
SELECT
Long,
Lat
FROM
OPENJSON(#JsonSample, '$.features[0].geometry.coordinates[0]')
WITH
(
Long varchar(100) '$[0]',
Lat varchar(100) '$[1]'
)
)d
-- Need SQL for inserting Point and Line features into the GEOGRAPHY column.
-- Need to combine these statements into a single script that can parse a geojson file and store data accordingly