XQuery wont return a document - sql

I just started learning about XQuery and am trying to play with it a little.
Now, iv'e built what i think is a simple query and a correct one (a much as i can see) but SQL Manger
prints me the error:
XQuery [query()]: "=" was expected.
I looked back at the code but i cant understand how to fix it..
here is my code:
DECLARE #x AS XML = N'
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="web" cover="paperback">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>';
SELECT #x.query('for $i in bookstore/book
let $a := $i/author
let $t := $i/title
let $p := $i/price
where $i/#price < 40
return
<This is a test>
<author>$a</author>
<title>$t</title>
<price>$p</price>
</This is a test>')
AS [I hope this works]
Thanks in advance for your help :)

This is not a valid xml element:
<This is a test>
changing this to
<ThisIsATest>
will fix your query but after the fix, as-is, your query doesn't return anything. My T-SQL XQuery skills are rusty but you can get what you're trying to do using nodes and the values method.
This:
SELECT
author = bs.book.value('(author/text())[1]', 'varchar(8000)'),
title = bs.book.value('(title/text())[1]', 'varchar(8000)'),
price = bs.book.value('(price/text())[1]', 'varchar(8000)')
FROM (VALUES(#x)) AS ml(x)
CROSS APPLY ml.x.nodes('bookstore/book') AS bs(book);
Returns:
author title price
--------------------- --------------------- ----------
Giada De Laurentiis Everyday Italian 30.00
J K. Rowling Harry Potter 29.99
James McGovern XQuery Kick Start 49.99
Erik T. Ray Learning XML 39.95
This (which includes your sample XML):
DECLARE #x AS XML = N'
<bookstore>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
<book category="web" cover="paperback">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>';
SELECT
author = bs.book.value('(author/text())[1]', 'varchar(8000)'),
title = bs.book.value('(title/text())[1]', 'varchar(8000)'),
price = bs.book.value('(price/text())[1]', 'varchar(8000)')
FROM (VALUES(#x)) AS ml(x)
CROSS APPLY ml.x.nodes('bookstore/book') AS bs(book)
FOR XML PATH('Book');
Returns:
<Book>
<author>Giada De Laurentiis</author>
<title>Everyday Italian</title>
<price>30.00</price>
</Book>
<Book>
<author>J K. Rowling</author>
<title>Harry Potter</title>
<price>29.99</price>
</Book>
<Book>
<author>James McGovern</author>
<title>XQuery Kick Start</title>
<price>49.99</price>
</Book>
<Book>
<author>Erik T. Ray</author>
<title>Learning XML</title>
<price>39.95</price>
</Book>

You were close, but there are three things to mention:
As Alan Burstein has told you, the tag <This is a test> is not valid. The message you get, is because the engine thinks of an element <This> and expects the is to be the first attribute, which should be followed by = like in <This is="some value">.
Your where tries to query a #price attribute
In order to resolve the values you need {} around your variables.
Try this:
SELECT #x.query('for $i in bookstore/book
let $a := $i/author
let $t := $i/title
let $p := $i/price
where $p < 40
return
<This-is-a-test>
<author>{$a}</author>
<title>{$t}</title>
<price>{$p}</price>
</This-is-a-test>')
AS [I hope this works]

Related

Create Smaller XML based on value of element

On Python 3.7, I am looking to create a subset of a XML. For example, the larger XML is:
<data>
<student>
<result>
<grade>A</grade>
</result>
<details>
<name>John</name>
<id>100</id>
<age>16</age>
<email>john#mail.com</email>
</details>
</student>
<student>
<result>
<grade>B</grade>
</result>
<details>
<name>Alice</name>
<id>101</id>
<age>17</age>
<email>alice#mail.com</email>
</details>
</student>
<student>
<result>
<grade>F</grade>
</result>
<details>
<name>Bob</name>
<id>102</id>
<age>16</age>
<email>bob#mail.com</email>
</details>
</student>
<student>
<result>
<grade>A</grade>
</result>
<details>
<name>Hannah</name>
<id>103</id>
<age>17</age>
<email>hannah#mail.com</email>
</details>
</student>
</data>
and am looking for a new XML like below, the condition to create a smaller subset depends on a list of ids in this case 101 and 102. All other student blocks will be deleted.
<data>
<student>
<result>
<grade>B</grade>
</result>
<details>
<name>Alice</name>
<id>101</id>
<age>17</age>
<email>alice#mail.com</email>
</details>
</student>
<student>
<result>
<grade>F</grade>
</result>
<details>
<name>Bob</name>
<id>102</id>
<age>16</age>
<email>bob#mail.com</email>
</details>
</student>
</data>
i.e. The output XML will depend on a list of id's, in this case ['101',102']
This is what I tried:
import lxml.etree
#Original Large XML
tree = etree.parse(open('students.xml'))
root = tree.getroot()
results = root.findall('student')
textnumbers = [r.find('details/id').text for r in results]
print(textnumbers)
required_ids = ['101','102']
wanted = tree.xpath("//student/details/[not(#id in required_ids)]")
for node in unwanted:
node.getparent().remove(node)
#New Smaller XML
tree.write(open('student_output.xml', 'wb'))
But I am getting an expected error of "Invalid expression" for
wanted = tree.xpath("//student/details/[not(#id in required_ids)]")
I know it's a read, but i am fairly new to Python, thanks in advance for your help.
I think you can do it like this:
from lxml import etree as ET
required_ids = ['101','102']
for event, element in ET.iterparse('students.xml'):
if element.tag == 'student' and not(element.xpath('.//id/text()')[0] in required_ids):
element.clear()
element.getparent().remove(element)
if element.tag == 'data':
ET.dump(element)
Instead of the dump you would of course want to write to a file, that is use
if element.tag == 'data':
tree = ET.ElementTree(element)
tree.write('student_output.xml')
Your attempt fails as you can't simply use a Python list variable in XPath and in is not an XPath 1.0 operator.

Find element or attribute value anywhere in XML

I am trying to find the value of an element / attribute regardless of where it exists in the XML.
XML:
<?xml version="1.0" encoding="UTF-8"?>
<cXML payloadID="12345677-12345567" timestamp="2017-07-26T09:11:05">
<Header>
<From>
<Credential domain="1212">
<Identity>01235 </Identity>
<SharedSecret/>
</Credential>
</From>
<To>
<Credential domain="1212">
<Identity>01234</Identity>
</Credential>
</To>
<Sender>
<UserAgent/>
<Credential domain="8989">
<Identity>10678</Identity>
<SharedSecret>Testing123</SharedSecret>
</Credential>
</Sender>
</Header>
<Request deploymentMode="Prod">
<ConfirmationRequest>
<ConfirmationHeader noticeDate="2017-07-26T09:11:05" operation="update" type="detail">
<Total>
<Money>0.00</Money>
</Total>
<Shipping>
<Description>Delivery</Description>
</Shipping>
<Comments>WO# generated</Comments>
</ConfirmationHeader>
<OrderReference orderDate="2017-07-25T15:22:11" orderID="123456780000">
<DocumentReference payloadID="5678-4567"/>
</OrderReference>
<ConfirmationItem quantity="1" lineNumber="1">
<ConfirmationStatus quantity="1" type="detail">
<ItemIn quantity="1">
<ItemID>
<SupplierPartID>R954-89</SupplierPartID>
</ItemID>
<ItemDetail>
<UnitPrice>
<Money currency="USD">0.00</Money>
</UnitPrice>
<Description>Test Descritpion 1</Description>
<UnitOfMeasure>QT</UnitOfMeasure>
</ItemDetail>
</ItemIn>
</ConfirmationStatus>
</ConfirmationItem>
<ConfirmationItem quantity="1" lineNumber="2">
<ConfirmationStatus quantity="1" type="detail">
<ItemIn quantity="1">
<ItemID>
<SupplierPartID>Y954-89</SupplierPartID>
</ItemID>
<ItemDetail>
<UnitPrice>
<Money currency="USD">0.00</Money>
</UnitPrice>
<Description>Test Descritpion 2</Description>
<UnitOfMeasure>QT</UnitOfMeasure>
</ItemDetail>
</ItemIn>
</ConfirmationStatus>
</ConfirmationItem>
</ConfirmationRequest>
</Request>
</cXML>
I want to get the value of the payloadID on the DocumentReference element. This is what I have tried so far:
BEGIN
Declare #Xml xml
Set #Xml = ('..The XML From Above..' as xml)
END
--no value comes back
Select c.value('(/*/DocumentReference/#payloadID)[0]','nvarchar(max)') from #Xml.nodes('//cXML') x(c)
--no value comes back
Select c.value('#payloadID','nvarchar(max)') from #Xml.nodes('/cXML/*/DocumentReference') x(c)
--check if element exists and it does
Select #Xml.exist('//DocumentReference');
I tried this in an xPath editor: //DocumentReference/#payloadID
This does work, but I am not sure what the equivalent syntax is in SQL
Calling .nodes() (like suggested in comment) is an unecessary overhead...
Better try it like this:
SELECT #XML.value('(//DocumentReference/#payloadID)[1]','nvarchar(max)')
And be aware, that XPath starts counting at 1. Your example with [0] cannot work...
--no value comes back
Select c.value('(/*/DocumentReference/#payloadID)[0]','nvarchar(max)') from...

Conditional xml in sql

I only want to show the categoryid node and value if its not an empty guid. I've tried different formats but nothing worked. It keeps just printing the if statement as a string in the results. I read that you had to have an else condition as part of your code, can I just put an empty string there? I've looked through a few articles online about this and the msdn reference with no luck. For some reason it's evaluating my if statement.
declare #XML xml = N'
<books>
<book>
<title>Book 1</title>
<categoryid>00000000-0000-0000-0000-000000000000</categoryid>
<author>Chris</author>
<price>10</price>
</book>
<book>
<title>Book 2</title>
<categoryid>DF3D696D-B7A3-44A2-BC7D-1D45745C979B</categoryid>
<author>Spencer</author>
<price>20</price>
</book>
</books>';
select #XML.query(
'<books>
{
for $b in /books/book
return
<book>
{$b/title}
{$b/price}
if(not($b/categoryid = 00000000-0000-0000-0000-000000000000))
then {$b/categoryid}
else
</book>
}
</books>'
);
I think there were two issues.
1 - The conditional block needed to be enclosed in curly braces before the "if" and after the "else" ended.
2 - The empty guid value needed to be its own variable - I think within the condition it was treating it as a string.
I also had trouble getting the "not" operator to behave as expected, so I just reversed the condition to bypass that problem.
declare #XML xml = N'
<books>
<book>
<title>Book 1</title>
<categoryid>00000000-0000-0000-0000-000000000000</categoryid>
<author>Chris</author>
<price>10</price>
</book>
<book>
<title>Book 2</title>
<categoryid>DF3D696D-B7A3-44A2-BC7D-1D45745C979B</categoryid>
<author>Spencer</author>
<price>20</price>
</book>
</books>';
declare #emptyguid uniqueidentifier = '00000000-0000-0000-0000-000000000000'
select #XML.query('
<books>
{
for $b in /books/book
return
<book>
{$b/title}
{$b/price}
{
if($b/categoryid = sql:variable("#emptyguid")) then
()
else (
$b/categoryid
)
}
</book>
}
</books>
');

VTD XML navigating to child nodes which are siblings to first child

My XML
<catalog>
<item Id="1">
<book id="b1">
</book>
<cd id="c1">
</cd>
</item>
<item Id="11">
<book id="b11">
</book>
<cd id="c11">
</cd>
</item>
</catalog>
what I have ..
ap.selectXPath("/catalog/item");
while ((result = ap.evalXPath()) != -1) {
if (vn.toElement(VTDNav.FIRST_CHILD, "item")) {
do {
//do something with the contnets in the item node
} while (vn.toElement(VTDNav.NEXT_SIBLING, "book"));
}
// move to the parent node to traverse the rest of the items
}
What I want is to get to the "cd" node.
In some of the examples I saw VTDNav.NEXT_CHILD but seems like is not available . Can anyone suggest how to get to the node I need to . For now I am managing to do it by getting to the FIRST_CHILD and then moving to the next sibling
if (vn.toElement(VTDNav.FIRST_CHILD, "book")) {
// Move to the next sibling
vn.toElement(VTDNav.NEXT_SIBLING, "cd");
}
Appreciate all your help
Regards
You should not need to have next child. What you look for is probably NEXT_SIBLING, after you call FIRST_CHILD...

Format / Indent XML code while sending email using sp_send_dbmail [formatting xml code in mail body]

I am trying to send a dbmail with XML code as a body, see my code below showing the same,
declare #xml xml = '<?xml version="1.0"?>
<Order>
<Date>2003/07/04</Date>
<CustomerId>123</CustomerId>
<CustomerName>Acme Alpha</CustomerName>
<Item>
<ItemId> 987</ItemId>
<ItemName>Coupler</ItemName>
<Quantity>5</Quantity>
</Item>
<Item>
<ItemId>654</ItemId>
<ItemName>Connector</ItemName>
<Quantity unit="12">3</Quantity>
</Item>
<Item>
<ItemId>579</ItemId>
<ItemName>Clasp</ItemName>
<Quantity>1</Quantity>
</Item>
</Order>'
--SELECT #xml
declare #bodyprep varchar(max)
select #bodyprep = cast(#xml as varchar(max))
EXEC msdb. dbo.sp_send_dbmail
#profile_name='Profile-A' ,
#recipients ='.com',
#from_address = '.com' ,
#subject = 'test',
#body = #bodyprep
but the mail i receive is unaligned like shown below,
<Order><Date>2003/07/04</Date><CustomerId>123</CustomerId><CustomerName>Acme Alpha</CustomerName><Item><ItemId> 987</ItemId><ItemName>Coupler</ItemName><Quantity>5</Quantity></Item><Item><ItemId>654</ItemId><ItemName>Connector</ItemName><Quantity unit="12">3</Quantity></Item><Item><ItemId>579</ItemId><ItemName>Clasp</ItemName><Quantity>1</Quantity></Item></Order>
Is there a way to format or indent the XML code above ?
I would like to send a formatted code as shown below,
<Order>
<Date>2003/07/04</Date>
<CustomerId>123</CustomerId>
<CustomerName>Acme Alpha</CustomerName>
<Item>
<ItemId> 987</ItemId>
<ItemName>Coupler</ItemName>
<Quantity>5</Quantity>
</Item>
<Item>
<ItemId>654</ItemId>
<ItemName>Connector</ItemName>
<Quantity unit="12">3</Quantity>
</Item>
<Item>
<ItemId>579</ItemId>
<ItemName>Clasp</ItemName>
<Quantity>1</Quantity>
</Item>
</Order>
Thanks
msdb.dbo.sp_send_dbmail has an additional parameter, #body_format = 'HTML', which would allow you to display the XML as you like, once you'd decorated it with the proper HTML markup. That's the only way you can control the appearance of the email body to the level you've specified, i.e. with proper indentation and color-coding.
If you can do without the color-coding, #body_format = 'TEXT' (the default) is fine, but either way you have some work on your hands to get it indented as you indicate.