How to delete a jsonb item in a nested psql array - sql

I have a table users:
`CREATE TABLE users(
id SERIAL PRIMARY KEY NOT NULL,
username TEXT UNIQUE,
saved_articles JSONB[],
)`
I added a user like so:
"INSERT INTO users (username, saved_articles) VALUES (?, array[]::jsonb[]) RETURNING id, username, saved_articles"
After adding some articles I have this data shape:
{ id: 1,
username: 'test',
saved_articles:
[ { url: 'test',
title: '',
author: '',
source: '',
content:"",
urlToImage: ''
},
{ url: 'not-test',
title: '',
author: '',
source: '',
content:"",
urlToImage: ''
}
]
}
I want to be able to delete a specific item from the saved_articles array based on the url value.
For example, if my url value is 'test', after running the query my data should look like:
{ id: 1,
username: 'test',
saved_articles:
[ { url: 'not-test',
title: '',
author: '',
source: '',
content:"",
urlToImage: ''
}
]
}

First of all, the format of JSONB columns's value should be fixed. That might be tested through CASTing AS JSONB by a SELECT statement such as
SELECT '{ "id": "1",
"username": "test",
"saved_articles":
[ { "url": "test",
"title": "",
"author": "",
"source": "",
"content":"",
"urlToImage": ""
},
{ "url": "not-test",
"title": "",
"author": "",
"source": "",
"content":"",
"urlToImage": ""
}
]}'::jsonb
whether returns error or not.
Then, remove the desired element from the array by use of jsonb_array_elements(json_data -> 'saved_articles') function together with ->> 'url' != 'test' criteria.
And then reconstruct the array by remaining elements by using jsonb_build_array and jsonb_object_agg.
At the last step concatenate the part which doesn't contain that individual array extracted by json_data #- '{saved_articles}' :
SELECT js0||jsonb_object_agg( 'saved_articles', js1 ) AS "Result JSONB"
FROM
(
SELECT json_data #- '{saved_articles}' AS js0, jsonb_build_array( js ) AS js1
FROM tab
CROSS JOIN jsonb_array_elements(json_data -> 'saved_articles') js
WHERE js ->> 'url' != 'test'
) q
GROUP BY js0
Demo

Related

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

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) => {...}
);
}
}
);

How to get specific json object from json array using json_query in oracle

For example, I have a json array like below
[
{ id: 1, name: "larry" },
{ id: 2, name: "curly" },
{ id: 3, name: "moe" }
]
How can I get the json object which name is curly using json_query function?
The answer depends on your Oracle version, which you did not include. (Always include your database version, especially in questions about JSON!)
In Oracle 19 (I think - perhaps also in Oracle 18) you can use JSON_QUERY with a path expression with a predicate, something like this:
with
sample_data (j_arr) as (
select '
[
{ id: 1, name: "larry" },
{ id: 2, name: "curly" },
{ id: 3, name: "moe" }
]
' from dual
)
select json_query(j_arr, '$[*]?(#.name == "curly")' returning varchar2) as curly
from sample_data
;
CURLY
-----------------
{
"id" : 2,
"name" : "curly"
}

Karate match each on response assertion is failing to identify missing keys in response

