Modifying XML Column with Select Query - sql

I have a SQL Server table with a XML column which got information in. I want to select whole ID's from this table and modify my another xml column.
My query is;
declare #name nvarchar(max);
set #name = 'mark';
update table1
set table1.Information1.modify('insert <s n="' + cast((select cast(table2.Information2 as varchar(100))
from table2
where table2.Information2.exist('/r/s[#n=sql:variable("#name")]') = 1) as varchar(400)) + '"/> into (/r)[1]') where table1.Name = #name;
I'm getting
Msg 8172, Level 16, State 1, Line 5
The argument 1 of the XML data type method "modify" must be a string literal.
Any help would be nice.

Are you sure you want to put the whole xml into attribute of Information1, like this:
declare #name nvarchar(max), #data xml
select #name = 'mark'
select cast(Information2 as varchar(100)) as n
from table2 as t
where t.Information2.exist('/r/s[#n=sql:variable("#name")]') = 1
for xml raw('s')
update table1 set Information1.modify('insert sql:variable("#data") into (/r)[1]')
sql fiddle demo

Much as you are doing with the filtering, you need to use sql:variable. You can't build up a string inside a .modify function.
declare #newData xml
select #newData = '<s n="' +
cast(table2.Information2 as varchar(100))
+ '"/>'
from table2
where table2.Information2.exist('/r/s[#n=sql:variable("#name")]') = 1
update table1 set Information1.modify('insert sql:variable("#newData") into (/r)[1]')

Related

SET more than 1 row output into 1 variable

I am wanting to be able to save more than just 1 output from a SELECT query in a single variable.
Currently I am gathering my needed data like so:
DECLARE #something1 VARCHAR(MAX)
DECLARE #something2 VARCHAR(MAX)
DECLARE #something3 VARCHAR(MAX)
SET #something1 = (
SELECT
Custom AS 'XXL Format'
FROM
tblData
WHERE
ID = 1);
SET #something2 = (
SELECT
Custom.value('(/Individual/text())[1]', 'varchar(MAX)') AS 'Non XML Format'
FROM
tblData
WHERE
ID = 1)
SET #something3 = (
SELECT
tbl1.paper,
tbl2.type
FROM
tblData AS tbl1
JOIN tblData2 tbl2
ON tbl1.ID = tbl2.ID
WHERE
ID = 1);
I have the following demo that shows what I am wanting to do
DECLARE #tester VARCHAR(MAX)
SET #tester = (
SELECT
tbl1.Custom AS 'XXL Format',
tbl1.Custom.value('(/Individual/text())[1]', 'varchar(MAX)') AS 'Non XML Format'
tbl1.paper,
tbl2.type
FROM
tblData AS tbl1
JOIN tblData2 tbl2
ON tbl1.ID = tbl2.ID
WHERE
ID = 1);
I get the error of:
Only one expression can be specified in the select list when
the subquery is not introduced with EXISTS
Both demos can be found here
I have tried to set the variable to "table" and store the data like that but that also does not seem to work correctly [I'm sure I am doing something wrong - that may be the answer to this question I'm asking]
How can I just use 1 variable for all that the above query outputs?
Your error is that the subquery returned more than one value. You can use top if you don't care which value gets assigned:
SET #tester = (SELECT TOP (1) description FROM ForgeRock);
You have to return only one value you can try top or limit like SET #tester = (SELECT TOP 1 description FROM ForgeRock); or SET #tester = (SELECT description FROM ForgeRock LIMIT 1);
So I guess I was correct with the set the variable to a temp "table". I finally just now got it to work for my needs!
DECLARE #tmpTbl table (_xml, _parsedXML, _paper, _type)
INSERT INTO #tmpTbl
SELECT
tbl1.Custom AS 'XML Format',
tbl1.Custom.value('(/Individual/text())[1]', 'varchar(MAX)') AS 'Non XML Format',
tbl1.paper,
tbl2.type
FROM
tblData AS tbl1
JOIN tblData2 AS tbl2
ON tbl1.ID = tbl2.ID
WHERE
ID = 1;
DECLARE #something1 VARCHAR(MAX) = (SELECT _xml FROM #tmpTbl);
DECLARE #something2 VARCHAR(MAX) = (SELECT _parsedXML FROM #tmpTbl);
DECLARE #something3 VARCHAR(MAX) = (SELECT _paper FROM #tmpTbl);
DECLARE #something4 VARCHAR(MAX) = (SELECT _type FROM #tmpTbl);
DELETE FROM #tmpTbl --Not really needed but nice to be offical :)
This above stores the values into one place. Although its not inside 1 variable I guess having to create a temp table isn't all that bad for the database/performance...

Add range to an array with JSON_MODIFY

