Add attribute with colon to xml node with FOR XML PATH - sql

I am trying to modify a stored proc to contain the following:
SET #XML = (
SELECT Category.Title,
(
SELECT 'true' AS [#json:Array], Book.Name, Book.Value
FROM #Book Book
WHERE Category.CategoryID = Book.CategoryID
FOR XML PATH('Values'), ROOT('Book'), TYPE
)
FROM #Category Category
FOR XML PATH('Category'), ROOT('Response')
)
The "SELECT 'true' AS [#json:Array]" is there to force the xml to add "json:Array='true' to the values node so that even if there's only one child element it will be contained in an array. But, the #json:Array throws an error: "XML name space prefix 'json' declaration is missing for FOR XML column name '#json:Array'."
I've looked at links like this but they all seem to deal with adding attributes that don't include a colon. I've also tried adding "WITH NAMESPACES..." but couldn't get the syntax right.
Can someone tell me how to modify the SQL to have this work?

If you do it like this:
DECLARE #XML XML;
WITH XMLNAMESPACES('xmlns:json' AS json)
SELECT #XML=
(
SELECT 'YourTitle',
(
SELECT 'true' AS [#json:Array], 'BookName', 'BookValue'
FOR XML PATH('Values'), ROOT('Book'), TYPE
)
FOR XML PATH('Category'), ROOT('Response')
)
SELECT #xml
... you'll get the attribut. But the price is a repeated namespace in all of your root nodes (in nested too).
This might be a trick, but you'll have to declare your namespace in the top element:
DECLARE #XML XML;
SELECT #XML=
(
REPLACE(REPLACE(
(
SELECT 'YourTitle',
(
SELECT 'true' AS [#jsonArray], 'BookName', 'BookValue'
FOR XML PATH('Values'), ROOT('Book'), TYPE
)
FOR XML PATH('Category'), ROOT('Response')
),'jsonArray','json:Array'),'<Response','<Response xmlns:json="urnJson"')
);
SELECT #xml

Related

Insert or store a XML created with XML PATH that has namespaces

I generate a XML with XML PATH that has 6 namespaces. There is a schema and therefore I can not change the XML structure.
WITH XMLNAMESPACES ('NS1' AS ns1,
'NS2' AS ns2,
'NS3' AS ns3,
'NS4' AS ns4,
'NS5' AS ns5,
'NS6' AS ns6)
SELECT(SELECT 'something' AS 'ns3:node2' FOR XML PATH('ns2:Node1'), TYPE)
FOR XML PATH(''),
ROOT('ns1:RootNode');
Now I need to either insert the output into a tmp table or store it in a variable. The problem I am stuck at is that with needs a ; in front of it. So Set #myVariable = above Codeblock and insert into tmp table values(above codeblock) are both not working and I am wondering if there is a way to store it. The XML is valid and works fine if I look at it or save it to the hard disk, but I need to do some more work with that XML.
You need to put your whole SELECT within a further subquery. So for an insert:
DECLARE #T TABLE (X XML);
WITH XMLNAMESPACES ('NS1' AS ns1,
'NS2' AS ns2,
'NS3' AS ns3,
'NS4' AS ns4,
'NS5' AS ns5,
'NS6' AS ns6)
INSERT #T (X)
SELECT (SELECT (SELECT 'something' AS 'ns3:node2' FOR XML PATH('ns2:Node1'), TYPE)
FOR XML PATH(''),
ROOT('ns1:RootNode'));
SELECT * FROM #T;
Or to assign a variable:
DECLARE #X XML;
WITH XMLNAMESPACES ('NS1' AS ns1,
'NS2' AS ns2,
'NS3' AS ns3,
'NS4' AS ns4,
'NS5' AS ns5,
'NS6' AS ns6)
SELECT #X= (SELECT (SELECT 'something' AS 'ns3:node2' FOR XML PATH('ns2:Node1'), TYPE)
FOR XML PATH(''),
ROOT('ns1:RootNode')
);
SELECT #X;

Concatenate/aggregate strings with JSON in SQL Server

This might be a simple question for those who are experienced in working with JSON in SQL Server. I found this interesting way of aggregating strings using FOR XML in here.
create table #t (id int, name varchar(20))
insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')
select id
,Names = stuff((select ', ' + name as [text()]
from #t xt
where xt.id = t.id
for xml path('')), 1, 2, '')
from #t t
group by id
How can I do the same using JSON instead of XML?
You cannot replace the XML approach with JSON. This string concatenation works due to some XML inner peculiarities, which are not the same in JSON.
Starting with SQL Server 2017 onwards you can use STRING_AGG(), but with earlier versions, the XML approach is the way to go.
Some background and a hint
First the hint: The code you showed is not safe for the XML special characters. Check my example below.
First I declare a simple XML
DECLARE #xml XML=
N'<a>
<b>1</b>
<b>2</b>
<b>3</b>
<c>
<d>x</d>
<d>y</d>
<d>z</d>
</c>
</a>';
--The XPath . tells the XML engine to use the current node (and all within)
--Therefore this will return any content within the XML
SELECT #xml.value('.','varchar(100)')
--You can specify the path to get 123 or xyz
SELECT #xml.query('/a/b').value('.','varchar(100)')
SELECT #xml.query('//d').value('.','varchar(100)')
Now your issue to concatenate tabular data:
DECLARE #tbl TABLE(SomeString VARCHAR(100));
INSERT INTO #tbl VALUES('This'),('will'),('concatenate'),('magically'),('Forbidden Characters & > <');
--The simple FOR XML query will tag the column with <SomeString> and each row with <row>:
SELECT SomeString FROM #tbl FOR XML PATH('row');
--But we can create the same without any tags:
--Attention: Look closely, that the result - even without tags - is XML typed and looks like a hyper link in SSMS.
SELECT SomeString AS [*] FROM #tbl FOR XML PATH('');
--Now we can use as a sub-select within a surrounding query.
--The result is returned as string, not XML typed anymore... Look at the forbidden chars!
SELECT
(SELECT SomeString FROM #tbl FOR XML PATH('row'))
,(SELECT SomeString AS [*] FROM #tbl FOR XML PATH(''))
--We can use ,TYPE to enforce the sub-select to be treated as XML typed itself
--This allows to use .query() and/or .value()
SELECT
(SELECT SomeString FROM #tbl FOR XML PATH('row'),TYPE).query('data(//SomeString)').value('.','nvarchar(max)')
,(SELECT SomeString AS [*] FROM #tbl FOR XML PATH(''),TYPE).value('.','nvarchar(max)')
XQuery's .data() can be used to concatenate named elements with blanks in between.
XQuery's .value() must be used to re-escpae forbidden characters.

Why does STUFF remove XML?

Please see the DDL below:
create table #Test (id int,Name varchar(30))
insert into #Test values (1,'Ian')
insert into #Test values(1,'Mark')
insert into #Test values(2,'James')
insert into #Test values(3,'Karen')
insert into #Test values(3,'Suzie')
and the SQL below:
select * from #Test for xml path('')
which returns:
<id>1</id>
<Name>Ian</Name>
<id>1</id>
<Name>Mark</Name>
<id>2</id>
<Name>James</Name>
<id>3</id>
<Name>Karen</Name>
<id>3</id>
<Name>Suzie</Name>
This is what I would expect. Now see the SQL below:
SELECT distinct ID,
STUFF( (select ','+ NAME from #Test as #Test1 where #Test1.id=#Test2.id FOR XML PATH('')),1,1,'') FROM #Test as #Test2
which returns:
1 Ian,Mark
2 James
3 Karen,Suzie
This is what I want returned. However, where have the XML elements gone?
You have to compare apples to apples. While it's true that
select * from #Test for xml path('')
produces something that looks like XML (but technically isn't because it doesn't have a root element), this (what you're actually running)
select ',' + name from #Test for xml path('')
doesn't. On my machine, it produces the ff string: ",Ian,Mark,James,Karen,Suzie". From there, the stuff function whacks the first comma and you get a list of comma-separated values.
Why does STUFF remove XML?
STUFF removes the first comma in the string, it is not responsible for removing the XML element names.
FOR XML PATH uses the column names to create the XML element names. When you concat two values together ','+ NAME the resulting column has no name so FOR XML PATH can not generate an element name for you.
The behavior is documented in Columns without a Name.
Any column without a name will be inlined. For example, computed
columns or nested scalar queries that do not specify column alias will
generate columns without any name.
It's not the STUFF, this is only for removing the superfluous first ,.
The concat removes the XML stuff:
','+ NAME
or
NAME + ''
Don't ask me why it's working like this, maybe it's documented somewhere :-)
Inner for xml statement is just for producing concatenating result. Add outer for xml statement:
SELECT distinct ID,
STUFF( (select ','+ NAME
from Test as #Test1
where #Test1.id=#Test2.id
FOR XML PATH('')),1,1,'') as Names
FROM Test as #Test2
FOR XML PATH('')
Output:
<ID>1</ID><Names>Ian,Mark</Names><ID>2</ID><Names>James</Names><ID>3</ID><Names>Karen,Suzie</Names>
Fiddle http://sqlfiddle.com/#!6/5f254/13
alias the column - then you get xml tags
do not alias the column - then no xml tags
-- with tags
select 'apple' apple for xml path('')
-- without tags
select 'apple' for xml path('')

Querying XML data types which have xmlns node attributes

I have the following SQL query:
DECLARE #XMLDOC XML
SET #XMLDOC = '<Feed><Product><Name>Foo</Name></Product></Feed>'
SELECT x.u.value('Name[1]', 'varchar(100)') as Name
from #XMLDOC.nodes('/Feed/Product') x(u)
This returns:
Name
----
Foo
However, if my <Feed> node has an xmlns attribute, then this doesn't return any results:
DECLARE #XMLDOC XML
SET #XMLDOC = '<Feed xmlns="bar"><Product><Name>Foo</Name></Product></Feed>'
SELECT x.u.value('Name[1]', 'varchar(100)') as Name
from #XMLDOC.nodes('/Feed/Product') x(u)
Returns:
Name
----
This only happens if I have an xmlns attribute, anything else works fine.
Why is this, and how can I modify my SQL query to return results regardless of the attributes?
If your XML document has XML namespaces, then you need to consider those in your queries!
So if your XML looks like your sample, then you need:
-- define the default XML namespace to use
;WITH XMLNAMESPACES(DEFAULT 'bar')
SELECT
x.u.value('Name[1]', 'varchar(100)') as Name
from
#XMLDOC.nodes('/Feed/Product') x(u)
Or if you prefer to have explicit control over which XML namespace to use (e.g. if you have multiple), use XML namespace prefixes:
-- define the XML namespace
;WITH XMLNAMESPACES('bar' as b)
SELECT
x.u.value('b:Name[1]', 'varchar(100)') as Name
from
#XMLDOC.nodes('/b:Feed/b:Product') x(u)
As well as the XMLNAMESPACES solution, you can also use the hideously bulky local-name syntax...
DECLARE #XMLDOC XML
SET #XMLDOC = '<Feed xmlns="bar"><Product><Name>Foo</Name></Product></Feed>'
SELECT x.u.value('*[local-name() = "Name"][1]', 'varchar(100)') as Name
from #XMLDOC.nodes('/*[local-name() = "Feed"]/*[local-name() = "Product"]') x(u)
You can define namespaces like:
WITH XMLNAMESPACES ('bar' as b)
SELECT x.u.value('b:Name[1]', 'varchar(100)') as Name
FROM #XMLDOC.nodes('/b:Feed/b:Product') x(u)

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.