SQL Server check constraint failing on correct input - sql-server

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

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

Related

Issue with SQL INSERT Trigger

I'm reaching out for some help on this trigger I'm trying to get working.
Basically this is what I'm trying to do.
We have DMS software that writes to a Database and on a particular INSERT value I want the trigger to fire.
This is an example of an INSERT statement that will get processed.
INSERT INTO DOCSADM.ACTIVITYLOG (CR_IN_USE,ACTIVITY_DESC,BILLED_ON,BILLABLE,PAGES,KEYSTROKES,
TYPE_TIME,ELAPSED_TIME,TYPIST,AUTHOR,START_DATE,ACTIVITY_TYPE,REF_DOCUMENT,REF_LIBRARY,APPLICATION,VERSION_LABEL,DOCNUMBER,SYSTEM_ID)
VALUES ('','DOCSFusion','1753-01-01','',0,0,0,0,1920,1920,'2020-08-26T10:17:56',**115**,0,-1,1173,'',75,3252)
but I only want the trigger to fire when we see a value of 115 for the bold section in the INSERT statement (the Activity_type value).
For all other values that re not 115 I don't want to do anything.
This is what I have so far:
CREATE TRIGGER BW_TRIGGER
ON DOCSADM.ACTIVITYLOG
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--Declare some variable and set it as a value of 115.
--Example:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
--Next, you should have a fork or path in your trigger to determine how it proceeds.
--Path 1: The #AlogType value matches the inserted value so you want to process the rest of the trigger. Example path – “ProcessTrigger:”
--Path 2: The #AlogType value does NOT match the inserted value, you want to exit the trigger. Example Path – “ExitTrigger:”
IF #AlogType <> 115
GOTO TriggerExit;
ELSE
Begin
/*Create first temp table to collect insert values*/ --This table will have the SysID Value and the corresponding docnumber for the items you want.
--You can add whatever other values you think you need.
CREATE TABLE #TempSet1
(
AlogsysID INT,
Docnum INT,
AlogDate Varchar(64),
AlogTypist INT,
AlogAuthor INT,
AlogDesc varchar(32),
ALOGVER varchar(10),
ALOG_MATTER INT
)
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
select top 1 System_id, docnumber, LAST_ACCESS_DATE, TYPIST, AUTHOR, MATTER from docsadm.PROFILE where EXISTS (SELECT CLIENT.SYSTEM_ID FROM DOCSADM.CLIENT INNER JOIN DOCSADM.MATTER ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc OR INH_LUP_SEC_FROM IS NULL OR INH_LUP_SEC_FROM = 0) AND MATTER=#AlogDesc
/*Set variable #SysID as the LASTKEY value -1. This will be used to set the SysID column on the #TempSet table*/
--DECLARE #SysID INT = (SELECT LASTKEY FROM DOCSADM.SEQ_SYSTEMKEY) -1;
/*Set the SysID value for every row on the #TempSet1 table as the #SysID variable +1*/
--UPDATE #TempSet1
--SET #SysID = AlogsysID = #SysID + 1
--Your #TempSet should now be set with ALL of the System_IDs and Docnumbers necessary for your insert!!!!—
--Verify this by doing a select against the #TempSet1 Table
SELECT * FROM #TempSet1;
--Next you need to set the SystemID to the correct value for future processing. To do this, we need to get a total count from the #TempSet table.
/*Set a variable to update the NEXTKEY value on the DOCSADM.SEQ_SYSTEMKEY table. The NEXTKEY value is used for the SYSTEM_ID field*/
--DECLARE #SeqUpdateCount INT = (SELECT COUNT(*) FROM #TempSet1);
/*Update the LASTKEY Value on the SEQ_SYSTEMKEY table to the next available value for DM.*/
--UPDATE DOCSADM.SEQ_SYSTEMKEY SET LASTKEY = LASTKEY+#SeqUpdateCount
--If you have all the values you need in your temp table, you can now insert them into the ACTIVITYLOG table.
--INSERT INTO DOCSADM.ACTIVITY
--(SYSTEM_ID, DOCNUMBER, START_DATE, version, EXT,)
--SELECT
--AlogSysID,Docnum,GETUTCDATE(),BLAH, BLAH
--FROM #TableSet1
INSERT INTO DOCSADM.ACTIVITYLOG
(SYSTEM_ID,
DOCNUMBER,
START_DATE,
TYPIST,
AUTHOR,
ACTIVITY_DESC,
VERSION_LABEL,
ACTIVITY_TYPE)
SELECT
AlogsysID, Docnum,AlogDate,AlogTypist, AlogAuthor, ALOG_MATTER, '',115
FROM #TempSet1;
--Now you need to Drop the Temp Table
DROP TABLE #TempSet1
--Go to the other half of your path above to exit the trigger.
END
TriggerExit:
END
Go
but when I try to run any INSERT statement on this table I get this error message. It doesn't matter if the activity_type has a value of 115 or not
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I know the issue is with this section of the trigger:
INSERT INTO #TempSet1 (AlogsysID,Docnum,AlogDate,AlogTypist,AlogAuthor, ALOG_MATTER)
--SELECT  You SELECT STATEMENT WILL GO HERE MODIFIED TO POPULATE THE TABLE WITH THE DOCNUMBERS YOU WANT!!
SELECT TOP 1
System_id
, docnumber
, LAST_ACCESS_DATE
, TYPIST
, AUTHOR
, MATTER
FROM docsadm.PROFILE
WHERE EXISTS (SELECT CLIENT.SYSTEM_ID
FROM DOCSADM.CLIENT
INNER JOIN DOCSADM.MATTER
ON MATTER.CLIENT_ID = CLIENT.SYSTEM_ID
WHERE MATTER.SYSTEM_ID =#AlogDesc
OR INH_LUP_SEC_FROM IS NULL
OR INH_LUP_SEC_FROM = 0)
AND MATTER=#AlogDesc
It's the SELECT statement that is causing it to fail.
I know that this statement will bring back multiple rows but I only need the value from one of them so I can use this value for my INSERT. I though having the "select top 1" would do this for me but it's not working like I think it should. What am I missing?
If I had to guess I would say your problem is here:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM DOCSADM.ACTIVITYLOG A, INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from docsadm.ACTIVITYLOG A, INSERTED I)
How are ACTIVITYLOG and INSERTED joined in the above ? without a where it would be a CROSS JOIN. Why do you even drag ACTIVITYLOG into it, you can simply use INSERTED. Also please try to stop using implicit joins ( I can see that later down the script you use the proper, more verbose join syntax)
TRY:
DECLARE #AlogType int = (SELECT I.ACTIVITY_TYPE FROM INSERTED I) --This is the value you are looking for regarding the DM client/Matter actitivty type.
DECLARE #AlogDesc varchar(32) = (Select i.ACTIVITY_DESC from INSERTED I)
Be careful that this will work with single inserts only. When you do batched inserts the INSERTED is a table containing multiple rows and you will run into issues again.

