Change element name with xml.modify or other method in TSQL - sql

Suppose you have a TSQL query:
1 as 'Category1/#Level',
2 as 'Category2/#Level',
t.MainCat as 'Category1',
t.SubCat as 'Category2'
FOR XML PATH (''), ROOT('Taxonomy')
which generates following result:
<Taxonomy>
<Category1 Level="1">Clothing</Category1>
<Category2 Level="2">Jeans</Category2>
</Taxonomy>
The element Category1 and Category2 have unique names for the sql query to generate and convert in to xml format. That's why I used Category1 and Category2. I would like the end result to be like this:
<Taxonomy>
<Category Level="1">Clothing</Category>
<Category Level="2">Jeans</Category>
</Taxonomy>
This could be done using XML EXPLICIT like here, but since my code is much larger it becomes very cluttered and complex quite fast.
With xml.modify you can change values or attributes, but not the element itself.
Is there a way to store the first query result in a #X1 xml variable and then change the element Category1 & Category2 into Category and put it into variable #X2? Something like a search and replace used in text files, but then during the query.

sneaking a null in between elements can do the trick:
Select
1 as 'Category/#Level',
t.MainCat as "Category",
null,
2 as 'Category/#Level',
t.SubCat as "Category"
FOR XML PATH (''), ROOT('Taxonomy')

Also you may use nesting:
select
(select 1 as '#Level', 'Clothing'
for xml path('Category'), type),
(select 2 as '#Level', 'Jeans'
for xml path('Category'), type)
for xml path('Taxonomy');
or values clause to construct list of categories before forming output xml:
select Cat.Level as '#Level', Cat.Value as 'text()'
from (values
(1, 'Clothing')
,(2, 'Jeans')
) Cat(Level, Value)
for xml path('Category'), root('Taxonomy');

Related

Wrapping an xml document loses utf coding

In Sql Server I'm using
select * from [Database].[dbo].[Table] for xml path ('whatever')
which gives me an xml output with, importantly, newline 
 notifiers on any entries that have the new lines.
I need this output wrapped in a few more xml formatting elements but using
select '<?xml version="1.0"?><root><whateverses>'
+ (select * from [Database].[dbo].[Table] for xml path ('whatever'))
+ '</whateverses></root>';
returns just a string, with the newlne notifiers MISSING.
How can I preserve these? How do I wrap my xml in a few extras while keeping the output as an xml?
By trying to wrap the XML in varchar/string you're implicitly converting the XML to varchar/string. If you want to embed the XML in other tags try something like the following:
select *
into [dbo].[Foo]
from (values ('Hello', 'World') ) Src ([Bar], [Baz]);
select *
from [dbo].[Foo]
for xml path ('whatever');
select (
select *
from [dbo].[Foo]
for xml path ('whatever'), type
)
for xml path('whateverses'), root('root');
Which yields the XML results:
<whatever>
<Bar>Hello</Bar>
<Baz>World</Baz>
</whatever>
and:
<root>
<whateverses>
<whatever>
<Bar>Hello</Bar>
<Baz>World</Baz>
</whatever>
</whateverses>
</root>

How to use XML AUTO with SQL to create a structure with 2 nested tables

