Postgresql update JSONB object to array - sql

I don't know why, but probably PHP persisted some of my data as object and some of them as array. My table looks something like:
seller_info_address Table:
ID (INT) | address (JSONB) |
------------+--------------------------------------------------|
1 | {"addressLines":{"0":"Technology Park",...},...} |
2 | {"addressLines":["Technology Park",...],...} |
Some addressLines are objects:
{
"addressLines": {
"0": "Technology Park",
"1": "Blanchard Road",
"3": "Dublin",
"4": "2"
},
"companyName": "...",
"emailAddress": [],
"...": "..."
}
Some addressLines are arrays:
{
"addressLines": [
"Technology Park",
"Blanchard Road",
"Dublin",
"2"
],
"companyName": "...",
"emailAddress": [],
"...": "..."
}
I would like to equalize the data with a SQL query, but I'm not sure how to do it. All addressLines persisted as object should be updated to array form.
I am grateful for help, thanks!

You can convert the objects to an array using this:
select id, (select jsonb_agg(e.val order by e.key::int)
from jsonb_each(sia.address -> 'addressLines') as e(key,val))
from seller_info_address sia
where jsonb_typeof(address -> 'addressLines') = 'object'
The where condition makes sure we only do this for addresslines that are not an array.
The aggregation used can also be used inside an UPDATE statement:
update seller_info_address
set address = jsonb_set(address, '{addressLines}',
(select jsonb_agg(e.val order by e.key::int)
from jsonb_each(address -> 'addressLines') as e(key,val))
)
where jsonb_typeof(address -> 'addressLines') = 'object';

