Create json object and aggregate into json array in SqlServer - json

I have following query in ORACLE:
SELECT *
FROM "Supplier" s
OUTER APPLY(
SELECT JSON_ARRAYAGG(JSON_OBJECT(p."Id", p."Description", p."Price")) as "Products"
FROM "Products" p
WHERE p."SupplierId" = s."Id"
) sp
In OUTER APPLY subquery I am creating json from columns I need and then aggregating those objects into json array. I need those two functions because sometimes I use only one of them. The same operation I would like to do in SqlServer. This is solution I managed so far:
SELECT *
FROM "Supplier" as s
OUTER APPLY(
SELECT p."Id", p."Description", p."Price"
FROM "Products" as p
WHERE p."SupplierId" = s."Id"
FOR JSON PATH
) as sp("Products")
The problem is that SqlServer executing those two functions at once (this is purpose for FOR JSON PATH statement). So here are my questions:
1) Is there possible to create json object without wrapping it into array (oracle-like syntax)?
2) Is there possible to aggregate json objects into an array?
UPDATE
I am using SqlServer version 2019 15.0.2000.5
Expected result (single record of products in json format)
"Products":{
"Id":"FEB0646B709B45B5A306E10599716F28",
"Description":"Database Manager",
"Price":149.99
}

If I understand the question correctly, the following statements are possible soltion (of course, they are based on the example data and statements in the question):
How to create a single JSON object:
If you want to generate one single JSON object, you need to use FOR JSON PATh for each row in the OUTER APPLY statement with the appropriate path expression. JSON_QUERY() is needed, because it returns a valid JSON and FOR JSON doesn't escape special characters.
Tables:
CREATE TABLE Supplier (
Id int,
Description varchar(50),
DateStart date
)
CREATE TABLE Products (
Id varchar(5),
SupplierId int,
Description varchar(100),
Price numeric(10, 2)
)
INSERT INTO Supplier (Id, Description, DateStart)
VALUES (1, 'Oracle', '19900505')
INSERT INTO Products (Id, SupplierId, Description, Price)
VALUES ('11111', 1, 'Database Manager', 149.99)
INSERT INTO Products (Id, SupplierId, Description, Price)
VALUES ('22222', 1, 'Chassi', 249.99)
Statement:
SELECT *
FROM "Supplier" s
OUTER APPLY(
SELECT Products = JSON_QUERY((
SELECT
p."Id" AS 'Product.Id',
p."Description" AS 'Product.Description',
p."Price" AS 'Product.Price'
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
))
FROM "Products" as p
WHERE p."SupplierId" = s."Id"
) sp ("Products")
Result:
Id Description DateStart Products
1 Oracle 1990-05-05 {"Product":{"Id":"11111","Description":"Database Manager","Price":149.99}}
1 Oracle 1990-05-05 {"Product":{"Id":"22222","Description":"Chassi","Price":249.99}}
How to aggregate JSON objects into an array:
By default FOR JSON creates a JSON array with one JSON object for each row. You only need to set a root key:
Statement:
SELECT *
FROM "Supplier" s
OUTER APPLY(
SELECT p."Id", p."Description", p."Price"
FROM "Products" p
WHERE p."SupplierId" = s."Id"
FOR JSON PATH
) sp("Products")
Result:
Id Description DateStart Products
1 Oracle 1990-05-05 [{"Id":"11111","Description":"Database Manager","Price":149.99},{"Id":"22222","Description":"Chassi","Price":249.99}]

DECLARE #data varchar(max)
DECLARE #LIST NVARCHAR(MAX)
DECLARE #Temp TABLE (YourColumnName VARCHAR(MAX) NULL);
INSERT INTO #Temp SELECT DISTINCT columnName FROM YourTableName WHERE(Id > 1000);
SELECT #LIST = STRING_AGG(CONVERT(NVARCHAR(max), ISNULL(YourColumnName, 'N/A')), ',') FROM #Temp
SET #data =(select #LIST as Name1,#LIST as Name2 For Json PATH)
select #data

Related

Escaped for JSON nested nodes using union command

