Recursive query in PostgreSQL to create a Menu tree - postgresql

How to create a json of a hierarchical structure in SQL with PostgreSQL 12?
I need to create a menu like the image below, this menu is already working, but the solution I am using is not very optimized and has a great overhead because I am making many accesses to the database in PHP code with foreach and recursion.
I would like a solution to access the database only once and get the JSON tree ready.
I made a SQL code that is not working very well because it is not merging menu items from the same parent
My current test implementation in SQL is failing, because it is repeating "Jubarte", it is not merging the items of "Jubarte":
CREATE TABLE menus
(
id bigserial NOT NULL PRIMARY KEY,
customer_id integer NOT NULL,
"idPai" bigint,
label text,
rota text
);
INSERT INTO menus(customer_id, "idPai", label, rota) VALUES
(1,NULL,'Jubarte', ''),
(2,1,'Cadastros', ''),
(3,NULL,'Ciente', ''),
(4,1,'Autorizações', ''),
(5,4,'Menus', ''),
(6,2,'Organograma', ''),
(7,1,'Minha Conta', '');
WITH RECURSIVE menus_tree("id", "customer_id", "idPai", "label", "rota", "children") AS (
-- tree leaves (no matching children)
(SELECT c.*, json '[]'
FROM menus c
WHERE NOT EXISTS(SELECT * FROM menus AS hypothetic_child WHERE hypothetic_child."idPai" = c.id)
)
UNION ALL
-- pozs's awesome "little hack"
SELECT (parent).*, json_agg(child) AS "children"
FROM (
SELECT parent, child
FROM menus_tree AS child
JOIN menus parent ON parent.id = child."idPai"
) branch
GROUP BY branch.parent
)
SELECT jsonb_pretty(json_agg(t)::jsonb)
FROM menus_tree t
LEFT JOIN menus AS hypothetic_parent ON(hypothetic_parent.id = t."idPai")
WHERE hypothetic_parent.id IS NULL;
Result:
[
{
"id": 3,
"rota": "",
"idPai": null,
"label": "Ciente",
"children": [
],
"customer_id": 3
},
{
"id": 1,
"rota": "",
"idPai": null,
"label": "Jubarte",
"children": [
{
"id": 7,
"rota": "",
"idPai": 1,
"label": "Minha Conta",
"children": [
],
"customer_id": 7
}
],
"customer_id": 1
},
{
"id": 1,
"rota": "",
"idPai": null,
"label": "Jubarte",
"children": [
{
"id": 2,
"rota": "",
"idPai": 1,
"label": "Cadastros",
"children": [
{
"id": 6,
"rota": "",
"idPai": 2,
"label": "Organograma",
"children": [
],
"customer_id": 6
}
],
"customer_id": 2
},
{
"id": 4,
"rota": "",
"idPai": 1,
"label": "Autorizações",
"children": [
{
"id": 5,
"rota": "",
"idPai": 4,
"label": "Menus",
"children": [
],
"customer_id": 5
}
],
"customer_id": 4
}
],
"customer_id": 1
}
]

I got a solution, I don’t know if it’s the best but it’s working like this, if someone has a better idea, please can suggest
WITH RECURSIVE location_with_level AS (
(
SELECT
menus.*,
0 AS level
FROM
menus,
usuarios u, permissoes P -- <== part to filter by user permissions
WHERE
"idPai" IS NULL
-- part to filter by user permissions
AND u."idPerfil" = P."idPerfil"
AND u."idSistema" = P."idSistema"
AND P."idMenu" = menus."id"
AND u."idPessoa" = 2
AND menus."ativo" = 't'
) UNION ALL
(
SELECT
child.*,
parent.level + 1
FROM
usuarios u, permissoes P, -- <== part to filter by user permissions
menus child
JOIN location_with_level parent ON parent.ID = child."idPai"
-- part to filter by user permissions
WHERE
u."idPerfil" = P."idPerfil"
AND u."idSistema" = P."idSistema"
AND P."idMenu" = child."id"
AND u."idPessoa" = 2
AND child."ativo" = 't'
)
),
-- pega o nivel
maxlvl AS ( SELECT MAX ( level ) maxlvl FROM location_with_level ),
-- generates the JSON of the menu items for each system module
c_tree AS (
(
SELECT location_with_level.*, NULL::JSONB nodes FROM location_with_level, maxlvl WHERE level = maxlvl )
UNION
(
(
SELECT
( branch_parent ).*,
jsonb_agg ( branch_child order by (branch_child->>'ordem')::numeric)
FROM
(
SELECT
branch_parent,
to_jsonb ( branch_child ) /*- 'level' - 'idPai' - 'id'*/ AS branch_child
FROM
location_with_level branch_parent
JOIN c_tree branch_child ON branch_child."idPai" = branch_parent.ID
) branch
GROUP BY
branch.branch_parent
) UNION
(
SELECT C
.*,
NULL :: JSONB
FROM
location_with_level C
WHERE
NOT EXISTS ( SELECT 1 FROM location_with_level hypothetical_child WHERE hypothetical_child."idPai" = C.ID )
)
)
)
SELECT array_to_json(
array_agg(
row_to_json(c_tree)::JSONB /*- 'level' - 'idPai' - 'id'*/ order by sistemas.ordem
)
)::JSONB
AS tree
FROM c_tree
JOIN sistemas on c_tree."idSistema" = sistemas.id
WHERE level=0

