Couchbase N1QL - Nest within a Nest - couchbase

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
;

Related

Creating JSON Nested Level Arrays with Query

I'm trying to recreate a JSON output that is about 5 levels deep. I can recreate 2 levels deep, but after that, I'm stumped. It has worked using FOR JSON AUTO, but I need control over the naming and how they appear, so I need to be able to use FOR JSON PATH.
Below is sample code and would allow for a 3 level deep output. I'm hoping that whatever is needed to make it 3 levels, I can learn from to go to 5 levels.
CREATE TABLE [FamilyTree](
[ID] INT NOT NULL ,
[Name] VARCHAR(250) NOT NULL,
[ParentID] INT NOT NULL,
) ON [PRIMARY]
GO
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(1,N'Person1',0)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(2,N'Person2',0)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(3,N'Person3',1)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(4,N'Person4',2)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(5,N'Person5',3)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(6,N'Person6',3)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(7,N'Person7',4)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(8,N'Person8',4)
INSERT [FamilyTree]([ID],[Name],[ParentID])
VALUES(9,N'Person9',4)
Using this query:
SELECT
FT1.Name AS [name],
(SELECT Name FROM FamilyTree WHERE ParentID = FT1.ID FOR JSON PATH) children
FROM FamilyTree FT1
WHERE FT1.ParentID = 0
FOR JSON PATH
I can get the following output:
[
{
"name": "Person1",
"children": [
{
"Name": "Person3"
}
]
},
{
"name": "Person2",
"children": [
{
"Name": "Person4"
}
]
}
]
but I want to be able to get it to go even further to look like this:
[
{
"name": "Person1",
"children": [
{
"Name": "Person3",
"children": [
{
"name": "Person 5",
"name": "Person 6",
}
]
}
]
},
{
"name": "Person2",
"children": [
{
"name": "Person4",
"children": [
{
"name": "Person 7",
"name": "Person 8",
"name": "Person 9"
}
]
}
]
}
]
You can just keep nesting subqueries if you want
SELECT
ft1.ID,
ft1.Name,
ft2.children
FROM FamilyTree ft1
CROSS APPLY (
SELECT
ft2.ID,
ft2.Name,
ft3.children
FROM FamilyTree ft2
CROSS APPLY (
SELECT
ft3.ID,
ft3.Name,
ft4.children
FROM FamilyTree ft3
CROSS APPLY (
SELECT
ft4.ID,
ft4.Name,
ft5.children
FROM FamilyTree ft4
CROSS APPLY (
SELECT
ft5.ID,
ft5.Name
FROM FamilyTree ft5
WHERE ft5.ParentID = ft4.ID
FOR JSON PATH
) ft5(children)
WHERE ft4.ParentID = ft3.ID
FOR JSON PATH
) ft4(children)
WHERE ft3.ParentID = ft2.ID
FOR JSON PATH
) ft3(children)
WHERE ft2.ParentID = ft1.ID
FOR JSON PATH
) ft2(children)
WHERE ft1.ParentID IS NULL
FOR JSON PATH;
db<>fiddle
To do this recursively, or for that matter to prevent duplication of code, you cannot use a Table Valued Function. You can only do this with a scalar UDF (oh the horror!).
CREATE OR ALTER FUNCTION dbo.GetJson (#ParentID int)
RETURNS nvarchar(max)
AS
BEGIN
RETURN (
SELECT
ft.ID,
ft.Name,
children = dbo.GetJson(ft.ID)
FROM FamilyTree ft
WHERE EXISTS (SELECT ft.ParentID INTERSECT SELECT #ParentID) -- null compare
FOR JSON PATH
);
END;
SELECT dbo.GetJson(NULL);
db<>fiddle
Note that in both of these examples, root nodes have a ParentID of NULL not 0. This is the correct way to do it, as there is no 0 row.
Try this, Mabe its usful:
DECLARE #FamilyTree TABLE (
[ID] INT NOT NULL ,
[Name] VARCHAR(250) NOT NULL,
[ParentID] INT NOT NULL
)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(1,N'Person1',0)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(2,N'Person2',0)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(3,N'Person3',1)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(4,N'Person4',2)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(5,N'Person5',3)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(6,N'Person6',3)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(7,N'Person7',4)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(8,N'Person8',4)
INSERT #FamilyTree([ID],[Name],[ParentID])
VALUES(9,N'Person9',4)
SELECT
FT1.Name AS [name],children.Name,grandchild.Name AS grandchild
FROM #FamilyTree FT1
INNER JOIN (SELECT * FROM #FamilyTree ) children ON children.ParentID = FT1.ID
INNER JOIN (SELECT * FROM #FamilyTree ) grandchild ON grandchild.ParentID = children.ID
WHERE FT1.ParentID = 0
FOR JSON AUTO
For each level, you can set another "INNER JOIN"
It's the result:
[
{
"name": "Person1",
"children": [
{
"Name": "Person3",
"grandchild": [
{
"grandchild": "Person5"
},
{
"grandchild": "Person6"
}
]
}
]
},
{
"name": "Person2",
"children": [
{
"Name": "Person4",
"grandchild": [
{
"grandchild": "Person7"
},
{
"grandchild": "Person8"
},
{
"grandchild": "Person9"
}
]
}
]
}
]

Recursive parent child JSON Postgresql

I have the following recursive PostgreSQL table.
CREATE TABLE public."data" (
id int4 NULL,
parentid int4 NULL,
menu varchar NULL
);
I am trying to create a JSON object from the table.
[
{
"menu": "home",
"children": [
{
"menu": "home 1",
"children": []
},
{
"menu": "home 2",
"children": [
{
"menu": "home 2.1",
"children": []
}
]
}
]
},
{
"menu": "config",
"children": []
}
]
How could I create a hierarchal object like this?
First you should use the jsonb format instead of the json format in postgres, see the documentation here :
In general, most applications should prefer to store JSON data as
jsonb, unless there are quite specialized needs, such as legacy
assumptions about ordering of object keys..
Then, assuming your table is a set of (id, menu) tuples and parentid is the id of the parent of this tuple, you can try :
CREATE VIEW parent_children (parent, children, root, cond) AS
( SELECT jsonb_build_object('menu', p.menu, 'children', '[]' :: jsonb) :: text AS parent
, jsonb_agg(jsonb_build_object('menu', c.menu, 'children', '[]' :: jsonb)) :: text AS children
, array[c.parentid] AS root
, array[c.parentid] AS cond
FROM public.data AS c
LEFT JOIN public.data AS p
ON p.id = c.parentid
GROUP BY c.parentid
) ;
WITH RECURSIVE list(parent, children, root, cond) AS
( SELECT parent, children, root, cond
FROM parent_children
WHERE root = array[NULL] -- start with the root parents
UNION
SELECT p.parent
, replace(p.children, c.parent, replace(c.parent, '[]', c.children))
, p.root
, p.cond || c.cond
FROM list AS p
INNER JOIN parent_children AS c
ON position(c.parent IN p.children) > 0
AND NOT p.cond #> c.root -- condition to avoid circular path
)
SELECT children :: jsonb
FROM list AS l
ORDER BY array_length(cond, 1) DESC
LIMIT 1 ;

How to deal with not existing values using JSON_EXTRACT?

I have a list ob objects. Each object contains several properties. Now I want to make a SELECT statement that gives me a list of a single property values. The simplified list look like this:
[
[
{
"day": "2021-10-01",
"entries": [
{
"name": "Start of competition",
"startTimeDelta": "08:30:00"
}
]
},
{
"day": "2021-10-02",
"entries": [
{
"name": "Start of competition",
"startTimeDelta": "03:30:00"
}
]
},
{
"day": "2021-10-03",
"entries": [
{
"name": "Start of competition"
}
]
}
]
]
The working SELECT is now
SELECT
JSON_EXTRACT(column, '$.days[*].entries[0].startTimeDelta') AS list
FROM table
The returned result is
[
"08:30:00",
"03:30:00"
]
But what I want to get (and also have expected) is
[
"08:30:00",
"03:30:00",
null
]
What can I do or how can I change the SELECT statement so that I also get NULL values in the list?
SELECT startTimeDelta
FROM test
CROSS JOIN JSON_TABLE(val,
'$[*][*].entries[*]' COLUMNS (startTimeDelta TIME PATH '$.startTimeDelta')) jsontable
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=491f0f978d200a8a8522e3200509460e
Do you also have a working idea for MySQL< 8? – Lars
What is max amount of objects in the array on the 2nd level? – Akina
Well it's usually less than 10 – Lars
SELECT JSON_EXTRACT(val, CONCAT('$[0][', num, '].entries[0].startTimeDelta')) startTimeDelta
FROM test
-- up to 4 - increase if needed
CROSS JOIN (SELECT 0 num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) nums
WHERE JSON_EXTRACT(val, CONCAT('$[0][', num, '].entries[0]')) IS NOT NULL;
https://www.db-fiddle.com/f/xnCCSTGQXevcpfPH1GAbUo/0

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')

NEST in COUCHBASE

How to nest data in couhcbase similar to mongo DB we do on reference key.
we have two table In a bucket first table is "CHAIN", and second table is "STORE".
I am MONGO user previously and very new to couchbase.
Please suggest how I can nest using N1QL for couchbase
Table 1 CHAIN
{
"chId": "chid_1",
"chName": "Walmart",
"type": "CHAIN"
}
2nd table STORE
{
"chId": "chid_1",
"csName": "store1",
"type": "STORE"
}
{
"chId": "chid_1",
"csName": "store2",
"type": "STORE"
}
I want to get data by joing these table as
{
"chId": "chid_1",
"chName": "Walmart",
"type": "CHAIN",
"stores": [
{"csName": "store1", "type": "STORE"},
{"csName": "store2", "type": "STORE"}]
}
Use JOIN, GROUP BY. Also checkout https://blog.couchbase.com/ansi-join-support-n1ql/
CREATE INDEX ix1 ON (chId) WHERE type = "CHAIN";
CREATE INDEX ix2 ON (chId) WHERE type = "STORE";
SELECT c.*, ARRAY_AGG({s.type, s.csName}) AS stores
FROM default AS c
JOIN default AS s ON c.chId = s.chId
WHERE c.type = "CHAIN" AND s.type = "STORE"
GROUP BY c;
You Can also use ANSI NEST if you want include whole document
SELECT c.*, s AS stores
FROM default AS c
NEST default AS s ON c.chId = s.chId AND s.type = "STORE"
WHERE c.type = "CHAIN";