Ok, I have now found a solution myself. Definitely not the most eloquent solution. I'm sure there's a nicer and more efficient one, but it works...
DROP FUNCTION update_address_object_to_array(id INTEGER);
CREATE OR REPLACE FUNCTION
update_address_object_to_array(id INTEGER)
RETURNS VOID AS
$UPDATE_ADDRESS_OBJECT_TO_ARRAY$
BEGIN
UPDATE seller_info_address
SET address = jsonb_set(address, '{addressLines}', (
SELECT CASE
WHEN jsonb_agg(addressLines) IS NOT NULL THEN jsonb_agg(addressLines)
ELSE '[]'
END
FROM seller_info_address sia,
jsonb_each(address #> '{addressLines}') as t(key, addressLines)
WHERE jsonb_typeof(sia.address -> 'addressLines') = 'object'
AND seller_info_id = update_address_object_to_array.id
), true)
WHERE seller_info_id = update_address_object_to_array.id
AND jsonb_typeof(address -> 'addressLines') = 'object';
END
$UPDATE_ADDRESS_OBJECT_TO_ARRAY$
LANGUAGE 'plpgsql';
SELECT update_address_object_to_array(sia.seller_info_id)
FROM seller_info_address sia
WHERE jsonb_typeof(address -> 'addressLines') = 'object';
The inner SELECT fetches all lines in the addressLines object using jsonb_each and then aggregates them into an array using jsonb_agg. The conditional expressions is to prevented null cases.
The result is then stored in the UPDATE via jsonb_set to the required position in the json. The WHERES should be self-explanatory.

Related

How can I modify all values that match a condition inside a json array?

I have a table which has a JSON column called people like this:
Id
people
1
[{ "id": 6 }, { "id": 5 }, { "id": 3 }]
2
[{ "id": 2 }, { "id": 3 }, { "id": 1 }]
...and I need to update the people column and put a 0 in the path $[*].id where id = 3, so after executing the query, the table should end like this:
Id
people
1
[{ "id": 6 }, { "id": 5 }, { "id": 0 }]
2
[{ "id": 2 }, { "id": 0 }, { "id": 1 }]
There may be more than one match per row.
Honestly, I didnĀ“t tried any query since I cannot figure out how can I loop inside a field, but my idea was something like this:
UPDATE mytable
SET people = JSON_SET(people, '$[*].id', 0)
WHERE /* ...something should go here */
This is my version
SELECT VERSION()
+-----------------+
| version() |
+-----------------+
| 10.4.22-MariaDB |
+-----------------+
If the id values in people are unique, you can use a combination of JSON_SEARCH and JSON_REPLACE to change the values:
UPDATE mytable
SET people = JSON_REPLACE(people, JSON_UNQUOTE(JSON_SEARCH(people, 'one', 3)), 0)
WHERE JSON_SEARCH(people, 'one', 3) IS NOT NULL
Note that the WHERE clause is necessary to prevent the query replacing values with NULL when the value is not found due to JSON_SEARCH returning NULL (which then causes JSON_REPLACE to return NULL as well).
If the id values are not unique, you will have to rely on string replacement, preferably using REGEXP_REPLACE to deal with possible differences in spacing in the values (and also avoiding replacing 3 in (for example) 23 or 34:
UPDATE mytable
SET people = REGEXP_REPLACE(people, '("id"\\s*:\\s*)2\\b', '\\14')
Demo on dbfiddle
As stated in the official documentation, MySQL stores JSON-format strings in a string column, for this reason you can either use the JSON_SET function or any string function.
For your specific task, applying the REPLACE string function may suit your case:
UPDATE
mytable
SET
people = REPLACE(people, CONCAT('"id": ', 3, ' '), CONCAT('"id": ',0, ' '))
WHERE
....;

Check if row contains a nested item in DynamoDB?

I am trying to use PartiQL with DynamoDB to perform SQL queries to check if a device is inactive and contains an error. Here's is the query I am using:
SELECT *
FROM "table"
WHERE "device"."active" = 0 AND "device"."error" IS NOT NULL
However I've noticed that even if a device doesn't have the error item, the query still returns a row. How can I query a device that only contains the error item?
With error item
{
"id": "value",
"name": "value,
"device": {
"active": 0,
"error": {
"reason": "value"
}
}
}
Without error item
{
"id": "value",
"name": "value,
"device": {
"active": 0
}
}
You're looking for IS NOT MISSING :) That's the partiql version of the filter expression operator function attribute_exists.
Given a table with a primary key PK, sort key SK, and the following data:
PK
SK
myMap
foo
1
{}
foo
2
{"test": {}}
-- Returns both foo 1 and foo 2
SELECT *
FROM "my-table"
WHERE "PK" = 'foo' AND "myMap"."test" IS NOT NULL
-- Returns just foo 2
SELECT *
FROM "my-table"
WHERE "PK" = 'foo' AND "myMap"."test" IS NOT MISSING
Also made sure my example specifies the PK in the WHERE clause - otherwise, your query will be a full scan. Maybe that's what you want, though. Just something to be aware of.

jsonb find a value in an array

There is such a data structure:
Column "recipient" type jsonb
{
"phoneNumbers": [
{
"isDefault": true,
"type": "MOBILE",
"number": "3454654645"
},
{
"isDefault": true,
"type": "MOBILE",
"number": "12423543645"
}
]
}
I need to write a search request by number. In the postgres documentation, I did not find a search by value in an array, only it is obtained by an index. It doesn't suit me
I made a query like this, it gets executed, but are there any other ways to search through an array?
SELECT *
FROM my_table
WHERE recipient -> 'phoneNumbers' #> '[{"number":3454654645}]'
That's pretty much the best way, yes.
If you have a (GIST) index on recipient the index would not be used by your condition. But the following could make use of such an index:
SELECT *
FROM my_table
WHERE recipient #> '["phoneNumbers": {"number":3454654645}]}'
If you are using Postgres 12 or later, you can also use a JSON path expression:
SELECT *
FROM my_table
WHERE recipient ## '$.phoneNumbers[*].number == "12423543645"'
If you can't pass a JSON object to your query, you can use an EXISTS sub-select:
SELECT mt.*
FROM my_table mt
WHERE EXISTS (SELECT *
FROM jsonb_array_elements_text(mt.recipient -> 'phoneNumbers') as x(element)
WHERE x.element ->> 'number' = '3454654645')
The '3454654645' can be passed as a parameter to your query. This will never make use of an index though.

How to update multiple nested keys in a json field in single update query?

I'm trying to write a function that updates a json (not jsonb) field (called settings) in a table (called user).
My json object looks like this (however it might not have some of the keys on any nesting level):
{
"audiences": ["val1", "val2"],
"inviteNumber": "123",
"workExperience": [
{
"company": {
"name": "Ace",
"industry": "Accounting",
"revenues": "1M-10M",
"size": "1-10"
},
"title": "Product",
"startDate": {
"month": 1,
"year": 2018
}
}
],
"notifications": {
"emailNotifications": true
},
"jobFunction": "Research & Development",
"phoneNumber": "2134447777",
"areasOfInterest": {
"Recruiting and Staffing": true
}
}
I need to be able to update the "title" and "name" fields of the 0th element inside "workExperience" array.
What I currently have is this:
create or replace function my_function()
returns trigger language plpgsql as $$
declare
companyName character varying(255);
begin
select company.name into companyName from company where id = new.companyid;
update "user" set
email = new.email,
"settings" = jsonb_set(
"settings"::jsonb,
'{workExperience,0}'::TEXT[],
format(
'{"company": {"name": %s}, "title": %s}',
'"' || companyName || '"', '"' || new.title || '"'
)::jsonb
)
where id = new.userid;
return new;
end $$;
However the above implementation rewrites the while workExperience object removing the keys other than company and title.
I've tried looking through this answer on SO, but still wasn't able to implement the updating correctly.
I see that the problem is with how the jsonb_set works and it does just what I tell it to do: set 0th element of "workExperience" array to the object I define inside format function.
Seems like I need to use multiple jsonb_set one inside another, but I can't figure out the way to do it correctly.
How can I update my json field correctly without removing other keys from the object?
jsonb_set() returns the modified JSON value.
You could nest the calls and change the company name first, and use the result of that as the input to another jsonb_set().
"settings" = jsonb_set(jsonb_set("settings"::jsonb, '{workExperience,0,company,name}', to_jsonb(companyname)),
'{workExperience,0,title}', to_jsonb(new.title)
)

Postgresql search if exists in nested jsonb

I'm new with jsonb request and i got a problem. Inside an 'Items' table, I have 'id' and 'data' jsonb. Here is what can look like a data:
[
{
"paramId": 3,
"value": "dog"
},
{
"paramId": 4,
"value": "cat"
},
{
"paramId": 5,
"value": "fish"
},
{
"paramId": 6,
"value": "",
"fields": [
{
"paramId": 3,
"value": "cat"
},
{
"paramId": 4,
"value": "dog"
}
]
},
{
"paramId": 6,
"value": "",
"fields": [
{
"paramId": 5,
"value": "cat"
},
{
"paramId": 3,
"value": "dog"
}
]
}
]
The value in data is always an array with object inside but sometimes the object can have a 'fields' value with objects inside. It is maximum one level deep.
How can I select the id of the items which as for example an object containing "paramId": 3 and "value": "cat" and also have an object with "paramId": 5 and "value" LIKE '%ish%'.
I already have found a way to do that when the object is on level 0
SELECT i.*
FROM items i
JOIN LATERAL jsonb_array_elements(i.data) obj3(val) ON obj.val->>'paramId' = '3'
JOIN LATERAL jsonb_array_elements(i.data) obj5(val) ON obj2.val->>'paramId' = '5'
WHERE obj3.val->>'valeur' = 'cat'
AND obj5.val->>'valeur' LIKE '%ish%';
but I don't know how to search inside the fields array if fields exists.
Thank you in advance for you help.
EDIT:
It looks like my question is not clear. I will try to make it better.
What I want to do is to find all the 'item' having in the 'data' column objects who match my search criteria. This without looking if the objects are at first level or inside a 'fields' key of an object.
Again for example. This record should be selected if I search:
'paramId': 3 AND 'value': 'cat
'paramId': 4 AND 'value': LIKE '%og%'
the matching ones are in the 'fields' key of the object with 'paramId': 6 and I don't know how to do that.
This can be expressed using a JSON/Path expression without the need for unnesting everything
To search for paramId = 3 and value = 'cat'
select *
from items
where data #? '$[*] ? ( (#.paramId == 3 && #.value == "cat") || exists( #.fields[*] ? (#.paramId == 3 && #.value == "cat")) )'
The $[*] part iterates over all elements of the first level array. To check the elements in the fields array, the exists() operator is used to nest the expression. #.fields[*] iterates over all elements in the fields array and applies the same expression again. I don't see a way how repeating the values could be avoided though.
For a "like" condition, you can use like_regex:
select *
from items
where data #? '$[*] ? ( (#.paramId == 4 && #.value like_regex ".*og.*") || exists( #.fields[*] ? (#.paramId == 4 && #.value like_regex ".*og.*")) )'
For now I have found a solution but it is not really clean and I don't know how it will perform in production with 10M records.
SELECT i.id, i.data
FROM ( -- A;
select it.id, it.data, i as value
from items it,
jsonb_array_elements(it.data) i
union
select it.id, it.data, f as value
from items it,
jsonb_array_elements(it.data) i,
jsonb_array_elements(i -> 'fields') f
) as i
WHERE (i.value ->> 'paramId' = '5' -- B1;
AND i.value ->> 'value' LIKE '%ish%')
OR (i.value ->> 'paramId' = '3' -- B2;
AND i.value ->> 'value' = 'cat')
group by i.id, i.data
having COUNT(*) >= 2; -- C;
A: I "flatten" the first and second level (second level is in 'fields' key)
B1, B2: These are my search criteria
C: I make sure the fields have all the criteria matching. If 3 criteria --> COUNT(*) >=3
It really doesn't look clean to me. It is working for dev purpose but I think there is a better way to do it.
If somebody have an idea Big thanks to him/her!