How to create a Trigger on table to identify whether a particular record been deleted/updated and store the information of entity updating it? - database

i need to save information in another temp table say , TableTemp having the records being modified and with one more column defining which entity updated it.

You look like you're just discovering, and ask very wide questions. However, here is a possible solution, assuming the below:
a_sqnc is the sequence you will use in TableTemp to keep track of the order of actions in column NO_ORD (even though there is also a D_UPD column with the modification time).
create sequence a_sqnc
minvalue 1
maxvalue 99999999
start with 1
increment by 1
nocache;
TableTemp will have a TABLE_NAME column in order to track changes from different tables. It also have a PK_VALUE and ROW_VALUE where we store the data that changed. Here is the table creation with useful indexes:
create table TableTemp (
table_name VARCHAR2(50) not null,
action VARCHAR2(240) not null,
no_ord NUMBER(12) not null,
nature VARCHAR2(3) not null,
pk_value VARCHAR2(4000),
row_value VARCHAR2(4000),
ori VARCHAR2(250),
c_user VARCHAR2(20),
d_upd DATE
);
create index AP_D_UPD on TableTemp (D_UPD);
create index AP_NO_ORD on TableTemp (NO_ORD);
create index AP_TABLE_NAME on TableTemp (TABLE_NAME);
Say you have a simple table BANK with two columns PK_val (the primary key) and val:
create table BANK (
pk_val VARCHAR2(50) not null,
val VARCHAR2(240) not null
);
alter table BANK
add constraint BK_PK primary key (pk_val)
using index ;
Use DBMS_APPLICATION_INFO.READ_MODULE(w_sess_mod, w_sess_act) to know what module and what action operates: I concatenate both in column ORI in TableTemp;
user Oracle session variable will allow you tracking who did the change in column c_user;
Here is how to create trigger AUD_BNK to track changes in table BANK; it will categorize in 3 actions: DELETE, UPDATE, INSERT (you can remove the INSERT case if needed).
CREATE OR REPLACE TRIGGER "AUD_BNK"
AFTER DELETE OR INSERT OR UPDATE
ON BANQUE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
w_a VARCHAR2(10);
W_ERRM VARCHAR2(1000);
W_CODE VARCHAR2(1000);
w_n VARCHAR2(200) := 'BANK';
w_id NUMBER := a_sqnc.nextval;
w_act v$session.action%type;
w_mod v$session.module%type;
w_ori TableTemp.ORI%TYPE;
BEGIN
DBMS_APPLICATION_INFO.READ_MODULE(w_mod, w_act);
w_ori := 'Module : '||w_mod ||' ; Action : '||w_act;
----------------------------------
-- test which action is for change
----------------------------------
IF UPDATING
THEN
w_a := 'UPDATE';
ELSIF DELETING
THEN
w_a := 'DELETE';
ELSIF INSERTING
THEN
w_a := 'INSERT';
END IF;
----------------------------------
-- Insert into TableTemp
----------------------------------
If w_a in ('UPDATE', 'DELETE') then
Insert into TableTemp
Select w_n, w_a, w_id, 'OLD', :OLD.pk_val, :OLD.val
, w_ori, user, sysdate
From Dual;
End if;
-- if you update, there is a new value and an old value
If w_a in ('UPDATE', 'INSERT') then
Insert into TableTemp
Select w_n, w_a, w_id, 'NEW', :NEW.pk_val, :NEW.val
, w_ori, user, sysdate
From Dual;
End if;
Exception
When others then
Begin
W_ERRM := SQLERRM;
W_CODE := SQLCODE;
-- try inserting in case of error anyway
Insert into TableTemp
Select w_n, w_a, -1, 'ERR', 'Grrr: '||W_CODE, W_ERRM
, w_ori, user, sysdate
From Dual;
End;
End;
/
Beware!
This way of tracking every change on the table will deeply impair performances if table changes. But it is great for parameter tables that scarcely change.

Related

How to create a column null or not-null dependent on the value of another column?

