Querying XML Data Using SQL Parameters - sql-server

I am trying to query XML data in a table using a SQL parameter containing the search criteria and I am getting unexpected results.
As an example, consider the following T-SQL code:
DECLARE #Configuration AS TABLE
(
[Data] XML NOT NULL
);
INSERT INTO Configuration ([Data]) VALUES '<Configuration><Colors><Color>RED</Color><Color>BLUE</Color></Colors></Configuration>');
DECLARE #Color AS NVARCHAR(MAX);
SET #Color = N'YELLOW';
SELECT COUNT(*) FROM #Configuration WHERE [Data].exist('/Configuration/Colors/Color/text() = sql:variable("#Color")') = 1;
I would expect the result of this query to return 0. In fact, this query returns 1. Even worse, it appears to return one regardless of what value I set #Color to.
What am I missing?

You have missing [ in your query
SELECT COUNT(*) FROM #Configuration WHERE [Data].exist('/Configuration/Colors/Color[text() = sql:variable("#Color")]') = 1;
XPath expressions are supposed to be enclosed between [ and ]. For more information on XPath and writing expressions, please refer this tutorial page.

Related

Method exist in SQL Server does not work correctly with C# parameters #XPath in a request

The method exist for XML object does not work correctly with parameter in SQL request from .NET backend side. In my case, I try to send #XPath parameter to the request and put whole this one through exist method to select all XML items according to this XPath but as result, I get all non-filtered by these XPath XML from a table.
exec sp_executesql N'SELECT TOP(1000) ord.TrackingID, ord.DateCreate, ord.OrderType, ord.Error_Description, ord.Response, ord.Request
FROM FilteredByXPath AS ord
WHERE (ord.Request.exist(''sql:variable("#XPath")'') = 1)',
N'#TrackingID nvarchar(4000),#ShowAutotestCases bit,#DateFrom datetime,#DateTo datetime,#InputOrderSource nvarchar(1),#Agency int, #XPath nvarchar(max)',
#TrackingID=NULL,#ShowAutotestCases=0,#DateFrom='1753-01-01 00:00:00',#DateTo='9999-12-31 23:59:59.997',#InputOrderSource=N'%',#Agency=0, #XPath=N'//*[text() = "10:33:34.9375000-05:00"]'
Following the exist description I think the ''sql:variable("#XPath")'' returns all time nonempty result and all XML is suitable.
The variants which work well are
WHERE ord.Request.exist(''//*[text() = sql:variable("#XPath")]'') = 1
or
WHERE ord.Request.exist(''//*[text() = "10:33:34.9375000-05:00"]'') = 1
As we see the two variants well when we use XPath as a string directly or #XPath parameter into XQuery.
The question how can I use #XPath parameter as a full XQuery expression like in first variant that does not work.
WHERE (ord.Request.exist(''sql:variable("#XPath")'') = 1)

Sql Server - How do I get JSON nested value in my SQL Select statement

Environment: SQL Server 2014 and above
How do I access the email value in my JSON value with my SELECT statement?
select JSON_VALUE('[{"data":{"email":"test#email.com"}}]', '$.email') as test
Json support was only introduced in SQL Server 2016 - so with any prior version you would need to either use string manipulation code or simply parse the json outside of SQL Server (maybe using a CLR function)
For 2016 version or higher, you can use JSON_VALUE like this:
declare #json as varchar(100) = '[{"data":{"email":"test#email.com"}}]';
select JSON_VALUE(#json, '$[0].data.email') as test
For older versions - you might be able to get away with this, but if your json value does not contain an email property, you will get unexpected results:
select substring(string, start, charindex('"', string, start+1) - start) as test
from (
select #json as string, charindex('"email":"', #json) + 9 as start
) s
You can see a live demo on db<>fiddle
Another way. PatternSplitCM is great for stuff like this.
Extract a single Email value:
DECLARE #json as varchar(200) = '[{"data":{"email":"test#email.com"}}]';
SELECT f.Item
FROM dbo.patternsplitCM(#json,'[a-z0-9#.]') AS f
WHERE f.item LIKE '%[a-z]%#%.%[a-z]%'; -- Simple Email Check Pattern
Extracting all Email Addresses (if/when there are more):
DECLARE #json VARCHAR(200) = '[{"data":{"email":"test#email.com"},{"email2":"test2#email.net"}},{"data":{"MoreEmail":"test3#email.555whatever"}}]';
SELECT f.Item
FROM dbo.patternsplitCM(#json,'[a-z0-9#.]') AS f
WHERE f.item LIKE '%[a-z]%#%.%[a-z]%'; -- Simple Email Check Pattern
Returns:
Item
--------------------------
test#email.com
test2#email.net
test3#email.555whatever
Or... the get only the first Email address that appears:
SELECT TOP (1) f.Item
FROM dbo.patternsplitCM(#json,'[a-z0-9#.]') AS f
WHERE f.item LIKE '%[a-z]%#%.%[a-z]%' -- Simple Email Check Pattern
ORDER BY ROW_NUMBER() OVER (ORDER BY f.ItemNumber)
Nasty fast, super-simple. No cursors, loops or other bad stuff.
With v2014 there is no JSON support, but - if your real JSON is that simple - it is sometimes a good idea to use some replacements in order to transform the JSON to XML like here, which allows for the native XML methods:
DECLARE #YourJSON NVARCHAR(MAX)=N'[{"data":{"email":"test#email.com"}}]';
SELECT CAST(REPLACE(REPLACE(REPLACE(REPLACE(#YourJSON,'[{"','<'),'":{"',' '),'":"','="'),'}}]',' />') AS XML).value('(/data/#email)[1]','nvarchar(max)');
It can be done in two ways:
First, if your JSON data is between [ ] like in your question:
select JSON_VALUE('[{"data":{"email":"test#email.com"}}]','$[0].data.email' ) as test
And if your JSON data is not between [ ]:
select JSON_VALUE('{"data":{"email":"test#email.com"}}','$.data.email' ) as test
You can teste the code above here
Your query should be like this (SQL Server 2016):
DECLARE #json_string NVARCHAR(MAX) = 'your_json_value'
SELECT [key],value
FROM OPENJSON(#json_string, '$.email'))
UPDATE :
select JSON_VALUE(#json_string, '$[0].data.email') as test

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.

Parsing xml in sql server

I have a table with an ntext type column that holds xml. I have tried to apply many examples of how to pull the value for the company's name from the xml for a particular node, but continue to get a syntax error. Below is what I've done, except substituted my select statement for the actual xml output
DECLARE #companyxml xml
SET #companyxml =
'<Home>
<slideshowImage1>1105</slideshowImage1>
<slideshowImage2>1106</slideshowImage2>
<slideshowImage3>1107</slideshowImage3>
<slideshowImage4>1108</slideshowImage4>
<slideshowImage5>1109</slideshowImage5>
<bottomNavImg1>1155</bottomNavImg1>
<bottomNavImg2>1156</bottomNavImg2>
<bottomNavImg3>1157</bottomNavImg3>
<pageTitle>Acme Capital Management |Homepage</pageTitle>
<metaKeywords><![CDATA[]]></metaKeywords>
<metaDescription><![CDATA[]]></metaDescription>
<companyName>Acme Capital Management</companyName>
<logoImg>1110</logoImg>
<pageHeader></pageHeader>
</Home>'
SELECT c.value ('companyName','varchar(1000)') AS companyname
FROM #companyxml.nodes('/Home') AS c
For some reason, the select c.value statement has a syntax problem that I can't figure out. On hover in SSMS, it says 'cannot find either column "c" or the user-defined function or aggregate "c.value", or the name is ambiguous.'
Any help on the syntax would be greatly appreciated.
try this
DECLARE #companyxml xml
SET #companyxml =
'<Home>
<slideshowImage1>1105</slideshowImage1>
<slideshowImage2>1106</slideshowImage2>
<slideshowImage3>1107</slideshowImage3>
<slideshowImage4>1108</slideshowImage4>
<slideshowImage5>1109</slideshowImage5>
<bottomNavImg1>1155</bottomNavImg1>
<bottomNavImg2>1156</bottomNavImg2>
<bottomNavImg3>1157</bottomNavImg3>
<pageTitle>Acme Capital Management Homepage</pageTitle>
<metaKeywords>CDATA</metaKeywords>
<metaDescription>CDATA</metaDescription>
<companyName>Acme Capital Management</companyName>
<logoImg>1110</logoImg>
<pageHeader></pageHeader>
</Home>'
DECLARE #Result AS varchar(50)
SET #result = #companyxml.value('(/Home/companyName/text())[1]','varchar(50)')
SELECT #result

In SQL Server can I insert multiple nodes into XML from a table?

I want to generate some XML in a stored procedure based on data in a table.
The following insert allows me to add many nodes but they have to be hard-coded or use variables (sql:variable):
SET #MyXml.modify('
insert
<myNode>
{sql:variable("#MyVariable")}
</myNode>
into (/root[1]) ')
So I could loop through each record in my table, put the values I need into variables and execute the above statement.
But is there a way I can do this by just combining with a select statement and avoiding the loop?
Edit I have used SELECT FOR XML to do similar stuff before but I always find it hard to read when working with a hierarchy of data from multiple tables. I was hoping there would be something using the modify where the XML generated is more explicit and more controllable.
Have you tried nesting FOR XML PATH scalar valued functions?
With the nesting technique, you can brake your SQL into very managable/readable elemental pieces
Disclaimer: the following, while adapted from a working example, has not itself been literally tested
Some reference links for the general audience
http://msdn2.microsoft.com/en-us/library/ms178107(SQL.90).aspx
http://msdn2.microsoft.com/en-us/library/ms189885(SQL.90).aspx
The simplest, lowest level nested node example
Consider the following invocation
DECLARE #NestedInput_SpecificDogNameId int
SET #NestedInput_SpecificDogNameId = 99
SELECT [dbo].[udfGetLowestLevelNestedNode_SpecificDogName]
(#NestedInput_SpecificDogNameId)
Let's say had udfGetLowestLevelNestedNode_SpecificDogName had been written without the FOR XML PATH clause, and for #NestedInput_SpecificDogName = 99 it returns the single rowset record:
#SpecificDogNameId DogName
99 Astro
But with the FOR XML PATH clause,
CREATE FUNCTION dbo.udfGetLowestLevelNestedNode_SpecificDogName
(
#NestedInput_SpecificDogNameId
)
RETURNS XML
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar XML
-- Add the T-SQL statements to compute the return value here
SET #ResultVar =
(
SELECT
#SpecificDogNameId as "#SpecificDogNameId",
t.DogName
FROM tblDogs t
FOR XML PATH('Dog')
)
-- Return the result of the function
RETURN #ResultVar
END
the user-defined function produces the following XML (the # signs causes the SpecificDogNameId field to be returned as an attribute)
<Dog SpecificDogNameId=99>Astro</Dog>
Nesting User-defined Functions of XML Type
User-defined functions such as the above udfGetLowestLevelNestedNode_SpecificDogName can be nested to provide a powerful method to produce complex XML.
For example, the function
CREATE FUNCTION [dbo].[udfGetDogCollectionNode]()
RETURNS XML
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar XML
-- Add the T-SQL statements to compute the return value here
SET #ResultVar =
(
SELECT
[dbo].[udfGetLowestLevelNestedNode_SpecificDogName]
(t.SpecificDogNameId)
FROM tblDogs t
FOR XML PATH('DogCollection') ELEMENTS
)
-- Return the result of the function
RETURN #ResultVar
END
when invoked as
SELECT [dbo].[udfGetDogCollectionNode]()
might produce the complex XML node (given the appropriate underlying data)
<DogCollection>
<Dog SpecificDogNameId="88">Dino</Dog>
<Dog SpecificDogNameId="99">Astro</Dog>
</DogCollection>
From here, you could keep working upwards in the nested tree to build as complex an XML structure as you please
CREATE FUNCTION [dbo].[udfGetAnimalCollectionNode]()
RETURNS XML
AS
BEGIN
DECLARE #ResultVar XML
SET #ResultVar =
(
SELECT
dbo.udfGetDogCollectionNode(),
dbo.udfGetCatCollectionNode()
FOR XML PATH('AnimalCollection'), ELEMENTS XSINIL
)
RETURN #ResultVar
END
when invoked as
SELECT [dbo].[udfGetAnimalCollectionNode]()
the udf might produce the more complex XML node (given the appropriate underlying data)
<AnimalCollection>
<DogCollection>
<Dog SpecificDogNameId="88">Dino</Dog>
<Dog SpecificDogNameId="99">Astro</Dog>
</DogCollection>
<CatCollection>
<Cat SpecificCatNameId="11">Sylvester</Cat>
<Cat SpecificCatNameId="22">Tom</Cat>
<Cat SpecificCatNameId="33">Felix</Cat>
</CatCollection>
</AnimalCollection>
Use sql:column instead of sql:variable. You can find detailed info here: http://msdn.microsoft.com/en-us/library/ms191214.aspx
Can you tell a bit more about what exactly you are planning to do.
Is it simply generating XML data based on a content of the table
or adding some data from the table to an existing xml structure?
There are great series of articles on the subject on XML in SQLServer written by Jacob Sebastian, it starts with the basics of generating XML from the data in the table

Resources