JSON_MODIFY does not work with variable parameter - sql

I have a SQL query which modifies some JSON. I'm iterating through the data and modifies part of the JSON based on the iteration.
For that i need to pass a variable parameter into JSON_MODIFY, but for some reason, it doesn't work!
SET #json = JSON_MODIFY(#ProData, '$.' + #ProKey + '.hasAnswer', CAST(1 as BIT))
I also tried passing the whole expression as a variable:
DECLARE #hasAnswerPath VARCHAR(100);
SET #hasAnswerPath = '$.' + #ProKey + '.hasAnswer';
SET #json = JSON_MODIFY(#ProData, #hasAnswerPath, CAST(1 as BIT))
But it has the same output, the hasAnswer is added to the root of the JSON and not in the element specified by #ProKey.
This works just fine:
SET #json = JSON_MODIFY(#ProData, '$.SomeName1.hasAnswer', CAST(1 as BIT))
It's like the #ProKey is disregarded.
Complete query:
BEGIN TRAN
DECLARE #ProID as uniqueidentifier;
DECLARE #ProData as nvarchar(max);
DECLARE #ProKey as varchar(200);
DECLARE ProCursor CURSOR FOR
SELECT Id, [Data] FROM [dbo].[PRO]
OPEN ProCursor;
FETCH NEXT FROM ProCursor INTO #ProID, #ProData;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #json NVARCHAR(max);
DECLARE DataCursor CURSOR FOR
SELECT [key] FROM OPENJSON(#ProData) WHERE type = 5; --5 is object data
OPEN DataCursor;
FETCH NEXT FROM DataCursor INTO #ProKey;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #json=JSON_MODIFY(#ProData, '$.' + #ProKey + '.hasAnswer', CAST(1 as BIT))
SET #json=JSON_MODIFY(#json,'$.' + #ProKey + '.questionType','intro')
FETCH NEXT FROM DataCursor INTO #ProKey;
END;
UPDATE [dbo].[PRO]
SET [Data] = #json
WHERE Id = #ProID
PRINT #json
CLOSE DataCursor;
DEALLOCATE DataCursor;
FETCH NEXT FROM ProCursor INTO #ProID, #ProData;
END
CLOSE ProCursor;
DEALLOCATE ProCursor;
ROLLBACK
Sample JSON:
{
"SomeName1": {
"header": "Some text",
"answer": {
"type": "specified",
"numberValue": 1.0
}
},
"SomeName2": {
"header": "Some text",
"answer": {
"type": "specified",
"numberValue": 4.0
}
},
"SomeName3": {
"header": "Some text",
"answer": {
"type": "specified",
"numberValue": 2.0
}
}
}
Expected result:
},
"SomeName1": {
"header": "Some text",
"answer": {
"type": "specified",
"numberValue": 1.0
}
"hasAnswer": true,
"questionType": "intro",
}
}
Actual result:
},
"SomeName1": {
"header": "Some text",
"answer": {
"type": "specified",
"numberValue": 1.0
}
}
},
"hasAnswer":true,
"questionType":"intro"
}
What am I doing wrong here?

You seem to be overwriting your variables unintentionally:
-- first iteration
SET #json=JSON_MODIFY(#ProData, '$.' + #ProKey + '.hasAnswer', CAST(1 as BIT))
-- #json contains the #prodata after modification to first key (#prodata itself is not changed)
SET #json=JSON_MODIFY(#json,'$.' + #ProKey + '.questionType','intro')
-- #json (its first key to be more precise) is modified further
But on next iteration, this line will revert the modified #json back to the original value of #prodata. Only the last key will retain the modifications:
-- second iteration
SET #json=JSON_MODIFY(#ProData, '$.' + #ProKey + '.hasAnswer', CAST(1 as BIT))
-- you just overwrote your modifications with the value inside #prodata
The solution is to re-arrange the code a little, may be initialize #json outside the loop.

Related

Using JSON_VALUE for parse column in SQL Server table