Related

Jsonb array of objects update

So this is my jsonb array of objects. Column is called bids in my db.
bids column
[
{
"id": "1",
"size": "5.5Y",
"price": 180
},
{
"id": "f0d1d36a-f6af-409e-968e-54c1dc104566",
"size": "6.5Y",
"price": 22
}
]
I want to update price property by the ID of an element for ex. "f0d1d36a-f6af-409e-968e-54c1dc104566", so the price would change from 22 to 150 IN ROW WHICH CONTAINS ELEMENT WITH DESIRED ID IN THE COLUMN.
How can I do that?
create table json_update (id integer, json_fld jsonb);
insert into json_update values (1, '[
{
"id": "1",
"size": "5.5Y",
"price": 180
},
{
"id": "f0d1d36a-f6af-409e-968e-54c1dc104566",
"size": "6.5Y",
"price": 22
}
]'
)
;
UPDATE
json_update
SET
json_fld = jsonb_set(json_fld, ARRAY[(idx)::text, 'price'::text], '150'::jsonb)
FROM (
SELECT
(row_number() OVER (ORDER BY t.a ->> 'id') - 1) AS idx,
t.a
FROM (
SELECT
jsonb_array_elements(json_fld)
FROM
json_update) AS t (a)) AS i
WHERE
i.a ->> 'id' = 'f0d1d36a-f6af-409e-968e-54c1dc104566';
select * from json_update ;
id | json_fld
----+---------------------------------------------------------------------------------------------------------------------------
1 | [{"id": "1", "size": "5.5Y", "price": 180}, {"id": "f0d1d36a-f6af-409e-968e-54c1dc104566", "size": "6.5Y", "price": 150}]

How to correctly index jsonb arrays and the fields in array elements in postgreSQL

a have a simple table of purchases consisting of id and jsonb column, like this:
CREATE TABLE purchase (id SERIAL, products JSONB)
Then and index:
CREATE INDEX idx_purchase_products ON purchase USING GIN (products);
The sample data are like this:
INSERT INTO purchase VALUES (
1, jsonb('[
{
"country": 1,
"type": 1,
"size": 10,
"color": 3
}
]')
),
(
2, jsonb('[
{
"country": 1,
"type": 1,
"size": 10,
"color": 3
},
{
"country": 1,
"type": 2,
"size": 12,
"color": 4
},
{
"country": 2,
"type": 1,
"size": 12,
"color": 3
}
]')
),
(
3, jsonb('[
{
"country": 1,
"type": 1,
"size": 10,
"color": 3
}
]')
),
(
4, jsonb('[
{
"country": 1,
"type": 1,
"size": 10,
"color": 3
},
{
"country": 1,
"type": 2,
"size": 12,
"color": 4
},
{
"country": 2,
"type": 1,
"size": 12,
"color": 3
}
]')
);
And some scenarios of searching:
SELECT *
FROM purchase
WHERE products #> '[{"country": 1}]'
SELECT *
FROM purchase
WHERE products #> '[{"country": 1, "type": 1}]'
SELECT *
FROM purchase
WHERE products #> '[{"size": 12}]'
SELECT *
FROM purchase
WHERE products #> '[{"size": 12, "color": 4}]'
It is expected, that the customer could search for combinations:
country,
country + type
country + type + size
country + type + size + color
country + size
size + color
type + color
etc.
And there is a big chance, the list of 4 field (country, type, size, color) will grow in future to 7-10.
And of course we want also search combinations like this:
.. WHERE products #> '[{"country": 1}]' OR products #> '[{"color": 4}]' OR products #> '[{"type": 1, "size": 10}]'
Estimated size of the table purchase is 9-12 millions rows (depending on season).
Any idea how to implement the indexes to get the query result as fast as possible?

PostgresSQL nested jsonb update value of complex key/value pairs

