When trying to validate a user supplied GUID within a stored procedure a simple approach was used; take the user input as a CHAR(36) then explicitly CAST it as a UNIQUEIDENTIFIER within a TRY CATCH. The CATCH then bubbles the error with a custom error description using a RAISERROR.
Running the stored procedure manually everything performs as expected and the error is raised.
Create a tSQLt test to call the unit (the procedure with GUID validation) and handle the error that is output and compare with the expected error continually fails with a transaction error; tSQLt has detected an error and handled within the tSQLt framework.
This suggests to me that the severity of a failure to CAST to a different datatype is being handled by tSQLt and it is preventing the TRY/CATCH within the stored procedure to handle it. Much like nested procedures sometimes ignore the TRY/CATCH within the child procedure and bubble up to the parent procedure; example being if the child proc. references a table that doesn't exist.
Has anyone had a similar issue? Just simply to validate my current line of thinking.
I've removed the test and it's being tested elsewhere, but this has caused me a 'hole' it my DB unit tests.
Finally, I think I should mention that I know I can perform a different validation on a supplied CHAR parameter, other than a CAST, and raise an error that way, but this is a tSQLt query and not a tSQL query.
EDIT
Example of the code:
#sGUID is a CHAR(36) and is a parameter passed to the procedure.
BEGIN TRY
SELECT CAST(#sGUID AS UNIQUEIDENTIFIER)
END TRY
BEGIN CATCH
RAISERROR('Invalid GUID format',16,1)
END CATCH
The SELECT line never triggers the CATCH tSQLt appears to intervene before hand and throws the ROLLBACK transaction error.
When you call RAISEERROR(), you're terminating the transaction that tSQLt is running --> hence the transaction error you're seeing.
To improve this for the purpose of unit testing, one option you might consider would be to replace the RAISEERROR() statement with a call to a custom stored procedure that only contains RAISERROR(). That way, you can unit-test that stored procedure seperately.
BEGIN TRY
SELECT CAST(#sGUID AS UNIQUEIDENTIFIER)
END TRY
BEGIN CATCH
EXEC dbo.customprocedure
--RAISERROR('Invalid GUID format',16,1)
END CATCH
Related
I have stored procedure which conditionally execute nested stored procedure.
In unit test, I need to check if this nested stored procedure was executed.
I tried tSQLt.SpyProcedure, but it doesnt seems to work the way i want.
content of my unit test
-- Assembly
exec tSQLt.SpyProcedure 'procedureName', 'raiserror(''procedureName was fired'',16,1)'
-- Assert
exec tSQLt.ExpectException 'procedureName was fired'
-- Action
exec masterProcedureName -- triggers procedureName
but tsqlt.run 'unitestName' returns
failed: (Failure) Expected an error to be raised.
Do you have any idea ?
While, as you mentioned, your approach works, I suggest you use the ..._spyprocedurelog table instead. It’ll allow you to catch multiple executions as well as the parameters passed each time. And if you at some point add error handling to the outer procedure, this will still work.
Check out the example in the SpyProcedure documentation.
My apologies, following code does work i had mistake somewhere else.
I have a stored procedure where I need to cast to a type, but do not know if the cast will succeed. In an imperative language, I would use some sort of TryCast pattern. I figured that this would be equivalent in T-SQL:
begin try
select cast(#someValue as SomeType)
end try begin catch end catch
On the surface, it does appear to be equivalent. If #SomeTypeVar is uninitialized and the cast fails, I get NULL to work with; the correct value if the cast succeeds.
I used this same code in a stored procedure, but that yields an error: Msg 2812, Level 16, State 62, Line 20. The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction. Some research led me to other questions on Stack Overflow and this table of times when try-catch fails in T-SQL:
TRY…CATCH constructs do not trap the following conditions:
Warnings or informational messages that have a severity of 10 or
lower.
Errors that have a severity of 20 or higher that stop the SQL
Server Database Engine task processing for the session. If an error
occurs that has severity of 20 or higher and the database connection
is not disrupted, TRY…CATCH will handle the error.
Attentions, such as
client-interrupt requests or broken client connections.
When the
session is ended by a system administrator by using the KILL
statement.
The following types of errors are not handled by a CATCH
block when they occur at the same level of execution as the TRY…CATCH
construct:
Compile errors, such as syntax errors, that prevent a batch from
running.
Errors that occur during statement-level recompilation, such
as object name resolution errors that occur after compilation because
of deferred name resolution.
These errors are returned to the level
that ran the batch, stored procedure, or trigger.
At first, I thought I fell into the statement-level recompilation bucket (as my error level is 16) until I tried to bisect the problem. The minimal reproduction is as follows:
create procedure failsInTransactions
as
begin
begin try
select cast(#someValue as SomeType)
end try begin catch end catch
end
and the calling code:
begin tran
exec failsInTransactions
commit
This yields the error I discussed above. However, I remembered that if a stored procedure doesn't have any parameters, you can call it without exec. This:
begin tran
failsInTransactions
commit
succeeds with Command(s) completed successfully. Further experimentation led me to another error with level 16:
begin try
select 1/0
end try begin catch end catch
which works in both cases, producing no rows of output.
I have two questions:
Why is there different behavior calling the procedure with and without exec?
Why does another error of the same error level proceed after the catch?
The EXECUTE keyword is optional only if it is the first statement in the batch. It is not related to parameters and is required in all other contexts. Microsoft inherited this odd behavior from the Sybase code base as well as other many lax T-SQL parsing rules. I suggest you follow a strict T-SQL coding style to avoid gotchas.
The code below runs without error because it is not executing a proc at all. Since there are no semicolon statement terminators, the stored procedure name becomes part of the BEGIN TRAN statement and is interpreted as a transaction name.
begin tran
failsInTransactions
commit
You will get the expected syntax error during compilation if you add statement terminators and this will lead you down the path to specify EXEC.
begin tran;
EXEC failsInTransactions;
commit;
Be aware that not using statement terminators is deprecated so I suggest you get in the habit of specifying them. See https://www.dbdelta.com/always-use-semicolon-statement-terminators/.
I have this SQL code that checks for some parameters, if invalid will raise an error, I want also to insert an error record into errors table. the problem is that if error happens the whole transaction will be roll-backed including the error record, I want to rollback the whole transaction except the error record.
I tried creating a separate transaction and commit it with no luck.
IF #Input IS NULL
BEGIN
insert into [dbo].Errors('Field1') values ('Input is null')
RAISERROR ('some message', 16, 1)
RETURN -1
END
Is there a way to isolate the insert statement alone in a separate transaction?
Edit:
This stored procedure is called from other procedures and need to be roll-backed, even from outside, so probably i need to separate this insert statement into a separate transaction.
You need to do this in a separate transaction. No lock changes if the table is locked throughout the course of the transaction, but it does not affect what is rolled back or not. If an error occurs, the whole transaction is rolled back. Can you add a 'try-catch' block to the code that you are calling the stored procedure from? If you 'try' the stored procedure and it throws an error, you should catch the error. In your catch block, you could call a different stored procedure that records the error in the necessary table.
It sounds like the issue that you are having is where to call the stored procedure that records the errors from. This can be tricky and it all depends on how you are handling the transaction and where you are catching the errors. Let's say you have code a that calls code b and b calls the first stored procedure. Now, when the stored procedure throws an error, if you are handling that error in code a, everything that you did in b is rolled back. If you try to insert the error record in code b, that will be rolled back from code a as well.
I'm trying to insert a duplicate value in to a primary key column which raises a primary key violation error.I want to log this error inside the catch block .
Code Block :-
SET XACT_ABORT OFF
BEGIN TRY
BEGIN TRAN
INSERT INTO #Calender values (9,'Oct')
INSERT INTO #Calender values (2,'Unknown')
COMMIT TRAN
END TRY
BEGIN CATCH
Insert into #LogError values (1,'Error while inserting a duplicate value')
if ##TRANCOUNT >0
rollback tran
raiserror('Error while inserting a duplicate value ',16,20)
END CATCH
when i execute the above code it prints out the custom error message which is displayed in the catch block but doesn't insert the value in to the #LogError table
Error while inserting a duplicate value
But when i use SET XACT_ABORT ON i get a different error message but still it doesn't inserts the error message into the table
The current transaction cannot be committed and cannot support operations
that write to the log file. Roll back the transaction.
My question is
1.How to log error into the table
2.Why do i get different error message when i set xact_ABORT on .Is it a good practice to set XACT_ABORT on before every transaction
It does insert the record into #LogError but then you rollback the transaction which removes it.
You need to do the insert after the rollback or insert into a table variable instead (that are not affected by the rollback).
When an error is encountered in the try block this can leave your transaction in a doomed state. You should test the value of XACT_STATE() (see example c in the TRY ... CATCH topic) in the catch block to check for this before doing anything that writes to the log or trying to commit.
When XACT_ABORT is on any error of severity > 10 in a try block will have this effect.
As SqlServer doesn't support Autonomous transaction (nested and independent transaction), it's not possible (in fact, you can, under some condition, use CLR SP with custom connectstring - doing it's own, non local, connection) to use a database table to log SP execution activity/error messages.
To fix, this missing functionnality, I've developed a toolbox (100% T-SQL) based on the use of XML parameter passed as reference (OUTPUT parameter) which is filled during SP execution and can be save into a dedicated database table at the end.
Disclamer: I'm a Iorga employee (cofounder) and I've developped the following LGPL v3 toolbox. My objective is not Iorga/self promotion but sharing knowledge and simplify T-SQL developper life.
See, teach, enhance as you wish SPLogger
Today (October 19th of 2015) I've just released the 1.3 including a Unit Test System based on SPLogger.
I have a stored procedure that calls several others, one of which is failing to insert a row into a table due to a duplicate primary key
The error raised is
Msg 2627, Level 14, State 1, Procedure ..., Line 16
Violation of PRIMARY KEY constraint '...'. Cannot insert duplicate key in object '...'.
I am calling the this from an Excel spreadsheet via VBA, with usual On Error handling in place, but the routine is failing silently without triggering the error.
I'm not sure if this is down to the stored-proc within stored-proc or the severity of the error being too low.
Has anyone experienced anything like this and can suggest a work around?
My initial attempt was to put a BEGIN TRY / BEGIN CATCH block around the stored procedure call, with the CATCH running RAISERROR at a higher severity, but it doesn't seem to be triggering.
Thanks
In the outer proc add an explicit transaction. BEGIN TRANSACTION at the beginning and COMMIT TRANSACTION at the end.
Then before the begin transaction add SET XACT_ABORT ON;. That will take care of batch failures.
After the inner proc with the error, check the error value for statement level errors e.g.
IF ##ERROR <> 0
BEGIN
ROLLBACK TRANSACTION;
RETURN 1;
END
We do this with OUTPUT variables in T-SQL, though I only know the SQL side of it: you'll need to google getting an output parameter back from SQL with VBA. Declare an output variable as a varchar, and set it immediately to '':
DECLARE #MyError VARCHAR(500) OUTPUT
SET #MyError = ''
Do your normal error checking in T-SQL and, if you do find an error, SET a description into the #MyError variable. Your VBA code will always get the #MyError message, but it will normally be an empty string, ''. If it isn't, then you go to your error handling in VBA.