How to get the partial value of a XML Node value - sql-server

I'm new to Xpath and This is my XML . I'm trying to the get the attribute value #name in the appl/*__job tag and the value 'TESTQUEUE 'in the node snmp_notify/message and I'm taking one step at a time. As of now I was able to get the child nodes of all _job, but I couldn't get the value in the node /snmp_notifylist/snmp_notify/message. This is the SQL and Could someone help me with identifying where I got stuck.
This is the Sample XML Document stored as DEFINITION in the table TAB_AR.
<appl xmlns="http://dto.wa.ca.com/application" name="TEST_NEW_AGENT">
<version>12.0</version>
<comment />
<unix_job name="TEST_JOB">
<dependencies><relcount>0</relcount></dependencies>
<snmp_notifylist>
<snmp_notify>
<returncode>4</returncode>
<monitor_states><monitor_state>FAILED</monitor_state></monitor_states>
<snmpagent />
<message>TICKET TESTQUEUE TSTMSG</message>
</snmp_notify>
</snmp_notifylist>
</unix_job>
<link name="HOLD_LINK">
<dependencies><relcount>0</relcount></dependencies>
<hold>true</hold>
<job_ancestor_wait_default_ignore>true</job_ancestor_wait_default_ignore>
</link>
<sftp_job name="TEST_SFTP1">
<dependencies><relcount>0</relcount></dependencies>
<snmp_notifylist>
<snmp_notify>
<returncode>4</returncode>
<monitor_states>
<monitor_state>FAILED</monitor_state>
</monitor_states>
<snmpagent />
<message>TICKET MFG1AWA TSTMSG</message>
</snmp_notify>
</snmp_notifylist>
</sftp_job>
</appl>
And this is the SQL I wrote,
SELECT
SFTP_Job_name = DEFT1.value('(#name)[1]','nvarchar(max)'),
Server_Address = DEFT1.query('local-name(/*:snmp_notifylist/*:snmp_notify/*:message)')
from (select CAST([DEFINITION] as XML) as DEFT from TAB_AR)TAB
CROSS APPLY TAB.DEFT.nodes('/*:appl/*[fn:contains(local-name(),"_job")]') as XMLTAB1(DEFT1)

You were close...
In this line I'm not sure, what you really wanted to get:
DEFT1.query('local-name(/*:snmp_notifylist/*:snmp_notify/*:message)')
With local-name() you can return the name of one specific node. As you are reading from several nodes ending on _job it perfectly makes sense to return the name of the element you are reading from.
But you are telling us, that you are trying to read the <message> too. Might be, that you are mixing two calls in one line?
I slightly modified your code:
SELECT
SFTP_Job_name = DEFT1.value('(#name)[1]','nvarchar(max)')
,NodeName = DEFT1.value('local-name(.)','nvarchar(max)')
,Server_Address = DEFT1.value('(*:snmp_notifylist/*:snmp_notify/*:message)[1]','nvarchar(max)')
from (select CAST([DEFINITION] as XML) as DEFT from TAB_AR)TAB
CROSS APPLY TAB.DEFT.nodes('/*:appl/*[fn:contains(local-name(.),"_job")]') as XMLTAB1(DEFT1);
This returns
SFTP_Job_name NodeName Server_Address
TEST_SFTP1 sftp_job TICKET MFG1AWA TSTMSG
TEST_JOB unix_job TICKET TESTQUEUE TSTMSG
Like Roger Wolf pointed out, it was better to read with a specified namespaces like this:
WITH XMLNAMESPACES (default 'http://dto.wa.ca.com/application')
SELECT
SFTP_Job_name = DEFT1.value('(#name)[1]','nvarchar(max)')
,NodeName = DEFT1.value('local-name(.)','nvarchar(max)')
,Server_Address = DEFT1.value('(snmp_notifylist/snmp_notify/message)[1]','nvarchar(max)')
from (select CAST([DEFINITION] as XML) as DEFT from TAB_AR)TAB
CROSS APPLY TAB.DEFT.nodes('/appl/*[fn:contains(local-name(.),"_job")]') as XMLTAB1(DEFT1);
The general rule is: Be as specific as possible!
Hint
If you can change this, you should store your XML in a column of type XML.
This construction from (select CAST([DEFINITION] as XML) as DEFT from TAB_AR)TAB should really not be necessary...
Might be, that your column is XML actually and you just did not know how to transfer the code you found somewhere to get the right syntax for the .nodes()? In this case just try this:
SELECT
SFTP_Job_name = DEFT1.value('(#name)[1]','nvarchar(max)')
,NodeName = DEFT1.value('local-name(.)','nvarchar(max)')
,Server_Address = DEFT1.value('(*:snmp_notifylist/*:snmp_notify/*:message)[1]','nvarchar(max)')
from TAB_AR
CROSS APPLY TAB_AR.[DEFINITION].nodes('/*:appl/*[fn:contains(local-name(.),"_job")]') as XMLTAB1(DEFT1);

