Get a ordered JSON tree from Postgres - json

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

Related

Select data from json object using mysql

I have a table containg two columns:
Customer id
json_data (it contains JSON object) is as follow
{
"nameValuePairs": {
"CONTACTS": {
"nameValuePairs": {
"contacts": {
"values": [
{
"nameValuePairs": {
"contact_id": "1",
"contact_phoneNumber": "080000016",
"contact_phoneNumberCategory": "Mobile",
"contact_firstName": "Huawei Customer Service",
"contact_last_name": "Huawei Customer Service",
"contact_title": "Huawei Customer Service",
"contact_email": "mobile.pk#huawei.com"
}
},
{
"nameValuePairs": {
"contact_id": "2",
"contact_phoneNumber": "15",
"contact_phoneNumberCategory": "Mobile",
"contact_firstName": "Police Helpline",
"contact_last_name": "Police Helpline",
"contact_title": "Police Helpline"
}
},
{
"nameValuePairs": {
"contact_id": "3",
"contact_phoneNumber": "16",
"contact_phoneNumberCategory": "Mobile",
"contact_firstName": "Fire Brigade Helpline",
"contact_last_name": "Fire Brigade Helpline",
"contact_title": "Fire Brigade Helpline"
}
}
]
}
}
}
}
}
Now I want to extract information using MySQL such that I get the table such that
Customer id
contact_title
1
Huawei helpline
1
Police
How can I get above table? I need query
I try this query
JSON_EXTRACT(json_data, '$.nameValuePairs.CONTACTS.nameValuePairs.contacts.values[0]
.nameValuePairs.contact_title') AS "Contact name",
I am getting this table
customer_id
contact_title
1
['Huawei helpline', 'Police', 'Fire Brigade']
2
['Huawei helpline', 'Police', 'Fire Brigade']
I don't want this table. I want one contact_title per row against the same customer_id
If the DB version is 8.0+, then you can use JSON_TABLE() function along with a cross join such as
SELECT contact_id, contact_title
FROM `tab`
JOIN JSON_TABLE(
`json_data`,
'$.nameValuePairs.CONTACTS.nameValuePairs.contacts.values[*]
.nameValuePairs'
COLUMNS (
contact_id VARCHAR(100) PATH '$.contact_id'
)
) j1
JOIN JSON_TABLE(
`json_data`,
'$.nameValuePairs.CONTACTS.nameValuePairs.contacts.values[*]
.nameValuePairs'
COLUMNS (
contact_title VARCHAR(100) PATH '$.contact_title'
)
) j2
or a cleaner option might be
WITH t AS
(
SELECT contact_id, contact_title
FROM `tab`
JOIN JSON_TABLE(
`json_data`,
'$.nameValuePairs.CONTACTS.nameValuePairs.contacts.values[*]
.nameValuePairs'
COLUMNS (
contact_id VARCHAR(100) PATH '$.contact_id',
contact_title VARCHAR(100) PATH '$.contact_title'
)
) AS j
)
SELECT t2.contact_id, t1.contact_title
FROM t AS t1
JOIN t AS t2
Demo

Convert flat SQL rows into nested JSON array using FOR JSON

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
}
]
}
]
}
]

Full text search in concrete node in json