Why I can not insert or update valid data after Instead of Trigger has been fired?

Here is my simple test trigger transactions:
First, I designed a table:
CREATE TABLE T2_Score (
UserID INT Primary key,
Months INT,
Score INT
);
Then, create an instead of trigger to set the restractions to make sure the value of Months should be between 1 and 12.
CREATE TRIGGER T2_Score_Months_Restriction
ON T2_Score
INSTEAD OF UPDATE, INSERT
AS
IF ((SELECT Months FROM inserted) > 12)
BEGIN
PRINT ('Month must be between 1 and 12!')
ROLLBACK TRAN
END
But, the issue is I can not insert any valid values if the trigger has been fired once.
For example:
INSERT INTO T2_Score VALUES (11,15,18);
And inserted filed if I try the valid value(No warning notes, but file to insert value into table), for example:
INSERT INTO T2_Score VALUES (11,12,18);
Can someone explain why and how to modify my code? Thanks!!
Wouldn't it be much simpler to use a constraint?
CREATE TABLE T2_Score (
UserID INT Primary key,
Months INT,
Score INT,
CONSTRAINT CHK_MONTHS_1_12 CHECK (Months BETWEEN 1 AND 12)
);
That trigger will never insert any rows. It is an INSTEAD OF trigger that doesn't have any INSERT statement.
You need:
CREATE TRIGGER T2_Score_Months_Restriction
ON T2_Score
INSTEAD OF UPDATE, INSERT
AS
IF (SELECT MAX(Months) FROM inserted) > 12 BEGIN
PRINT ('Month must be between 1 and 12!') ;
ROLLBACK TRAN ;
END
ELSE BEGIN
INSERT INTO T2_Score SELECT * FROM inserted ;
END ;
Or perhaps:
CREATE TRIGGER T2_Score_Months_Restriction
ON T2_Score
INSTEAD OF UPDATE, INSERT
AS
INSERT INTO T2_Score
SELECT * FROM inserted WHERE Months BETWEEN 1 AND 12 ;
The second versions allows partial inserts.
INSERT INTO T2_Score VALUES ( 4, 4, 4 ), ( 13, 13, 13 );
-- With the first trigger, this will insert 0 rows.
-- With the second trigger, this will insert 1 row.
The instead of trigger does not work in MySQL. You can use a BEFORE INSERT and BEFORE update and abort the action:
CREATE TRIGGER T2_Score_Months_Restriction
ON T2_Score
BEFORE INSERT,UPDATE
AS
IF ((SELECT Months FROM inserted) > 12)
BEGIN
DECLARE msg VARCHAR(255);
IF (SomeTestToFail = "FAIL!") THEN
set msg = "Month must be between 1 and 12!";
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END;
Your INSERT will be ecexuted atomically. It will succeed as a whole, or it will fail as a whole. You might be able to work around that by just deleting invalid values from inserted, but I don't recommend it.
It's a really bad idea to allow a SQL statement to partially succeed when part of it fails.

