How to put an attribute on the root element, and only the root element, in FOR XML PATH? - sql

I'm generating XML from a SQL Server table.
This is my code:
;WITH XMLNAMESPACES
(
'http://www.w3.org/2001/XMLSchema-instance' AS xsi
--,DEFAULT 'http://www.w3.org/2001/XMLSchema-instance' -- xmlns
)
SELECT
'T_Contracts' AS "#tableName",
(SELECT * FROM T_Contracts
FOR XML PATH('row'), TYPE, ELEMENTS xsinil)
FOR XML PATH('table'), TYPE, ELEMENTS xsinil
I want the result to look like this (note: attribute tableName on the root element):
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">
<row>
<VTR_UID>779FE899-4E81-4D8C-BF9B-3F17BC1DF146</VTR_UID>
<VTR_MDT_ID>0</VTR_MDT_ID>
<VTR_VTP_UID xsi:nil="true" />
<VTR_Nr>0050/132251</VTR_Nr>
</row>
</table>
But it duplicates the XSI namespace on the row element...
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<VTR_UID>779FE899-4E81-4D8C-BF9B-3F17BC1DF146</VTR_UID>
<VTR_MDT_ID>0</VTR_MDT_ID>
<VTR_VTP_UID xsi:nil="true" />
<VTR_Nr>0050/132251</VTR_Nr>
</row>
</table>
What's the correct way to add an attribute to the root element, and only the root element ?
Note
NULL-values must be returned as <columnName xsi:nil="true" /> and not be omitted.
(And no xml.modify after the select)
Please note that this is NOT a duplicate of an existing question.

