T-SQL transaction encounters errors but is not canceled - sql

By definition T-SQL transaction should be cancelled if any statement inside it results with error. However, I stumbled upon the situation where transaction passes with an error happening in the middle of it.
Example environment setup:
--test tables:
IF OBJECT_ID('t2', 'U') IS NOT NULL
DROP TABLE t2;
GO
IF OBJECT_ID('t1', 'U') IS NOT NULL
DROP TABLE t1;
GO
CREATE TABLE t1 (a INT NOT NULL PRIMARY KEY);
CREATE TABLE t2 (a INT NOT NULL REFERENCES t1(a));
GO
INSERT INTO t1
VALUES (1), (3), (4), (6);
GO
Transaction test:
BEGIN TRANSACTION
INSERT INTO t2 VALUES (1);
INSERT INTO t2 VALUES (2); -- Foreign key error will be thrown.
INSERT INTO t2 VALUES (3);
COMMIT TRANSACTION;
This should have been cancelled because of the foreign key error, but it isn't:
SELECT * FROM t2;
results with two rows returned - values 1 and 3.
Adding SET XACT_ABORT ON does the job, but how is it possible that entire transaction holds up when there's an error in the middle of it?

Depending on the specific error, the T-SQL batch will continue after an error without XACT_ABORT ON. I recommend TRY/CATCH with IF ##TRANCOUNT > 0 ROLLBACK; in the CATCH BLOCK plus XACT_ABORT ON to make sure the rollback occurs immediately in the case of a query cancel or client timeout (which prevents the CATCH block from being executed):
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO t2 VALUES (1);
INSERT INTO t2 VALUES (2); -- Foreign key error will be thrown.
INSERT INTO t2 VALUES (3);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
END CATCH;

Related

SQL Server does not rollback trigger transaction to savepoint

I am facing an issue trying to set up a transaction within a trigger of my view. Here is my DDL setup:
CREATE TABLE entity1 (
id INT NOT NULL IDENTITY PRIMARY KEY,
attr1 INT NOT NULL,
attr2 INT NOT NULL
);
GO
CREATE TABLE entity2 (
entity1_id INT NOT NULL FOREIGN KEY REFERENCES entity1(id),
attr3 INT NOT NULL,
attr4 INT NOT NULL
);
GO
CREATE VIEW my_view AS
SELECT attr1, attr2, attr3, attr4
FROM entity1 AS e1
INNER JOIN entity2 AS e2
ON e1.id = e2.entity1_id;
GO
CREATE TRIGGER tg_my_view_ins ON my_view
INSTEAD OF INSERT AS
BEGIN
BEGIN TRY
SAVE TRANSACTION here; -- checkpoint
INSERT INTO entity1 (attr1, attr2)
SELECT attr1, attr2 FROM inserted;
INSERT INTO entity2 (entity1_id, attr3, attr4)
SELECT SCOPE_IDENTITY(), attr3, attr4 FROM inserted;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION here; -- rollback to checkpoint in case on an error
END CATCH
END
GO
As you can see, I make a savepoint while in the trigger, and rollback in case of any errors (I assume that constraint errors are also handled by TRY/CATCH blocks).
The problem is, when I execute bad inserts within the transaction, trigger error handling block does not rollback:
BEGIN TRY
BEGIN TRANSACTION;
-- successful insert
INSERT INTO my_view (attr1, attr2, attr3, attr4) VALUES (1,2,3,4);
SELECT * FROM entity1; -- one entity
-- i wrap the bad insert into try/catch so the error is discarded,
-- but still rolled back
BEGIN TRY
INSERT INTO my_view (attr1, attr2, attr3) VALUES (3,2,1);
END TRY
BEGIN CATCH
END CATCH;
SELECT * FROM entity1; -- should only have one entity, but has two
ROLLBACK; -- discard the whole transaction
END TRY
BEGIN CATCH
ROLLBACK; -- discard the whole transaction in case of any errors
END CATCH;
I do not seem to be able to set up the trigger the way it would not create orphan records in case of an error. I have tried using BEGIN TRANSACTION here and COMMIT TRANSACTION here within my trigger instead of SAVE TRANSACTION here as well, with no luck. What is the correct way to handle constraint erros within triggers?
The execution setup I would like to keep the way it is, if possible. I create and rollback the transaction for testing purposes. I wrap the bad insert into a try/catch block to discard the error I know should happen.
This seemingly confused behaviour can be made clear by adding error logging into your catch blocks. The following modification of your test code adds error logging (and some other improvements), which shows what actually happens in the process:
begin try
begin transaction;
INSERT INTO dbo.my_view (attr1, attr2, attr3, attr4) VALUES (1,2,3,4);
SELECT * FROM dbo.entity1;
BEGIN TRY
INSERT INTO dbo.my_view (attr1, attr2, attr3) VALUES (3,2,1);
END TRY
BEGIN CATCH
-- Logging - inner CATCH
select 'Inner', ##trancount, error_number(), error_message(), error_procedure(), error_line();
END CATCH;
select * from dbo.entity1;
rollback;
end try
begin catch
-- Logging - outer CATCH
select 'Outer', ##trancount, error_number(), error_message(), error_procedure(), error_line();
-- Conditional rollback, because some errors always terminate the transaction
if ##trancount > 0
rollback;
end catch;
If you run this code with your trigger intact, you will see an error caught by the inner CATCH:
3931
The current transaction cannot be committed and cannot be rolled
back to a savepoint. Roll back the entire transaction.
Searching by the error number leads to this post with a similar question. In his answer, Rutzky shows that the culprit of this behaviour is the XACT_ABORT session option which is apparently set to ON for triggers by default. If your intent is to pursue your trigger-based architecture, then turning this option off inside your trigger will help:
create or alter trigger dbo.tg_my_view_ins
on dbo.my_view
instead of insert as
-- Implicitly set to ON in triggers by default; makes error handling impossible
set xact_abort off;
begin try
save transaction here;
INSERT INTO dbo.entity1 (attr1, attr2)
SELECT attr1, attr2 FROM inserted;
INSERT INTO dbo.entity2 (entity1_id, attr3, attr4)
SELECT e.id, attr3, attr4
FROM inserted i
-- The actual JOIN condidions should reference a natural key in the master table.
-- This is just an example.
inner join dbo.entity1 e on e.attr1 = i.attr1 and e.attr2 = i.attr2;
end try
begin catch
if ##trancount > 0
rollback transaction here;
end catch;
return;
GO
(Again, I have corrected several other issues with your code.)

