How to remove object by value from a JSONB type array? - sql

I want to remove a JSONB object by their unique 'id' value from a JSONB array. I am no expert at writing SQL code, but I managed to write the concatenate function.
For an example: Remove this object from an array below.
{
"id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e",
"title": "Wikipedia",
"links": [
"https://en.wikipedia.org/1",
"https://en.wikipedia.org/2"
]
},
Schema:
CREATE TABLE users (
url text not null,
user_id SERIAL PRIMARY KEY,
name VARCHAR,
list_of_links jsonb default '[]'
);
list_of_links format:
[
{
"id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e",
"title": "Wikipedia",
"links": [
"https://en.wikipedia.org/1",
"https://en.wikipedia.org/2"
]
},
{
"id": "451ac172-b93e-4158-8e53-8e9031cfbe72",
"title": "Russian Wikipedia",
"links": [
"https://ru.wikipedia.org/wiki/",
"https://ru.wikipedia.org/wiki/"
]
},
{
"id": "818b99c8-479b-4846-ac15-4b2832ec63b5",
"title": "German Wikipedia",
"links": [
"https://de.wikipedia.org/any",
"https://de.wikipedia.org/any"
]
},
...
]
The concatenate function:
update users set list_of_links=(
list_of_links || (select *
from jsonb_array_elements(list_of_links)
where value->>'id'='ad26e2be-19fd-4862-8f84-f2f9c87b582e'
)
)
where url='test'
returning *
;

Your json data is structured so you have to unpack it, operate on the unpacked data, and then repack it again:
SELECT u.url, u.user_id, u.name,
jsonb_agg(
jsonb_build_object('id', l.id, 'title', l.title, 'links', l.links)
) as list_of_links
FROM users u
CROSS JOIN LATERAL jsonb_to_recordset(u.list_of_links) AS l(id uuid, title text, links jsonb)
WHERE l.id != 'ad26e2be-19fd-4862-8f84-f2f9c87b582e'::uuid
GROUP BY 1, 2, 3
The function jsonb_to_recordset is a set-returning function so you have to use it as a row source, joined to its originating table with the LATERAL clause so that the list_of_links column is available to the function to be unpacked. Then you can delete the records you are not interested in using the WHERE clause, and finally repack the structure by building the record fields into a jsonb structure and then aggregating the individual records back into an array.

I wrote this on JS but that does not matter to how it works. Essentially, its getting all the items from the array, then finding the matching id which returns an index. And using that index, I use "-" operator which takes the index and removes it from the array. Sorry if my grammar is bad.
//req.body is this JSON object
//{"url":"test", "id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e"}
var { url, id } = req.body;
pgPool.query(
`
select list_of_links
from users
where url=$1;
`,
[url],
(error, result) => {
//block code executing further if error is true
if (error) {
res.json({ status: "failed" });
return;
}
if (result) {
// this function just returns the index of the array element where the id matches from request's id
// 0, 1, 2, 3, 4, 5
var index_of_the_item = result.rows.list_of_links
.map(({ id: db_id }, index) =>
db_id === id ? index : false
)
.filter((x) => x !== false)[0];
//remove the array element by it's index
pgPool.query(
`
update users
set list_of_links=(
list_of_links - $1::int
)
where url=$2
;
`,
[index_of_the_item, url], (e, r) => {...}
);
}
}
);

Related

Accessing a tag in an array element of a postgres table with json column

My Postgres DB Table BookingDetails has columns - id(bigint), info(jsonb)
My Json structure:
"bookingType": “special”,
“travellers”: [
{
"id": 1,
"nationality": "SomeValue",
}
],
“someTag”: “abc”
}
The travellers here is an array (and in this example, there’s only one element)
I want to:
Select records where nationality=‘CertainValue’
Update records set nationality=‘AnotherValue’ where nationality=‘CertainValue’
I could retrieve the array using:
select CAST (info->'travellers' AS TEXT) from BookingDetails
[
{
"id": 1,
"nationality": "SomeValue",
}
]
I could retrieve the array first element using:
select CAST (info->'travellers'->>0 AS TEXT) from BookingDetails
{
"id": 1,
"nationality": "SomeValue"
}
But how to do a select based on nationality=‘CertainValue’?
try this :
SELECT jsonb_set(info, array['travellers', (a.id - 1) :: text, 'nationality'], to_jsonb('AnotherValue' :: text))
FROM BookingDetails
CROSS JOIN LATERAL jsonb_array_elements(info->'travellers') WITH ORDINALITY AS a(content, id)
WHERE a.content->>'nationality' = 'CertainValue'
see the test result in dbfiddle

