PostgreSQL: Delete key/value pair from array with json objects - postgresql

I have a table:
CREATE TABLE movies( id text, data jsonb );
INSERT INTO movies(id, data) VALUES (
'1',
{
"actors": [
{
"name": "actor1",
"email": "actor1#somemail.com"
},
{
"name": "actor2",
"email": "actor2#somemail.com"
}
]
}
);
What I want is to delete the email field (key + value) from each json object of the actors array.
I've tried the following solution and although it does execute, it doesn't have any effect on the array at all:
update movies
set data = jsonb_set(data, '{actors}', (data->'actors') - '{actors, email}')
where id = '1';

To manipulate all items in the array, you will need to use a subquery:
UPDATE movies
SET data = jsonb_set(data, '{actors}', (
SELECT jsonb_agg(actor - 'email')
FROM jsonb_array_elements(data->'actors') actor
))
WHERE id = '1';
(online demo)

You need to specify indexes individually to delete the array element email
update movies
set data = jsonb_set(data, '{actors}', data -> 'actors' #- '{0,email}' #- '{1,email}')
where id = '1';
the path element {1,email} might be replaced by {-1,email} (Negative integers count from the end).
Demo

Related

Using index for jsonb sub keys postgresql

I have a table like:
create table items
(
id int constraint items_pk primary key,
acl jsonb
);
With such items:
id,acl
1,{
"users": {
"2": { <-- The key "2" is the user_id
"role1": {...},
"role2": {...}
},
"3": {
"role1": {...}
}
},
"groups": {...}
}
...
I want to count the number of items where the user "2" has the role "role2", what I do:
SELECT COUNT(*) FROM items WHERE ( acl->'users'->'2' ? 'role2')
The problem is that I want this query to use an index, but I can't make this query to use them. Here are the index I setup:
CREATE INDEX _index1 ON items using gin (acl jsonb_ops);
CREATE INDEX _index2 ON items using gin ((acl->'users') jsonb_ops);
Then I tried this query that is using the index but it is slower ( like 40x ) than the first one so it is unuseful. And also goes beyond the fact that I just want to verify the presence of the "role2" key in acl->'users'->'2'.
SELECT COUNT(*) FROM items WHERE ( acl #> '{"users": {"2": {"role2": {...}}}}');
My question is how can I make this query to use an index keeping my current json data structure ?
I know I can use string arrays and lots of other things to make this usecase work but they imply changing the data structure, and this is not my point here because the problem is that this data structure is used at scale and I want to know if something is possible with this structure.

Querying a many:many relationship on PK of the related table (ie. filtering by related table column)

I have a many:many relationship between 2 tables: note and tag, and want to be able to search all notes by their tagId. Because of the many:many I have a junction table note_tag.
My goal is to expose a computed field on my Postgraphile-generated Graphql schema that I can query against, along with the other properties of the note table.
I'm playing around with postgraphile-plugin-connection-filter. This plugin makes it possible to filter by things like authorId (which would be 1:many), but I'm unable to figure out how to filter by a many:many. I have a computed column on my note table called tags, which is JSON. Is there a way to "look into" this json and pick out where id = 1?
Here is my computed column tags:
create or replace function note_tags(note note, tagid text)
returns jsonb as $$
select
json_strip_nulls(
json_agg(
json_build_object(
'title', tag.title,
'id', tag.id,
)
)
)::jsonb
from note
inner join note_tag on note_tag.tag_id = tagid and note_tag.note_id = note.id
left join note_tag nt on note.id = nt.note_id
left join tag on nt.tag_id = tag.id
where note.account_id = '1'
group by note.id, note.title;
$$ language sql stable;
as I understand the function above, I am returning jsonb, based on the tagid that was given (to the function): inner join note_tag on note_tag.tag_id = tagid. So why is the json not being filtered by id when the column gets computed?
I am trying to make a query like this:
query notesByTagId {
notes {
edges {
node {
title
id
tags(tagid: "1")
}
}
}
}
but right now when I execute this query, I get back stringified JSON in the tags field. However, all tags are included in the json, whether or not the note actually belongs to that tag or not.
For instance, this note with id = 1 should only have tags with id = 1 and id = 2. Right now it returns every tag in the database
{
"data": {
"notes": {
"edges": [
{
"node": {
"id": "1",
"tags": "[{\"id\":\"1\",\"title\":\"Psychology\"},{\"id\":\"2\",\"title\":\"Logic\"},{\"id\":\"3\",\"title\":\"Charisma\"}]",
...
The key factor with this computed column is that the JSON must include all tags that the note belongs to, even though we are searching for notes on a single tagid
here are my simplified tables...
note:
create table notes(
id text,
title text
)
tag:
create table tag(
id text,
title text
)
note_tag:
create table note_tag(
note_id text FK references note.id
tag_id text FK references tag.id
)
Update
I am changing up the approach a bit, and am toying with the following function:
create or replace function note_tags(n note)
returns setof tag as $$
select tag.*
from tag
inner join note_tag on (note_tag.tag_id = tag.id)
where note_tag.note_id = n.id;
$$ language sql stable;
I am able to retrieve all notes with the tags field populated, but now I need to be able to filter out the notes that don't belong to a particular tag, while still retaining all of the tags that belong to a given note.
So the question remains the same as above: how do we filter a table based on a related table's PK?
After a while of digging, I think I've come across a good approach. Based on this response, I have made a function that returns all notes by a given tagid.
Here it is:
create or replace function all_notes_with_tag_id(tagid text)
returns setof note as $$
select distinct note.*
from tag
inner join note_tag on (note_tag.tag_id = tag.id)
inner join note on (note_tag.note_id = note.id)
where tag.id = tagid;
$$ language sql stable;
The error in approach was to expect the computed column to do all of the work, whereas its only job should be to get all of the data. This function all_nuggets_with_bucket_id can now be called directly in graphql like so:
query MyQuery($tagid: String!) {
allNotesWithTagId(tagid: $tagid) {
edges {
node {
id
title
tags {
edges {
node {
id
title
}
}
}
}
}
}
}

Jsonb object parsing in PostgreSql

How to parse jsonb object in PostgreSql. The problem is - object every time is different by structure inside. Just like below.
{
"1":{
"1":{
"level":2,
"nodeType":2,
"id":2,
"parentNode":1,
"attribute_id":363698007,
"attribute_text":"Finding site",
"concept_id":386108004,
"description_text":"Heart tissue",
"hierarchy_id":0,
"description_id":-1,
"deeperCnt":0,
"default":false
},
"level":1,
"nodeType":1,
"id":1,
"parentNode":0,
"concept_id":22253000,
"description_id":37361011,
"description_text":"Pain",
"hierarchy_id":404684003,
"deeperCnt":1,
"default":false
},
"2":{
"1":{
"attribute_id":"363698007",
"attribute_text":"Finding site (attribute)",
"value_id":"321667001",
"value_text":"Respiratory tract structure (body structure)",
"default":true
},
"level":1,
"nodeType":1,
"id":3,
"parentNode":0,
"concept_id":11833005,
"description_id":20419011,
"description_text":"Dry cough",
"hierarchy_id":404684003,
"deeperCnt":1,
"default":false
},
"level":0,
"recAddedLevel":1,
"recAddedId":3,
"nodeType":0,
"multiple":false,
"currNodeId":3,
"id":0,
"lookForAttributes":false,
"deeperCnt":2,
}
So how should I parse all object and for example look if object inside has "attribute_id" = 363698007?
In this case we should get 'true' while selecting data rows in PostgreSql with WHERE statement.
2 question - what index should I use for jsonb column to get wanted results?
Already tried to create btree and gin indexes but even simple select returns 'null' with sql like this:
SELECT object::jsonb -> 'id' AS id
FROM table;
if I use this:
SELECT object
FROM table;
returns firstly described object.
The quick and dirty way (extended upon Collect Recursive JSON Keys In Postgres):
WITH RECURSIVE doc_key_and_value_recursive(id, key, value) AS (
SELECT
my_json.id,
t.key,
t.value
FROM my_json, jsonb_each(my_json.data) AS t
UNION ALL
SELECT
doc_key_and_value_recursive.id,
t.key,
t.value
FROM doc_key_and_value_recursive,
jsonb_each(CASE
WHEN jsonb_typeof(doc_key_and_value_recursive.value) <> 'object' THEN '{}'::jsonb
ELSE doc_key_and_value_recursive.value
END) AS t
)
SELECT t.id, t.data->'id' AS id
FROM doc_key_and_value_recursive AS c
INNER JOIN my_json AS t ON (t.id = c.id)
WHERE
jsonb_typeof(c.value) <> 'object'
AND c.key = 'attribute_id'
AND c.value = '363698007'::jsonb;
Online example: https://dbfiddle.uk/?rdbms=postgres_11&fiddle=57b7c4e817b2dd6580bbf28cbac10981
This may be improved a lot by stopping the recursion as soon as the relevant key and value are found, reverse sort and limit 1, aso. But it does the basic thing generically.
It also shows that jsonb->'id' does work as expected.

PostgreSQL conditional expressions

Is there a way to write two insert statements if a javascript object's value is not an empty string, or one insert statement if it is an empty string in a postgreSQL query using node-postgres?
My database table "job" setup:
CREATE TABLE "job" (
"id" SERIAL PRIMARY KEY,
"company" VARCHAR (20) NOT NULL
);
For example, I would like this object to insert two rows:
{
company: 'Apple',
company_two: 'Google'
}
And I would like this object to insert one row:
{
company: 'Facebook',
company_two: ''
}
If the javascript object has a value for "company_two", I would like to add a second row to the database.
Here is what I have working to insert one row, but this doesn't take the other property into account:
pool.query('INSERT INTO "job" ("company") VALUES $1', [testObject.company]);
Check if the object has a value in the company_two property and act accordingly, for example:
const query = 'INSERT INTO "job" ("company") VALUES ($1)';
if (testObject.company_two != '') {
pool.query(`${query}, ($2)`, [ testObject.company, textObject.company_two ]);
} else {
pool.query(query, [ testObject.company ]);
}
This will insert a single row if company_two is an empty string and two rows if it is not and empty string. Of course you should adjust the if condition to match what you expect.
You can "unnest" the JSON to rows, and then use that as the source for an insert:
insert into job (company)
select t.comp
from jsonb_each_text('{
"company": "Apple",
"company_two": "Google"
}'::jsonb) t(k,comp)
where t.comp <> '';
will insert both companies.
And this will only insert one row:
insert into job (company)
select t.comp
from jsonb_each_text('{
"company": "Facebook",
"company_two": ""
}'::jsonb) t(k,comp)
where t.comp <> '';

Postgresql json select from values in second layer of containment of arrays

I have a jsonb column 'data' that contains a tree like json, example:
{
"libraries":[
{
"books":[
{
"name":"mybook",
"type":"fiction"
},
{
"name":"yourbook",
"type":"comedy"
}
{
"name":"hisbook",
"type":"fiction"
}
]
}
]
}
I want to be able to do a index using query that selects a value from the indented "book" jsons according to the type.
so all book names that are fiction.
I was able to do this using jsonb_array_elements a join query, but as i understand this would not be optimized with using the GIN index.
my query is
select books->'name'
from data,
jsonb_array_elements(data->'libraries') libraries,
jsonb_array_elements(libraries->'books') books,
where books->>'type'='grading'
If the example data you are showing is the type of data that is common in your JSON, I would suggest that you may be setting things up wrong.
Why not make a library table and a book table and not use JSON at all, it seems JSON is not the right choice here.
CREATE TABLE library
(
id serial,
name text
);
CREATE TABLE book
(
isbn BIGINT,
name text,
book_type text
);
CREATE TABLE library_books
(
library_id integer,
isbn BIGINT
)
select book.* from library_books where library_id = 1;