This seems to be working:
with xmlnamespaces (default 'http://dto.wa.ca.com/application')
select j.c.value('./#name', 'sysname') as [JobName],
m.c.value('./text()[1]', 'varchar(max)') as [MessageText]
from (
select cast(t.[Definition] as xml) as [Deft] from tab_ar t
) sq
cross apply sq.Deft.nodes('/appl/*[fn:contains(local-name(),"_job")]') j(c)
cross apply j.c.nodes('./snmp_notifylist/snmp_notify/message') m(c);
After that, splitting the string by spaces and taking the middle part should be relatively trivial.

Related

SQL: Using XML as input to do an inner join

I have XML coming in as the input, but I'm unclear on how I need to setup the data and statement to get the values from it. My XML is as follows:
<Keys>
<key>246</key>
<key>247</key>
<key>248</key>
</Keys>
And I want to do the following (is simplified to get my point across)
Select *
From Transaction as t
Inner Join #InputXml.nodes('Keys') as K(X)
on K.X.value('#Key', 'INT') = t.financial_transaction_grp_key
Can anyone provide how I would do that? What would my 3rd/4th line in the SQL look like?
Thanks!
From your code I assume this is SQL-Server but you added the tag [mysql]...
For your next question please keep in mind, that it is very important to know your tools (vendor and version).
Assuming T-SQL and [sql-server] (according to the provided sample code) you were close:
DECLARE #InputXml XML=
N'<Keys>
<key>246</key>
<key>247</key>
<key>248</key>
</Keys>';
DECLARE #YourTransactionTable TABLE(ID INT IDENTITY,financial_transaction_grp_key INT);
INSERT INTO #YourTransactionTable VALUES (200),(246),(247),(300);
Select t.*
From #YourTransactionTable as t
Inner Join #InputXml.nodes('/Keys/key') as K(X)
on K.X.value('text()[1]', 'INT') = t.financial_transaction_grp_key;
What was wrong:
.nodes() must go down to the repeating element, which is <key>
In .value() you are using the path #Key, which is wrong on two sides: 1) <key> is an element and not an attribute and 2) XML is strictly case-sensitive, so Key!=key.
An alternative might be this:
WHERE #InputXml.exist('/Keys/key[. cast as xs:int? = sql:column("financial_transaction_grp_key")]')=1;
Which one is faster depends on the count of rows in your source table as well as the count of keys in your XML. Just try it out.
You probably need to parse the XML to a readable format with regex.
I wrote a similar event to parse the active DB from an xmlpayload that was saved on a table. This may or may not work for you, but you should be able to at least get started.
SELECT SUBSTRING(column FROM IF(locate('<key>',column)=0,0,0+LOCATE('<key>',column))) as KEY FROM table LIMIT 1\G

Parse XML using SQL

