Update xml column in a table - sql-server

I need to remove a Xml element which has the attribute of A with the value of xxxx from the Xml values in a column.
Method 1:
update t set x = x.query('//E[#A != "xxxx"]')
Method 2:
update t set x.modify('delete /E[#A = "xxxx"]')
Which one is better?

Both calls would not do the same:
DECLARE #xml XML=
N'<root>
<test pos="1" a="xxx">test 1</test>
<test pos="2" a="SomeOther">test 2</test>
<test pos="3" a="xxx">test 3</test>
<OtherElement>This is another element</OtherElement>
</root>';
--Try this with either this approach
SET #xml=#xml.query(N'//test[#a!="xxx"]')
--Or try it with this
SET #xml.modify(N'delete //test[#a="xxx"]')
SELECT #xml;
The result of the first is
<test pos="2" a="SomeOther">test 2</test>
While the second returns
<root>
<test pos="2" a="SomeOther">test 2</test>
<OtherElement>This is another element</OtherElement>
</root>
XML is not stored as the text you see. It is stored as a tree structure representing a complex document. To modify this is fairly easy, just kick out some elements. The query()approach has to rebuild the XML and replace the first with an new one. So my clear advise is: Use the modify()approach! If you are really good with XQuery and FLWOR the query() approach is much mightier, but this is another story...

Related

Is that valid XML and how to replicate with SQL Server

