Logging into table in SQL Server trigger - sql

I am coding SQL Server 2005 trigger. I want to make some logging during trigger execution, using INSERT statement into my log table. When there occurs error during execution, I want to raise error and cancel action that cause trigger execution, but not to lose log records. What is the best way to achieve this?
Now my trigger logs everything except situation when there is error - because of ROLLBACK. RAISERROR statement is needed in order to inform calling program about error.
Now, my error handling code looks like:
if (#err = 1)
begin
INSERT INTO dbo.log(date, entry) SELECT getdate(), 'ERROR: ' + out from #output
RAISERROR (#msg, 16, 1)
rollback transaction
return
end

Another possible option is to use a table variable to capture the info you want to store in your permanent log table. Table variables are not rolled back if a ROLLBACK TRANSACTION command is given. Sample code is below...
--- Declare table variable
DECLARE #ErrorTable TABLE
( [DATE] smalldatetime,
[ENTRY] varchar(64) )
DECLARE #nErrorVar int
--- Open Transaction
BEGIN TRANSACTION
--- Pretend to cause an error and catch the error code
SET #nErrorVar = 1 --- ##ERROR
IF (#nErrorVar = 1)
BEGIN
--- Insert error info table variable
INSERT INTO #ErrorTable
( [Date], [Entry] )
SELECT
getdate(), 'Error Message Goes Here'
RAISERROR('Error Message Goes Here', 16, 1)
ROLLBACK TRANSACTION
--- Change this to actually insert into your permanent log table
SELECT *
FROM #ErrorTable
END
IF ##TRANCOUNT 0
PRINT 'Open Transactions Exist'
ELSE
PRINT 'No Open Transactions'

The problem here is that logging is part of transaction that modifies your data. Nested transactions will not help here. What you need is to put you logging actions into a separate context (connection), i.e. make it independent from you current transaction.
Two options come to my mind:
use Service Broker for logging - put log data to queue, receive and save the data 'on the other side of the pipe' (i.e. in another process/connection/transaction)
use OPENQUERY - you will need to register you own server as a 'linked server' and execute queries 'remotely' (I know, this looks a little bit strange, but an option anyway ...)
HTH

Don't know if I'm thinking too simple, but why not just change the order of the error handler to insert AFTER the rollback??
if (#err = 1)
begin
RAISERROR (#msg, 16, 1)
rollback transaction
INSERT INTO dbo.log(date, entry) SELECT getdate(), 'ERROR: ' + out from #output
return
end

Checkout error handling in triggers.

Related

SQL Server linked database: Insert does not happen on local table after writing to certain remote tables

I have the following code:
CREATE PROCEDURE [dbo].[USER_ARC_EXPORT_$sp]
AS
DECLARE #errmsg VARCHAR(255),
#errno INT,
#next_batch_id NUMERIC(12,0),
#object_name NVARCHAR(255),
#operation_name NVARCHAR(100)
SELECT #errmsg = NULL,
#next_batch_id = 100 --to simplify my explanation, just setting this to 100. In the actual code it goes through a process generate a batch number
-- Insert process start into tracking table
INSERT INTO LocalDB.[dbo].[ARC_PROCESS_TRACKING] ([BATCH_ID],[TRAN_CREATE_TS], [TRAN_STATUS], [ETL_START_TS], [ETL_END_TS])
SELECT batch_id, TRAN_CREATE_TS, 'START', ETL_START_TS, ETL_END_TS
FROM ARC_PROCESS_TRACKING
WHERE BATCH_ID = #next_batch_id
SELECT #errno = ##error
IF #errno <> 0
BEGIN
SELECT #errmsg = 'An error occurred while inserting LocalDB.[dbo].[ARC_PROCESS_TRACKING] with 2.',
GOTO error
END
---- Move data to remote server
INSERT INTO REMOTE.RemoteDB.[dbo].[ARC_TRANS_HEADER]
SELECT *
FROM LocalDB.[dbo].ARC_TRANSACTION_HEADER
SELECT #errno = ##error
IF #errno <> 0
BEGIN
SELECT #errmsg = 'An error occurred while inserting REMOTE [ARC_TRANS_HEADER].',
GOTO error
END
---- Insert Tracking with status of 2 to show the data has moved
-- **This ROW DOES NOT INSERT **
INSERT INTO LocalDB.[dbo].[ARC_PROCESS_TRACKING] ([BATCH_ID],[TRAN_CREATE_TS], [TRAN_STATUS], [ETL_START_TS], [ETL_END_TS])
SELECT batch_id, TRAN_CREATE_TS, 'END', ETL_START_TS, ETL_END_TS
FROM ARC_PROCESS_TRACKING
WHERE BATCH_ID = #next_batch_id
SELECT #errno = ##error
IF #errno <> 0
BEGIN
SELECT #errmsg = 'An error occurred while inserting LocalDB.[dbo].[ARC_PROCESS_TRACKING] with 2.',
GOTO error
END
RETURN
/* Error Handler */
error:
INSERT INTO error_table
SELECT #errno, #errmsg
RETURN
The issue is that the last insert to the ARC_PROCESS_TRACKING table does not happen. The first insert to the ARC_PROCESS_TRACKING table works. The insert to the Remote.RemoteDB.dbo.ARC_TRANS_HEADER works for all records (20,000).
No errors are logged. If I change the procedure to write to a temp table, it works. Something with the insert into the remote table is causing the last insert not to fire. Is there a requirement with the DBLINK table that I am missing to get the subsequent insert statement to work?
Since when you switched statements and the second select worked leads me to believe that the problem is not with MS SQL Server its most likely with the linked server you are connecting to. What may be happening is (and this happened to me when connecting to DB2) the source is not cleaning up its connection right away and is not reporting anything. It may be falling off a cliff.
Eventually the target will clean up the connection pool. You can try using a waitfor time statement between the reads. I would start with an hour
waitfor time '01:00';
and if it works slowly narrow the waitfor time until it stops working.

Why is my code not returning an error message when ##ROWCOUNT=0?

I created the below sp. When I execute it and insert an ID that does not exist, I want an error message to appear. However, the error message does not get printed... Does anyone can help me understand what I'm doing wrong please? Thank you
create procedure campaign_data
#campaign_ID bigint
as
begin
select campaignname,totalspend,clicks,impressions,totalspend/clicks as cpc
from DSP_RawData_Archive
where #campaign_ID=Campaign_ID
end
exec campaign_data 2
if ##ROWCOUNT=0 print 'Campaign_ID does not exist'
The PRINT statement will be displayed under the Messages tab on the results window if you are using SSMS and doesn't affect control flow.
Try throwing an error using RAISERROR with high enough severity (the middle parameter), which can affect control flow (jumps to CATCH or stops execution, for example).
IF ##ROWCOUNT = 0
RAISERROR('Campaign_ID does not exist', 15, 1)
The problem here is the scope of your ##ROWCOUNT. ##ROWCOUNT returns the number of effected rows of the last executed statement in a batch, in this case that from exec campain_data 2 and not that from the first select. Another example:
BEGIN TRAN
SELECT 0 WHERE 1 = 0
PRINT ##ROWCOUNT -- Displays 0
EXEC dbo.DoSomething -- Say this procedure returns 2 rows...
PRINT ##ROWCOUNT -- Displays 2
COMMIT
Another thing here is that you maybe want to show a proper error message in your scenario (instead of a simple PRINTed line). You can do achieve this using either
RAISERROR('Display my custom error message via RAISERROR!',16,1)
or
THROW 50000,'Display my custom error message via THROW!',1
Helpful article:
http://sqlhints.com/2013/06/30/differences-between-raiserror-and-throw-in-sql-server/
Try :
create procedure campaign_data
#campaign_ID bigint
as
begin
select campaignname,totalspend,clicks,impressions,totalspend/clicks as cpc
from DSP_RawData_Archive
where #campaign_ID=Campaign_ID
end
exec campaign_data 2
IF (SELECT ##rowcount) = 0
SELECT 'Campaign_ID does not exist'
I tried your stored procedure in Sequel Server Management Studio, and I saw the right response in message tab.
Where are you trying it?

Why does the Try/Catch not complete in SSMS query window?

This sample script is supposed to create two tables and insert a row into each of them.
If all goes well, we should see OK and have two tables with data. If not, we should see FAILED and have no tables at all.
Running this in a query window displays an error for the second insert (as it should), but does not display either a success or failed message. The window just sits waiting for a manual rollback. ??? What am I missing in either the transactioning or the try/catch?
begin try
begin transaction
create table wpt1 (id1 int, junk1 varchar(20))
create table wpt2 (id2 int, junk2 varchar(20))
insert into wpt1 select 1,'blah'
insert into wpt2 select 2,'fred',0 -- <<< deliberate error on this line
commit transaction
print 'OK'
end try
begin catch
rollback transaction
print 'FAILED'
end catch
The problem is that your error is of a high severity, and is a type that breaks the connection immediately. TRY-CATCH can handle softer errors, but it does not catch all errors.
Look for - What Errors Are Not Trapped by a TRY/CATCH Block:
It looks like after the table is created, the following inserts are parsed (recompiled), which trigger statement level recompilations and breaks the batch.

TRY CATCH Block in T-SQL

I encountered a stored procedure that had the following error handling block immediately after an update attempt. The following were the last lines of the SP.
Is there any benefit of doing this? It appears to me as though this code is just rethrowing the same error that it caught without any value added and that the code would presumably behave 100% the same if the Try Block were ommited entirely.
Would there be ANY difference in the behavior of the resulting SP if the TRY block were ommitted?
BEGIN CATCH
SELECT #ErrMsg = ERROR_MESSAGE(), #ErrSev = ERROR_SEVERITY(), #ErrState = ERROR_STATE()
RAISERROR (#ErrMsg, #ErrSev, #ErrState)
END CATCH
Barring the fact that the "line error occured on" part of any message returned would reference the RAISERROR line and not the line the error actually occured on, there will be no difference. The main reason to do this is as #Chris says, to allow you to programmatically use/manipulate the error data.
What we usually do in our stored procedure is to write the catch block like this
BEGIN CATCH
DECLARE #i_intErrorNo int
DECLARE #i_strErrorMsg nvarchar(1000)
DECLARE #i_strErrorProc nvarchar(1000)
DECLARE #i_intErrorLine int
SELECT #i_intErrorNo=Error_Number()
SELECT #i_strErrorMsg=Error_Message()
SELECT #i_strErrorProc=Error_Procedure()
SELECT #i_intErrorLine=Error_Line()
INSERT INTO error table ////// Insert statement.
END CATCH
This is something we use to do to store error. For proper message to user, I always use the output parameter to the stored procedure to show the detailed/required reason of the error.
if you look on the msdn page for RAISERROR then you see this general description:
Generates an error message and
initiates error processing for the
session. RAISERROR can either
reference a user-defined message
stored in the sys.messages catalog
view or build a message dynamically.
The message is returned as a server
error message to the calling
application or to an associated CATCH
block of a TRY…CATCH construct.
It appears that the "calling application" will get the error message. It may be that the creator of the stored procedure wanted only the error message, severity, and state to be reported and no other options that can be added. This may be because of security concerns or just that the calling application did not need to know the extra information (which could have been verbose or overly detailed, perhaps).
There is a subtle difference, as demonstrated below.
First setup the following:
CREATE TABLE TMP
( ROW_ID int NOT NULL,
ALTER TABLE TMP ADD CONSTRAINT PK_TMP PRIMARY KEY CLUSTERED (ROW_ID)
)
GO
CREATE PROC pTMP1
AS
BEGIN TRY
INSERT INTO TMP VALUES(1)
INSERT INTO TMP VALUES(1)
INSERT INTO TMP VALUES(2)
END TRY
BEGIN CATCH
DECLARE #ErrMsg varchar(max)= ERROR_MESSAGE(),
#ErrSev int = ERROR_SEVERITY(),
#ErrState int = ERROR_STATE()
RAISERROR (#ErrMsg, #ErrSev, #ErrState)
END CATCH
GO
CREATE PROC pTMP2
AS
INSERT INTO TMP VALUES(1)
INSERT INTO TMP VALUES(1)
INSERT INTO TMP VALUES(2)
GO
Now run the following:
SET NOCOUNT ON
DELETE TMP
exec pTMP1
SELECT * FROM TMP
DELETE TMP
exec pTMP2
SELECT * FROM TMP
SET NOCOUNT OFF
--Cleanup
DROP PROCEDURE pTMP1
DROP PROCEDURE pTMP2
DROP TABLE TMP
You should get the following results:
Msg 50000, Level 14, State 1, Procedure pTMP1, Line 12
Violation of PRIMARY KEY constraint 'PK_TMP'. Cannot insert duplicate key in object 'dbo.TMP'. The duplicate key value is (1).
ROW_ID
-----------
1
Msg 2627, Level 14, State 1, Procedure pTMP2, Line 4
Violation of PRIMARY KEY constraint 'PK_TMP'. Cannot insert duplicate key in object 'dbo.TMP'. The duplicate key value is (1).
The statement has been terminated.
ROW_ID
-----------
1
2
Notice that the TRY..CATCH version did not execute the third INSERT statement, whereas the pTMP2 proc did. This is because control jumps to CATCH as soon as the error occurs.
NOTE: The behaviour of pTMP2 is affected by the XACT_ABORT setting.
Conclusion
The benefit of using TRY..CATCH as demonstrated depends on how you manage your transaction boundaries.
If you roll-back on any error, then the changes will be undone. But this doesn't eliminate side-effects such as addtional processing. NOTE: If a different session simultaneously queries TMP using WITH(NOLOCK) it may even be able to observe the temporary change.
However, if you don't intend rolling back a transaction, you may find the technique is quite important to prevent certain data changes being applied in spite of an earlier error.

SQL Statement Termination using RAISERROR

(SQL 2005)
Is it possible for a raiserror to terminate a stored proc.
For example, in a large system we've got a value that wasn't expected being entered into a specific column. In an update trigger if you write:
if exists (select * from inserted where testcol = 7)
begin
raiseerror('My Custom Error', 16, 1)
end
the update information is still applied.
however if you run
if exists (select * from inserted where testcol = 7)
begin
select 1/0
end
a divide by 0 error is thrown that actually terminates the update.
is there any way i can do this with a raiseerror so i can get custom error messages back?
In a trigger, issue a ROLLBACK, RAISERROR and then RETURN.
see Error Handling in SQL Server - Trigger Context by Erland Sommarskog
Can you not just add a CHECK constraint to the column to prevent it from being inserted in the first place?
ALTER TABLE YourTable ADD CONSTRAINT CK_No_Nasties
CHECK (testcol <> 7)
Alternatively you could start a transaction in your insert sproc (if you have one) and roll it back if an error occurs. This can be implemented with TRY, CATCH in SQL Server 2005 and avoids having to use a trigger.
Begin try
#temp number
#temp=1/0
End try
Begin catch
#errormsg varchar(100)
#errormsg=error_massage()
Raiseerror(#errormsg,16,1)
End catch
You should check for valid data prior to performing the update.
IF (#testvalue = 7)
RAISERROR("Invalid value.", 16, 1);
ELSE
UPDATE...