T sql Case in Where clause - tsql

I know this has been asked a few times but I can't seem to get this sorted. What am I doing wrong here?
WHERE B.ACCTSET IN CASE
WHEN #AcctSet ='Maize' THEN ('001X04','010X04')
WHEN #AcctSet ='Wheat' THEN ('002X04')
ELSE ('004X04')
END
Help will be really appreciated.

A CASE statement can only return one value, so the line for 'Maize' will not work.
The CASE statement would also be within the parenthesis.
WHERE B.ACCTSET IN (CASE
WHEN #AcctSet ='Maize' THEN '001X04'
WHEN #AcctSet ='Wheat' THEN '002X04'
ELSE '004X04'
END
)
If the results are fixed like in your example then you could create a table var and insert the records in to it based on your logic, then join on that or use it in your IN().
DECLARE #Values TABLE( [Value] VARCHAR(6))
IF #AcctSet ='Maize'
BEGIN
INSERT INTO #Values VALUE('001X04')
INSERT INTO #Values VALUE('010X04')
END
ELSE IF #AcctSet ='Wheat'
BEGIN
INSERT INTO #Values VALUE('002X04')
END
ELSE
BEGIN
INSERT INTO #Values VALUE('004X04')
END
/..snip../
WHERE WHERE B.ACCTSET IN (SELECT [Value] FROM #Values)

Your CASE expression should return only 1 value and not a list of values like this:
WHERE #AcctSet = CASE
WHEN B.ACCTSET IN ('001X04','010X04') THEN 'Maize'
WHEN B.ACCTSET IN ('002X04') THEN 'Wheat'
WHEN B.ACCTSET IN ('004X04') THEN #AcctSet
END

Related

Is it possible to use Scalar-valued Functions in IF-statement?

So we have this SQL-query that we use in very many places that I would like to simplify by putting it in a function so that we don't have to write the query every single time. But when replacing the uses I stumbled upon an issue where we sometimes would use it inside an IF-statement.
Is there any way to use this as a function? Or maybe move it to a stored procedure instead?
Function:
ALTER FUNCTION [dbo].[fn_GetOption]
(
-- Add the parameters for the function here
#id int,
#searchKey nvarchar(32) = NULL
)
RETURNS nvarchar(128)
AS
BEGIN
DECLARE #Result nvarchar(128)
IF #searchKey IS NULL
BEGIN
SELECT #Result = [VALUE]
FROM [OPTIONS]
WHERE [ID] = #id
END
ELSE
BEGIN
SELECT #result = ISNULL(eoo.[VALUE], eo.[VALUE])
FROM
[OPTIONS] eo
LEFT OUTER JOIN [OPTIONS_OVERRIDE] eoo ON eo.[ID] = eoo.[ID] AND eoo.[SEARCH_KEY] = #searchKey
WHERE
eo.[ID] = #id
END
RETURN #Result
END
Usage: (not working)
IF 'Y' = EXEC [fn_GetOption] 69, 'myKey'
BEGIN
PRINT '1'
END
Remove the EXEC and add brackets:
IF 'Y' = dbo.[fn_GetOption] (69, 'myKey')
BEGIN
PRINT '1'
END

PostgreSQL - ALL ( array ) Operator - Suggestion

Sample Code as follows : ALL or ANY operator is not working. I need to compare ALL the values of the array
CREATE OR REPLACE FUNCTION public.sample_function(
tt_sample_function text)
RETURNS TABLE (..... )
LANGUAGE 'plpgsql'
COST 100
VOLATILE
ROWS 1000
AS $BODY$
declare
e record;
v_cnt INTEGER:=0;
rec record;
str text;
a_v text [];
BEGIN
FOR rec IN ( SELECT * FROM json_populate_recordset(null::sample_function ,sample_function::json) )
LOOP
a_v:= array_append(a_v, ''''||rec.key || '#~#' || rec.value||'''');
END LOOP;
SELECT MAInfo.userid FROM
(SELECT DISTINCT i.userid,
CASE WHEN (i.settingKey || '#~#' || i.settingvalue) = ALL (a_v)
THEN i.settingKey || '#~#' || 'Y'
ELSE i.settingKey || '#~#' || 'N' END
AS MatchResult
FROM public.sample_table i
WHERE (i.settingKey || '#~#' || i.settingvalue) = ALL (a_v)
GROUP BY i.userid, MatchResult) AS MAInfo
GROUP BY MAInfo.userid
HAVING COUNT(MAInfo.userid) >= 1;
RETURN QUERY (....);
END;
$BODY$;
CREATE TYPE tt_sample_function AS
(
key character varying,
value character varying
)
Inputs are
SELECT public.sample_function(
'[{"key":"devicetype", "value":"TestType"},{"key":"ostype", "value":"TestType"}]'
)
Any suggestion, why my ALL operator is not working. I mean its always giving false, it should match with all the array elements...
Note: ofcourse data is there in table.
You are over complicating things. You don't need the FOR loop or the array to do the comparison. You can do that all in a single statement. No need for an extra TYPE or generating an array.
The parameter to the function should be declared as jsonb as you clearly want to pass valid JSON there.
I don't understand what you are trying to achieve with the CASE expression. The WHERE clause only returns rows that match the first condition in the CASE, so the second one will never be reached.
I also don't understand why you have the CASE at all, as you discard the result of that in the outer query completely.
But keeping the original structure as close as possible, I think you can simplify this to a single CREATE TABLE AS statement and get rid of all the array processing.
CREATE OR REPLACE FUNCTION public.sample_function(p_settings jsonb)
RETURNS TABLE (..... )
LANGUAGE plpgsql
AS $BODY$
declare
...
begin
CREATE TEMP TABLE hold_userID AS
SELECT MAInfo.userid
FROM (
-- the distinct is useless as the GROUP BY already does that
SELECT i.userid,
CASE
-- this checks if the parameter contains the settings key/value from sample_table
-- but the WHERE clause already makes sure of that???
WHEN p_settings #> jsonb_build_object('key', i.settingKey, 'value', i.settingvalue)
THEN i.settingKey || '#~#' || 'Y'
ELSE i.settingKey || '#~#' || 'N'
END AS MatchResult
FROM public.sample_table i
WHERE (i.settingKey, i.settingvalue) = IN (select t.element ->> 'key' as key,
t.element ->> 'value' as value
from jsonb_array_elements(p_settings) as t(element))
GROUP BY i.userid, MatchResult
) AS MAInfo
GROUP BY MAInfo.userid
HAVING COUNT(MAInfo.userid) >= 1;
return query ...;
end;
$body$
If you want to check if certain users have all the settings passed to the function, you don't really need a CASE expression, just a proper having condition
So maybe you want this instead:
CREATE TEMP TABLE hold_userID AS
SELECT i.userid,
FROM public.sample_table i
WHERE (i.settingKey, i.settingvalue) = IN (select t.element ->> 'key' as key,
t.element ->> 'value' as value
from jsonb_array_elements(p_settings) as t(element))
GROUP BY i.userid
HAVING COUNT(*) = jsonb_array_length(p_settings);
Or alternatively:
SELECT i.userid
FROM (
select userid, settingkey as key, settingvalue as value
from public.sample_table
) i
group by i.userid
HAVING jsonb_object_agg(key, value) = p_settings

RAISERROR within Case statement

Can you not raise errors within a case statement in T-SQL? I always have problems with SQL case statements :/
begin try
declare #i int
--set #i = (select COUNT(1) from table_name)
select Item_Num =
CASE (select COUNT(1) from table_name)
when 1 then (select Item_Num from table_name)
when 0 then (raiserror('No records in database', 0, 0))
ELSE (raiserror('Multiple records in database', 0, 0))
END
from table_name
end try
begin catch
declare #errormsg nvarchar(1024),
#severity int,
#errorstate int;
select #errormsg = error_message(),
#severity = error_severity(),
#errorstate = error_state();
raiserror(#errormsg, #severity, #errorstate);
end catch
Think of Case/When as operating on a single piece of data. If you think of it this way, a lot of your problems will go away.
If/Then is used to control the flow of logic.
Something like this should work for you.
declare #i int
set #i = (select COUNT(1) from table_name)
If #i = 1
Begin
Print "1 row"
End
Else If #i = 0
Begin
Print "no rows"
End
Else
Begin
Print "too many rows"
End
You can raise an error from the case expression by converting an error string to int.
select case (select count(*) from mytable)
when 1 then 100
when 2 then 200
else convert(int, 'ERROR')
end
This gives an error message like
Conversion failed when converting the varchar value 'ERROR' to data type int.
which is about as good as you're going to get.
Not all failed conversions give the input string in the error message. Conversions to datetime, for example, do not. So if your case expression returns a datetime, you still have to trigger the error with a string-to-integer conversion:
select case (select count(*) from mytable)
when 1 then getdate()
else convert(datetime, convert(int, 'ERROR'))
end
It gets worse: if you are returning a date, you can't explicitly convert that from int, so you have to resort to
convert(date, convert(char(1), convert(int, 'ERROR')))
It's pretty horrible, but in my opinion the only thing more important than clean code is informative error messages, so I live with it.
As I said in the comment, I think it would be easier to simply create a flag that you check outside the scope of the CASE statement. Something along the lines of:
--- code before the TRY...
BEGIN TRY
DECLARE #i int
-- declare a variable to act as a flag
DECLARE #my_flag as int
-- than change your statement to simply fill the value of the flag
CASE (SELECT COUNT(1) FROM table_name)
WHEN 1 THEN SET #my_flag = 1
WHEN 0 THEN SET #my_flag = 0
ELSE SET #my_flag = -1
END
IF (NOT #my_flag in (-1, 0))
BEGIN
SET #Item_Num = (SELECT Item_Num FROM table_name) -- consider a filter here
END
ELSE
BEGIN
IF (-1 = #my_flag) RAISERROR('too many records', 0, 0)
IF (0 = #my_flag) RAISERROR('no records', 0, 0)
END
END TRY
BEGIN CATCH
--- rest of the code goes here....
With #TempTable as
(
Select * from ...
-- record set that determines if I should error out
)
SELECT CASE WHEN COUNT(1) > 0
THEN 'Multiple records in database'
ELSE 0
END AS [Value]
FROM #TempTable
datatype mismatch will kill the method if you're trying to error out this whole call with TSQL. Worked in my case because it was a heavy process that I needed to know was transferred correctly. If My record set was >1 then I know I should fail this. Useful if you're using SSIS or multiple methods within a .NET environment

How to use a T-SQL UDF in a SELECT or UPDATE statement?

Context: SQL Server 2000
I've written a UDF that gives me the text between two other texts, viz
CREATE FUNCTION dbo.StrBetween
(
#Text nvarchar(4000),
#Lhs nvarchar(4000),
#Rhs nvarchar(4000)
)
RETURNS nvarchar(4000)
AS
BEGIN
DECLARE #LhsOffset INT;
DECLARE #RhsOffset INT;
DECLARE #Result NVARCHAR(4000);
SET #LhsOffset = CHARINDEX( #Lhs, #Text );
IF #LhsOffset = 0
BEGIN
RETURN #Text;
END
SET #Result = SUBSTRING( #Text, #LhsOffset+1, LEN(#Text)-LEN(#Lhs));
SET #RhsOffset = CHARINDEX( #Rhs, #Result );
IF #RhsOffset = 0
BEGIN
RETURN #Result;
END
SET #Result = SUBSTRING( #Result, 1, #RhsOffset - 1 );
RETURN #Result;
END
This works fine in SQL Query Analyser if I have, say,
SELECT dbo.StrBetween('dog','d','g')
However, when I pass a column in as the value of the first argument, I get no response. For example,
SELECT [TEST].[dbo].StrBetween(Referrer,'//', '/') as tst FROM tblTest
Referrer is declared as an nvarchar field. I'm a newbie when it comes to T-SQL. What obvious thing am I not seeing?
It's not an issue with calling - it's a logic issue, and the fact that your #Rhs value is part of the #Lhs value.
SET #Result = SUBSTRING( #Text, #LhsOffset+1, LEN(#Text)-LEN(#Lhs));
This is removing the first character of your #Lhs string. However, since the second character is /, and that's what your #Rhs match is searching for, it immediately finds it at position 1 and so you get an empty string.
Instead, try:
SET #Result = SUBSTRING( #Text, #LhsOffset+LEN(#Lhs), 4000);
You don't have to be exact with computing a length. If you ask for 4000 characters and the string is only 12 characters long, SUBSTRING will give you back at most 12 characters. So don't bother computing the new length.

Try-Catch in User Defined Function?

I'm trying to write a UDF to translate a string that is either a guid or a project code associated with that guid into the guid:
CREATE FUNCTION fn_user_GetProjectID
(
#Project nvarchar(50)
)
RETURNS uniqueidentifier
AS
BEGIN
declare #ProjectID uniqueidentifier
BEGIN TRY
set #ProjectID = cast(#Project as uniqueidentifier)
END TRY
BEGIN CATCH
set #ProjectID = null
END CATCH
if(#ProjectID is null)
BEGIN
select #ProjectID = ProjectID from Project where projectcode = #Project
END
return #ProjectID
END
This works fine if the above code is embedded in my Stored Procedures, but I'd like to make a function out of it so that I follow DRY.
When I try to create the Function, I get errors like this:
Msg 443, Level 16, State 14, Procedure fn_user_GetProjectID, Line 16
Invalid use of side-effecting or time-dependent operator in 'BEGIN TRY' within a function.
Does anyone have an idea how I can get around this error?
Edit: I know I can't use Try-Catch in a Function, I guess a simplified questions would be, is there a way to do a cast that will just return NULL if the cast fails, instead of an error?
Apparently you can't use TRY-CATCH in a UDF.
According to this bug-reporting page for SQL Server:
Books Online documents this behaviour,
in topic "CREATE FUNCTION
(Transact-SQL)": "The following
statements are valid in a function:
[...] Control-of-Flow statements
except TRY...CATCH statements. [...]"
But they were giving hope for the future back in 2006:
However, this is a severe limitation
that should be removed in a future
release. You should post a suggestion
in this regard and I will
wholeheartedly vote for it.
From MSDN:
A column or local variable of
uniqueidentifier data type can be
initialized to a value in the
following ways:
By using the NEWID function.
By converting from a string constant
in the form
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,
in which each x is a hexadecimal digit
in the range 0-9 or a-f.
For example,
6F9619FF-8B86-D011-B42D-00C04FC964FF
is a valid uniqueidentifier value.
You can use pattern matching to verify the string. Note that this won't work for specific encoding that reduces the size of the GUID:
declare #Project nvarchar(50)
declare #ProjectID uniqueidentifier
declare #HexPattern nvarchar(268)
set #HexPattern =
'[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]' +
'[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]' +
'[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]' +
'[A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9][A-F0-9]'
/* Take into account GUID can have curly-brackets or be missing dashes */
/* Note: this will not work for GUIDs that have been specially encoded */
set #Project = '{' + CAST(NEWID() AS VARCHAR(36)) + '}'
select #Project
set #Project = REPLACE(REPLACE(REPLACE(#Project,'{',''),'}',''),'-','')
/* Cast as uniqueid if pattern matches, otherwise return null */
if #Project LIKE #HexPattern
select #ProjectID = CAST(
SUBSTRING(#Project,1,8) + '-' +
SUBSTRING(#Project,9,4) + '-' +
SUBSTRING(#Project,13,4) + '-' +
SUBSTRING(#Project,17,4) + '-' +
SUBSTRING(#Project,21,LEN(#Project)-20)
AS uniqueidentifier)
select #ProjectID
I know I can't use Try-Catch in a Function, I guess a simplified questions would be, is there a way to do a cast that will just return NULL if the cast fails, instead of an error?
Starting from SQL Server 2012 you could use TRY_CAST/TRY_CONVERT functions:
Returns a value cast to the specified data type if the cast succeeds; otherwise, returns null.
CREATE FUNCTION fn_user_GetProjectID(#Project nvarchar(50))
RETURNS uniqueidentifier
AS
BEGIN
declare #ProjectID uniqueidentifier = TRY_CAST(#Project as uniqueidentifier);
IF(#ProjectID is null)
BEGIN
select #ProjectID = ProjectID from Project where projectcode = #Project;
END
return #ProjectID;
END
Not sure, but why not flip it around... at first glance I would simplify it like this:
select #ProjectID =
ISNULL((select ProjectID from Project where
projectcode = #Project)
,(cast #Project as uniqueidentifier))
If this doesn't provide enough error handling, I'm sure there's a better way to pre-check that the cast can work without using try/catch...
My brute force method was to create my own ToGuid() function that verifies it can be converted to a GUID first, if not, it returns null. It may not be very fast but it does the job, and it is probably faster to convert the guid if it is one than to try to look it up in the table. EDIT: I meant to give credit to this blog, where I got the basis of my code for this function: http://jesschadwick.blogspot.com/2007/11/safe-handling-of-uniqueidentifier-in.html
CREATE FUNCTION [dbo].[ToGuid]
(
#input NVARCHAR(MAX)
)
RETURNS uniqueidentifier
AS
BEGIN
DECLARE #isValidGuid BIT;
DECLARE #temp NVARCHAR(MAX);
SET #isValidGuid = 1;
SET #temp = UPPER(LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(#input, '-', ''), '{', ''), '}', ''))));
IF(#temp IS NOT NULL AND LEN(#temp) = 32)
BEGIN
DECLARE #index INT;
SET #index = 1
WHILE (#index <= 32)
BEGIN
IF (SUBSTRING(#temp, #index, 1) IN ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'A', 'B', 'C', 'D', 'E', 'F'))
BEGIN
SET #index = #index + 1
END
ELSE
BEGIN
SET #isValidGuid = 0
BREAK;
END
END
END
ELSE
BEGIN
SET #isValidGuid = 0
END
DECLARE #ret UNIQUEIDENTIFIER
IF(#isValidGuid = 1)
set #ret = cast(#input AS UNIQUEIDENTIFIER)
ELSE
set #ret = NULL
RETURN #ret
END
I'm still very interested if there is a better answer than this.
Verify if #Project is a number using the ISNUMERIC function.
your code should looks like that:
declare #ProjectID uniqueidentifier
set #ProjectID = null
IF ISNUMERIC(#Project) > 0
BEGIN
set #ProjectID = cast(#Project as uniqueidentifier)
END
if(#ProjectID is null)
BEGIN
select #ProjectID = ProjectID from Project where projectcode = #Project
END
return #ProjectID