XQuery Comparison Expressions in SQL Column - sql

I would like to have a table that I can store XQuery Comparison Expressions in, so that I can evaluate them in a query.
I've been doing a bit of R&D into if it is possible, and I'm struggling.
If I put an XQuery expression in a column, then it seems to evaluate differently to if I put the XQuery expression directly into the query. For example, when I run the below query:
declare
#x xml = ''
create table #condition
(
condition nvarchar(255)
)
insert into #condition
values
('''1''=''1''')
select
condition,
#x.query('sql:column("condition")'),
#x.query('''1''=''1''')
from #condition
I would expect this to return:
'1'='1', true, true
However it actually returns:
'1'='1', '1'='1', true
Does anybody know how I can evaluate comparison expressions that are stored in a column?
The eventual plan is to be able to use this technique to filter down rows of a table based on XQuery conditions present. So ultimately I'd want to be able to do this in the where clause of a select statement.
I've put the above example into an sql fiddle encase it is useful.
Many thanks

Short answer: Unfortunately you can't.
sql:column("condition") will be evaluated to a suitable XML primitive data type based on the table column type. In this case the value from condition column will always be evaluated as XML string type instead of an XQuery statement, as you have figured out from running your sample query. And I can't see anyway of evaluating dynamic XQuery statement, unless you want to construct the entire query dynamically and execute it later on possibly using sp_executesql.

Try this query:
declare
#x xml = ''
create table #condition
(
condition nvarchar(255)
)
insert into #condition
values
('''1''=''1''')
select
condition,
case when col1 like col2 then 'True' else 'False' END col,
quer
from
(
select
condition,
PARSENAME(REPLACE(condition,'=','.'),2) col1,
PARSENAME(REPLACE(condition,'=','.'),1) col2 ,
#x.query('''1''=''1''') as quer
from #condition
)base

Related

T-SQL get substring

I am looking to get an order number from a column named KEY_Ref, this ref column have various contents, but some rows look like this
LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^
Now I am interested in getting the value for ORDER_NO (176572 in this case)
How would I (In SQL Server) go about getting this (Or other) value from the main string
The logic is always
key1=value1^key2=value2^key3=value3^
You can use string_split():
select t.*, s.orderno
from t outer apply
(select stuff(s.value, 1, 9, '') as orderno
from string_split(t.key_ref, '^') s
where s.value like 'ORDER_NO=%'
) s;
Here is a db<>fiddle.
this is going to be a bit lengthy answer however if your SQL server version doesn't support string_split function you may use this.
declare #str varchar(100) = 'LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^'
declare #substr1 varchar(50) = substring(#str,charindex('^',#str)+1,len(#str))
declare #substr2 varchar(50) = substring(#substr1,charindex('=',#substr1)+1,charindex('^',#substr1)-charindex('=',#substr1)-1)
select #substr2 as 'order number'
the final variable will produce the desired value and you must merge the above queries to a single query that can fetch the value from the table in a single select statement.
this will work only if the pattern doesn't deviate from the one you've mentioned.

why I need a dynamic query for "column value from a query"