Stored procedure for referential integrity between two tables in different database?

For two tables A and B, I'd like to implement referential integrity such that in tables A a foreign key's value must present in table B, while in table B a primary key can only be deleted or modified if that value does not present in table A. My requirement is that I'd like to have table A, and B as variable, and apply the procedure to any arbitrary instances of tables. That is,
sp_referential_integrity_across_databases(A, B)
I have figured out how to do the referential integrity as triggers for a pair of particular tables. I wonder if it's feasible to write such stored procedure to save future effort?
My environment is Microsoft SQL Server 2017. The more portable the solution, the better.
Here are my crafted procedures:
The triggers on table "A" for insert and update:
USE DWPractice
IF OBJECT_ID ('dbo.trgCheckCustomer_Cat_Id_Customer_D', 'TR') IS NOT NULL
DROP Trigger trgCheckCustomer_Cat_Id_Customer_D;
GO
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_D
ON Customer_D
AFTER INSERT, UPDATE
AS
IF NOT EXISTS
(
SELECT Customer_Cat_Id
FROM inserted
WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [OtherDW].[dbo].[Customer_Cat_D])
)
BEGIN
RAISERROR('Lookup Value Not Found -- Inerst Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
The trigger on table "B" for delete and update:
USE OtherDW
IF OBJECT_ID ('dbo.trgCheckCustomer_Cat_Id_Customer_Cat_D', 'TR') IS NOT NULL
DROP Trigger trgCheckCustomer_Cat_Id_Customer_Cat_D;
GO
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_Cat_D
ON Customer_Cat_D
AFTER DELETE, UPDATE
AS
Begin
IF EXISTS
(
SELECT Customer_Cat_Id
FROM deleted
WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [DWPractice].[dbo].[Customer_D])
)
BEGIN
RAISERROR('Lookup Value Found -- Delete Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
-- It seems that the following for the case of update is not needed
-- The above clauses would get executed even for the case of update.
-- IF EXISTS
-- (
-- SELECT Customer_Cat_Id
-- FROM inserted
-- WHERE Customer_Cat_Id IN (SELECT Customer_Cat_Id FROM [DWPractice].[dbo].[Customer_D])
-- )
-- BEGIN
-- RAISERROR('Lookup Value Found -- Update Failed', 16, 1);
-- ROLLBACK TRANSACTION;
-- END;
End;
If stored procedure is not the best practice, then what is the best practice? It seems to me, there's much boiler-plate code, with only database name and table name are variables.
The logic in your (first) trigger is not correct. If you have multiple rows in inserted, then only one has to match. Instead, you want:
CREATE TRIGGER trgCheckCustomer_Cat_Id_Customer_D ON Customer_D AFTER INSERT, UPDATE
AS BEGIN
IF EXISTS (SELECT 1
FROM inserted i LEFT JOIN
[OtherDW].[dbo].[Customer_Cat_D] d
ON i.Customer_Cat_Id = d.Customer_Cat_Id
WHERE d.Customer_Cat_Id IS NULL
)
BEGIN
RAISERROR('Lookup Value Not Found -- Insert Failed', 16, 1);
ROLLBACK TRANSACTION;
END;
END; -- trigger

Transaction roll back not working in Postgresql

I am working on PostgreSQL 9.1.4 .
I am inserting the data into 2 tables its working nicely.
I wish to apply transaction for my tables both table exist in
same DB. If my 2nd table going fail on any moment that time my 1 st
table should be rollback.
I tried the properties in "max_prepared_transactions" to a non zero
value in /etc/postgres/postgres.conf. But Still Transaction roll
back is not working.
in postgresql you cannot write commit or roll back explicitly within a function.
I think you could have use a begin end block
just write it simple
BEGIN;
insert into tst_table values ('ABC');
Begin
insert into 2nd_table values ('ABC');
EXCEPTION
when your_exception then
ROLL BACK;
END;
END;
Probably you didn't started transaction.
Please, try
BEGIN;
INSERT INTO first_table VALUES(10);
-- second insert should fail
INSERT INTO second_table VALUES(10/0);
ROLLBACK;
I think it would be helpfull
create proc DataInsertInTable
as
begin tran
insert into Table1 values('Table1Data','XYZ')
if(##ERROR <>0)
begin
rollback tran;
return 0
end
insert into Table2 values('Table2Data','ABC')
if(##ERROR <>0)
begin
rollback tran;
return 0
end
commit Tran
return 1

Will ROLLBACK TRAN roll everything back?

Will ROLLBACK TRAN rollback everything in my SP? (including the call to another SP)
Example:
BEGIN TRAN
INSERT INTO (table1)
VALUES (1,'abc')
EXEC InsertTable2
INSERT INTO (table3)
VALUES (1,'abc')
ROLLBACK TRAN
from msdn
Rolls back an explicit or implicit transaction to the beginning of the transaction, or to a savepoint inside the transaction. You can use ROLLBACK TRANSACTION to erase all data modifications made from the start of the transaction or to a savepoint. It also frees resources held by the transaction.
so YES
Without knowing the definition of InsertTable2 impossible to say for sure. Here's an example where it doesn't.
CREATE TABLE table1
(
Num int,
String char(3)
)
GO
CREATE PROC InsertTable2
AS
COMMIT;
BEGIN TRAN;
GO
BEGIN TRAN
INSERT INTO table1
VALUES (1,'abc')
EXEC InsertTable2
INSERT INTO table1
VALUES (1,'abc')
ROLLBACK TRAN
GO
SELECT *
FROM table1
GO
DROP TABLE table1
DROP PROC InsertTable2
But assuming a sane definition the answer is "yes"
Yes. It will ROLLBACK everything in the Store Procedure that you have mentioned above.
More on ROLLBACK TRANSACTION

T-SQL could not rollback

I have some code that has a purely sequential flow, without transaction.
I sandwich them with a begin transaction and commit transaction
begin transaction
......--My code here......
......
......--code to create Table1
......
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2])
REFERENCES [dbo].[Table2] ([field3], [field4])
GO
....
......--End of My code here......
rollback transaction
commit transaction
when i run the script until just above "rollback transaction" in management studio, if a simple error occurs such as division by zero, I run "rollback transaction", all changes are rolledback without problem.
But if the alter table statement fails because Table2 doesn't exist, it then triggers further errors.
Msg 1767, Level 16, State 0, Line 2
Foreign key 'FK_Constraint references invalid table 'dbo.Table2'.
Msg 1750, Level 16, State 0, Line 2
Could not create constraint. See previous errors.
Msg 1767, Level 16, State 0, Line 2
Foreign key 'FK_xxxxxx' references invalid table 'Table1'.
When I run "rollback transaction", I got this error message "The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION." which is silly, because I DO HAVE a begin transaction on top!
Please tell me what went wrong. Any help would be much appreciated. Using SQL-Server 2008.
EDIT:
I added
SELECT ##TRANCOUNT;
before and after "ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT"
....
SELECT ##TRANCOUNT;
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2]) REFERENCES [dbo].[Table2] ([field3], [field4])
GO
SELECT ##TRANCOUNT;
....
The results are 1 and 0 respectively. The alter table automatically rollbacks my transaction on error!? I can't understand this.
I think there's nothing you can do about Sql Server treatment with DDL error severity handling, some of it are handled automatically (forcibly rolling back transaction for example) by Sql Server itself.
What you can just do is make your script code cope around it and provide script users with descriptive error.
An example:
-- drop table thetransformersmorethanmeetstheeye
-- select * from thetransformersmorethanmeetstheeye
-- first batch begins here
begin tran
create table thetransformersmorethanmeetstheeye(i int); -- non-erring if not yet existing
-- even there's an error here, ##ERROR will be 0 on next batch
ALTER TABLE [dbo].[Table1] WITH CHECK ADD CONSTRAINT [FK_constraint] FOREIGN KEY([field1], [field2])
REFERENCES [dbo].[Table2] ([field3], [field4]);
go -- first batch ends here
-- second batch begins here
if ##TRANCOUNT > 0 begin
PRINT 'I have a control here if things needed be committed or rolled back';
-- ##ERROR is always zero here, even there's an error before the GO batch.
-- ##ERROR cannot span two batches, it's always gets reset to zero on next batch
PRINT ##ERROR;
-- But you can choose whether to COMMIT or ROLLBACK non-erring things here
-- COMMIT TRAN;
-- ROLLBACK TRAN;
end
else if ##TRANCOUNT = 0 begin
PRINT 'Sql Server automatically rollback the transaction. Nothing can do about it';
end
else begin
PRINT 'Anomaly occured, ##TRANCOUNT cannot be -1, report this to Microsoft!';
end
-- second batch implicitly ends here
The only way this happens is if there is no open transaction in that SPID.
That's it. And the only way there's no open transaction is that either:
You never started a new transaction after the old one committed or rolled back
You have another commit or rollback somewhere you didn't notice
Something killed your connection or forced a rollback from outside your spid (like a kill command from another session)
You don't provide much code. Is there any error trapping or any other conditional logic in your query that's not shown?
As far as I know, the ALTER TABLE command will create its own new transaction, and when it fails, will rollback that transaction. A single rollback within a proc will cause all the open transactions within that proc to be rolled back. So you're seeing the error because the failure of the ALTER TABLE statement is implicitly rolling back your transaction before you try to do it..
You can confirm this easily enough by checking the #TRANCOUNT within your code, and only calling rollback when it is not zero
The error from the ALTER TABLE statement is a compile error rather than a runtime error - and so the whole batch in which that statement occurs is never executed. I'm guessing that there's no GO between BEGIN TRANSACTION and ALTER TABLE - hence the BEGIN TRANSACTION never executed, and what SQL Server is telling you is perfectly true.
Try adding a GO immediately after the BEGIN TRANSACTION.
Given this:
create table z
(
i int identity(1,1) not null,
zzz int not null
);
When you try the following..
begin try
begin transaction
alter table z drop column aaa;
commit tran;
end try
begin catch
print 'hello';
SELECT
ERROR_NUMBER() as ErrorNumber,
ERROR_MESSAGE() as ErrorMessage;
IF (XACT_STATE()) = -1
BEGIN
PRINT
N'The transaction is in an uncommittable state. ' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
end catch
print 'reached';
..the error can be caught:
ErrorNumber ErrorMessage
4924 ALTER TABLE DROP COLUMN failed because column 'aaa' does not exist in table 'z'.
But try changing alter table z drop column aaa; to alter table z add zzz int;, Sql Server can catch the error..
Column names in each table must be unique. Column name 'zzz' in table
'z' is specified more than once.
..but won't yield back the control to you, CATCH block will not be triggered. Seems there's no hard and fast rules what errors are catch-able and which are not.
To illustrate the difference, here's the error catch-able by your code
Here's an error un-catch-able by your code, which is similar to your problem.
Notice that there's no grid there(via SELECT ERROR_NUMBER() as ErrorNumber, ERROR_MESSAGE() as ErrorMessage;). That means, Sql Server did not yield back the control to you after it detected an exception.
Maybe you can see other details here that might help: http://msdn.microsoft.com/en-us/library/ms179296.aspx
See this guideline for error handling ##ERROR and/or TRY - CATCH
By the way, on Postgresql all kind of DDL errors are catch-able by your code.
do $$
begin
-- alter table z drop column aaa;
alter table z add zzz int;
exception when others then
raise notice 'The transaction is in an uncommittable state. '
'Transaction was rolled back';
raise notice 'Yo this is good! --> % %', SQLERRM, SQLSTATE;
end;
$$ language 'plpgsql';
Here's the dev-rendered error message for alter table z drop column aaa; on Postgresql:
Here's the dev-rendered error message for alter table z add zzz int; on Postgresql; which by the way in Sql Server, when it has an error on this type of statement, it won't yield back the control to you, hence your CATCH sections are sometimes useful, sometimes useless.