Starting out with JSONB data type and I'm hoping someone can help me out.
I have a table (properties) with two columns (id as primary key and data as jsonb).
The data structure is:
{
"ProductType": "ABC",
"ProductName": "XYZ",
"attributes": [
{
"name": "Color",
"type": "STRING",
"value": "Silver"
},
{
"name": "Case",
"type": "STRING",
"value": "Shells"
},
...
]
}
I would like to update the value of a specific attributes element by name for a row with a given id. For example, for the element with "name"="Case" change the value to "Glass". So it ends up like
{
"ProductType": "ABC",
"ProductName": "XYZ",
"attributes": [
{
"name": "Color",
"type": "STRING",
"value": "Silver"
},
{
"name": "Case",
"type": "STRING",
"value": "Glass"
},
...
]
}
Is this possible with this structure using SQL?
I have created table structure if any of you would like to give it a shot.
dbfiddle
Use the jsonb concatenation operator, ||, to replace keys on the fly:
WITH properties (id, data) AS (
values
(1, '{"ProductType": "ABC","ProductName": "XYZ","attributes": [{"name": "Color","type": "STRING","value": "Silver"},{"name": "Case","type": "STRING","value": "Shells"}]}'::jsonb),
(2, '{"ProductType": "ABC","ProductName": "XYZ","attributes": [{"name": "Color","type": "STRING","value": "Red"},{"name": "Case","type": "STRING","value": "Shells"}]}'::jsonb)
)
SELECT id,
data||
jsonb_build_object(
'attributes',
jsonb_agg(
case
when attribs->>'name' = 'Case' then attribs||'{"value": "Glass"}'::jsonb
else attribs
end
)
) as data
FROM properties m
CROSS JOIN LATERAL JSONB_ARRAY_ELEMENTS(data->'attributes') as a(attribs)
GROUP BY id, data
Updated fiddle

Extracting info out of lists inside a jsonb

I have a with a jsonb column called jsonb that contains data in the following format.
{
"stuff": [
{
"name": "foo",
"percent": "90.0000"
},
{
"name": "bar",
"percent": "10.0000"
}
],
"countries": [
{
"name": "USA",
"value": "30"
},
{
"name": "Canada",
"value": "25"
},
{
"name": "Mexico",
"value": "20"
},
{
"name": "Ecuador",
"value": "10"
}
]
}
I am having a lot of trouble working with this data. Specifically what I want to do is find all the different values "name" can have in "stuff" as well as in "countries", kind of like a SELECT distinct.
But my problem is that I can't seem to extract anything useful from this jsonb. My approach so far was to do
SELECT jsonb->>'stuff' FROM table, but this only gave me a column of type text which contained [{"name": "foo","percent": "90.0000"},{"name": "bar","percent": "10.0000"}].
But since this is text I can't really do anything with it. I also tried SELECT jsonb_array_elements_text(jsonb) FROM table but that returned the following Error:
ERROR: cannot extract elements from an object
SQL state: 22023
Any help with working with this format of data is greatly appreciated!
You need to unnest each array separately and then create a union on the result of those two steps:
select c.x ->> 'name'
from the_table
cross join lateral jsonb_array_elements(json_column -> 'countries') as c(x)
union
select s.x ->> 'name'
from the_table
cross join lateral jsonb_array_elements(json_column -> 'stuff') as s(x);
Online example: https://rextester.com/ZEOIXF91294

How to get an associative array of rows from a subquery with postgres

I'm new to postgres and trying out some things before I take the leap over from mySQL.
What I'm trying to do is get an array of associative arrays into a single query.
It has to do with users that can select multiple contact types like phone, email and Facebook and I would like to retrieve those into the column 'contact'.
For a visualisation:
{
"first_name": "This",
"last_name": "is me",
"likes": [],
"city": null
}
And I would like to get something like this:
{
"first_name": "This",
"last_name": "Is me",
"likes": [],
"city": null,
"contact":
[
{"type":1, "value":"myemail#gmail.com", "privacy_rule":1},
{"type":4, "value":"myfacebook", "privacy_rule":1},
{"type":9, "value":"mylinkedin", "privacy_rule":1}
]
}
So the main query would be:
SELECT u.first_name, u.last_name, u.about, ARRAY(SELECT like_id FROM users_likes l WHERE l.user_id = u.user_id), u.city FROM users u WHERE user_id = {id}
The subquery would be:
SELECT c.type, c.value, c.privacy_rule FROM users_contact c WHERE c.user_id = u.user_id
But how do I integrate it in the main query to return the array of result rows?
Is it even possible?
Thanks in advance!
Ron
Ah, after some more filling about, here is the answer.
use json_build_object:
SELECT u.first_name, u.last_name,
ARRAY(SELECT like_id FROM users_likes l WHERE l.user_id = u.user_id) as likes,
ARRAY(SELECT json_build_object("contact_id", c.contact_id,
"value", c.value, "privacy",c.privacy)
FROM users_contact c WHERE c.user_id = u.user_id) as contact
FROM users_basic u WHERE user_id = {id}
This gives:
"first_name": "This",
"last_name": "Is Me",
"about": null,
"likes": [],
"city": null,
"contact": [
{
"contact_id": 1,
"value": "bbla",
"privacy": 2,
"type": "Phone"
},
{
"contact_id": 3,
"value": "blabla",
"privacy": 2,
"type": "Company Email"
},
{
"contact_id": 4,
"value": "blablabla",
"privacy": 2,
"type": "Telegram Id"
}
]
Hope it helps someone