I'm using MS SQL2016 and I have an XML file that I need to parse to put various data elements into the separate fields. For the most part everything works find except I need a little help to identify a particular node value. If I have (I put only a snippet of the xml here but it does show the problem)
DECLARE #xmlString xml
SET #xmlString ='<PubmedArticle>
<MedlineCitation Status="PubMed-not-MEDLINE" Owner="NLM">
<PMID Version="1">25685064</PMID>
<Article PubModel="Electronic-eCollection">
<Journal>
<ISSN IssnType="Electronic">1234-5678</ISSN>
<ISSN IssnType="Print">1475-2867</ISSN>
<JournalIssue CitedMedium="Print">
<Volume>15</Volume>
<Issue>1</Issue>
<PubDate>
<Year>2015</Year>
</PubDate>
</JournalIssue>
</Journal>
</Article>
</MedlineCitation>
</PubmedArticle>'
select
nref.value('Article[1]/Journal[1]/ISSN[1]','varchar(max)') ISSN
from #xmlString.nodes ('//MedlineCitation[1]') as R(nref)
I bypass the second ISSNType and read the first value available. I need to pull both values. What do I need to change? Thanks
You can read as second column:
SELECT
nref.value('Article[1]/Journal[1]/ISSN[1]','varchar(max)') ISSN,
nref.value('Article[1]/Journal[1]/ISSN[2]','varchar(max)') ISSN2
FROM #xmlString.nodes('//MedlineCitation[1]') as R(nref)
Or
SELECT
nref.value('ISSN[1]','varchar(max)') ISSN,
nref.value('ISSN[2]','varchar(max)') ISSN2
FROM #xmlString.nodes('//MedlineCitation[1]/Article[1]/Journal[1]') as R(nref)
Or as a separate row:
SELECT nref.value('.','varchar(MAX)') ISSN
from #xmlString.nodes('//MedlineCitation[1]/Article[1]/Journal[1]/ISSN') as R(nref)
Update
If number of ISSNs may vary, I recommend normalize your resultset:
SELECT
nref.value('.','varchar(MAX)') Issn,
nref.value('#IssnType','varchar(MAX)') IssnType
FROM #xmlString.nodes('//MedlineCitation[1]/Article[1]/Journal[1]/ISSN') as R(nref)

How do I update an XML column in sql server by checking for the value of two nodes including one which needs to do a contains (like) comparison

