SQL REPLACE function inside Xml.modify 'replace value of' - sql-server

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

Related

Is there a way to return either a string or embedded JSON using FOR JSON?

I have a nvarchar column that I would like to return embedded in my JSON results if the contents is valid JSON, or as a string otherwise.
Here is what I've tried:
select
(
case when IsJson(Arguments) = 1 then
Json_Query(Arguments)
else
Arguments
end
) Results
from Unit
for json path
This always puts Results into a string.
The following works, but only if the attribute contains valid JSON:
select
(
Json_Query(
case when IsJson(Arguments) = 1 then
Arguments
else
'"' + String_escape(IsNull(Arguments, ''), 'json') + '"' end
)
) Results
from Unit
for json path
If Arguments does not contain a JSON object a runtime error occurs.
Update: Sample data:
Arguments
---------
{ "a": "b" }
Some text
Update: any version of SQL Server will do. I'd even be happy to know that it's coming in a beta or something.
I did not find a good solution and would be happy, if someone comes around with a better one than this hack:
DECLARE #tbl TABLE(ID INT IDENTITY,Arguments NVARCHAR(MAX));
INSERT INTO #tbl VALUES
(NULL)
,('plain text')
,('[{"id":"1"},{"id":"2"}]');
SELECT t1.ID
,(SELECT Arguments FROM #tbl t2 WHERE t2.ID=t1.ID AND ISJSON(Arguments)=0) Arguments
,(SELECT JSON_QUERY(Arguments) FROM #tbl t2 WHERE t2.ID=t1.ID AND ISJSON(Arguments)=1) ArgumentsJSON
FROM #tbl t1
FOR JSON PATH;
As NULL-values are omitted, you will always find eiter Arguments or ArgumentsJSON in your final result. Treating this JSON as NVARCHAR(MAX) you can use REPLACE to rename all to the same Arguments.
The problem seems to be, that you cannot include two columns with the same name within your SELECT, but each column must have a predictable type. This depends on the order you use in CASE (or COALESCE). If the engine thinks "Okay, here's text", all will be treated as text and your JSON is escaped. But if the engine thinks "Okay, some JSON", everything is handled as JSON and will break if this JSON is not valid.
With FOR XML PATH there are some tricks with column namig (such as [*], [node()] or even twice the same within one query), but FOR JSON PATH is not that powerfull...
When you say that your statement "... always puts Results into a string.", you probably mean that when JSON is stored in a text column, FOR JSON escapes this text. Of course, if you want to return an unescaped JSON text, you need to use JSON_QUERY function only for your valid JSON text.
Next is a small workaround (based on FOR JSON and string manipulation), that may help to solve your problem.
Table:
CREATE TABLE #Data (
Arguments nvarchar(max)
)
INSERT INTO #Data
(Arguments)
VALUES
('{"a": "b"}'),
('Some text'),
('{"c": "d"}'),
('{"e": "f"}'),
('More[]text')
Statement:
SELECT CONCAT(N'[', j1.JsonOutput, N',', j2.JsonOutput, N']')
FROM
(
SELECT JSON_QUERY(Arguments) AS Results
FROM #Data
WHERE ISJSON(Arguments) = 1
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) j1 (JsonOutput),
(
SELECT STRING_ESCAPE(ISNULL(Arguments, ''), 'json') AS Results
FROM #Data
WHERE ISJSON(Arguments) = 0
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) j2 (JsonOutput)
Output:
[{"Results":{"a": "b"}},{"Results":{"c": "d"}},{"Results":{"e": "f"}},{"Results":"Some text"},{"Results":"More[]text"}]
Notes:
One disadvantage here is that the order of the items in the generated output is not the same as in the table.

SQL query to get only image name with extension without complete path from SQL table

I have a column in a table that saves image along with the path.
Below are few image path examples:
/Products_images/Essentials/111219_Essent_Org_Red_Clsd-XL.jpg
/ul/products/1417/130514-Montecito-20-spinner-French-Roast.jpg
/ul/products/1470/130419_RodeoDrive_20HybridRed-M.jpg
/ul/newproducts/266-07-437-TVC-main.jpg
When i use sql query:
select Image from Product
It returns complete image path as it should be.
How can i fetch only image name with extension from this image column without the image path.
So my required result should be:
111219_Essent_Org_Red_Clsd-XL.jpg
130514-Montecito-20-spinner-French-Roast.jpg
130419_RodeoDrive_20HybridRed-M.jpg
266-07-437-TVC-main.jpg
Is there any way to get this directly using sql query and not using c# code.
Is there any Substring or Regex in SQL Server.
I would be very thank full for your help.
Thanks.
Can you try it
declare #a VARCHAR(200)
set #a = ' /Products_images/Essentials/111219_Essent_Org_Red_Clsd-XL.jpg'
select right(#a, charindex('/', reverse(#a)) - 1) as filename
In your case
Select right(ColumnName, charindex('/', reverse(ColumnName)) - 1) as filename From TableName
One way of doing it -
DECLARE #Image TABLE (Imagepath VARCHAR(2000))
INSERT INTO #Image(Imagepath)
SELECT '/Products_images/Essentials/111219_Essent_Org_Red_Clsd-XL.jpg' UNION
SELECT '/ul/products/1417/130514-Montecito-20-spinner-French-Roast.jpg' UNION
SELECT '/ul/products/1470/130419_RodeoDrive_20HybridRed-M.jpg' UNION
SELECT '/ul/newproducts/266-07-437-TVC-main.jpg'
SELECT REVERSE(SUBSTRING(REVERSE(Imagepath),1,CHARINDEX('/',REVERSE(Imagepath))-1)) FROM #Image
when you got your image name and path in your string you can use the split function to get the last item after the /
try this:
string onlyImageName = stringFullPathAndName.Split('/').Last();
Use SQL Functions to extract only the file names:
SELECT
LTRIM(
RTRIM(
REVERSE(
SUBSTRING(
REVERSE(ImagePath),
0,
CHARINDEX('/', REVERSE(ImagePath),0)
)
)
)
) AS FilenameOnly
FROM SomeTable

SQL Server FOR XML PATH: Set xml-declaration or processing instruction "xml-stylesheet" on top

I want to set a processing instruction to include a stylesheet on top of an XML:
The same issue was with the xml-declaration (e.g. <?xml version="1.0" encoding="utf-8"?>)
Desired result:
<?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>
<TestPath>
<Test>Test</Test>
<SomeMore>SomeMore</SomeMore>
</TestPath>
My research brought me to node test syntax and processing-instruction().
This
SELECT 'type="text/xsl" href="stylesheet.xsl"' AS [processing-instruction(xml-stylesheet)]
,'Test' AS Test
,'SomeMore' AS SomeMore
FOR XML PATH('TestPath')
produces this:
<TestPath>
<?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>
<Test>Test</Test>
<SomeMore>SomeMore</SomeMore>
</TestPath>
All hints I found tell me to convert the XML to VARCHAR, concatenate it "manually" and convert it back to XML. But this is - how to say - ugly?
This works obviously:
SELECT CAST(
'<?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>
<TestPath>
<Test>Test</Test>
<SomeMore>SomeMore</SomeMore>
</TestPath>' AS XML);
Is there a chance to solve this?
There is another way, which will need two steps but don't need you to treat the XML as string anywhere in the process :
declare #result XML =
(
SELECT
'Test' AS Test,
'SomeMore' AS SomeMore
FOR XML PATH('TestPath')
)
set #result.modify('
insert <?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>
before /*[1]
')
Sqlfiddle Demo
The XQuery expression passed to modify() function tells SQL Server to insert the processing instruction node before the root element of the XML.
UPDATE :
Found another alternative based on the following thread : Merge the two xml fragments into one? . I personally prefer this way :
SELECT CONVERT(XML, '<?xml-stylesheet type="text/xsl" href="stylesheet.xsl"?>'),
(
SELECT
'Test' AS Test,
'SomeMore' AS SomeMore
FOR XML PATH('TestPath')
)
FOR XML PATH('')
Sqlfiddle Demo
As it came out, har07's great answer does not work with an XML-declaration. The only way I could find was this:
DECLARE #ExistingXML XML=
(
SELECT
'Test' AS Test,
'SomeMore' AS SomeMore
FOR XML PATH('TestPath'),TYPE
);
DECLARE #XmlWithDeclaration NVARCHAR(MAX)=
(
SELECT N'<?xml version="1.0" encoding="UTF-8"?>'
+
CAST(#ExistingXml AS NVARCHAR(MAX))
);
SELECT #XmlWithDeclaration;
You must stay in the string line after this step, any conversion to real XML will either give an error (when the encoding is other then UTF-16) or will omit this xml-declaration.

Parse nvarchar value that looks like a simplified XML

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.

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