T-SQL XML get a value from a node problem? - sql

I have an XML like:
<?xml version="1.0" encoding="utf-16"?>
<ExportProjectDetailsMessage xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Project">
<CPProjectId>7665699f-6772-424c-8b7b-405b9220a8e7</CPProjectId>
</ExportProjectDetailsMessage>
I'm trying to get the CPProjectId as a Uniqueidentifier using:
DECLARE #myDoc xml
DECLARE #ProdID varchar(max)
SET #myDoc = '<ExportProjectDetailsMessage xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Project"><CPProjectId>7665699f-6772-424c-8b7b-405b9220a8e7</CPProjectId></ExportProjectDetailsMessage>'
SET #ProdID = #myDoc.value('(ExportProjectDetailsMessage/CPProjectId)[1]', 'varchar(max)' )
SELECT #ProdID
All i can receive is NULL =/
I've tried many combinations on #myDoc.value but no results =/
How can i retrieve the value from my XML ?
Thanks!
--EDIT:
Something that i noted, when i remove the namespace declaration from the XML it works fine!
The problem is that i need this namespaces! =/

You're right the namespace is the issue. You're query is looking for a node ExportProjectDetailsMessage but such a node doesn't exist in your document, because there is a namespace declared as a default in your document. Since you can't remove that (nor should you) you should include it in your XPATH query like so:
set #ProdId = #myDoc.value('
declare namespace PD="http://schemas.datacontract.org/2004/07/Project";
(PD:ExportProjectDetailsMessage/PD:CPProjectId)[1]', 'varchar(max)' )
You may also want to consider not using varchar(max) but perhaps uniqueidentifier

A better way to do this is to simply declare the namespace before each of your queries:
;WITH XMLNAMESPACES(DEFAULT 'http://schemas.datacontract.org/2004/07/Project')
It's like a temporary default. When you run the next query in the batch you'll get nulls again if you don't specify this before each of your selects.
So instead of using "SET", you can use "SELECT" to set the value like so:
;WITH XMLNAMESPACES(DEFAULT 'http://schemas.datacontract.org/2004/07/Project')
SELECT #ProdID = #myDoc.value('(ExportProjectDetailsMessage/CPProjectId)[1]', 'VarChar(MAX)')
SELECT #ProdID
Same results, just more readable and maintainable.
I found the solution here: http://www.sqlservercentral.com/Forums/Topic967100-145-1.aspx#bm967325

Related

SQL Replace Typed XML Data

