'local-name()' requires a singleton (or empty sequence) T-SQL Xquery - sql

This is a variation/combination of these two questions, but I think I've done as they described:
Getting element's name in XPATH
XQuery [value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'
Is there a way to get the name of a node in a SQL Server 2016 T-SQL query?
I want to do this:
select
trcid,
trcXML.value(
'local-name(//*[local-name()="after"][1]/*[1])',
'varchar(32)'
) as XmlTableName,
trcXml,
trcGUID,
trcCorrID
from trace
order by trcId desc
I have explicitly included the [1] after each item in the hierarchy.
Error: XQuery [trace.trcXML.value()]: 'local-name()' requires a singleton (or empty sequence), found operand of type 'element(*,xdt:untyped) *'
Sample data:
<s1:DB2Request xmlns:s1="XYZDB2">
<s1:sync>
<s1:after identityInsert="false">
<s1:PG204AT5>
<s1:ISA06>1975111</s1:ISA06>
etc...
</s1:PG204AT5>
</s1:after>
</s1:sync>
</s1:DB2Request>
I want the table name (this is a BizTalk "update-gram") which is between the "after" and the ISA06 elements, in this case: PG204AT5.
Tested in xpathtester.com: local-name(//*[local-name()="after"][1]/* [1])
- for some reason when I save it the XPATH gets changed: http://www.xpathtester.com/xpath/2bd602d8fc7aee0484de14bf93f71ef2

One more set of parenthesis gives you a singleton as required:
DECLARE #t TABLE(s XML);
INSERT INTO #t(s)VALUES(N'<s1:DB2Request xmlns:s1="XYZDB2">
<s1:sync>
<s1:after identityInsert="false">
<s1:PG204AT5>
<s1:ISA06>1975111</s1:ISA06>
<somethingsomething/>
</s1:PG204AT5>
</s1:after>
</s1:sync>
</s1:DB2Request>');
select
s.value(
'local-name((//*[local-name()="after"][1]/*)[1])',
'varchar(32)'
) as XmlTableName
from #t;
Results in:
XmlTableName
PG204AT5

Remember that the SQL Server engine, unlike most XQuery implementations, does "pessimistic static typing" - it assumes that if things can go wrong, they will go wrong.
The expression //*[local-name()="after"][1]/* [1] is capable of returning more than one element - for example it will do so with this input
<x>
<y>
<after>
<z/>
</after>
</y>
<y>
<after>
<z/>
</after>
</y>
</x>
so the XQuery compiler infers a static type of element()*, which doesn't meet the requirement.
It's probably enough to simply do (//*[local-name()="after"]/*)[1].
Personally, I always thought pessimistic static typing was a really poor design choice, but some of the early XQuery implementors including Microsoft were very keen on it.

Or even simpler solution by asking explicitly first child node name of the <s1:after> element.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE(xmldata XML);
INSERT INTO #tbl(xmldata)
VALUES(N'<s1:DB2Request xmlns:s1="XYZDB2">
<s1:sync>
<s1:after identityInsert="false">
<s1:PG204AT5>
<s1:ISA06>1975111</s1:ISA06>
<somethingsomething/>
</s1:PG204AT5>
</s1:after>
</s1:sync>
</s1:DB2Request>');
-- DDL and sample data population, end
;WITH XMLNAMESPACES (DEFAULT 'XYZDB2')
SELECT xmldata.value('local-name((/DB2Request/sync/after/child::node())[1])','VARCHAR(50)') as XmlTableName
FROM #tbl;
Output
+--------------+
| XmlTableName |
+--------------+
| PG204AT5 |
+--------------+

Related

Parse XML data from a column in SQL Server

I'm trying to put together a report for a badge program. Some the data for custom fields we created are stored in a single column in the table as XML. I need to return a couple of the items in there to the report and am having a hard time getting the proper syntax to get it to parse out.
Aside from the XML, the query itself is simple:
SELECT
Person1.firstName
,Person1.lastName
,Person1.idNumber
,Person1.idNumber2
,Person1.idNumber3
,Person1.status
,Person1.customdata
FROM
Person1
the field "customdata" is the XML field that I need to pull Title, and 2 different dates out of. This is what the XML looks like:
<person1_7:CustomData xmlns:person1_7="http://www.badgepass.com/Person1_7">
<Title>IT Director</Title>
<Gaming_x0020_Level>Level 1</Gaming_x0020_Level>
<Gaming_x0020_Issue_x0020_Date>2021-02-18T12:00:00Z</Gaming_x0020_Issue_x0020_Date>
<Gaming_x0020_Expire_x0020_Date>2022-02-18T12:00:00Z</Gaming_x0020_Expire_x0020_Date>
<Betting_x0020_Level>Level 1</Betting_x0020_Level>
<Betting_x0020_Issue_x0020_Date>2021-02-18T12:00:00Z</Betting_x0020_Issue_x0020_Date>
<Betting_x0020_Expire_x0020_Date>2022-02-18T12:00:00Z</Betting_x0020_Expire_x0020_Date>
<BadgeType>Dual Employee</BadgeType>
<Gaming_x0020_Status>TEMP</Gaming_x0020_Status>
<Betting_x0020_Status>TEMP</Betting_x0020_Status>
</person1_7:CustomData>
I have tried a couple of different methods trying to follow the advice from How to query for Xml values and attributes from table in SQL Server? and then tried declaring a XML namespace with the following query:
WITH XMLNAMESPACES ('http://www.badgepass.com/Person1_7' as X)
SELECT
Person1.firstName
,Person1.lastName
,Person1.idNumber
,Person1.idNumber2
,Person1.idNumber3
,Person1.status
,Person1.customdata.value('(/X:person1_7:customdata/X:Title)[1]', 'varchar(100)') AS Title
FROM
Person1
So far all of my results keep returning "XQuery [Person1.customData.value()]: ")" was expected.
" I'm assuming I have a syntax issue that I'm overlooking as I've never had to manipulate XML with SQL before. Thank you in advance for any help.
Please try the following solution.
Notable points:
XQuery .nodes() method establishes a context so you can access any
XML element right away without long XPath expressions.
Use of the text() in the XPath expressions is for performance
reasons.
SQL
-- DDL and sample data population, start
DECLARE #person1 TABLE (firstname varchar(50), customdata xml);
INSERT INTO #person1(firstname, customdata) VALUES
('John', '<person1_7:CustomData xmlns:person1_7="http://www.badgepass.com/Person1_7">
<Title>IT Director</Title>
<Gaming_x0020_Level>Level 1</Gaming_x0020_Level>
<Gaming_x0020_Issue_x0020_Date>2021-02-18T12:00:00Z</Gaming_x0020_Issue_x0020_Date>
<Gaming_x0020_Expire_x0020_Date>2022-02-18T12:00:00Z</Gaming_x0020_Expire_x0020_Date>
<Betting_x0020_Level>Level 1</Betting_x0020_Level>
<Betting_x0020_Issue_x0020_Date>2021-02-18T12:00:00Z</Betting_x0020_Issue_x0020_Date>
<Betting_x0020_Expire_x0020_Date>2022-02-18T12:00:00Z</Betting_x0020_Expire_x0020_Date>
<BadgeType>Dual Employee</BadgeType>
<Gaming_x0020_Status>TEMP</Gaming_x0020_Status>
<Betting_x0020_Status>TEMP</Betting_x0020_Status>
</person1_7:CustomData>');
-- DDL and sample data population, end
WITH XMLNAMESPACES ('http://www.badgepass.com/Person1_7' as person1_7)
SELECT firstName
, c.value('(Title/text())[1]', 'VARCHAR(100)') AS Title
, c.value('(Gaming_x0020_Issue_x0020_Date/text())[1]', 'DATETIME') GamingIssueDate
FROM #person1
CROSS APPLY customdata.nodes('/person1_7:CustomData') AS t(c);
Output
+-----------+-------------+-------------------------+
| firstName | Title | GamingIssueDate |
+-----------+-------------+-------------------------+
| John | IT Director | 2021-02-18 12:00:00.000 |
+-----------+-------------+-------------------------+

Parse a varchar column containing XML like data into row wise

i have a column in table which contain data like XML,i would like to get data in rows.
My table data as-
select printDataColumn from Mytable
It returns value-
<PrintData>
<Line1>.MERCHANT ID: *****4005</Line1>
<Line2>.CLERK ID: ADMIN</Line2>
<Line3>.</Line3>
<Line4>. VOID SALE</Line4>
<Line5>.</Line5>
<Line6>.VISA ************0006</Line6>
<Line7>.ENTRY METHOD: SWIPED</Line7>
<Line8>.DATE: 03/05/2019 TIME: 16:57:20</Line8>
<Line9>.</Line9>
<Line10>.INVOICE: 1551785225020</Line10>
<Line11>.REFERENCE: 1008</Line11>
<Line12>.AUTH CODE: 08354A</Line12>
<Line13>.</Line13>
<Line14>.AMOUNT USD$ 1.14</Line14>
<Line15>. ==========</Line15>
<Line16>.TOTAL USD$ 1.14</Line16>
<Line17>.</Line17>
<Line18>. APPROVED - THANK YOU</Line18>
<Line19>.</Line19>
<Line20>.I AGREE TO PAY THE ABOVE TOTAL AMOUNT</Line20>
<Line21>.ACCORDING TO CARD ISSUER AGREEMENT</Line21>
<Line22>.(MERCHANT AGREEMENT IF CREDIT VOUCHER)</Line22>
<Line23>.</Line23>
<Line24>.</Line24>
<Line25>.</Line25>
<Line26>.x_______________________________________</Line26>
<Line27>. Merchant Signature</Line27>
<Line28>.</Line28>
</PrintData>
but i want to use this information in another way like that
MERCHANT ID: *****4005
CLERK ID: ADMIN
SALE
AMEX ***********1006
ENTRY METHOD: CHIP
DATE: 03/07/2019 TIME: 14:37:23
INVOICE: 1551949638173
REFERENCE: 1005
AUTH CODE: 040749. . . . .and so on.
any help is appreciable.
Besides the fact, that it always is a good idea to use the appropriate type to store your data, you can use a cast on the fly to use your xml-like-data with XML methods:
DECLARE #tbl TABLE(ID INT IDENTITY,PrintData VARCHAR(4000));
INSERT INTO #tbl VALUES
('<PrintData>
<Line1>.MERCHANT ID: *****4005</Line1>
<Line2>.CLERK ID: ADMIN</Line2>
<Line3>.</Line3>
<Line4>. VOID SALE</Line4>
<!-- more lines -->
</PrintData>');
SELECT t.ID
,A.Casted.value(N'(/PrintData/Line1/text())[1]','nvarchar(max)') AS Line1
FROM #tbl t
CROSS APPLY(SELECT CAST(t.PrintData AS XML)) A(Casted);
In this case I use CROSS APPLY to add a column A.Casted to the result set, which is a row-wise casted XML.
This will break, in cases of invalid XML (of course). You might try TRY_CAST instead. This would return NULL, but will hide data errors...
Some more background
The cast to XML is a rather expensive operation. Doing this whenever you want to read out of your data is some heavy load for your server. Furthermore, using VARCHAR is prone to two major errors:
If there are foreign characters you might get question marks
If the XML is not valid, you will not see it - until you use it.
If possible, try to change the table's design to use native XML.
And one more hint
It is a bad approach to name-number elements (same for columns). Instead of <Line1><Line2><Line3> better use <Line nr="1"><Line nr="2"><Line nr="3">...

Parse XML value with Name Space in SQl

I have following XML :
<ProductionSchedule xmlns:inp2="http://www.wbf.org/xml/B2MML-V0401" xmlns="http://www.wbf.org/xml/B2MML-V0401">
<inp2:ProductionRequest>
<inp2:ID>0916A</inp2:ID>
<inp2:Description>SUBH190916A</inp2:Description>
<inp2:Location>
<inp2:EquipmentID>MYEqupiment</inp2:EquipmentID>
</inp2:Location>
<inp2:SegmentRequirement>
<inp2:ID>000</inp2:ID>
<inp2:EarliestStartTime>2015-10-17T12:00:00</inp2:EarliestStartTime>
<inp2:LatestEndTime>2015-10-19T12:00:00</inp2:LatestEndTime>
<inp2:MaterialProducedRequirement>
<inp2:MaterialDefinitionID>GEEC3MA0025EMZI</inp2:MaterialDefinitionID>
<inp2:Quantity>
<inp2:QuantityString>2</inp2:QuantityString>
</inp2:Quantity>
<inp2:MaterialProducedRequirementProperty>
<inp2:ID>ERPWOStatus</inp2:ID>
<inp2:Value>
<inp2:ValueString>Released</inp2:ValueString>
</inp2:Value>
</inp2:MaterialProducedRequirementProperty>
<inp2:MaterialProducedRequirementProperty>
<inp2:ID>ROUTING</inp2:ID>
<inp2:Value>
<inp2:ValueString>SOmeMPRVaue</inp2:ValueString>
</inp2:Value>
</inp2:MaterialProducedRequirementProperty>
<inp2:MaterialProducedRequirementProperty>
<inp2:ID>MPValue2</inp2:ID>
<inp2:Value>
<inp2:ValueString>2016-01-21T12:00:00</inp2:ValueString>
</inp2:Value>
</inp2:MaterialProducedRequirementProperty>
</inp2:MaterialProducedRequirement>
</inp2:SegmentRequirement>
</inp2:ProductionRequest>
</ProductionSchedule>
I am trying to get the value MPValue2 , from the XML.
I tried with following:
Select `#xml.value('(/ProductionSchedule/inp2:ProductionRequest/inp2:SegmentRequirement/inp2:MaterialProducedRequirement/inp2:MaterialProducedRequirementProperty)[1]','nvarchar(255)')`
Your select is OK, but you must consider/declare the namespaces:
WITH XMLNAMESPACES(DEFAULT 'http://www.wbf.org/xml/B2MML-V0401'
,'http://www.wbf.org/xml/B2MML-V0401' AS inp2)
Select #xml.value('(/ProductionSchedule/inp2:ProductionRequest/inp2:SegmentRequirement/inp2:MaterialProducedRequirement/inp2:MaterialProducedRequirementProperty)[1]','nvarchar(255)')
This works too (wildcard) but it's better to be as specific as possible:
Select #xml.value('(/*:ProductionSchedule/*:ProductionRequest/*:SegmentRequirement/*:MaterialProducedRequirement/*:MaterialProducedRequirementProperty)[1]','nvarchar(255)')
The fast and lazy would work too :-) but not fast in terms of performance...
Select #xml.value('(//*:MaterialProducedRequirementProperty)[1]','nvarchar(255)')
UPDATE
This is the query to get all your Properties:
WITH XMLNAMESPACES(DEFAULT 'http://www.wbf.org/xml/B2MML-V0401'
,'http://www.wbf.org/xml/B2MML-V0401' AS inp2)
SELECT prop.value('(inp2:ID)[1]','nvarchar(100)') AS Property
FROM #xml.nodes('/ProductionSchedule/inp2:ProductionRequest/inp2:SegmentRequirement/inp2:MaterialProducedRequirement/inp2:MaterialProducedRequirementProperty') AS A(prop)
The result
Property
--------
ERPWOStatus
ROUTING
MPValue2
UPDATE 2: Use the ID as filter in XQuery
See how I added the filter at the end of the XPath in .nodes().
Nodes will return all sub-elements row-wise. The filter will reduce the resultset to one single row (if inp2:ID is unique!) and then read the Value/ValueString.
I let the namespace declaration for DEFAULT and inp2. But, as #Serf pointed out correctly, both URLs are equal. It would be enough to declare only the DEFAULT and query without any namespace-prefixes...
DECLARE #TheID NVARCHAR(100)='MPValue2';
WITH XMLNAMESPACES(DEFAULT 'http://www.wbf.org/xml/B2MML-V0401'
,'http://www.wbf.org/xml/B2MML-V0401' AS inp2)
SELECT prop.value('(inp2:Value/inp2:ValueString)[1]','nvarchar(100)') AS Property
FROM #xml.nodes('/ProductionSchedule/inp2:ProductionRequest/inp2:SegmentRequirement/inp2:MaterialProducedRequirement/inp2:MaterialProducedRequirementProperty[inp2:ID=sql:variable("#TheID")]') AS A(prop)

Using Fields[0].Value to get XML from FOR XML RAW, ELEMENTS query is messed up

I have a query that uses FOR XML RAW, ELEMENTS to return a SELECT query as a structured XML document. However, when I get the result using a TSQLDataSet by using Fields[0].Value, the result is different from what I see when I run the query in SQL Server Management Studio.
What I see in the result from the TSQLDataSet:
੄customerIdфname၄governmentNumberไdebtorAddress1ไdebtorAddress2ไdebtorAddress3ไdebtorAddress4ࡄpostCodeୄcontactNameՄphonë́faxൄcustomerSinceՄtermsلactiveไcurrentBalanceلDebtorခŁ䄁ഃӤ
What I see in the result in SSMS:
<Debtor>
<customerId>C0E449E5B2C </customerId>
<name>New Customer 2 </name>
<governmentNumber> </governmentNumber>
<debtorAddress1>Address Line 1 </debtorAddress1>
<debtorAddress4>Address Line 4 </debtorAddress4>
<postCode>1234 </postCode>
<phone>1234567890 </phone>
<fax>1234567890 </fax>
<customerSince>2013-06-10T18:16:06.213</customerSince>
<terms>M </terms>
<active>true</active>
<currentBalance>0.0000</currentBalance>
</Debtor>
Is there a particular way it should be executed to get the right result?
AFAIK this is a DbExpress limitation. I know how overcome this, but using ADO (the returned data must be requested using a special parametrized object and a set of ADO streams). However you can use a workaround converting the XML data to a string in the server side sorrounding the sentence with a select (subquery) or just using a simple CAST statement.
For example if you sentence is like so
SELECT Foo, Bar FROM FooTable FOR XML RAW, ELEMENTS
you can rewrite to
SELECT (SELECT Foo, Bar FROM FooTable FOR XML RAW, ELEMENTS)
or you can rewrite to (use a CAST VARCHAR or NVARCHAR)
SELECT CAST( (SELECT Foo, Bar FROM FooTable FOR XML RAW, ELEMENTS) AS VARCHAR(MAX))
and finally
Retrieve the result like this
SQLDataSet1.Fields[0].AsString

Better way in TSQL to search xml for a node that doesn't exist

We have a source XML file that has an address node, and each node is supposed to have a zip_code node beneath in order to validate. We received a file that failed the schema validation because at least one node was missing it's zip_code (there were several thousand addresses in the file).
We need to find the elements that do not have a zip code, so we can repair the file and send an audit report to the source.
--declare #x xml = bulkcolumn from openrowset(bulk 'x:\file.xml',single_blob) as s
declare #x xml = N'<addresses>
<address><external_address_id>1</external_address_id><zip_code>53207</zip_code></address>
<address><external_address_id>2</external_address_id></address>
</addresses>'
declare #t xml = (
select #x.query('for $a in .//address
return
if ($a/zip_code)
then <external_address_id />
else $a/external_address_id')
)
select x.AddressID.value('.', 'int') AddressID
from #t.nodes('./external_address_id') x(AddressID)
where x.AddressID.value('.', 'int') > 0
GO
Really, it's the where clause that bugs me. I feel like I'm depending on a cast for a null value to 0, and it works, but I'm not really sure that it should. I tried a few variations with the .exist function, but I couldn't get the correct result.
If you just want to ensure that you are selecting address elements that have a zip_code element, then adjust your XPATH to include that criteria in a predicate filter:
/addresses/address[zip_code]
If you also want to ensure that the zip_code element also has a value, use a predicate filter for the zip_node to select those that have text() nodes:
/addresses/address[zip_code[text()]]
EDIT:
Actually, I'm looking for the
opposite. I need to identify the nodes
that don't have a zip, so we can
manually correct the source data.
So, if you want to identify all of the address elements that do not have a zip_code, you can specify it in the XPATH like this:
/addresses/address[not(zip_code)]
If you just want to locate those nodes that are missing their <zip_code> element, you could use something like this:
SELECT
ADRS.ADR.value('(external_address_id)[1]', 'int') as 'ExtAdrID'
FROM
#x.nodes('/addresses/address') as ADRS(ADR)
WHERE
ADRS.ADR.exist('zip_code') = 0
It uses the built-in .exist() method in XQuery to check the existence of a subnode inside an XML node.