INSERT and UPDATE after one row inserted MS SQL

I don't know if I'm thinking correct, so I'm open for suggestions.
I'm using MS SQL SERVER 2008 R2.
Here is the 'story'
Everytime someone insert a row into tblDelivered there is a trigger that insert in tblConditionDel five values(1,1,1,1,1). This is a table with an auto ID increment. And for that inserted row must fieldname ConditionID be updated with the ID from tblConditionDel.
I think there is something wrong with my where statement
If I delete the where statement the ID is update for the entire table, but it must be for the one inserted row.
My code:
ALTER TRIGGER [dbo].[trgCond2] ON [dbo].[tblDelivered]
AFTER INSERT
AS
insert into tblConditionDel(Con1,Con2,Con3,Con4, Con5)
values(1,1,1,1,1);
update tblDelivered set ConditionID = (select max(ConditionID) from tblConditionDel)
where (select 1 from inserted) = tblDelivered.ConditionID
Thx in advance
Your solution does not work if more than one row is inserted.
Do like this
CREATE TABLE tblDelivered (
DeliveredID int NOT NULL
,ConditionID int
);
CREATE TABLE tblConditionDel (
ConditionID int IDENTITY(1,1)
,Con1 int NOT NULL
,Con2 int NOT NULL
,Con3 int NOT NULL
,Con4 int NOT NULL
,Con5 int NOT NULL
);
CREATE TRIGGER [dbo].[trgCond2] ON [dbo].[tblDelivered]
AFTER INSERT
AS
SET NOCOUNT ON;
DECLARE #ConditionIDs AS table ( -- stores inserted conditionsIDs
DeliveredID int NOT NULL
,ConditionID int NOT NULL
);
MERGE INTO tblConditionDel -- INSERT does not support OUTPUT INTO for multiple rows
USING inserted AS triggerinserted
ON 1 = 0
WHEN NOT MATCHED THEN
INSERT (Con1, Con2, Con3, Con4, Con5)
VALUES (1,1,1,1,1)
OUTPUT triggerinserted.DeliveredID
,inserted.ConditionID
INTO #ConditionIDs;
UPDATE tblDelivered
SET ConditionID = ConditionIDs.ConditionID
FROM tblDelivered
INNER JOIN #ConditionIDs AS ConditionIDs
ON ConditionIDs.DeliveredID = tblDelivered.DeliveredID
-- Test code
INSERT INTO tblDelivered (DeliveredID)
VALUES (4),(5),(6);
SELECT * FROM tblConditionDel
SELECT * FROM tblDelivered
You would want to do something like this:
ALTER TRIGGER [dbo].[trgCond2] ON [dbo].[tblDelivered]
AFTER INSERT
AS
declare #new_id int, #delivered_id int
set #delivered_id = (select ConditionID from inserted) -- captures id from inserted table
insert into tblConditionDel(Con1,Con2,Con3,Con4, Con5)
values(1,1,1,1,1);
set #new_id = scope_identity() -- captures new id from tblCondition into a variable
update tblDelivered set ConditionID = #new_id -- set new tblcondition id
where ConditionID = #delivered_id -- for the record that matches the inserted one

TSQL variable object reference not set to instance of an object