In a stored procedure I have a for json node (boxes):
select
(
select
os.Name,
os.Address,
ss.carrierCode,
(
select
ob.envelopeCode,
ob.boxNumber,
ob.weight,
ob.width,
ob.length,
ob.height
from OrdersBoxes ob
...
where os.OID=ob.OID
...
for json path
) boxes,
....
for json path
) orderDetails
In this way I correctly get:
"boxes":[{
"envelopeCode":"E70345D2AB90A879D4F53506FB465086",
"boxNumber":1,
"weight":3000,
"width":300,
"length":300,
"height":100
}]
Now I need to get details from 2 tables, therefore I will use union command, wrap the 2 select in another select the query to avoid following error:
The FOR XML and FOR JSON clauses are invalid in views, inline functions, derived tables, and subqueries when they contain a set operator. To work around, wrap the SELECT containing a set operator using derived table or common table expression or view and apply FOR XML or FOR JSON on top of it.
And add JSON_QUERY to avoid to get escaped nested node:
select
(
select
*
from
(
select
os.Name,
os.Address,
ss.carrierCode,
JSON_QUERY((
select
ob.envelopeCode,
ob.boxNumber,
ob.weight,
ob.width,
ob.length,
ob.height
from OrdersBoxes ob
...
where os.OID=ob.OID
...
for json path
)) boxes,
....
from table1
where....
union
select
os.Name,
os.Address,
ss.carrierCode,
JSON_QUERY((
select
ob.envelopeCode,
ob.boxNumber,
ob.weight,
ob.width,
ob.length,
ob.height
from OrdersBoxes ob
...
where os.OID=ob.OID
...
for json path
)) boxes,
....
from table2
where....
) jj
for json path
) orderDetails
That works, but boxes node is returned escaped:
"boxes":"[{\"envelopeCode\":\"E70345D2AB90A879D4F53506FB465086\",\"boxNumber\":1,\"weight\":3000,\"width\":300,\"length\":300,\"height\":100}]"
I tried also this Solution but it works well only if returning data from 1 table:
since it returns objects {} to get an array need to change first line from
select STRING_AGG (order_details,',') ods from (
to
select concat('[',STRING_AGG (order_details,','),']') ods from (
and it seems me not very "elegant" although it works.
Can someone suggest a better way to get all data correctly formatted (thus unescaped boxes node)?
The documentation about JSON_QUERY() explains: ... JSON_QUERY returns a valid JSON fragment. As a result, FOR JSON doesn't escape special characters in the JSON_QUERY return value. If you're returning results with FOR JSON, and you're including data that's already in JSON format (in a column or as the result of an expression), wrap the JSON data with JSON_QUERY without the path parameter.. So, if I understand the schema correctly, you need to use JSON_QUERY() differently:
Tables:
SELECT *
INTO table1
FROM (VALUES
(1, 'Name1', 'Address1')
) v (oid, name, address)
SELECT *
INTO table2
FROM (VALUES
(2, 'Name2', 'Address2')
) v (oid, name, address)
SELECT *
INTO OrdersBoxes
FROM (VALUES
(1, 'E70345D2AB90A879D4F53506FB465086', 1, 3000, 300, 300, 100),
(2, 'e70345D2AB90A879D4F53506FB465086', 2, 3000, 300, 300, 100)
) v (oid, envelopeCode, boxNumber, weight, width, length, height)
Statement:
select Name, Address, JSON_QUERY(boxes) AS Boxes
from (
select
os.Name,
os.Address,
(
select ob.envelopeCode, ob.boxNumber, ob.weight, ob.width, ob.length, ob.height
from OrdersBoxes ob
where os.OID = ob.OID
for json path
) boxes
from table1 os
union all
select
os.Name,
os.Address,
(
select ob.envelopeCode, ob.boxNumber, ob.weight, ob.width, ob.length, ob.height
from OrdersBoxes ob
where os.OID = ob.OID
for json path
) boxes
from table2 os
) j
for json path
As an additional option, you may try to use FOR JSON AUTO (the format of the JSON output is automatically determined based on the order of columns in the SELECT list and their source tables):
SELECT
cte.Name, cte.Address,
boxes.envelopeCode, boxes.boxNumber, boxes.weight, boxes.width, boxes.length, boxes.height
FROM (
SELECT oid, name, address FROM table1
UNION ALL
SELECT oid, name, address FROM table2
) cte
JOIN OrdersBoxes boxes ON cte.oid = boxes.oid
FOR JSON AUTO
Result:
[
{
"Name":"Name1",
"Address":"Address1",
"boxes":[{"envelopeCode":"E70345D2AB90A879D4F53506FB465086","boxNumber":1,"weight":3000,"width":300,"length":300,"height":100}]
},
{
"Name":"Name2",
"Address":"Address2",
"boxes":[{"envelopeCode":"e70345D2AB90A879D4F53506FB465086","boxNumber":2,"weight":3000,"width":300,"length":300,"height":100}]
}
]

passing json values in sql select statement

I am trying to pass the each line of json file to the SQL Select statement.How I can pass the values using python in iterative way
Json File contains following lines :
{"row_id":"1","a":"600","b":"hello","date":"2017-07-01","enabled":"TRUE","id":"234"}
{"row_id":"2","a":"650","b":"world","date":"2018-08-02","enabled":"FALSE","id":"456"}
{"row_id":"3","a":"700","b":"world","date":"2019-02-10","enabled":"FALSE","id":"789"}
I am trying to pass each line values to the sql statement.
Eg :
when row_id = 1 then it should pass the values of the respective line
Select * from xyz where a='600' and b='hello' and date ='2017-07-01';
when row_id =2 then then it should pass the values of the respective line
Select * from xyz where a='650' and b='world' and date ='2018-08-02';
when row_id =2 then then it should pass the values of the respective line
Select * from xyz where a='700' and b='world' and date ='2019-02-10';
Thank you
Assuming you are passing one JSON object at a time
Example
Declare #JSON varchar(max) ='{"row_id":"1","a":"600","b":"hello","date":"2017-07-01","enabled":"TRUE","id":"234"}'
SELECT *
From xyz
Where a = JSON_VALUE(#JSON, '$.a')
and b = JSON_VALUE(#JSON, '$.b')
and date = JSON_VALUE(#JSON, '$.date')

SQL Server 2016 - How to select integer array from JSON

I received a valid JSON string from client side, it contains an array of integer values:
declare #JSON nvarchar(max) = N'{"Comments": "test", "Markets": [3, 151]}'
How to select the market IDs correctly?
If I use a query like this: select * from openjson(#JSON) j, it returns
The type of Markets is 4, which means an object,
but the query below returns null value:
select j.Markets from openjson(#JSON) with(Markets nvarchar(max)) j
My goal is to update Market table based on these IDs, eg:
update Market set Active = 1 where MarketID in (3, 151)
Is there a way to do this?
Any built-in function compatible with SQL server 2016 can be used.
Note:
Thanks to #johnlbevan
SELECT VALUE FROM OPENJSON(#JSON, '$.Markets') works perfectly for this problem.
Just for the completeness, here is how I created the JSON integer array ("Markets": [3, 151]) from SQL server.
Since there is no array_agg function out of the box in 2016, I did this:
SELECT (
JSON_QUERY('[' + STUFF(( SELECT ',' + CAST(MarketID AS VARCHAR)
FROM Market
FOR XML PATH('')),1,1,'') + ']' ) AS Markets)
To expand the Markets array alongside other columns you can do this:
SELECT Comments, Market
FROM OPENJSON('{"Comments": "test", "Markets": [3, 151]}')
WITH (Comments nvarchar(32), Markets NVARCHAR(MAX) AS JSON) AS a
CROSS APPLY OPENJSON (a.Markets) WITH (Market INT '$') AS b
Convert the string to json
Map the first field returned to the Comments column with type nvarchar(32)
Map the second field to Markets column with type nvarchar(max), then use as json to say that the contents is json (see https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql#arguments for a more detailed description - search the page for as json; the key paragraph starts at the 4th occurrence)
Use a cross apply to apply the OPENJSON function to the Markets column so we can fetch values from that property.
Finally use the WITH statement to map the name Market to the returned value, and assign it a data type of INT.
However, to just get the list of values needed to do the update, you can do this:
UPDATE Market
SET Active = 1
WHERE MarketID IN
(
SELECT value
FROM OPENJSON('{"Comments": "test", "Markets": [3, 151]}','$.Markets')
);
Again OPENJSON lets us query the string as JSON
However this time we specify a path to point at the Markets value directly (see https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql)
We now return the values returned and filter our UPDATE on those, as we would were we dealing with any other subquery.

How do I generate nested json objects using mysql native json functions?

Using only the native JSON fuctions (no PHP, etc) in MySQL version 5.7.12 (section 13.16 in the manual) I am trying to write a query to generate a JSON document from relational tables that contains a sub object. Given the following example:
CREATE TABLE `parent_table` (
`id` int(11) NOT NULL,
`desc` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `child_table` (
`id` int(11) NOT NULL,
`parent_id` int(11) NOT NULL,
`desc` varchar(20) NOT NULL,
PRIMARY KEY (`id`,`parent_id`)
);
insert `parent_table` values (1,'parent row 1');
insert `child_table` values (1,1,'child row 1');
insert `child_table` values (2,1,'child row 2');
I am trying to generate a JSON document that looks like this:
[{
"id" : 1,
"desc" : "parent row 1",
"child_objects" : [{
"id" : 1,
"parent_id" : 1,
"desc" : "child row 1"
}, {
"id" : 2,
"parent_id" : 1,
"desc" : "child row 2"
}
]
}]
I am new to MySQL and suspect there is a SQL pattern for generating nested JSON objects from one to many relationships but I'm having trouble finding it.
In Microsoft SQL (which I'm more familiar with) the following works:
select
[p].[id]
,[p].[desc]
,(select * from [dbo].[child_table] where [parent_id] = [p].[id] for json auto) AS [child_objects]
from [dbo].[parent_table] [p]
for json path
I attempted to write the equivalent in MySQL as follows:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',(select json_object('id',id,'parent_id',parent_id,'desc',`desc`)
from child_table where parent_id = p.id)
)
from parent_table p;
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',json_array((select json_object('id',id,'parent_id',parent_id,'desc',`desc`)
from child_table where parent_id = p.id))
)
from parent_table p
Both attempts fail with the following error:
Error Code: 1242. Subquery returns more than 1 row
The reason you are getting these errors is that the parent json object is not expecting a result set as one of its inputs, you need to have simple object pairs like {name, string} etc bug report - may be available in future functionality... this just means that you need to convert your multi row results into a concatination of results separated by commas and then converted into a json array.
You almost had it with your second example.
You can achieve what you are after with the GROUP_CONCAT function
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',json_array(
(select GROUP_CONCAT(
json_object('id',id,'parent_id',parent_id,'desc',`desc`)
)
from child_table
where parent_id = p.id))
)
from parent_table p;
This almost works, it ends up treating the subquery as a string which leaves the escape characters in there.
'{\"id\": 1,
\"desc\": \"parent row 1\",
\"child_objects\":
[\"
{\\\"id\\\": 1,
\\\"desc\\\": \\\"child row 1\\\",
\\\"parent_id\\\": 1
},
{\\\"id\\\": 2,
\\\"desc\\\": \\\"child row 2\\\",
\\\"parent_id\\\": 1}\"
]
}'
In order to get this working in an appropriate format, you need to change the way you create the JSON output as follows:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',(select CAST(CONCAT('[',
GROUP_CONCAT(
JSON_OBJECT(
'id',id,'parent_id',parent_id,'desc',`desc`)),
']')
AS JSON) from child_table where parent_id = p.id)
) from parent_table p;
This will give you the exact result you require:
'{\"id\": 1,
\"desc\": \"parent row 1\",
\"child_objects\":
[{\"id\": 1,
\"desc\": \"child row 1\",
\"parent_id\": 1
},
{\"id\": 2,
\"desc\": \"child row 2\",
\"parent_id\": 1
}]
}'
For MariaDb, CAST AS JSON does not work. But JSON_EXTRACT may be used to convert a string to a JSON object:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',JSON_EXTRACT(IFNULL((select
CONCAT('[',GROUP_CONCAT(
json_object('id',id,'parent_id',parent_id,'desc',`desc`)
),']')
from child_table where parent_id = p.id),'[]'),'$')
) from parent_table p;
I tried group_concat solution but I found it's problems larger string because of group_concat limitations (group_concat_max_len).
I wrote the new function resolve the problem about converting a string to JSON object as bellow and how to use it.
Tested on MariaDB 10.5.12
Usage: https://i.stack.imgur.com/cWfd7.jpg
CREATE FUNCTION `ut_tf_array`(input_json longtext) RETURNS longtext CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
COMMENT 'Function for transform json array agg'
BEGIN
DECLARE transformed_data_list longtext ;
DECLARE record longtext ;
DECLARE i_count int ;
DECLARE i_count_items int ;
SET i_count = 0;
SET i_count_items = JSON_LENGTH(JSON_EXTRACT(input_json,'$'));
SET transformed_data_list = '[]';
-- return array with length = zero
IF input_json is NULL THEN
RETURN transformed_data_list;
END IF;
WHILE i_count < i_count_items DO
-- fetch into record
SELECT JSON_EXTRACT( JSON_EXTRACT( input_json ,'$') , CONCAT('$[',i_count,']')) INTO record;
-- append to transformed_data_list
SELECT JSON_ARRAY_APPEND(transformed_data_list, '$', JSON_EXTRACT(record, '$')) into transformed_data_list;
SET i_count := i_count + 1;
END WHILE;
-- done
RETURN transformed_data_list;
END
Below Query works for me.
SELECT JSON_ARRAYAGG(JSON_OBJECT('Id', p.id, 'desc', p.`desc`, 'child_objects', temp_json)) AS json_value
FROM (
SELECT p.id, p.`desc`,
JSON_ARRAYAGG(JSON_OBJECT('id', p.id, 'parent_id', p.parent_id, 'desc', p.`desc`)) AS temp_json
FROM parent_table p
GROUP BY p.id
) t;