For example this returns a value from a query, which I will then use as a column name.
#A=Select top 1 productid from productlist order by timestamp desc
then I would like this "productid" A to be used in the other table
Select #A from customerlist
then the result is #A value instead of field value in customerlist.
When I use dynamic query, I can get right result.
Why?
(I know I can use join but because this productlist table is dynamic, so let's assume it is a sub query)
You need "dynamic SQL" because SQL will NOT allow you to use a parameter as a column name or a table name. You can only use parameters for data values such as in a where clause where column1 = #val
set #A = 'çolumn1'
Select #A from customerlist -- this fails because it is not allowed
Dynamic SQL is a "hack" to get around those restrictions as the SQL statement is placed into a string along with any value held by parameters.
set #A = 'çolumn1'
set #SQL = 'Select ' + #A + ' from customerlist;'
execute #SQL -- this works, the SQL statement is valid with no parameters as column names
The string formed as #SQL is a complete sql statement without needing any parameters as column names.
Note: the syntax I used here is incomplete and is based on MS SQL Server, different databases will use a different, but similar, syntax.

SQL HASHBYTES function returns weird output when used in CASE WHEN/IIF

I have written a stored procedure that hashes the value of a certain column. I need to use this HASHBYTES function in a CASE WHEN or IIF statement, like this:
DECLARE #Hash varchar(255) = 'testvalue'
SELECT IIF(1=1, HASHBYTES('SHA1',#Hash), #Hash)
SELECT CASE WHEN 1=1 THEN HASHBYTES('SHA1',#Hash) END AS Hashcolumn
I can't get my head around why I get different outputs from above queries? it seems that whenever I add an ELSE in the CASE WHEN / IIF statement, it returns a string of weird characters (like ü<þ+OUL'RDOk{­\Ìø in above example).
Can anyone tell me why this is happening? I need to use the CASE WHEN or IIF.
Thanks guys
IIF returns the data type with the highest precedence from the types in true_value and false_value. In this case, it's #Hash1 which is varchar(255) so your result is getting cast to varchar(255). See below.
DECLARE #Hash varchar(255) = 'testvalue'
SELECT cast(HASHBYTES('SHA1',#Hash) as varchar(255))
Similarly, CASE works the same way. However, if you don't add an ELSE or another WHEN that would conflict with the data type, it will work. This is because an ELSE NULL is implied. i.e.
SELECT CASE WHEN 1=1 THEN HASHBYTES('SHA1',#Hash) END
However, if you add another check, then precedence kicks in, and it will be converted.
SELECT CASE WHEN 1=1 THEN HASHBYTES('SHA1',#Hash) WHEN 1=2 THEN #Hash END AS Hashcolumn
SELECT CASE WHEN 1=1 THEN HASHBYTES('SHA1',#Hash) ELSE #Hash END AS Hashcolumn
The output of a select query is a virtual table. In a relational db a column of a table is constrained to single data type.. so here what happens is implicit conversion is being done by the server engine inorder to render a sigle type and hence weird characters are returned.
The nature of conversion is as #scsimon says it follows highest precedence order.
The following query should help.
DECLARE #Hash varchar(255) = 'testvalue'
SELECT IIF(1=1, CONVERT(VARCHAR(255),HASHBYTES('SHA1',#Hash),2), #Hash)
SELECT CASE WHEN 1=2 THEN CONVERT(VARCHAR(255),HASHBYTES('SHA1',#Hash),2)
ELSE #Hash END AS Hashcolumn

How can I search a SQL database with multiple "%" wildcards?

I am trying to write a SQL query in SQL Server 2008 R2 that will allow a user to search a database table by a number of parameters. The way this should work is, my user enters his criteria and the query looks for all close matches, while ignoring those criteria for which the user did not enter a value.
I've written my query using LIKE and parameters, like so:
select item
from [item]
where a like #a and b like #b and c like #c ...
where 'a', 'b', and 'c' are table columns, and my # parameters all default to '%' wildcards. This goes on for about twenty different columns, and that leads to my problem: if this query is entered as is, no input, just wildcards, it returns no results. Yet this table contains over 30,000 rows, so an all-wildcard query should return the whole table. Obviously I'm going about this the wrong way, but I don't know how to correct it.
I can't use 'contains' or 'freetext', as those look for whole words, and I need to match user input no matter where it occurs in the actual column value. I've tried breaking my query up into individual steps using 'intersect', but that doesn't change anything. Does anyone know a better way to do this?
To allow for null inputs, this is a good pattern:
select * from my table where ColA LIKE isnull(#a, ColA) AND ColB like isnull(#b, ColB)
This avoids having to construct and execute a dynamic SQL statement (and creating possible SQL injection issues.)
my # parameters all default to '%' wildcards
Don't do this. Default them to null. The way to disregard empty parameters is with a short circuit:
(#a IS NULL OR #a LIKE a)
Depending on how you want to handle missing data in the column, you might want a third term, because null will not match LIKE statements:
(#a IS NULL OR a IS NULL OR #a LIKE a)
How can I search a SQL database with multiple "%" wildcards?
Slowly. SQL is suboptimal for doing text comparisons. The best approach is to perform this search somewhere else, or at least structure your data to facilitate these kinds of queries. If you know you'll be performing a lot of these queries, consider redesigning your schema in the shape of a suffix tree. At an absolute bare minimum, do something so that every LIKE match is suffix-only, meaning LIKE 'xxx%' and never LIKE '%xxx' or LIKE 'x%x'. The latter two preclude the use of indexes. And put an index on every column you need to search.
Thanks for the guidance, all. It turns out that the table I'm querying can easily contain null values in the columns I'm searching against, so I expanded my query to say "where (a like #a or a is null) and ... " and it works now.
Personally, I'd do this in the application layer (assuming you have one), and build the query around the parameters the user supplies, eliminating the ones they don't.
For example, the following bit of code builds the query in SQL, where only the parameters the user has supplied (not null) are included in the where clause.
NOTE: This is very crude, as it doesn't take into account AND's if the first parameter if null, and it doesn't remove the WHERE clause if no parameters are supplied. If you let me know what language your application layer is built in, I'll provide a better example. (This is purely pseudo-code!)
DECLARE #a VARCHAR(100) = '''%SomeValue%''', #b VARCHAR(100)= '''%AnotherValue%''', #c VARCHAR(100)
DECLARE #SQL VARCHAR(MAX) = 'SELECT * FROM MyTable WHERE'
IF #a IS NOT NULL
BEGIN
SET #SQL += ' ColA LIKE ' + #a
END
IF #b IS NOT NULL
BEGIN
SET #SQL += ' AND ColA LIKE ' + #b
END
IF #c IS NOT NULL
BEGIN
SET #SQL += ' AND ColC LIKE ' + #c
END
PRINT #SQL
--EXEC(#SQL)
Output:
SELECT * FROM MyTable WHERE ColA = '%SomeValue%' AND ColB = '%AnotherValue%'

SQL Server - Replacing Single Quotes and Using IN

I am passing a comma-delimited list of values into a stored procedure. I need to execute a query to see if the ID of an entity is in the comma-delimited list. Unfortunately, I think I do not understand something.
When I execute the following stored procedure:
exec dbo.myStoredProcedure #myFilter=N'1, 2, 3, 4'
I receive the following error:
"Conversion failed when converting the varchar value '1, 2, 3, 4' to data type int."
My stored procedure is fairly basic. It looks like this:
CREATE PROCEDURE [dbo].[myStoredProcedure]
#myFilter nvarchar(512) = NULL
AS
SET NOCOUNT ON
BEGIN
-- Remove the quote marks so the filter will work with the "IN" statement
SELECT #myFilter = REPLACE(#myFilter, '''', '')
-- Execute the query
SELECT
t.ID,
t.Name
FROM
MyTable t
WHERE
t.ID IN (#myFilter)
ORDER BY
t.Name
END
How do I use a parameter in a SQL statement as described above? Thank you!
You could make function that takes your parameter, slipts it and returns table with all the numbers in it.
If your are working with lists or arrays in SQL Server, I recommend that you read Erland Sommarskogs wonderful stuff:
Arrays and Lists in SQL Server 2005
You need to split the string and dump it into a temp table. Then you join against the temp table.
There are many examples of this, here is one at random.
http://blogs.microsoft.co.il/blogs/itai/archive/2009/02/01/t-sql-split-function.aspx
Absent a split function, something like this:
CREATE PROCEDURE [dbo].[myStoredProcedure]
#myFilter varchar(512) = NULL -- don't use NVARCHAR for a list of INTs
AS
SET NOCOUNT ON
BEGIN
SELECT
t.ID,
t.Name
FROM
MyTable t
WHERE
CHARINDEX(','+CONVERT(VARCHAR,t.ID)+',',#myFilter) > 0
ORDER BY
t.Name
END
Performance will be poor. A table scan every time. Better to use a split function. See: http://www.sommarskog.se/arrays-in-sql.html
I would create a function that takes your comma delimited string and splits it and returns a single column table variable with each value in its own row. Select that column from the returned table in your IN statement.
I found a cute way of doing this - but it smells a bit.
declare #delimitedlist varchar(8000)
set #delimitedlist = '|1|2|33|11|3134|'
select * from mytable where #delimitedlist like '%|' + cast(id as varchar) + '|%'
So... this will return all records with an id equal to 1, 2, 33, 11, or 3134.
EDIT:
I would also add that this is not vulnerable to SQL injection (whereas dynamic SQL relies on your whitelisting/blacklisting techniques to ensure it isn't vulnerable). It might have a performance hit on large sets of data, but it works and it's secure.
I have a couple of blog posts on this as well, with a lot of interesting followup comments and dialog:
More on splitting lists
Processing list of integers