I am trying to add an array to another array using JSON_MODIFY.
The situation is, I have an array kind stored json data in database. It looks like this:
declare #base nvarchar(max) = '[{"name":"base"}]';
And I am getting another set of data which is also in the shape of array:
declare #test1 nvarchar(max) = '[{"name":"test1"},{"name":"example1"}]';
I am trying to use JSON_MODIFY and JSON_QUERY magics to append them together but it gives me unexpected results.
declare #base nvarchar(max) = '[{"name":"base"}]';
declare #test1 nvarchar(max) = '[{"name":"test1"},{"name":"example1"}]';
set #base = JSON_MODIFY(#base,'append $',JSON_QUERY(#test1));
select #base;
Output:
[{"name":"base"}, [{"name":"test1"},{"name":"example1"}]]
But what I want is using those methods to make it work like kind of Add-Range:
[{"name":"base"},{"name":"test1"},{"name":"example1"}]
I am kind of lost on this process and I don't know where to look at for this kind of functionality.
I will use this from a C# service to directly modify through the code. That's why I cannot use Store procedures and functions as well.
Edit #1:
With regarding to reply from #Salman A, i appreciate your answer but the thing is, as i said earlier, it is a little bit difficult to use on my query running through code. Which is:
declare #test1 nvarchar(max) = '[{"name":"test1"},{"name":"example1"}]';
UPDATE dbo.ExampleTable
SET [Data] = JSON_MODIFY([Data], 'append $', JSON_QUERY(#test1))
WHERE [UniqueId] = 'some_guid_here'
I have tried it to adapt the answer that i like this :
declare #test1 nvarchar(max) = '[{"name":"test1"},{"name":"example1"}]';
UPDATE dbo.ExampleTable
SET [Data] = (
select [Data] = JSON_MODIFY([Data],'append $',item)
from OPENJSON(#test1)
with ([item] nvarchar(max) '$' as JSON)
)
WHERE [UniqueId] = 'some_id'
Actually, it works if #test1 only have 1 item, but in case of having more than 1 item in #test1, it gives the error:
Subquery returned more than 1 value. This is not permitted when the subquery follows = .....
What is the logical way to use this in a update set subquery
You can use OPENJSON to convert the array to rows and append items one by one:
declare #base nvarchar(max) = '[{"name":"base"}]';
declare #test1 nvarchar(max) = '[{"name":"test1"},{"name":"example1"}]';
select #base = json_modify(#base, 'append $', item)
from openjson(#test1)
with ([item] nvarchar(max) '$' as json);
select #base;
Returns:
[{"name":"base"},{"name":"test1"},{"name":"example1"}]
Revised answer for update query
If you're using SQL Server 2017+ then a reasonably safe solution is to concatenate the array using STRING_AGG but build individual rows using JSON functions. It is relatively easy to use this idea in an update query:
DECLARE #base NVARCHAR(MAX) = '[{"name":"base"}]';
DECLARE #test NVARCHAR(MAX) = '[{"foo":"bar"},{"baz":"meh"}]';
SELECT '[' + STRING_AGG(jsonstr, ',') WITHIN GROUP (ORDER BY pos) + ']'
FROM (
SELECT value, 1000 + [key] FROM OPENJSON(#base)
UNION ALL
SELECT value, 2000 + [key] FROM OPENJSON(#test)
) AS x(jsonstr, pos);
Alternately, you can use a recursive CTE that calls JSON_MODIFY multiple times to build the JSON; you can use the result in update query:
CREATE TABLE t(
id INT NOT NULL PRIMARY KEY IDENTITY,
data NVARCHAR(MAX)
);
INSERT INTO t(data) VALUES
('[{"name":"1.1"}]'),
('[{"name":"2.1"},{"name":"2.2"}]');
WITH rows(data, pos) AS (
SELECT value, [key]
FROM OPENJSON('[{"foo":"bar"},{"baz":"meh"}]')
), rcte(id, data, pos) AS (
SELECT id, data, -1
FROM t
UNION ALL
SELECT prev.id, JSON_MODIFY(prev.data, 'append $', JSON_QUERY(curr.data)), prev.pos + 1
FROM rcte AS prev
JOIN rows AS curr ON curr.pos = prev.pos + 1
)
UPDATE t
SET data = (
SELECT TOP 1 data
FROM rcte
WHERE id = t.id
ORDER BY pos DESC
);
Demo on db<>fiddle

Stored procedure returning result even thought it's not supposed to

I have a stored procedure which looks like following:
ALTER PROCDURE [dbo].[zsp_selectallupceans_listProduction]
(#UPCList NVARCHAR(4000),
#EANList NVARCHAR(4000),
#Type TINYINT)
AS
SELECT
dd.UPC, dd.EAN, dd.EBAYID AS ItemID
FROM
ThirdPartyData AS dd
WHERE
EXISTS (SELECT 1 FROM dbo.SplitStringProduction(#UPCList,',') S1
WHERE dd.UPC = S1.val)
OR EXISTS (SELECT 1 FROM dbo.SplitStringProduction(#EANList,',') S2
WHERE dd.EAN = S2.val)
AND dd.Type = #Type
The parameters are passed like following:
#UPCList='709127309019',
#EanList='0709127309019',
#Type=4
The "SplitStringProduction" function looks like this:
ALTER FUNCTION [dbo].[SplitStringProduction]
(#string NVARCHAR(MAX),
#delimiter NVARCHAR(5))
RETURNS #t TABLE
(
val NVARCHAR(500)
)
AS
BEGIN
DECLARE #xml XML
SET #xml = N'<root><r>' + replace(#string,#delimiter,'</r><r>') + '</r></root>'
INSERT INTO #t(val)
SELECT
r.value('.','varchar(500)') AS item
FROM
#xml.nodes('//root/r') as records(r)
RETURN
END
Now when I do a simple select from my table like following:
select *
from thirdpartydata dd
where dd.UPC = '709127309019' -- note this is one of the parameters passed to the stored procedure...
I will get only 1 result with a column Type set to "1"....
Now when I try out my stored procedure:
exec zsp_selectallupceans_listProduction '709127309019','0709127309019',4
I still get 1 result, even though I'm not supposed to get any result, because if you can see the "Type" parameter is set to 4, thus no matching records should be found....
What am I doing wrong here, I can't seem to figure it out ??
You need to enclose the OR condition inside parenthesis:
WHERE (
EXISTS (SELECT 1 FROM dbo.SplitStringProduction(#UPCList,',') S1 WHERE dd.UPC=S1.val)
OR EXISTS (SELECT 1 FROM dbo.SplitStringProduction(#EANList,',') S2 WHERE dd.EAN=S2.val)
) AND dd.Type = #Type
Without them your query like this:
WHERE EXISTS (...)
OR (EXISTS (...) AND dd.Type = #Type)
And the result your get is because the first OR condition matches.

Local variable with multiple value list

I use Excel connection to connect to SQL Server to query data from SQL server to Excel.
I have below WHERE clause in the Excel connection couple times. I need to replace the WHERE multiple value list from time to time. To simply the replacement, I want to use a local parameter, #Trans. With the local parameter, I can change it only and all SQL will use it to query.
WHERE Type in ('R','D','C')
If it is single option, below code works.
DECLARE #TRans CHAR(200)= 'R';
SELECT .....
WHERE Type in (#Trans)
If it is multiple options, the below code does not works
DECLARE #TRans CHAR(200)= 'R,D,C';
SELECT .....
WHERE Type in (#Trans)
DECLARE #TRans CHAR(200)= '''R'''+','+'''D'''+','+'''C''';
SELECT .....
WHERE Type in (#Trans)
How to declare #Trans for multiple value list, for example ('R','D','C')? Thank you.
You can use dynamic sql
DECLARE #TRans VARCHAR(200)= '''R'',''D'',''C''';
DECLARE #sql VARCHAR(MAX) = '';
SET #sql = 'SELECT * FROM table WHERE Type in (' + #Trans + ');'
EXEC #sql
Take note of the quotes for the values in #TRans since these character values.
If you want to check the value of #sql which you will see the constructed sql statement, replace EXEC #sql with PRINT #sql.
Result of #sql
SELECT * FROM table WHERE Type in ('R','D','C');
As you can see by now, SQL Server does NOT support macro substition. This leaves a couple of options. One is to split the string.
If not 2016, here is a quick in-line approach which does not require a Table-Valued Function
Example
Declare #Trans varchar(max)='R,D,C' -- Notice no single quotes
Select ...
Where Type in (
Select RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace(#Trans,',','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
)
You can create a table named LocalParameter and keep local variables there. You can only get datas by updating LocalParameter table without changing the queries.
CREATE TABLE LocalParameter (Trans VARCHAR(MAX))
INSERT INTO LocalParameter
VALUES
(
',R,'
)
With LIKE you can use it like this:
SELECT .....
WHERE (SELECT TOP 1 A.Trans FROM LocalParameter A) LIKE ',' + Type + ','
To change WHERE clause:
UPDATE LocalParameter
SET Trans = ',R,D,C,'
Queries:
SELECT .....
WHERE (SELECT TOP 1 A.Trans FROM LocalParameter A) LIKE ',' + Type + ','
Local variables are added to the beginning and end of the comma.
You can use a split method to split csv values as shown below
DECLARE #delimiter VARCHAR(10)=','
DECLARE #input_string VARCHAR(200)='R,D,C'
;WITH CTE AS
(
SELECT
SUBSTRING(#input_string,0,CHARINDEX(#delimiter,#input_string)) AS ExtractedString,
SUBSTRING(#input_string,CHARINDEX(#delimiter,#input_string) + 1,LEN(#input_string)) AS PartString
WHERE CHARINDEX(#delimiter,#input_string)>0
UNION ALL
SELECT
SUBSTRING(PartString,0,CHARINDEX(#delimiter,PartString)) AS ExtractedString,
SUBSTRING(PartString,CHARINDEX(#delimiter,PartString)+1,LEN(PartString)) AS PartString
FROM CTE WHERE CHARINDEX(#delimiter,PartString)>0
)
SELECT ExtractedString FROM CTE
UNION ALL
SELECT
CASE WHEN CHARINDEX(#delimiter,REVERSE(#input_string))>0
THEN REVERSE(SUBSTRING(REVERSE(#input_string),0,CHARINDEX(#delimiter,REVERSE(#input_string))))
ELSE #input_string END
OPTION (MAXRECURSION 0)
This split method doesnt have any loops so it will be fast. then you integrate this with your query as below mentioned
DECLARE #delimiter VARCHAR(10)=','
DECLARE #input_string VARCHAR(200)='R,D,C'
;WITH CTE AS
(
SELECT
SUBSTRING(#input_string,0,CHARINDEX(#delimiter,#input_string)) AS ExtractedString,
SUBSTRING(#input_string,CHARINDEX(#delimiter,#input_string) + 1,LEN(#input_string)) AS PartString
WHERE CHARINDEX(#delimiter,#input_string)>0
UNION ALL
SELECT
SUBSTRING(PartString,0,CHARINDEX(#delimiter,PartString)) AS ExtractedString,
SUBSTRING(PartString,CHARINDEX(#delimiter,PartString)+1,LEN(PartString)) AS PartString
FROM CTE WHERE CHARINDEX(#delimiter,PartString)>0
)
SELECT * FROM [YourTableName] WHERE Type IN
(SELECT ExtractedString FROM CTE
UNION ALL
SELECT
CASE WHEN CHARINDEX(#delimiter,REVERSE(#input_string))>0
THEN REVERSE(SUBSTRING(REVERSE(#input_string),0,CHARINDEX(#delimiter,REVERSE(#input_string))))
ELSE #input_string END
)OPTION (MAXRECURSION 0)
If possible add a new table and then join to it in all your queries:
CREATE TABLE SelectedType
(
[Type] CHAR(1) PRIMARY KEY
)
INSERT INTO SelectedType
VALUES ('R','D','C')
Then your queries become:
SELECT *
FROM MyTable MT
INNER JOIN SelectedType [ST]
ON ST.[Type] = MT.[Type]
If you need to add, update or delete types then update the rows in SelectedType table.
This has the benefit of using SET BASED queries, is easy to understand and easy to add, update or delete required types.

Pass in list of parameters for LIKE query into stored procedure

I have a list of items that I would like to query on. The problem is that the number of items in the list is not constant. For example
select * from table1 where
field1 like #value1 + '%' OR
field1 like #value2 + '%'
I would like to pass value1, value2, etc into the stored procedure as a comma delimited string or something similar.
If you stored the values one per row in a table variable you could simply JOIN, or better, use WHERE EXISTS:
SELECT DISTINCT a.*
FROM Table1 a
WHERE EXISTS (SELECT 1
FROM #Table2 b
WHERE a.field1 like b.value + '%')
Here is a way you can pass a CSV to a stored proc, convert it to XML and use it in a join in your select.
Function to convert CSV to XML:
create function udf_CsvToXML(#Csv as varchar(8000),#Delim as varchar(15)=',')
returns xml
as
begin
declare #xml as xml = CAST('<XML>'+('<X>'+REPLACE(#Csv,#Delim,'</X><X>')+'</X></XML>') AS XML)
return #xml
end
Put the following in a stored proc, #Titles being a parameter instead of a declare:
declare #Titles varchar(8000) = NULL
SET #Titles = ISNULL(#Titles, 'ALL')
DECLARE #TitlesXML as XML
if upper(#Titles) = 'ALL'
SET #TitlesXML = (select distinct Title as X from LegalConfiguration for xml path(''), root('XML'))
else
SET #TitlesXML = dbo.udf_CsvToXML(#Titles,',')
select Title
from MonthlyTitlePerformance p
join (SELECT N.value('.[1]', 'varchar(25)') as value FROM #TitlesXML.nodes('/XML/X') as T(N)) tt
on tt.value = p.Title