Parse nvarchar value that looks like a simplified XML - sql-server

I have a column of type nvarchar(1000) that contains something that looks like an XML:
<Master>
<NodeA>lorem ipsum</NodeA>
<NodeB>lorem ipsum</NodeB>
<NodeC>lorem ipsum</NodeC>
<NodeD>lorem ipsum</NodeD>
</Master>
The value might have some carriage return and new lines embedded on it.
What would be the easiest way to get the value inside NodeA?
I've tried to remove the hardcoded string value <masterA> but then I feel I'm doing something wrong here.

Try this:
DECLARE #XmlTable TABLE (ID INT NOT NULL, XmlContent NVARCHAR(1000))
INSERT INTO #XmlTable (ID, XmlContent)
VALUES (1, N'<Master>
<NodeA>lorem ipsum</NodeA>
<NodeB>lorem ipsum</NodeB>
<NodeC>lorem ipsum</NodeC>
<NodeD>lorem ipsum</NodeD>
</Master>')
SELECT
CAST(XmlContent AS XML).value('(/Master/NodeA)[1]', 'varchar(50)')
FROM
#XmlTable
WHERE
ID = 1
But if your column really only stores XML - you should make it an XML column - that's easier (no need to always do a CAST(... AS XML) before applying any XQuery methods), and it's also optimized in terms of storage.

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

SQL REPLACE function inside Xml.modify 'replace value of'

I'm trying to update some XML data in SQL Server. The XML contains data that looks like this:
<root>
<id>1</id>
<timestamp>16-10-2017 19:24:55</timestamp>
</root>
Let's say this XML exists in a column called Data in a table called TestTable.
I would like to be able to change the hyphens in the timestamp to forward slashes.
I was hoping I might be able to do something like:
update TestTable
set Data.modify('replace value of
(/root/timestamp/text())[1] with REPLACE((/root/timestamp/text())[1], "-", "/")')
I get the following error:
XQuery [TestTable]: There is no function '{http://www.w3.org/2004/07/xpath-functions}:REPLACE()'
When I think about it, this makes sense. But I wonder, is there a way to do this in a single update statement? Or do I first need to query the timestamp value and save it as a variable, and then update the XML with the variable?
You can also do this with a join to an inline view and use the SQL REPLACE function:
CREATE TABLE TestTable
(
Id INT IDENTITY(1,1) NOT NULL,
Data XML NOT NULL
)
INSERT TestTable (Data) VALUES ('<root>
<id>1</id>
<timestamp>16-10-2017 19:24:55</timestamp>
</root>')
UPDATE TestTable
SET Data.modify('replace value of
(/root/timestamp/text())[1] with sql:column("T2.NewData")')
FROM TestTable T1
INNER JOIN (
SELECT Id
, REPLACE( Data.value('(/root/timestamp/text())[1]', 'nvarchar(max)'), '-', '/') AS NewData
FROM TestTable
) T2
ON T1.Id = T2.Id
SELECT * FROM TestTable
Note: this answer assumes you want to have this formatted for the purpose of displaying this as a string, and not parsing the content as a xs:dateTime. If you want the latter, Shungo's answer will format it as such.
It seems that replace is not a supported XQuery function in SQL Server at the time of this writing. You can use the substring function along with the concat function in a "replace value of (XML DML)" though.
CREATE TABLE #t(x XML);
INSERT INTO #t(x)VALUES(N'<root><id>1</id><timestamp>16-10-2017 19:24:55</timestamp></root>');
UPDATE
#t
SET
x.modify('replace value of (/root/timestamp/text())[1]
with concat(substring((/root/timestamp/text())[1],1,2),
"/",
substring((/root/timestamp/text())[1],4,2),
"/",
substring((/root/timestamp/text())[1],7)
) ')
SELECT*FROM #t;
Giving as a result:
<root><id>1</id><timestamp>16/10/2017 19:24:55</timestamp></root>
If there's no external need you have to fullfill, you should use ISO8601 date/time strings within XML.
Your dateTime-string is culture related. Reading this on different systems with differing language or dateformat settings will lead to errors or - even worse!!! - to wrong results.
A date like "08-10-2017" can be the 8th of October or the 10th of August...
The worst point is, that this might pass all your tests successfully, but will break on a customer's machine with strange error messages or bad results down to real data dammage!
Switching the hyphens to slashes is just cosmetic! An XML is a strictly defined data container. Any non-string data must be represented as a secure convertible string.
This is what you should do:
DECLARE #tbl TABLE(ID INT IDENTITY,YourXML XML);
INSERT INTO #tbl VALUES
(N'<root>
<id>1</id>
<timestamp>16-10-2017 19:24:55</timestamp>
</root>');
UPDATE #tbl SET YourXml.modify(N'replace value of (/root/timestamp/text())[1]
with concat( substring((/root/timestamp/text())[1],7,4), "-"
,substring((/root/timestamp/text())[1],4,2), "-"
,substring((/root/timestamp/text())[1],1,2), "T"
,substring((/root/timestamp/text())[1],12,8)
) cast as xs:dateTime?');
SELECT * FROM #tbl;
The result
<root>
<id>1</id>
<timestamp>2017-10-16T19:24:55</timestamp>
</root>
you can try string replacement like below
update testtable
set data= cast(
concat(
left(cast(data as varchar(max)),charindex('<timestamp>',cast(data as varchar(max)))+len('<timestamp>')-1),
replace(
substring(
cast(data as varchar(max)),
len('<timestamp>') +
charindex( '<timestamp>', cast(data as varchar(max))) ,
charindex('</timestamp>',cast(data as varchar(max)))
-charindex('<timestamp>',cast(data as varchar(max)))
-len('<timestamp>')
),
'-','/'),
right(cast(data as varchar(max)),len(cast(data as varchar(max)))-charindex('</timestamp>',cast(data as varchar(max)))+1)
) as xml)
select *
from testtable
working demo

Trying to transfer xml data into SQL as elements (not attribute)

I am new at this so please bear with me. I am attempting to transfer some XML data into Microsoft SQL Server. I am assuming that this data needs to be transferred as elements and not attributes because the contents of the columns will not be static.
However for some reason when I attempt to transfer the data as elements I get NULL values. But when I try to transfer this same data as attributes it works and looks the way it is supposed to. I am tempted to shrug and just move on but I'm worried that things might go awry for me if I do that later on down the road.
I already have some attributes from this XML that I managed to transfer as attributes which I plan to combine with these elements that are masquerading as attributes into a single table. Will it work? And if it does will there be problems down the road?
Here is my SQL code when I attempt to transfer the elements as elements:
SELECT *
FROM OPENXML (#hdoc, '/roll/voter', 2)
WITH (
id int,
[value] char(50),
[state] char(2))
Here is my SQL code when I attempt to transfer the elements as attributes:
SELECT *
FROM OPENXML (#hdoc, '/roll/voter', 1)
WITH (
id int,
[value] char(50),
[state] char(2))
Here is a miniaturized version of the XML document:
<roll>
<voter id="400048" value="Yea" state="FL" />
<voter id="412516" value="Yea" state="CA" />
</roll>
Here is a link to the xml document via google drive (very small XML): https://drive.google.com/open?id=0B5VgOwWcGeLHaWctRU56Qlk3UWM
A screenshot of my SQL query, the table results, and the XML
FROM OPENXML is outdated and should not be used anymore (rare exceptions exist)...
Try with the real XML methods:
DECLARE #xml XML=
N'<roll>
<voter id="400048" value="Yea" state="FL" />
<voter id="412516" value="Yea" state="CA" />
</roll>';
SELECT #xml.value(N'(/roll/voter/#id)[1]',N'int') AS voter_id
,#xml.value(N'(/roll/voter/#value)[1]',N'nvarchar(max)') AS voter_value
,#xml.value(N'(/roll/voter/#state)[1]',N'nvarchar(max)') AS voter_state
The result
voter_id voter_value voter_state
400048 Yea FL

Using attribute more than once in FOR XML Path T-SQL query with same element name

I am trying to create an xml output in SQL 2008 using FOR XML Path. This is working fine:
<Taxonomy>
<Category Level="1">Clothing</Category>
<SubCategory Level="2">Jeans</SubCategory>
</Taxonomy>
But I would like the output to be:
<Taxonomy>
<Category Level="1">Clothing</Category>
<Category Level="2">Jeans</Category>
</Taxonomy>
Of course you can code as following:
1 as 'Taxonomy/Category/#Level',
2 as 'Taxonomy/Category/#Level',
t.MainCat as 'Taxonomy/Category',
t.SubCat as 'Taxonomy/Category',
But this gives an error message:
Attribute-centric column 'Column name is repeated. The same attribute cannot be generated more than once on the same XML tag.
What can be done to get the desired output?
Would a subselect work or some kind of cross apply? Or perhaps a union? But how?
---- EDIT - after several answers came up with following solution:
SELECT
1 as 'Category/#Level',
t.Cat as 'Category'
FROM table t
UNION
SELECT
2 as 'Category/#Level',
t.SubCat as 'Category'
FROM table t
FOR XML PATH (''), ROOT('Taxonomy')
gives this output:
<Taxonomy>
<Category Level="1">Clothing</Category>
<Category Level="2">Jeans</Category>
</Taxonomy>
Still have to figure out how to put this partial coding in a much larger code with several 'nested' FOR XML's already
The shortcut methods may not cut it for this. AUTO and PATH don't like multiple elements with the same name. Looks like you would have to use the FOR XML EXPLICIT command.
It works, but is cumbersome.
Sample:
--Generate Sample Data
--FOR XML EXPLICIT requires two meta fields: Tag and Parent
--Tag is the ID of the current element.
--Parent is the ID of the parent element, or NULL for root element.
DECLARE #DataTable as table
(Tag int NOT NULL
, Parent int
, TaxonomyValue nvarchar(max)
, CategoryValue nvarchar(max)
, CategoryLevel int)
--Fill with sample data: Category Element (2), under Taxonomy(1), with no Taxonomy value.
INSERT INTO #DataTable
VALUES (2, 1, NULL, 1, 'Clothing')
, (2, 1, NULL, 2, 'Jeans')
--First part of query: Define the XML structure
SELECT
1 as Tag --root element
, NULL as Parent
, NULL as [Taxonomy!1] --Assign Taxonomy Element to the first element, aka root.
, NULL as [Category!2] --Assign Category Element as a child to Taxonomy.
, NULL as [Category!2!Level] --Give Category an Attribute 'Level'
--The actual data to fill the XML
UNION
SELECT
Data.Tag
, Data.Parent
, Data.TaxonomyValue
, Data.CategoryValue
, Data.CategoryLevel
FROM
#DataTable as Data
FOR XML EXPLICIT
Generates XML
<Taxonomy>
<Category Level="1">Clothing</Category>
<Category Level="2">Jeans</Category>
</Taxonomy>
Edit: Had columns reversed. No more Jeans level.
I know this is an old post, but I want to share one solution that avoids that FOR XML EXPLICIT command complexity for big xmls.
It's enough to add null as a child of Taxonomy, and error will disappear:
select 1 as 'Taxonomy/Category/#Level',
t.MainCat as 'Taxonomy/Category',
NULL AS 'Taxonomy/*',
2 as 'Taxonomy/Category/#Level',
t.SubCat as 'Taxonomy/Category',
from t
I hope it helps.
I have another way. It seemed a tad bit easy to me.
Say, for example I have an xml like
DECLARE #xml xml='<parameters></parameters>'
DECLARE #multiplenodes XML = '<test1><test2 Level="1">This is a test node 2</test2><test2 Level="2">This is another test node</test2></test1>'
SET #xml.modify('insert sql:variable("#multiplenodes") into (/parameters)[1]')
SELECT #xml
Do tell me if this helps.

SQL Server XQuery - Selecting a Subset

Take for example the following XML:
Initial Data
<computer_book>
<title>Selecting XML Nodes the Fun and Easy Way</title>
<isbn>9999999999999</isbn>
<pages>500</pages>
<backing>paperback</backing>
</computer_book>
and:
<cooking_book>
<title>50 Quick and Easy XML Dishes</title>
<isbn>5555555555555</isbn>
<pages>275</pages>
<backing>paperback</backing>
</cooking_book>
I have something similar in a single xml-typed column of a SQL Server 2008 database. Using SQL Server XQuery, would it be possible to get results such as this:
Resulting Data
<computer_book>
<title>Selecting XML Nodes the Fun and Easy Way</title>
<pages>500</pages>
</computer_book>
and:
<cooking_book>
<title>50 Quick and Easy XML Dishes</title>
<isbn>5555555555555</isbn>
</cooking_book>
Please note that I am not referring to selecting both examples in one query; rather I am selecting each via its primary key (which is in another column). In each case, I am essentially trying to select the root and an arbitrary subset of children. The roots can be different, as seen above, so I do not believe I can hard-code the root node name into a "for xml" clause.
I have a feeling SQL Server's XQuery capabilities will not allow this, and that is fine if it is the case. If I can accomplish this, however, I would greatly appreciate an example.
Here is the test data I used in the queries below:
declare #T table (XMLCol xml)
insert into #T values
('<computer_book>
<title>Selecting XML Nodes the Fun and Easy Way</title>
<isbn>9999999999999</isbn>
<pages>500</pages>
<backing>paperback</backing>
</computer_book>'),
('<cooking_book>
<title>50 Quick and Easy XML Dishes</title>
<isbn>5555555555555</isbn>
<pages>275</pages>
<backing>paperback</backing>
</cooking_book>')
You can filter the nodes under to root node like this using local-name() and a list of the node names you want:
select XMLCol.query('/*/*[local-name()=("isbn","pages")]')
from #T
Result:
<isbn>9999999999999</isbn><pages>500</pages>
<isbn>5555555555555</isbn><pages>275</pages>
If I understand you correctly the problem with this is that you don't get the root node back.
This query will give you an empty root node:
select cast('<'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'/>' as xml)
from #T
Result:
<computer_book />
<cooking_book />
From this I have found two solutions for you.
Solution 1
Get the nodes from your table to a table variable and then modify the XML to look like you want.
-- Table variable to hold the node(s) you want
declare #T2 table (RootNode xml, ChildNodes xml)
-- Fetch the xml from your table
insert into #T2
select cast('<'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'/>' as xml),
XMLCol.query('/*/*[local-name()=("isbn","pages")]')
from #T
-- Add the child nodes to the root node
update #T2 set
RootNode.modify('insert sql:column("ChildNodes") into (/*)[1]')
-- Fetch the modified XML
select RootNode
from #T2
Result:
RootNode
<computer_book><isbn>9999999999999</isbn><pages>500</pages></computer_book>
<cooking_book><isbn>5555555555555</isbn><pages>275</pages></cooking_book>
The sad part with this solution is that it does not work with SQL Server 2005.
Solution 2
Get the parts, build the XML as a string and cast it back to XML.
select cast('<'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'>'+
cast(XMLCol.query('/*/*[local-name()=("isbn","pages")]') as varchar(max))+
'</'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'>' as xml)
from #T
Result:
<computer_book><isbn>9999999999999</isbn><pages>500</pages></computer_book>
<cooking_book><isbn>5555555555555</isbn><pages>275</pages></cooking_book>
Making the nodes parameterized
In the queries above the nodes you get as child nodes is hard coded in the query. You can use sql:varaible() to do this instead. I have not found a way of making the number of nodes dynamic but you can add as many as you think you need and have null as value for the nodes you don't need.
declare #N1 varchar(10)
declare #N2 varchar(10)
declare #N3 varchar(10)
declare #N4 varchar(10)
set #N1 = 'isbn'
set #N2 = 'pages'
set #N3 = 'backing'
set #N4 = null
select cast('<'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'>'+
cast(XMLCol.query('/*/*[local-name()=(sql:variable("#N1"),
sql:variable("#N2"),
sql:variable("#N3"),
sql:variable("#N4"))]') as varchar(max))+
'</'+XMLCol.value('local-name(/*[1])', 'varchar(100)')+'>' as xml)
from #T
Result:
<computer_book><isbn>9999999999999</isbn><pages>500</pages><backing>paperback</backing></computer_book>
<cooking_book><isbn>5555555555555</isbn><pages>275</pages><backing>paperback</backing></cooking_book>

Resources