Stored Procedure Error Handling - Clean up but return original error - sql-server-2008

I'm writing a stored procedure that needs to clean up some data if an insert fails. I'd like it to perform the clean up, but return the original error if this insert fails (primarily for logging as I want to see exactly why the insert failed). Basically like a throw; in C#. Is there a simple way to do this?
BEGIN TRY
Insert into table (col1) values ('1")
END TRY
BEGIN CATCH
--do clean up here
--then throw original error
END TRY
Is this feasible/good practice? In the application code that calls the proc, I'm handling the error from an application standpoint, but the clean up statements seem to better fit inside the proc.

I usually do something like this:
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'[dbo].[procedure_name]') AND ObjectProperty(id, N'IsProcedure') = 1)
DROP PROCEDURE [dbo].[procedure_name]
GO
CREATE PROCEDURE [dbo].[procedure_name]
(
#param1 VARCHAR(100)
,#param2 INT
)
AS
/*
*******************************************************************************
<Name>
[procedure_name]
</Name>
<Purpose>
[Purpose]
</Purpose>
<Notes>
</Notes>
<OutsideRef>
Called From: [Called From]
</OutsideRef>
<ChangeLog>
Change No: Date: Author: Description:
_________ ___________ __________ _____________________________________
001 [DATE] [YOU] Created.
</ChangeLog>
*******************************************************************************
*/
BEGIN
SET NOCOUNT ON
SET XACT_ABORT OFF -- Allow procedure to continue after error
-- *****************************************
-- Parameter string. Used for error handling
-- *****************************************
DECLARE #ErrorNumber INT
,#ErrorMessage VARCHAR(400)
,#ErrorSeverity INT
,#ErrorState INT
,#ErrorLine INT
,#ErrorProcedure VARCHAR(128)
,#ErrorMsg VARCHAR(2000)
,#NestedProc BIT = 1
,#Params VARCHAR(255); -- String representing parameters, make it an appropriate size given your parameters.
--Be Careful of the CONVERT here, GUIDs (if you use them) need 36 characters, ints need 10, etc.
SET #Params = ''
+ CHAR(13) + '#param1 = ' + COALESCE(CONVERT(VARCHAR(100), #param1), 'NULL')
+ CHAR(13) + '#param2 = ' + COALESCE(CONVERT(VARCHAR(10), #param2), 'NULL')
BEGIN TRY
--If you're using transactions, and want an 'all or nothing' approach, use this so that
--you only start a single transaction in the outermost calling procedure (or handle
--that through your application layer)
IF ##TRANCOUNT = 0
BEGIN
SET #NestedProc = 0
BEGIN TRANSACTION
END
INSERT INTO [TABLE]
(
COL1
,COL2
)
VALUES
(
#param1
,#param2
);
--Commit the transaction if this is the outtermost procedure and if there is a transaction to rollback.
IF ##TRANCOUNT > 0 AND #NestedProc = 0
BEGIN
COMMIT TRANSACTION
END
END TRY
BEGIN CATCH
--Roll back the transaction if this is the outtermost procedure and if there is a transaction to rollback.
IF ##TRANCOUNT > 0 AND #NestedProc = 0
BEGIN
ROLLBACK TRANSACTION
END
-- Execute the error retrieval routine.
SELECT
#ErrorNumber = ERROR_NUMBER(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorProcedure = ERROR_PROCEDURE(),
#ErrorState = ERROR_STATE(),
#ErrorLine = ERROR_LINE(),
#ErrorMessage = ERROR_MESSAGE();
SET #ErrorMsg = 'Error Number : ' + CAST(#ErrorNumber AS VARCHAR(5)) + CHAR(13)
+ 'Procedure Name : ' + #ErrorProcedure + CHAR(13)
+ 'Procedure Line : ' + CAST(#ErrorLine AS VARCHAR(5)) + CHAR(13)
+ 'Error Message : ' + #ErrorMessage + CHAR(13)
+ 'Parameters : ' + CHAR(13) + #Params + CHAR(13);
--Raise the exception.
RAISERROR (#ErrorMsg, #ErrorSeverity, #ErrorState);
END CATCH
END
GO
This type of procedure allows you to have nesting procs with transactions (so long as the desired effect is that if an error is thrown anywhere, you'll eventually throw back up to the outer procedure and then rollback). A pretty important scenario that I don't think this template handles is the case where an error that's severe enough to completely kill the procedure is thrown. Perhaps someone else could chime in on that front.

Assuming we are using a table MyTable defined as
CREATE TABLE [dbo].[MyTable](
[Col1] [int] NOT NULL
) ON [PRIMARY]
The I would use a procedure similar to the one below.
In the case of insert failure the code will enter the Catch block where a check for the error number/message can be perform and assigned.
Once assigned the transaction can be rolled back and the error number/message returned.
You may need to change the SQL Server Error number in the RAISERROR error line depending on what you are doing.
CREATE PROCEDURE [dbo].[zTestProc]
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#LocalError INT,
#ErrorMessage VARCHAR(4000)
BEGIN TRY
BEGIN TRANSACTION TestTransaction
Insert into MyTable(col1) values ('01/01/2002')
COMMIT TRANSACTION TestTransaction
END TRY
BEGIN CATCH
SELECT #LocalError = ERROR_NUMBER(),
#ErrorMessage = ERROR_MESSAGE()
IF( XACT_STATE()) <>0
BEGIN
ROLLBACK TRANSACTION TestTransaction
END
RAISERROR ('TestSP: %d: %s', 16, 1, #LocalError, #ErrorMessage) ;
RETURN(0)
END CATCH
END

Try the following snippet.
DECLARE #errNum int
DECLARE #rowCount int
BEGIN TRY
INSERT INTO [TABLE] (COL1) VALUES ('1")
END TRY
BEGIN CATCH
SET #errNum = ##ERROR
SET #rowCount = ##ROWCOUNT
RAISEERROR(#errNum)
END CATCH

Related

Transaction error in stored procedure that is not using BEGIN or END TRANSACTION

I have a stored procedure "let's call it MY_NEW_SP" in which I'm not using BEGIN TRY / BEGIN CATCH. but, when I'm excecuting this SP (MY_NEW_SP), I get the following error:
Msg 266, Level 16, State 2, Procedure <MY_NEW_SP>, Line 132
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.
This new stored procedure makes a big select basically, no transactions are made "in the sense of make DML operations on tables (INSERT, DELETE, UPDATE)", but in temp tables "i.e. #tmp".
I'm thinking this transaction error is due I'm using SET XACT_ABORT ON; in other stored procedures, but, I'm not sure.
I follow what it is said here: C. Using TRY...CATCH with XACT_STATE
The basic structure of the stored procedure that uses SET XACT_ABORT ON; is as follows:
IF NOT EXISTS (SELECT * FROM sysobjects WHERE TYPE = 'P' AND NAME = 'PROCEP_NEW_SP' )
BEGIN
EXEC dbo.sp_executesql #statement = N'CREATE PROCEDURE PROCEP_NEW_SP AS'
END
GO
ALTER PROCEDURE PROCEP_NEW_SP
(
#ID_TABLE INT
)
AS
BEGIN
DECLARE #TBL_CONSECUTIVE TABLE ( LOG_CONSECUTIVE INT );
SET XACT_ABORT ON;
BEGIN TRANSACTION
BEGIN TRY
IF ISNULL(#ID_TABLE, -1) = -1
BEGIN
SET #ID_TABLE = 1;
DELETE FROM #TBL_CONSECUTIVE;
INSERT INTO T_BH_LOG_TABLE (ASO_NCODE, CHA_NCODE, TSO_NCODE,
MSO_DACTION_DATE, MSO_CRESULT, MSO_CCAUSE_FAILURE)
OUTPUT INSERTED.MSO_NCODE INTO #TBL_CONSECUTIVE
SELECT #ASO_NCODE, ISNULL(#CHA_NCODE, 1), ISNULL(#TSO_NCODE, 1),
GETDATE() AS MSO_DACTION_DATE, #CST_FAIL_OR_SUC, #CST_GENERIC_MSG;
IF (XACT_STATE()) = 1
BEGIN
COMMIT TRANSACTION;
END
SELECT NULL Id_table, 'Failed' Result_process, 'Parameter (ID_TABLE) is required.' Result_process_message;
RETURN;
END
-- Operation:
UPDATE MY_TABLE
SET NAME = 'SAMPLE'
WHERE ID_TABLE = #ID_TABLE;
IF (XACT_STATE()) = 1
BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
INSERT INTO T_BH_LOG_TABLE (ASO_NCODE, CHA_NCODE, TSO_NCODE,
MSO_DACTION_DATE, MSO_CRESULT, MSO_CCAUSE_FAILURE)
OUTPUT INSERTED.MSO_NCODE INTO #TBL_CONSECUTIVE
SELECT 1 AS ASO_NCODE, 1, 1 As TSO_NCODE,
GETDATE() AS MSO_DACTION_DATE, #CST_FAIL_OR_SUC, #CST_GENERIC_MSG;
SELECT NULL Id_table, 'Failed' Result_process, 'Internal error. See log # (' + CAST(L.LOG_CONSECUTIVE AS NVARCHAR) + ') for more details.' Result_process_message;
FROM #TBL_CONSECUTIVE L;
RETURN;
END CATCH
END;
I really don't know if by using SET XACT_ABORT ON; is causing this kind of error.
Anyone can point me in the right direction for solve this issue?

Adding a record with a stored procedure does not quite work

I'm experiencing some issues with adding a record in a ms sql server database with an access form. I enter data in my access form, then I execute a stored procedure which should add the data to 2 different tables.
The issue here is, the data is being submitted to one table, but not in the other one. Here's my stored procedure:
ALTER PROCEDURE [dbo].[spAddArticle]
(
#description VARCHAR(128),
#stock integer,
#price decimal(8,2),
#startdate varchar(30),
#enddate varchar(30),
#supplier varchar(128)
)
AS
BEGIN
IF (
SELECT COUNT(*)
FROM article
WHERE description = #description
) = 0
begin try
BEGIN TRANSACTION
DECLARE #articlenr INT
SELECT #articlenr = MAX(articlenr) + 1 FROM article
INSERT INTO article (articlenr, description, catcode, supplier, stock)
VALUES (#articlenr, #description, NULL, #supplier, #stock)
INSERT INTO articleprice (articlenr, price, startdate, enddate)
VALUES (#articlenr, #price, #startdate, #enddate)
commit
Raiserror('The article has been added!', 16, 1)
end try
begin catch
if ##TRANCOUNT > 0
begin
ROLLBACK
end
Raiserror('An article with description %s already exists!', 16,1,#description)
end catch
END
The data is being added to table article, but not to articleprice, and I just can't seem to figure out why... Any idea's?
Hope you guys can help me out here..
The are a number of issues with this stored procedure. I can't explain why the second insert is failing but the error handling will obfuscate errors. The "success" raiseerror will fire the catch block due to the severity 16 error. A more serious issue is that the code is vulnerable to race conditions (both the EXISTS check and the SELECT MAX). I added an UPDLOCK hint to serialize for the purpose of this proc. The version below requires SQL 2012 or later. If you are using an earlier version, change the CATCH block handling to use RAISERROR instead of THROW.
ALTER PROCEDURE [dbo].[spAddArticle]
(
#description VARCHAR(128),
#stock integer,
#price decimal(8,2),
#startdate varchar(30),
#enddate varchar(30),
#supplier varchar(128)
)
AS
SET NOCOUNT ON; --rowcount messages can confuse some applications, especially ADO classic API
BEGIN TRY
BEGIN TRANSACTION;
IF NOT EXISTS(
SELECT *
FROM article WITH(UPDLOCK, HOLDLOCK) --serialize access for this proc
WHERE description = 'a'
)
BEGIN
DECLARE #articlenr INT;
SELECT #articlenr = MAX(articlenr) + 1 FROM article;
INSERT INTO article (articlenr, description, catcode, supplier, stock) SELECT #articlenr, #description, NULL, #supplier, #stock);
INSERT INTO articleprice (articlenr, price, startdate, enddate) VALUES (#articlenr, #price, #startdate, #enddate);
COMMIT;
RAISERROR('The article has been added!', 0, 0); --informational message for debugging
END
ELSE
BEGIN
RAISERROR('An article with description %s already exists!', 16,1,#description);
END;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK;
END;
THROW;
END CATCH
GO

Can I handle an exception and then let it continue on without causing a break in the code?

I have a stored procedure (that I didn't write) that uses openquery to populate a temporary table. The problem is that we have an expected error (it hits active directory for a user that no longer exists) that is stopping the entire procedure. What I was hoping to do is catch the error, fill in some default values and allow the cursor to continue. Currently, I'm catching the error, but the proc is stopping at that point. Is there a way I can force it to continue? Here's the piece of the proc:
BEGIN
SET #SQL=N'INSERT INTO #AD_Display_Names (GUID, Display_Name)
SELECT objectGUID, displayName
FROM OPENQUERY(ADSI,''SELECT objectGUID, displayName
FROM ''''LDAP://<GUID=' + CONVERT (VARCHAR (MAX), #GUID) + '>''''
WHERE objectCategory = ''''Person'''' AND objectClass = ''''user'''''')'
BEGIN TRY
EXEC SP_EXECUTESQL #SQL
END TRY
BEGIN CATCH
SET #SQL=N'INSERT INTO #AD_Display_Names (GUID, Display_Name)
VALUES(''00000000-0000-0000-0000-000000000000'', ''Unknown'')'
EXEC SP_EXECUTESQL #SQL
END CATCH
FETCH NEXT FROM [User_Names_Cursor]
INTO #GUID
END
Why not do something like this?
-- cursor stuff here
BEGIN
DECLARE #objectGUID UNIQUEIDENTIFIER
DECLARE #displayName VARCHAR(100)
SELECT #objectGUID = objectGUID, #displayName = displayName
FROM OPENQUERY(ADSI, N'SELECT objectGUID, displayName
FROM ''LDAP://<GUID=' + CONVERT (VARCHAR (MAX), #GUID) + '>''
WHERE objectCategory = ''Person'' AND objectClass = ''user'''
IF(#objectGUID IS NULL)
BEGIN
SET #objectGUID = '00000000-0000-0000-0000-000000000000'
SET #displayName = 'Unknown'
END
INSERT INTO #AD_Display_Names (GUID, Display_Name)
VALUES(#objectGUID, #displayName)
FETCH NEXT FROM [User_Names_Cursor]
INTO #GUID
END

Porting MySQL Stored Procedure to Oracle

I'm trying to port a stored procedure from MySQL to Oracle, and I'm having a lot of trouble. I've gone through Oracle documentation, and I'm having trouble doing very basic things like declaring variables properly. I was hoping someone could show me how to properly declare and set variables.
My stored procedure is used to add values to two different tables and ensure that it's being mapped properly and the foreign keys aren't being violated.
Here is my MySQL Code:
CREATE DEFINER=root#% PROCEDURE proc_add_entry(IN theName vARCHAR(50), IN theKey VARCHAR(50), IN theOtherData VARCHAR(50), IN theOtherData2 INT, IN theStartDate DATE, IN theEndDate DaTE, IN theReferenceDate DaTE)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
BEGIN
declare theNameID int ;
declare theKeyID int ;
declare theOtherDataID int default null;
declare error bool default false;
declare continue handler for SQLEXCEPTION
set error = true;
set theKeyID = (select KeyID from map_alias ma where ma.alias = trim(theKey));
set theOtherDataID = (select theOtherDataID from map_otherdata mc where mc.otherdata = trim(theOtherData));
set theNameID = (select max(nameID) from inserttable);
set theNameID = theNameID + 1;
insert into inserttable values (theNameID , theKeyID , theOtherDataID , theOtherData2, theStartDate ,
theEndDate , theReferenceDate);
if error = true then
insert into errors_inserttable values (theNameID , theKeyID , theOtherDataID , theOtherData2, theStartDate ,
theEndDate , theReferenceDate);
end if;
set error = false;
insert into map_inserttable (theNameID , datasourceid, theName) values (theNameID , 1, theName);
if error = true then
insert into errors_map_inserttable (theNameID , datasourceid, theName) values (theNameID , 1, theName);
end if;
END
In Oracle, my last statement are being ignored (ORA-00922: Missing or invalid option). It should be a local variable, so I'm not sure why i'm getting that particular error.
I'm struggling to declare the continue handler as well. I'm getting the error:
Error(16,27): PLS-00103: Encountered the symbol "FOR" when expecting one of the following: := . ( # % ; not null range default character.
Here is my oracle code so far:
CREATE OR REPLACE PROCEDURE PROC_ADD_ENTRY
(
THENAME IN VARCHAR2
, THEKEY IN VARCHAR2
, THEOTHERDATA IN VARCHAR2
, THEOTHERDATA2 IN NUMBER
, THEFIRSTDATE IN DATE
, THELASTDATE IN DATE
, THEREFERENCEDATE IN DATE
) AS
THENAMEID INT;
THEKEYID INT;
THEOTHERDATAID int;
ERROR bool default false;
BEGIN
declare continue HANDLER FOR SQLEXCEPTION set error = true;
set THEKEYID = (select KEYID from map_INSERTTABLE mc where mc.Key = trim(THEKEY));
END PROC_ADD_ENTRY;
I'm sure this is stupidly simple for someone that uses oracle, but I'm reading the documentation and I'm seeing conflicting information on where and how to declare variables, continue handlers, and assign values to variables. (is it := or = to assign values? Do i use the word declare after the begin statement to declare variables, or do I do it the way I show below?)
If someone could show me:
a) where to declare a local variable
b) how to assign a value to it (i.e. 1 to an int)
c) how to assign a value from the DB to a variable (set var = select number from table_number tn where tn.number = 1)
d) how to declare a continue handler properly
I would really appreciate it.
You've go the basic structure fine.
create or replace procedure <name> (<param list>) as
<local variables>
begin
<body>
end <name>;
To address your specific questions:
a) where to declare a local variable
I've marked this section up above.
b) how to assign a value to it (i.e. 1 to an int)
You would use := for assignment.
eg. thenameid := 1;
The data type you want will typically match the sql data types (eg. NUMBER for the above) though there are PL/SQL-specific data types such as PLS_INTEGER. See the PL/SQL data types documentation for more details.
c) how to assign a value from the DB to a variable (set var = select number from table_number tn where tn.number = 1)
You would use the into keyword with a locally defined variable to store the value in. eg.
l_num_rows number;
select count(*) into l_num_rows from user_objects;
d) how to declare a continue handler properly
If I'm reading understanding your code correctly, you want set error = true to be executed every time there is a problem with an sql statement and then you want the stored procedure to carry on.
Exception handling is what you are after. You would wrap any or SQL PL/SQL statements that you think may have errors in an exception block like this, with as many exception cases as needed (eg. NO_DATA_FOUND):
begin
<statements that may fail>
exception when <exception name> then
<action>
...
exception when others then
<action>
end;
"other" is the catchall. You can have just this case handled but as with any error handling it is better practise to catch specific cases first.
For completion, here's roughly what your example procedure would look like. I've removed the error code flag as it is not needed and also changed the ints to numbers:
create or replace procedure proc_add_entry (
in thename varchar(50),
in thekey varchar(50),
in theotherdata varchar(50),
in theotherdata2 number,
in thestartdate date,
in theenddate date,
in thereferencedate date
) as
thenameid number;
thekeyid number;
theotherdataid number default null;
begin
begin
select keyid into thekeyid from map_alias ma where ma.alias = trim(thekey);
select theotherdataid into theotherdataid from map_otherdata mc where mc.otherdata = trim(theotherdata);
select max(nameid) into thenameid from inserttable;
thenameid := thenameid + 1;
insert into inserttable values (thenameid, thekeyid, theotherdataid, theotherdata2, thestartdate, theenddate, thereferencedate);
exception when others then
insert into errors_inserttable values (thenameid, thekeyid, theotherdataid, theotherdata2, thestartdate, theenddate, thereferencedate);
end;
begin
insert into map_inserttable (thenameid, datasourceid, thename) values (thenameid, 1, thename);
exception when others then
insert into errors_map_inserttable (thenameid, datasourceid, thename) values (thenameid, 1, thename);
end;
end proc_add_entry;

linq to sql stored procedure call fails

I have recursive stored procedure on SQL Server. I'm using Linq-to-SQL generated classes, and drag & drop procedure to this class.
Other procedures are working fine, but this procedures fails with exception:
"System.Void" not allowed return
type. invalid operation exception
Stored procedure:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[delListEnterprise]
#pin_list_enterprise_id numeric(38,0),
#cCriteria varchar(250) = null,
#iLevel int = 0
AS
begin
set nocount on
declare
#cSQL varchar(255),
#cChildCriteria varchar(255),
#iLevelNew int
IF #iLevel = 0
BEGIN
set #cCriteria='parent_list_enterprise_id='+cast(#pin_list_enterprise_id as varchar(30));
delete from list_enterprise where list_enterprise_id=#pin_list_enterprise_id;
Create Table #tblCascadeDelete (CallLevel int NOT NULL, PKValue int NOT NULL)
END
SET #cSQL = 'INSERT INTO #tblCascadeDelete ( CallLevel, PKValue ) SELECT ' + Convert(varchar(3), #iLevel) + ' As CallLevel, list_enterprise_id As PKValue FROM list_enterprise WHERE ' + #cCriteria
EXEC (#cSQL)
IF ##RowCount > 0
BEGIN
SET #cChildCriteria = '[parent_list_enterprise_id] IN (SELECT [PKValue] FROM #tblCascadeDelete Where [CallLevel] = ' + Convert(varchar(3), #iLevel) + ')'
SET #iLevelNew = #iLevel + 1
EXEC delListEnterprise null,#cChildCriteria, #iLevelNew
END
SET #cSQL = 'DELETE FROM list_enterprise WHERE ' + #cCriteria
EXEC (#cSQL)
IF #iLevel = 0
BEGIN
Drop Table #tblCascadeDelete
END
ELSE
DELETE FROM #tblCascadeDelete WHERE CallLevel = #iLevel
end
It works, if I run it in SQL Server Management Studio.
Just add a return/output parameter to the stored proc. I suspect this is a Linq2SQL limitation (or no-one thought of it).