Get the attribute value of the first element? - sql-server

The following code tries to get the attribute a of the first node y in SQL Server.
declare #x xml = '<x><y a="1" /><y a="2" /></x>'
select #x.query('/x/y[1]/#a')
select #x.query('(/x/y/#a)[1]')
However, it got the error of
Msg 6307, Level 16, State 1, Line 5
XML well-formedness check: Attribute cannot appear outside of element declaration. Rewrite your XQuery so it returns well-formed XML.

If I understand
Example
declare #x xml = '<x><y a="1" /><y a="2" /></x>'
select #x.value('x[1]/y[1]/#a','varchar(max)')
Returns
1

This works
select #x.query('data(/x/y[1]/#a)')

Related

Is that valid XML and how to replicate with SQL Server

I do have to replicate an XML file with SQL Server and I am now stumbling over the following structure inside the XML file and I don't know how to replicate that.
The structure looks like this at the moment for certain tags:
<ART_TAG1>
<UNMLIMITED/>
</ART_TAG1>
<ART_TAG2>
<ART_TAG3>
<Data_Entry/>
</ART_TAG3>
</ART_TAG2>
I am wondering if this is proper XML that the data inside (unlimited and Data_Entry) is enclosed with a closing XML tag. The XML validator https://www.w3schools.com/xml/xml_validator.asp is telling me this is correct. But now I am struggling with replicating that with Transact-SQL.
If I try to replicate that I can only come up with the following TSQL script, which obviously does not fully look like the original.
SELECT 'UNLIMITED' as 'ART_TAG1'
, 'Data_Entry' as 'ART_TAG2/ART_TAG3'
FOR XML PATH(''), ROOT('root')
<root>
<ART_TAG1>UNLIMITED</ART_TAG1>
<ART_TAG2>
<ART_TAG3>Data_Entry</ART_TAG3>
</ART_TAG2>
</root>
If I get this correctly, your question is:
How can I put my query to create those <SomeElement /> tags?
Look at this:
--This will create filled nodes
SELECT 'outer' AS [OuterNode/#attr]
,'inner' AS [OuterNode/InnerNode]
FOR XML PATH('row');
--The empty string is some kind of content
SELECT 'outer' AS [OuterNode/#attr]
,'' AS [OuterNode/InnerNode]
FOR XML PATH('row');
--the missing value (NULL) is omited by default
SELECT 'outer' AS [OuterNode/#attr]
,NULL AS [OuterNode/InnerNode]
FOR XML PATH('row');
--Now check what happens here:
--First XML has an empty element, while the second uses the self-closing element
DECLARE #xml1 XML=
N'<row>
<OuterNode attr="outer">
<InnerNode></InnerNode>
</OuterNode>
</row>';
DECLARE #xml2 XML=
N'<row>
<OuterNode attr="outer">
<InnerNode/>
</OuterNode>
</row>';
SELECT #xml1,#xml2;
The result is the same for both...
Some background: Semantically the empty element <element></element> is exactly the same as the self-closing element <element />. It should not make any difference, whether you use the one or the other. If your consumer cannot deal with this, it is a problem in the reading part.
Yes, you can force any content into XML on string level, but - as the example shows above - this is just a (dangerous) hack.
XML within T-SQL returns - by default - a missing node as NULL and an empty element as empty (depending on the datatype, and beware of the difference between an element and its text() node).
In short: This is nothing you should have to think about...

XML parsing: line 1, character 345, duplicate attribute

I am trying to get the particular attribute value from XML column, but I'm getting an error
XML parsing: line 1, character 345, duplicate attribute
My code:
select
ship_to_cust_num,
tank_num,
tank_capacity_qty,
tank_pkg_type_code,
COALESCE(REPLACE(CAST(CAST(b.tank_inspection AS NTEXT) AS XML).value('(/TankInspection/Questions/Question[#AASAQno="9"]/#QAns)[1]', 'VARCHAR(50)'), '#', ''), 0)
from
bulk_site_tank (nolock)b
where
convert(varchar, b.tank_inspection) != 'NULL'
The simple answer is that the error is telling you the problem. But to explain further. Take this simple statement:
DECLARE #xml varchar(MAX);
SET #XML = '
<root>
<child>
<element attribute="1">value</element>
<element attribute="2" attribute="2">Another Value</element>
</child>
</root>';
SELECT *
FROM (VALUES(CONVERT(xml, #XML)))V(X);
If you run that, you'll get the error:
Msg 9437, Level 16, State 1, Line 11 XML parsing: line 5, character 46, duplicate attribute
Unsurprising, as if you look, the second element node has attribute declared twice.
So, how do you fix this?
Firstly, this means that you're storing your XML data as a datatype other than in an xml data type. XML should be stored using the xml data type (that's exactly what it's for), and only valid XML can be stored in it; as a result you wouldn't have been able to insert invalid XML into the row and wouldn't be in this position. As you are, there's only one thing you can do; find all the "bad" rows:
SELECT tank_inspection
FROM bulk_site_tank
WHERE TRY_CONVERT(xml,tank_inspection) IS NULL
AND tank_inspection IS NOT NULL;
Then inspect every single row returned in the above dataset and fix the data. Make it valid XML. After that, fix your data type:
ALTER TABLE bulk_site_tank ALTER COLUMN tank_inspection xml;
Now everything is valid XML, you can fix that query of yours:
SELECT ship_to_cust_num,
tank_num,
tank_capacity_qty,
tank_pkg_type_code,
REPLACE(b.tank_inspection.value('(/TankInspection/Questions/Question[#AASAQno="9"]/#QAns)[1]', 'varchar(50)'), '#', '') --AS ?
FROM bulk_site_tank b
WHERE b.tank_inspection IS NOT NULL;
Note I change to ANSI_NULL syntax, and got rid of the NOLOCK (as I assume you don't know what it actually does here). The CAST/CONVERT expressions are gone too, and I've removed the COALESCE. As your value expression returns a varchar(50) and the COALESCE has a 0 for the second parameter. This would implicitly cast the value returned from the XML to an int and likely result in a conversion error.
I'm afraid it's up to you to clean up your data though, no one else can help you here I'm afraid. This is just one reason why poor data type choices is a problem; as if the correct data type was used then,as I said before, the invalid XML could never have been inserted.
Good luck!

Supply xml element value in modify() method

I know how to replace element value for the xml element in the modify() method. Here's the example
TSQL Replace value in XML String
My problem is a bit different. Taking example from above link...
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/educational/text())[1]
with sql:variable("#updatedEducation")')
WHERE feedbackID = #FBID
What I want to do is provide value for 'educational'. In other words I want to do something like this
UPDATE dbo.TFS_Feedback_New
SET Details.modify('
replace value of (/optional/sql:variable("#name")/text())[1]
with sql:variable("#value")')
WHERE feedbackID = #FBID
I'm getting the following error because of sql:variable("#name")
The XQuery syntax '/function()' is not supported.
How can I pass both the name of the element to be updated and its value to my
stored procedure and have it update the XML column?
You are not allowed to use variables as part of the XPath, but you can use a predicate:
DECLARE #xml XML=
N'<root>
<optional>
<educational>SomeText</educational>
<someOther>blah</someOther>
</optional>
</root>';
--The straight approach as you know it:
SET #xml.modify('replace value of (/root/optional/educational/text())[1] with "yeah!"');
SELECT #xml;
--Now we use a variable to find the first node below <optional>, which name is as given:
DECLARE #ElementName VARCHAR(100)='educational';
SET #xml.modify('replace value of (/root/optional/*[local-name()=sql:variable("#ElementName")]/text())[1] with "yeah again!"');
SELECT #xml;
Try it out...

Select element from XML

Given the following:
declare #samplexml as xml
set #samplexml = '<root><someelement><another /><somethingElse>test</somethingElse></someelement></root>'
select
#samplexml.value('/root[1]','nvarchar(max)')
I get the result:
test
But I want the result:
<root><someelement><another /><somethingElse>test</somethingElse></someelement></root>
How can I select the actual XML element? I also tried:
select
#samplexml.value('/root[1]','XML')
But I got the error The data type 'XML' used in the VALUE method is invalid..
Just use the .query() method instead of .value() :
SELECT #samplexml.query('/root[1]')
or
SELECT #samplexml.query('.')
This returns the element (and its contents) that matches that XPath expression given, and it's returned as XML type

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

Resources