Update multiple XML nodes in single Update SQL statement - sql

I want to update multiple XML nodes in single Update query
XML:
<TransmissionData>
<HolderName>Tony Chase</CardHolderName>
<Type>VS</CardType>
<TransactionDetails>
<TransactionId />
</TransactionDetails>
<ValueType>CAPT</ValueType>
</TransmissionData>
This is what I have tried:
DECLARE #Type nVarchar(10) = 'MS'
DECLARE #ValueType nVarchar(10) = 'OPT'
DECLARE #TransactionId bigint = 122344555
UPDATE
Table1
SET
Data.modify('replace value of (/TransmissionData/CardType/text())[1] with sql:variable("#Type")'),
Data.modify('replace value of (/TransmissionData/ValueType/text())[2] with sql:variable("#ValueType")'),
Data.modify('replace value of (/TransmissionData/TransactionDetails/TransactionId/text())[1] with sql:variable("#TransactionId")')
WHERE
RequestId = 2133831593
It works only for single Update, if I use more then one like ValueType and TransactionId, it shows an error. Please help me - how to update this?
Msg 264, Level 16, State 1, Line 7
The column name 'TransmissionData' is specified more than once in the SET clause or column list of an INSERT. A column cannot be assigned more than one value in the same clause. Modify the clause to make sure that a column is updated only once. If this statement updates or inserts columns into a view, column aliasing can conceal the duplication in your code.

