I'm working on a dynamic menu and I need to get a JSON tree with data to build the options in the interface (using CSS, HTML and JS), based on the constraints defined in three tables ( SQL Fiddle still has postgres 9.6, but I'm using postgres 15.0 )
The general idea is to first find the nodes that point to a file (file IS NOT NULL), which are active (status = A) and which are from subsystem 1 (id_subsystem = 1), as long as the user 1 (id_user = 1) is also active (status = A) and has access permission (according to the users_modules table)
With these records, it would be enough to find each parent recursively related and add them to the tree, but keeping the order defined by the group_order field
users
CREATE TABLE users (
id SMALLINT NOT NULL,
name CHARACTER VARYING(20) NOT NULL,
status CHAR(1) NOT NULL
);
id
name
status
1
John Doe
A
2
Jane Doe
A
3
Duh Doe
I
modules
CREATE TABLE modules (
id SMALLINT NOT NULL,
id_subsystem SMALLINT NOT NULL,
id_master SMALLINT,
group_order SMALLINT NOT NULL,
label CHARACTER VARYING(30) NOT NULL,
file CHARACTER VARYING(30),
icon CHARACTER VARYING(20),
status CHAR(1) NOT NULL
);
INSERT INTO
modules
VALUES
(1,1,NULL,1,'Dashboard','dashboard','dashboard','A'),
(2,1,NULL,2,'Registrations',NULL,'cabinet','A'),
(3,1,2,1,'Customers','customers',NULL,'A'),
(4,1,2,2,'Suppliers','suppliers',NULL,'A'),
(5,1,2,3,'Products','products',NULL,'A'),
(6,1,2,4,'Staff',NULL,NULL,'A'),
(7,1,6,1,'Countries','countries',NULL,'A'),
(8,1,6,2,'States','states',NULL,'A'),
(9,1,6,3,'Cities','cities',NULL,'A'),
(10,1,6,4,'Means of contacts',NULL,NULL,'A'),
(11,1,10,1,'Electronic contacts','electronic_contacts',NULL,'A'),
(12,1,10,2,'Phone contacts','phone_contacts',NULL,'A'),
(13,1,10,3,'Deprecated contacts','deprecated_contacts',NULL,'I'),
(14,1,NULL,3,'Settings','settings','sliders','A'),
(15,2,NULL,1,'Dashboard','dashboard','dashboard','A'),
(16,2,NULL,2,'Financial',NULL,'cash','A'),
(17,2,16,1,'Bills to pay','bills_to_pay',NULL,'A'),
(18,2,16,2,'Bills to receive','bills_to_receive',NULL,'A');
id
id_subsystem
id_master
order
label
file
icon
status
1
1
NULL
1
Dashboard
dashboard
dashboard
A
2
1
NULL
2
Registrations
NULL
cabinet
A
3
1
2
1
Customers
customers
NULL
A
4
1
2
2
Suppliers
suppliers
NULL
A
5
1
2
3
Products
products
NULL
A
6
1
2
4
Staff
NULL
NULL
A
7
1
6
1
Countries
countries
NULL
A
8
1
6
2
States
states
NULL
A
9
1
6
3
Cities
cities
NULL
A
10
1
6
4
Means of contacts
NULL
NULL
A
11
1
10
1
Electronic contacts
electronic_contacts
NULL
A
12
1
10
2
Phone contacts
phone_contacts
NULL
A
13
1
10
3
Deprecated contacts
deprecated_contacts
NULL
I
14
1
NULL
3
Settings
settings
sliders
A
15
2
NULL
1
Dashboard
dashboard
dashboard
A
16
2
NULL
2
Financial
NULL
cash
A
17
2
16
1
Bills to pay
bills_to_pay
NULL
A
18
2
16
2
Bills to receive
bills_to_receive
NULL
A
users_modules
CREATE TABLE users_modules (
id_user SMALLINT NOT NULL,
id_module SMALLINT NOT NULL
);
INSERT INTO
users_modules
VALUES
(1,1),
(1,3),
(1,4),
(1,5),
(1,7),
(1,8),
(1,11),
(1,12);
id_user
id_module
1
1
1
3
1
4
1
5
1
7
1
8
1
11
1
12
I created the query below and it seems to be pretty close to solving the problem, but I still can't figure out what it is
WITH RECURSIVE
sub1 (id_master,sub) AS
(
(
/*
THE FIRST PART OF A RECURSIVE CTE IS FOR NON-RECURSIVE DATA
HERE I GET ALL THE RECORDS THAT POINT TO A FILE THAT CAN BE ACCESSED BY THE USER, BUT ONLY IF IT DOESN'T HAVE THE ROOT AS THE PARENT
*/
SELECT
B.id_master,
JSONB_AGG(
JSONB_BUILD_OBJECT(
'icon',B.icon,
'label',B.label,
'module',B.file
)
ORDER BY
B.group_order
) AS sub
FROM
(
SELECT
X.id_module
FROM
users_modules X
INNER JOIN
users Y
ON
X.id_user=Y.id
WHERE
X.id_user=1 AND
Y.status='A'
) A
INNER JOIN
modules B
ON
A.id_module=B.id
WHERE
B.id_master IS NOT NULL AND
B.id_subsystem=1 AND
B.status='A'
GROUP BY
B.id_master
)
UNION ALL
(
/*
THE SECOND PART OF A RECURSIVE CTE IS FOR RECURSIVE DATA
HERE I ASSEMBLE THE TREE CONNECTING ALL FINAL NODES AND BRANCHES POINTED RECURSIVELY
*/
SELECT
A.id_master,
JSONB_BUILD_OBJECT(
'icon',A.icon,
'label',A.label,
'sub',B.sub
) AS sub
FROM
modules A
INNER JOIN
sub1 B
ON
A.id=B.id_master
WHERE
A.status='A'
ORDER BY
A.group_order
)
)
SELECT
JSONB_AGG(sub ORDER BY group_order)
FROM
(
SELECT
sub,
group_order
FROM
(
(
/*
TYING AT THE ROOT ALL KNOTS POINTING TO THE ROOT
*/
SELECT
JSONB_BUILD_OBJECT(
'icon',A.icon,
'label',A.label,
'sub',B.sub
) AS sub,
A.group_order
FROM
modules A
INNER JOIN
sub1 B
ON
A.id=B.id_master
WHERE
A.id_master IS NULL AND
A.id_subsystem=1 AND
A.status='A'
)
UNION ALL
(
/*
ADDING ALL USER ACCESSIBLE FILE NODES THAT HAVE ROOT AS THE PARENT
*/
SELECT
JSONB_BUILD_OBJECT(
'icon',B.icon,
'label',B.label,
'module',B.file
) AS sub,
B.group_order
FROM
(
SELECT
A.id_module
FROM
users_modules A
INNER JOIN
modules B
ON
A.id_module=B.id
WHERE
A.id_user=1 AND
B.id_master IS NULL AND
B.status='A'
GROUP BY
A.id_module
) A
INNER JOIN
modules B
ON
A.id_module=B.id AND
B.status='A'
)
) sub2
) sub3
What I get is this:
[
{
"icon": "dashboard",
"label": "Dashboard",
"module": "dashboard"
},
{
"icon": "cabinet",
"label": "Registrations",
"sub":
[
{
"icon": null,
"label": "Customers",
"module": "customers"
},
{
"icon": null,
"label": "Suppliers",
"module": "suppliers"
},
{
"icon": null,
"label": "Products",
"module": "products"
}
]
},
{
"icon": "cabinet",
"label": "Registrations",
"sub":
{
"icon": null,
"label": "Staff",
"sub":
[
{
"icon": null,
"label": "Countries",
"module": "countries"
},
{
"icon": null,
"label": "States",
"module": "states"
}
]
}
},
{
"icon": "cabinet",
"label": "Registrations",
"sub":
{
"icon": null,
"label": "Staff",
"sub":
{
"icon": null,
"label": "Means of contacts",
"sub":
[
{
"icon": null,
"label": "Electronic contacts",
"module": "electronic_contacts"
},
{
"icon": null,
"label": "Phone contacts",
"module": "phone_contacts"
}
]
}
}
}
]
But what I need is this:
[
{
"icon": "dashboard",
"label": "Dashboard",
"module": "dashboard"
},
{
"icon": "cabinet",
"label": "Registrations",
"sub":
[
{
"icon": null,
"label": "Customers",
"module": "customers"
},
{
"icon": null,
"label": "Suppliers",
"module": "suppliers"
},
{
"icon": null,
"label": "Products",
"module": "products"
},
{
"icon": null,
"label": "Staff",
"sub":
[
{
"icon": null,
"label": "Countries",
"module": "countries"
},
{
"icon": null,
"label": "States",
"module": "states"
},
{
"icon": null,
"label": "Means of contacts",
"sub":
[
{
"icon": null,
"label": "Electronic contacts",
"module": "electronic_contacts"
},
{
"icon": null,
"label": "Phone contacts",
"module": "phone_contacts"
}
]
}
]
}
]
},
{
"icon": "sliders",
"label": "Settings",
"module": "settings"
}
]
Part of the members of the registrations node was not nested inside it and the settings node was lost
Almost there...
The query here below provides the expected result according to your data set :
WITH RECURSIVE list AS (
SELECT m.id_subsystem
, p.id :: integer AS id_master
, array_agg(m.id :: integer ORDER BY m.group_order) AS children_array
, bool_and(NOT EXISTS(SELECT 1 FROM modules where id_master = m.id)) AS leaves_only
, jsonb_build_object('icon', p.icon, 'label', p.label,'sub', jsonb_agg(jsonb_build_object('icon', m.icon, 'label', m.label, 'module', m.file) ORDER BY m.group_order)) AS module_hierarchy
FROM modules m
LEFT JOIN modules p
ON p.id = m.id_master
GROUP BY m.id_subsystem, p.id, p.icon, p.label
), tree AS (
SELECT id_subsystem
, id_master
, module_hierarchy
FROM list
WHERE leaves_only -- starts with the leaf modules
UNION ALL
SELECT t.id_subsystem
, l.id_master
, jsonb_set(l.module_hierarchy, array['sub', (array_position(l.children_array, t.id_master) - 1) :: text], t.module_hierarchy, true)
FROM tree t
INNER JOIN list l
ON l.children_array #> array[t.id_master] -- bottom-up tree build
AND l.id_subsystem = t.id_subsystem
WHERE t.id_master IS NOT NULL
)
SELECT id_subsystem, jsonb_pretty(module_hierarchy->'sub')
FROM tree
WHERE id_master IS NULL
Some conditions are not implemented yet : status of the modules and users, users_modules relationship, but adding these conditions should not be a big deal.
This query provides the expected result only if there is only one branch with 2 levels or more which is the case in your data set. When there are 2 or more branches with 2 levels or more, we need to merge the branches all together with a dedicated aggregate function :
CREATE OR REPLACE FUNCTION jsonb_merge (x jsonb, y jsonb)
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
SELECT jsonb_agg(
CASE
WHEN x.content->>'sub' IS NOT NULL AND y.content->>'sub' IS NOT NULL
THEN (x.content - 'sub') || jsonb_build_object('sub', jsonb_merge(x.content->'sub', y.content->'sub'))
WHEN x.content->>'sub' IS NOT NULL
THEN x.content
ELSE y.content
END
ORDER BY x.id
)
FROM jsonb_path_query(COALESCE(x, y), '$[*]') WITH ORDINALITY AS x(content, id)
INNER JOIN jsonb_path_query(y, '$[*]') WITH ORDINALITY AS y(content, id)
ON x.id = y.id
$$ ;
CREATE OR REPLACE AGGREGATE jsonb_merge(jsonb)
( stype = jsonb, sfunc = jsonb_merge) ;
This aggregate function cannot be called directly inside the recursive query because postgres doesn't accept aggregate functions in the recursive part of the query, but it can be called after :
WITH RECURSIVE list AS (
SELECT m.id_subsystem
, p.id :: integer AS id_master
, array_agg(m.id :: integer ORDER BY m.group_order) AS children_array
, bool_and(NOT EXISTS(SELECT 1 FROM modules where id_master = m.id)) AS leaves_only
, jsonb_build_object('icon', p.icon, 'label', p.label,'sub', jsonb_agg(jsonb_build_object('icon', m.icon, 'label', m.label, 'module', m.file) ORDER BY m.group_order)) AS module_hierarchy
FROM modules m
LEFT JOIN modules p
ON p.id = m.id_master
GROUP BY m.id_subsystem, p.id, p.icon, p.label
), tree AS (
SELECT id_subsystem
, id_master
, module_hierarchy
FROM list
WHERE leaves_only
UNION ALL
SELECT t.id_subsystem
, l.id_master
, jsonb_set(l.module_hierarchy, array['sub', (array_position(l.children_array, t.id_master) - 1) :: text], t.module_hierarchy, true)
FROM tree t
INNER JOIN list l
ON l.children_array #> array[t.id_master]
AND l.id_subsystem = t.id_subsystem
WHERE t.id_master IS NOT NULL
)
SELECT id_subsystem
, jsonb_pretty(jsonb_merge(module_hierarchy->'sub'))
FROM tree
WHERE id_master IS NULL
GROUP BY id_subsystem
This query should work for any kind of trees with any levels.
see test result in dbfiddle
I'm new to JSONB and I am wondering, if the following would be possible with a single query:
I have a lot of tables that look like this:
ID (INT) | members (JSONB)
all the tables has only one row.
example for 2 tables
table1:
id: 1
data:
[
{
"computer": "12.12.12.12",
"tag": "dog"
},
{
"computer": "1.1.1.1",
"tag": "cat"
},
{
"computer": "2.2.2.2",
"tag": "cow"
}
]
table2:
id: 1
data:
[
{
"IP address": "12.12.12.12",
"name": "Beni",
"address": "Rome"
},
{
"IP address": "1.1.1.1",
"name": "Jone",
"address": "Madrid"
}
]
The result should be rows like this :
computer
tag
name
12.12.12.12
dog
Beni
1.1.1.1
cat
Jone
Thanks !
Convert jsons into setof types using jsonb_to_recordset function and then join them (like they were relational tables).
with table1 (id,members) as (
values (1,'[{"computer": "12.12.12.12","tag": "dog"},{"computer": "1.1.1.1","tag": "cat"},{"computer": "2.2.2.2","tag": "cow"}]'::jsonb)
), table2 (id,members) as (
values (1,'[{"IP address": "12.12.12.12","name": "Beni", "address": "Rome"},{"IP address": "1.1.1.1","name": "Jone", "address": "Madrid"}]'::jsonb)
)
select t1.computer, t1.tag, t2.name
from jsonb_to_recordset((select members from table1 where id=1)) as t1(computer text,tag text)
join jsonb_to_recordset((select members from table2 where id=1)) as t2("IP address" text,name text)
on t1.computer = t2."IP address"
db fiddle
to get values out of a jsonb array of objects you somehow have to explode them.
another way with jsonb_array_elements:
with _m as (
select
jsonb_array_elements(members.data) as data
from members
),
_m2 as (
select
jsonb_array_elements(members2.data) as data
from members2
)
select
_m.data->>'computer' as computer,
_m.data->>'tag' as tag,
_m2.data->>'name' as name
from _m
left join _m2 on _m2.data->>'IP address' = _m.data->>'computer'
https://www.db-fiddle.com/f/68iC5TzLKbzkLZ8gFWYiLz/0
I have table people and it's maintain Four column which is Name ,TagName ,Value , Location.
I want to convert the tagname and value in json with name and location column as rootnode (Name & location same for multiple records)
Need output as :
{
"{"Name":"EMP1","Location":"mumbai"}": [
{
"TagName": "1",
"Value": "844.17769999999996"
},
{
"TagName": "abc",
"Value": "837.43679999999995"
},
{
"TagName": "pqr",
"Value": "0"
},
{
"TagName": "XYZ",
"Value": "1049.2429999999999"
}
]
}
please check the below query, In which I am trying to create json string using json path but stuck in root node.
SELECT TagName
,Value
FROM dbo.people
FOR JSON PATH, ROOT('')---
when I convert the above json into tabular format, required output as :
Name | Location |TagName| Value
EMP1 | Mumbai |1 | 844.17769999999996|
EMP1 | Mumbai |abc | 837.43679999999995|
.....
Your expected output is not a valid JSON, but you are probably looking for something like this:
Table:
CREATE TABLE People (
[Name] varchar(10),
[Location] varchar(50),
[TagName] varchar(3),
[Value] numeric(20, 14)
)
INSERT INTO People ([Name], [Location], [TagName], [Value])
VALUES
('EMP1', 'Mumbai', '1', 844.17769999999996),
('EMP1', 'Mumbai', 'abc', 837.43679999999995),
('EMP2', 'Mumbai', 'abc', 837.43679999999995)
Statement:
SELECT DISTINCT p.[Name], p.[Location], c.Items
FROM People p
CROSS APPLY (
SELECT [TagName], [Value]
FROM People
WHERE [Name] = p.[Name] AND [Location] = p.[Location]
FOR JSON AUTO
) c (Items)
FOR JSON PATH
Result:
[
{
"Name":"EMP1",
"Location":"Mumbai",
"Items":[
{
"TagName":"1",
"Value":844.17769999999996
},
{
"TagName":"abc",
"Value":837.43679999999995
}
]
},
{
"Name":"EMP2",
"Location":"Mumbai",
"Items":[
{
"TagName":"abc",
"Value":837.43679999999995
}
]
}
]
If you want to parse the generated JSON, you need to use OPENJSON() twice:
Generated JSON:
DECLARE #json varchar(max) = N'[
{
"Name":"EMP1",
"Location":"Mumbai",
"Items":[
{
"TagName":"1",
"Value":844.17769999999996
},
{
"TagName":"abc",
"Value":837.43679999999995
}
]
},
{
"Name":"EMP2",
"Location":"Mumbai",
"Items":[
{
"TagName":"abc",
"Value":837.43679999999995
}
]
}
]'
Statement:
SELECT j1.Name, j1.Location, j2.TagName, j2.Value
FROM OPENJSON(#json) WITH (
[Name] varchar(10) '$.Name',
[Location] varchar(50) '$.Location',
[Items] nvarchar(max) '$.Items' AS JSON
) j1
OUTER APPLY OPENJSON(j1.Items) WITH (
[TagName] varchar(3) '$.TagName',
[Value] numeric(20, 14) '$.Value'
) j2
So, I have a simple view that looks like this:
Name | Type | Product | QuantitySold
------------------------------------------------------
Walmart | Big Store | Gummy Bears | 10
Walmart | Big Store | Toothbrush | 6
Target | Small Store | Toothbrush | 2
Without using nested queries, using sql's FOR JSON clause, can this be easily converted to this json.
[
{
"Type": "Big Store",
"Stores": [
{
"Name": "Walmart",
"Products": [
{
"Name": "Gummy Bears",
"QuantitySold": 10
},
{
"Name": "Toothbrush",
"QuantitySold": 6
}
]
}
]
},
{
"Type": "Smaller Store",
"Stores": [
{
"Name": "Target",
"Products": [
{
"Name": "Toothbrush",
"QuantitySold": 2
}
]
}
]
}
]
Essentially Group by Type, Store then, line items. My attempt so far below. Not sure how to properly group the rows.
SELECT Type, (
SELECT Store,
(SELECT Product,QuantitySold from MyTable m3 where m3.id=m2.id for json path) as Products
FROM MyTable m2 where m1.ID = m2.ID for json path) as Stores
) as Types FROM MyTable m1
You can try something like this:
DECLARE #Data TABLE (
Name VARCHAR(20), Type VARCHAR(20), Product VARCHAR(20), QuantitySold INT
);
INSERT INTO #Data ( Name, Type, Product, QuantitySold ) VALUES
( 'Walmart', 'Big Store', 'Gummy Bears', 10 ),
( 'Walmart', 'Big Store', 'Toothbrush', 6 ),
( 'Target', 'Small Store', 'Toothbrush', 2 );
SELECT DISTINCT
t.[Type],
Stores
FROM #Data AS t
OUTER APPLY (
SELECT (
SELECT DISTINCT [Name], Products FROM #Data x
OUTER APPLY (
SELECT (
SELECT Product AS [Name], QuantitySold FROM #Data n WHERE n.[Name] = x.[Name]
FOR JSON PATH
) AS Products
) AS p
WHERE x.[Type] = t.[Type]
FOR JSON PATH
) AS Stores
) AS Stores
ORDER BY [Type]
FOR JSON PATH;
Returns
[{
"Type": "Big Store",
"Stores": [{
"Name": "Walmart",
"Products": [{
"Name": "Gummy Bears",
"QuantitySold": 10
}, {
"Name": "Toothbrush",
"QuantitySold": 6
}]
}]
}, {
"Type": "Small Store",
"Stores": [{
"Name": "Target",
"Products": [{
"Name": "Toothbrush",
"QuantitySold": 2
}]
}]
}]
If you had normalized data structure you could use a another approach.
--Let's assume that Types are stored like this
DECLARE #Types TABLE (
id int,
Type nvarchar(20)
);
INSERT INTO #Types VALUES (1, N'Big Store'), (2, N'Small Store');
--Stores in separate table
DECLARE #Stores TABLE (
id int,
Name nvarchar(10),
TypeId int
);
INSERT INTO #Stores VALUES (1, N'Walmart', 1), (2, N'Target', 2),
(3, N'Tesco', 2); -- I added one more just for fun
--Products table
DECLARE #Products TABLE (
id int,
Name nvarchar(20)
);
INSERT INTO #Products VALUES (1, N'Gummy Bears'), (2, N'Toothbrush'),
(3, N'Milk'), (4, N'Ball') -- Added some here
-- And here comes the sales
DECLARE #Sales TABLE (
StoreId int,
ProductId int,
QuantitySold int
);
INSERT INTO #Sales VALUES (1, 1, 10), (1, 2, 6), (2, 2, 2),
(3, 4, 15), (3, 3, 7); -- I added few more
Now we can join the tables a get result that you need
SELECT Type = Type.Type,
Name = [Stores].Name,
Name = Products.Product,
QuantitySold = Products.QuantitySold
FROM (
SELECT s.StoreId,
p.Name Product,
s.QuantitySold
FROM #Sales s
INNER JOIN #Products p
ON p.id = s.ProductId
) Products
INNER JOIN #Stores Stores
ON Stores.Id = Products.StoreId
INNER JOIN #Types [Type]
ON Stores.TypeId = [Type].id
ORDER BY Type.Type, [Stores].Name
FOR JSON AUTO;
Output:
[
{
"Type": "Big Store",
"Stores": [
{
"Name": "Walmart",
"Products": [
{
"Name": "Gummy Bears",
"QuantitySold": 10
},
{
"Name": "Toothbrush",
"QuantitySold": 6
}
]
}
]
},
{
"Type": "Small Store",
"Stores": [
{
"Name": "Target",
"Products": [
{
"Name": "Toothbrush",
"QuantitySold": 2
}
]
},
{
"Name": "Tesco",
"Products": [
{
"Name": "Ball",
"QuantitySold": 15
},
{
"Name": "Milk",
"QuantitySold": 7
}
]
}
]
}
]