Way for moving stored procedure from Firebird to SQL Server

I have working SP on Firebird and can'not find the way to translate code to MS SQL server SP
here is the code:
I have one table to get data from.
One fields called "iznos" for data summary and one field for recognite diference for summary by category
create or alter procedure SOME_PROCEDURE (
B_Date date,
E_Date date)
returns(
Dat date,
Value1 decimal(18,2),
Value2 decimal(18,2),
Value3 (18,2))
AS
BEGIN
FOR
SELECT gk.date
FROM gk
WHERE
gk.date BETWEEN :B_Date AND :E_Date
GROUP BY 1
INTO :Dat
DO
BEGIN
/* Value 1 */
SELECT sum(iznos) from gk where gk.category=1 and gk.datum=:Dat
INTO :Value1;
/* Value 2 */
SELECT sum(iznos) from gk where gk.category=2 and gk.datum=:Dat
INTO :Value2;
/* Value 3 */
SELECT sum(iznos) from gk where gk.category=3 and gk.datum=:Dat
INTO :Value3;
suspend;
end
end
You can solve this using PIVOT:
select datum, [1] as Value1, [2] as Value2, [3] as Value3
from (
select datum, category, iznos
from gk
where datum between #B_Date AND #E_Date
) as src
pivot (
sum(src.iznos)
for src.category in ([1], [2], [3])
) as pvt
This doesn't have the exact same effect as the Firebird stored procedure though, because that is returning a row for each row in gk. If you want to have the exact same result, you may have to join gk to the above query.
If you intend to use this stored procedure in a select (which is possible in Firebird, but not in SQL Server), then you need to convert it to a table-valued user-defined function.
You could also create a view instead. In that case the where clause would need to be removed.
Small example in TSQL:
create table #example (
datum date,
category int,
val int
);
insert into #example(datum, category, val) values
('2015-07-24', 1, 1),
('2015-07-24', 2, 1),
('2015-07-24', 3, 1),
('2015-07-24', 1, 1),
('2015-07-24', 2, 1),
('2015-07-25', 3, 1);
select datum, [1] as Value1, [2] as Value2, [3] as Value3
from (
select datum, category, val
from #example
) as src
pivot (
sum(src.val)
for src.category in ([1], [2], [3])
) as pvt;
drop table #example;