How to not insert xmlns in SQL Server? - sql

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>

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;

Why doesn't SQL Server's xml.modify with a JOIN update all the elements specified?

I've googled and read a bunch of this, but I still don't understand why this code doesn't work. I know .modify() is a "singleton", but it doesn't explain to me why it's not executing once per row in the returned set. This makes zero sense to me, and I'm just hoping to come to some understanding. Honestly, this feels like a bug, not expected behavior.
Basically, I'm trying to update xml based on a table of inputs that I cannot predict ahead of time (it will be different for every client). I want to update by just joining to a table that describes the updates, and having it work. But it doesn't.
I've boiled it down to this example. In this example, I expect the code to update each node specified with the vegetable="false" attribute. Note that I use the "sql:Column()" syntax to navigate to the correct node to update. But it only updates the first one:
SET NOCOUNT ON;
DECLARE #xml xml = '<top>
<element name="apple">red</element>
<element name="pear">green</element>
<element name="banana">yellow</element>
</top>';
DECLARE #xmlData TABLE (ID int, Contents xml);
DECLARE #elements TABLE (ID int, Element varchar(50));
INSERT INTO #xmlData (ID, Contents) VALUES (1, #xml);
INSERT INTO #elements(ID, Element)
VALUES (1, 'apple'), (2, 'pear'), (3, 'banana');
SET NOCOUNT OFF;
SELECT *
FROM #xmlData CROSS JOIN #elements;
UPDATE x
SET Contents.modify('insert attribute vegetable {"false"} into (/top/element[#name=sql:column("y.Element")])[1]')
FROM #xmlData x
CROSS JOIN #elements y
SELECT * FROM #xmlData;
I put the select in to show that it returns three rows, so I expect three updates. But the SET NOCOUNT OFF shows that only one row is updated. THIS MAKES NO SENSE.
The only way around this I've found it to put a WHILE loop in, but that seems ridiculous because the intent is already perfectly and explicitly stated in the code above... it just doesn't work.
This code works, but I have no clue why I have to go through this kind of hack and performance killer just to do something painfully obvious:
SET NOCOUNT ON;
DECLARE #xml xml = '<top>
<element name="apple">red</element>
<element name="pear">green</element>
<element name="banana">yellow</element>
</top>';
DECLARE #xmlData TABLE (ID int, Contents xml);
DECLARE #elements TABLE (ID int, Element varchar(50));
INSERT INTO #xmlData (ID, Contents) VALUES (1, #xml);
INSERT INTO #elements(ID, Element)
VALUES (1, 'apple'), (2, 'pear'), (3, 'banana');
SET NOCOUNT OFF;
SELECT *
FROM #xmlData CROSS JOIN #elements;
DECLARE #maxElement int = (SELECT MAX(ID) FROM #elements);
DECLARE #currentElement int = 1;
WHILE (#currentElement <= #maxElement)
BEGIN
UPDATE x
SET Contents.modify('insert attribute vegetable {"false"} into (/top/element[#name=sql:column("y.Element")])[1]')
FROM #xmlData x
CROSS JOIN #elements y
WHERE y.ID = #currentElement
SET #currentElement += 1;
END
SELECT * FROM #xmlData;
Can someone explain to me why the modify isn't executed once per row in the first block? And is the solution I have in the second block the best solution here? It really feels like a gross hack to me... but I do not know the element names or the count of element names ahead of time, so I can't just do what most articles recommend which is just writing multiple UPDATE statements. UPDATE with a JOIN should do the right thing, I thought. And I'm stumped about why it doesn't.
This is a way to do it by using XQuery FLWOR expression. It does it through XML reconstruction. Also, I added some protection if the #vegetable attribute already exists. XQuery contains() function checks if the element is on the update list. I added a new fig element to test it.
SQL
-- DDL and sample data population, start
DECLARE #xmlData TABLE (ID INT IDENTITY PRIMARY KEY, Contents XML);
INSERT INTO #xmlData (Contents)
VALUES ('<top>
<element name="apple" vegetable="true">red</element>
<element name="pear">green</element>
<element name="fig">reddish</element>
<element name="banana">yellow</element>
</top>');
DECLARE #elements TABLE (ID INT IDENTITY PRIMARY KEY, Element VARCHAR(50));
INSERT INTO #elements(Element)
VALUES ('apple'), ('pear'), ('banana');
-- DDL and sample data population, end
-- before
SELECT * FROM #xmlData;
DECLARE #element_list AS NVARCHAR(MAX) = '';
SELECT #element_list += ',[' + el.Element + ']'
FROM #elements AS el;
-- just to see
SELECT #element_list AS [element_list];
DECLARE #vegetable VARCHAR(10) = 'false';
UPDATE #xmlData
SET Contents = Contents.query('<top>
{
for $x in /top/element
return if(empty($x/#vegetable)
and contains(sql:variable("#element_list"), concat("[",$x/#name,"]"))) then (
<element name="{($x/#name)}">{data($x)}
{attribute vegetable {sql:variable("#vegetable")}}
</element>)
else ($x)
}
</top>');
-- after
SELECT * FROM #xmlData;
Unfortunately, even the latest SQL Server 2019 still doesn't support XQuery 3.0 or 3.1 based on the w3c standards.
Useful links:
(1) XQuery Update
(2) SQL Server vNext (post 2019) and NoSQL functionality

Parsing non-standard XML in SQL Server with XTbl

I have a serialised piece of data in a column that I want to retrieve a single value from, in the following form:
<FirstNode>Something</><SecondNode>Something Else</>
I want to retrieve, for example, 'Something' from this in SQL Server. I've tried the following:
declare #data xml;
set #data = cast([my data source] as xml);
select #data.value('(/UserName)[1]','varchar(50)')
I'm probably way off with this, I don't have a huge deal of experience with parsing XML. Any help would be great.
Edit: I get the error
XML parsing: line 1, character 20, illegal qualified name character
Just use the CHARINDEX and SUBSTRING functions to get the data you want. Rolling my example into a function would probably be your best bet.
DECLARE #tbl TABLE(data VARCHAR(MAX))
INSERT INTO #tbl VALUES
('<FirstNode>Something</><SecondNode>Something Else</>'),
('<SecondNode>Something Else</><FirstNode>More Something</>'),
('<BadNoe>Something</><SecondNode>Something Else</>')
DECLARE #fnd VARCHAR(64)
DECLARE #end VARCHAR(64)
SET #fnd = '<FirstNode>'
SET #end = '</>'
SELECT SUBSTRING(a.[data], a.[start] + LEN(#fnd), a.[end] - (a.[start] + LEN(#fnd)))
FROM (SELECT data [data], CHARINDEX(#fnd, data, 0) [start], CHARINDEX(#end, data, CHARINDEX(#fnd, data, 0)) [end] FROM #tbl) a
WHERE a.[start] > 0

how to get values inside an xml column, when it's of type nvarchar

My question is similar to this one: Choose a XML node in SQL Server based on max value of a child element
except that my column is NOT of type XML, it's of type nvarchar(max).
I want to extract the XML node values from a column that looks like this:
<Data>
<el1>1234</el1>
<el2>Something</el2>
</Data>
How can I extract the values '1234' and 'Something' ?
doing a convert and using the col.nodes is not working.
CONVERT(XML, table1.col1).value('(/Data/el1)[1]','int') as 'xcol1',
After that, I would like to do a compare value of el1 (1234) with another column, and update update el1 as is. Right now I'm trying to just rebuild the XML when passing the update:
ie
Update table set col1 ='<Data><el1>'+#col2+'</el1><el2>???</el2>
You've got to tell SQL Server the number of the node you're after, like:
(/Data/el1)[1]
^^^
Full example:
declare #t table (id int, col1 varchar(max))
insert #t values (1, '<Data><el1>1234</el1><el2>Something</el2></Data>')
select CAST(col1 as xml).value('(/Data/el1)[1]', 'int')
from #t
-->
1234
SQL Server provides a modify function to change XML columns. But I think you can only use it on columns with the xml type. Here's an example:
declare #q table (id int, col1 xml)
insert #q values (1, '<Data><el1>1234</el1><el2>Something</el2></Data>')
update #q
set col1.modify('replace value of (/Data/el1/text())[1] with "5678"')
select *
from #q
-->
<Data><el1>5678</el1><el2>Something</el2></Data>
At the end of the day, SQL Server's XML support makes simple things very hard. If you value maintainability, you're better off processing XML on the client side.

SQL server insert data from table into an XML variable

How can I insert a whole bunch of rows into an XML variable without using a cursor?
I know I can do
SET #errors.modify('insert <error>{ sql:variable("#text") }</error> as last into /errors[1]')
to insert the value of a variable, but I want to basically do
SET #errors.modify(SELECT 'insert <error>{ sql:column("text") }</error>' FROM table)
which, of course, isn't legal syntax.
Edit: Obviously my question wasn't clear. What I want is to be able to do like this:
CREATE TABLE my_table(text nvarchar(50))
INSERT INTO my_table VALUES('Message 2')
INSERT INTO my_table VALUES('Message 3')
DECLARE #errors xml
SET #errors = '<errors><error>Message 1</error></errors>'
SET #errors.modify('INSERT EVERYTHING FROM my_table MAGIC STATEMENT')
And after running this code, #errors should contain
<errors>
<error>Message 1</error>
<error>Message 2</error>
<error>Message 3</error>
</errors>
Isn't this simpler?
set ErrorXML=(SELECT * from #MyTable FOR XML AUTO)
LAST UPDATE:
OK, now that the question is much clearer, he's the solution - hopefully!!
DECLARE #errors xml
SET #errors = '<errors><error>Message 1</error></errors>'
DECLARE #newErrors XML
SELECT #newErrors = (SELECT text AS 'error'
FROM dbo.my_table
FOR XML PATH(''), ELEMENTS)
SELECT #errors, #newErrors
SET #errors.modify('insert sql:variable("#newErrors") as last into (/errors)[1]')
SELECT #errors
This gives me
#errors at the beginning
<errors><error>Message 1</error></errors>
#newError after the "magic" SELECT:
<error>Message 2</error><error>Message 3</error>
#errors after the UPDATE:
<errors>
<error>Message 1</error>
<error>Message 2</error>
<error>Message 3</error>
</errors>
Is THAT what you're looking for?? :-)
(old answers - not what the OP was looking for.....)
You need to look at the .nodes() function in SQL XQuery - this will break up an XML
variable into a list of XML nodes, based on an XPath expression (that references some point in your XML where you are likely to have an enumeration of nodes of the same structure), and it gives them a "virtual" table and column name.
Based on that "Table.Column" element, you can select single values from that XML node - either attributes or sub-elements - and you get these back as "atomic" values, e.g. as INT, VARCHAR(x), whatever you need. These values can be inserted into the table:
INSERT dbo.YourTable(col1, col2, col3, ..., colN)
SELECT
Error.Column.value('#attr1[1]', 'varchar(20)'),
Error.Column.value('subitem[1]', 'int'),
.....
Error.Column.value('subitemN[1]', 'DateTime')
FROM
#xmldata.nodes('/error') AS Error(Column)
UPDATE: ok, so you want to do the opposite - turn relational data into XML - that's even easier :-)
DECLARE #NewXmlContent XML
SELECT #NewXmlContent =
(SELECT
col1 as '#ID',
col2 as 'SomeElement',
.....
colN as 'LastElement'
FROM
dbo.YourTable
WHERE
....
FOR XML PATH('element'), ROOT('root')
)
UPDATE YourOtherTable
SET XmlField.modify('insert sql:variable("#NewXmlContent")
as last into (/XPath)[1]')
WHERE (some condition)
This will give you something like this in #NewXmlContent:
<root>
<element ID="(value of col1)">
<SomeElement>(value of col2)</SomeElement>
.....
<LastElement>(value of colN)</LastElement>
</element>
</root>
and the UPDATE statement with the .modify() call will actually insert that content into an existing XML field in your database. This is the only way to get XML contents into an existing XML column - there's no way of directly referencing another XML column inside a XML fragment being inserted....
The new "FOR XML PATH" syntax is very powerful and flexible and allows you to do just about anything.
And of course, you can easily store that into a XML variable.
Marc
Based on marc's answer, here is a solution that works for SQL Server 2005:
CREATE TABLE #my_table(text nvarchar(50))
INSERT INTO #my_table VALUES('Message 2')
INSERT INTO #my_table VALUES('Message 3')
DECLARE #errors xml
SET #errors = '<errors><error>Message 1</error></errors>'
SELECT #errors = CAST(#errors AS nvarchar(max)) + '<new>' + (SELECT text AS 'error' FROM #my_table FOR XML PATH(''), ELEMENTS) + '</new>'
SET #errors = CAST(#errors AS nvarchar(max)) + '<new>' + #newErrors + '</new>'
SET #errors.modify('insert (/new/*) as last into (/errors)[1]')
SET #errors.modify('delete (/new)')
SELECT #errors
DROP TABLE #my_table
Will return
<errors>
<error>Message 1</error>
<error>Message 2</error>
<error>Message 3</error>
</errors>