This annoying behaviour of repeated namespaces with sub-queries was a reported issue for more than 10 years on MS-Connect with thousands of votes. This platform was dismissed, so was this issue and there is no perspective that MS will ever solve this.
Just to be fair: It is not wrong to repeat the namespace declaration. It's just bloating the string-based output...
Even stranger is the the unsupported attribute on a root level node...
Well, if you need a head-ache, you might look into OPTION EXPLICIT :-)
The accepted answer by Marc Guillot will not produce xsi:nil="true" attributes as you seem to need them. It will just wrap your result with the appropriate root node.
Finally: This cannot be solved with XML methods, you can try this:
Update: Found a way, see below...
DECLARE #tbl TABLE(ID INT,SomeValue INT);
INSERT INTO #tbl VALUES(1,1),(2,NULL);
SELECT CAST(REPLACE(CAST(
(
SELECT *
FROM #tbl
FOR XML PATH('row'),ROOT('table'),TYPE, ELEMENTS XSINIL
) AS nvarchar(MAX)),'<table ','<table tableName="T_Contracts" ') AS XML);
The result
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">
<row>
<ID>1</ID>
<SomeValue>1</SomeValue>
</row>
<row>
<ID>2</ID>
<SomeValue xsi:nil="true" />
</row>
</table>
The idea in short:
We create the XML without a sub-query and add the attribute with a string method into the casted XML.
As the position of an attribute is not important, we can add it everywhere.
alternatively you might search for the first closing > and use STUFF() there...
UPDATE
Heureka, I just found a way, to create this without swithing to string, but it's clumsy :-)
DECLARE #tbl TABLE(ID INT,SomeValue INT);
INSERT INTO #tbl VALUES(1,1),(2,NULL);
SELECT
(
SELECT 'T_Contracts' AS [#tableName]
,(
SELECT 'SomeRowAttr' AS [#testAttr] --added this to test row-level attributes
,*
FROM #tbl
FOR XML PATH('row'),TYPE, ELEMENTS XSINIL
)
FOR XML PATH('table'),TYPE, ELEMENTS XSINIL
).query('<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">{/table/#*}
{
for $nd in /table/row
return
<row>{$nd/#*}
{
$nd/*
}
</row>
}
</table>');
The result
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">
<row testAttr="SomeRowAttr">
<ID>1</ID>
<SomeValue>1</SomeValue>
</row>
<row testAttr="SomeRowAttr">
<ID>2</ID>
<SomeValue xsi:nil="true" />
</row>
</table>

Why don't you build manually the root element ?
Example:
with CTE as (
select (select * from T_Contracts for xml path('row')) as MyXML
)
select '<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">' +
MyXML +
'</table>'
from CTE

Unfortunately you cannot do this with the SQL Server out of the box nor exists an elegant way to do that. To alleviate the issue, you can replace NULLs with empty strings. This will remove xmlns, but you have to define your select list explicitly as follows. Moreover, this works only with character string data types as you cannot assign an empty string ('' in ISNULL function) to-for example-an integer.
;WITH XMLNAMESPACES
(
'http://www.w3.org/2001/XMLSchema-instance' AS xsi
--,DEFAULT 'http://www.w3.org/2001/XMLSchema-instance' -- xmlns
)
SELECT 'T_Contracts' AS "#tableName",
(
SELECT
ISNULL(VTR_UID, '') 'row/VTR_UID'
,ISNULL(VTR_MDT_ID, '') 'row/VTR_MDT_ID'
,ISNULL(VTR_VTP_UID, '') 'row/VTR_VTP_UID'
,ISNULL(VTR_Nr, '') 'row/VTR_Nr'
FROM T_Contracts
FOR XML PATH(''), TYPE
)
FOR XML PATH('table'), TYPE, ELEMENTS xsinil
The result will be like below:
<table xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" tableName="T_Contracts">
<row>
<VTR_UID>779FE899-4E81-4D8C-BF9B-3F17BC1DF146</VTR_UID>
<VTR_MDT_ID>0</VTR_MDT_ID>
<VTR_VTP_UID />
<VTR_Nr>0050/132251</VTR_Nr>
</row>
</table>

Related

SQL server XML query with xquery

I'm trying to reach "row" data below using xquery on sql server.
DECLARE #xmlData XML
SET #xmlData = '
<Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<Header>
<WorkContext xmlns="http://oracle.com/weblogic/soap/workarea/">rO0ABXdbABh3ZWJsb2duNDAAAA==</WorkContext>
</Header>
<Body>
<getReport xmlns:ns0="http://report_xml.org">
<return>
<pagekey>F1D96FB7ACFBA3BD5D5E77C5</pagekey>
<rapor>
<dataSet xmlns="http://report_xml.org/dataSet/201006">
<dataTable>
<id>ABC1</id>
<row>
<dim1>Paris</dim1>
<dim2>Societe</dim2>
<dim3>Car</dim3>
</row>
<row>
<dim1>Paris</dim1>
<dim2>Credit</dim2>
<dim3>Car</dim3>
</row>
</dataTable>
</dataSet>
</rapor>
</return>
</getReport>
</Body>
</Envelope>
'
When I use below code I can get the the "pagekey" value
; WITH XMLNAMESPACES ('http://report_xml.org/dataSet/201006' AS DS)
select #xmlData.query('/Envelope/Body/getReport/return/pagekey')
or I can reach the "dataSet" xml using this one
; WITH XMLNAMESPACES ('http://report_xml.org/dataSet/201006' AS DS)
select #xmlData.query('/Envelope/Body/getReport/return/rapor/DS:dataSet')
However, when I try to reach further down like
; WITH XMLNAMESPACES ('http://report_xml.org/dataSet/201006' AS DS)
select #xmlData.query('/Envelope/Body/getReport/return/rapor/DS:dataSet/dataTable/id')
or
select #xmlData.query('/Envelope/Body/getReport/return/rapor/DS:dataSet/dataTable/row')
Queries return empty.
what am I missing?
dataSet element introduces default namespace http://report_xml.org/dataSet/201006. Note that unlike prefixed namespace, descendant elements inherit default namespace from ancestor implicitly, so you should use the same prefix to access elements within dataSet:
; WITH XMLNAMESPACES ('http://report_xml.org/dataSet/201006' AS DS)
select #xmlData.query('/Envelope/Body/getReport/return/rapor/DS:dataSet/DS:dataTable/DS:id')
or
select #xmlData.query('/Envelope/Body/getReport/return/rapor/DS:dataSet/DS:dataTable/DS:row')

SQL query for XML data

I have a SQL Server database table with a column called XML that contains XML data which is structured like this:
<Item xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://test/data">
<Roots>
<Root>
<Name>Field Name</Name>
<Value>Field Value</Value>
</Root>
<Root>
<Name>Field Name</Name>
<Value>Field Value</Value>
</Root>
</Roots>
I want to use T-SQL to get the Value where Name = Total. I have tried the following but it isn't returning any data:
SELECT [XML]
FROM [BusinessAccount]
WHERE [XML].value('(/Root/Name)[13]', 'VARCHAR(MAX)') LIKE '%Total%'
If anyone could tell me where I've gone wrong?
You are missing the required WITH XMLNAMESPACES for your XML and the path is incorrect.
If you want to bring back rows where the 13th element consists of the text Total you can use the below.
WITH XMLNAMESPACES (DEFAULT 'http://test/data')
SELECT [XML]
FROM [BusinessAccount]
WHERE 1 = [XML].exist('(/Item/Roots/Root/Name)[13][text() = "Total"]')
Otherwise you can add the WITH XMLNAMESPACES to your original query and fix the path there too.
You need to specify namespaces. You can then match <Name> and <Value> pairs and extract the contents of <Value> like so:
SELECT NameNode.value('declare namespace x="http://test/data"; (../x:Value)[1]', 'varchar(100)')
FROM [BusinessAccount]
CROSS APPLY [XML].nodes('declare namespace x="http://test/data"; //x:Root/x:Name') AS n(NameNode)
WHERE NameNode.value('.', 'varchar(100)') = 'Total'
Demo on db<>fiddle

SQL:Looping and Reading from XML type

i have set of records from XML type and i need to loop through the Nodes and extract data from them
<Data xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<OP xmlns="http://tempuri.org/Types.xsd">
<ID>
<FDSerial>00000008</FDSerial>
<Type>1</Type>
</ID>
<Param>1</Param>
<OperationName>ReadData</OperationName>
</OP>
<OP xmlns="http://tempuri.org/Types.xsd">
<ID>
<FDSerial>00000009</FDSerial>
<Type>1</Type>
</ID>
<Param>1</Param>
<OperationName>ReadData</OperationName>
</OP>
</Data>
i already read from the record but i need to loop through the Nodes
;WITH XMLNAMESPACES('http://tempuri.org/Types.xsd' as ns)
SELECT Data.value('/Data[1]/ns:Op[1]/ns:ID[1]/ns:FDSerial[1]', 'varchar(50)')
as Serial
FROM [dbo].[DB]
what i need to replace the # 1 with i in a loop
Update:
i updated the xml data.
the expected from this data is the Nodes value of the FDSerial.
i hope i explained my issue clearly
You don't need a loop at all. Use nodes() in a cross apply to shred on /Data/OP.
with xmlnamespaces('http://tempuri.org/Types.xsd' as ns)
select T.X.value('(ns:ID/ns:FDSerial/text())[1]', 'varchar(50)') as Serial
from dbo.DB
cross apply DB.Data.nodes('/Data/ns:OP') as T(X)
this will work also
;WITH XMLNAMESPACES('http://tempuri.org/Types.xsd' as ns)
SELECT Data.value('(Data/ns:Op/ns:ID/ns:FDSerial/text())[1]', 'varchar(50)')
as Serial
FROM [dbo].[DB]

How do I set the xmlns attribute on the root element in the generated XML by using T-SQL's xml data type method: query?

I've created a simplified version of my problem:
DECLARE #X XML =
'<Root xmlns="TestNS" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Test>
<Id>1</Id>
<InnerCollection>
<InnerItem>
<Value>1</Value>
</InnerItem>
<InnerItem>
<Value>2</Value>
</InnerItem>
<InnerItem>
<Value>3</Value>
</InnerItem>
</InnerCollection>
</Test>
<Test>
<Id>2</Id>
<InnerCollection>
<InnerItem>
<Value>5</Value>
</InnerItem>
<InnerItem>
<Value>6</Value>
</InnerItem>
<InnerItem>
<Value>7</Value>
</InnerItem>
</InnerCollection>
</Test>
</Root>'
I'm trying to write a query that takes each <Test> element and breaks it into a row. On each row I want to select the Id and the InnerCollection as XML. I want to create this InnerCollection XML for the first row (Id:1):
<InnerCollection xmlns="Reed.Api" xmlnsi="http//www.w3.org/2001/XMLSchema-instance">
<InnerItem>
<Value>1</Value>
</InnerItem>
<InnerItem>
<Value>2</Value>
</InnerItem>
<InnerItem>
<Value>3</Value>
</InnerItem>
</InnerCollection>
I tried doing that with this query but it puts a namespace I don't want on the elements:
;WITH XMLNAMESPACES
(
DEFAULT 'TestNS'
, 'http://www.w3.org/2001/XMLSchema-instance' AS i
)
SELECT
X.value('Id[1]', 'INT') Id
-- Creates a p1 namespace that I don't want.
, X.query('InnerCollection') InnerCollection
FROM #X.nodes('//Test') AS T(X)
My Google-fu isn't very strong today, but I imagine it doesn't make it any easier that the darn function is called query. I'm open to using other methods to create that XML value other than the query method.
I could use this method:
;WITH XMLNAMESPACES
(
DEFAULT 'TestNS'
, 'http://www.w3.org/2001/XMLSchema-instance' AS i
)
SELECT
X.value('Id[1]', 'INT') Id
,CAST(
(SELECT
InnerNodes.Node.value('Value[1]', 'INT') AS 'Value'
FROM X.nodes('./InnerCollection[1]//InnerItem') AS InnerNodes(Node)
FOR XML PATH('InnerItem'), ROOT('InnerCollection')
) AS XML) AS InnerCollection
FROM #X.nodes('//Test') AS T(X)
But that involves calling nodes on it to break it out into something selectable, and then selecting it back into XML using FOR XML... when it was XML to begin with. This seems like a inefficient method of doing this, so I'm hoping someone here will have a better idea.
This is how to do the SELECT using the query method to create the XML on each row that my question was looking for:
;WITH XMLNAMESPACES
(
'http://www.w3.org/2001/XMLSchema-instance' AS i
, DEFAULT 'TestNS'
)
SELECT
Test.Row.value('Id[1]', 'INT') Id
, Test.Row.query('<InnerCollection xmlns="TestNS" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">{InnerCollection}</InnerCollection>')
FROM #X.nodes('/Root/Test') AS Test(Row)

SQL XML Replacing elements

Please help! Is it possible to replace elements within an xml field of an sql database with other elements. I have tried using .modify(replace value of) but I can only replace text within elements rather than nodes.
Ultimately I am trying to update an element which may or may not contain other elements, with another element (possibly of the same name) within an XML field. (I am using SQL Server 2008)
E.g:
<Root>
<Sub>
<Value1>
</Value1>
<Value2>
</Value2>
<Value3>
</Value3>
</Sub>
</Root>
Would be replaced by:
<Root>
<SubVERSION2>
<Value1>
</Value1>
<Value2>
</Value2>
<Value3>
</Value3>
</SubVERSION2>
</Root>
Any help would be very much appreciated!
You can recreate your XML:
declare #x xml = '<Root>
<Sub>
<Value1>1
</Value1>
<Value2>2
</Value2>
<Value3>3
</Value3>
</Sub>
</Root>'
select cast(('<Root>' +
cast(
(
select t.c.query('.')
from #x.nodes('Root/Sub/*') t(c)
for xml path(''), root('SubVERSION2')
) as nvarchar(max)) + '</Root>') as xml)
produces desired output:
<Root>
<SubVERSION2>
<Value1>1
</Value1>
<Value2>2
</Value2>
<Value3>3
</Value3>
</SubVERSION2>
</Root>
declare #T table(XMLCol xml)
insert into #T values ('
<Root>
<Sub>
<Value1></Value1>
<Value2></Value2>
<Value3></Value3>
</Sub>
</Root>')
update #T set
XMLCol = XMLCol.query('for $s in Root/Sub
return
<Root>
<SubVERSION2>
{ $s/* }
</SubVERSION2>
</Root>')
Result:
<Root>
<SubVERSION2>
<Value1 />
<Value2 />
<Value3 />
</SubVERSION2>
</Root>