Rkptl
Rkptl

Reputation: 33

SQL Server check constraint failing on correct input

I'm using a check constraint on a table to restrict what values are inserted in the table..

Here's an explanation of what I'm trying to do

If any Product(sedan) is associated to a specific ObjLevel (Toyota) then the same Product cannot be associated to another specific ObjLevel (Lexus)

After I apply the check constraint on the table, any insert containing ObjLevel "toyota" or "lexus" fails..

create table ObjLevel(
OLID int identity,
Name varchar(50) not null
)

insert into ObjLevel values('Ford')
insert into ObjLevel values('Toyota')
insert into ObjLevel values('Lexus')
insert into ObjLevel values('GM')
insert into ObjLevel values('Infiniti')


create table ObjInstance(
OLIID int identity (20,1),
OLID int
)

insert into ObjInstance values(1)
insert into ObjInstance values(2)
insert into ObjInstance values(3)
insert into ObjInstance values(4)
insert into ObjInstance values(5)


create table Product(
PID int identity(50,1),
Name varchar(20)
)

insert into Product values ('sedan')
insert into Product values ('coupe')
insert into Product values ('hatchback')


create table ObjInstanceProd(
OLIID int,
PID int
)


create FUNCTION [dbo].[fnObjProd] (@Pid int) RETURNS bit WITH EXECUTE AS CALLER AS

BEGIN
   DECLARE @rv bit
   DECLARE @cnt int
   SET @cnt = 0
   SET @rv = 0

      SET @cnt= 
            (Select Count(*) from ObjInstanceProd olip
            join ObjInstance oli 
            on olip.OLIID = oli.OLIID
            join ObjLevel ol
            on ol.OLID = oli.OLID
            where ol.Name in ('Toyota','Lexus')
            and PID = @Pid)

            if(@cnt>0)
                SET @rv = 1

   RETURN @rv
END


ALTER TABLE [dbo].[ObjInstanceProd]  WITH CHECK ADD  CONSTRAINT [CK_OLIP] CHECK  ([dbo].[fnObjProd]([PID])=0)


--Insert Statement    
insert into ObjInstanceProd(OLIID,PID) values (22,51)

Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the CHECK constraint "CK_OLIP". The conflict occurred in database "tmp", table "dbo.ObjInstanceProd", column 'PID'.
The statement has been terminated.


--Execute Function    
select [dbo].[fnObjProd] (51) 
0

Initially the Table ObjInstanceProd is empty.. So, no matter what value I put in the table, as long as the function in the constraint returns a 0, it should accept it.. But it does not.. The function is correctly returning a 0 (when executed independently), but for some reason, the check constraint returns a 1

Upvotes: 3

Views: 3618

Answers (1)

Aaron Bertrand
Aaron Bertrand

Reputation: 280431

When the CHECK constraint fires, the row is already in the table. Therefore, the function is called, and since there is a row returned by the query, the function returns 1, not 0. Try this. Drop the constraint, insert your row successfully, and then run this query:

SELECT OLIID, PID, dbo.fnObjProd([PID]) FROM dbo.ObjInstanceProd;

It should return 1 for every value of PID. Try to add the constraint now. It will fail for the same reason.

Have you considered using a trigger for this? If you use a check constraint, this will turn any multi-row insert or update into a cursor behind the scenes. This can absolutely kill performance and concurrency depending on how you touch your tables. Here is a simple INSTEAD OF INSERT trigger to prevent bad values going in with a single operation, even for a multi-row insert:

CREATE TRIGGER dbo.trObjProd 
ON dbo.ObjInstanceProd
INSTEAD OF INSERT AS
BEGIN
    SET NOCOUNT ON;

    IF NOT EXISTS
    (
      SELECT 1 FROM inserted
      WHERE EXISTS 
      (
        SELECT 1
            FROM dbo.ObjInstanceProd AS olip
            INNER JOIN dbo.ObjInstance AS oli 
            ON olip.OLIID = oli.OLIID
            INNER JOIN dbo.ObjLevel AS ol
            ON ol.OLID = oli.OLID
        WHERE 
            ol.Name in ('Toyota','Lexus')
            AND olip.PID = inserted.PID
      )
  )
  BEGIN
    INSERT ObjInstanceProd(OLIID, PID)
        SELECT OLIID, PID FROM inserted;
  END
  ELSE
  BEGIN
    RAISERROR('At least one value was not good.', 11, 1); 
    SELECT OLIID, PID FROM inserted;
  END
END
GO

If you're going to stick with a function, this is a much more efficient approach, however you need to define a way to determine that the current row being inserted is excluded from the check - I couldn't determine how to do that because there are no constraints on dbo.ObjInstanceProd. Is OLIID, PID unique?

ALTER FUNCTION [dbo].[fnObjProd]
(
    @Pid INT
) 
RETURNS BIT
WITH EXECUTE AS CALLER 
AS
BEGIN
    RETURN 
    (
        SELECT CASE WHEN EXISTS 
        (
            SELECT 1 
                FROM dbo.ObjInstanceProd AS olip
                INNER JOIN dbo.ObjInstance AS oli 
                ON olip.OLIID = oli.OLIID
                INNER JOIN dbo.ObjLevel AS ol
                ON ol.OLID = oli.OLID
            WHERE 
                ol.Name in ('Toyota','Lexus')
                AND olip.PID = @Pid
        ) THEN 1 ELSE 0 END
    );
END
GO

Upvotes: 6

Related Questions