Why does STUFF remove XML? - sql

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('')

Related

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.

SQL Server 2012 create list of VARBINARY in single XML element

I am currently struggling to find a solution to the following problem.
I have a result set of VARBINARY values - for example:
QAAAAAAAAAE=
QAAAAAAAAAQ=
these results Need to be packed into a XML Element delimited by a single space (to siginify an array of values). Example of how the result should look:
<results>QAAAAAAAAAE= QAAAAAAAAAQ=</results>
The issue I am having while using XML PATH is that I cannot combine a ' ' (varchar) and the result varbinary field. If I add the ' ' before the result and then convert to varbinary the result is incorrect due to having converted the space as well. Here is an example of what I have attempted thus far:
(
STUFF((SELECT DISTINCT ' ' + CONVERT( VARBINARY, id)
FROM results
FOR XML PATH('ns2:children')
),1,1,'')
),
You should not deal with XML on string level. This might have various side effects...
You can try this:
I fill a table with some values in a VARBINARY column:
DECLARE #table TABLE(SomeData VARBINARY(MAX));
INSERT INTO #table VALUES(0x12AF)
,(CAST('test' AS VARBINARY(MAX)))
,(CAST(GETDATE() AS VARBINARY(MAX)));
SELECT *
FROM #table;
The result
SomeData
0x12AF
0x74657374
0x0000A81100AD9F69
If you get this as XML, the conversion to base64 is done implicitly
SELECT * FROM #table FOR XML PATH('result')
the result
<result>
<SomeData>Eq8=</SomeData>
</result>
<result>
<SomeData>dGVzdA==</SomeData>
</result>
<result>
<SomeData>AACoEQCtn2k=</SomeData>
</result>
Now there is XQuery-function data() to your rescue. It will automatically concatenate all sub-values separated by a blank:
SELECT(
SELECT *
FROM #table
FOR XML PATH('result'),TYPE
).query('data(/result/SomeData)') AS result
FOR XML PATH('results')
The result is
<results>
<result>Eq8= dGVzdA== AACoEQCtn2k=</result>
</results>
Pls. try short of SQL Command which could help u to achieve the above result :
SELECT REPLACE(T.Data, '</results><results>', ' ')
FROM
(
SELECT STUFF(
(
SELECT DATA [results]
FROM <table_name> FOR XML PATH('')
), 1, 1, '<') [Data]
) T;
Result :
<results>QAAAAAAAAAE= QAAAAAAAAAQ=</results>
If I understand what do you want?! A way to get <results>QAAAAAAAAAE= QAAAAAAAAAQ=</results> as result is:
select ltrim((
select ' '+cast(id as varchar(50))
from t
group by id
for xml path(''))) results
for xml path('');
SQL Fiddle Demo

Xquery to return rows with restricted nodes

I have a table where a column contains XML data. Now i want to retrieve those xml data with restriction of nodes. Kindly see the following example for more explanation on my scenario,
declare #table table (id int, xmlfield xml) insert into #table select 1,'<Root xmlns="">
<Sample>
<Issue>
<Level>one</Level>
<Descp>First Example</Descp>
</Issue>
<Issue>
<Level>two</Level>
<Descp>Second Example</Descp>
</Issue>
</Sample> </Root>'
select * from #table
Now i need the following result set
Id XMLfield
1 first example
ie, for the selected level,i need the decription for it. More clearly, the node should be restricted for <level>one</level>
(need: What is the description for level one ?)
thanks in advance
Have a look at the xml Data Type Methods
select id,
xmlfield.value('(//Issue[Level = "one"]/Descp/text())[1]', 'varchar(100)') as XMLField
from #table
The XQuery you're looking for is
//Issue[Level = "one"]/Descp/data()

SQL- Collect all data into a variable

i need to collect all return data into a variable using comma separated.
let say i have a select command like: select * from #temptable.
it's return:
Field1|Field2
-------------
Value1|Value2
Expected Result: #testvariable hold the value: 'Value1','Value2'
On this table their may have 2 columns and i need to store all the return result into a single variable. We can easily collect a single value like: select #var=column1 from #temptable. But i need to store all.Here the problem is, the number of column can be vary. Mean, number of column and name of column generate from another query.So, i can't mention the field name.I need a dynamic way to do it. on this table only one row will be return. Thanks in advance.
You can do this without dynamic SQL using XML
DECLARE #xml XML = (SELECT * FROM #temptable FOR XML PATH(''))
SELECT stuff((SELECT ',' + node.value('.', 'varchar(100)')
FROM #xml.nodes('/*') AS T(node)
FOR XML PATH(''), type).value('.','varchar(max)')
, 1, 1, '');
This can probably be simplified by someone more adept at XML querying than me.
Since your column names are dynamic, so first you have to take the column names as comma separated in a variable and then can use EXEC()
for example :-
//making comma seperated column names from table B
DECLARE #var varchar(1000)=SELECT SUBSTRING(
(SELECT ',' + Colnames
FROM TABLEB
ORDER BY Colnames
FOR XML PATH('')),2,200000)
//Execute the sql statement
EXEC('select '+#var+' from tableA')
if you want to get the value returned after execution of sql statement then you can use
sp_executesql (Transact-SQL)

NULL elements in FOR XML Clause / Alternative to XSINIL

I have some legacy code similar to:
...
'<field1>' +
case when field1 is null then '' else cast( field1 as varchar ) end +
'</field1>' +
...
Which generates the following XML for empty elements:
....
<field1></field1>
...
And I'm replacing the query with FOR XML:
SELECT field1,
...
FOR XML RAW, ELEMENTS
Now, this does not output an element for columns with NULL values. I know about XSINIL:
FOR XML RAW, ELEMENTS XSINIL
But this generates a namespaced empty XML element, which is not compatible with the legacy code reading this output.
...
<field1 xsi:nil="true" />
...
Any suggestions on generating the format below while still using the FOR XML Clause?
....
<field1></field1>
...
Thanks!
One very simple solution would be: just don't specify the "XSINIL" after ELEMENTS!
FOR XML RAW, ELEMENTS
In that case, you'll just get no XML entry for any values that are NULL.
If you really want an empty XML tag, you need to use something like this:
SELECT
......
ISNULL(CAST(field1 AS VARCHAR(100)), '') AS 'field1',
......
FROM dbo.YourTable
FOR XML RAW, ELEMENTS
thus turning the empty field1 into an empty string and thus serializing it into the XML.
You can double up on the columns that can have a null value with an empty string. The values will be concatenated and the empty string makes sure you will always have something that builds the node.
You need to use for xml path instead of for xml raw.
declare #T table
(
Col1 int,
Col2 int
)
insert into #T values(1, 2)
insert into #T values(1, null)
insert into #T values(null, 2)
insert into #T values(null, null)
select Col1,
'' as Col1,
Col2,
'' as Col2
from #T
for xml path('row')
Result:
<row>
<Col1>1</Col1>
<Col2>2</Col2>
</row>
<row>
<Col1>1</Col1>
<Col2></Col2>
</row>
<row>
<Col1></Col1>
<Col2>2</Col2>
</row>
<row>
<Col1></Col1>
<Col2></Col2>
</row>
you can avoid labor work of isnull by this
declare #Var varchar(max)
set #Var=(select
FirstName,
LastName,
Middldle_Name
From Customer
FOR XML AUTO, ELEMENTS xsinil , ROOT('Customer')
)
select CONVERT(xml,REPLACE(#Var,' xsi:nil="true"',''))