Check constraint violation before After Update trigger fires - sql-server

I have a table which has a bit column and a corresponding datetime2 column which tracks when that flag was set:
CREATE TABLE MyTable
(
Id int primary key identity,
Processed bit not null,
DateTimeProcessed datetime2
)
I've added a check constraint as follows:
ALTER TABLE MyTable
ADD CHECK ((Processed = 0 AND DateTimeProcessed IS NULL)
OR (Processed = 1 AND DateTimeProcessed IS NOT NULL))
I attempted to control setting of the DateTimeProcessed column using an AFTER UPDATE trigger:
CREATE TRIGGER tr_MyTable_AfterUpdate ON MyTable
AFTER UPDATE
AS
BEGIN
IF(UPDATE(Processed))
BEGIN
UPDATE MyTable
SET DateTimeProcessed = CASE
WHEN tab.Processed = 1 THEN GETDATE()
ELSE NULL
END
FROM MyTable tab
JOIN INSERTED ins
ON ins.Id = tab.Id
END
END
The problem with this is that the check constraint is enforced before the AFTER UPDATE trigger runs, so the constraint is violated when the Processed column is updated.
What would be the best way to achieve what I am trying to do here?

Now, according to the MSDN page for CREATE TABLE:
If a table has FOREIGN KEY or CHECK CONSTRAINTS and triggers, the constraint conditions are evaluated before the trigger is executed.
This rules out the possibility of using an "INSTEAD OF" trigger as well.
You should remove the CHECK CONSTRAINT as ultimately it is not needed since the AFTER trigger itself can provide the same enforcement of the rule:
You are already making sure that the date field is being set upon the BIT field being set to 1.
Your CASE statement is already handling the BIT field being set to 0 by NULLing out the date field.
You can have another block to check on IF UPDATE(DateTimeProcessed) and either put it back to what it was in the DELETED table or throw an error.
If you update it back to the original value then you might need to test for recursive trigger calls and exit if it is a recursive call.
If you want to throw an error, just use something along the lines of:
IF(UPDATE(DateTimeProcessed))
BEGIN
RAISERROR('Update of [DateTimeProcessed] field is not allowed.', 16, 1);
ROLLBACK; -- cancel the UPDATE statement
RETURN;
END;
Keep in mind that the UPDATE() function only indicates that the field was in the UPDATE statement; it is not an indication of the value changing. Hence, doing an UPDATE wherein you SET DateTimeProcessed = DateTimeProcessed would clearly not change the value but would cause UPDATE(DateTimeProcessed) to return "true".
You can also handle this portion of the "rule" outside of a trigger by using a column-level DENY:
DENY UPDATE ON MyTable (DateTimeProcessed) TO {User and/or Role};

Related

Update a row in one table and also creating a new row in another table to satisfy a foreign key relationship