Postgresql convert array of objects to map of objects

I have a table in the data base which has a column texts of type jsonb which looks like:
{
"texts": [
{
"locale": "ar",
"textDictionary": {
"key1": "arabic1",
"key2": "arabic2"
}
},
{
"locale": "en",
"textDictionary": {
"key1": "english1",
"key2": "english2"
}
}
]
}
Given the structure above, the goal is to convert this column to use a map of locales to dictionary instead of an array of locales, this is in order to support indexing the tsvector by locale in a more decent way like this:
CREATE INDEX texts_index ON docs
USING gin ( to_tsvector('english',texts->'en') );
Desired structure:
{
"ar": {
"key1": "arabic1",
"key2": "arabic2"
},
"en": {
"key1": "english1",
"key2": "english2"
}
}
Any advice would be great! Thank you very much! - Is there anyway doing that in PostgreSQL, or should this be done in the application level?
You can convert this by unnesting and aggregating back:
select id, jsonb_object_agg(x.e ->> 'locale', x.e -> 'textDictionary')
from data d
cross join jsonb_array_elements(d.texts -> 'texts') as x(e)
group by id;
This can be used for an UPDATE statement:
update data
set texts = t.new_texts
from (
select id, jsonb_object_agg(x.e ->> 'locale', x.e -> 'textDictionary') as new_texts
from data d
cross join jsonb_array_elements(d.texts -> 'texts') as x(e)
group by id
) t
where t.id = data.id;
You can try this:
update docs
set texts= (
select jsonb_object_agg(j->>'locale',j->'textDictionary')
from
jsonb_array_elements(texts->'texts') j
)
DEMO

Query values from each json in a json array on postgresql

Each record in my database has a json list, from which I'm trying to return a value inside each element.
I'm trying to return
environmental services, energy & utilities
From
[
{
"key": "environmental services"
},
{
"key": "energy & utilities"
}
]
And.
construction
From
[
{
"key": "construction"
}
]
Where the length of each json list in each record is undetermined.
I can get the first value with:
select
column_name -> 0 -> 'key'
from table
But how to get all.
How can I do this in postgresql?
Use the function jsonb_array_elements() in a lateral join to get all elements of the arrays and the aggregate function string_agg():
with my_table(json_col) as (
values
('[
{
"key": "environmental services"
},
{
"key": "energy & utilities"
}
]'::jsonb),
('[
{
"key": "construction"
}
]')
)
select string_agg(value->>'key', ', ')
from my_table
cross join jsonb_array_elements(json_col)
group by json_col -- use PK here (if exists)
string_agg
--------------------------------------------
environmental services, energy & utilities
construction
(2 rows)

Invalid column name on stream analytics, when column exists

Given this dataset:
[
{
"dataChannelId": 8516,
"measures": [
{
"dateTime": "2019-01-01T12:00:00",
},
{
"dateTime": "2019-01-02T12:00:00",
}
}]
And this query:
WITH
temp AS
(
SELECT
dataChannelId,
arrayElement.ArrayValue as element
FROM GriegInputStream
CROSS APPLY GetArrayElements([mesurasdfes]) AS arrayElement
)
SELECT
temp.dataChannelId as sensorId, temp.element.dateTime, temp.element.value,temp.element.unit,temp.element.maxValue, temp.element.minValue
INTO
Sensoroutput
FROM
temp
I get invalid column name, does not exist on dataChannelId, but measures seem to work fine. How can I access this value without stream analytics complaining?
Your sample json data lack a square bracket ].
sample data:
[
{
"dataChannelId": 8516,
"measures": [
{
"dateTime": "2019-01-01T12:00:00",
},
{
"dateTime": "2019-01-02T12:00:00",
}
]
}
]
Query sql:
WITH
temp AS
(
SELECT
jsoninput.dataChannelId,
arrayElement.ArrayValue as element
FROM jsoninput
CROSS APPLY GetArrayElements(jsoninput.measures) AS arrayElement
)
SELECT
temp.dataChannelId as sensorId, temp.element.dateTime
INTO
Sensoroutput
FROM
temp
Output:

