Recursive JSONB postgres - postgresql

I am trying to build a recursive CTE in Postgres that supports both arrays and objects, to return a list of key-value pairs and don't seem to be able to find a good example. This is my current code.
with recursive jsonRecurse as
(
select
j.key as Path
,j.key
,j.value
from jsonb_each(to_jsonb('{
"key1": {
"key2": [
{
"key3": "test3",
"key4": "test4"
}
]
},
"key5": [
{
"key6":
[
{
"key7": "test7"
}
]
}
]
}'::jsonb)) j
union all
select
jr.path || '.' || jr2.Key
,jr2.key
,jr2.value
from jsonRecurse jr
left join lateral jsonb_each(jr.value) jr2 on true
where jsonb_typeof(jr.value) = 'object'
)
select
*
from jsonRecurse;
As you can see the code stops recursing as soon as I hit an array instead of an object. I've tried playing around with using a case statement and putting the function call to jsonb_each or jsonb_array_element in the case statement instead but I get an error telling me to use lateral joins instead.

I have used this example table to make the query more readable:
create table my_table(id serial primary key, jdata jsonb);
insert into my_table (jdata) values
('{
"key1": {
"key2": [
{
"key3": "test3",
"key4": "test4"
}
]
},
"key5": [
{
"key6":
[
{
"key7": "test7"
}
]
}
]
}');
You have to join both jsonb_each(value) and jsonb_array_elements(value) conditionally, depending on the type of value:
with recursive extract_all as
(
select
key as path,
value
from my_table
cross join lateral jsonb_each(jdata)
union all
select
path || '.' || coalesce(obj_key, (arr_key- 1)::text),
coalesce(obj_value, arr_value)
from extract_all
left join lateral
jsonb_each(case jsonb_typeof(value) when 'object' then value end)
as o(obj_key, obj_value)
on jsonb_typeof(value) = 'object'
left join lateral
jsonb_array_elements(case jsonb_typeof(value) when 'array' then value end)
with ordinality as a(arr_value, arr_key)
on jsonb_typeof(value) = 'array'
where obj_key is not null or arr_key is not null
)
select *
from extract_all;
Output:
path | value
--------------------+------------------------------------------------
key1 | {"key2": [{"key3": "test3", "key4": "test4"}]}
key5 | [{"key6": [{"key7": "test7"}]}]
key1.key2 | [{"key3": "test3", "key4": "test4"}]
key5.0 | {"key6": [{"key7": "test7"}]}
key1.key2.0 | {"key3": "test3", "key4": "test4"}
key5.0.key6 | [{"key7": "test7"}]
key1.key2.0.key3 | "test3"
key1.key2.0.key4 | "test4"
key5.0.key6.0 | {"key7": "test7"}
key5.0.key6.0.key7 | "test7"
(10 rows)
Elements of json arrays have no keys, we should use their indexes to build a path. Therefore the function jsonb_array_elements() should be called with ordinality. Per the documentation (see 7.2.1.4. Table Functions):
If the WITH ORDINALITY clause is specified, an additional column of type bigint will be added to the function result columns. This column numbers the rows of the function result set, starting from 1.
The function call
jsonb_array_elements(case jsonb_typeof(value) when 'array' then value end)
with ordinality as a(arr_value, arr_key)
returns pairs (value, ordinality) aliased as (arr_value, arr_key).

Related

Convert individual postgres jsonb array elements to row elements

I have to query a table with 2 columns, id and content. Id is just a uuid and the content column looks like
{
"fields": [
{
"001": "mig00004139229"
},
{
"856": {
"ind1": " ",
"ind2": " ",
"subfields": [
{
"u": "https://some.domain.com"
},
{
"z": "some text"
}
]
}
},
{
"999": {
"subfields": [
{
"i": "81be1acf-11df-4d13-a5c6-4838e3a808ee"
},
{
"s": "3a6aa357-8fd6-4451-aedc-13453c1f2296"
}
]
}
}
]
}
I need to select the id, 001, and 856 elements where the subfield "u" domain matches a string "domain.com" so the output would be
id
001
856
81be1acf-11df-4d13-a5c6-4838e3a808ee
mig00004139229
https://some.domain.com
If this were a flat table, the query would correspond with "select id, 001, 856 from table where 856 like '%domain.com%'"
I can select individual columns based on the criteria I need, but they appear in separate rows except the id which appears with any other individual field in a regular select statement. How would I get the other fields to appear in the same row since it's part of the same record?
Unfortunately, my postgres version doesn't support jsonb_path_query, so I've been trying something along the lines of:
SELECT id, jsonb_array_elements(content -> 'fields') -> '001',
jsonb_array_elements(content -> 'fields') -> '856' -> 'subfields'
FROM
mytable
WHERE....
This method returns the data I need, but the individual elements arrive on separate rows with the with the id in the first column and nulls for every element that is neither the 001 nor 856 e.g.
id
001
856
id_for_first_record
001_first_record
null
id_for_first_record
null
null
id_for_first_record
null
null
id_for_first_record
null
856_first_record
id_for_second_record
001_second_record
null
id_for_second_record
null
null
id_for_second_record
null
null
id_for_second_record
null
856_second_record
Usable, but clunky so I'm looking for something better
I think my query can help you. There are different ways to resolve this, I am not sure if this is the best approach.
I use jsonb_path_query() function with the path for the specified JSON value.
SELECT
id,
jsonb_path_query(content, '$.fields[*]."001"') AS "001",
jsonb_path_query(content, '$.fields[*]."856".subfields[*].u') AS "856"
FROM t
WHERE jsonb_path_query_first(content, '$.fields[*]."856".subfields[*].u' )::text ilike '%domain%';
Output:
id
001
856
81be1acf-11df-4d13-a5c6-4838e3a808ee
"mig00004139229"
"https://some.domain.com"
UPDATED: because of Postgresql version is prior to 12.
You could try something like this, but I think there must be a better approach:
SELECT
t.id,
max(sq1."001") AS "001",
max(sq2."856") AS "856"
FROM t
INNER JOIN (SELECT id, (jsonb_array_elements(content -> 'fields') -> '001')::text AS "001" FROM t) AS sq1 ON t.id = sq1.id
INNER JOIN (SELECT id, (jsonb_array_elements(jsonb_array_elements(content -> 'fields') -> '856' -> 'subfields') -> 'u')::text AS "856" FROM t) AS sq2 ON t.id = sq2.id
WHERE sq2."856" ilike '%domain%'
GROUP BY t.id;

Postgres jsonb: querying multi level

In a Postgres (9+) table there is a column of type jsonb with the following json:
{
"dynamicFields":[
{
"name":"040",
"subfields":[
{
"name":"a",
"value":"abc"
},
{
"name":"a",
"value":"xyz"
}
]
}
]
}
I would like to write a query that return only the rows where the field name equals 040 and subfield a equals xyz.
This is as far as I got, so far:
select e.obj from my_table
cross join lateral jsonb_array_elements(my_column-> 'dynamicFields') as e(obj)
where e.obj ->> 'name' = '040' and e.obj ->> 'subfields' #> '{"name": "a", "value": "xyz"}'::jsonb
How should this query be to achieve this?
e.obj ->> 'subfields' has a text result. You'll want to use e.obj -> 'subfields' that returns the jsonb value where the #> operator works. Also the containment checks needs to have another array as the right hand side, so that it will test whether all values in the right array are contained in the left array - it doesn't work to pass the element object directly.
select e.obj from my_table
cross join lateral jsonb_array_elements(my_column-> 'dynamicFields') as e(obj)
where e.obj ->> 'name' = '040' and e.obj -> 'subfields' #> '[{"name": "a", "value": "xyz"}]'::jsonb
-- ^ ^ ^
(online demo)
As you have an equality condition you can use the "contains" operator directly by providing a JSON value of what you want. There is no need to unnest the arrays.
select *
from my_table
where my_column -> 'dynamicFields' #> '[{"name": "040", "subfields": [{"name":"a", "value": "xyz"}]}]'
Starting with Postgres 12 an alternative is to a SQL/JSON path operator:
select *
from my_table
where my_column #? '$.dynamicFields[*] ? (#.name == "040").subfields[*] ? (#.name == "a" && #.value == "xyz")'

There is a similar function json_merge_patch() in Postgres as in Oracle

My endpoint, accepts client request HTTP method PATCH, a payload content type of JSON Merge Patch (RFC 7396).
https://www.rfc-editor.org/rfc/rfc7396
We used Oracle and it was very convenient to update json content in the database, used function json_merge_patch()
UPDATE table_name SET po_document =
json_mergepatch(po_document, json_by_rfc7396);
https://docs.oracle.com/en/database/oracle/oracle-database/19/adjsn/updating-json-document-json-merge-patch.html
I have not found a similar function in Postgres, jsonb_set() and operators || and #-, not convenient for deep patches of json content.
What's the PostgreSQL best practice for deep patching json content?
Example:
SELECT json_merge_patch(
'{"root": {"k1": "v1", "k2": "v2"} }'::jsonb, -- source JSON
'{"root": {"k1": "upd", "k2": null, "k3": "new"} }'::jsonb -- JSON patch (RFC 7396)
)
Output
{"root": {"k1": "upd","k3": "new"} }
The spec is simple enough to follow with recursion.
create or replace function jsonb_merge_patch(v_basedoc jsonb, v_patch jsonb)
returns jsonb as $$
with recursive patchexpand as(
select '{}'::text[] as jpath, v_patch as jobj, jsonb_typeof(v_patch) as jtype, 0 as lvl
union all
select p.jpath||o.key as jpath, p.jobj->o.key as jobj, jsonb_typeof(p.jobj->o.key) as jtype, p.lvl + 1 as lvl
from patchexpand p
cross join lateral jsonb_each(case when p.jtype = 'object' then p.jobj else '{}'::jsonb end) as o(key, value)
), pathnum as (
select *, row_number() over (order by lvl, jpath) as rn
from patchexpand
), apply as (
select case
when jsonb_typeof(v_basedoc) = 'object' then v_basedoc
else '{}'::jsonb
end as basedoc,
p.rn
from pathnum p
where p.rn = 1
union all
select case
when p.jtype = 'object' then a.basedoc
when p.jtype = 'null' then a.basedoc #- p.jpath
else jsonb_set(a.basedoc, p.jpath, p.jobj)
end as basedoc,
p.rn
from apply a
join pathnum p
on p.rn = a.rn + 1
)
select case
when jsonb_typeof(v_patch) != 'object' then v_patch
else basedoc
end
from apply
order by rn desc
limit 1;
$$
language sql;
Testing with the example in the RFC:
select jsonb_pretty(jsonb_merge_patch('{
"title": "Goodbye!",
"author" : {
"givenName" : "John",
"familyName" : "Doe"
},
"tags":[ "example", "sample" ],
"content": "This will be unchanged"
}'::jsonb,
'{
"title": "Hello!",
"phoneNumber": "+01-123-456-7890",
"author": {
"familyName": null
},
"tags": [ "example" ]
}'::jsonb));
jsonb_pretty
------------------------------------------
{ +
"tags": [ +
"example" +
], +
"title": "Hello!", +
"author": { +
"givenName": "John" +
}, +
"content": "This will be unchanged",+
"phoneNumber": "+01-123-456-7890" +
}
(1 row)
Testing with the example in your question:
SELECT jsonb_merge_patch(
'{"root": {"k1": "v1", "k2": "v2"} }'::jsonb, -- source JSON
'{"root": {"k1": "upd", "k2": null, "k3": "new"} }'::jsonb -- JSON patch (RFC 7396)
);
jsonb_merge_patch
--------------------------------------
{"root": {"k1": "upd", "k3": "new"}}
(1 row)
Leaving my 2 cents for a more compact solution here, based on this post:
CREATE OR REPLACE FUNCTION json_merge_patch("target" jsonb, "patch" jsonb) RETURNS jsonb AS $$
BEGIN
RETURN COALESCE(jsonb_object_agg(
COALESCE("tkey", "pkey"),
CASE
WHEN "tval" ISNULL THEN "pval"
WHEN "pval" ISNULL THEN "tval"
WHEN jsonb_typeof("tval") != 'object' OR jsonb_typeof("pval") != 'object' THEN "pval"
ELSE json_merge_patch("tval", "pval")
END
), '{}'::jsonb)
FROM jsonb_each("target") e1("tkey", "tval")
FULL JOIN jsonb_each("patch") e2("pkey", "pval")
ON "tkey" = "pkey"
WHERE jsonb_typeof("pval") != 'null'
OR "pval" ISNULL;
END;
$$ LANGUAGE plpgsql;
As far as I'm concerned, it follows the RFC 7396.

org.postgresql.util.PSQLException: ERROR: set-returning functions are not allowed in WHERE

I need a help for the below mentioned detail,
I am using Postgres + Spring-Data-JPA. Moreover, I have used the jsonb data type for storing data.
I am trying to execute a query, but it gives me the following error:
ERROR: set-returning functions are not allowed in WHERE
The cause here is that I have added a jsonb condition in the WHERE clause (kindly refer to the below query for more detail).
Query (I have renamed column name just because of hiding the actual column name):
select distinct
jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'firstName' as firstName,
jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'lastName' as lastName,
jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'country' as country
from
tale1 table10_
left outer join
table2 table21_
on table10_.end_user_id=table21_.end_user_id
left outer join
table3 table32_
on table10_.manufacturer_id=table32_.manufacturer_id
where
table21_.end_user_uuid=(
?
)
and table21_.is_active=true
and table32_.manufacturer_uuid=(
?
)
and table32_.is_active=true
and table10_.is_active=true
and table32_.is_active=true
and jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'action' = ('PENDING')
order by
jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'firstName',
jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'lastName'
limit ?
The following line in the above query is causing the error:
and jsonb_array_elements(initiated_referral_detail->'listOfAttribue')::jsonb
->> 'action' = ('PENDING')
Can anyone please guide me about how do fetch data from the inner JSON? Especially in my case I have an inner List and a few elements inside.
I recommend a lateral join with jsonb_array_elements for cases like that. Here is an example:
CREATE TABLE tale1 (
id integer PRIMARY KEY,
initiated_referral_detail jsonb NOT NULL
);
INSERT INTO tale1 VALUES
(1, '{
"name": "one",
"listOfAttribue": [
{ "id": 1, "action": "DONE"},
{ "id": 2, "action": "PENDING" },
{ "id": 3, "action": "ACTIVE" }
]
}');
INSERT INTO tale1 VALUES
(2, '{
"name": "two",
"listOfAttribue": [
{ "id": 1, "action": "DONE"},
{ "id": 2, "action": "ACTIVE" }
]
}');
To find all ids where the associated JSON contains an array element with action = PENDING, you can query like this:
SELECT DISTINCT id
FROM tale1 CROSS JOIN LATERAL
jsonb_array_elements(initiated_referral_detail -> 'listOfAttribue') AS attr
WHERE attr ->> 'action' = 'PENDING';

Query rows for matching JSONB column where key ends with a name and key value is a specific value

Given the following rows with a jsonb column details. How do I write a query so that records where the key name ends with _col with value B are selected. So records with ids 1, 2.
id | details
1 | { "one_col": "A", "two_col": "B" }
2 | { "three_col": "B" }
3 | { another: "B" }
So far I've only find ways to match based on the value, not the key.
Use the function jsonb_each_text() which gives json objects as pairs (key, value):
with the_data(id, details) as (
values
(1, '{ "one_col": "A", "two_col": "B" }'::jsonb),
(2, '{ "three_col": "B" }'),
(3, '{ "another": "B" }')
)
select t.*
from the_data t,
lateral jsonb_each_text(details)
where key like '%_col'
and value = 'B';
id | details
----+----------------------------------
1 | {"one_col": "A", "two_col": "B"}
2 | {"three_col": "B"}
(2 rows)