thank you for reading.
I'm currently trying to use XML AUTO with 3 tables to produce an xml document with 1 top level and 2 different tables at a sub level.
A simpler example with 2 tables:
SELECT
dbo.SalesOrder.SupplementaryReference AS SupplementaryReference,
dbo.SalesOrder.Address AS Address,
dbo.SalesOrder.Address2 AS Address2,
dbo.SalesOrder.CardCode AS CardCode,
dbo.SalesOrder.DocDate AS DocDate,
dbo.SalesOrder.DocDueDate AS DocDueDate,
dbo.SalesOrderLines.ItemCode AS ItemCode,
dbo.SalesOrderLines.Quantity AS Quantity
FROM
dbo.SalesOrderLines,
dbo.SalesOrder
WHERE
dbo.SalesOrderLines.SupplementaryReference = dbo.SalesOrder.SupplementaryReference
FOR XML AUTO, ELEMENTS, ROOT('root')
This produces an output xml document like this:
<root>
<dbo.SalesOrder>
<Address>...</Address>
<Address2>...</Address2>
.....
.....
<dbo.SalesOrderLines>
<ItemCode>...</ItemCode>
<Quantity>...</Quantity>
</dbo.SalesOrderLines>
</dbo.SalesOrder>
</root>
This is what I would expect, however I'm attempting to get an XML document as describe below from 3 tables SalesOrder, SalesOrderLines, and SalesOrderExpenses:
<root>
<dbo.SalesOrder>
<Address>...</Address>
<Address2>...</Address2>
.....
.....
<dbo.SalesOrderLines>
<ItemCode>...</ItemCode>
<Quantity>...</Quantity>
</dbo.SalesOrderLines>
<dbo.SalesOrderExpenses>
<BaseType>...</BaseType>
<ExpnsCode>...</ExpnsCode>
<LineTotal>...</LineTotal>
</dbo.SalesOrderExpenses>
</dbo.SalesOrder>
</root>
I have attempted doing the following query:
SELECT
dbo.SalesOrder.SupplementaryReference AS SupplementaryReference,
dbo.SalesOrder.Address AS Address,
dbo.SalesOrder.Address2 AS Address2,
dbo.SalesOrder.CardCode AS CardCode,
dbo.SalesOrder.DocDate AS DocDate,
dbo.SalesOrder.DocDueDate AS DocDueDate,
dbo.SelesOrderExpenses.BasteType AS BaseType,
dbo.SelesOrderExpenses.ExpnsCode AS ExpnsCode,
dbo.SelesOrderExpenses.LineTotal AS LineTotal,
dbo.SalesOrderLines.ItemCode AS ItemCode,
dbo.SalesOrderLines.Quantity AS Quantity
FROM
dbo.SalesOrderLines,
dbo.SalesOrderExpenses,
dbo.SalesOrder
WHERE
dbo.SalesOrderLines.SupplementaryReference = dbo.SalesOrder.SupplementaryReference
FOR XML AUTO, ELEMENTS, ROOT('root')
however I receive this output which nests the third table inside the second:
<root>
<dbo.SalesOrder>
<Address>...</Address>
<Address2>...</Address2>
.....
.....
<dbo.SalesOrderExpenses>
<BaseType>...</BaseType>
<ExpnsCode>...</ExpnsCode>
<LineTotal>...</LineTotal>
<dbo.SalesOrderLines>
<ItemCode>...</ItemCode>
<Quantity>...</Quantity>
</dbo.SalesOrderLines>
</dbo.SalesOrderExpenses>
</dbo.SalesOrder>
</root>
So any ideas?
Thank you for your time.
Without sample data it's a bit hard to give advice, but you might be looking for something like this:
SELECT so.[Address]
,so.Address2
/*more columns*/
,(
SELECT sol.ItemCode
,sol.Quantity
FROM dbo.SalesOrderLines AS sol
WHERE so.SupplementaryReference =sol.SupplementaryReference
FOR XML PATH('dbo.SalesOrderLines'),TYPE --only one row???
) AS [node()]
,(
SELECT sol.ItemCode
,sol.Quantity
FROM dbo.SalesOrderExpenses AS soe
WHERE so.SupplementaryReference =soe.SupplementaryReference
FOR XML PATH('dbo.SalesOrderLines'),TYPE --only one row???
) AS [node()]
FROM dbo.SalesOrder AS so
Btw: Avoid old fashioned joins where you add table names with comma and set the joining predicate in the WHERE clause. Better use the appropriate JOIN. In this case this an INNER JOIN...

How to generate XML using SQL PATH Mode, with Line Items

I am trying to generate an XML string based on data in two SQL tables. One contains Order Header data, the other one Line Item data.
My problem is that I can't get the Line Item info to appear properly as multiple elements within a single order:
This is the SQL statement:
Select
LTRIM(RTRIM(H.CustPONbr)) As "Transactions/Transaction/CustomerOrdNumber",
(
Select LTRIM(RTRIM(InvtID)) As "data()" From X888_Amazon_Order_Line L1
Where L1.CpnyID = H.CpnyID And L1.CustPONbr = H.CustPONbr
FOR XML PATH (''), ELEMENTS
) As "Transactions/Transaction/LineItems/LineItem/InvtId"
From X888_Amazon_Order_Header H (nolock)
where h.CustPONbr = '99999014'
For XML PATH ('ProcessEngineSubmission'), Root ('XML'), ELEMENTS
This is the result that I get:
<XML>
<Transactions>
<Transaction>
<CustomerOrdNumber>99999014</CustomerOrdNumber>
<LineItems>
<LineItem>
<InvtId>TEST 1235 TEST 1234</InvtId>
</LineItem>
</LineItems>
</Transaction>
</Transactions>
</XML>
If I execute the inner select (replacing "data()" with InvtId), I get what I am trying to achieve:
<InvtId>TEST 1235</InvtId>
<InvtId>TEST 1234</InvtId>
Try it like this:
Nested selects need the ,TYPE extension to come back as XML...
You might need to specify a path in the inner PATH('') or give a name to the column with AS ...
Select LTRIM(RTRIM(H.CustPONbr)) As "Transactions/Transaction/CustomerOrdNumber"
,(
Select LTRIM(RTRIM(InvtID))
From X888_Amazon_Order_Line L1
Where L1.CpnyID = H.CpnyID And L1.CustPONbr = H.CustPONbr
FOR XML PATH (''), TYPE
) As "Transactions/Transaction/LineItems/LineItem/InvtId"
From X888_Amazon_Order_Header H (nolock)
where h.CustPONbr = '99999014'
For XML PATH ('ProcessEngineSubmission'), Root ('XML')

