How can I debug an SQL Server data issue that I can't seem to reproduce in any test environment? - sql-server

I have a stored procedure in production that does 2 things. It updates one table and then inserts a record into another. The first step (the update) seems to occur but we've found instances by examining the data where the second step did not occur. I have looked at the data and confirmed that it is not a data issue. I've confirmed that the queries return the appropriate data in order to ensure that the queries complete and in normal circumstances both should execute. I don't know if perhaps there is some sort of performance issue ... or blocking issue that is occurring on the second step that prevents that step from occurring.
The error handling for the stored procedure is as follows.
BEGIN TRY
BEGIN TRANSACTION;
-- perform update to data
-- insert record into second table.
IF ( ##ERROR = 0 AND ##TRANCOUNT > 0 )
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ( ##TRANCOUNT > 0 )
BEGIN
ROLLBACK TRANSACTION;
END
DECLARE #WebSafeErrorId INT;
EXEC dbo.spErrorInsert #WebSafeErrorId OUTPUT, 'Proc';
-- Reraise the error back to the client.
IF ( #WebSafeErrorId != 0 )
BEGIN
DECLARE #Error VARCHAR(20);
SET #Error = CAST( #WebSafeErrorId AS VARCHAR(20) );
RAISERROR( #Error, 11, 1 );
END
ELSE
BEGIN
RAISERROR( 'An error has occurred but there is no error to log.', 11, 1 );
END
END CATCH;
Surely if an error occurred in this procedure that cause the insert to not occur it would be logged and then raised. The code for spErrorInsert is below ...
CREATE PROCEDURE [dbo].[spErrorInsert]
#ReturnErrorId INT OUTPUT
, #ErrorSourceType VARCHAR(4) = NULL
, #ParentErrorId INT = NULL
, #StackTrace VARCHAR(MAX) = NULL
AS
SET NOCOUNT ON;
--SET XACT_ABORT ON;
-- Will indicate an error was not logged.
SET #ReturnErrorID = 0;
DECLARE
#ErrorSource VARCHAR(200)
, #ErrorMessage VARCHAR(MAX)
, #ComposedErrorMessage VARCHAR(MAX)
, #ErrorLine INT
, #ErrorSeverity INT
, #ErrorState INT
, #ErrorNumber INT;
SET #ErrorSource = ERROR_PROCEDURE();
SET #ErrorMessage = ERROR_MESSAGE();
SET #ErrorLine = ERROR_LINE();
SET #ErrorSeverity = ERROR_SEVERITY();
SET #ErrorState = ERROR_STATE();
SET #ErrorNumber = ERROR_NUMBER();
SET #ComposedErrorMessage = 'Message: Error occurred in procedure ' + CAST( #ErrorSource AS VARCHAR(MAX) )
+ ' on line ' + CAST( #ErrorLine AS VARCHAR(MAX) )
+ '. Error: ' + #ErrorMessage;
BEGIN TRY
INSERT INTO Errors(
ParentId
, ErrorSourceType
, ErrorSource
, [Message]
, [LineNo]
, Severity
, Stacktrace
, ts)
VALUES (#ParentErrorId
, #ErrorSourceType --#ErrorSourceType --- NOTE: move this into a parameter ...
, #ErrorSource
, #ComposedErrorMessage
, #ErrorLine
, #ErrorState
, #Stacktrace
, GETDATE()
);
SET #ReturnErrorId = SCOPE_IDENTITY();
END TRY
BEGIN CATCH
RAISERROR( 'An error has occurred but there is no error to log.', 11, 1 );
END CATCH;
I don't know if maybe there is a way to get a snapshot of what's going on the database at a specific time when a certain procedure is called ... I'm not sure how to determine if something isn't happening when it should? Are there any tools that I can make use of or sql features that I don't know about???

If you want to monitor the database, SQL Profiler is a good place to start but it is going to be deprecated.
Extended events are much more capable and I would suggest reading about those if you are really interested in monitoring what's going on.
As a thought, if your procedure code is using the same data to update the row as it is to insert to the other table, consider using OUTPUT.
Update table set col1 = 'value'
OUTPUT inserted.col INTO othertable
Where col3 = stuff
OUTPUT and OUTPUT INTO
Or if this is for some sort of Audit or Log table, you can use DELETED.col1
That will be the original value prior to it being updated. Note that INSERTED will return the value that you are updating or inserting, it's just called INSERTED for both.

If you have a copy of Visual Studio, try it. It allows you to step through stored procedures.

The approach I would try is to firstly take a copy of this procedure and comment out the try/catch. I have found that tsql does not raise errors if the error generating code is within a try/catch block - I guess this is the sql equivalent of an exception being handled by the catch clause.
I use a table in my database to keep a permanent record of errors as they occur (a trick I learned doing embedded programming)
The errors table creation code is :
CREATE TABLE dbo.errors (
id smallint NOT NULL IDENTITY(1, 1) PRIMARY KEY,
errordesc nvarchar (max) NOT NULL,
dateandtime smalldatetime NOT NULL, -- Date and time of last occurance
errorcount int NOT NULL) ;
My stored procedure for adding a record into the error table is:
CREATE PROCEDURE jc_diagnostics.jpwsp0005_adderrordescription(
#Errordescription nvarchar( max ))
AS
BEGIN
DECLARE
#Id smallint = 0 ,
#Currentcount int = 0;
IF((#Errordescription IS NULL) OR ( #Errordescription = ''))
BEGIN
SET #Errordescription = 'Error description missing';
END;
SELECT #Id = ( SELECT TOP ( 1 ) id
FROM jc_diagnostics.errors
WHERE errordesc = #Errordescription );
IF(#Id IS NOT NULL)
BEGIN
SELECT #Currentcount = (SELECT errorcount
FROM jc_diagnostics.errors
WHERE id = #Id );
SET #Currentcount = #Currentcount + 1;
UPDATE jc_diagnostics.errors
SET errorcount = #Currentcount
WHERE id = #Id;
UPDATE jc_diagnostics.errors
SET dateandtime = CONVERT(smalldatetime , GETDATE())
WHERE id = #Id;
END;
ELSE
BEGIN
--new entry
INSERT INTO jc_diagnostics.errors( errordesc ,
dateandtime ,
errorcount )
VALUES( #Errordescription ,
CONVERT(smalldatetime , GETDATE()) ,
1 );
END;
IF(#Id IS NULL)
BEGIN
SET #Id = SCOPE_IDENTITY( );
END;
RETURN #Id;
END;
The calling code when an error occurs is:
Declare #Failuredesc nvarchar(max) = 'Description of error';
EXEC #Retval = jc_diagnostics.jpwsp0005_adderrordescription #Failuredesc;
The return value #Retval contains the id of the record in the error table so you can look it up
Finally I would create some code to continuously call your procedure until an error is declared. You can then inspect the error table and see if this throws light on your problem.
Hope this helps.
Jude

Logically thinking - because you declare transaction before these 2 steps, any error would result in rollback of both transactions. So most likely there is no error at all here. I would suggest inspect your queries again as it seems that the problem could be in them rather than anywhere else. Please post the entire code if you like more suggestions.
Regards
Roman

Related

Cannot see expected PRINT or RAISERROR output when a later error is raised

I'm getting an error message in a stored procedure, saying that I can't insert a NULL value into a table, when I should be getting errors earlier in the code if the value is null.
Here's the relevant part of the stored procedure:
CREATE PROCEDURE [dbo].[udp_AddUpdateStaffVariable]
-- Add the parameters for the stored procedure here
#StaffID int=null,
#VariableTypeID int,
#VariableIntValue int=null,
#VariableVarcharValue varchar(max)=null,
#VariableDatetimeValue datetime=null,
#VariableDecimalValue decimal=null
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
BEGIN TRY
DECLARE #PrintOutput varchar(150)
SET #PrintOutput = '#StaffID = ' + CASE WHEN #StaffID = NULL THEN 'Null' ELSE CONVERT(varchar(20), #StaffID) END
RAISERROR (#PrintOutput, 10, 1) WITH NOWAIT
IF (#StaffID = NULL) -- If the staffid of the current user was not supplied, find it in the Staff table
BEGIN
DECLARE #CurrentUser nvarchar(255) = SUSER_SNAME();
SELECT #StaffID = [StaffID] FROM [dbo].[Staff] WHERE [UserName] = #CurrentUser;
SET #PrintOutput = '#StaffID = ' + CASE WHEN #StaffID = NULL THEN 'Null' ELSE CONVERT(varchar(20), #StaffID) END
RAISERROR (#PrintOutput, 10, 1) WITH NOWAIT
IF #StaffID = NULL -- raise error if staffid wasn't found
BEGIN
RAISERROR (50001 --error number
, 16 -- severity
, 1 --state
, #CurrentUser -- parameter
)
END
END
-- Get the variable data type (used to determine where the variable is stored)
DECLARE #VarDataTypeDesc varchar(20)
DECLARE #StaffVarID int
SELECT #VarDataTypeDesc = dt.[StaffVariableDataType]
FROM [list].[DataTypes] dt INNER JOIN [list].[StaffVariableTypes] svt ON dt.DataTypeID = svt.DataTypeID
WHERE svt.VariableTypeID = #VariableTypeID
-- update or add the staff variable
IF EXISTS (SELECT 1 FROM [dbo].[StaffVariables] WHERE StaffID = #StaffID AND [VariableTypeID] = #VariableTypeID) -- update
BEGIN
IF #VarDataTypeDesc = 'int'
BEGIN -- only update here - other data types are updated further down
UPDATE [dbo].[StaffVariables] SET VariableIntValue = #VariableIntValue WHERE StaffID = #StaffID AND VariableTypeID = #VariableTypeID
END
ELSE -- StaffVariableID is only needed if the variable type is not int
BEGIN
SELECT #StaffVarID = StaffVariableID FROM [dbo].[StaffVariables] WHERE StaffID = #StaffID AND [VariableTypeID] = #VariableTypeID
END
END
ELSE -- insert
BEGIN
IF #VarDataTypeDesc = 'int'
BEGIN
INSERT INTO [dbo].[StaffVariables] (StaffID, VariableTypeID, VariableIntValue)
VALUES (#StaffID, #VariableTypeID, #VariableIntValue)
END
ELSE -- StaffVariableID is only needed if the variable type is not int
BEGIN
DECLARE #StaffVarIDTbl table(ID int)
INSERT INTO [dbo].[StaffVariables] (StaffID, VariableTypeID, VariableIntValue)
OUTPUT INSERTED.[StaffVariableID] INTO #StaffVarIDTbl
VALUES (#StaffID, #VariableTypeID, #VariableIntValue)
SELECT #StaffVarID = ID FROM #StaffVarIDTbl
END
END
-- Cutting out the section where I deal with other variable types besides int here - not relevant to this problem
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH;
END
Here's the test procedure run code:
DECLARE #return_value int
EXEC #return_value = [dbo].[udp_AddUpdateStaffVariable]
#VariableTypeID = 1,
#VariableIntValue = 10
SELECT 'Return Value' = #return_value
GO
...and here's the response:
Msg 50000, Level 16, State 2, Procedure dbo.udp_AddUpdateStaffVariable, Line 130 [Batch Start Line 2]
Cannot insert the value NULL into column 'StaffID', table 'SnippingDbName.dbo.StaffVariables'; column does not allow nulls. INSERT fails.
(1 row affected)
Completion time: 2020-06-01T21:17:08.2049072-05:00
So... here's the question. The error seems to indicate that it either never ran the whole, if #StaffID = NULL portion of the code, or it did, and didn't find the StaffID and set the #StaffID variable. But if that were the case, why can't I see the results of my earlier RAISERROR statements?
I initially tried PRINT and switched to RAISERROR when PRINT wasn't working.
SQL Server 2017 Developer Edition, SSMS 15.0.18183.0
It was a syntax error, that people commenting on the question figured out. IF (#StaffID = NULL) should have been, IF (#StaffID IS NULL) Fixing that in all places in the procedure fixed the problem, and altering my test Staff record so UserName doesn't match SUSER_SNAME() resulted in the expected error.

SQL Server: how to generate serial number by dynamic SQL

create procedure test
(#TABLE_NAME varchar(20))
as
declare #lastval varchar(10)
set #lastval = right('000000000' + convert(varchar(10),
(select IsNull(max(Serialno), 0) + 1
from #TABLE_NAME)), 10)
return #lastval
end
Now tell me how I could compose or form dynamic SQL with above SQL where I will pass table name to store procedure when call that stored procedure?
How to return #lastval value to its calling environment?
How to call stored procedure test from another stored procedure where I will store the return value ?
Guide me with sample code.
Genearlly, it's best to use an IDENTITY or a SEQUENCE to assign serial numbers. A zero-padded string or other formatting requirements could be added to the table as a computed column based on the underlying serial integer or formatted in the app code. However, both IDENTITY and SEQUENCE may have gaps, such as due to rollbacks or SQL Server service restart.
In cases where an unbroken sequence of serial values is required by business, one can maintan the last assigned values in a table and assign values transactionally. Below is an example that returns the value using an OUTPUT parameter. Although the proc in your question uses the stored proc RETURN value for this purpose, that should only be used to indicate success or failure, not return data.
CREATE TABLE dbo.TableSerialNumber(
TableName sysname NOT NULL
CONSTRAINT PK_SerialNumber PRIMARY KEY
, SerialNumber int NOT NULL
, FormatString nvarchar(20) NULL
);
GO
INSERT INTO dbo.TableSerialNumber VALUES('Invoice', 0, '0000000000');
INSERT INTO dbo.TableSerialNumber VALUES('PurchaseOrder', 0, '0000000000');
GO
CREATE PROC dbo.GetNextSerialNumberForTable
#TableName sysname
, #FormattedSerialNumber varchar(10) OUTPUT
AS
SET NOCOUNT ON;
DECLARE
#SerialNumber int
, #FormatString nvarchar(20);
UPDATE dbo.TableSerialNumber
SET
#SerialNumber = SerialNumber += 1
, #FormatString = FormatString
WHERE TableName = #TableName;
IF ##ROWCOUNT = 0
RAISERROR('Table %s does not exist in dbo.TableSerialNumber', 16, 1, #TableName);
SET #FormattedSerialNumber = CAST(FORMAT(#SerialNumber, #FormatString) AS varchar(10));
GO
--example usage
CREATE PROC dbo.InsertInvoice
#InvoiceData int
AS
SET XACT_ABORT ON;
DECLARE #InvoiceNumber varchar(10);
BEGIN TRY
BEGIN TRAN;
EXECUTE dbo.GetNextSerialNumberForTable
#TableName = N'Invoice'
, #FormattedSerialNumber = #InvoiceNumber OUTPUT;
INSERT INTO dbo.Invoice (InvoiceID, InvoiceData)
VALUES(#InvoiceNumber, #InvoiceData);
SELECT #InvoiceNumber AS InvoiceNumber;
COMMIT;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
GO

Can I have two stored procs running on the same message queue for SQL Server Service Broker?

I have a queue that fills up with multiple types of messages and I want to have two stored procedures running on that same queue, one for each message type.
I don't want one that checks for multiple types.
The issue is, I can't see how to define two in the queue definition:
CREATE QUEUE MyQueue WITH ACTIVATION
(
STATUS = ON,
-- How have two?
PROCEDURE_NAME = [my_listener_proc],
MAX_QUEUE_READERS = 4,
EXECUTE AS SELF
)
GO
A comment above has the right of it - you only get one activation procedure. But you can put logic in that proc to do other things based on the message type. Here's a slightly redacted version of one that I have in production:
ALTER PROCEDURE [repl].[myActivation]
AS
BEGIN
DECLARE
#message_type nvarchar(256),
#message XML,
#rc INT,
#queuing_order BIGINT;
DECLARE #messages TABLE (
[message_type] sysname,
[message] XML
);
SET NOCOUNT ON;
WHILE(1=1)
BEGIN
WAITFOR(
RECEIVE TOP (1000)
[message_type_name],
CAST([message_body] AS XML)
FROM [repl].[myQueue]
INTO #messages
), TIMEOUT 5000;
IF (##rowCount = 0)
BREAK;
ELSE
BEGIN
DECLARE [messages] CURSOR FAST_FORWARD LOCAL
FOR
SELECT [message], [message_type]
FROM #messages;
OPEN [messages];
WHILE(1=1)
BEGIN
FETCH NEXT FROM [messages]
INTO #message, #message_type;
IF (##fetch_Status <> 0)
BREAK;
BEGIN TRY
IF (#message_type = 'A')
BEGIN
EXEC #rc = [repl].[ProcessAMessage]
#message = #message;
END
ELSE IF (#message_type = 'B')
BEGIN
EXEC #rc = [repl].[ProcessBMessage]
#message = #message;
END
IF (#rc <> 0)
BEGIN
INSERT INTO [repl].[DeadLetters]
( [Payload], [MessageType] )
VALUES ( #message, #message_type );
END
END TRY
BEGIN CATCH
INSERT INTO [repl].[DeadLetters]
( [Payload], [MessageType] )
VALUES ( #message, #message_type );
END CATCH
END
CLOSE [messages];
DEALLOCATE [messages];
DELETE #messages;
END
END
END
GO
There's a fair bit going on there (aside from routing to different procs based on the message type) including:
Receiving multiple messages at a time so as not to lock the conversation too often
Taking a couple of steps to make sure that the activation procedure doesn't roll back/error out. Poison messages can wreck your day.
I don't use transactions in mine because there's another process that ensures that the effects of the message are applied. So, it doesn't matter much if any given message makes it (as the fact that it doesn't will be detected relatively quickly).

Return Resultset from SQL Server to VB.NET application

I need to return a resultset consisting of database errors from a SQL Server stored procedure's CATCH clause but I'm stuck with it. Do I need to use cursors to return resultset and if so, then what is the type declaration for the OUTPUT parameter in my .NET application? I tried Object and Variant but did not work.
I also tried the simple way just using a SELECT statement to return and it works with one stored procedure but not with another as thus in my CATCH clause:
while (#I <= #count)
begin
BEGIN TRY
-- delete all previous rows inserted in #customerRow for previous counts #I
delete from #customerRow
-- this is inserting the current row that we want to save in database
insert into #customerRow
SELECT
[id],[firstname], [lastname], [street], [city],
[phone],[mobile],[fax], [email], [companyName],
[licence],[brn], [vat], [companyStreet], [companyCity], [status]
FROM
(SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS rownumber,
[id], [firstname], [lastname], [street], [city],
[phone], [mobile], [fax], [email], [companyName],
[licence], [brn], [vat], [companyStreet], [companyCity], [status]
FROM
#registerDetails) AS foo
WHERE
rownumber = #I
-- this stored procedure handles the saving of the current customer row just defined above
-- if there is any error from that sproc, it will jump to CATCH block
--save the error message in the temp table and continue
--with the next customer row in the while loop.
exec dbo.sp_SaveCustomer #customerRow
END TRY
BEGIN CATCH
IF ##TranCount = 0
-- Transaction started in procedure.
-- Roll back complete transaction.
ROLLBACK TRANSACTION;
if XACT_STATE()= -1 rollback transaction
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE() + ' ' + (select firstname from #registerDetails where id=#I)
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE()
INSERT INTO #registrationResults (error,id)
SELECT #ErrorMessage, #I
END CATCH
set #I = #I +1
end
COMMIT TRANSACTION registerTran
select * from #registrationResults
The above works with one stored procedure when I call it in my vb.net code as :
ta.Fill(registrationErrors, clientDetailsDT)
where registrationErrors and clientDetailsDT are strongly typed data tables.
This one does not :
begin catch
IF ##TranCount > 0 or XACT_STATE()= -1 ROLLBACK TRANSACTION;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
DECLARE #ErrorLine INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
SELECT #ErrorLine = ERROR_Line();
****ERROR -- THIS SELECT WAS MESSING ALL UP as it was this select that was being returned to the .NET and not the select of the desired #temp table after, hence returning 0 resultset as this select was EMPTY. !!
select status_indicator from InsertInvoiceTriggerData where session_GUID = guid**
delete from InsertInvoiceTriggerData where session_GUID = #guid**
INSERT INTO #registrationResults (error,id)
SELECT #ErrorMessage, NULL
select * from #registrationResults
end catch
Any suggestions how to return resultsets?
I haven't seen your database code, but in my experience the very first error caught by catch means that the entire transaction has to be rolled back. Apart from other things, it also implies that I never have more than 1 error to return in any given situation.
As such, I use 2 scalar output parameters in my stored procedures, that is:
#Error int = null output,
#Message nvarchar(2048) = null output
And I can retrieve them just like any other output variables.
UPD: Even after you have added some code, I still fail to understand what is your problem, exactly. However, I see several problems with your code, so I'll point them out and chances are, one of them will solve the problem. I am commenting only the first snippet, since the last one is too incomplete.
You should have been started the outermost transaction somewhere before the loop. If not, the code will fail.
If I guessed correctly, you implemented all savepoint logic inside the dbo.sp_SaveCustomer stored proc. If not, the whole discussion is pointless, since there are no save tran or rollback #savepoint statements in the code you have shown.
The first catch statement - IF ##TranCount = 0 ROLLBACK TRANSACTION is all wrong. If the condition is successful, it will result in error trying to rollback nonexistent transaction. Should not be here if you rely on savepoints.
The next after it should result in unconditional break:
if XACT_STATE()= -1 begin
rollback transaction;
break;
end;
The rest of your catch code can be replaced with this:
INSERT INTO #registrationResults (error, id)
SELECT error_message() + ' ' + firstname, id
from #registerDetails where id=#I;
Also, never use temp tables for this purpose, because rollback will affect them as well. Always use table variables for this, they are non-transactional (just like any other variable).
The commit should be conditional, because you may end up at this point with no transaction to commit:
if ##trancount > 0 commit tran;
There is no point in specifying savepoint name in the commit statement, it only leads to confusion (though isn't considered an error). Also, there should not any savepoint in this module (unless you have defined it before the loop).
I suspect that's just the tip of the iceberg, since I have no idea what actually happens inside the dbo.SaveCustomer stored procedure.
UPD2: Here is a sample of my VB.NET code which I use to receive recordsets from stored procedures:
Private Function SearchObjectsBase( _
SearchMode As penum_SEARCH_MODE, SearchCriteria As String
) As DataSet
Dim Cmd As DbCommand, Pr As DbParameter, dda As DbDataAdapter
' Initialise returning dataset object
SearchObjectsBase = New DataSet()
Cmd = MyBase.CreateCommand(String.Format("dbo.{0}", SearchMode))
With Cmd
' Parameter definitions
Pr = .CreateParameter()
With Pr
.ParameterName = "#SearchCriteria"
.DbType = DbType.Xml
.Value = SearchCriteria
End With
.Parameters.Add(Pr)
' Create data adapter to use its Fill() method
dda = DbProviderFactories.GetFactory(.Connection).CreateDataAdapter()
' Assign the prepared DbCommand as a select method for the adapter
dda.SelectCommand = Cmd
' A single resultset is expected here
dda.Fill(SearchObjectsBase)
End With
' Set error vars and get rid of it
Call MyBase.SetErrorOutput(Cmd)
' Check for errors and, if any, discard the dataset
If MyBase.ErrorNumber <> 0 Then SearchObjectsBase.Clear()
End Function
I use .NET 4.5, which has a very nice method to automatically select the most appropriate data adapter based on the actual connection.
And here is a call of this function:
Dim XDoc As New XElement("Criteria"), DS As DataSet = Nothing, DT As DataTable
...
DS = .SearchPatients(XDoc.ToString(SaveOptions.None))
' Assign datasource to a grid
Me.dgr_Search.DataSource = DS.Tables.Item(0)
Here, SearchPatients() is a wrapper on top of the SearchObjectsBase().

Stored Procedure Transaction

I have never used a Transaction, Commit and Rollback before and now I need to use one. I have checked around online, etc for examples to make sure that I am in fact using this correctly but I am still not sure if I have coded this correct. I am hoping someone can review and advise me if this seems correct.
Basically I have 2 databases for an application. One is an archive - meaning data that is no longer going to be manipulated by the users will be moved to this DB. But in the event they ever need it, I will move the needed data back to the main database for use. My stored proc is below:
CREATE PROCEDURE [dbo].[spReopenClosed]
(
#Return_Message VARCHAR(1024) = '' OUT,
#IID uniqueidentifier,
#OpenDate smalldatetime,
#ReopenedBy uniqueidentifier
)
AS
BEGIN
SET NOCOUNT ON;
/******************************
* Variable Declarations
*******************************/
DECLARE #ErrorCode int
/******************************
* Initialize Variables
*******************************/
SELECT #ErrorCode = ##ERROR
IF #ErrorCode = 0
BEGIN TRANSACTION
/****************************************************************************
* Step 1
* Copy the Closed from the Archive
****************************************************************************/
INSERT INTO OPS.dbo.SM_T_In
SELECT
FROM OPS_ARCHIVE.Archive.SM_T_In W
WHERE W.GUID = #IID
AND W.OpenDate = #OpenDate
IF #ErrorCode <> 0
BEGIN
-- Rollback the Transaction
ROLLBACK
RAISERROR ('Error in Copying from the archive', 16, 1)
RETURN
END
/****************************************************************************
* Step 2
* copy the notes
****************************************************************************/
INSERT INTO OPS.dbo.SM_T_Notes
SELECT
FROM OPS_ARCHIVE.Archive.SM_T_Notes W
WHERE W.GUID = #IID
IF #ErrorCode <> 0
BEGIN
-- Rollback the Transaction
ROLLBACK
RAISERROR ('Error in copying the notes', 16, 1)
RETURN
END
/****************************************************************************
* Step 3
* Delete the from the Archive - this will also delete the notes
****************************************************************************/
DELETE
FROM OPS_ARCHIVE.Archive.SM_T_In
WHERE OPS_ARCHIVE.Archive.SM_T_In.GUID = #IID
IF #ErrorCode <> 0
BEGIN
-- Rollback the Transaction
ROLLBACK
RAISERROR ('Error in deleting the items from the Archive', 16, 1)
RETURN
END
COMMIT
BEGIN
SELECT #ErrorCode = ##ERROR
IF #ErrorCode = 0
SELECT #Return_Message = 'All data was moved over'
END
/*************************************
* Get the Error Message for ##Error
*************************************/
IF #ErrorCode <> 0
BEGIN
SELECT #Return_Message = [Description] -- Return the SQL Server error
FROM master.dbo.SYSMESSAGES
WHERE error = #ErrorCode
END
/*************************************
* Return from the Stored Procedure
*************************************/
RETURN #ErrorCode -- =0 if success, <>0 if failure
END
I have two inserts that move the data from 2 tables from the Archive database. If those inserts are successful, then I will delete the data from the Archive DB. I would appreciate any feedback on this, I need to make sure that I am doing this properly.
Thanks
Oh well i rewrite quickly your SP using the concept TRY CATCH and the TRANSACTION as you requested but i didnt check it.
This code will work in SQL 2005/2008
Let me know if this feedback can be useful for you
CREATE PROCEDURE [dbo].[spReopenClosed]
(
#Return_Message VARCHAR(1024) = '' OUT,
#IID uniqueidentifier,
#OpenDate smalldatetime,
#ReopenedBy uniqueidentifier
)
AS
SET NOCOUNT ON;
/******************************
* Variable Declarations
*******************************/
DECLARE #ErrorCode int
DECLARE #ErrorStep varchar(200)
/******************************
* Initialize Variables
*******************************/
SELECT #ErrorCode = ##ERROR
BEGIN TRY
BEGIN TRAN
/****************************************************************************
* Step 1
* Copy the Closed from the Archive
****************************************************************************/
SELECT #ErrorStep = 'Error in Copying from the archive';
INSERT INTO OPS.dbo.SM_T_In
SELECT *
FROM OPS_ARCHIVE.Archive.SM_T_In
WHERE GUID = #IID
AND W.OpenDate = #OpenDate
/****************************************************************************
* Step 2
* copy the notes
****************************************************************************/
SELECT #ErrorStep = 'Error in copying the notes'
INSERT INTO OPS.dbo.SM_T_Notes
SELECT *
FROM OPS_ARCHIVE.Archive.SM_T_Notes
WHERE GUID = #IID
/****************************************************************************
* Step 3
* Delete the from the Archive - this will also delete the notes
****************************************************************************/
SELECT #ErrorStep = 'Error in deleting the items from the Archive'
DELETE
FROM OPS_ARCHIVE.Archive.SM_T_In
WHERE OPS_ARCHIVE.Archive.SM_T_In.GUID = #IID
COMMIT TRAN
SELECT #ErrorCode = 0, #Return_Message = 'All data was moved over'
/*************************************
* Return from the Stored Procedure
*************************************/
RETURN #ErrorCode -- =0 if success, <>0 if failure
END TRY
BEGIN CATCH
/*************************************
* Get the Error Message for ##Error
*************************************/
IF ##TRANCOUNT > 0 ROLLBACK
SELECT #ErrorCode = ERROR_NUMBER()
, #Return_Message = #ErrorStep + ' '
+ cast(ERROR_NUMBER() as varchar(20)) + ' line: '
+ cast(ERROR_LINE() as varchar(20)) + ' '
+ ERROR_MESSAGE() + ' > '
+ ERROR_PROCEDURE()
/*************************************
* Return from the Stored Procedure
*************************************/
RETURN #ErrorCode -- =0 if success, <>0 if failure
END CATCH
First, databases are fairly reliable. And if they fail, you have a bigger problem than handling individual transactions. So my feedback would be that you have too much error checking for a simple transaction. A failing insert is such an unusual event that you normally wouldn't write code to handle it.
Second, this code won't actually "catch" errors:
IF #ErrorCode <> 0
An error in the SQL statement will abort the stored procedure and return to the client. You'd have to try ... catch to actually handle an error in a stored procedure.
Third, I try to avoid raiserr. It can do unexpected things both on the server and the client side. Instead, consider using an output parameter to return error information to the client program.

Resources