SQL Create Procedure Abort Logic - sql

Good afternoon all -
I have a temporary stored procedure that needs to be run as a hotfix in several places and I'd like to abort the creation and compilation of the SP if the version of the application is not exactly what I enter. I have the basic idea working but I'd like the messages to come out without all the schema issues from trying to compile the SP.
Here is basically what I have:
IF EXISTS ... DROP PROCEDURE
SELECT TOP 1 Version INTO #CurrentVersion FROM Application_Version ORDER BY UpdateDate DESC
IF NOT EXISTS (SELECT 1 FROM #CurrentVersion WHERE Version = 10)
RAISERROR ('This is for U10 only. Check the application version.', 20, 1) WITH LOG
CREATE PROCEDURE ....
The RAISERROR causes the SP to not end up in the DB and I do get an error but I also end up with schema errors due to schema changes in the past. Due to the SP needing to be the first statement in the batch I can't use IF / ELSE and NOEXEC yields the same results as RAISERROR (without the error).
Any ideas for what can be done to get all of the same results as above without the SP checking the schema if it hits the RAISERROR so I don't end up with a bunch of extra messages reported?

What you want is the error condition to stop execution of the script, which is possible in SQLCMD mode of the query editor with a simple :on error exit:
:on error exit
SELECT TOP 1 Version INTO #CurrentVersion FROM Application_Version ORDER BY UpdateDate DESC
IF NOT EXISTS (SELECT 1 FROM #CurrentVersion WHERE Version = 10)
RAISERROR ('This is for U10 only. Check the application version.', 16, 1);
go
IF EXISTS ... DROP PROCEDURE
go
CREATE PROCEDURE ....
...
go
With this in place there is no need to raise severity 20. Severity 16 is enough, which will take care of the ERRORLOG issue you complain.

The RETURN statement will exit out of a SP. When doing error checking, put a BEGIN and END after your IF statement and after your RAISERROR put a RETURN statement.

There are a couple of options here. My approach would be as follows, because I feel that it provides the best flow:
IF EXISTS ... DROP PROCEDURE
IF EXISTS (SELECT * FROM Application_Version WHERE Version = 10)
BEGIN
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'CREATE PROCEDURE blablabla AS
BEGIN
-- Your Procedure HERE
END'
EXEC sp_executesql #sql
END ELSE
RAISERROR ('This is for U10 only. Check the application version.', 20, 1) WITH LOG

Related

Stored procedure with a GO statement to free up resources

I have a stored procedure in a SQL Server 2019 database that does various checks for me when I run. This stored procedure contains several SELECT type statements - if the condition is true it will send an email.
This all works great when I run the whole stored procedure. As none of the statements are linked, I thought it would be more efficient if I ran each statement then had a GO command to release any memory or resources from previous SELECT.
But I can't do this as the GO does not work when I run me ALTER script on the stored procedure. It cuts off any lines after the GO. Is there any alternative T-SQL command that I can use similar to GO to free up resources?
ALTER PROCEDURE [dbo].[uspDataChecker]
AS
SET NOCOUNT OFF;
IF ((SELECT COUNT(DISTINCT GroupName) FROM tblSurveys) <> 5)
BEGIN
EXEC uspWebmasterSendEmail 'FAILED GroupName Check', 'GroupName does not contain 5 rows as expected'
END
-- I would like to put a "GO" statement here but when I
-- execute my ALTER script it chops off the below check
IF ((SELECT COUNT(*) FROM tblAccounts) <> 0)
BEGIN
EXEC uspWebmasterSendEmail 'FAILED Accounts Check', 'tblAccounts has rwos'
END

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)?

Rollback transaction if parameter name too long

I'm using the SimpleMembershipProvider that you get out of the box when you create an new .NET MVC application and I wanted to allow an admin user the ability to add roles. For learning purposes, (and I don't know if this is the correct way to do it) I wanted to limit the length of the description of the RoleName column to 15 characters, so I wrote the transaction:
create proc spInsertRole
(
#roleName varchar(50)
--really shouldn't be 50, but that's
--how I originally wrote my code
)
as
begin
set nocount on
begin try
begin tran
insert into dbo.webpages_Roles(RoleName)
values (#roleName)
commit transaction
end try
begin catch
select ERROR_MESSAGE() as ErrorMessage
if len(#roleName) > 15
rollback transaction
end catch
end
There is not a check constraint on the table for length of RoleName. This proc will compile but it will also let me add a RoleName of greater than 15 characters. What am I missing and what is the best way to do this?
You should check the length before you run the insert statement. By putting the length check in the catch block, you are telling the program to only check the length and roll back if there is some other error condition.
(My T-SQL is rusty and I don't have a database to test on so please verify before accepting. Also, given these changes, you probably don't need transactions anymore.)
create proc spInsertRole
(
#roleName varchar(50)
--really shouldn't be 50, but that's
--how I originally wrote my code
)
as
begin
set nocount on
begin try
begin tran
-- length check moved here. Raise error when > 15.
-- Severity (argument 2) needs to be higher than 10
-- to stop execution and trigger the catch block.
-- State (argument 3) is an arbitrary value between 0 and 255.
if len(#roleName) > 15
raiserror('Role name is too long.', 11, 5)
insert into dbo.webpages_Roles(RoleName)
values (#roleName)
commit transaction
end try
begin catch
select ERROR_MESSAGE() as ErrorMessage
-- length check was here. program will always roll back now.
rollback transaction
end catch
end
See RAISERROR for more information about how that works.

T-SQL install / upgrade script as a transaction

I'm trying to write a single T-SQL script which will upgrade a system which is currently in deployment. The script will contain a mixture of:
New tables
New columns on existing tables
New functions
New stored procedures
Changes to stored procedures
New views
etc.
As it's a reasonably large upgrade I want the script to rollback if a single part of it fails. I have an outline of my attempted code below:
DECLARE #upgrade NVARCHAR(32);
SELECT #upgrade = 'my upgrade';
BEGIN TRANSACTION #upgrade
BEGIN
PRINT 'Starting';
BEGIN TRY
CREATE TABLE x ( --blah...
);
ALTER TABLE y --blah...
);
CREATE PROCEDURE z AS BEGIN ( --blah...
END
GO --> this is causing trouble!
CREATE FUNCTION a ( --blah...
END TRY
BEGIN CATCH
PRINT 'Error with transaction. Code: ' + ##ERROR + '; Message: ' + ERROR_MESSAGE();
ROLLBACK TRANSACTION #upgrade;
PRINT 'Rollback complete';
RETURN;
END TRY
END
PRINT 'Upgrade successful';
COMMIT TRANSACTION #upgrade;
GO
Note - I know some of the syntax is not perfect - I'm having to re-key the code
It seems as though I can't put Stored Procedures into a transaction block. Is there a reason for this? Is it because of the use of the word GO? If so, how can I put SPs into a transaction block? What are the limitations as to what can go into a transaction block? Or, what would be a better alternative to what I'm trying to achieve?
Thanks
As Thomas Haratyk said in his answer, your issue was the "go". However, you can have as many batches in a transaction as you want. It's the try/catch that doesn't like this. Here's a simple proof-of-concept:
begin tran
go
select 1
go
select 2
go
rollback
begin try
select 1
go
select 2
go
end try
begin catch
select 1
end catch
Remove the GO and create your procedure by using dynamic sql or it will fail.
EXEC ('create procedure z
as
begin
print "hello world"
end')
GO is not a SQL keyword, it is a batch separator. So it cannot be included into a transaction.
Please refer to those topics for further information :
sql error:'CREATE/ALTER PROCEDURE' must be the first statement in a query batch?
Using "GO" within a transaction
http://msdn.microsoft.com/en-us/library/ms188037.aspx

What leads to this strange SQL behavior?

Running SQL 2005 X64.
First, create the following stored proc on a database:
CREATE PROCEDURE dbo.Test
#Value int = null
AS
BEGIN
IF (IsNull(#Value, '') = '')
SELECT '*I am NULL!*'
ELSE
SELECT 'I am ' + CONVERT(varchar(20), #Value)
END
Try executing the above proc as follows, and you get the result below:
EXEC dbo.Test
I am NULL!
Now, ALTER the proc so that the EXEC statement is part of the sproc itself:
ALTER PROCEDURE dbo.Test
#Value int = null
AS
BEGIN
IF (IsNull(#Value, '') = '')
SELECT 'I am NULL!'
ELSE
SELECT 'I am ' + CONVERT(varchar(20), #Value)
END
EXEC dbo.Test
If you execute it now, you get...
I am NULL!
I am NULL!
I am NULL!
...ad infinitum until the output breaks with this error:
Msg 217, Level 16, State 1, Procedure
Test, Line 16 Maximum stored
procedure, function, trigger, or view
nesting level exceeded (limit 32).
Ignoring for the moment that this isn't at all a standard practice and that most likely someone would do something like this only by accident, could someone please provide some low-level insight on what SQL 2005 is "thinking" when the second incarnation of this proc is executed?
Your code is behaving as expected. The procedure is calling itself recursively.
If you do not want that, try this:
ALTER PROCEDURE dbo.Test
#Value int = null
AS
BEGIN
IF (IsNull(#Value, '') = '')
SELECT 'I am NULL!'
ELSE
SELECT 'I am ' + CONVERT(varchar(20), #Value)
END
GO
EXEC dbo.Test
If you do want to use recursion, you have to define a base case (AKA "exit condition") which will make stored procedure exit the recursion stack.
The recursion is because everything is being considered part of the proc, not just the BEGIN to END block.
From my comment:
No great mystery. It's going to treat everything until the next GO or other indicator of the end of the batch as part of the proc. The outermost BEGIN and END are not required syntax as part of the procedure.
It's called recursion, as others have mentioned.
You can avoid it as #Adrian has shown (using 'GO' to prevent the sp from calling itself), or you can also escape it using a control structure...
Here's a sample / experiment you can study if you want to learn about recursion: http://msdn.microsoft.com/en-us/library/aa175801.aspx
It allows for 32 nested calls. and with every Exec call you are nesting it forever. So think recursively.
Exec proc
Select
Exec
Select
exec
Infinitely.
once it reaches the 32nd nested calls it hits its maximum and says whoa i can not continue.
My reading of the question is not "Why is my SP exhibiting recursion?" but "Why is recursion limited to 32 and how do i get around that?"
I had completely forgotten that SQL Recursion dies on you like that.
An answer I just worked out is to make use of TRY-CATCH and ##NestLevel. The below is a small demonstrator rig. In your code it would be far better to have an independent end condition, for example running out of chunks to process.
My code has been mangled by the editor, I have no time to work round your issues.
BEGIN TRY DROP PROCEDURE dbo.Nester END TRY BEGIN CATCH END catch
GO
CREATE PROCEDURE dbo.Nester #NestLevel INT = 0 OUT
AS
BEGIN
DECLARE #MaxActNestLevel INT = 40;
SELECT #NestLevel += 1;
PRINT (CONVERT(sysname, ##NestLevel) + ' ' + CONVERT(sysname, #NestLevel))
IF #NestLevel < #MaxActNestLevel
BEGIN TRY
EXEC dbo.Nester #NestLevel OUT
END TRY
BEGIN CATCH
PRINT 'Catch Block'
PRINT (ERROR_NUMBER())
SELECT #NestLevel += 1;
IF ##NestLevel < 30 --AND ERROR_NUMBER() = 217
BEGIN
EXEC dbo.Nester #NestLevel OUT
END
ELSE
THROW
END CATCH
END
GO
EXEC dbo.Nester;