TSQL Format Query Output - sql

I have a stored procedure that is generating an XML output for me.
The SP looks like this:
ALTER PROCEDURE [dbo].[InternFetchProf]
#positionID INT=''
AS
BEGIN
SET NOCOUNT ON;
BEGIN
SELECT A.[id],
A.[positionTitle],
(SELECT B.[id],
B.[question],
B.[order],
(SELECT D.[optionName],
D.[order]
FROM internshipProfOptions AS D
WHERE questionID = B.[id]
ORDER BY D.[order] ASC
FOR XML PATH ('optionsBlock'), TYPE, ELEMENTS, ROOT ('options'))
FROM internshipProfQuestions AS B
WHERE positionID = A.[id]
ORDER BY B.[order] ASC
FOR XML PATH ('questionBlock'), TYPE, ELEMENTS, ROOT ('questions'))
FROM internships AS a
WHERE A.[id] = #positionID
ORDER BY closeDate ASC
FOR XML PATH ('positions'), TYPE, ELEMENTS, ROOT ('root');
END
END
This gives me the XML output of
<root>
<positions>
<id>1</id>
<positionTitle>APS Team</positionTitle>
<questions>
<questionBlock>
<id>1</id>
<question>Whats your fav color</question>
<order>1</order>
<options>
<optionsBlock>
<optionName>VBA</optionName>
<order>1</order>
</optionsBlock>
<optionsBlock>
<optionName>JavaScript</optionName>
<order>2</order>
</optionsBlock>
<optionsBlock>
<optionName>HTML</optionName>
<order>3</order>
</optionsBlock>
</options>
</questionBlock>
<questionBlock>
<id>2</id>
<question>Whos your daddy?</question>
<order>2</order>
<options>
<optionsBlock>
<optionName>PHP</optionName>
<order>1</order>
</optionsBlock>
<optionsBlock>
<optionName>Perl</optionName>
<order>2</order>
</optionsBlock>
</options>
</questionBlock>
</questions>
</positions>
</root>
I have an additional SELECT statement here:
SELECT C.[groupName],
C.[order],
FROM internshipProfGroups AS C
WHERE questionID = B.[id]
FOR XML PATH ('groupBlock'), TYPE, ELEMENTS, ROOT ('groups')
Which I am trying to get to generate the XML in the root of "QuestionBlock" like so:
<questionBlock>
...
<groups>
<groupBlock>
<groupName>Testing</groupName>
</groupBlock>
<groupBlock>
<groupName>Another Test</groupName>
</groupBlock>
</groups>
...
</questionBlock>
I am struggling to get the statement in the right area to produce this. Any help with what I may be missing?

IF I understood well you need to insert the second query in the same level that "SELECT D.[OptionName]" (also I think you have some mistakes in the code):
SELECT
A.[id],
A.[positionTitle],
(SELECT B.[id],
B.[question],
B.[ord],
(SELECT D.[optionName],
D.[ord]
FROM internshipProfOptions AS D
WHERE questionID = B.[id]
ORDER BY D.[ord] ASC
FOR XML PATH ('optionsBlock'), TYPE, ELEMENTS, ROOT ('options')),
(SELECT C.[groupName],
C.[ord]
FROM internshipProfGroups AS C
WHERE questionID = B.[id]
FOR XML PATH ('groupBlock'), TYPE, ELEMENTS, ROOT ('groups'))
FROM internshipProfQuestions AS B
WHERE positionID = A.[id]
ORDER BY B.[ord] ASC
FOR XML PATH ('questionBlock'), TYPE, ELEMENTS, ROOT ('questions'))
FROM internships A
WHERE A.[id] = 3
ORDER BY closeDate ASC
FOR XML PATH ('positions'), TYPE, ELEMENTS, ROOT ('root');
WORKING SAMPLE
That gives this result:
<?xml version="1.0"?>
<root>
<positions>
<id>3</id>
<positionTitle>Trimepost</positionTitle>
<questions>
<questionBlock>
<id>2</id>
<question>What?</question>
<ord>1</ord>
<options>
<optionsBlock>
<optionName>Opt 2</optionName>
<ord>3</ord>
</optionsBlock>
</options>
<groups>
<groupBlock>
<groupName>ALPHA</groupName>
<ord>1</ord>
</groupBlock>
<groupBlock>
<groupName>BETA</groupName>
<ord>2</ord>
</groupBlock>
</groups>
</questionBlock>
</questions>
</positions>
</root>

Related

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

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>

How SQL sets the value and attributes of the same node in XML

