How AND expressions are evaluated? - tsql

I have the following situation:
DECLARE #DataSource TABLE
(
[ID] BIGINT
)
INSERT INTO #DataSource ([ID])
VALUES (1)
,(2)
,(3)
DECLARE #IDForSearch VARCHAR(4) = 'TEXT'
If we try to extract record using our wrong type parameter:
SELECT IIF( EXISTS( SELECT 1 FROM #DataSource WHERE [ID] = #IDForSearch), 'Found', 'Missing')
we are getting the error below:
Msg 8114, Level 16, State 5, Line 14
Error converting data type varchar to bigint.
But If I add a simple isNumeric check everything is working fine:
SELECT IIF( ISNUMERIC(#IDForSearch) = 1 AND EXISTS( SELECT 1 FROM #DataSource WHERE [ID] = #IDForSearch), 'Found', 'Missing')
I firstly thought that the first part of the expression is executed:
ISNUMERIC(#IDForSearch) = 1
and because it fails, the second one is not executed and the error is not thrown.
But, if I change the expressions places everything is working again:
SELECT IIF( EXISTS( SELECT 1 FROM #DataSource WHERE [ID] = #IDForSearch) AND ISNUMERIC(#IDForSearch) = 1, 'Found', 'Missing')
Why there is no error in these situations?

I think that there is some internal cast (I speak about SQL 212), because:
select IIF( 1='a','Found','Missing')
results in
Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'a' to data type int.
and
select IIF(isnumeric('a')=1 and 1='a','Found','Missing')
returns
Missing

Related

Coalesce sentence containing an insert into clause fails in PostgreSQL

This is my trivial test table,
create table test (
id int not null generated always as identity,
first_name. varchar,
primary key (id),
unique(first_name)
);
As an alternative to insert-into-on-conflict sentences, I was trying to use the coalesce laziness to execute a select whenever possible or an insert, only when select fails to find a row.
coalesce laziness is described in documentation. See https://www.postgresql.org/docs/current/functions-conditional.html
Like a CASE expression, COALESCE only evaluates the arguments that are needed to determine the result; that is, arguments to the right of the first non-null argument are not evaluated. This SQL-standard function provides capabilities similar to NVL and IFNULL, which are used in some other database systems.
I also want to get back the id value of the row, having being inserted or not.
I started with:
select coalesce (
(select id from test where first_name='carlos'),
(insert into test(first_name) values('carlos') returning id)
);
but an error syntax error at or near "into" was found.
See it on this other DBFiddle
https://www.db-fiddle.com/f/t7TVkoLTtWU17iaTAbEhDe/0
Then I tried:
select coalesce (
(select id from test where first_name='carlos'),
(with r as (
insert into test(first_name) values('carlos') returning id
) select id from r
)
);
Here I am getting a WITH clause containing a data-modifying statement must be at the top level error that I don't understand, as insert is the first and only sentence within the with.
I am testing this with DBFiddle and PostgreSQL 13. The source code can be found at
https://www.db-fiddle.com/f/hp8T1iQ8eS4wozDCBhBXDw/5
Different method: chained CTEs:
CREATE TABLE test
( id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY
, first_name VARCHAR UNIQUE
);
WITH sel AS (
SELECT id FROM test WHERE first_name = 'carlos'
)
, ins AS (
INSERT INTO test(first_name)
SELECT 'carlos'
WHERE NOT EXISTS (SELECT 1 FROM test WHERE first_name = 'carlos')
RETURNING id
)
, omg AS (
SELECT id FROM sel
UNION ALL
SELECT id FROM ins
)
SELECT id
FROM omg
;
It seems that the returning value from the insert into clause is not equivalent in nature to the scalar query of a select clause. So I try encapsulating the insert into into an SQL function and it worked.
create or replace function insert_first_name(
_first_name varchar
) returns int
language sql as $$
insert into test (first_name)
values (_first_name)
returning id;
$$;
select coalesce (
(select id from test where first_name='carlos'),
(select insert_first_name('carlos'))
);
See it on https://www.db-fiddle.com/f/73rVXgqGfrG4VmjrAk6Z3i/2
This is a refinement on #wildplasser accepted answer. it avoids comparing first_name twice and uses coalesce instead of union all. Kind of an selsert in just one sentence.
with sel as (
select id from test where first_name = 'carlos'
)
, ins as (
insert into test(first_name)
select 'carlos'
where (select id from sel) is null
returning id
)
select coalesce (
(select id from sel),
(select id from ins)
);
See it at https://www.db-fiddle.com/f/goRh4TyAebTkEZFHk6WbtK/6

Dynamic Datatype in SELECT result

This is a scalar query, originally within a function. The result datatype varies, depending on which field I'm intresting of.
In this example, I expect a scalar result of datatype NVARCHAR 'Andy' but got an error:
Msg 245, Level 16, State 1, Line xx Conversion failed when converting
the nvarchar value 'Andy' to data type int.
Is there any way to get around this?
CREATE TABLE ATable (
Idf INT PRIMARY KEY,
Col1 INT,
Col2 NVARCHAR(255),
)
GO
INSERT INTO Atable (Idf, Col1, Col2) VALUES (1, 75, 'Andy')
INSERT INTO Atable (Idf, Col1, Col2) VALUES (2, 39, 'Pete')
GO
DECLARE #Idf INT = 1
DECLARE #Col NVARCHAR(15) = 'Col2'
SELECT
CASE
WHEN #Col='Col1' THEN Col1
WHEN #Col='Col2' THEN Col2
ELSE NULL
END AS MyScalarResultOfDynamicDatatype
FROM ATable
WHERE Idf=#Idf
If it is really, really necessary... you might use the sql_variant data type.
CASE
WHEN #Col='Col1' THEN CAST(Col1 AS SQL_VARIANT)
WHEN #Col='Col2' THEN CAST(Col2 AS SQL_VARIANT)
END AS MyScalarResultOfDynamicDatatype
(Note that you do not specifically need an ELSE NULL in your CASE-statement. If there is no matching WHEN, the result will be NULL by default.)
Edit:
Based on a question in comment regarding the drawbacks, I would like to refer to the article Problems Caused by the Use of the SQL_VARIANT Datatype, written by somebody under the alias Phil Factor (Redgate Simple Talk, Redgate Blog, GitHub).

How to use CONTAINS() and LOWER() together in T-SQL?

I am developing a stored proc in SQL Server 2012
In some part I would like to determine the condition if "NAME" contains sub-string #pattern, Case Insesitive-ly
#pattern is a string with lower case letters only
Somehow I've tried
where CONTAINS(NAME, #pattern) is fine
but when I add LOWER()
where CONTAINS(LOWER(NAME), #pattern)
It gives me syntax error.
I am new to T-SQL, can anyone help me out?
Why is it syntax error, and how to solve it?
Is there any better way to do exact the same task?
Thanks!
if your table not full-text indexed then CONTAINS will not work
another variants to solve your issue below:
DECLARE #table AS TABLE ( SomeText VARCHAR(40) )
INSERT INTO #table
VALUES ( 'abcdfh' ),
( 'ghijkl' ),
( 'mnopq' );
DECLARE #pattern AS VARCHAR(10)
SET #pattern = '%abc%'
--variant using like
SELECT T.sometext
FROM #table AS T
WHERE T.SomeText LIKE #pattern
--variant using PATINDEX
SELECT sometext
FROM #table AS T
WHERE PATINDEX(#pattern, T.SomeText) > 0
--variant using CHARINDEX, but it can't be used with '%' and '[ ]',
--just to find first char position of the searched word in text as below
SET #pattern = 'abc'
SELECT sometext
FROM #table AS T
WHERE CHARINDEX(#pattern, T.SomeText) > 0

Change a CTE SELECT to a table value user defined function

In an earlier post, I constructed a SQL that uses a CTE with the help from a SO contributor. I'd like now to encapsulate that SQL in a user defined function that returns a table but I am getting the error below.
Here's the code:
Alter FUNCTION GetDescendentSteps
(
#StepId INT
)
RETURNS TABLE
AS
RETURN
;WITH cteRecursion
AS (SELECT
StepId
,1 AS Level
FROM
Step
WHERE
StepId = #StepId
UNION ALL
SELECT
t.StepId
,c.Level + 1
FROM
Step t
INNER JOIN cteRecursion c
ON t.ParentStepId = c.StepId
)
SELECT
StepId,Level
FROM
cteRecursion
ORDER BY
Level,
StepId;
I get the error:
Msg 102, Level 15, State 1, Procedure GetDescendentSteps, Line 8
Incorrect syntax near ';'.
Note that line 8 is:
;WITH cteRecursion
..and that if I execute only the SQL beginning at line 8 (after I replace the variable #StepId with a literal).
Also, this simple example works:
ALTER FUNCTION GetDescendentSteps
(
#StepId INT
)
RETURNS TABLE
AS
RETURN
select 7 As Stepid,1 As Level
Remove the first ; and the order by clause.
Alter FUNCTION GetDescendentSteps
(
#StepId INT
)
RETURNS TABLE
AS
RETURN
WITH cteRecursion
AS (SELECT
StepId
,1 AS Level
FROM
Step
WHERE
StepId = #StepId
UNION ALL
SELECT
t.StepId
,c.Level + 1
FROM
Step t
INNER JOIN cteRecursion c
ON t.ParentStepId = c.StepId
)
SELECT
StepId,Level
FROM
cteRecursion

How do I TryParse in SQL 2000?

I have a stored procedure in an old SQL 2000 database that takes a comment column that is formatted as a varchar and exports it out as a money object. At the time this table structure was setup, it was assumed this would be the only data going into this field. The current procedure functions simply this this:
SELECT CAST(dbo.member_category_assign.coment AS money)
FROM dbo.member_category_assign
WHERE member_id = #intMemberId
AND
dbo.member_category_assign.eff_date <= #dtmEndDate
AND
(
dbo.member_category_assign.term_date >= #dtmBeginDate
OR
dbo.member_category_assign.term_date Is Null
)
However, data is now being inserted into this column that is not parsable to a money object and is causing the procedure to crash. I am unable to remove the "bad" data (since this is a third party product), but need to update the stored procedure to test for a money parsable entry and return that.
How can I update this procedure so that it will only return the value that is parsable as a money object? Do I create a temporary table and iterate through every item, or is there a more clever way to do this? I'm stuck with legacy SQL 2000 (version 6.0) so using any of the newer functions unfortunately is not available.
Checking for IsNumeric may help you - you can simply return a Zero value. If you want to return a 'N/a' or some other string value
I created the sample below with the columns from your query.
The first query just returns all rows.
The second query returns a MONEY value.
The third one returns a String value with N/A in place of the non-integer value.
set nocount on
drop table #MoneyTest
create table #MoneyTest
(
MoneyTestId Int Identity (1, 1),
coment varchar (100),
member_id int,
eff_date datetime,
term_date datetime
)
insert into #MoneyTest (coment, member_id, eff_date, term_date)
values
(104, 1, '1/1/2008', '1/1/2009'),
(200, 1, '1/1/2008', '1/1/2009'),
(322, 1, '1/1/2008', '1/1/2009'),
(120, 1, '1/1/2008', '1/1/2009')
insert into #MoneyTest (coment, member_id, eff_date, term_date)
values ('XX', 1, '1/1/2008', '1/1/2009')
Select *
FROM #MoneyTest
declare #intMemberId int = 1
declare #dtmBeginDate DateTime = '1/1/2008'
declare #dtmEndDate DateTime = '1/1/2009'
SELECT
CASE WHEN ISNUMERIC (Coment)=1 THEN CAST(#MoneyTest.coment AS money) ELSE cast (0 as money) END MoneyValue
FROM #MoneyTest
WHERE member_id = #intMemberId
AND #MoneyTest.eff_date <= #dtmEndDate
AND
(
#MoneyTest.term_date >= #dtmBeginDate
OR
#MoneyTest.term_date Is Null
)
SELECT
CASE WHEN ISNUMERIC (Coment)=1 THEN CAST (CAST(#MoneyTest.coment AS money) AS VARCHAR) ELSE 'N/a' END StringValue
FROM #MoneyTest
WHERE member_id = #intMemberId
AND #MoneyTest.eff_date <= #dtmEndDate
AND
(
#MoneyTest.term_date >= #dtmBeginDate
OR
#MoneyTest.term_date Is Null
)
Apologies for making a new answer, where a comment would suffice, but I lack the required permissions to do so. Onto the answer to your question, I would only like to add that you should use the above ISNUMERIC carefully. While it works much as expected, it also parses things like '1.3E-2' as a value numeric, which strangely enough you cannot cast into a numeric or money without generating an exception. I generally end up using:
SELECT
CASE WHEN ISNUMERIC( some_value ) = 1 AND CHARINDEX( 'E', Upper( some_value ) ) = 0
THEN Cast( some_value as money )
ELSE Cast( 0 as money )
END as money_value