I have a stored procedure that updates a type of Star. The database table, starValList, has a foreign key for a table called galaxyValList. That key is galaxyID.
So I need to create a new galaxyID value if it is null or empty GUID.
So I try this:
IF(#galaxyID IS NULL OR #galaxyID = '00000000-0000-0000-0000-000000000000')
BEGIN
SELECT #galaxyID=NEWID()
END
UPDATE starValList SET
[starIRR]= #starIRR,
[starDesc] = #starDesc,
[starType] = #starType,
[galaxyID]=#galaxyID
WHERE [starID] = #starID;
And it works for the starValList table!
But I think it fails too because of this error:
The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_starValList_galaxyValList". The conflict occurred in database "Astro105", table "dbo.galaxyValList", column 'galaxyID'.
It fails because there may not yet be an entry for that particular galaxy in galaxyValList table.
But I still need the row in galaxyValList because it can be used later.
How can I fix my stored procedure so that it doesn't generate this error?
Thanks!
Use if exists to check if the value exists on the table. If it does then do an update. If it doesn't then have some other logic to maybe create it or whatever your requirements may be so the value can then be used in an update. Basic example below:
IF(#galaxyID IS NULL OR #galaxyID = '00000000-0000-0000-0000-000000000000')
BEGIN
SELECT #galaxyID=NEWID()
END
if not exists ( select top 1 1 from galaxyTable where galaxyId = #galaxyId)
begin
-- the #galaxyId doesnt exist, create it so you can use the value in an update later
insert into galaxyTable ( galaxyId ) select #galaxyId
end
UPDATE starValList SET
[starIRR]= #starIRR,
[starDesc] = #starDesc,
[starType] = #starType,
[galaxyID]=#galaxyID
WHERE [starID] = #starID;

How do I make Check Constraints run before statement

I am running into a problem where my check constraints are correctly stopping commands from executing but my Identity column value increases. I guess this is because the check occurs after the statement runs and the transaction gets rolled back due to the check failing. This leaves the identity value incremented by 1.
Is there a way to run the constraint check before the SQL statement gets executed?
CREATE TABLE TestTable
(
Id INT IDENTITY(1,1) PRIMARY KEY(Id),
Name VARCHAR(100)
)
INSERT INTO TestTable VALUES ('Type-1'),('Type-2'),('Type-55'),('Type-009')
--Add a check constraint so nobody can edit this without doing serious work
ALTER TABLE TestTable WITH NOCHECK ADD CONSTRAINT [CHECK_TestTable_READONLY] CHECK(1=0)
--This fails with the constraint as expected
INSERT INTO TestTable VALUES('This will Fail')
INSERT INTO TestTable VALUES('This will again....')
--Check the Id, it was incremented...
SELECT (IDENT_CURRENT( 'TestTable' ) ) As CurrentIdentity
When I had to do the same thing in the past I created a trigger that just threw an exception on insert and delete. this has several advantages, most importantly is that it prevents updates and deletes and you could give a custom exception message explaining what you did there and why, its an extremely bad habit to just put illogical constraints and hope that 3 months from now people would understand whats going on there and know they should ask you about it. It also prevents the Id counter from being incremented if its that important. If it is important, I would also not use auto increment and just set the ID number manually, since even if you are using these triggers you could always have an accidental syntax error or any other error after you disabled them and tried to add a value.
create trigger PreventChanges
on TestTable
FOR INSERT, UPDATE, DELETE
as
begin
throw 51000, 'DO NOT change anything in that table unless you really have to! in order to do so pleasae talk to GER (or just disable and reenable this trigger)',1
and
It sounds like you're intending to use the identity column for something it's not meant for. But to answer your question, could you not just manually code up some SQL Server IF statements to test your data before the insert happens (perhaps in a stored procedure)? I wouldn't know how to make this dynamic to 'fit all constraints on any table', but the process would do what you want - prevent the INSERT from firing. Though, if your constraints change, then you would have to change the procedure too.
e.g.,
IF 1 = 0 -- or use any of your constraints here...
BEGIN
-- nest more IFs if you have multiple check-constraints...
INSERT INTO TestTable
VALUES ('This will not increase your identity number since 1 does not equal 0')
END

Create a trigger that won't let updating the primary key columns

I have a table that has a composite primary key made from 3 columns, let's say A, B, C. I want to create a trigger that on UPDATE will check that these three columns won't be changed. This is what I have so far, but it doesn't seem to work:
CREATE TRIGGER TableTrigger
ON Table
AFTER INSERT, UPDATE AS
BEGIN
IF (EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted))
BEGIN
-- Update Operation
IF (SELECT COUNT(*) FROM inserted WHERE A IS NOT NULL OR B IS NOT NULL OR C IS NOT NULL) > 0
BEGIN
RAISERROR('Error, you cannot change Primary Key columns', 16, 1)
ROLLBACK
RETURN
END
END
I was expecting that if I update some values in a table, in inserted the values for the columns I don't update to be NULL, but it's not like that. I read somewhere that I need to look both in inserted and deleted to see if these values changed. So my question is this, can I check this without using a cursor?
Thank you.
You could do
CREATE TRIGGER TableTrigger
ON Table
AFTER UPDATE AS
BEGIN
IF UPDATE(A) OR UPDATE(B) OR UPDATE(C)
BEGIN
RAISERROR('Error, you cannot change Primary Key columns', 16, 1)
ROLLBACK
RETURN
END
END
Or deny update permissions on those columns.
Both approaches would deny any attempt to update the PK columns irrespective of whether or not the values actually change. SQL Server does not have row level triggers and unless there is an IDENTITY column in the table (guaranteed immutable) there is no reliable way to tell in a trigger if the PK was actually updated.
For example the INSERTED and DELETED tables in an UPDATE trigger on the table below would be identical for both the UPDATE statements.
CREATE TABLE T(C INT PRIMARY KEY);
INSERT INTO T VALUES (1),(-1)
/*Both values swapped*/
UPDATE T SET C = -C
/*Both values left the same*/
UPDATE T SET C = C

Stuck at creating a trigger

performing some MSSQL exercises, and I am trying to create a trigger. However, the solution I have, comes across as theoretically correct to me but it is not working.
The aim is to create a trigger for a table that has only two columns. One column is the primary key and is Identity and does not allow null values. The other column is one that ALLOWS NULL values. However, it permits NULL values ONLY FOR A SINGLE ROW in the entire table. Basically a trigger should fire for an insert/update operation on this table which attempts to insert/update the column to a NULL value when there is already an existing NULL value for the column in the table.
This condition I capture in my trigger code as follows:
After Insert, Update
AS
set ANSI_WARNINGS OFF
If ( (select count(NoDupName) from TestUniqueNulls where NoDupName is null) > 1 )
BEGIN
Print 'There already is a row that contains a NULL value, transaction aborted';
ROLLBACK TRAN
END
However, the transaction executes itself nonetheless. I am not sure why this is happening and the trigger is not firing itself.
So anybody to enlighten my misgivings here?
I also have used set ANSI_WARNINGS OFF at the start of the trigger.
count(col) only counts non null values so count(NoDupName) ... where NoDupName is null will always be zero. You would need to check count(*) instead.
I realise this is just a practice exercise but an indexed view might be a better mechanism for this.
CREATE VIEW dbo.NoMoreThanOneNull
WITH SCHEMABINDING
AS
SELECT NoDupName
FROM dbo.TestUniqueNulls
WHERE NoDupName IS NULL
GO
CREATE UNIQUE CLUSTERED INDEX ix ON dbo.NoMoreThanOneNull(NoDupName)
Yeah that's a gotcha. The expression inside parens of the COUNT has to evaluate to not null, otherwise it will not be counted. So it is safer to use *, or 1 or any not nullable column in the expression. The most commonly encountered expression is '*', although you can come across '1' as well. There is no difference between these expressions in terms of performance. However if you use expression that can evaluate to null (like nullable column), your counts and other aggregations can be completely off.
create table nulltest(a int null)
go
insert nulltest(a) values (1), (null), (2)
go
select * from nulltest
select COUNT(*) from nulltest
select COUNT(1) from nulltest
select COUNT(a) from nulltest
go
drop table nulltest

SQL Server bit column constraint, 1 row = 1, all others 0

I have a bit IsDefault column. Only one row of data within the table may have this bit column set to 1, all the others must be 0.
How can I enforce this?
All versions:
Trigger
Indexed view
Stored proc (eg test on write)
SQL Server 2008: a filtered index
CREATE UNIQUE INDEX IX_foo ON bar (MyBitCol) WHERE MyBitCol = 1
Assuming your PK is a single, numeric column, you could add a computed column to your table:
ALTER TABLE YourTable
ADD IsDefaultCheck AS CASE IsDefault
WHEN 1 THEN -1
WHEN 0 THEN YourPK
END
Then create a unique index on the computed column.
CREATE UNIQUE INDEX IX_DefaultCheck ON YourTable(IsDefaultCheck)
I think the trigger is the best idea if you want to change the old default record to 0 when you insert/update a new one and if you want to make sure one record always has that value (i.e. if you delete the record with the value you would assign it to a different record). You would have to decide on the rules for doing so. These triggers can be tricky because you have to account for multiple records in the inserted and deleted tables. So if 3 records in a batch try to update to become the default record, which one wins?
If you want to make sure the one default record never changes when someone else tries to change it, the filtered index is a good idea.
Different approaches can be taken here, but I think only two are correct. But lets do it step by step.
We have table Hierachy table in which we have Root column. This column tells us what row is currently the starting point. As in question asked, we want to have only one starting point.
We think that we can do it with:
Constraint
Indexed View
Trigger
Different table and relation
Constraint
In this approach first we need to create function which will do the job.
CREATE FUNCTION [gt].[fnOnlyOneRoot]()
RETURNS BIT
BEGIN
DECLARE #rootAmount TINYINT
DECLARE #result BIT
SELECT #rootAmount=COUNT(1) FROM [gt].[Hierarchy] WHERE [Root]=1
IF #rootAmount=1
set #result=1
ELSE
set #result=0
RETURN #result
END
GO
And then the constraint:
ALTER TABLE [gt].[Hierarchy] WITH CHECK ADD CONSTRAINT [ckOnlyOneRoot] CHECK (([gt].[fnOnlyOneRoot]()=(1)))
Unfortunately approach is wrong as this constraint won't allow us to change any values in the table. It need to have exactly one root marked (insert with Root=1 will throw exception, and update with set Root=0 also)
We could change the fnOnyOneRoot to allow having 0 selected roots but it not what we wanted.
Index
Index will remove all rows which are defined in the where clause and on the rest data will setup unique constraint. We have different options here:
- Root can be nullable and we can add in where Root!=0 and Root is not null
- Root must have value and we can add only in where Root!=0
- and different combinations
CREATE UNIQUE INDEX ix_OnyOneRoot ON [gt].[Hierarchy](Root) WHERE Root !=0 and Root is not null
This approach also is not perfect. Maximum one Root will be forced, but minimum not. To update data we need to set previous rows to null or 0.
Trigger
We can do two kinds of trigger both behaves differently
- Prevent trigger - which won't allow us to put wrong data
- DoTheJob trigger - which in background will update data for us
Prevent trigger
This is basically the same as constraint, if we want to force only one root than we cannot update or insert.
CREATE TRIGGER tOnlyOneRoot
ON [gt].[Hierarchy]
AFTER INSERT, UPDATE
AS
DECLARE #rootAmount TINYINT
DECLARE #result BIT
SELECT #rootAmount=COUNT(1) FROM [gt].[Hierarchy] WHERE [Root]=1
IF #rootAmount=1
set #result=1
ELSE
set #result=0
IF #result=0
BEGIN
RAISERROR ('Only one root',0,0);
ROLLBACK TRANSACTION
RETURN
END
GO
DoTheJob trigger
This trigger will check for all inserted/updated rows and if more than one Root will be passed it will throw exception. In other case, so if one new Root will be updated or inserted, trigger will allow to do it and after operation it will change Root value for all other rows to 0.
CREATE TRIGGER tOnlyOneRootDoTheJob
ON [gt].[Hierarchy]
AFTER INSERT, UPDATE
AS
DECLARE #insertedCount TINYINT
SELECT #insertedCount = COUNT(1) FROM inserted WHERE [Root]=1
if (#insertedCount > 1)
BEGIN
RAISERROR ('Only one root',0,0);
ROLLBACK TRANSACTION
RETURN
END
DECLARE #newRootId INT
SELECT #newRootId = [HierarchyId] FROM inserted WHERE [Root]=1
UPDATE [gt].[Hierarchy] SET [Root]=0 WHERE [HierarchyId] <> #newRootId
GO
This is the solution we tried to achieve. Only one root rule is always meet. (Additional trigger for Delete should be done)
Different table and relation
This is lets say more normalized way. We create new table allow only to have one row (using the options described above) and we join.
CREATE TABLE [gt].[HierarchyDefault](
[HierarchyId] INT PRIMARY KEY NOT NULL,
CONSTRAINT FK_HierarchyDefault_Hierarchy FOREIGN KEY (HierarchyId) REFERENCES [gt].[Hierarchy](HierarchyId)
)
Does it will hit the performance?
With one column
SET STATISTICS TIME ON;
SELECT [HierarchyId],[ParentHierarchyId],[Root]
FROM [gt].[Hierarchy] WHERE [root]=1
SET STATISTICS TIME OFF;
Result
CPU time = 0 ms, elapsed time = 0 ms.
With join:
SET STATISTICS TIME ON;
SELECT h.[HierarchyId],[ParentHierarchyId],[Root]
FROM [gt].[Hierarchy] h
INNER JOIN [gt].[HierarchyDefault] hd on h.[HierarchyId]=hd.[HierarchyId]
WHERE [root]=1
SET STATISTICS TIME OFF;
Result
CPU time = 0 ms, elapsed time = 0 ms.
Summary
I will use the trigger. It is some magic in the table, but it did all job under the hood.
Easy table creation:
CREATE TABLE [gt].[Hierarchy](
[HierarchyId] INT PRIMARY KEY IDENTITY(1,1),
[ParentHierarchyId] INT NULL,
[Root] BIT
CONSTRAINT FK_Hierarchy_Hierarchy FOREIGN KEY (ParentHierarchyId)
REFERENCES [gt].[Hierarchy](HierarchyId)
)
You could apply an Instead of Insert trigger and check the value as it's coming in.
Create Trigger TRG_MyTrigger
on MyTable
Instead of Insert
as
Begin
--Check to see if the row is marked as active....
If Exists(Select * from inserted where IsDefault= 1)
Begin
Update Table Set IsDefault=0 where ID= (select ID from inserted);
insert into Table(Columns)
select Columns from inserted
End
End
Alternatively you could apply a unique constraint on the column.
The accepted answer to the below question is both interesting and relevant:
Constraint for only one record marked as default
"But the serious relational folks will tell you this information
should just be in another table."
Have a separate 1 row table that tells you which record is 'default'. Anon touched on this in his comment.
I think this is the best approach - simple, clean & doesn't require a 'clever' esoteric solution prone to errors or later misunderstanding. You can even drop the IsDefualt column.

Resources