I'm trying to create a trigger that initialises a record over multiple tables when a master record is inserted to a master table. The code below gives the error "Object reference not set to an instance of an object."
Appraisal is the table the trigger exists for and every table concerned has an AppraisalID foreign key.
BEGIN TRANSACTION
DECLARE #appraisalid int
SET #appraisalid = (SELECT AppraisalID FROM inserted)
INSERT INTO dbo.AppraisalCPD (AppraisalID)
VALUES (#appraisalid)
COMMIT;
The code below works, but I would rather use a variable to assign the value as I need to add rows to a fair few tables.
BEGIN
INSERT INTO dbo.AppraisalCPD
(AppraisalID)
SELECT AppraisalID
FROM inserted;
END;
If anyone can suggest a way to set an appraisalid variable using the inserted row in the Appraisal table to insert rows into every other table I need to, that'd be very helpful.
I am assuming you are talking about home appraisal.
The [appraisal] table is the parent and the [owners]/[location] are the children.
Here is a play schema in tempdb.
-- just playing
use tempdb;
go
-- drop table
if object_id('appraisal') > 0
drop table appraisal
go
-- create table - home appraisal
create table appraisal
(
app_id int identity (1,1),
app_request datetime,
app_employee_id int
);
-- drop table
if object_id('owners') > 0
drop table owners
go
-- create table - the owners
create table owners
(
own_id int identity (1,1) primary key,
own_first_nm varchar(64),
own_last_nm varchar(64),
app_id int
);
-- drop table
if object_id('location') > 0
drop table location
go
-- the location
create table location
(
loc_id int identity (1,1) primary key,
loc_street varchar(64),
loc_city varchar(64),
loc_state varchar(2),
loc_zip varchar(9),
app_id int
);
go
When creating empty child records via a trigger, you either have to define default values or supply them in the trigger.
Also, I leave setting up foreign keys for you to handle.
The code for the trigger could look like the following.
-- The trigger
CREATE TRIGGER DBO.make_empty_children on dbo.appraisal
for insert, update, delete
as
BEGIN
-- nothing to do?
IF (##rowcount = 0) RETURN;
-- do not count rows
SET NOCOUNT ON;
-- delete
IF NOT EXISTS (SELECT * FROM inserted)
BEGIN
RETURN;
END
-- insert
ELSE IF NOT EXISTS (SELECT * FROM deleted)
BEGIN
-- dummy record for owners
insert into owners
(
own_first_nm,
own_last_nm,
app_id
)
select
'enter first name',
'enter last name',
app_id
from
inserted;
-- dummy record for location
insert into location
(
loc_street,
loc_city,
loc_state,
loc_zip,
app_id
)
select
'enter street',
'enter city',
'ri',
'00000',
app_id
from
inserted;
RETURN;
END
-- update
ELSE
BEGIN
RETURN;
END
END
GO
I left place holders for DELETE and UPDATE operations.
1 - Do you want to reject updates to the id (key) in the appraisal table. Probably not. This can also be taken care (prevented) of by a Foreign Key (FK).
2 - Do you want to cascade deletes? This also can be handled by a FK or in code in the trigger.
Lets see no records in the tables.
select * from appraisal;
select * from owners;
select * from location;
Like Marc / Aaron said, the inserted / deleted tables are record sets. Not a single row.
Order is not guaranteed. Use an order by if you want the records inserted by app id order.
-- Insert 2 records
insert into appraisal
(
app_request,
app_employee_id
)
values
(getdate(), 7),
(dateadd(d, 1, getdate()), 7);
-- Lets see the data
select * from appraisal;
select * from owners;
select * from location;

Why is the natural ID generation in this SQL Stored Proc creating duplicates?

I am incrementing the alphanumeric value by 1 for the productid using stored procedure. My procedure incrementing the values up to 10 records, once its reaching to 10th say for PRD0010...no more its incrementing... however, the problem is it is repeating
the same values PRD0010.. for each SP call.
What could be the cause of this?
create table tblProduct
(
id varchar(15)
)
insert into tblProduct(id)values('PRD00')
create procedure spInsertInProduct
AS
Begin
DECLARE #PId VARCHAR(15)
DECLARE #NId INT
DECLARE #COUNTER INT
SET #PId = 'PRD00'
SET #COUNTER = 0
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
--here increse the vlaue to numeric id by 1
SET #NId = #NId + 1
--GENERATE ACTUAL APHANUMERIC ID HERE
SET #PId = #PId + cast(#NId AS VARCHAR)
INSERT INTO tblProduct(id)values (#PId)
END
Change
SELECT #NId = cast(substring(MAX(id), 4, len(MAX(id))) as int)
FROM tblProduct group by left(id, 3) order by left(id, 3)
To
SELECT TOP 1
#NId = cast(substring(id, 4, len(id)) as int)
FROM tblProduct order by LEN(id) DESC, ID DESC
You have to remember that
PRD009
is always greater than
PRD0010
or
PRD001
All in all, I think your approach is incorrect.
Your values will be
PRD00
PRD001
...
PRD009
PRD0010
PRD0011
...
PRD0099
PRD00100
This will make sorting a complete nightmare.
In addition to astander's analysis, you also have a concurrency issue.
The simple fix would be to add this at the beginning of your proc:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
And add a COMMIT at the end. Otherwise, two callers of this stored proc will get the same MAX/TOP 1 value from your table, and insert the same value.
Also, you can and should prevent these duplicates from existing by adding a key to your table, for this column. If you already have a PRIMARY KEY on this table, you can add an additional key using a UNIQUE constraint. This will prevent duplicates occurring in the future, no matter what programming errors occur. E.g.
ALTER TABLE tblProduct ADD CONSTRAINT UQ_Product_ID UNIQUE (ID)

Resources