I do have to replicate an XML file with SQL Server and I am now stumbling over the following structure inside the XML file and I don't know how to replicate that.
The structure looks like this at the moment for certain tags:
<ART_TAG1>
<UNMLIMITED/>
</ART_TAG1>
<ART_TAG2>
<ART_TAG3>
<Data_Entry/>
</ART_TAG3>
</ART_TAG2>
I am wondering if this is proper XML that the data inside (unlimited and Data_Entry) is enclosed with a closing XML tag. The XML validator https://www.w3schools.com/xml/xml_validator.asp is telling me this is correct. But now I am struggling with replicating that with Transact-SQL.
If I try to replicate that I can only come up with the following TSQL script, which obviously does not fully look like the original.
SELECT 'UNLIMITED' as 'ART_TAG1'
, 'Data_Entry' as 'ART_TAG2/ART_TAG3'
FOR XML PATH(''), ROOT('root')
<root>
<ART_TAG1>UNLIMITED</ART_TAG1>
<ART_TAG2>
<ART_TAG3>Data_Entry</ART_TAG3>
</ART_TAG2>
</root>
If I get this correctly, your question is:
How can I put my query to create those <SomeElement /> tags?
Look at this:
--This will create filled nodes
SELECT 'outer' AS [OuterNode/#attr]
,'inner' AS [OuterNode/InnerNode]
FOR XML PATH('row');
--The empty string is some kind of content
SELECT 'outer' AS [OuterNode/#attr]
,'' AS [OuterNode/InnerNode]
FOR XML PATH('row');
--the missing value (NULL) is omited by default
SELECT 'outer' AS [OuterNode/#attr]
,NULL AS [OuterNode/InnerNode]
FOR XML PATH('row');
--Now check what happens here:
--First XML has an empty element, while the second uses the self-closing element
DECLARE #xml1 XML=
N'<row>
<OuterNode attr="outer">
<InnerNode></InnerNode>
</OuterNode>
</row>';
DECLARE #xml2 XML=
N'<row>
<OuterNode attr="outer">
<InnerNode/>
</OuterNode>
</row>';
SELECT #xml1,#xml2;
The result is the same for both...
Some background: Semantically the empty element <element></element> is exactly the same as the self-closing element <element />. It should not make any difference, whether you use the one or the other. If your consumer cannot deal with this, it is a problem in the reading part.
Yes, you can force any content into XML on string level, but - as the example shows above - this is just a (dangerous) hack.
XML within T-SQL returns - by default - a missing node as NULL and an empty element as empty (depending on the datatype, and beware of the difference between an element and its text() node).
In short: This is nothing you should have to think about...

How to delete particular value from XML node instead of full node in SQL table

I have an xml field in my table : XmlDoc which has various tags.. but I need to delete a particular value from a tag rather than deleting whole tag itself.
I have already tried using
update table
set XmlDoc.modify('delete //DeliveryMechanism//Address//text()[contains("abc#gmail.com")]')
but it deletes all the value from the tag but I need to remove only abc#gmail.com"
<DeliveryMechanism>
<ID>1</ID>
<Name>Email</Name>
<Description />
<IsActive>false</IsActive>
<Address>def#gmail.com,abc#gmail.com,hij#gmail.com</Address>
<DeliveryOptions xmlns:p3="http://www.w3.org/2001/XMLSchema-instance" p3:type="DeliveryOptionsEmail">
<MailPriority>Normal</MailPriority>
</DeliveryOptions>
</DeliveryMechanism>
I need tag to be like
<Address>def#gmail.com,hij#gmail.com</Address>
This is not as simple as you might think...
The problem is: You should never ever store your data as a delimited string. Instead of the comma-separated list of email addresses, you should rather use a list of nested nodes. But sometimes we have to stick with bad design. This is a way to solve this:
A mockup-scenario with three test cases:
DECLARE #mockTable TABLE(ID INT IDENTITY, Descr VARCHAR(100), yourXml XML);
INSERT INTO #mockTable VALUES
('Your sample'
,'<DeliveryMechanism>
<ID>1</ID>
<Name>Email</Name>
<Description />
<IsActive>false</IsActive>
<Address>def#gmail.com,abc#gmail.com,hij#gmail.com</Address>
<DeliveryOptions xmlns:p3="http://www.w3.org/2001/XMLSchema-instance" p3:type="DeliveryOptionsEmail">
<MailPriority>Normal</MailPriority>
</DeliveryOptions>
</DeliveryMechanism>')
,('Your sample without the given mail address'
,'<DeliveryMechanism>
<ID>1</ID>
<Name>Email</Name>
<Description />
<IsActive>false</IsActive>
<Address>def#gmail.com,hij#gmail.com;oneMore#test.com</Address>
<DeliveryOptions xmlns:p3="http://www.w3.org/2001/XMLSchema-instance" p3:type="DeliveryOptionsEmail">
<MailPriority>Normal</MailPriority>
</DeliveryOptions>
</DeliveryMechanism>')
,('Your sample with twice the given mail address'
,'<DeliveryMechanism>
<ID>1</ID>
<Name>Email</Name>
<Description />
<IsActive>false</IsActive>
<Address>abc#gmail.com,hij#gmail.com,abc#gmail.com</Address>
<DeliveryOptions xmlns:p3="http://www.w3.org/2001/XMLSchema-instance" p3:type="DeliveryOptionsEmail">
<MailPriority>Normal</MailPriority>
</DeliveryOptions>
</DeliveryMechanism>');
--a variable to set the email we want to find and delete
DECLARE #givenAddress VARCHAR(100)='abc#gmail.com';
--the query
WITH PreComputation AS
(
SELECT t.*
,STUFF(
CAST('<x>' + REPLACE(t.yourXml.value('(/DeliveryMechanism/Address/text())[1]','nvarchar(max)'),',','</x><x>') + '</x>' AS XML)
.query('for $a in /x[text()[1] != sql:variable("#givenAddress")]/text()
return
<x>{concat(",",$a)}</x>').value('.','nvarchar(max)'),1,1,'') theNewList
FROM #mockTable t
WHERE t.yourXml.exist('/DeliveryMechanism[Address[contains(text()[1],sql:variable("#givenAddress"))]]')=1
)
UPDATE PreComputation SET yourXml.modify('replace value of (/DeliveryMechanism/Address/text())[1] with sql:column("theNewList")');
--Check the result
SELECT * FROM #mockTable;
The idea in short:
The CTE PreComputation will first use .exist() to reduce the work onto rows which contain the sepcific address.
The next step is to use some string manipulations, to transform your CSV-list to XML and cast it to a native XML.
Once having a native XML we can use XQuery
The .query() will return all email fragments, where the text() is not equal to the given address.
Some tricks with concat() ,.value() and STUFF() later, we now have the newly created CSV-list without the given address.
The final UPDATE can use this with sql:column().
Good luck ;-)

Supply xml element value in modify() method

I know how to replace element value for the xml element in the modify() method. Here's the example
TSQL Replace value in XML String
My problem is a bit different. Taking example from above link...
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/educational/text())[1]
with sql:variable("#updatedEducation")')
WHERE feedbackID = #FBID
What I want to do is provide value for 'educational'. In other words I want to do something like this
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/sql:variable("#name")/text())[1]
with sql:variable("#value")')
WHERE feedbackID = #FBID
I'm getting the following error because of sql:variable("#name")
The XQuery syntax '/function()' is not supported.
How can I pass both the name of the element to be updated and its value to my
stored procedure and have it update the XML column?
You are not allowed to use variables as part of the XPath, but you can use a predicate:
DECLARE #xml XML=
N'<root>
<optional>
<educational>SomeText</educational>
<someOther>blah</someOther>
</optional>
</root>';
--The straight approach as you know it:
SET #xml.modify('replace value of (/root/optional/educational/text())[1] with "yeah!"');
SELECT #xml;
--Now we use a variable to find the first node below <optional>, which name is as given:
DECLARE #ElementName VARCHAR(100)='educational';
SET #xml.modify('replace value of (/root/optional/*[local-name()=sql:variable("#ElementName")]/text())[1] with "yeah again!"');
SELECT #xml;
Try it out...

How to store XML data list in a table

Im working on a script that will allow me to save the xml data in a table. This will be used to compare with other tables which also contains an xml data.
I've been successful so far for simple node tags, but have encountered an issue when trying to store the data from a list.
The XML data looks like this:Sample XML Data
And my query is this:
XML Query display
I am able to get the 'TypeCode' as the main node but for the value, it's always showing blank. I'm not sure how to handle the list in XML.
I'm thinking as long as I can save the data
''
'' as text in the Value column, then I can find another way to parse it and display it in a nicer way as another query.
Any help is appreciated :D Thanks!
For your next question: Please do not post pictures. I had to type this in... Please provide consumable data, best as a stand-alone example to reproduce your issue.
DECLARE #xml XML=
'<Codes>
<TypeCodes type="list">
<item key="A" text="A1"/>
<item key="C" text="C1"/>
</TypeCodes>
</Codes>';
--the Xml "as is"
SELECT #xml;
--fetch one value with XQuery
SELECT #xml.value('(/Codes/TypeCodes/item[#key="A"]/#text)[1]','varchar(10)');
--fetch all items as list
SELECT #xml.value('(/Codes/TypeCodes/#type)[1]','varchar(10)') AS TypeCode_type
,i.value('#key','varchar(10)') AS item_key
,i.value('#text','varchar(10)') AS item_text
FROM #xml.nodes('/Codes/TypeCodes/item') A(i);
check it out
Good day,
SQL Server include special data type XML which is what you should use in order to store your XML data.
declare #MyXML XML = '
<codes>
<Type>
<Item key="1" />
</Type>
</codes>
'
select #MyXML
Here is example of using table:
DROP TABLE IF eXISTS T;
CREATE TABLE T(MyXML XML)
GO
INSERT T(MyXML) values ('
<codes>
<Type>
<Item key="1" />
</Type>
</codes>
')
SELECT * FROM T
GO
For more information check this documentation:
XML Data Type and Columns (SQL Server)

Using SQL Server 2005's XQuery select all nodes with a specific attribute value, or with that attribute missing

Update: giving a much more thorough example.
The first two solutions offered were right along the lines of what I was trying to say not to do. I can't know location, it needs to be able to look at the whole document tree. So a solution along these lines, with /Books/ specified as the context will not work:
SELECT x.query('.') FROM #xml.nodes('/Books/*[not(#ID) or #ID = 5]') x1(x)
Original question with better example:
Using SQL Server 2005's XQuery implementation I need to select all nodes in an XML document, just once each and keeping their original structure, but only if they are missing a particular attribute, or that attribute has a specific value (passed in by parameter). The query also has to work on the whole XML document (descendant-or-self axis) rather than selecting at a predefined depth.
That is to say, each individual node will appear in the resultant document only if it and every one of its ancestors are missing the attribute, or have the attribute with a single specific value.
For example:
If this were the XML:
DECLARE #Xml XML
SET #Xml =
N'
<Library>
<Novels>
<Novel category="1">Novel1</Novel>
<Novel category="2">Novel2</Novel>
<Novel>Novel3</Novel>
<Novel category="4">Novel4</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
<Volume category="2">G-L</Volume>
<Volume category="3">M-S</Volume>
<Volume category="4">T-Z</Volume>
</Encyclopedia>
</Encyclopedias>
<Dictionaries category="1">
<Dictionary>Webster</Dictionary>
<Dictionary>Oxford</Dictionary>
</Dictionaries>
</Library>
'
A parameter of 1 for category would result in this:
<Library>
<Novels>
<Novel category="1">Novel1</Novel>
<Novel>Novel3</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
</Encyclopedia>
</Encyclopedias>
<Dictionaries category="1">
<Dictionary>Webster</Dictionary>
<Dictionary>Oxford</Dictionary>
</Dictionaries>
</Library>
A parameter of 2 for category would result in this:
<Library>
<Novels>
<Novel category="2">Novel2</Novel>
<Novel>Novel3</Novel>
</Novels>
<Encyclopedias>
<Encyclopedia>
<Volume>A-F</Volume>
<Volume category="2">G-L</Volume>
</Encyclopedia>
</Encyclopedias>
</Library>
I know XSLT is perfectly suited for this job, but it's not an option. We have to accomplish this entirely in SQL Server 2005. Any implementations not using XQuery are fine too, as long as it can be done entirely in T-SQL.
It's not clear for me from your example what you're actually trying to achieve. Do you want to return a new XML with all the nodes stripped out except those that fulfill the condition? If yes, then this looks like the job for an XSLT transform which I don't think it's built-in in MSSQL 2005 (can be added as a UDF: http://www.topxml.com/rbnews/SQLXML/re-23872_Performing-XSLT-Transforms-on-XML-Data-Stored-in-SQL-Server-2005.aspx).
If you just need to return the list of nodes then you can use this expression:
//Book[not(#ID) or #ID = 5]
but I get the impression that it's not what you need. It would help if you can provide a clearer example.
Edit: This example is indeed more clear. The best that I could find is this:
SET #Xml.modify('delete(//*[#category!=1])')
SELECT #Xml
The idea is to delete from the XML all the nodes that you don't need, so you remain with the original structure and the needed nodes. I tested with your two examples and it produced the wanted result.
However modify has some restrictions - it seems you can't use it in a select statement, it has to modify data in place. If you need to return such data with a select you could use a temporary table in which to copy the original data and then update that table. Something like this:
INSERT INTO #temp VALUES(#Xml)
UPDATE #temp SET data.modify('delete(//*[#category!=2])')
Hope that helps.
The question is not really clear, but is this what you're looking for?
DECLARE #Xml AS XML
SET #Xml =
N'
<Books>
<Book ID="1">Book1</Book>
<Book ID="2">Book2</Book>
<Book ID="3">Book3</Book>
<Book>Book4</Book>
<Book ID="5">Book5</Book>
<Book ID="6">Book6</Book>
<Book>Book7</Book>
<Book ID="8">Book8</Book>
</Books>
'
DECLARE #BookID AS INT
SET #BookID = 5
DECLARE #Result AS XML
SET #result = (SELECT #xml.query('//Book[not(#ID) or #ID = sql:variable("#BookID")]'))
SELECT #result

Resources