I'm using database first approach with EF core and trying to figure out a clean solution to the below problem -
Consider a Student attendance table (irrelevant columns removed) below that stores date of class and allows the student to enter his class rating -
create table Student (
Id int Identity(1, 1) not null,
ClassDate smalldatetime not null,
ClassRatingByStudent varchar(250) not null
)
This is a webapp where school attendance system automatically populates the above table at EOD and then the student (let's say a few days later) is required to add class ratings. When the table is populated by the school attendance system, there is nothing in the ClassRatingByStudent column. Then when the student logs in, he must add the rating.
As you see, ClassRatingByStudent must be null when the school attendance system populates the table and must be not-null when the student saves his changes. One obvious solution is make ClassRatingByStudent column nullable ad handle it in the code but I'm wondering if there is a neater database (or maybe EF) level solution exists or some sort of pattern/architecture guidelines for this type of scenarios?
I don't know but maybe CHECK constraint could help you:
CREATE TABLE TestTable(
ID int NOT NULL IDENTITY,
RatingAllowed bit NOT NULL DEFAULT 0, -- switcher
RatingValue varchar(250),
CONSTRAINT PK_TestTable PRIMARY KEY(ID),
CONSTRAINT CK_TestTable_RatingValue CHECK( -- constraint
CASE
WHEN RatingAllowed=0 AND RatingValue IS NULL THEN 1
WHEN RatingAllowed=1 AND RatingValue IS NOT NULL THEN 1
ELSE 0
END=1
)
)
INSERT TestTable(RatingAllowed,RatingValue)VALUES(0,NULL)
INSERT TestTable(RatingAllowed,RatingValue)VALUES(1,'AAA')
-- The INSERT statement conflicted with the CHECK constraint "CK_TestTable_RatingValue"
INSERT TestTable(RatingAllowed,RatingValue)VALUES(0,'AAA')
INSERT TestTable(RatingAllowed,RatingValue)VALUES(1,NULL)
I found a variant how to check using another table as switcher
CREATE TABLE TableA(
ID int NOT NULL IDENTITY PRIMARY KEY,
StudentID int NOT NULL,
Grade int
)
CREATE TABLE TableB(
StudentID int NOT NULL PRIMARY KEY
)
GO
-- auxiliary function
CREATE FUNCTION GradeIsAllowed(#StudentID int)
RETURNS bit
BEGIN
DECLARE #Result bit=CASE WHEN EXISTS(SELECT * FROM TableB WHERE StudentID=#StudentID) THEN 1 ELSE 0 END
RETURN #Result
END
GO
-- constraint to check
ALTER TABLE TableA ADD CONSTRAINT CK_TableA_Grade CHECK(
CASE dbo.GradeIsAllowed(StudentID) -- then we can use the function here
WHEN 1 THEN CASE WHEN Grade IS NOT NULL THEN 1 ELSE 0 END
WHEN 0 THEN CASE WHEN Grade IS NULL THEN 1 ELSE 0 END
END=1)
GO
-- Tests
INSERT TableB(StudentID)VALUES(2) -- allowed student
INSERT TableA(StudentID,Grade)VALUES(1,NULL) -- OK
INSERT TableA(StudentID,Grade)VALUES(2,5) -- OK
INSERT TableA(StudentID,Grade)VALUES(1,4) -- Error
INSERT TableA(StudentID,Grade)VALUES(2,NULL) -- Error
INSERT TableB(StudentID)VALUES(1) -- add 1
UPDATE TableA SET Grade=4 WHERE StudentID=1 -- OK
UPDATE TableA SET Grade=NULL WHERE StudentID=1 -- Error

How can I use a trigger to allow an incremented, user-assigned ID?

I am moving a small database from MS Access into SQL Server. Each year, the users would create a new Access database and have clean data, but this change will put data across the years into one pot. The users have relied on the autonumber value in Access as a reference for records. That is very inaccurate if, say, 238 records are removed.
So I am trying to accommodate them with an id column they can control (somewhat). They will not see the real primary key in the SQL table, but I want to give them an ID they can edit, but still be unique.
I've been working with this trigger, but it has taken much longer than I expected.
Everything SEEMS TO work fine, except I don't understand why I have the same data in my INSERTED table as the table the trigger is on. (See note in code.)
ALTER TRIGGER [dbo].[trg_tblAppData]
ON [dbo].[tblAppData]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #NewUserEnteredId int = 0;
DECLARE #RowIdForUpdate int = 0;
DECLARE #CurrentUserEnteredId int = 0;
DECLARE #LoopCount int = 0;
--*** Loop through all records to be updated because the values will be incremented.
WHILE (1 = 1)
BEGIN
SET #LoopCount = #LoopCount + 1;
IF (#LoopCount > (SELECT Count(*) FROM INSERTED))
BREAK;
SELECT TOP 1 #RowIdForUpdate = ID, #CurrentUserEnteredId = UserEnteredId FROM INSERTED WHERE ID > #RowIdForUpdate ORDER BY ID DESC;
IF (#RowIdForUpdate IS NULL)
BREAK;
-- WHY IS THERE A MATCH HERE? HAS THE RECORD ALREADY BEEN INSERTED?
IF EXISTS (SELECT UserEnteredId FROM tblAppData WHERE UserEnteredId = #CurrentUserEnteredId)
BEGIN
SET #NewUserEnteredId = (SELECT Max(t1.UserEnteredId) + 1 FROM tblAppData t1);
END
ELSE
SET #NewUserEnteredId = #CurrentUserEnteredId;
UPDATE tblAppData
SET UserEnteredId = #NewUserEnteredId
FROM tblAppData a
WHERE a.ID = #RowIdForUpdate
END
END
Here is what I want to accomplish:
When new record(s) are added, it should increment values from the Max existing
When a user overrides a value, it should check to see the existence of that value. If found restore the existing value, otherwise allow the change.
This trigger allows for multiple rows being added at a time.
It is great for this to be efficient for future use, but in reality, they will only add 1,000 records a year.
I wouldn't use a trigger to accomplish this.
Here is a script you can use to create a sequence (op didn't tag version), create the primary key, use the sequence as your special id, and put a constraint on the column.
create table dbo.test (
testid int identity(1,1) not null primary key clustered
, myid int null constraint UQ_ unique
, somevalue nvarchar(255) null
);
create sequence dbo.myid
as int
start with 1
increment by 1;
alter table dbo.test
add default next value for dbo.myid for myid;
insert into dbo.test (somevalue)
select 'this' union all
select 'that' union all
select 'and' union all
select 'this';
insert into dbo.test (myid, somevalue)
select 33, 'oops';
select *
from dbo.test
insert into dbo.test (somevalue)
select 'oh the fun';
select *
from dbo.test
--| This should error
insert into dbo.test (myid, somevalue)
select 3, 'This is NO fun';
Here is the result set:
testid myid somevalue
1 1 this
2 2 that
3 3 and
4 4 this
5 33 oops
6 5 oh the fun
And at the very end a test, which will error.

trigger for insert or update after checking relationship

I have this 3 tables:
And i need to build a trigger that: A date ("encontro") can only works when theres a friendship ("amizade") between 2 profiles ("perfis").
I've created this trigger but i feel lost.. HELP ME
CREATE TRIGGER relaƧoes_after_insert
ON encontros
INSTEAD OF insert -
as
begin
declare #idperfilA int;
declare #idperfilB int;
declare #data datetime;
declare #count int;
declare cursor_1 cursor for select * from inserted;
open cursor_1;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
WHILE ##FETCH_STATUS = 0
BEGIN
if exists( select * from inserted i, amizade a
where i.IDPERFILA = a.IDPERFILA and i.IDPERFILB = a.IDPERFILB and GETDATE() > DATA)
RAISERROR('there isnt friendship', 16, 10);
else
insert into ENCONTROS select * from inserted;
end;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
END
close cursor_1;
deallocate cursor_1;
I think the better answer would be to not create use a trigger for this at all. Instead I would create and enforce a foreign key constraint between encontros and amizade.
As far as I can tell, this will result in doing what you want without having to write your own code to try and recreate behavior provided by the database. It also makes it much easier to understand from a database design point of view.
alter table dbo.encontros
add constraint fk_amizade__encontros
foreign key (idperflia, idperflib) references dbo.amizade (idperflia, idperflib)
/* optional
on delete { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
on update { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
--*/*
;
More about table constraints.
NO ACTION
The SQL Server Database Engine raises an error and the delete action on the row in the parent table is rolled back.
CASCADE
Corresponding rows are deleted from the referencing table if that row is deleted from the parent table.
SET NULL
All the values that make up the foreign key are set to NULL when the corresponding row in the parent table is deleted. For this constraint to execute, the foreign key columns must be nullable.
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
Based on your reply to #3N1GM4:
#3N1GM4 if exists some friendship with a date after today (for example) it is an error, so the friendship doesnt exist. But i dont know if it matters at this point. IDPERFILA and IDPERFILB will match A and B at amizade table, but i need to make sure that they were not the same
You could create a check constraint on amizade that will prevent rows with invalid dates from being inserted into the table.
alter table dbo.amizade
add constraint chk_data_lt_getdate ([data] < get_date());
More about check constraints; more examples from Gregory Larson.
original answer:
I'm still waiting on some clarification on the question, but one of the versions in this should be on the right path:
create trigger relaƧoes_after_insert
on encontros
instead of insert
as
begin
/* To abort when any row doesn't have a matching friendship */
if not exists (
select 1
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
begin;
raiserror('there isnt friendship', 16, 10);
else
insert into encontros
select * from inserted;
end;
end;
/* To insert all rows that have a matching friendship, you could use this instead */
insert into encontros
select i.*
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
end;
The only potential issue I see with using an inner join instead of exists for the second option (inserting rows that have a friendship and ignoring ones that don't) is if there could ever be an issue where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb) or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila) would return duplicates of the inserted rows from each condition returning a match.

PLS-00103: Encountered the symbol "end-of-file" Sql Developer

I want to execute following sql code.
--1
CREATE TABLE User_Type(
User_Type_ID Integer NOT NULL PRIMARY KEY,
User_Type_Name Char(20 ) NOT NULL
);
CREATE SEQUENCE User_Type_seq;
CREATE TRIGGER User_Type_ID_trg
BEFORE INSERT ON User_Type
FOR EACH ROW
BEGIN
SELECT User_Type_seq.NEXTVAL INTO :new.User_Type_ID
FROM DUAL;
END;
/
When I write it by hand to sql developer, it works. But I need a lot of block like this. So, I've edited those with notepad++ and copy/paste to sql developer again. This time; tables and sequences were created but it gives an error about trigger code (I've taken this routine with select from user_errors)
PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following: ERROR 103
;
Here is the full code which doesn't work. create table and sequences codes works, but the trigger code doesn't.
--1
CREATE TABLE User_Type(
User_Type_ID Integer NOT NULL PRIMARY KEY,
User_Type_Name Char(20 ) NOT NULL
);
CREATE SEQUENCE User_Type_seq;
CREATE TRIGGER User_Type_ID_trg
BEFORE INSERT ON User_Type
FOR EACH ROW
BEGIN
SELECT User_Type_seq.NEXTVAL INTO :new.User_Type_ID
FROM DUAL;
END;
/
--2
CREATE TABLE Users(
User_ID Integer NOT NULL PRIMARY KEY,
User_Password Char(8 ) NOT NULL,
User_Name Char(20 ) NOT NULL,
User_Type_ID Integer NOT NULL
);
CREATE SEQUENCE Users_seq;
CREATE TRIGGER User_ID_trg
BEFORE INSERT ON Users
FOR EACH ROW
BEGIN
SELECT Users_seq.NEXTVAL INTO :new.User_ID
FROM DUAL;
END;
/
--3
CREATE TABLE Admin(
Admin_ID Integer NOT NULL PRIMARY KEY,
E_mail Char(20 ) NOT NULL,
Phone_Number Char(20 ) NOT NULL,
User_ID Integer NOT NULL
);
CREATE SEQUENCE Admin_seq;
CREATE TRIGGER Admin_ID_trg
BEFORE INSERT ON Admin
FOR EACH ROW
BEGIN
SELECT Admin_seq.NEXTVAL INTO :new.Admin_ID
FROM DUAL;
END;
/
--4
CREATE TABLE City(
City_ID Integer NOT NULL PRIMARY KEY,
City_Name Char(20 ) NOT NULL
);
CREATE SEQUENCE City_seq;
CREATE TRIGGER City_ID_trg
BEFORE INSERT ON City
FOR EACH ROW
BEGIN
SELECT City_seq.NEXTVAL INTO :new.City_ID
FROM DUAL;
/
I've searched similar questions but no response to solve my problem.
Sorry for English, thank you...
You're just missing an END; in your final trigger:
CREATE TRIGGER City_ID_trg
BEFORE INSERT ON City
FOR EACH ROW
BEGIN
SELECT City_seq.NEXTVAL INTO :new.City_ID
FROM DUAL;
END;
/
Everything else is already created and compiled successfully.
Incidentally, from 11g you can assign a sequence directly in PL/SQL, withouth having to select from dual:
...
BEGIN
:new.City_ID := City_seq.NEXTVAL;
END;
/
It was solved. Unfortunately, a letter mistake I have not understood yet why, but:
it must be
create trigger
instead of
CREATE TRIGGER

Determine Old primary key in a SQL Trigger

I've done this before somewhere I'm sure of it!
I have a SQL Server 2000 table that I need to log changes to fields on updates and inserts into a second Logging table. A simplified version of the structure I'm using is below:
MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)
LogTable
OLDID varchar(10)
NEWID varchar(10)
For any other field something like this would work great:
Select i.DESCRIPTION As New, d.DESCRIPTION As Old
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID
...But obviously the join would fail if ID was changed.
I cannot modify the Tables in way, the only power I have in this database is to create a trigger.
Alternatively is there someone who can teach me time travelling and I'll go back into the past and ask myself back then how I did this? Cheers :)
Edit:
I think I need to clarify a few things here. This is not actually my database, it is a pre-existing system that I have almost no control of, other than writing this trigger.
My question is how can I retrieve the old primary key if said primary key was changed. I don't need to be told that I shouldn't change the primary key or about chasing up foreign keys etc. That's not my problem :)
DECLARE #OldKey int, #NewKey int;
SELECT #Oldkey = [ID] FROM DELETED;
SELECT #NewKey = [ID] FROM INSERTED;
This only works if you have a single row. Otherwise you have no "anchor" to link old and new rows. So check in your trigger for > 1 in INSERTED.
Is it possible to assume that the INSERTED and DELETED tables presented to you in a trigger are guaranteed to be in the same order?
I don't think it's possible. Imagine if you have 4 rows in the table:
1 Val1
2 Val2
3 Val3
4 Val4
Now issue the following update:
UPDATE MainTable SET
ID = CASE ID WHEN 1 THEN 2 WHEN 2 THEN 1 ELSE ID END
Description = CASE ID WHEN 3 THEN 'Val4' WHEN 4 THEN 'Val3' ELSE Description END
Now, how are you going to distinguish between what happened to rows 1 & 2 and what happened to rows 3 & 4. And more importantly, can you describe what's different between them? All of the stuff that tells you which columns have been updated won't help you.
If it's possible in this case that there's an additional key on the table (e.g. Description is UNIQUE), and your update rules allow it, you could write the trigger to prevent simultaneous updates to both keys, and then you can use whichever key hasn't been updated to correlate the two tables.
If you must handle multiple-row inserts/updates, and there's no alternate key that's guaranteed not to change, the only way I can see to do this is to use an INSTEAD OF trigger. For example, in the trigger you could break the original insert/update command into one command per row, grabbing each old id before you insert/update.
Within triggers in SQL Server you have access to two tables: deleted and inserted. Both of these have already been mentioned. Here's how they function depending on what action the trigger is firing on:
INSERT OPERATION
deleted - not used
inserted - contains the new rows being added to the table
DELETE OPERATION
deleted - contains the rows being removed from the table
inserted - not used
UPDATE OPERATION
deleted - contains the rows as they would exist before the UPDATE operation
inserted - contains the rows as they would exist after the UPDATE operation
These function in every way like tables. Therefore, it is entirely possible to use a row based operation such as something like the following (Operation exists only on the audit table, as does DateChanged):
INSERT INTO MyAuditTable
(ID, FirstColumn, SecondColumn, ThirdColumn, Operation, DateChanged)
VALUES
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-Before', GETDATE()
FROM deleted
UNION ALL
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-After', GETDATE()
FROM inserted
----new----
add an identity column to the table that the application can not change, you can then use that new column to join the inserted to the deleted tables within the trigger:
ALTER TABLE YourTableName ADD
PrivateID int NOT NULL IDENTITY (1, 1)
GO
----old----
Don't ever update/change key values. How can you do this and fix all of your foreign keys?
I wouldn't recommend ever using a trigger that can't handle a set of rows.
If you must change the key, insert a new row with the proper new key and values, use SCOPE_IDENTITY() if that is what your are doing. Delete the old row. Log for the old row that it was changed to the new row's key, which you should now have. I hope there is no foreign key on the changed key in your log...
You can create a new identity column on table MainTable (named for example correlationid) and correlate inserted and deleted tables using this column.
This new column should be transparent for existing code.
INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted
INNER JOIN deleted
ON inserted.correlationid = deleted.correlationid
Pay attention, you could insert duplicate records in the log table.
Of course nobody should be changing the primary key on the table -- but that is exactly what triggers are supposed to be for (in part), is to keep people from doing things they shouldn't do. It's a trivial task in Oracle or MySQL to write a trigger that intercepts changes to primary keys and stops them, but not at all easy in SQL Server.
What you of course would love to be able to do would be to simply do something like this:
if exists
(
select *
from inserted changed
join deleted old
where changed.rowID = old.rowID
and changed.id != old.id
)
... [roll it all back]
Which is why people go out googling for the SQL Server equivalent of ROWID. Well, SQL Server doesn't have it; so you have to come up with another approach.
A fast, but sadly not bombproof, version is to write an instead of update trigger that looks to see whether any of the inserted rows have a primary key not found in the updated table or vice versa. This would catch MOST, but not all, of the errors:
if exists
(
select *
from inserted lost
left join updated match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
-- roll it all back
But this still doesn't catch an update like...
update myTable
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
Now, I've tried making the assumption that the inserted and deleted tables are ordered in such a way that cursoring through the inserted and deleted tables simultaneously will give you properly matching rows. And this APPEARS to work. In effect you turn the trigger into the equivalent of the for-each-row triggers available in Oracle and mandatory in MySQL...but I would imagine the performance will be bad on massive updates since this is not native behavior to SQL Server. Also it depends upon an assumption that I can't actually find documented anywhere and so am reluctant to depend on. But code structured that way APPEARS to work properly on my SQL Server 2008 R2 installation. The script at the end of this post highlights both the behavior of the fast-but-not-bombproof solution and the behavior of the second, pseudo-Oracle solution.
If anybody could point me to someplace where my assumption is documented and guaranteed by Microsoft I'd be a very grateful guy...
begin try
drop table kpTest;
end try
begin catch
end catch
go
create table kpTest( id int primary key, name nvarchar(10) )
go
begin try
drop trigger kpTest_ioU;
end try
begin catch
end catch
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
if exists
(
select *
from inserted lost
left join deleted match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
raisError( 'Changed primary key', 16, 1 )
else
update kpTest
set name = i.name
from kpTest
join inserted i
on i.id = kpTest.id
;
end
go
insert into kpTest( id, name ) values( 0, 'zero' );
insert into kpTest( id, name ) values( 1, 'one' );
insert into kpTest( id, name ) values( 2, 'two' );
insert into kpTest( id, name ) values( 3, 'three' );
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This throws an error, appropriately
update kpTest set id = 5, name = 'FIVE' where id = 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This allows the change, inappropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = UPPER( name )
go
select * from kpTest
/*
0 ZERO
1 TWO -- WRONG WRONG WRONG
2 ONE -- WRONG WRONG WRONG
3 THREE
*/
-- Put it back
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
drop trigger kpTest_ioU
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
declare newIDs cursor for select id, name from inserted;
declare oldIDs cursor for select id from deleted;
declare #thisOldID int;
declare #thisNewID int;
declare #thisNewName nvarchar(10);
declare #errorFound int;
set #errorFound = 0;
open newIDs;
open oldIDs;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
while ##FETCH_STATUS = 0 and #errorFound = 0
begin
if #thisNewID != #thisOldID
begin
set #errorFound = 1;
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
raisError( 'Primary key changed', 16, 1 );
end
else
begin
update kpTest
set name = #thisNewName
where id = #thisNewID
;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
end
end;
if #errorFound = 0
begin
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
end
end
go
-- Succeeds, appropriately
update kpTest
set name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
-- Succeeds, appropriately
update kpTest
set name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = id + 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Succeeds, appropriately
update kpTest
set id = id, name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
drop table kpTest
go

Resources