Please try the following solution.
It is using XQuery and its FLWOR expression.
It will update XML elements in question, leaving everything else intact.
I had to fix the input XML to make it well-formed.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata XML);
INSERT INTO #tbl (xmldata) VALUES
(N'<TransmissionData>
<CardHolderName>Tony Chase</CardHolderName>
<CardType>VS</CardType>
<TransactionDetails>
<TransactionId />
</TransactionDetails>
<ValueType>CAPT</ValueType>
</TransmissionData>');
-- DDL and sample data population, end
DECLARE #Type NVARCHAR(10) = 'MS'
, #ValueType NVARCHAR(10) = 'OPT'
, #TransactionId BIGINT = 122344555;
UPDATE #tbl
SET xmldata = xmldata.query('<TransmissionData>
{
for $x in /TransmissionData/*
return if (local-name($x)="CardType") then
<CardType>{sql:variable("#Type")}</CardType>
else if (local-name($x)="ValueType") then
<ValueType>{sql:variable("#ValueType")}</ValueType>
else if (local-name($x)="TransactionDetails") then
<TransactionDetails>
<TransactionId>{sql:variable("#TransactionId")}</TransactionId>
</TransactionDetails>
else $x
}
</TransmissionData>');
-- test
SELECT * FROM #tbl;

Related

SQL Server XQuery.modify syntax issues, unable to resolve

I have a SQL Server 2016 table dbo.Agent which has a column called XMLDta, but its datatype is nvarchar(max).
The data within this column is structured as XML, like so:
<misc id="m12345">
<pauth id="p12345">
<AEmail/>
<ipauth id="i12345">
<IProd>xxxxxx</IProd>
<achannel id="00000000">
<Chan>ABCEDF</Chan>
<Selected>1</Selected>
</achannel>
<Seg>ZZZ</Seg>
<states id="s12345">
<Sel>0</Sel>
<Avail>1</Avail>
<State>XX</State>
</states>
<states id="s67890">
<Sel>1</Sel>
<Avail>1</Avail>
<State>YY</State>
</states>
<LOB>FFFF</LOB>
<AUW>abc#email.com</AUW>
<WQue>10</WQue>
<AgChan>ABCEDF,</AgChan>
<State>XX,YY,</State>
<Status>Active</Status>
</ipauth>
<ipauth>
....
</ipauth>
</pauth>
</misc>
Trying to modify the Status node in above xml using following XQuery.modify() SQL/XQuery statement:
UPDATE dbo.Agent
SET CAST(xmldta AS xml).modify('replace value of (/misc/pauth/ipauth/Status/text()) with "Pending"')
WHERE agID = 209
But I keep getting the following error:
Incorrect syntax near '('.
Also tried using this SQL:
DECLARE #Dta As XML
DECLARE #id AS INT = 209
SELECT #Dta = CAST(XMLDta AS XML)
FROM dbo.Agent
WHERE agID = #id
UPDATE dbo.Agent
SET #Dta.modify('replace value of (/misc/pauth/ipauth/Status/text()) with "Pending"')
WHERE agID = 209
But I still get the same error:
Incorrect syntax near '('.
How do I correctly structure my SQL statement so it can reference the XMLDta column within the XQuery.modify function?
Please remember XMLDta column has a datatype of nvarchar(max) in the dbo.Agent table.
If I am going about this wrong way, please let me know and suggest better path.
Thanks and any guidance would be greatly appreciated.
Rebecca
Please take into account that the SQL Server XQuery .modify() method will update just one single XML element at a time.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata NVARCHAR(MAX));
INSERT INTO #tbl (xmldata) VALUES
(N'<misc id="m12345">
<pauth id="p12345">
<AEmail/>
<ipauth id="i12345">
<IProd>xxxxxx</IProd>
<achannel id="00000000">
<Chan>ABCEDF</Chan>
<Selected>1</Selected>
</achannel>
<Seg>ZZZ</Seg>
<states id="s12345">
<Sel>0</Sel>
<Avail>1</Avail>
<State>XX</State>
</states>
<states id="s67890">
<Sel>1</Sel>
<Avail>1</Avail>
<State>YY</State>
</states>
<LOB>FFFF</LOB>
<AUW>abc#email.com</AUW>
<WQue>10</WQue>
<AgChan>ABCEDF,</AgChan>
<State>XX,YY,</State>
<Status>Active</Status>
</ipauth>
<ipauth>....</ipauth>
</pauth>
</misc>');
-- DDL and sample data population, end
DECLARE #Dta AS XML
, #param VARCHAR(30) = 'Pending'
, #id AS INT = 1;
SET #Dta = (SELECT TRY_CAST(xmldata AS XML) FROM #tbl WHERE ID = #id);
SET #Dta.modify('replace value of (/misc/pauth/ipauth/Status/text())[1] with sql:variable("#param")');
UPDATE #tbl
SET xmldata = TRY_CAST(#Dta AS NVARCHAR(MAX))
WHERE ID = 1;
SELECT * FROM #tbl;

How to not insert xmlns in SQL Server?

I need to add some XML elements into an XML column in SQL Server.
Here's a simplified example of my code:
DECLARE #temp XML = '<Rate>' + CONVERT(VARCHAR(20), #RateAmt, 1) + '</Rate>'
UPDATE [TableName]
SET [XMLValue].modify('declare namespace ns="http://www.mycompany.com/schema";insert sql:variable("#temp") as last into (/ns:Element1/ns:Element2)[1]')
WHERE id = #Id
Here's the output:
<Rate xmlns="">12.00</Rate>
So, the code is working, however, how do I remove the xmlns="" attribute?
Why are you inserting a namespace if you don't want one in the xml?
DECLARE #RateAmt decimal(9,2) = 12.00
DECLARE #temp XML = '<Rate>' + CONVERT(VARCHAR(20), #RateAmt, 1) + '</Rate>'
DECLARE #tempTable TABLE
(
Column1 Xml
)
INSERT INTO #tempTable(Column1)
SELECT #temp
OR
UPDATE #tempTable
SET Column1 = (SELECT #temp)
SELECT * FROM #tempTable
<Rate>12.00</Rate>
(1 row(s) affected)
There is an accepted answer already (especially concerning your namespace issue), great, just some hints:
There are very rare situation where one should create XML via string concatenation... Especially in connection with strings (special characters!), numbers (format!) and date/time values (culture and format!) it is much better to rely on the implicit translations using SELECT ... FOR XML ...
DECLARE #RateAmt DECIMAL(12,4)=12.0;
This is possible, but not good:
DECLARE #temp XML = '<Rate>' + CONVERT(VARCHAR(20), #RateAmt, 1) +'</Rate>'
Better try this
DECLARE #temp XML=(SELECT #RateAmt FOR XML PATH('Rate'));
Your attempt to insert this into existing XML can be done the way you do it already (create the XML-node externally and insert it as-is), it might be easier to insert the plain value:
DECLARE #tbl TABLE(ID INT IDENTITY,XMLValue XML);
INSERT INTO #tbl VALUES
(N'<Element1><Element2><test>FirstTest</test></Element2></Element1>')
,(N'<Element1><Element2><test>Second</test></Element2></Element1>');
--ID=1: Insert the way you do it:
UPDATE #tbl
SET [XMLValue].modify('insert sql:variable("#temp") as last into (/Element1/Element2)[1]')
WHERE id = 1
--ID=2: Insert the value of #RateAmt directly
SET #RateAmt=100.00;
UPDATE #tbl
SET [XMLValue].modify('insert <Rate>{sql:variable("#RateAmt")}</Rate> as last into (/Element1/Element2)[1]')
WHERE id = 2
This is Result ID=1
<Element1>
<Element2>
<test>FirstTest</test>
<Rate>12.0000</Rate>
</Element2>
</Element1>
And ID=2
<Element1>
<Element2>
<test>Second</test>
<Rate>100</Rate>
</Element2>
</Element1>

SQL Merge Statement - Output into a scalar variable (SQL Server)

I'm getting my head around the MERGE statement in SQL server. I generally use it to insert/update a single row, which I realise isn't the only use, but it's something I seem to do quite often.
But what happens if you want to insert a value of 1, or update to increment the value and output the incremented value eg:
CREATE TABLE [Counter] (
[Key] VARCHAR(255) NOT NULL PRIMARY KEY,
[Value] INT NOT NULL
);
DECLARE #paramKey VARCHAR(255);
SET #paramKey = 'String';
MERGE [Counter] AS targt
USING (Values(#paramKey)) AS source ([Key])
ON (targt.[Key] = source.[Key])
WHEN MATCHED THEN
UPDATE SET Value = Value +1
WHEN NOT MATCHED THEN
INSERT ([Key], Value)
VALUES (source.[Key], 1);
-- but now I want the new value!
Is there a way of doing this? I notice the output clause in https://msdn.microsoft.com/en-gb/library/bb510625.aspx but it doesn't seem to work with scalars (I could output to a single row-ed table variable but that seems wrong):
-- using table variables but seems
DECLARE #paramKey VARCHAR(255), #value int;
SET #paramKey = 'String'
DECLARE #Tab table (
[Value] INT
)
MERGE Counter AS targt
USING (Values(#paramKey)) AS source ([Key])
ON (targt.[Key] = source.[Key])
WHEN MATCHED THEN
UPDATE SET Value = Value +1
WHEN NOT MATCHED THEN
INSERT ([Key], Value)
VALUES (source.[Key], 1)
OUTPUT inserted.[Value] INTO #Tab;
-- can now use #Tab as a single rowed table variable
Is there a better option?

SQL - "incrementing" a char value causes collation error

I'm dealing with a table in which a bunch of arbitrary settings are stored as VARCHAR(255) values. The particular one I'm tasked with dealing with is a sequence number that needs to be incremented and returned to the caller. (Again, note that the sequence "number" is stored as VARCHAR, which is something I don't have any control over).
Because it's a sequence number, I don't really want to select and update in separate steps. When I've dealt with this sort of thing in the past with actual numeric fields, my method has been something like
UPDATE TABLE SET #SEQ_NUM = VALUE = VALUE + 1
which increments the value and gives me the updated value in one swell foop. I thought in this situation, I'd try the same basic thing with casts:
DECLARE #SEQ_NUM VARCHAR(255)
UPDATE SOME_TABLE
SET #SEQ_NUM = VALUE = CAST((CAST(VALUE AS INT) + 1) AS VARCHAR)
WHERE NAME = 'SOME_NAME'
The actual update works fine so long as I don't try to assign the result to the variable; as soon as I do, I receive the following error:
Msg 549, Level 16, State 1, Line 4 The collation
'SQL_Latin1_General_CP1_CI_AS' of receiving variable is not equal to
the collation 'Latin1_General_BIN' of column 'VALUE'.
I understand what that means, but I don't understand why it's happening, or by extension, how to remedy the issue.
As an aside to fixing the specific error, I'd welcome suggestions for alternative approaches to incrementing a char sequence "number".
From one of the comments, sounds like you may have already hit on this, but here's what I would recommend:
UPDATE TABLE
SET VALUE = CAST((CAST(VALUE AS INT) + 1) AS VARCHAR)
OUTPUT inserted.VALUE
WHERE NAME = 'SOME_NAME'
This will output the new value like a SELECT statement does. You can also cast inserted.VALUE to an int if you wanted to do that in the SQL.
If you wanted to put the value into #SEQ_NUM instead of outputing the value from the statement/stored procedure, you can't use a scalar variable, but you can pump it into a table variable, like so:
DECLARE #SEQ_NUM AS TABLE ( VALUE VARCHAR(255) );
UPDATE TABLE
SET VALUE = CAST((CAST(VALUE AS INT) + 1) AS VARCHAR)
OUTPUT inserted.VALUE INTO #SEQ_NUM ( VALUE )
WHERE NAME = 'SOME_NAME'
SELECT VALUE FROM #SEQ_NUM
Maintaining a sequential number manually is by no means a solution I'd like to work with, but I can understand there might be constraints around this.
If you break it down in to 2 steps, then you can work around the issue. Note I've replaced your WHERE clause for this example code to work:
CREATE TABLE #SOME_TABLE ( [VALUE] VARCHAR(255) )
INSERT INTO #SOME_TABLE
( VALUE )
VALUES ( '12345' )
DECLARE #SEQ_NUM VARCHAR(255)
UPDATE #SOME_TABLE
SET [VALUE] = CAST(( CAST([VALUE] AS INT) + 1 ) AS VARCHAR(255))
WHERE 1 = 1
SELECT *
FROM #SOME_TABLE
SELECT #SEQ_NUM = [VALUE]
FROM #SOME_TABLE
WHERE 1 = 1
SELECT #SEQ_NUM
DROP TABLE #SOME_TABLE
You can continue using the quirky update in OP but you have to split the triple assignment #Variable = Column = Expression in the UPDATE statement to two simple assignments of #Variable = Expression and Column = #Variable like this
CREATE TABLE #SOME_TABLE (
NAME VARCHAR(255)
, VALUE VARCHAR(255) COLLATE Latin1_General_BIN
)
INSERT #SOME_TABLE SELECT 'SOME_NAME', '42'
DECLARE #SEQ_NUM VARCHAR(255)
/*
-- this quirky update fails on COLLATION mismatch or data-type mismatch
UPDATE #SOME_TABLE
SET #SEQ_NUM = VALUE = CAST((CAST(VALUE AS INT) + 1) AS VARCHAR)
WHERE NAME = 'SOME_NAME'
*/
-- this quirky update works in all cases
UPDATE #SOME_TABLE
SET #SEQ_NUM = CAST((CAST(VALUE AS INT) + 1) AS VARCHAR)
, VALUE = #SEQ_NUM
WHERE NAME = 'SOME_NAME'
SELECT *, #SEQ_NUM FROM #SOME_TABLE
This simple rewrite prevents db-engine complaining on difference in data-type between #Variable and Column too (e.g. VARCHAR vs NVARCHAR) and seems like a more "portable" way of doing quirky updates (if there is such thing)

SQL Delete Where Not In

I have a relation mapping table like this:
attributeid bigint
productid bigint
To clean relations that are not used any more, I want to delete all recors where productid = x and attributeid not in (#includedIds), like the following example:
#attributetypeid bigint,
#productid bigint,
#includedids varchar(MAX)
DELETE FROM reltable
WHERE productid = #productid AND
attributetypeid = #attributetypeid AND
attributeid NOT IN (#includedids);
When running the SQL with the includedids param containing more than 1 id - like this: 25,26 - I get a SqlException saying:
Error converting data type varchar to bigint.
And that is of course due to the , in that varchar(max) param...
How should I construct my delete statement to make it work?
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[ListToTable] (
/*
FUNCTION ListToTable
Usage: select entry from listtotable('abc,def,ghi') order by entry desc
PURPOSE: Takes a comma-delimited list as a parameter and returns the values of that list into a table variable.
*/
#mylist varchar(8000)
)
RETURNS #ListTable TABLE (
seqid int not null,
entry varchar(255) not null)
AS
BEGIN
DECLARE
#this varchar(255),
#rest varchar(8000),
#pos int,
#seqid int
SET #this = ' '
SET #seqid = 1
SET #rest = #mylist
SET #pos = PATINDEX('%,%', #rest)
WHILE (#pos > 0)
BEGIN
set #this=substring(#rest,1,#pos-1)
set #rest=substring(#rest,#pos+1,len(#rest)-#pos)
INSERT INTO #ListTable (seqid,entry) VALUES (#seqid,#this)
SET #pos= PATINDEX('%,%', #rest)
SET #seqid=#seqid+1
END
set #this=#rest
INSERT INTO #ListTable (seqid,entry) VALUES (#seqid,#this)
RETURN
END
Run that script in your SQL Server database to create the function ListToTable. Now, you can rewrite your query like so:
#attributetypeid bigint,
#productid bigint,
#includedids varchar(MAX)
DELETE FROM reltable
WHERE productid = #productid AND
attributetypeid = #attributetypeid AND
attributeid NOT IN (SELECT entry FROM ListToTable(#includedids));
Where #includedids is a comma delimited list that you provide. I use this function all the time when working with lists. Keep in mind this function does not necessarily sanitize your inputs, it just looks for character data in a comma delimited list and puts each element into a record. Hope this helps.
Joel Spolsky answered a very similar question here: Parameterize an SQL IN clause
You could try something similar, making sure to cast your attributetypeid as a varchar.
You can't pass a list as an parameter (AFAIK).
Can you rewrite the sql to use a subquery, something like this:
delete from reltable
WHERE productid = #productid AND
attributetypeid = #attributetypeid AND
attributeid NOT IN (select id from ... where ... );
?
That comma delimited list can be sent to a user defined function which will return it as a simple table. That table can then be queried by your NOT IN.
If you need the fn I can provide.. It's been about 5 yrs since I used sql much and I'll have to dust off that section of my brain..
Erland has the definitive guide for dealing with lists to table in SQL 2005, SQL 2008 gives you table based params.
On a side note I would avoid a NOT IN pattern for large lists, cause it does not scale, instead look at using left joins.