SQL XML column filtered by partial XML

Is it possible to filter a XML column, by matching a partial XML?
Examples:
My XML column for a given record might have:
<element1>
<subelement1>value1</subelement1>
<subelement2>value2</subelement2>
<subelement3>value3</subelement3>
</element1>
And I have the following partial XML to be used as a filter:
<element1>
<subelement2>value2</subelement2>
<subelement3>value3</subelement3>
</element1>
In this case, since both elements and their values match the ones in the record, it should return the record.
However, in the partial XML below, it's not a match, since subelement4 is not present in the XML column for this given record:
<element1>
<subelement2>value2</subelement2>
<subelement4>value4</subelement4>
</element1>
In the same line, what's the recommended way of filtering a XML column using multiple elements? The examples I see are always filtering one element, not multiple.
My current solution is a comma separated value string that I split into a table with 2 columns (element name and value) and cross apply. It works well. I'm just wondering if there is something better out there. I was thinking about sending a partial XML string and, somehow (this question), match this partial XML with the XML column.
I am not completely clear on what you are trying to do. I think the code below gives you some guidance. If you'll clarify your question I can try to be more thorough.
declare #x1 [xml] =N'<element1>
<subelement1>value1</subelement1>
<subelement2>value2</subelement2>
<subelement3>value3</subelement3>
</element1>'
, #x2 [xml] = N'<element1>
<subelement2>value2</subelement2>
<subelement3>value3</subelement3>
</element1>'
, #x3 [xml] = N'<element1>
<subelement2>value2</subelement2>
<subelement4>value4</subelement4>
</element1>';
--
select t.c.query(N'.')
, t.c.value(N'(./text())[1]'
, N'[sysname]')
from #x1.nodes(N'/*/*') as t(c)
where t.c.value(N'(./text())[1]'
, N'[sysname]') = N'value2';
--
select t.c.query(N'.')
, t.c.value(N'(./text())[1]'
, N'[sysname]')
from #x1.nodes(N'/*/*') as t(c)
where t.c.exist(N'//*[local-name()="subelement4"]') = 1;

How do I select a top-level attribute of an XML column in SQL Server?

I have an XML column in SQL Server that is the equivalent of:
<Test foo="bar">
<Otherstuff baz="belch" />
</Test>
I want to get the value of the foo attribute of Test (the root element) as a varchar. My goal would be something along the lines of:
SELECT CAST('<Test foo="bar"><Otherstuff baz="belch" /></Test>' AS xml).value('#foo', 'varchar(20)') AS Foo
But when I run the above query, I get the following error:
Msg 2390, Level 16, State 1, Line 1
XQuery [value()]: Top-level attribute
nodes are not supported
John Saunders has it almost right :-)
declare #Data XML
set #Data = '<Test foo="bar"><Otherstuff baz="belch" /></Test>'
select #Data.value('(/Test/#foo)[1]','varchar(20)') as Foo
This works for me (SQL Server 2005 and 2008)
Marc
If you dont know the root element:
select #Data.value('(/*/#foo)[1]','varchar(20)') as Foo
Why does .value('#foo', 'varchar(20)') generate the error “Top-level attribute nodes are not supported”?
When you query the xml data type, the context is the document node, which is an implicit node that contains the root element(s) of your XML document. The document node has no name and no attributes.
How can I get the value of an attribute on the root element?
In your XQuery expression, include the path to the first root element:
DECLARE #Data xml = '<Customer ID="123"><Order ID="ABC" /></Customer>'
SELECT #Data.value('Customer[1]/#ID', 'varchar(20)')
-- Result: 123
If you don’t know (or don’t want to specify) the name of the root element, then just use * to match any element:
SELECT #Data.value('*[1]/#ID', 'varchar(20)')
-- Result: 123
Because the query context is the document node, you don’t need to prefix the XQuery expression with a forward slash (as the other answers unnecessarily do).
Why do I have to include [1]?
The XQuery expression you pass to value() must be guaranteed to return a singleton. The expression Customer/#ID doesn’t satisfy this requirement because it matches both ID="123" and ID="456" in the following example:
DECLARE #Data xml = '<Customer ID="123" /><Customer ID="456" />'
Remember that the xml data type represents an XML document fragment, not an XML document, so it can contain multiple root elements.
What’s the difference between Customer[1]/#ID and (Customer/#ID)[1]?
The expression Customer[1]/#ID retrieves the ID attribute of the first <Customer> element.
The expression (Customer/#ID)[1] retrieves the ID attribute of all <Customer> elements, and from this list of attributes, picks the first.
The following example demonstrates the difference:
DECLARE #Data xml = '<Customer /><Customer ID="123" /><Customer ID="456" />'
SELECT #Data.value('Customer[1]/#ID', 'varchar(20)')
-- Result: NULL (because the first Customer element doesn't have an ID attribute)
SELECT #Data.value('(Customer/#ID)[1]', 'varchar(20)')
-- Result: 123