Why doesn't this catch the error in the stored procedure? - sql

SQL Server 2014 (v12.0.5546) - I have a "master" stored procedure that I run a bunch of others from. If one errors out, I want it to print a line stating so.
I ran it today, and one of the stored procedures in the master stored procedure returned an error saying that the table insert I was trying had the wrong number of variables, but it did not print the error.
I thought that stored procedures returned 0 if successful, so anything other than that would mean an error. Am I wrong, or is there a flaw in my process?
FYI, I don't need it to stop running if it encounters an error, I just need it to spit out the error message so I know where it failed. This is going to grow to 20-30 stored procedures by the time it's all said and done.
Here is the master stored procedure:
ALTER PROCEDURE [dbo].[Master_CreateLoadTables]
AS
DECLARE #retval INT
-- Builds all tables required for the stored procedures
EXEC #retval = [BuildTables]
IF (#retval) = 0
BEGIN
SET #retval = 1
EXEC #retval = [Load_CustomerLookup]
IF (#retval) <> 0
PRINT 'Load of Customer Lookup Table Failed'
EXEC #retval = [Load_CustomerInvoices]
IF (#retval) <> 0
PRINT 'Load of Customer Invoice Tables Failed'
EXEC #retval = [Load_Payments]
IF (#retval) <> 0
PRINT 'Load of Payments Table Failed'
END
ELSE
PRINT 'Table Creation Failed'

I thought that stored procedures returned 0 if successful, so anything other than that would mean an error. Am I wrong, or is there a flaw in my process?
Stored procedures return whatever value you tell them to. If there is no return statement, then they return success, 0.
The generally accepted practice is to return 0 for success and an error code for failure. But that is not enforced.
You are referring to user stored procedures. You need to investigate how they work in your environment.
I also encourage you to put the body of the stored procedure in a begin/end block.

one of the stored procedures in the master stored procedure returned
an error saying that the table insert I was trying had the wrong
number of variables, but it did not print the error.
It seems this was a compilation error. The return code will not be set after compilation errors so the assigned variable will remain unchanged. For example:
CREATE PROC dbo.ExampleCompilationError
AS
SELECT * FROM dbo.ThisTableDoesNotExist;
GO
DECLARE #rc int = NULL;
EXEC #rc = dbo.ExampleCompilationError;
SELECT #rc;
GO
The return code is still NULL with this code.
You could surround each proc execution with TRY/CATCH, which will catch compilation errors and execution time errors in the inner scope:
BEGIN TRY
EXEC dbo.ExampleCompilationError;
END TRY
BEGIN CATCH
PRINT 'ExampleCompolationError failed';
END CATCH;

Related

Properly understanding the error Cannot use the ROLLBACK statement within an INSERT-EXEC statement - Msg 50000, Level 16, State 1

I understand there is a regularly quoted answer that is meant to address this question, but I believe there is not enough explanation on that thread to really answer the question.
Why earlier answers are inadequate
The first (and accepted) answer simply says this is a common problem and talks about having only one active insert-exec at a time (which is only the first half of the question asked there and doesn't address the ROLLBACK error). The given workaround is to use a table-valued function - which does not help my scenario where my stored procedure needs to update data before returning a result set.
The second answer talks about using openrowset but notes you cannot dynamically specify argument values for the stored procedure - which does not help my scenario because different users need to call my procedure with different parameters.
The third answer provides something called "the old single hash table approach" but does not explain whether it is addressing part 1 or 2 of the question, nor how it works, nor why.
No answer explains why the database is giving this error in the first place.
My use case / requirements
To give specifics for my scenario (although simplified and generic), I have procedures something like below.
In a nutshell though - the first procedure will return a result set, but before it does so, it updates a status column. Effectively these records represent records that need to be synchronised somewhere, so when you call this procedure the procedure will flag the records as being "in progress" for sync.
The second stored procedure calls that first one. Of course the second stored procedure wants to take those records and perform inserts and updates on some tables - to keep those tables in sync with whatever data was returned from the first procedure. After performing all the updates, the second procedure then calls a third procedure - within a cursor (ie. row by row on all the rows in the result set that was received from the first procedure) - for the purpose of setting the status on the source data to "in sync". That is, one by one it goes back and says "update the sync status on record id 1, to 'in sync'" ... and then record 2, and then record 3, etc.
The issue I'm having is that calling the second procedure results in the error
Msg 50000, Level 16, State 1, Procedure getValuesOuterCall, Line 484 [Batch Start Line 24]
Cannot use the ROLLBACK statement within an INSERT-EXEC statement.
but calling the first procedure directly causes no error.
Procedure 1
-- Purpose here is to return a result set,
-- but for every record in the set we want to set a status flag
-- to another value as well.
alter procedure getValues #username, #password, #target
as
begin
set xact_abort on;
begin try
begin transaction;
declare #tableVariable table (
...
);
update someOtherTable
set something = somethingElse
output
someColumns
into #tableVariable
from someTable
join someOtherTable
join etc
where someCol = #username
and etc
;
select
someCols
from #tableVariable
;
commit;
end try
begin catch
if ##trancount > 0 rollback;
declare #msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
raiserror (#msg, 16, 1);
return 55555
end catch
end
Procedure 2
-- Purpose here is to obtain the result set from earlier procedure
-- and then do a bunch of data updates based on the result set.
-- Lastly, for each row in the set, call another procedure which will
-- update that status flag to another value.
alter procedure getValuesOuterCall #username, #password, #target
as
begin
set xact_abort on;
begin try
begin transaction;
declare #anotherTableVariable
insert into #anotherTableVariable
exec getValues #username = 'blah', #password = #somePass, #target = ''
;
with CTE as (
select someCols
from #anotherTableVariable
join someOtherTables, etc;
)
merge anUnrelatedTable as target
using CTE as source
on target.someCol = source.someCol
when matched then update
target.yetAnotherCol = source.yetAnotherCol,
etc
when not matched then
insert (someCols, andMoreCols, etc)
values ((select someSubquery), source.aColumn, source.etc)
;
declare #myLocalVariable int;
declare #mySecondLocalVariable int;
declare lcur_myCursor cursor for
select keyColumn
from #anotherTableVariable
;
open lcur_muCursor;
fetch lcur_myCursor into #myLocalVariable;
while ##fetch_status = 0
begin
select #mySecondLocalVariable = someCol
from someTable
where someOtherCol = #myLocalVariable;
exec thirdStoredProcForSettingStatusValues #id = #mySecondLocalVariable, etc
end
deallocate lcur_myCursor;
commit;
end try
begin catch
if ##trancount > 0 rollback;
declare #msg nvarchar(2048) = error_message() + ' Error line: ' + CAST(ERROR_LINE() AS nvarchar(100));
raiserror (#msg, 16, 1);
return 55555
end catch
end
The parts I don't understand
Firstly, I have no explicit 'rollback' (well, except in the catch block) - so I have to presume that an implicit rollback is causing the issue - but it is difficult to understand where the root of this problem is; I am not even entirely sure which stored procedure is causing the issue.
Secondly, I believe the statements to set xact_abort and begin transaction are required - because in procedure 1 I am updating data before returning the result set. In procedure 2 I am updating data before I call a third procedure to update further data.
Thirdly, I don't think procedure 1 can be converted to a table-valued function because the procedure performs a data update (which would not be allowed in a function?)
Things I have tried
I removed the table variable from procedure 2 and actually created a permanent table to store the results coming back from procedure 1. Before calling procedure 1, procedure 2 would truncate the table. I still got the rollback error.
I replaced the table variable in procedure 1 with a temporary table (ie. single #). I read the articles about how such a table persists for the lifetime of the connection, so within procedure 1 I had drop table if exists... and then create table #.... I still got the rollback error.
Lastly
I still don't understand exactly what is the problem - what is Microsoft struggling to accomplish here? Or what is the scenario that SQL Server cannot accommodate for a requirement that appears to be fairly straightforward: One procedure returns a result set. The calling procedure wants to perform actions based on what's in that result set. If the result set is scoped to the first procedure, then why can't SQL Server just create a temporary copy of the result set within the scope of the second procedure so that it can be acted upon?
Or have I missed it completely and the issue has something to do with the final call to a third procedure, or maybe to do with using try ... catch - for example, perhaps the logic is totally fine but for some reason it is hitting the catch block and the rollback there is the problem (ie. so if I fix the underlying reason leading us to the catch block, all will resolve)?

If stored procedure returns 1, throw error, else continue

I have a stored procedure which is calling another stored procedure (cannot change this to a function). Depending on the result of the called stored procedure, I'd either like to throw an error, or carry on.
The stored procedure that is being called is looking for data in a table and returning 1 if it finds the data, or null if no data is returned.
How would the SQL look if I was using the following logic? If the stored procedure returns a value of 1, throw an error. If the stored procedure returns a NULL, carry on.
DECLARE #returnvalue INT
EXEC #returnvalue = Stored_procedure_name
IF #returnvalue = 1
BEGIN
--You could use THROW (available in SQL Server 2012+):
--THROW <error_number>, <message>, <state>
THROW 50000, 'Your custom error message', 1
END

SQL Unit Testing with TSQLUNIT

I noticed something weird while unit testing an update/insert stored procedure using TSQLUNIT on SQL Server 2012. when I call Exec tsu_RunTests, my test procedure runs but with unexpected behaviour. the line in the code that calls my original stored procedure is executed but no actual updates or inserts made to the database table as expected. Is there a valid reason for this behaviour? Or is this a bug I need to pay much attention to? I notice that when I execute the same original stored procedure outside of the test procedure, it works fine.
You can use Default parameter for each stored procedure and set this #IsTest parameter to true when use these stored procedures in tsu_RunTests.
CREATE PROCEDURE orginal_proc
--#parameter_name
#IsTest BIT = 0
AS
if #IsTest <> 1 Begin
-- Test statements
End Else Begin
-- statements
End
GO
you also can use ##NESTLEVEL for check that your procedure execute directly or execute by other procedure.
CREATE PROCEDURE orginal_proc
--#parameter_name
AS
if ##NESTLEVEL <> 1 Begin
-- Test statements
End Else Begin
-- statements
End
GO
EDIT : Stored Procedure Code must be like below :
If ##NESTLEVEL <> 1 Print 'Befor Update Message'
If ##NESTLEVEL = 1 Begin
Update YourTable
Set ...
End
If ##NESTLEVEL <> 1 Print 'After Update Message'

SQL stored procedure in one db called from another db. What is the context?

This is a question related to the context when calling a stored procedure from one database in the context of another database.
Say I have a procedure created in the MainDB:
USE MainDB;
GO
CREATE PROCEDURE dbo.sp_mainproc
#Login nvarchar(50),
#Error INT OUTPUT
AS
BEGIN
-- many details left out...
-- Login as string must be captured in the xUser table to get
-- the personal settings for the user...
SET #_user_id = ( SELECT dbo.xUser.user_id
FROM dbo.xUser
WHERE dbo.xUser.login = #Login );
IF( #_user_id IS NULL )
BEGIN
-- The user with the given #Login is not present. Indicate the failure.
SET #Error = 2
RETURN (1)
END
-- Do something in the MainDB. Here the main reason for calling
-- the stored procedure is implemented.
-- Indicate the success when finishing.
SET #Error = 0
RETURN (0)
END
GO
Now, I want to call the procedure from another procedure in the AuxDB:
USE AuxDB;
GO
CREATE PROCEDURE dbo.sp_action
AS
BEGIN
-- Call the MainDB.dbo.sp_mainproc to do the action in the MainDB.
-- The login name must be passed, and possible error must be checked.
DECLARE #error INT
DECLARE #retcode INT
EXEC #retcode = MainDB.dbo.sp_mainproc
N'the_user',
#error OUTPUT
IF (#retcode <> 0)
BEGIN
-- Here the error must be signalized.
RETURN 1
END
-- Everything OK, let's continue...
RETURN 0
END
GO
My question is: When the MainDB.dbo.sp_mainproc is called from within AuxDB.dbo.sp_action, where the dbo.xUser table used in the sp_mainproc is searched for. Is the MainDB.dbo.xUser considered, or is the AuxDB.dbo.xUser searched for?
Thanks,
Petr
Procs are compiled, so it will refer to the object in the same database in which the dbo.sp_mainproc exists, because when the proc was created, it refers only to dbo.xUser, which doesn't have a database name part
(i.e. MainDB.dbo.sp_mainproc will use MainDB.dbo.xUser irrespective of which database that the proc is called from).

Find out the calling stored procedure in SQL Server

Is it possible to find out who called a stored procedure?
For example, say I get an error in proc3. From within that proc I want to know if it was called by proc1 or proc2.
I would use an extra input parameter, to specify the source, if this is important for your logic.
This will also make it easier to port your database to another platform, since you don't depend on some obscure platform dependent function.
There is no nice automatic way to do this (alas). So it really depends on how much you are prepared to (re)write your procs in order to be able to do this.
If you have a logging mechanism, you might be able to read the log and work out who called you.
For example, if you implement logging by inserting to a table, for example:
CREATE TABLE Log
(timestamp dattime,
spid int,
procname varchar(255),
message varchar(255) )
... text of proc ...
INSERT INTO Log
SELECT get_date(), ##spid, #currentproc, 'doing something'
-- you have to define #currentproc in each proc
-- get name of caller
SELECT #caller = procname
FROM Log
WHERE spid = ##spid
AND timestamp = (SELECT max(timestamp)
FROM Log
WHERE timestamp < get_date()
AND procname != #currentproc )
This wouldn't work for recursive calls, but perhaps someone can fix that?
Do you need to know in proc3 at runtime which caused the error, or do you just need to know while debugging?
You can use SQL Server profiler if you only need to do it during debugging/monitoring.
Otherwise in 2005 I don't believe you have the ability to stack trace.
To work around it you could add and extra parameter to proc3, #CallingProc or something like that.
OR you could add try catch blocks to proc1 and proc2.
BEGIN TRY
EXEC Proc3
END TRY
BEGIN CATCH
SELECT 'Error Caught'
SELECT
ERROR_PROCEDURE()
END CATCH
Good reference here : http://searchsqlserver.techtarget.com/tip/1,289483,sid87_gci1189087,00.html
and of course always SQL Server Books Online
SQL Server 2008 does have the ability to debug through procedures however.
You could have proc1 and proc2 pass their names into proc3 as a parameter.
For example:
CREATE PROCEDURE proc3
#Caller nvarchar(128) -- Name of calling proc.
AS
BEGIN
-- Produce error message that includes caller's name.
RAISERROR ('Caller was %s.', 16,10, #Caller);
END
GO
CREATE PROCEDURE proc1
AS
BEGIN
-- Get the name of this proc.
DECLARE #ProcName nvarchar(128);
SET #ProcName = OBJECT_NAME(##PROCID);
-- Pass it to proc3.
EXEC proc3 #ProcName
END
GO
CREATE PROCEDURE proc2
AS
BEGIN
-- Get the name of this proc.
DECLARE #ProcName nvarchar(128);
SET #ProcName = OBJECT_NAME(##PROCID);
-- Pass it to proc3.
EXEC proc3 #ProcName
END
GO