We have this code:
SELECT
(SELECT
'infor' AS [Process/TenantID],
(SELECT
ROW_NUMBER() OVER(ORDER BY gld_serial_no ASC) as [#sequence],
(SELECT TOP (1)
'1' AS [DimensionCode/#sequence],
gld_dim1 AS [DimensionCode]
FROM
slr_gl_dta_paid
GROUP BY
gld_dim1
FOR XML PATH ('DimensionCode'), TYPE) AS [DimensionCodes]
FROM
slr_gl_dta_paid
FOR XML PATH ('JournalEntryLine'), TYPE) AS [SourceSystemJournalEntry]
FOR XML PATH ('DataArea'), type
)
FOR XML PATH (''), ROOT('ProcessSourceSystemJournalEntry')
The result is like this:
<ProcessSourceSystemJournalEntry>
<DataArea>
<Process>
<TenantID>infor</TenantID>
</Process>
<SourceSystemJournalEntry>
<JournalEntryLine sequence="1">
<DimensionCodes>
<DimensionCode>
<DimensionCode sequence="1">CD53</DimensionCode>
</DimensionCode>
</DimensionCodes>
</JournalEntryLine>
<JournalEntryLine sequence="2">
<DimensionCodes>
<DimensionCode>
<DimensionCode sequence="1">CD99</DimensionCode>
</DimensionCode>
</DimensionCodes>
</JournalEntryLine>
</SourceSystemJournalEntry>
</DataArea>
</ProcessSourceSystemJournalEntry>
But we need the result to be like this:
<ProcessSourceSystemJournalEntry>
<DataArea>
<Process>
<TenantID>infor</TenantID>
</Process>
<SourceSystemJournalEntry>
<JournalEntryLine sequence="1">
<DimensionCodes>
<DimensionCode sequence="1">CD53</DimensionCode>
</DimensionCodes>
</JournalEntryLine>
<JournalEntryLine sequence="2">
<DimensionCodes>
<DimensionCode sequence="1">CD99</DimensionCode>
</DimensionCodes>
</JournalEntryLine>
</SourceSystemJournalEntry>
</DataArea>
</ProcessSourceSystemJournalEntry>
How can this be done?
Try this:
SELECT
1 AS 'DimensionCode/#sequence',
gld_dim1 AS DimensionCode
FROM
gl_dta
GROUP BY
gld_dim1
FOR XML PATH('DimensionCodes'), ROOT('JournalEntry')
This should create a <DimensionCode> element that contains the gld_dim1 value as element text, and a sequence attribute on it.

How to get element values from an XML column?

I have the XML below in a column. I need to get to \Report\Criterias\Criteria (where name="Advertisers")\Elements\Element(where name="ListViewAvailable"). From here I need to list all the numbers that are in the Value element.
So far I got:
SELECT xmlColumn.query('/Report/Criterias/Criteria/Elements/Element')
from tbl
but no idea how to filter.
<Report>
<Criterias>
<Criteria name="Date Range">
...
</Criteria>
<Criteria name="Advertisers">
<Elements>
<Element name="CheckBoxOne">
<Value>0</Value>
</Element>
<Element name="ListViewAvailable">
<Value>314</Value>
<Value>57</Value>
<Value>18886</Value>
<Value>7437</Value>
</Element>
</Elements>
</Criteria>
<Criteria name="Revenue Types">
...
</Criteria>
</Criterias>
</Report>
You can filter using predicate ([]) in combination with CROSS APPLY to shred the XML on Value elements level :
SELECT C.value('.', 'int') AS Value
FROM tbl t
CROSS APPLY t.xmlColumn.nodes('
/Report/Criterias/Criteria[#name="Advertisers"]
/Elements/Element[#name="ListViewAvailable"]
/Value
') T(C)

TSQL XML Parsing and creating xml

I have a tool which I now will be creating reports for using the data I have. I am currently working on a year to date report and need to pull the numbers for that.
My goal is to have an XML output of each of the months in the current year with their totals.
Here is what the XML currently looks like with my select statement:
<root>
<data>
<classXML>
<courses>
<class>
<classTitle>Arts and Crafts</classTitle>
<tuitionCost>100</tuitionCost>
<bookCost>30</bookCost>
<classTotal>130</classTotal>
</class>
<class>
<classTitle>Paper 101</classTitle>
<tuitionCost>320</tuitionCost>
<bookCost>211</bookCost>
<classTotal>531</classTotal>
</class>
<class>
<classTitle>Introduction to Pencils</classTitle>
<tuitionCost>210</tuitionCost>
<bookCost>291</bookCost>
<classTotal>501</classTotal>
</class>
<class>
<classTitle>Intermediate Folding</classTitle>
<tuitionCost>110</tuitionCost>
<bookCost>22</bookCost>
<classTotal>132</classTotal>
</class>
<class>
<classTitle>Advanced Jumprope</classTitle>
<tuitionCost>11</tuitionCost>
<bookCost>22</bookCost>
<classTotal>33</classTotal>
</class>
<grandTotal>1327</grandTotal>
</courses>
</classXML>
<reimbursementDate>08/01/2014</reimbursementDate>
</data>
<data>
<classXML>
<courses>
<class>
<classTitle>dsfgfdsg</classTitle>
<tuitionCost>44</tuitionCost>
<bookCost>44</bookCost>
<classTotal>88</classTotal>
</class>
<grandTotal>88</grandTotal>
</courses>
</classXML>
<reimbursementDate>05/31/2014</reimbursementDate>
</data>
</root>
And my stored procedure:
SELECT
A.[classXML],
CONVERT(VARCHAR(10), A.[reimbursementDate], 101) as reimbursementDate
FROM
tuitionSubmissions as A
WHERE
A.[status] = 'Approved'
AND YEAR(A.[reimbursementDate]) = YEAR(GETDATE())
FOR XML PATH ('data'), TYPE, ELEMENTS, ROOT ('root');
As you can see, the column classXML stores that data in XML format with all of the classes they are enrolled in with their costs.
So I need to loop over the XML and create an output that is just numbers to assist with my reporting.
Here is my desired outcome:
<results>
<dataSet>
<month>8</month>
<year>2014</year>
<tuitionTotal>500</tuitionTotal>
<booksTotal>200</booksTotal>
<grandTotal>700</grandTotal>
</dataSet>
<dataSet>
<month>9</month>
<year>2014</year>
<tuitionTotal>100</tuitionTotal>
<booksTotal>500</booksTotal>
<grandTotal>600</grandTotal>
</dataSet>
</results>
You can use sum Function (XQuery) to do the aggregation against your XML column.
I put the query against the XML in a cross apply so you don't have to do the same XQuery twice just to calculate grandTotal.
You should also change your predicate against reimbursementDate so it may use and index to find the rows.
select datepart(month, T.reimbursementDate) as month,
datepart(year, T.reimbursementDate) as year,
S.tuitionTotal,
S.booksTotal,
S.tuitionTotal + S.booksTotal as grandTotal
from dbo.tuitionSubmissions as T
cross apply (
select T.classXML.value('sum(/courses/class/tuitionCost/text())', 'int') as tuitionTotal,
T.classXML.value('sum(/courses/class/bookCost/text())', 'int') as booksTotal
) as S
where T.status = 'Approved' and
T.reimbursementDate >= '20140101' and
T.reimbursementDate < '20150101'
for xml path('dataSet'), root('results'), type
SQL Fiddle
DECLARE #DocH INT
DECLARE #DOC XML = '
<root>
<data>
<classXML>
<courses>
<class>
<classTitle>Arts and Crafts</classTitle>
<tuitionCost>100</tuitionCost>
<bookCost>30</bookCost>
<classTotal>130</classTotal>
</class>
<class>
<classTitle>Paper 101</classTitle>
<tuitionCost>320</tuitionCost>
<bookCost>211</bookCost>
<classTotal>531</classTotal>
</class>
<class>
<classTitle>Introduction to Pencils</classTitle>
<tuitionCost>210</tuitionCost>
<bookCost>291</bookCost>
<classTotal>501</classTotal>
</class>
<class>
<classTitle>Intermediate Folding</classTitle>
<tuitionCost>110</tuitionCost>
<bookCost>22</bookCost>
<classTotal>132</classTotal>
</class>
<class>
<classTitle>Advanced Jumprope</classTitle>
<tuitionCost>11</tuitionCost>
<bookCost>22</bookCost>
<classTotal>33</classTotal>
</class>
<grandTotal>1327</grandTotal>
</courses>
</classXML>
<reimbursementDate>08/01/2014</reimbursementDate>
</data>
<data>
<classXML>
<courses>
<class>
<classTitle>dsfgfdsg</classTitle>
<tuitionCost>44</tuitionCost>
<bookCost>44</bookCost>
<classTotal>88</classTotal>
</class>
<grandTotal>88</grandTotal>
</courses>
</classXML>
<reimbursementDate>05/31/2014</reimbursementDate>
</data>
</root>'
EXEC sp_xml_preparedocument #DocH OUTPUT, #DOC
SELECT
MONTH(reimbursementDate) AS month
, YEAR(reimbursementDate) AS year
, SUM(tuitionCost) AS tuitionTotal, SUM(bookCost) AS bookTotal, SUM(tuitionCost+bookCost) AS grandTotal
FROM OPENXML(#DocH,'/root/data/classXML/courses/class') WITH (
classTitle varchar(40) 'classTitle'
, tuitionCost INT 'tuitionCost'
, bookCost INT 'bookCost'
, reimbursementDate date '../../../reimbursementDate'
)
GROUP BY MONTH(reimbursementDate)
, YEAR(reimbursementDate)
FOR XML PATH ('dataset')
EXEC sp_xml_removedocument #DocH;

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>