Create trigger which would not allow to delete values in specific column? - sql-server

I've a simple table like this:
MARK {IdMark Int, IdStudent Int, Value Int, Subject Varchar(10)}
and I would like to create trigger which would not allow to delete rows in that table but there should be possible to alter values in column "Value" unless it's NULL.
The code below does not work like I would like to at all:
CREATE TRIGGER delValue
ON mark
FOR INSERT, UPDATE, DELETE
AS
IF EXISTS(SELECT 1 FROM inserted i JOIN deleted d ON i.IdMark = d.IdMark WHERE i.IdMark IS NULL)
BEGIN
RAISERROR('You can't delete marks!', 16, 1)
ROLLBACK
END

Try the following:
You only need to check inserted for null values. And if there is nothing in inserted but something in deleted then its a delete.
Also, watch out when using single quotes in a message, you need to escape them (by repeating them).
CREATE TRIGGER delValue
ON mark
FOR INSERT, UPDATE, DELETE
AS
begin
-- If attempting to set to null, rollback
IF EXISTS (SELECT 1 FROM inserted WHERE IdMark IS NULL) BEGIN
RAISERROR('You can''t set marks to null!', 16, 1);
ROLLBACK;
END
-- If attempting to set to delete, rollback
-- There will never be anything in inserted for a delete
IF NOT EXISTS (SELECT 1 FROM inserted) and EXISTS (SELECT 1 FROM Deleted) BEGIN
RAISERROR('You can''t delete marks!', 16, 1);
ROLLBACK;
END
end

Related

Where is the issue in this trigger?

I need your expert observation to make this trigger work.
I have 3 tables, Create_Event is main, 2nd (events_status) is status table and 3rd (event_history) one is 'backup table'.
I just want to keep a track of every events as the status is updated to 'Completed' also want to delete the same row from the Main table but I don't want to lose it until I have a track.
So, here is that what I am trying..but its delete part is not working.
ALTER TRIGGER trgUpdhistory on Events_Status for update
as
declare #status varchar(255)
BEGIN
set xact_abort on
select #status=status from inserted
if (#status = 'Completed')
begin try
begin tran
insert into Event_History
select * from Create_Event
where exists(select * from create_event D left join inserted E on D.ID=E.CE_Ids)
delete from Create_Event
where exists(select * from create_event D left join deleted E on D.ID=E.CE_Ids)
COMMIT
end try
begin catch
ROLLBACK
RAISERROR ('Transaction is not completed',16,1)
end catch
END

T-SQL Trigger - prevent duplicate, but not always

I would like to avoid INSERTS and UPDATES which could duplicate field's value, but not always.
I have a varchar field Cat_Catalog. in table Catalog.
I can have two rows with Cat_Catalog's value "123" duplicated, but I cannot have duplicated field Cat_Catalog which starts with 'KAT' word (so I cannot have 2 rows with "KAT123" Cat_Catalog's value)
The following trigger i made doesn't work fine because field that's going to be updated starts with KAT trigger always raise error (variable #IfExist always return true - it is probably because of AFTER UPDATE,INSERT syntax).
I would like to avoid using INSTEAD OF syntax because updates are generated by some API which to i have no documentation and I'm not sure what to do in case when value doesn't starts with 'KAT'.
GO
/****** Object: Trigger [dbo].[Catalog_InsertUpdateCatalog] ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--If first three characters are 'KAT'
--then check for duplicate and raiseerror
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS
set nocount on;
DECLARE #CatalogInsert varchar(50)
DECLARE #IfKat varchar(10) = 'FALSE'
DECLARE #IfExist varchar(10) = 'FALSE'
SELECT #CatalogInsert = Cat_Catalog
FROM inserted
--Does It starts with 'KAT' ?
IF (#CatalogInsert like 'KAT%')
BEGIN
SET #IfKat = 'TRUE'
END
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
)
BEGIN
SET #IfExist = 'TRUE'
END
IF ( #IfExist = 'TRUE' and #IfKat = 'TRUE' )
BEGIN
RAISERROR ('Catalog allready exists: %s , ISKAT:%s , EXIST:%s', 16, 1, #CatalogInsert, #IfKat, #IfExist);
END
`
The problem is that I don't know how can I check the current value to be updated allready exists in Catalog table (check must be done before update).
Using CHECK constraint instead of a trigger would be a better solution, since triggers are executed much later in the transaction and eventual rollback could be expensive. You can define check constraint as follows:
CREATE FUNCTION IsDuplicate(#col varchar(50))
RETURNS BIT
AS
BEGIN
IF CHARINDEX('KAT', #col) = 1 AND (SELECT COUNT(*) FROM [Catalog] WHERE [Cat_Catalog] = #col) > 1
return 1;
return 0;
END;
GO
ALTER TABLE [Catalog]
ADD CONSTRAINT chkForDuplicates CHECK (dbo.IsDuplicate([Cat_Catalog]) = 0)
GO
Have in mind that if you already have duplicate "KATxxx" values in the table you'll have to either delete them or create the constraint with NOCHECK clause.
Please, try this new version:
ALTER TRIGGER [dbo].[Catalog_InsertUpdateCatalog] ON [dbo].[Catalog]
FOR UPDATE,INSERT
AS BEGIN
set nocount on;
--Check for Duplicate
IF EXISTS(
Select 1
From (
-- Updated
SELECT
COUNT(*) OVER (PARTITION BY Cat_Catalog) Cnt,
Cat_Catalog
FROM dbo.Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
) t
Join inserted i ON t.Cat_Catalog = i.Cat_Catalog AND Cnt > 1
)
BEGIN
RAISERROR ('Catalog allready exists!', 16, 1);
ROLLBACK TRANSACTION;
END
END
This trigger check the existence of rows with the same [Cat_Catalog] field as in the inserted (or updated) rows. If duplicated rows exists and [Cat_Catalog] starts with 'KAT' trigger RAISE error + rollback transaction.
UPDATED: I change trigger. It should now work correctly (i test it). Trigger FOR UPDATE, INSERT fires after changes take place in table. So we need check duplicate rows in the table.
I do it throught COUNT(*) OVER(PARTITION BY Cat_Catalog) but you may check it in a more familiar way:
SELECT
COUNT(*) Cnt,
Cat_Catalog
FROM
dbo._Catalog
WHERE
Cat_Catalog LIKE 'KAT%'
GROUP BY
Cat_Catalog
Use Instead OF Trigger instead After Trigger.
If your problem with Instead OF Triggers, then you need to use NOT Eqals operator to avoid such wrong indication to passed. I supposed to say
DECLARE #CATALOG_PK BIGINT;
SELECT #CATALOG_PK = CATALOG_PK FROM INSERTED
--Check for Duplicate
IF EXISTS(
Select * from Test.dbo.Catalog t
where t.Cat_Catalog = #CatalogInsert
AND CATALOG_PK <>#CATELOG_PK LIKE t.CATALOG LIKE 'KAT%'
)
BEGIN
SET #IfExist = 'TRUE'
END
However this will fail in-case of multiple UPDATES or INSERTS done.
Try these two triggers:
CREATE TRIGGER [MyCatalog_InsertCatalog] ON [dbo].[MyCatalog]
FOR INSERT
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
GO
CREATE TRIGGER [MyCatalog_UpdateCatalog] ON [dbo].[MyCatalog]
FOR UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
EDIT
You could combine both triggers into one:
CREATE TRIGGER [MyCatalog_InsertUpdateCatalog] ON [dbo].[MyCatalog]
FOR INSERT, UPDATE
AS
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
INNER JOIN deleted d
ON d.Cat_Id = i.Cat_Id
WHERE I.Cat_Catalog LIKE 'KAT%' AND d.Cat_Catalog <> i.Cat_Catalog)
BEGIN
RAISERROR ('Update Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
ELSE BEGIN
IF EXISTS(
SELECT I.*
FROM Inserted I
INNER JOIN MyCatalog M
ON M.Cat_Catalog = I.Cat_Catalog
WHERE I.Cat_Catalog LIKE 'KAT%' )
BEGIN
RAISERROR ('Insert Catalog already exists: ', 16, 1);
ROLLBACK TRANSACTION;
END
END

Trigger for three operation(Update,Delete,Insert) conversion error

I'm trying to create one trigger for insert,delete and update on customers table.
Trigger was created successfully ,however, I'm sure it contains many errors.
Here is my code of trigger
go
create trigger Onetrig
on customers
after update,insert,delete
as
declare #log varchar(100),#name varchar(20),#activity varchar(20)
begin
if exists(select * from deleted) and exists(select * from deleted)
set #activity = 'Update'
set #name = (select c_name from inserted)
set #log = 'Record Updated in database '+#name
insert into logs
values(#activity,#log,GETDATE())
if exists(select * from inserted) and not exists(select * from deleted)
set #activity = 'Insert'
set #name = (select c_name from inserted)
set #log = 'Record Inserted in database '+#name
insert into logs
values(#activity,#log,GETDATE())
if exists(select * from deleted) and not exists(select * from inserted)
set #activity = 'Insert'
set #name = (select c_name from deleted)
set #log = 'Record Deleted from database '+#name
insert into logs
values(#activity,#log,GETDATE())
end
My task is to populate the log table with the activities of these three operations. when I perform any of the three operations it throws error of some kind of conversion.
Msg 8152, Level 16, State 14, Procedure Onetrig, Line 11
String or binary data would be truncated.
The statement has been terminated.
customers table has some records.
Here is the code of my customers & log table:
create table logs
(activity varchar(20),report varchar(20),Time datetime)
create table customers
(c_id int primary key identity(1,1) NOT NULL,c_name varchar(20),c_lastname varchar(20))
What conversion am I missing?
You have declared report to be 20 characters in logs. But, you are inserting the string 'Record Updated in database '+#name into it. This has at least 27 characters.
Suggestions:
Fix the length of the varchar fields. You might as well make them much longer, even up to 8000 characters.
When doing insert, always include the list of columns explicitly.
Fix the first if statement to refer to inserted and deleted, so the logic makes sense.
Put semicolons at the end of each statement.
Gordon Linoff's answer is very good.
I would like to add that in case of trigger, following lines
set #name = (select c_name from inserted)
set #name = (select c_name from deleted)
assume that there is always only one row inserted / deleted / updated. Which is wrong because DML triggers are at statement level not at row level.
I would change (for I/U/D) thus:
if exists ...
begin
set #activity = 'U/I/D'
insert into dbo.logs (... columns ...) -- <-- You should use the schema (default schema is dbo)
select #activity, 'Record Updated in database ' + x.c_name, GETDATE()
-- ^
-- |
-- ----------------------------------------------------------
-- The maximum length of dbo.logs.c_name (!?) column should be
-- LEN('Record Updated in database') + 1 + MaxLength of dbo.customers.c_name column
from inserted as x -- or deleted
end

Create Transaction and Trigger on a field

Please am new in vb.net and sql server, I have created a two tables in database called Service and Trans.
Create Table Service(
ServiceID int,
ServiceName varchar(30),
ServiceStartValue int
);
Create Table Trans(
EntryTS datetime,
EntryCounter int,
ServedTS datetime,
ServedCounter int,
Skipped int
);
I am trying to create a 'transaction and trigger' that will check and update ServedCounter based on the values in EntryCounter upon ServiceID which the update statement must not allow the ServedCounter > EntryCounter.
I don't quite understand the full requirement, but here's how you can prevent a update (or insert) from happening with a trigger.
CREATE TRIGGER Trans_upd_trg ON Trans AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Don't allow the update
IF EXISTS(SELECT 1 FROM inserted WHERE ServedCounter > EntryCounter)
RAISERROR ('ServedCounter > EntryCounter', 16, 1 );
END
GO
Within the context of the trigger you have two logical tables, INSERTED, and DELETED.
These tables contain the old and new values. (deleted is empty for a insert operation)
Hope that helps.
Use a Instead Of Trigger
CREATE TRIGGER Trans_upd_trg
ON Trans
Instead OF INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter)
AND EXISTS (SELECT *
FROM deleted)
UPDATE A
SET EntryTS = I.EntryTS,
EntryCounter = I.EntryCounter,
ServedTS = I.ServedTS,
ServedCounter = I.ServedCounter,
Skipped = I.Skipped
FROM Trans A
JOIN inserted I
ON A.EntryTS = I.EntryTS
AND A.ServedTS = I.ServedTS
WHERE i.ServedCounter > i.EntryCounter
ELSE
INSERT INTO Trans
SELECT *
FROM inserted
WHERE ServedCounter > EntryCounter
END
GO

SQL trigger on update or delete

I have to have one single trigger that fires on either the UPDATE OR DELETE operations. I have the trigger working fine for when one certain column is updated. However, I need different logic for when a DELETE operation was fired. How would I have both logic inside of one trigger? Here is what I have so far:
ALTER TRIGGER [dbo].[Audit_Emp_Trigger]
ON [dbo].[EMPLOYEE]
AFTER UPDATE, DELETE
AS
BEGIN
--Only execute the trigger if the Dno field was updated or deleted
IF UPDATE(Dno)
BEGIN
--If the Audit_Emp_Record table does not exist already, we need to create it
IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL
BEGIN
--Table does not exist in database, so create table
CREATE TABLE Audit_Emp_Record
(
date_of_change smalldatetime,
old_Lname varchar (50),
new_Lname varchar (50),
old_ssn int,
new_ssn int,
old_dno int,
new_dno int
);
--Once table is created, insert the values of the update operation into the table
INSERT INTO Audit_Emp_Record(date_of_change, old_Lname, new_Lname, old_ssn, new_ssn, old_dno, new_dno) SELECT GETDATE(), D.Lname, I.Lname, D.Ssn, I.Ssn, D.Dno, I.Dno FROM inserted I JOIN deleted D ON I.Ssn = D.Ssn
END
ELSE
BEGIN
--The table already exists, so simply insert the new values of the update operation into the table
INSERT INTO Audit_Emp_Record(date_of_change, old_Lname, new_Lname, old_ssn, new_ssn, old_dno, new_dno) SELECT GETDATE(), D.Lname, I.Lname, D.Ssn, I.Ssn, D.Dno, I.Dno FROM inserted I JOIN deleted D ON I.Ssn = D.Ssn
END
END
END
You can test for the type of operation by seeing which of the magic-/pseudo-tables -- INSERTED and DELETED have data in them. I prefer to use something like the following:
DECLARE #Operation CHAR(1);
IF (EXISTS(SELECT * FROM inserted))
BEGIN
IF (EXISTS(SELECT * FROM deleted))
BEGIN
-- rows in both has to be an UPDATE
SET #Operation = 'U';
END;
ELSE
BEGIN
-- no rows in "deleted" has to be an INSERT
SET #Operation = 'I';
END;
END;
ELSE
BEGIN
-- no rows in "inserted" has to be a DELETE
SET #Operation = 'D';
END;
You can then use the #Operation variable in an IF statement to do one or the other of those operations.
Something like:
IF (#Operation = 'U')
BEGIN
--Only execute the trigger if the Dno field was updated or deleted
IF UPDATE(Dno)
BEGIN
{your current code here}
END;
END;
ELSE
BEGIN
{what to do if the operation is a DELETE goes here}
END;
Technically you don't need the ELSE condition that sets #Operation = 'I';, but if you are going to copy/paste this code into various triggers or keep around as a template then no harm in it handling all three conditions.
Also, just as a side-note, you don't need the ELSE condition of the IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL statement, nor the INSERT INTO Audit_Emp_Record that is just after the CREATE TABLE but before the END. Just do the CREATE TABLE if it doesn't exist and then do the INSERT outside of that test. Meaning:
IF UPDATE(Dno)
BEGIN
--If the Audit_Emp_Record table does not exist already, we need to create it
IF OBJECT_ID('dbo.Audit_Emp_Record') IS NULL
BEGIN
--Table does not exist in database, so create table
CREATE TABLE Audit_Emp_Record
...
END
INSERT INTO Audit_Emp_Record(...)
END

Resources