UPDATE: I've discovered there is a Microsoft Connect item raised for this issue here
When using FOR XML PATH and WITH XMLNAMESPACES to declare a default namespace, I will get the namespace declaration duplicated in any top level nodes for nested queries that use FOR XML, I've stumbled across a few solutions on-line, but I'm not totally convinced...
Here's an Complete Example
/*
drop table t1
drop table t2
*/
create table t1 ( c1 int, c2 varchar(50))
create table t2 ( c1 int, c2 int, c3 varchar(50))
insert t1 values
(1, 'Mouse'),
(2, 'Chicken'),
(3, 'Snake');
insert t2 values
(1, 1, 'Front Right'),
(2, 1, 'Front Left'),
(3, 1, 'Back Right'),
(4, 1, 'Back Left'),
(5, 2, 'Right'),
(6, 2, 'Left')
;with XmlNamespaces( default 'uri:animal')
select
a.c2 as "#species"
, (select l.c3 as "text()"
from t2 l where l.c2 = a.c1
for xml path('leg'), type) as "legs"
from t1 a
for xml path('animal'), root('zoo')
What's the best solution?
After hours of desperation and hundreds of trials & errors, I've come up with the solution below.
I had the same issue, when I wanted just one xmlns attribute, on the root node only. But I also had a very difficult query with lot's of subqueries and FOR XML EXPLICIT method alone was just too cumbersome. So yes, I wanted the convenience of FOR XML PATH in the subqueries and also to set my own xmlns.
I kindly borrowed the code of 8kb's answer, because it was so nice. I tweaked it a bit for better understanding. Here is the code:
DECLARE #Order TABLE (OrderID INT, OrderDate DATETIME)
DECLARE #OrderDetail TABLE (OrderID INT, ItemID VARCHAR(1), Name VARCHAR(50), Qty INT)
INSERT #Order VALUES (1, '2010-01-01'), (2, '2010-01-02')
INSERT #OrderDetail VALUES (1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
-- Your ordinary FOR XML PATH query
DECLARE #xml XML = (SELECT OrderID AS "#OrderID",
(SELECT ItemID AS "#ItemID",
Name AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('Item'), TYPE)
FROM #Order o
FOR XML PATH ('Order'), ROOT('dummyTag'), TYPE)
-- Magic happens here!
SELECT 1 AS Tag
,NULL AS Parent
,#xml AS [xml!1!!xmltext]
,'http://test.com/order' AS [xml!1!xmlns]
FOR XML EXPLICIT
Result:
<xml xmlns="http://test.com/order">
<Order OrderID="1">
<Item ItemID="A">Drink</Item>
<Item ItemID="B">Cup</Item>
</Order>
<Order OrderID="2">
<Item ItemID="A">Drink</Item>
<Item ItemID="C">Straw</Item>
<Item ItemID="D">Napkin</Item>
</Order>
</xml>
If you selected #xml alone, you would see that it contains root node dummyTag. We don't need it, so we remove it by using directive xmltext in FOR XML EXPLICIT query:
,#xml AS [xml!1!!xmltext]
Although the explanation in MSDN sounds more sophisticated, but practically it tells the parser to select the contents of XML root node.
Not sure how fast the query is, yet currently I am relaxing and drinking Scotch like a gent while peacefully looking at the code...
If I have understood correctly, you are referring to the behavior that you might see in a query like this:
DECLARE #Order TABLE (
OrderID INT,
OrderDate DATETIME)
DECLARE #OrderDetail TABLE (
OrderID INT,
ItemID VARCHAR(1),
ItemName VARCHAR(50),
Qty INT)
INSERT #Order
VALUES
(1, '2010-01-01'),
(2, '2010-01-02')
INSERT #OrderDetail
VALUES
(1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
;WITH XMLNAMESPACES('http://test.com/order' AS od)
SELECT
OrderID AS "#OrderID",
(SELECT
ItemID AS "#od:ItemID",
ItemName AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('od.Item'), TYPE)
FROM #Order o
FOR XML PATH ('od.Order'), TYPE, ROOT('xml')
Which gives the following results:
<xml xmlns:od="http://test.com/order">
<od.Order OrderID="1">
<od.Item xmlns:od="http://test.com/order" od:ItemID="A">Drink</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="B">Cup</od.Item>
</od.Order>
<od.Order OrderID="2">
<od.Item xmlns:od="http://test.com/order" od:ItemID="A">Drink</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="C">Straw</od.Item>
<od.Item xmlns:od="http://test.com/order" od:ItemID="D">Napkin</od.Item>
</od.Order>
</xml>
As you said, the namespace is repeated in the results of the subqueries.
This behavior is a feature according to a conversation on devnetnewsgroup (website now defunct) although there is the option to vote on changing it.
My proposed solution is to revert back to FOR XML EXPLICIT:
SELECT
1 AS Tag,
NULL AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
NULL AS [od:Order!2!OrderID],
NULL AS [od:Item!3],
NULL AS [od:Item!3!ItemID]
UNION ALL
SELECT
2 AS Tag,
1 AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
OrderID AS [od:Order!2!OrderID],
NULL AS [od:Item!3],
NULL [od:Item!3!ItemID]
FROM #Order
UNION ALL
SELECT
3 AS Tag,
2 AS Parent,
'http://test.com/order' AS [xml!1!xmlns:od],
NULL AS [od:Order!2],
o.OrderID AS [od:Order!2!OrderID],
d.ItemName AS [od:Item!3],
d.ItemID AS [od:Item!3!ItemID]
FROM #Order o INNER JOIN #OrderDetail d ON o.OrderID = d.OrderID
ORDER BY [od:Order!2!OrderID], [od:Item!3!ItemID]
FOR XML EXPLICIT
And see these results:
<xml xmlns:od="http://test.com/order">
<od:Order OrderID="1">
<od:Item ItemID="A">Drink</od:Item>
<od:Item ItemID="B">Cup</od:Item>
</od:Order>
<od:Order OrderID="2">
<od:Item ItemID="A">Drink</od:Item>
<od:Item ItemID="C">Straw</od:Item>
<od:Item ItemID="D">Napkin</od:Item>
</od:Order>
</xml>
An alternative solution I've seen is to add the XMLNAMESPACES declaration after building the xml into a temporary variable:
declare #xml as xml;
select #xml = (
select
a.c2 as "#species"
, (select l.c3 as "text()"
from t2 l where l.c2 = a.c1
for xml path('leg'), type) as "legs"
from t1 a
for xml path('animal'))
;with XmlNamespaces( 'uri:animal' as an)
select #xml for xml path('') , root('zoo');
The problem here is compounded by the fact that you cannot directly declare the namespaces manually when using XML PATH. SQL Server will disallow any attribute names beginning with 'xmlns' and any tag names with colons in them.
Rather than having to resort to using the relatively unfriendly XML EXPLICIT, I got around the problem by first generating XML with 'cloaked' namespace definitions and references, then doing string replaces as follows ...
DECLARE #Order TABLE (
OrderID INT,
OrderDate DATETIME)
DECLARE #OrderDetail TABLE (
OrderID INT,
ItemID VARCHAR(1),
ItemName VARCHAR(50),
Qty INT)
INSERT #Order
VALUES
(1, '2010-01-01'),
(2, '2010-01-02')
INSERT #OrderDetail
VALUES
(1, 'A', 'Drink', 5),
(1, 'B', 'Cup', 2),
(2, 'A', 'Drink', 2),
(2, 'C', 'Straw', 1),
(2, 'D', 'Napkin', 1)
declare #xml xml
set #xml = (SELECT
'http://test.com/order' as "#xxmlns..od", -- 'Cloaked' namespace def
(SELECT OrderID AS "#OrderID",
(SELECT
ItemID AS "#od..ItemID",
ItemName AS "data()"
FROM #OrderDetail
WHERE OrderID = o.OrderID
FOR XML PATH ('od..Item'), TYPE)
FROM #Order o
FOR XML PATH ('od..Order'), TYPE)
FOR XML PATH('xml'))
set #xml = cast(replace(replace(cast(#xml as nvarchar(max)), 'xxmlns', 'xmlns'),'..',':') as xml)
select #xml
A few things to point out:
I'm using 'xxmlns' as my cloaked version of 'xmlns' and '..' to stand in for ':'. This might not work for you if you're likely to have '..' as part of text values - you can substitute this with something else as long as you pick something that makes a valid XML identifier.
Since we want the xmlns definition at the top level, we cannot use the 'ROOT' option to XML PATH - instead I needed to add an another outer level to the subselect structure to achieve this.
I'm bit confusing about all these explanation while declaring a "xmlns:animals" manually is doing the job :
Here an example i wrote to generate Open graph meta data
DECLARE #l_xml as XML;
SELECT #l_xml =
(
SELECT 'http://ogp.me/ns# fb: http://ogp.me/ns/fb# scanilike: http://ogp.me/ns/fb/scanilike#' as 'xmlns:og',
(SELECT
(SELECT 'og:title' as 'property', title as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:type' as 'property', OpenGraphWebMetadataTypes.name as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:image' as 'property', image as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:url' as 'property', url as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:description' as 'property', description as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:site_name' as 'property', siteName as 'content' for xml raw('meta'), TYPE),
(SELECT 'og:appId' as 'property', appId as 'content' for xml raw('meta'), TYPE)
FROM OpenGraphWebMetaDatas INNER JOIN OpenGraphWebMetadataTypes ON OpenGraphWebMetaDatas.type = OpenGraphWebMetadataTypes.id WHERE THING_KEY = #p_index
for xml path('header'), TYPE),
(SELECT '' as 'body' for xml path(''), TYPE)
for xml raw('html'), TYPE
)
RETURN #l_xml
returning the expected result
<html xmlns:og="http://ogp.me/ns# fb: http://ogp.me/ns/fb# scanilike: http://ogp.me/ns/fb/scanilike#">
<header>
<meta property="og:title" content="The First object"/>
<meta property="og:type" content="scanilike:tag"/>
<meta property="og:image" content="http://www.mygeolive.com/images/facebook/facebook-logo.jpg"/>
<meta property="og:url" content="http://www.scanilike.com/opengraph?id=1"/>
<meta property="og:description" content="This is the very first object created using the IOThing & ScanILike software. We keep it in file for history purpose. "/>
<meta property="og:site_name" content="http://www.scanilike.com"/>
<meta property="og:appId" content="200270673369521"/>
</header>
<body/>
</html>
hope this will help people are searching the web for similar issue. ;-)
It would be really nice if FOR XML PATH actually worked more cleanly. Reworking your original example with #table variables:
declare #t1 table (c1 int, c2 varchar(50));
declare #t2 table (c1 int, c2 int, c3 varchar(50));
insert #t1 values
(1, 'Mouse'),
(2, 'Chicken'),
(3, 'Snake');
insert #t2 values
(1, 1, 'Front Right'),
(2, 1, 'Front Left'),
(3, 1, 'Back Right'),
(4, 1, 'Back Left'),
(5, 2, 'Right'),
(6, 2, 'Left');
;with xmlnamespaces( default 'uri:animal')
select a.c2 as "#species",
(
select l.c3 as "text()"
from #t2 l
where l.c2 = a.c1
for xml path('leg'), type
) as "legs"
from #t1 a
for xml path('animal'), root('zoo');
Yields the problem XML with repeated namespace declarations:
<zoo xmlns="uri:animal">
<animal species="Mouse">
<legs>
<leg xmlns="uri:animal">Front Right</leg>
<leg xmlns="uri:animal">Front Left</leg>
<leg xmlns="uri:animal">Back Right</leg>
<leg xmlns="uri:animal">Back Left</leg>
</legs>
</animal>
<animal species="Chicken">
<legs>
<leg xmlns="uri:animal">Right</leg>
<leg xmlns="uri:animal">Left</leg>
</legs>
</animal>
<animal species="Snake" />
</zoo>
You can migrate elements between namespaces using XQuery with wildcard namespace matching (that is, *:elementName), as below, but it can be quite cumbersome for complex XML:
;with xmlnamespaces( default 'http://tempuri.org/this/namespace/is/meaningless' )
select (
select a.c2 as "#species",
(
select l.c3 as "text()"
from #t2 l
where l.c2 = a.c1
for xml path('leg'), type
) as "legs"
from #t1 a
for xml path('animal'), root('zoo'), type
).query('declare default element namespace "uri:animal";
<zoo>
{ for $a in *:zoo/*:animal return
<animal>
{attribute species {$a/#species}}
{ for $l in $a/*:legs return
<legs>
{ for $m in $l/*:leg return
<leg>{ $m/text() }</leg>
}</legs>
}</animal>
}</zoo>');
Which yields your desired result:
<zoo xmlns="uri:animal">
<animal species="Mouse">
<legs>
<leg>Front Right</leg>
<leg>Front Left</leg>
<leg>Back Right</leg>
<leg>Back Left</leg>
</legs>
</animal>
<animal species="Chicken">
<legs>
<leg>Right</leg>
<leg>Left</leg>
</legs>
</animal>
<animal species="Snake" />
</zoo>
In the following code, I want to check if column is a Number. If it is - fill it with leading zeros.
Is there anyway using XML XQuery to check the original data type (int)
of the column?
declare #T table (string nchar(10), id int)
insert #T
select 'test1', 1
insert #T
select 'test2', 2
declare #X xml
SET ANSI_WARNINGS ON
set #X = (select * from #T order by id for xml path('row'), root('root'))
SELECT (
STUFF(
(
SELECT ';' + v.value('.','nvarchar(max)')
FROM r.nodes('*') AS B(v)
FOR XML PATH('')
),1,1,'')
) as [OUTPUT]
FROM #x.nodes('/root/row') AS A(r)
As you know your table you should not use a generical approach, if you do not have a good reason to do so.
What you probably should do is this
Include the formatted value into your XML. This allows you, to carry both information within the structure: typed and formatted.
declare #T table (string nvarchar(10), id int)
insert #T values
('test1', 1)
,('test2', 22)
declare #x xml = (select string
,REPLACE(STR(id,8),' ','0') AS [id/#formatted]
,id
from #T
order by id for xml path('row'), root('root'),TYPE)
SELECT (
STUFF(
(
SELECT ';' + r.value('(string/text())[1]','nvarchar(max)')
+ ';' + r.value('(id/#formatted)[1]','nvarchar(max)')
FOR XML PATH('')
),1,1,'')
) as [OUTPUT]
FROM #x.nodes('/root/row') AS A(r);
If you need the dynamic approach - look at this
declare #T table (string nchar(10), id int)
insert #T values
('test1', 1)
,('test2', 2)
declare #X xml = (select * from #T order by id for xml path('row'), root('root'))
select #x;
The first thing you see, that you - probably want to use NVARCHAR(10) instead of NCHAR(10). You might use LTRIM() too:
<root>
<row>
<string>test1 </string>
<id>1</id>
</row>
<row>
<string>test2 </string>
<id>2</id>
</row>
</root>
Now I start from scratch with NVARCHAR(10)
declare #T2 table (string nvarchar(10), id int);
insert #T2 values
('test1', 1)
,('test2', 22);
declare #X2 xml = (select * from #T2 order by id for xml path('row'), root('root'));
--First try is with ISNUMERIC and CASE WHEN
SELECT (
STUFF(
(
SELECT ';' + CASE WHEN ISNUMERIC(v.value('.','nvarchar(max)'))=1
THEN REPLACE(STR(v.value('.','int'),8),' ','0')
ELSE v.value('.','nvarchar(max)') END
FROM r.nodes('*') AS B(v)
FOR XML PATH('')
),1,1,'')
) as [OUTPUT]
FROM #x2.nodes('/root/row') AS A(r);
But: There are some character formats (scientific notations), which can be taken as numeric incidentically.
Add this to your table and try again
,('3d2',333) --breaks, because SELECT ISNUMERIC('3d2'),ISNUMERIC('1e1') returns 1 (for both)
Other/better approaches
If you are using SQL-Server 2012 or higher, you can use TRY_CAST, which will return NULL instead of an error
SELECT (
STUFF(
(
SELECT ';' + CASE WHEN TRY_CAST(v.value('.','nvarchar(max)') AS INT) IS NOT NULL
THEN REPLACE(STR(v.value('.','int'),8),' ','0')
ELSE v.value('.','nvarchar(max)') END
FROM r.nodes('*') AS B(v)
FOR XML PATH('')
),1,1,'')
) as [OUTPUT]
FROM #x2.nodes('/root/row') AS A(r);
Another option might the the explicit XQuery cast (Find possible XQuery functions here)
SELECT (
STUFF(
(
SELECT ';' + v.query('let $nd:=string(./text()[1])
,$nr:=concat("00000000",string($nd cast as xs:int?))
return if(string-length($nr)=8)
then $nd
else substring($nr,string-length($nr)-7)').value('.','nvarchar(max)')
FROM r.nodes('*') AS B(v)
FOR XML PATH('')
),1,1,'')
) as [OUTPUT]
FROM #x2.nodes('/root/row') AS A(r);
UPDATE
XML is not aware of an underlying type unless you specify a schema. Look at this:
declare #T2 table (string nvarchar(10), id int)
insert #T2 values
('test1', 1)
,('test2', 22)
,('1',333)
declare #x2 xml = (select * from #T2 order by id for xml raw('row'), root('root'),xmlschema)
select #x2;
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:sqltypes="http://schemas.microsoft.com/sqlserver/2004/sqltypes" targetNamespace="urn:schemas-microsoft-com:sql:SqlRowSet1" elementFormDefault="qualified">
<xsd:import namespace="http://schemas.microsoft.com/sqlserver/2004/sqltypes" schemaLocation="http://schemas.microsoft.com/sqlserver/2004/sqltypes/sqltypes.xsd" />
<xsd:element name="row">
<xsd:complexType>
<xsd:attribute name="string">
<xsd:simpleType>
<xsd:restriction base="sqltypes:nvarchar" sqltypes:localeId="1033" sqltypes:sqlCompareOptions="IgnoreCase IgnoreKanaType IgnoreWidth">
<xsd:maxLength value="10" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="id" type="sqltypes:int" />
</xsd:complexType>
</xsd:element>
</xsd:schema>
<row xmlns="urn:schemas-microsoft-com:sql:SqlRowSet1" string="test1" id="1" />
<row xmlns="urn:schemas-microsoft-com:sql:SqlRowSet1" string="test2" id="22" />
<row xmlns="urn:schemas-microsoft-com:sql:SqlRowSet1" string="1" id="333" />
</root>
In this case you might ask the schema for the underlying type. But - if I get you correctly - you want to look at any value if it might be a number. This works as shown, but is extremely dangerous...
Table Name : sample
Column Name : id,name
Every Row Create Separate tag with inside.
Show the Xml value like this
<Details>
<id>1</id>
<name>na</name>
<Details>
<id>2</id>
<name>aa</name>
</Details>
</Details>
I tried like this but its not working
select
id 'Details\id'
,name 'Details\name'
from sample
How do get that xml output?
It is hardcoded but should work:
DECLARE #x xml
SELECT #x = (
SELECT x+''
FROM (
SELECT '%details?%id?'+CAST(id as nvarchar(max))+'%/id?%name?'+name+'%/name?' x
FROM [sample] s
UNION ALL
SELECT '%/details?'
FROM [sample] s
) as t
FOR XML PATH('')
)
SELECT CAST(REPLACE(REPLACE((CAST(#x as nvarchar(max))),'%','<'),'?','>') as xml)
In [sample] table I got:
(1,'na'),
(2,'aa'),
(3,'sd')
Output:
<details>
<id>1</id>
<name>na</name>
<details>
<id>2</id>
<name>aa</name>
<details>
<id>3</id>
<name>sd</name>
</details>
</details>
</details>
EDIT
Also it could be done with recursive CTE:
DECLARE #x xml
;WITH rec AS (
SELECT CAST((
SELECT TOP 1 id,
[name]
FROM [sample]
ORDER BY id DESC
FOR XML PATH('details')
) as xml) as d,
1 as [Level]
UNION ALL
SELECT CAST((
SELECT id,
[name],
cast(r.d as xml)
FROM [sample]
WHERE s.id = id
FOR XML PATH('details')
) as xml) as d,
r.[Level]+1
FROM [sample] s
INNER JOIN rec r
ON s.id = CAST(r.d.query('/details/id/text()') as nvarchar(max))-1
)
SELECT TOP 1 WITH TIES d
FROM rec
ORDER BY [Level] desc
Same output.
You can use query like this:
SELECT
*,
(SELECT
*
FROM #details
WHERE id = 2
FOR xml PATH ('Details'), TYPE)
FROM #details
WHERE id = 1
FOR xml PATH ('Details')
For inner loop you can use CTE
Table creation scripts :
CREATE TABLE #details (
id int,
name varchar(10)
)
INSERT INTO #details (id, name)
VALUES (1, 'test'), (2, 'test2')