I'm working with third-party software that stores an XML document of parameters as a column. I'm trying to write a SQL-Server script that will replace the email address in the XML below.
<ArrayOfKeyValueOfstringanyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<KeyValueOfstringanyType>
<Key>Email</Key>
<Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string">Michael#dundermifflin.com</Value>
</KeyValueOfstringanyType>
</ArrayOfKeyValueOfstringanyType>
So far, the closest I've gotten is this... It runs and says rows were affected but does nothing.
update t
set XMLColumn.modify('replace value of (/ArrayOfKeyValueOfstringanyType/KeyValueOfstringanyType/Key/Value/string())[1] with "dwight#staples.com"')
After reviewing other posts and Microsoft's documentation (https://learn.microsoft.com/en-us/sql/t-sql/xml/replace-value-of-xml-dml?view=sql-server-ver15#a-replacing-values-in-an-xml-instance --Item D), it seems I'm missing something regarding the namespaces. If I understand the XML correctly, it appears that there are multiple namespaces to declare. After several attempts with no luck, my lack of XML experience has me turning here.
Any help is greatly appreciated!
Please try the following solution.
As you correctly guessed, the culprit was a default namespace.
Also, I had to adjust the XPath expression.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, XMLColumn XML);
INSERT INTO #tbl (XMLColumn) VALUES
(N'<ArrayOfKeyValueOfstringanyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<KeyValueOfstringanyType>
<Key>Email</Key>
<Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema"
i:type="d3p1:string">Michael#dundermifflin.com</Value>
</KeyValueOfstringanyType>
</ArrayOfKeyValueOfstringanyType>');
-- DDL and sample data population, end
-- before
SELECT * FROM #tbl;
;WITH XMLNAMESPACES(DEFAULT 'http://schemas.microsoft.com/2003/10/Serialization/Arrays')
UPDATE #tbl
SET XMLColumn.modify('replace value of (/ArrayOfKeyValueOfstringanyType/KeyValueOfstringanyType/Value/text())[1] with "dwight#staples.com"');
-- after
SELECT * FROM #tbl;
You muse declare default namespace
DECLARE #XML XML = N'<ArrayOfKeyValueOfstringanyType xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<KeyValueOfstringanyType>
<Key>Email</Key>
<Value xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string">Michael#dundermifflin.com</Value>
</KeyValueOfstringanyType>
</ArrayOfKeyValueOfstringanyType> '
set #XML.modify('
declare default element namespace "http://schemas.microsoft.com/2003/10/Serialization/Arrays";
replace value of (/ArrayOfKeyValueOfstringanyType/KeyValueOfstringanyType/Value/text())[1] with "dwight#staples.com"')
SELECT #XML

Is there a way to query XML data without explicitly using field names?

I am wondering if I can generalize this query so the user does not need to know the names of the fields?
My current query looks something like this:
USE [VideoGameProject]
DECLARE #myXML = "'<review><rating>97</rating><gameid>2</gameid><reviewdate>11/03/2017</reviewdate><reviewtitle>Super Mario Odyssey : A Masterpiece</reviewtitle><headline>Mario embarks on his greatest adventure yet!</headline></review>';
SELECT
value.value('(/review//rating/node()[1]', 'int') as Rating,
value.value('(/review//gameid/node()[1]', 'int') as GameId,
value.value('(/review//reviewdate/node()[1]', 'nvarchar(max)') as DateOfReview,
value.value('(/review//reviewtitle/node()[1]', 'nvarchar(max)') as TitleOfReview,
value.value('(/review//headline/node()[1]', 'nvarchar(max)') as Headline
FROM GameData;
I was hoping to do a generic query, something where I don't need to explicitly write all of the field names, maybe a SELECT * equivalent?
I see the above query to be a little error prone. I understand the #myXML variable is not necessary, but wanted to give a reference.
If anyone can help, that would be great.
Yea, you can use the ordinal location of the nodes (elements and attributes).
You'd need to adapt this for your need, but it would look something like this...
DECLARE #myXML AS xml
SET #myXML = '
<review>
<rating>97</rating>
<gameid>2</gameid>
<reviewdate>11/03/2017</reviewdate>
<reviewtitle>Super Mario Odyssey : A Masterpiece</reviewtitle>
<headline>Mario embarks on his greatest adventure yet!</headline>
</review>
';
SELECT #myXML.query('/node()[1]/node()[1]/node()[1]') AS [Rating]
SELECT #myXML.query('/node()[1]/node()[2]/node()[1]') AS [GameId]
SELECT #myXML.query('/node()[1]/node()[3]/node()[1]') AS DateOfReview
SELECT #myXML.query('/node()[1]/node()[4]/node()[1]') AS TitleOfReview
SELECT #myXML.query('/node()[1]/node()[5]/node()[1]') AS Headline
And, if your XML isn't in a variable, you can query it directly from a table.
One table for demonstration purposes:
CREATE TABLE #foo (id int identity(1,1), things xml)
DECLARE #myXML AS xml
SET #myXML = '
<review>
<rating>97</rating>
<gameid>2</gameid>
<reviewdate>11/03/2017</reviewdate>
<reviewtitle>Super Mario Odyssey : A Masterpiece</reviewtitle>
<headline>Mario embarks on his greatest adventure yet!</headline>
</review>
';
INSERT INTO #foo (things) VALUES (#myXML);
Now, we've got a table with one record of XML data. Here is one way to select it out...
SELECT things.value('/node()[1]/node()[1]/node()[1]', 'varchar(max)') AS [Rating]
FROM #foo
Here is another...
SELECT things.query('/node()[1]/node()[1]/node()[1]') AS [Rating]
FROM #foo

SQL - Blank default namespaces

I have a relation which has an XML column storing data in the following structure
<Report id="b5d9b8da-7af4-4257-b825-b28af91dd833">
<CreatedDate>04-12-2012</CreatedDate>
<LastUpdated>04-12-2012</LastUpdated>
<Reference>abc123</Reference>
</Report>
I'm writing a stored procedure to retrieve all reports and join them and wrap them in a root node called reports. I have the following so far;
WITH XMLNAMESPACES(DEFAULT 'http://www.defaultnamespace.com/1.0')
SELECT
#Xml =
(
SELECT
(
SELECT xml.query('.')
FROM
[database].[Reports]
WHERE
ClientId = #clientId
FOR XML PATH(''),
TYPE
)
FOR XML PATH('Reports'),
TYPE
)
Whilst this returns all the reports in the right format, there exists a blank default namespace on the report element like the following;
<Reports xmlns="http://www.defaultnamespace.com/1.0">
<Report xmlns="" id="b5d9b8da-7af4-4257-b825-b28af91dd833">
<CreatedDate>04-12-2012</CreatedDate>
<LastUpdated>04-12-2012</LastUpdated>
<Reference>abc123</Reference>
</Report>
</Reports>
Could someone explain a suitable way of excluding the namespace on the report element?
Any help is greatly appreciated guys :)
It's a little messy and probably not very efficient but you can redefine namespaces with an XQuery over your intermediate XML.
Instead of using SQL Server's WITH XMLNAMESPACES you declare the default namespace in XQuery, for example...
if object_id(N'Reports') is not null drop table [Reports];
go
create table [Reports] (
[ClientId] int not null,
[xml] [xml] not null
)
go
insert [Reports] ([ClientID], [xml])
values (1, N'<Report id="b5d9b8da-7af4-4257-b825-b28af91dd833">
<CreatedDate>04-12-2012</CreatedDate>
<LastUpdated>04-12-2012</LastUpdated>
<Reference>abc123</Reference>
</Report>');
go
declare #clientId int = 1
select (
select [xml].query('/*:Report')
from [Reports]
where ClientId = #clientId
for xml path('Reports'), type
).query('declare default element namespace "http://www.defaultnamespace.com/1.0";
for $x in /*:Reports return
<Reports>
{
for $y in $x/*:Report return
<Report>
{attribute id {$y/#id}}
{element CreatedDate {$y/*:CreatedDate/text()}}
{element LastUpdated {$y/*:LastUpdated/text()}}
{element Reference {$y/*:Reference/text()}}
</Report>
}
</Reports>')
go
This will return the following block of XML:
<Reports xmlns="http://www.defaultnamespace.com/1.0">
<Report id="b5d9b8da-7af4-4257-b825-b28af91dd833">
<CreatedDate>04-12-2012</CreatedDate>
<LastUpdated>04-12-2012</LastUpdated>
<Reference>abc123</Reference>
</Report>
</Reports>
Your issue is that the column was not stored with a default namespace of "http://www.defaultnamespace.com/1.0".
So the logical name of your tag is NS = "", name =Report.
SQL Server is being absolutely correct.
What you would like to do is to say
by the way, that XML data I stored, I want you to rewrite every tag from the "" namespace logically attached to every name, to a tag of the same relative name in the "http://www.defaultnamespace.com/1.0" namespace, and then make that the default namespace
AFAIK, You can't do that (but if you find a way let me know!). The closest you can get is cast it to nvarchar(max) with no namespace, then cast it back again with the desired namespace applied.

Getting result of FOR XML into a variable

SELECT #Name = Name FROM Table FOR XML AUTO
Does not work, how do you get the XML result from using FOR XML into a variable?
This will work:
SELECT #Name = CONVERT(XML, (
SELECT Name
FROM SomeTable
FOR XML AUTO
));
You can try it without the wrapping CONVERT(XML, (...)) statement but I've found that SQL Server doesn't like assigning to XML variables without that explicit cast.

How to delete an attribute from an XML variable in sql server 2008?

I have a table called XML (in SQL Server 2008) and it has a field called XmlDocument of type XML. I am trying to to delete an attribute from an XML variable.
Here is how my xml looks like
<clue_personal_auto xmlns="http://cp.com/rules/client">
<admin>
<receipt_date>03/16/2011</receipt_date>
<date_request_ordered>03/16/2011</date_request_ordered>
<report_usage>Personal</report_usage>
</admin>
</clue_personal_auto>
My query
UPDATE XML
SET XmlDocument.modify('delete (/clue_personal_auto/#xmlns)[1]')
WHERE xmlid = 357
When I run this query in query analyzer I see the message "1 row(s) affected" but in reality the xmlns attribute of clue_personal_auto element is not being removed. Any idea what am I doing wrong.
Thanks
BB
You need to use WITH xmlnamespaces, otherwise "/clue_personal_auto" does not match the NAMESPACED clue_personal_auto xmlns="..." node.
Not only that, you cannot actually remove a namespace since it is not a normal attribute.
Example of removing a regular attribute
declare #xml table (xmlid int, xmldocument xml)
insert #xml select 357, '
<clue_personal_auto xmlns="http://cp.com/rules/client" otherattrib="x">
<admin>
<receipt_date>03/16/2011</receipt_date>
<date_request_ordered>03/16/2011</date_request_ordered>
<report_usage>Personal</report_usage>
</admin>
</clue_personal_auto>'
;WITH XMLNAMESPACES ('http://cp.com/rules/client' as ns)
UPDATE #XML
SET XmlDocument.modify('delete (/ns:clue_personal_auto/#otherattrib)[1]')
WHERE xmlid = 357
select * from #xml
UPDATE XML
SET CONVERT(XML, REPLACE(CONVERT(NVARCHAR(MAX), XmlDocument), N' xmlns=...'))
WHERE ID = 357
I can't seem to find an easy way to do this - but the real question remains: why do you want to remove the namespace?? Using the WITH XMLNAMESPACES ... construct, you can easily make use of the namespaces.
Instead of putting a lot of effort in getting rid of it - learn about XML namespaces and start using them!
You can quite easily use that XML namespace in your queries:
;WITH XMLNAMESPACES (DEFAULT 'http://cp.com/rules/client' )
SELECT
XmlDocument.value('(/clue_personal_auto/admin/report_usage)[1]', 'varchar(25)')
FROM XML
WHERE ID = 357
and be happy with it - no need to artificially remove xmlns= declarations anymore!