How to join jsonb array elements in Postgres?

I am using Postgres 9.5, and I have the following tables:
Users
id UUID
name TEXT
Images
id UUID
key TEXT
width INTEGER
height INTEGER
Posts
id UUID
title TEXT
author_id UUID
content JSONB
The posts' content is like:
[
{ "type": "text", "text": "learning pg" },
{ "type": "image", "image_id": "8f4422b4-3936-49f5-ab02-50aea5e6755f" },
{ "type": "image", "image_id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835" },
{ "type": "text", "text": "pg is awesome" }
]
Now I want to join the image type of content, and populate them with image_id, like:
{
"id": "cb1267ca-b1ac-4daa-8c7e-72d4c000e9fa",
"title": "Learning join jsonb in Postgres",
"author_id": "deba01b7-ec58-4cc2-b3ae-7dc42e582767",
"content": [
{ "type": "text", "text": "learning pg" },
{
"type": "image",
"image": {
"id": "8f4422b4-3936-49f5-ab02-50aea5e6755f",
"key": "/upload/test1.jpg",
"width": 800,
"height": 600
}
},
{
"type": "image",
"image": {
"id": "57efc97c-b9b4-4cd5-b1e1-3539f5853835",
"key": "/upload/test2.jpg",
"width": 1280,
"height": 720
}
},
{ "type": "text", "text": "pg is awesome" }
]
}
Here is my test sql file:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DROP TABLE IF EXISTS Users;
DROP TABLE IF EXISTS Images;
DROP TABLE IF EXISTS Posts;
CREATE TABLE Users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name text NOT NULL
);
CREATE TABLE Images (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
key TEXT,
width INTEGER,
height INTEGER,
creator_id UUID
);
CREATE TABLE Posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title TEXT,
author_id UUID,
content JSONB
);
DO $$
DECLARE user_id UUID;
DECLARE image1_id UUID;
DECLARE image2_id UUID;
BEGIN
INSERT INTO Users (name) VALUES ('test user') RETURNING id INTO user_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test1.jpg', 800, 600, user_id) RETURNING id INTO image1_id;
INSERT INTO Images (key, width, height, creator_id) VALUES ('upload/test2.jpg', 600, 400, user_id) RETURNING id INTO image2_id;
INSERT INTO Posts (title, author_id, content) VALUES (
'test post',
user_id,
('[ { "type": "text", "text": "learning pg" }, { "type": "image", "image_id": "' || image1_id || '" }, { "type": "image", "image_id": "' || image2_id || '" }, { "type": "text", "text": "pg is awesome" } ]') :: JSONB
);
END $$;
Is there any way to implement this requirement?
SELECT jsonb_pretty(to_jsonb(p)) AS post_row_as_json
FROM (
SELECT id, title, author_id, c.content
FROM posts p
LEFT JOIN LATERAL (
SELECT jsonb_agg(
CASE WHEN c.elem->>'type' = 'image' AND i.id IS NOT NULL
THEN elem - 'image_id' || jsonb_build_object('image', i)
ELSE c.elem END) AS content
FROM jsonb_array_elements(p.content) AS c(elem)
LEFT JOIN images i ON c.elem->>'type' = 'image'
AND i.id = (elem->>'image_id')::uuid
) c ON true
) p;
How?
Unnest the jsonb array, producing 1 row per array element:
jsonb_array_elements(p.content) AS c(elem)
For each element LEFT JOIN to images on the conditions that
... the key 'type' has the value 'image': c.elem->>'type' = 'image'
... the UUID in image_id matches: i.id = (elem->>'image_id')::uuid
An invalid UUID in content would raise an exception.
For image types, where a matching image was found
c.elem->>'type' = 'image' AND i.id IS NOT NULL
remove the key 'image_id' and add the related image row as jsonb value:
elem - 'image_id' || jsonb_build_object('image', i)
Else keep the original element.
Re-aggregate the modified elements to a new content column with jsonb_agg().
Would work with a plain ARRAY constructor as well.
Unconditionally LEFT JOIN LATERAL the result to posts and select all columns, only replace p.content with the generated replacement c.content
In the outer SELECT, convert the whole row to jsonb with a simple to_jsonb().
jsonb_pretty() is only for human-readable representation and totally optional.
All jsonb functions are documented in the manual.