I have never worked with JSON in SQL Server before that's why need some help.
I have written a simple snippet of code:
DECLARE #json NVARCHAR(4000)
SET #json =
N'{
"id":"40476",
"tags":[
{
"id":"5f5883",
},
{
"id":"5fc8",
}
],
"type":"student",
"external_id":"40614476"
}'
SELECT
JSON_value(#json, '$.tags[0].id') as tags
In sample above I write code how get first "id" from "tags".
But how looks like script if in "tags" not 2 "id", but an unknown number this "id" and result should be in column like this:
1 5f5883
2 5fc8
You may use OPENJSON() with explicit schema to parse the $.tags JSON array:
DECLARE #json NVARCHAR(4000)
SET #json =
N'{
"id":"40476",
"tags":[
{
"id":"5f5883"
},
{
"id":"5fc8"
}
],
"type":"student",
"external_id":"40614476"
}'
SELECT id
FROM OPENJSON(#json, '$.tags') WITH (id varchar(10) '$.id')
Result:
id
------
5f5883
5fc8
If you want to get the index of each id in the $.tags JSON array, then you need a combination of OPENJSON() with default schema and JSON_VALUE():
SELECT CONVERT(int, [key]) AS rn, JSON_VALUE([value], '$.id') AS id
FROM OPENJSON(#json, '$.tags')
Result:
rn id
----------
0 5f5883
1 5fc8

Update properties in SQL Json fields with nested objects

I'm new to JSON methods in SQL
I'm working with a huge JSON field, which has such a structure :
{
"A": 1,
"B": 2,
"C": 0,
"data": [
{
"id": "id_i_want",
"value": "[{
"prop1":7,
"prop2":"X",
"prop3":"4",
"passages":[
{
"id":0,
"number":"E24",
"date":"11/12/2019"
},
{
"id":0,
"number":"F28",
"date":"11/11/2019"
},
{
...
}
]
}]
},
{
"id": "id_i_do_NOT_want",
"value": Same structure as above
}
]
}
This JSON fields is stored in a nvarchar(MAX) field in SQLServer.
So the JSON has a property data, which contains a list of elements.
These elements have a value property, which contains a list of passages.
All the passages currently have id = 0
What I need to do :
I would like ton increment all the id of passages, starting from 1, but only the ones in the object which has the ID id_i_want, and NOT the others.
How can I do that with a SQL script ?
I tried to follow this post, but without success
Any help appreciated
First change the Json data to tabular data, then update the table, and then convert the table back to Json.
I have prepared the following code, you can use it easily with a little change.
declare #varData nvarchar(max) = '{
"A": 1,
"B": 2,
"C": 0,
"data": [
{
"id": "id_i_want",
"value": [{
"prop1":7,
"prop2":"X",
"prop3":"4",
"passages":[
{
"id":0,
"number":"E24",
"date":"11/12/2019"
},
{
"id":0,
"number":"F28",
"date":"11/11/2019"
}
]
}]
},
{
"id": "id_i_do_NOT_want"
}
]
}';
DECLARE #jsontable TABLE (A varchar(5), b varchar(5), c varchar(5),id NVARCHAR(50),prop1 int,prop2 varchar(5),prop3 varchar(5),mid int ,number varchar(5),date date);
DECLARE #maintable TABLE (A varchar(5), b varchar(5), c varchar(5),id NVARCHAR(50),prop1 int,prop2 varchar(5),prop3 varchar(5),mid int ,number varchar(5),date date);
insert into #jsontable
SELECT A,b,C,id,prop1,prop2,prop3,mid,number,date
FROM OPENJSON(#varData)
WITH (
A varchar(5) '$.A',
B varchar(5) '$.B',
C varchar(5) '$.C',
jdata NVARCHAR(MAX) '$.data' AS JSON
)
OUTER APPLY OPENJSON(jdata)
WITH (
id NVARCHAR(50) '$.id',
jvalue NVARCHAR(MAX) '$.value' AS JSON
)
OUTER APPLY OPENJSON(jvalue)
WITH (
prop1 int '$.prop1',
prop2 varchar(5) '$.prop2',
prop3 varchar(5) '$.prop3',
jpassages NVARCHAR(MAX) '$.passages' AS JSON
)
OUTER APPLY OPENJSON(jpassages)
WITH (
mid int '$.id',
number varchar(5) '$.number',
date date '$.date'
)
where id = 'id_i_want'
Declare #A varchar(5)
Declare #b varchar(5)
Declare #c varchar(5)
Declare #id NVARCHAR(50)
Declare #prop1 int
Declare #prop2 varchar(5)
Declare #prop3 varchar(5)
Declare #mid int = 0
Declare #number varchar(5)
Declare #date date
While((Select Count(*) From #jsontable)>0)
Begin
set #mid = #mid + 1
Set #A=(Select Top 1 A From #jsontable)
Set #B=(Select Top 1 B From #jsontable)
Set #C=(Select Top 1 C From #jsontable)
Set #id=(Select Top 1 id From #jsontable)
Set #prop1=(Select Top 1 prop1 From #jsontable)
Set #prop2=(Select Top 1 prop2 From #jsontable)
Set #prop3=(Select Top 1 prop3 From #jsontable)
Set #number=(Select Top 1 number From #jsontable)
Set #date=(Select Top 1 date From #jsontable)
insert into #maintable values
(#A,#B,#C,#id,#prop1,#prop2,#prop3,#mid,#number,#date)
Delete #jsontable Where A=#A and B = #B and C = #C and id = #id and prop1 = #prop1
and prop2 = #prop2 and prop3 = #prop3 and number = #number and date = #date
End
select * from #maintable
demo in db<>fiddle

Concatenate nested JSON in SQL Server

I am trying to concatenate nested JSON in SQL, how to do that.
Please help me out with this.
Example: I have JSON Like below.
[
{
"Name": "Cards",
"Value": [
"Pack of 24 Dare Cards"
],
"SourceSpecified": false,
"Any": []
},
{
"Name": "Boppers and Shot Glasses",
"Value": [
"12 Willy Shot Glass and 12 Hen Boppers"
],
"SourceSpecified": false,
"Any": []
}
]
I want Out Put:
Pack of 24 Dare Cards-12 Willy Shot Glass and 12 Hen Boppers
If Value always contains one element
DECLARE #JSON NVARCHAR(MAX) = ' [
{
"Name": "Cards",
"Value": [
"Pack of 24 Dare Cards"
],
"SourceSpecified": false,
"Any": []
},
{
"Name": "Boppers and Shot Glasses",
"Value": [
"12 Willy Shot Glass and 12 Hen Boppers"
],
"SourceSpecified": false,
"Any": []
}
]'
SELECT STRING_AGG(Val, '-')
FROM OPENJSON(#JSON)
WITH (Val NVARCHAR(MAX) '$.Value[0]')
Support of multiple values is also easy.
SELECT STRING_AGG(Val, '-')
FROM OPENJSON(#JSON)
WITH (VALUE NVARCHAR(MAX) '$.Value' AS JSON)
OUTER APPLY OPENJSON(VALUE) WITH (Val NVARCHAR(MAX) '$')
This one I achieved with creating a scalar-valued function to populate concatenated value.
Create FUNCTION [dbo].[test]
(
#VariationData nvarchar(max)
)
RETURNS nvarchar(max)
AS
BEGIN
Declare #val nvarchar(max)
select #val = COALESCE(#val+', ','')+ Value from OPENJSON(#VariationData)
With ([Value] NVARCHAR(MAX) N'$.Value[0]')
return #val
END
Call function with nested JSON value i.e
Select [dbo].[test] (#JSON);
This worked for me.

How to make JSON from SQL query in MS SQL 2014

Question: What is best solution to generate JSON from a SQL query in MS SQL 2014? I created a procedure, but it is very slow.
My Example:
DECLARE #customers xml;
DECLARE #json NVARCHAR(max);
SET #customers = (SELECT * FROM dbo.Customers FOR XML path, root)
EXEC [dbo].[HTTP_JSON] #customers, #json
EXEC [dbo].[HTTP_JSON](#Shopping)
Create PROCEDURE [dbo].[HTTP_JSON]
#parameters xml, #response NVARCHAR(max) OUTPUT
WITH EXEC AS CALLER
AS
set #response = (SELECT Stuff(
(SELECT * from
(SELECT ',
{'+
Stuff((SELECT ',"'+coalesce(b.c.value('local-name(.)', 'NVARCHAR(MAX)'),'')+'":"'+
b.c.value('text()[1]','NVARCHAR(MAX)') +'"'
from x.a.nodes('*') b(c)
for xml path(''),TYPE).value('(./text())[1]','NVARCHAR(MAX)')
,1,1,'')+'}'
from #parameters.nodes('/root/*') x(a)
) JSON(theLine)
for xml path(''),TYPE).value('.','NVARCHAR(MAX)' )
,1,1,''))
GO
Just for fun, I created a scalar function based off of my prior answer.
Aside from the obvious XML parameter, I added two additional: 1) Include Header (illustrated below), and 2) ToLower case (I prefer my JSON field names in lower case which links to my classes and such).
If the query is more than one record, a formatted array will be returned.
Declare #Table table (ID int,Active bit,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50))
Insert into #Table values
(1,1,'John','Smith','john.smith#email.com'),
(2,0,'Jane','Doe' ,'jane.doe#email.com')
Select A.ID
,A.Last_Name
,A.First_Name
,B.JSON
From #Table A
Cross Apply (Select JSON=[dbo].[udf-Str-JSON](0,1,(Select A.* For XML Raw)) ) B
Returns
ID Last_Name First_Name JSON
1 Smith John {"id":"1","active":"1","first_name":"John","last_name":"Smith","email":"john.smith#email.com"}
2 Doe Jane {"id":"2","active":"0","first_name":"Jane","last_name":"Doe","email":"jane.doe#email.com"}
Or even more simply
Select JSON=[dbo].[udf-Str-JSON](0,1,(Select * From #Table for XML RAW))
Returns with Header ON
{
"status": {
"successful": "true",
"timestamp": "2016-10-09 06:08:16 GMT",
"rows": "2"
},
"results": [{
"id": "1",
"active": "1",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith#email.com"
}, {
"id": "2",
"active": "0",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe#email.com"
}]
}
Returns with Header Off
[{
"id": "1",
"active": "1",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith#email.com"
}, {
"id": "2",
"active": "0",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe#email.com"
}]
The UDF
ALTER FUNCTION [dbo].[udf-Str-JSON] (#IncludeHead int,#ToLowerCase int,#XML xml)
Returns varchar(max)
AS
Begin
Declare #Head varchar(max) = '',#JSON varchar(max) = ''
; with cteEAV as (Select RowNr=Row_Number() over (Order By (Select NULL))
,Entity = xRow.value('#*[1]','varchar(100)')
,Attribute = xAtt.value('local-name(.)','varchar(100)')
,Value = xAtt.value('.','varchar(max)')
From #XML.nodes('/row') As R(xRow)
Cross Apply R.xRow.nodes('./#*') As A(xAtt) )
,cteSum as (Select Records=count(Distinct Entity)
,Head = IIF(#IncludeHead=0,IIF(count(Distinct Entity)<=1,'[getResults]','[[getResults]]'),Concat('{"status":{"successful":"true","timestamp":"',Format(GetUTCDate(),'yyyy-MM-dd hh:mm:ss '),'GMT','","rows":"',count(Distinct Entity),'"},"results":[[getResults]]}') )
From cteEAV)
,cteBld as (Select *
,NewRow=IIF(Lag(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,'',',{')
,EndRow=IIF(Lead(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,',','}')
,JSON=Concat('"',IIF(#ToLowerCase=1,Lower(Attribute),Attribute),'":','"',Value,'"')
From cteEAV )
Select #JSON = #JSON+NewRow+JSON+EndRow,#Head = Head From cteBld, cteSum
Return Replace(#Head,'[getResults]',Stuff(#JSON,1,1,''))
End
-- Parameter 1: #IncludeHead 1/0
-- Parameter 2: #ToLowerCase 1/0 (converts field name to lowercase
-- Parameter 3: (Select * From ... for XML RAW)
**EDIT - Corrected Typo
The following should create the JSON array for just about any data set. However, I have not created a way to convert bit to true/false yet.
Just one point to consider: The FIRST column in the initial SELECT has to be the Primary Key which is equates to the ENTITY field. In this case, Select * from #User for XML RAW ... ID is the Entity and just so happens to be the first field in the table
As far as performance, 500 records with 19 fields creates a JSON string 191,987 bytes in 0.694 seconds (50 records in 0.098 seconds)
Consider the following:
Declare #User table (ID int,Active bit,First_Name varchar(50),Last_Name varchar(50),EMail varchar(50),LastOn DateTime)
Insert into #User values
(1,1,'John','Smith','john.smith#email.com','2016-10-05 17:32:41.903'),
(2,0,'Jane','Doe' ,'jane.doe#email.com','2016-10-05 08:25:18.203')
Declare #XML xml = (Select * From #User for XML RAW)
Declare #JSON varchar(max) = ''
;with cteEAV as (
Select RowNr = Row_Number() over (Order By (Select NULL))
,Entity = xRow.value('#*[1]','varchar(100)')
,Attribute = xAtt.value('local-name(.)','varchar(100)')
,Value = xAtt.value('.','varchar(max)')
From #XML.nodes('/row') As A(xRow)
Cross Apply A.xRow.nodes('./#*') As B(xAtt) )
,cteBld as (
Select *
,NewRow = IIF(Lag(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,'',',{')
,EndRow = IIF(Lead(Entity,1) over (Partition By Entity Order By (Select NULL))=Entity,',','}')
,JSON = Concat('"',Attribute,'":','"',Value,'"')
From cteEAV )
Select #JSON = #JSON+NewRow+JSON+EndRow
From cteBld
Select '['+Stuff(#JSON,1,1,'')+']'
Returns
[{"ID":1, "Active":1, "First_Name":"John", "Last_Name":"Smith", "EMail":"john.smith#email.com", "LastOn":"2016-10-05T17:32:41.903", "TotalSales":25569.0000} ,{"ID":2, "Active":0, "First_Name":"Jane", "Last_Name":"Doe", "EMail":"jane.doe#email.com", "LastOn":"2016-10-05T08:25:18.203", "TotalSales":22888.0000}]
A more readable version
cteEAV will dynamically unpivot the data and generate the following:
cteBLD will extend and add flags New/End Row
The Final Select
This will put it all together and generate one final string which can be wrapped or nested as you please.

String concatenation in procedure parameters

I want to do something like this:
declare #var varchar(50) = '2';
exec sp_myprocedure 'first', 'sec ' + #var
but I get an error:
Incorrect syntax near '+'.
Of course I can set variable before exec procedure:
set #var = 'sec ' + #var;
but I don't want this, because I have many execution in my code and I don't want create million variables.
You need to fix your code, sorry. There is no magic that will make this legal:
EXEC dbo.procedure_name #var + 'constant';
Also, STOP using the sp_ prefix for stored procedures.
I think it's not possible to use string concatenation in procedure execution if you're using EXEC.
From this:
Execute a stored procedure or function
[ { EXEC | EXECUTE } ]
{
[ #return_status = ]
{ module_name [ ;number ] | #module_name_var }
[ [ #parameter = ] { value
| #variable [ OUTPUT ]
| [ DEFAULT ]
}
]
[ ,...n ]
[ WITH <execute_option> [ ,...n ] ]
} [;]
The #parameter must be a value.
But this: 'sec ' + #var is an expression.
You don't need to declare a million variables, you simply need to declare a single variable, and set it prior to each EXEC:
DECLARE #cmd nvarchar(max) = '';
declare #var varchar(50) = '2';
set #cmd = 'sec ' + #var;
exec sp_myprocedure 'first', #cmd
set #cmd = 'try ' + #var;
exec sp_myprocedure 'first', #cmd
set #cmd = 'foo ' + #var;
exec sp_myprocedure 'first', #cmd
set #cmd = 'bar ' + #var;
exec sp_myprocedure 'first', #cmd