I have a match each assertion like below in my code. Just tried creating similar examples as my code, just to explain the issue.
Scenario: Example scenario 1
* def response =
"""
[
{
id: 1,
name: "John",
password: "abc123"
},
{
id: 2,
name: "David",
password: "abc123"
},
{
id: 3,
name: "Mike",
password: "abc123"
},
{
id: 4,
name: "Johny"
}
]
"""
* match each response[*].password contains 'abc123'
Test status : Pass
Password field is missing in object 4(where id=4). Above test is passing for me. I am expecting Karate to fail the test in this case. How can I make my test fail in this case?
Scenario: Example scenario 2
* def response =
"""
[
{
id: 1,
name: "John",
},
{
id: 2,
name: "David",
},
{
id: 3,
name: "Mike",
},
{
id: 4,
name: "Johny"
}
]
"""
* match each response[*].password contains 'abc123'
Test status : Pass
Here, there is no password field at all in response. But my test is passing.
Need a work around to fail these kind of scenarios.
Example 3 :
* def response =
"""
[
{
id: 1,
name: "John",
password: "abc123",
skills :[ "training", "management"
]
},
{
id: 2,
name: "David",
password: "abc123",
skills :[ "training", "management"
]
},
{
id: 3,
name: "David",
password: "abc123",
skills :[ "training", "coding"
]
},
{
id: 4,
name: "David",
password: "abc123",
skills :[ "training", "management"
]
}
]
"""
Considering * match each response contains { password: 'abc123' } format(mentioned by #peter) to check example 1 and 2, what if I want to check skills array having 'training' in each object under response? How can I achieve this?
you can use match each to validate the json schema
https://github.com/intuit/karate#match-each
Note that response[*].password is a JsonPath expression that will return an array of all the password key-values found and will return only 3 in your case.
What you are looking for is this:
* match each response contains { password: 'abc123' }

SQL Server JSON Query Filtered Return

I have the following JSON data in a single column (varchar(max)) related to a user:
[{
"ExtensionData": {},
"IsDefault": false,
"MethodType": "OneWaySMS"
},
{
"ExtensionData": {},
"IsDefault": false,
"MethodType": "TwoWayVoiceMobile"
},
{
"ExtensionData": {},
"IsDefault": false,
"MethodType": "PhoneAppOTP"
},
{
"ExtensionData": {},
"IsDefault": true,
"MethodType": "PhoneAppNotification"
}]
Any way to query the users record and a leverage a subquery (or something like it) to return me just the MethodType that is in the same block as "isDefault: true"
So the column returned would just say "PhoneAppNotification" based on the example above?
Try this:
select MethodType
from [table]
cross apply OPENJSON([column], '$')
with (IsDefault varchar(80), MethodType varchar(80))
where IsDefault = 'true'

Return SQL Server database query with nested Json

I am trying to get this kind of answer when I consume my endpoint :
[
{
"McqID":"7EED5396-9151-4E3D-BCBF-FDB72CDD22B7",
"Questions":[
{
"QuestionId":"C8440686-531D-4099-89E9-014CAF9ED054",
"Question":"human text",
"Difficulty":3,
"Answers":[
{
"AnswerId":"7530DCF4-B2D9-48B0-9978-0E4690EA0C34",
"Answer":"human text2",
"IsTrue":false
},
{
"AnswerId":"5D16F17F-E205-42A5-873A-1A367924C182",
"Answer":"human text3",
"IsTrue":false
},
{
"AnswerId":"64E78326-77C3-4628-B9E3-2E8614D63632",
"Answer":"human text4",
"IsTrue":false
},
{
"AnswerId":"199241A9-0EF6-4F96-894A-9256B129CB1F",
"Answer":"human text5",
"IsTrue":true
},
{
"AnswerId":"EDCCAC18-5209-4457-95F2-C91666F8A916",
"Answer":"human text6",
"IsTrue":false
}
]
}
]
}
]
Here's my query (example) :
SELECT
Questions.QcmID AS QcmID,
(SELECT
Questions.id AS QuestionId,
Questions.Intitule AS Question,
Questions.Difficulte AS Difficulty,
(SELECT
Reponses.id AS AnswerId,
Reponses.Libelle AS Answer,
Reponses.IsTrue AS IsTrue
FROM
Reponses
WHERE
Reponses.QuestionID = Questions.id
FOR JSON PATH) AS Answers
FROM
Questions
WHERE
Questions.QcmID = '7EED5396-9151-4E3D-BCBF-FDB72CDD22B7'
FOR JSON PATH) AS Questions
FROM
Questions
WHERE
Questions.QcmID = '7EED5396-9151-4E3D-BCBF-FDB72CDD22B7'
FOR JSON PATH
I want a nested JSON representing my data, but it ends up being formatted like (smaller example) :
[
{
"JSON_F52E2B61-18A1-11d1-B105-00805F49916B":"[{\"QcmID\":\"7EED5396-9151-4E3D-BCBF-FDB72CDD22B7\"}]"
}
]
I've tried everything, FOR JSON PATH, FOR JSON AUTO, JSON_QUERY, etc...
Nothing works. FOR JSON PATH doesn't seem to work with multiple nested collections.
How do I get this result ?
You need to use JOINs as you would normally.
Using FOR JSON AUTO will pick the JOIN alias and if you want more control use the FOR JSON PATH.
I'm going to give you a generic example that will be easy to map to your scenario:
Option 1 - FOR JSON AUTO:
The JOIN alias will be used as the nested collection property name.
SELECT
ent.Id AS 'Id',
ent.Name AS 'Name',
ent.Age AS 'Age',
Emails.Id AS 'Id',
Emails.Email AS 'Email'
FROM Entities ent
LEFT JOIN EntitiesEmails Emails ON Emails.EntityId = ent.Id
FOR JSON AUTO
Option 2 - FOR JSON PATH:
You handle everything and note that the inner select must return a string, here also using FOR JSON PATH.
SELECT
ent.Id AS 'Id',
ent.Name AS 'Name',
ent.Age AS 'Age',
EMails = (
SELECT
Emails.Id AS 'Id',
Emails.Email AS 'Email'
FROM EntitiesEmails Emails WHERE Emails.EntityId = ent.Id
FOR JSON PATH
)
FROM Entities ent
FOR JSON PATH
Both generate the same result:
[{
"Id": 1,
"Name": "Alex",
"Age": 35,
"Emails": [{
"Id": 1,
"Email": "abc#domain.com"
}, {
"Id": 2,
"Email": "def#domain.com"
}, {
"Id": 3,
"Email": "ghi#domain.net"
}]
}, {
"Id": 2,
"Name": "Another Ale",
"Age": 40,
"Emails": [{
"Id": 4,
"Email": "user#skdfh.com"
}, {
"Id": 5,
"Email": "asldkj#als09q834.net"
}]
}, {
"Id": 3,
"Name": "John Doe",
"Age": 33,
"Emails": [{
"Id": 6,
"Email": "ooaoasdjj#ksjsk0913.org"
}]
}, {
"Id": 4,
"Name": "Mario",
"Age": 54,
"Emails": [{}]
}]
Cheers!