I have an xml column called OrderXML in an Orders table...
there is an XML XPath like this in the table...
/Order/InternalInformation/InternalOrderBreakout/InternalOrderHeader/InternalOrderDetails/InternalOrderDetail
There InternalOrderDetails contains many InternalOrderDetail nodes like this...
<InternalOrderDetails>
<InternalOrderDetail>
<Item_Number>FBL11REFBK</Item_Number>
<CountOfNumber>10</CountOfNumber>
<PriceLevel>FREE</PriceLevel>
</InternalOrderDetail>
<InternalOrderDetail>
<Item_Number>FCL13COTRGUID</Item_Number>
<CountOfNumber>2</CountOfNumber>
<PriceLevel>NONFREE</PriceLevel>
</InternalOrderDetail>
</InternalOrderDetails>
My end goal is to modify the XML in the OrderXML column IF the Item_Number of the node contains COTRGUID (like '%COTRGUID') AND the PriceLevel=NONFREE. If that condition is met I want to change the PriceLevel column to equal FREE.
I am having trouble with both creating the xpath expression that finds the correct nodes (using OrderXML.value or OrderXML.exist functions) and updating the XML using the OrderXML.modify function).
I have tried the following for the where clause:
WHERE OrderXML.value('(/Order/InternalInformation/InternalOrderBreakout/InternalOrderHeader/InternalOrderDetails/InternalOrderDetail/Item_Number/node())[1]','nvarchar(64)') like '%13COTRGUID'
That does work, but it seems to me that I need to ALSO include my second condition (PriceLevel=NONFREE) in the same where clause and I cannot figure out how to do it. Perhaps I can put in an AND for the second condition like this...
AND OrderXML.value('(/Order/InternalInformation/InternalOrderBreakout/InternalOrderHeader/InternalOrderDetails/InternalOrderDetail/PriceLevel/node())[1]','nvarchar(64)') = 'NONFREE'
but I am afraid it will end up operating like an OR since it is an XML query.
Once I get the WHERE clause right I will update the column using a SET like this:
UPDATE Orders SET orderXml.modify('replace value of (/Order/InternalInformation/InternalOrderBreakout/InternalOrderHeader/InternalOrderDetails/InternalOrderDetail/PriceLevel[1]/text())[1] with "NONFREE"')
However, I ran this statement on some test data and none of the XML columns where updated (even though it said zz rows effected).
I have been at this for several hours to no avail. Help is appreciated. Thanks.
if you don't have more than one node with your condition in each row of Orders table, you can use this:
update orders set
data.modify('
replace value of
(
/Order/InternalInformation/InternalOrderBreakout/
InternalOrderHeader/InternalOrderDetails/
InternalOrderDetail[
Item_Number[contains(., "COTRGUID")] and
PriceLevel="NONFREE"
]/PriceLevel/text()
)[1]
with "FREE"
');
sql fiddle demo
If you could have more than one node in one row, there're a several possible solutions, none of each is really elegant, sadly.
You can reconstruct all xmls in table - sql fiddle demo
or you can do your updates in the loop - sql fiddle demo
This may get you off the hump.
Replace #HolderTable with the name of your table.
SELECT T2.myAlias.query('./../PriceLevel[1]').value('.' , 'varchar(64)') as MyXmlFragmentValue
FROM #HolderTable
CROSS APPLY OrderXML.nodes('/InternalOrderDetails/InternalOrderDetail/Item_Number') as T2(myAlias)
SELECT T2.myAlias.query('.') as MyXmlFragment
FROM #HolderTable
CROSS APPLY OrderXML.nodes('/InternalOrderDetails/InternalOrderDetail/Item_Number') as T2(myAlias)
EDIT:
UPDATE
#HolderTable
SET
OrderXML.modify('replace value of (/InternalOrderDetails/InternalOrderDetail/PriceLevel/text())[1] with "MyNewValue"')
WHERE
OrderXML.value('(/InternalOrderDetails/InternalOrderDetail/PriceLevel)[1]', 'varchar(64)') = 'FREE'
print ##ROWCOUNT
Your issue is the [1] in the above.
Why did I put it there?
Here is a sentence from the URL listed below.
Note that the target being updated must be, at most, one node that is explicitly specified in the path expression by adding a "[1]" at the end of the expression.
http://msdn.microsoft.com/en-us/library/ms190675.aspx
EDIT.
I think I've discovered the the root of your frustration. (No fix, just the problem).
Note below, the second query works.
So I think the [1] is some cases is saying "only ~~search~~ the first node".....and not (as you and I were hoping)...... "use the first node..after you find a match".
UPDATE
#HolderTable
SET
OrderXML.modify('replace value of (/InternalOrderDetails/InternalOrderDetail/PriceLevel/text())[1] with "MyNewValue001"')
WHERE
OrderXML.value('(/InternalOrderDetails/InternalOrderDetail/PriceLevel[text() = "NONFREE"])[1]', 'varchar(64)') = 'NONFREE'
/* and OrderXML.value('(/InternalOrderDetails/InternalOrderDetail/Item_Number)[1]', 'varchar(64)') like '%COTRGUID' */
UPDATE
#HolderTable
SET
OrderXML.modify('replace value of (/InternalOrderDetails/InternalOrderDetail/PriceLevel/text())[1] with "MyNewValue002"')
WHERE
OrderXML.value('(/InternalOrderDetails/InternalOrderDetail/PriceLevel[text() = "FREE"])[1]', 'varchar(64)') = 'FREE'
Try this :
;with InternalOrderDetail as (SELECT id,
Tbl.Col.value('Item_Number[1]', 'varchar(40)') Item_Number,
Tbl.Col.value('CountOfNumber[1]', 'int') CountOfNumber,
case
when Tbl.Col.value('Item_Number[1]', 'varchar(40)') like '%COTRGUID'
and Tbl.Col.value('PriceLevel[1]', 'varchar(40)')='NONFREE'
then 'FREE'
else
Tbl.Col.value('PriceLevel[1]', 'varchar(40)')
end
PriceLevel
FROM (select id ,orderxml from demo)
as a cross apply orderxml.nodes('//InternalOrderDetail')
as
tbl(col) ) ,
cte_data as(SELECT
ID,
'<InternalOrderDetails>'+(SELECT ITEM_NUMBER,COUNTOFNUMBER,PRICELEVEL
FROM InternalOrderDetail
where ID=Results.ID
FOR XML AUTO, ELEMENTS)+'</InternalOrderDetails>' as XML_data
FROM InternalOrderDetail Results
GROUP BY ID)
update demo set orderxml=cast(xml_data as xml)
from demo
inner join cte_data on demo.id=cte_data.id
where cast(orderxml as varchar(2000))!=xml_data;
select * from demo;
SQL Fiddle
I have handled following cases :
1. As required both where clause in question.
2. It will update all <Item_Number> like '%COTRGUID' and <PriceLevel>= NONFREE in one
node, not just the first one.
It may require minor changes for your data and tables.

SQL Server XML Type Select Where Attribute = X From Any Tag

select *
from tablename
where CONVERT(xml, Sections).value('(/sections/section/#value)[1]', 'varchar(1)') = 'f'
will properly retrieve a record with the following value in the Sections column:
<sections><section value="f" priority="4" /><section value="a" priority="4" /></sections>
But misses this:
<sections><section value="w" priority="4" /><section value="f" priority="4" /></sections>
Obviously this is the problem "/sections/section/#value)[1]" but I don't understand the syntax and Google hasn't been too helpful. I found some code that got me this far, but I don't know how to modify it so that it will look through all tags instead of just the first one. I tried dropping the [1] but that gave the following error:
XQuery [value()]: 'value()' requires a singleton (or empty sequence), found operand of type 'xdt:untypedAtomic *'
You can use exist().
select *
from tablename
where CONVERT(xml, Sections).exist('/sections/section[#value = "f"]') = 1
If you want to use some dynamic value instead a hard coded f in the query you can use sql:variable().
declare #Value varchar(10) = 'f'
select *
from tablename
where CONVERT(xml, Sections).exist('/sections/section[#value = sql:variable("#Value")]') = 1
If you have multiple entries of an XML tag, you need to use the .nodes() XQuery method:
select
*,
Sections(Section).value('(#value)[1]', 'varchar(1)')
from tablename
cross apply CONVERT(xml, Sections).nodes('/sections/section') AS Sections(Section)
With this, you create a "pseudo-table" called Sections(Section) that contains one XML row for each element that matches your XPath (for each <section> under <sections>). You can then reach into this pseudo-table and extract individual bits of information from those XML "rows" using hte usual .value() method

How to get a particular attribute from XML element in SQL Server

I have something like the following XML in a column of a table:
<?xml version="1.0" encoding="utf-8"?>
<container>
<param name="paramA" value="valueA" />
<param name="paramB" value="valueB" />
...
</container>
I am trying to get the valueB part out of the XML via TSQL
So far I am getting the right node, but now I can not figure out how to get the attribute.
select xmlCol.query('/container/param[#name="paramB"]') from LogTable
I figure I could just add /#value to the end, but then SQL tells me attributes have to be part of a node. I can find a lot of examples for selecting the child nodes attributes, but nothing on the sibling atributes (if that is the right term).
Any help would be appreciated.
Try using the .value function instead of .query:
SELECT
xmlCol.value('(/container/param[#name="paramB"]/#value)[1]', 'varchar(50)')
FROM
LogTable
The XPath expression could potentially return a list of nodes, therefore you need to add a [1] to that potential list to tell SQL Server to use the first of those entries (and yes - that list is 1-based - not 0-based). As second parameter, you need to specify what type the value should be converted to - just guessing here.
Marc
Depending on the the actual structure of your xml, it may be useful to put a view over it to make it easier to consume using 'regular' sql eg
CREATE VIEW vwLogTable
AS
SELECT
c.p.value('#name', 'varchar(10)') name,
c.p.value('#value', 'varchar(10)') value
FROM
LogTable
CROSS APPLY x.nodes('/container/param') c(p)
GO
-- now you can get all values for paramB as...
SELECT value FROM vwLogTable WHERE name = 'paramB'

Resources