I have a database running on SQL Server 2008.
I have a trigger in which I raise error with severity 16. When I test the trigger the error appears but the operation is not rolled back, i.e. I have additional row in the table. I can not understand why is that, because severity 16 results in rollback. We also used that convention in other triggers and it terminates trigger and causes rollback.
The table has also another trigger which does not allow deletion of rows.
Here is the trigger:
ALTER TRIGGER dbo.trg
ON dbo.tbl
AFTER INSERT, UPDATE
AS
BEGIN
IF (##ROWCOUNT > 0)
IF ((SELECT COUNT(SDS.ID) AS Count0 FROM dbo.tbl SDS WHERE SDS.IsIdleTimeReferred = 0) <> 1) OR
((SELECT MAX(SDS.CreatedDate) FROM dbo.tbl SDS WHERE SDS.IsIdleTimeReferred = 0) <
(SELECT MAX(SDS.CreatedDate) FROM dbo.tbl SDS WHERE SDS.IsIdleTimeReferred = 1))
BEGIN
--IF ##TRANCOUNT > 0 ROLLBACK
RAISERROR('Only one record with value IsIdleTimeReferred=0 must exist and it must be the last one', 16, 1);
END;
END;
When I uncomment ##TRANCOUNT the operation behaves correctly.
The table tbl consists of 3 columns:
[ID], [IsIdleTimeReferred], [CreatedDate]
I can not figure out where is the problem. For me code is designed correctly.
Any ideas?
EDIT:
Ok, I agree that Severity 16 does not rollback transaction in a trigger. So I implemented the following code (the trigger is AFTER INSERT, UPDATE):
BEGIN
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION;
RAISERROR('Does not allow modifications in the past', 16, 1);
RETURN;
END;
This code does not rollbacks transaction in a trigger. If I switch rows and first RAISERROR and then ROLLBACK the changes are not rollbacked.
Why this happening?
To be sure that transaction is roll back always try to use explicit 'ROLLBACK' (see comments of Damien_The_Unbeliever, JodyT and Andrew). All credits to them.
Kevin-Suchlicki is also right but in my case I don't want to use SET XACT_ABORT ON.
Instead of using RAISERROR, which does not respect XACT_ABORT, use THROW instead.
This is valid from SQL Server 2012 onwards, which by now everyone should have upgraded.
Because XACT_ABORT is on by default in triggers, the transaction is automatically rolled back. There is no need, nor should you, explicitly rollback.
CREATE OR ALTER TRIGGER dbo.trg
ON dbo.tbl
AFTER INSERT, UPDATE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted)
RETURN;
IF EXISTS (SELECT 1
FROM dbo.tbl notIdle
WHERE notIdle.IsIdleTimeReferred = 0
HAVING COUNT(*) <= 1
AND MAX(idle.CreatedDate) > (
SELECT MAX(idle.CreatedDate)
FROM dbo.tbl idle
WHERE idle.IsIdleTimeReferred = 1
)
)
BEGIN
THROW 50001, 'Only one record with value IsIdleTimeReferred=0 must exist and it must be the last one', 1;
END;
Note also the simplification of the checks, and the use of SET NOCOUNT.
However, in this particular case, you can enforce part of this with a constraint. A conditional unique constraint can be enforced with a filtered index
CREATE UNIQUE NONCLUSTERED INDEX IX_OneIdle ON dbo.tbl (IsIdleTimeReferred) WHERE (IsIdleTimeReferred = 0);
This doesn't help you for your other constraint though.
Related
I am using MS SQL Server 2016 where I have implemented a instead of delete trigger. It looks like this:
ALTER TRIGGER MyTrigger ON MyTable INSTEAD OF DELETE AS
BEGIN
IF --some condition
BEGIN
RAISERROR ('Error msg', 16, 1)
ROLLBACK TRAN
RETURN
END
DELETE MyTable FROM MyTable JOIN deleted ON MyTable.id = deleted.id
END
If I execute a DELETE statement on the table 'MyTable' and the condition in the if is not fulfilled the DELETE statement is executed after the if-block. This is absolutely correct. But in the console of SSMS it is written twice that the DELETE statement was executed. So the following is written in the console:
(1 rows affected)
(1 rows affected)
I do not understand why. Why does SSMS indicate twice that a row is affected? I use SSMS version 15.0.18338.0.
This is because there were 2 sets of data effect, the set outside the TRIGGER, and then again inside it, because the initial dataset doesn't perform the DML operation itself. If you don't want to see the latter count, turn NOCOUNT to ON. This, of course, means that if fewer rows are effected in your TRIGGER, you won't know about it in the output from SSMS (but it's just informational anyway).
It is also heavily advised that you don't use ROLLBACK inside a TRIGGER, handle transactions outside the TRIGGER, not inside. RAISERROR isn't recommend either and you should be using THROW for new development work (that's been recommended since 2012!). This results in a TRIGGER like below:
CREATE OR ALTER TRIGGER MyTrigger ON dbo.MyTable INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM deleted WHERE SomeVal = 'Nonsense')
THROW 95302, N'Error Msg', 16; --Use an error number appropriate for you
ELSE
DELETE MT FROM dbo.MyTable MT JOIN deleted ON MT.id = deleted.id;
END;
GO
I am running the following query:
SELECT * INTO dbo.2015_10_2_cs FROM dbo.2015_10_2
IF NOT EXISTS
(SELECT type FROM sys.indexes WHERE object_id = object_id('dbo.2015_10_2_cs')
AND NAME ='cci' AND type = 5)
BEGIN
CREATE CLUSTERED COLUMNSTORE INDEX cci
ON dbo.2015_10_2_cs
DROP TABLE dbo.2015_10_2
EXEC sp_rename "dbo.2015_10_2_cs" , "dbo.2015_10_2"
END
and I want to make sure that the part where I am renaming the table dbo.2015_10_2_cs to dbo.2015_10_2 is done successfully (without losing any data).
The step inside the loop should be surrounded with SQL transaction to keep the process safe and reliable (in case if any step will fail).
Could anyone help with this? Thanks in advance.
EXEC sp_rename "dbo.2015_10_2_cs" , "dbo.2015_10_2"
This will not do what you expect. The new table will be named [dbo].[dbo.2015_10_2] if you specify the schema name in the new table name. Renamed tables are implicitly in the existing table's schema since one must use ALTER SCHEMA instead of sp_rename to move an object between schemas.
There are a number of other problems with your script. Because the table name starts with a number, it doesn't conform to regular identifier naming rules and must be enclosed in square brackets or double quotes. The literal parameters passed to sp_rename should be single quotes. You can also check to stored procedure return code to ascertain success or failure. The example below performs these tasks in a transaction with structured error handling.
DECLARE #rc int;
BEGIN TRY
BEGIN TRAN;
IF NOT EXISTS
(SELECT type FROM sys.indexes WHERE object_id = object_id(N'dbo.2015_10_2_cs')
AND NAME ='cci' AND type = 5)
BEGIN
CREATE CLUSTERED COLUMNSTORE INDEX cci
ON dbo.[2015_10_2_cs];
DROP TABLE dbo.[2015_10_2];
EXEC #rc = sp_rename 'dbo.[2015_10_2_cs]' , '2015_10_2';
IF #rc <> 0
BEGIN
RAISERROR('sp_rename returned return code %d',16,1);
END;
END;
COMMIT;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
You can use an EXISTS checking for the tablename and schema.
IF NOT EXISTS (SELECT 'table does not exist' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'2015_10_2'AND TABLE_SCHEMA = 'dbo')
BEGIN
RAISERROR('The table doesn''t exist!!!!', 16, 1)
END
sp_rename won't make you lose table contents, it will just change the table reference name and update all it's contraints and indexes references. It will also raise an error if the table to rename does not exist. Maybe what you want is to wrap your process in a transaction and rollback if something fails.
EDIT:
For basic transaction handling you can use the following. Please read the documentation for using transaction, it might take a while to know how it works correctly.
IF OBJECT_ID('tempdb..#Test') IS NOT NULL
DROP TABLE #Test
CREATE TABLE #Test (Number INT)
SELECT AmountRecords = COUNT(1) FROM #Test -- AmountRecords = 0
BEGIN TRY
BEGIN TRANSACTION
-- Do your statements here
INSERT INTO #Test (Number)
VALUES (1)
DECLARE #errorVariable INT = CONVERT(INT, 'NotAnInteger!!') -- Example of error: can't convert
COMMIT
END TRY
BEGIN CATCH -- If something goes wrong
IF ##TRANCOUNT > 0 -- ... and transaction is still open
ROLLBACK -- Revert statements from the BEGIN TRANSACTION onwards
END CATCH
SELECT AmountRecords = COUNT(1) FROM #Test -- AmountRecords = 0 (the transaction was rolled back and the INSERT reverted)
Basically you use BEGIN TRANSACTION to initiate a restore point to go back to if something fails. Then use a COMMIT once you know everything is OK (from that point onwards, other users will see the changes and modifications will be persisted). If something fails (you need TRY/CATCH block to handle errors) you can issue a ROLLBACK to revert your changes.
Got an error when ran update statement.
For one record it worked fine, but for a chunk of records it gives me an error.
Also, why it tells me that 64801 row(s) affected and then 1 row(s) affected and then 0? How should I interpret that?
This is the script:
update tblQuotes
set QuoteStatusID = 11, --Not Taken Up
QuoteStatusReasonID = 9 --"Not Competitive"
where CAST(EffectiveDate as DATE) < CAST('2013-11-27' as DATE)
and CompanyLocationGuid = '32828BB4-E1FA-489F-9764-75D8AF7A78F1' -- Plaza Insurance Company
and LineGUID = '623AA353-9DFE-4463-97D7-0FD398400B6D' --Commercial Auto
I added BEGIN TRANSACTION statement, but it still won't work.
BEGIN TRANSACTION
update tblQuotes
set QuoteStatusID = 11, --Not Taken Up
QuoteStatusReasonID = 9 --"Not Competitive"
where CAST(EffectiveDate as DATE) < CAST('2017-11-27' as DATE)
AND CompanyLocationGuid = '32828BB4-E1FA-489F-9764-75D8AF7A78F1' -- Plaza Insurance Company
and LineGUID = '623AA353-9DFE-4463-97D7-0FD398400B6D' --Commercial Auto
IF ##TRANCOUNT>0
COMMIT TRANSACTION
In my opinion this is a "flaw", if not a "bug" in SQL Server. When you COMMIT a transaction, TRANCOUNT is decremented by 1. When you ROLLBACK any transaction, all transactions in the calling stack are rolled back! This means that any calling procedure that tries to commit or rollback will have this error and you've lost the integrity of your calling stack.
I worked through this when building a mechanism do do unit testing on SQL Server. I get around it by always using named transactions as shown in the example below. You can obviously also check XACT_STATE. The point is simply that, rather than blindly committing and rolling back anonymous transactions, if you manage transactions by name or transaction id you have better control.
For unit testing, I write a stored procedure as a test that calls the procedure under test. The unit test is in either serializable or snapshot mode and ONLY includes a rollback statement. I call the procedure under test, validate the results, build test output (pass/fail, parameters, etc.) as XML output, then everything gets rolled back. This gets around the need to build "mock data". I can use the data on any environment as the transaction is always rolled back.
--
-- get #procedure from object_name(##procid)
-------------------------------------------------
DECLARE #procedure SYSNAME = N'a_procedure_name_is_a_synonym_so_can_be_longer_than_a_transaction_name'
, #transaction_id BIGINT;
DECLARE #transaction_name NVARCHAR(32) = RIGHT(#procedure + N'_tx', 32);
--
BEGIN TRANSACTION #transaction_name;
BEGIN
SELECT #transaction_id = [transaction_id]
FROM [sys].[dm_tran_active_transactions]
WHERE [name] = #transaction_name;
SELECT *
FROM [sys].[dm_tran_active_transactions]
WHERE [name] = #transaction_name;
-- Perform work here
END;
IF EXISTS
(SELECT *
FROM [sys].[dm_tran_active_transactions]
WHERE [name] = #transaction_name)
ROLLBACK TRANSACTION #transaction_name;
This error states that in SQL Server, you have given a Commit or Commit Transaction without specifying a Begin Transaction or the number of commit transactions is greater than the number of begin transactions. To avoid this make sure you check the existing transactions on the current session before committing.
So a normal Commit Transaction will be updated as below
IF ##TRANCOUNT>0
COMMIT TRANSACTION
There is a trigger, that making sure about the integrity of the QuoteStatusID. So im my WHERE clause I have to exactly specify what current status ID policy have to have in order to be updated.
I have a script where I'm adding a column to the table, and immediately after I populate that column with data from another table. I'm getting 'Invalid column name' error on the column that I am adding.
The error, specifically, is Invalid column name 'tagID'.
The code between BEGIN TRANSACTION and COMMIT is actually an excerpt of a much larger script, but this is the relevant excerpt (and I need all of it to succeed or simply roll back):
BEGIN TRY
BEGIN TRANSACTION
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
MERGE INTO
Items AS target
USING
Tags AS t ON t.tag = target.tag
WHEN MATCHED THEN
UPDATE SET target.tagID = t.id;
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
SQL Server tries to compile all statements in the batch. If the table doesn't exist compilation of the statement is deferred but there is no deferred compilation for missing columns.
You can use
BEGIN TRY
BEGIN TRANSACTION
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
EXEC('
MERGE INTO
Items AS target
USING
Tags AS t ON t.tag = target.tag
WHEN MATCHED THEN
UPDATE SET target.tagID = t.id;
')
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
To push the usage of the column into a child batch compiled after the column is created. It still belongs to the same transaction opened in the parent scope.
Transaction scope are for DML operations and not for DDL operations. So not sure why you are having the ALTER statement in the same transaction block. If not wrong, you should be having that ALTER statement outside the transaction block.
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
BEGIN TRANSACTION
MERGE INTO
.....
Also, I would remove those [] from the datatype of the column from your ALTER statement
ALTER TABLE [Items] ADD tagID UniqueIdentifier;
I put transactions in all my "set" procedures. No problems. Everything works.
In this case, I need one set procedure, to call another, thankfully, only once, or that would potentially complicate things further.
So the happy bath would be.
I'm in ProcA and start a transaction.
It calls ProcB and it starts a transaction.
ProcB is successful and commits.
ProcA is successful and commits.
However, what happens if ProcB fails, rollsback, and rethrows the error. It should cause ProcA to rollback as well correct?
What if ProcB succeeds, commits, then ProcA subsequently fails, and rollsback...will what happened in ProcB be rolled back? or is it commited?
I need these two to work together, either both succeed, or fail and both be rolled back. What's the best way to ensure this happens?
I'm working with Microsoft SQL Server 2008 R2 (SP1)
Note: If ProcB requires a transaction because it can be called without ProcA wrapping it. And technically, ProcA won't always call ProcB (depends on input).
Here's a simple demo to show what happens with nested transations:
CREATE TABLE TranTest (Field1 INTEGER)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 1 open transaction
INSERT TranTest VALUES (1)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2 open transactions
INSERT TranTest VALUES (2)
ROLLBACK TRANSACTION -- this rolls back ALL transaction
SELECT ##TRANCOUNT -- 0 open transactions (you may have expected 1?)
SELECT * FROM TranTest -- No rows
Instead f the ROLLBACK above, if you did a COMMIT TRANSACTION, this actual does nothing other then decrement ##TRANCOUNT. So you then would need to to either COMMIT the outer transaction (which would COMMIT both rows to the table), or do a ROLLBACK which would result in no rows being committed to the table.
Here's the MSDN ref on nested transactions: http://msdn.microsoft.com/en-us/library/ms189336.aspx
Just use XACT_ABORT ON, and you are all set. Run the following script and see for yourself:
CREATE DATABASE ak_test;
GO
USE ak_test;
GO
CREATE TABLE dbo.a(i INT CONSTRAINT a_CannotInsertNegative CHECK(i>=0));
GO
CREATE TABLE dbo.b(i INT CONSTRAINT b_CannotInsertNegative CHECK(i>=0));
GO
CREATE PROCEDURE dbo.innerProc #i INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT b(i)VALUES(#i);
COMMIT;
GO
CREATE PROCEDURE dbo.outerProc #i1 INT, #i2 INT, #i3 INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT a(i)VALUES(#i1);
EXEC innerProc #i=#i2;
INSERT a(i)VALUES(#i3);
COMMIT;
GO
-- succeeds
EXEC dbo.outerProc 1, 2, 3;
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- inner proc fails
EXEC dbo.outerProc 2, -3, 4;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- second insert in outer proc fails
EXEC dbo.outerProc 3, 4, -5;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
I'm paranoid about transactions (there was this transaction left open on Production once that no one noticed for half an hour...) so I'd warp the potentially inner transaction like so:
CREATE PROCEDURE etcetc
...
DECLARE #IsTransaction bit = 0
IF ##trancount > 0
BEGIN
BEGIN TRANSACTION
SET #IsTransaction = 1
END
...
IF #IsTransaction = 1
BEGIN
COMMIT
-- or ROLLBACk, as necessary
END
All transaction processing (and handling of errors that occur within the transaction) must then be dealt with at whatever level launched the transaction.
(And did anyone else notice how BOL doesn't actually say what happens when you issue a ROLLBACK to a named transaction that isn't the outermost transaction? They do spell out every other permutation...)