I has table "Product" with two columns:
Id - Bigint primary key
data - Jsonb
Here example of json:
{
"availability": [
{
"qty": 10,
"price": 42511,
"store": {
"name": "my_best_store",
"hours": null,
"title": {
"en": null
},
"coords": null,
"address": null,
I insert json to column "data".
Here sql get find "my_best_store"
select *
from product
where to_tsvector(product.data) ## to_tsquery('my_best_store')
Nice. It's work fine.
But I need to find "my_best_store" only in section "availability".
I try this but result is empty:
select *
from product
where to_tsvector(product.data) ## to_tsquery('availability & my_best_store')
Assuming you want to search in the name attribute, you can do the following:
select p.*
from product p
where exists (select *
from jsonb_array_elements(p.data -> 'availability') as t(item)
where to_tsvector(t.item -> 'store' ->> 'name') ## to_tsquery('my_best_store'))
With Postgres 12, you can simplify that to:
select p.*
from product p
where to_tsvector(jsonb_path_query_array(data, '$.availability[*].store.name')) ## to_tsquery('my_best_store')

Couchbase N1QL - Nest within a Nest

Nesting within a Nest.
I've adapted my need into the following restaurant example:
Desired Output:
{
"restaurant": {
"id": "restaurant1",
"name": "Foodie",
"mains": [ // < main nested in restaurant
{
"id": "main1",
"title": "Steak and Chips",
"ingredients": [ // < ingredient nested in main (...which is nested in restaurant)
{
"id": "ingredient1",
"title": "steak"
},
{
"id": "ingredient2",
"title": "chips"
}
]
},
{
"id": "main2",
"title": "Fish and Chips",
"ingredients": [
{
"id": "ingredient3",
"title": "fish"
},
{
"id": "ingredient2",
"title": "chips"
}
]
}
]
"drinks": [ you get the idea ] // < drink nested in restaurant
}
}
Example Docs:
// RESTAURANTS
{
"id": "restaurant1",
"type": "restaurant",
"name": "Foodie",
"drinkIds": [ "drink1", "drink2" ],
"mainIds: [ "main1", "main2" ]
},
// MAINS
{
"id": "main1",
"type": "main",
"restaurantIds": [ "restaurant1" ],
"title": "Steak and Chips"
},
{
"id": "main2",
"type": "main",
"restaurantIds": [ "restaurant1" ],
"title": "Fish and Chips"
},
// INGREDIENTS
{
"id": "ingredient1",
"type": "ingredient",
"title": "steak",
"mainIds": [ "main1" ]
},
{
"id": "ingredient2",
"type": "ingredient",
"title": "chips",
"mainIds": [ "main1", "main2" ]
},
{
"id": "ingredient3",
"type": "ingredient",
"title": "fish",
"mainIds": [ "main2" ]
},
// DRINKS
{ you get the idea.... }
The closest I can get without error is:
SELECT restaurant, mains, drinks
FROM default restauant USE KEYS "restaurant1"
NEST default mains ON KEYS restaurant.mainIds
NEST default drinks ON KEYS restaurant.drinkIds;
But:
1. Obviously the nested nest is missing
2. The returned order is incorrect - the drinks nest comes first instead of last
(3. Since I'm also using Sync Gateway - it returns all the "_sync" fields with every doc - can't figure out how to omit this on each doc.)
UPDATE 1: ADAPTED SOLUTION
NB: I should have specified above that a main cannot hold ingredientIds.
Based on geraldss' v helpful input below, I added a doc which tracks keys per restaurant, eg:
{
"id": "restaurant1-JoeBloggs",
"dinerId": "JoeBloggs",
"ingredientIds": [ "ingredient1", "ingredient2" "ingredient3" ],
"mainOrdered": [ "main1" ], // < other potential uses...
"drinkOrdered": [ "drink2" ]
}
I added this to geraldss' first solution below as a JOIN to make it available to the query, eg:
SELECT *
FROM
(
SELECT
r.*,
(
SELECT
drink.*
FROM default AS drink
USE KEYS r.drinkIds
) AS drinks,
(
SELECT
main.*,
(
SELECT
ingredient.*
FROM default AS ingredient
USE KEYS keyIndex.ingredientIds // < keyIndex
WHERE ingredient.mainId=main.id
) AS ingredients
FROM default AS main
USE KEYS r.mainIds
) AS mains
FROM default AS r
USE KEYS "restaurant1"
JOIN default AS keyIndex ON KEYS "restaurant1-JoeBloggs" // < keyIndex JOINed
) AS restaurant
;
geraldss' second solution below also looks good - unfortunately it won't work for my case as this query requires that mains are found via ingredients; for my needs a main can exist without any ingredients. EDIT: > he came up with another solution. See 2.
UPDATE 2: FINAL SOLUTION
So, again, with geraldss' help I have a solution which does not require an additional doc to track keys:
SELECT *
FROM
(
SELECT
restaurant.id, restaurant.name,
(
SELECT
drink.id, drink.title
FROM default AS drink
USE KEYS restaurant.drinkIds
)
AS drinks,
(
SELECT
main.id, main.title,
ARRAY_AGG({"title":ingredient.title, "id":ingredient.id}) AS ingredients
FROM default AS ingredient
JOIN default AS main
ON KEYS ingredient.mainIds
WHERE main.restaurantId="restaurant1"
AND meta().id NOT LIKE '_sync:%' // < necessary only if using Sync Gateway
GROUP BY main
UNION ALL
SELECT
mainWithNoIngredients.id, mainWithNoIngredients.title
FROM default AS mainWithNoIngredients
UNNEST mainWithNoIngredients AS foo // < since this is being flattened the AS name is irrelevant
WHERE mainWithNoIngredients.restaurantId="restaurant1"
AND mainWithNoIngredients.type="main"
AND meta().id NOT LIKE '_sync:%' // < necessary only if using Sync Gateway
AND META(mainWithNoIngredients).id NOT IN
(
SELECT RAW mainId
FROM default AS ingredient
)
)
AS mains
FROM default AS restaurant
USE KEYS "restaurant1"
)
AS restaurant
;
NB - the AND meta().id NOT LIKE '_sync:%' lines are only necessary if using Sync Gateway.
With just 1 key I can pull all the related docs - even if they are unknown to the immediate 'parent'.
Thank you geraldss.
If the mains contain ingredientIds:
SELECT *
FROM
(
SELECT
r.*,
(
SELECT
drink.*
FROM default AS drink
USE KEYS r.drinkIds
) AS drinks,
(
SELECT
main.*,
(
SELECT
ingredient.*
FROM default AS ingredient
USE KEYS main.ingredientIds
) AS ingredients
FROM default AS main
USE KEYS r.mainIds
) AS mains
FROM default AS r
USE KEYS "restaurant1"
) AS restaurant
;
EDIT: Updated to include mains not referenced by any ingredients.
If the mains do not contain ingredientIds:
SELECT *
FROM
(
SELECT
r.*,
(
SELECT
drink.*
FROM default AS drink
USE KEYS r.drinkIds
) AS drinks,
(
SELECT
main.*,
ARRAY_AGG(ingredient) AS ingredients
FROM default AS ingredient
JOIN default AS main
ON KEYS ingredient.mainIds
WHERE "restaurant1" IN main.restaurantIds
GROUP BY main
UNION ALL
SELECT
main.*
FROM default AS main
WHERE "restaurant1" IN main.restaurantIds
AND META(main).id NOT IN (
SELECT RAW mainId
FROM default AS ingredient
UNNEST mainIds AS mainId
)
) AS mains
FROM default AS r
USE KEYS "restaurant1"
) AS restaurant
;

Create a json object using array of json objects

In postgres Say I have schema as such:
table item {
type varchar(40)
entity_id bigint
entity_type varchar(40)
user_id bigint
}
And I want to query the table to get the info like this:
{
"typeA": {
"count": 3,
"me": true
},
"typeC": {
"count": 3,
"me": false
},
"typeE": {
"count": 3,
"me": false
},
"typeR": {
"count": 3,
"me": true
}
}
From a query where the main data is this:
SELECT ARRAY_AGG(x)
FROM
(
SELECT type,
count(*),
(CASE
WHEN (SELECT id
FROM items as i
WHERE i.entity_type = 'sometype'
AND i.entity_id = 234
AND i.user_id = 32
AND i.type = items.type) is not null
THEN true
ELSE false
END) AS me
FROM items
WHERE items.entity_type = 'sometype'
AND items.entity_id = 234
GROUP BY type
) as x
This returns an array of the info i need type count and me. But I need it formatted like above versus:
[
{
"type": "typeA",
"count": 3,
"me": true
},
{
"type": "typeC",
"count": 3,
"me": false
},
{
"type": "typeE",
"count": 3,
"me": false
},
{
"type": "typeR",
"count": 3,
"me": true
}
]
Which is the current way it is formatted. Have been unable to find a way to build the json object I need. I was able to get three json objects that are like that But I need the three nested in one object.
Not exactly what you want, but from PostgreSQL - Aggregate Functions, I would guess, you can try json_object_agg(name, value), e.g.
SELECT JSON_OBJECT_AGG(type, x)
FROM
(
SELECT type,
count(*),
(CASE
WHEN (SELECT id
FROM items as i
WHERE i.entity_type = 'sometype'
AND i.entity_id = 234
AND i.user_id = 32
AND i.type = items.type) is not null
THEN true
ELSE false
END) AS me
FROM items
WHERE items.entity_type = 'sometype'
AND items.entity_id = 234
GROUP BY type, me
) as x