Concatenate/aggregate strings with JSON in SQL Server - sql

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.

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;

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

Select SQL Query to get xml node values from ntext column?

I want to get one xml node value from NTEXT column which contains xml based on where clause quering on another xml node value.
RDBMS Type: Microsoft SQL Server T-SQL
Here: I want to get Code node value based on StoreId where clause value. How do I get it?
Input: 100
Output:ABCDE
For example:
<root>
<StoreProfile>
<General>
<StoreId>100</StoreId>
<Code>ABCDE</Code>
</General>
</StoreProfile>
</root>
If you are using SQL Server 2005 or 2008 you can use XQuery like so:
For more on XQuery see XQuery Language Reference
DECLARE #storeId INT
SET #storeId = 100
CREATE TABLE #TestTable
(
xmlColumn NTEXT
)
INSERT INTO #TestTable (xmlColumn) Values('<root><StoreProfile><General><StoreId>100</StoreId><Code>ABCDE</Code></General></StoreProfile></root>')
INSERT INTO #TestTable (xmlColumn) Values('<root><StoreProfile><General><StoreId>200</StoreId><Code>FGHIJ</Code></General></StoreProfile></root>')
SELECT
StoreProfile.value('Code[1]', 'nvarchar(10)') as Code
FROM #TestTable
CROSS APPLY (SELECT CAST(xmlColumn AS XML)) AS A(B)
CROSS APPLY A.B.nodes('//root/StoreProfile/General[StoreId = sql:variable("#storeId")]') AS StoreProfiles(StoreProfile)
DROP TABLE #TestTable

SQL Server 2005: How to perform a split on a string

I have the following string that I need to split from a field called symbols
234|23|HC
This is my current SQL statement
declare #t xml;
Set #t = (
Select symbols from tc for xml auto, elements)
Select #t;
which produces <symbols>234|23|HC</symbols>
but I need to split the string into child nodes so the result is like this:
<symbols>
<symbol>234</symbol>
<symbol>23</symbol>
<symbol>HC</symbol>
</symbols>
A replace version that takes care of the problem characters.
declare #T table(symbol varchar(50))
insert into #T values ('234|23|HC|Some problem chars <> &')
select cast('<symbols><symbol>'+
replace(cast(cast('' as xml).query('sql:column("symbol")') as varchar(max)),
'|',
'</symbol><symbol>')+
'</symbol></symbols> ' as xml)
from #T
Result:
<symbols>
<symbol>234</symbol>
<symbol>23</symbol>
<symbol>HC</symbol>
<symbol>Some problem chars <> &</symbol>
</symbols>

how to get values inside an xml column, when it's of type nvarchar

My question is similar to this one: Choose a XML node in SQL Server based on max value of a child element
except that my column is NOT of type XML, it's of type nvarchar(max).
I want to extract the XML node values from a column that looks like this:
<Data>
<el1>1234</el1>
<el2>Something</el2>
</Data>
How can I extract the values '1234' and 'Something' ?
doing a convert and using the col.nodes is not working.
CONVERT(XML, table1.col1).value('(/Data/el1)[1]','int') as 'xcol1',
After that, I would like to do a compare value of el1 (1234) with another column, and update update el1 as is. Right now I'm trying to just rebuild the XML when passing the update:
ie
Update table set col1 ='<Data><el1>'+#col2+'</el1><el2>???</el2>
You've got to tell SQL Server the number of the node you're after, like:
(/Data/el1)[1]
^^^
Full example:
declare #t table (id int, col1 varchar(max))
insert #t values (1, '<Data><el1>1234</el1><el2>Something</el2></Data>')
select CAST(col1 as xml).value('(/Data/el1)[1]', 'int')
from #t
-->
1234
SQL Server provides a modify function to change XML columns. But I think you can only use it on columns with the xml type. Here's an example:
declare #q table (id int, col1 xml)
insert #q values (1, '<Data><el1>1234</el1><el2>Something</el2></Data>')
update #q
set col1.modify('replace value of (/Data/el1/text())[1] with "5678"')
select *
from #q
-->
<Data><el1>5678</el1><el2>Something</el2></Data>
At the end of the day, SQL Server's XML support makes simple things very hard. If you value maintainability, you're better off processing XML on the client side.