What leads to this strange SQL behavior? - sql

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;

Related

How to create a function to get error number of a query

When I execute this query select 1/0 in sql server, it will show this Message:
Msg 8134, Level 16, State 1, Line 16 Divide by zero error encountered.
I need a function to get Number of error. for this example output of my function should be 8134
how can I create this function to get my required result?
CREATE FUNCTION GetErrorNumber
(
#command NVARCHAR(MAX)
)
RETURNS INT
AS
BEGIN
--function Body
END
If you are going to pass the error message to function, then try this:
CREATE FUNCTION Fun_GetErrorNumber
(
#command NVARCHAR(MAX)
)
RETURNS INT
AS
BEGIN
RETURN CAST(SUBSTRING(#q,PATINDEX('%[0-9]%',#q),PATINDEX('%,%',#q)-PATINDEX('%[0-9]%',#q)) AS INT)
END
But if you want to pass the query to function, you should take note that executing command inside Function is not valid and you need to use stored procedure instead. Like this:
CREATE PROCEDURE Pro_Execute
(
#command NVARCHAR(MAX) ,
#num INT OUTPUT
)
AS
BEGIN
BEGIN TRY
EXEC(#command);
SET #num = 0;
END TRY
BEGIN CATCH
SET #num = ERROR_NUMBER();
END CATCH;
END;
And call it like this:
DECLARE #res INT
EXEC Pro_Execute 'Select 1/0' ,#num = #res output
SELECT #res
The above SP will get the command and execute it. If non-fatal error occurred while executing the command, error number will be returned.
Reading this article may help you decorate your solution in better way
If I understand the question, you want to parse the error message and get the number between Msg and ,.
This seems like a simple string manipulation to me:
CREATE FUNCTION GetErrorNumber
(
#command nvarchar(max)
)
RETURNS int
AS
BEGIN
SELECT CAST(SUBSTRING(#command, 5, CHARINDEX(',', #command, 5) - 5) as int)
END
Though I do agree with juergen d - simply using the built in ERROR_NUMBER() seems better in most cases.
As described in this answer, You can't.
When you want to check your command you need to execute that command that it is impossible in body of a function.

Main Stored Procedure to Execute another Proc depending on Input and output in a CSV

I have two procedures which basically give an Table output. I have the third Procedure which basically calls these two procedures and gives the output in an csv file format.
Can anyone help me in building this the right way. Below is something I am trying to do:
Each of the two Procedures gives out an output with like around 100k rows, I want to capture that and want to give the output here from the Main procedure in a csv file.(Please let me know if you need more info)
Create PROC MAIN
#InputParam int
AS
Begin
Set NOCOUNT ON;
BEGIN TRY
IF #InputParam not in (1,2)
BEGIN
Print 'Error Message'
END
Else
begin
IF #InputParam=1
BEGIN
Exec StoredProc1
Print 'Stored Procedure StoredProc1 ended at '+Convert(Varchar(25),GETDATE(),21);
End
Else
Begin
Print 'StoredProc1 does not exist'
END
IF #InputParam=2
BEGIN
Exec StoredProc2
Print 'Stored Procedure StoredProc2 ended at '+Convert(Varchar(25),GETDATE(),21);
End
Else
BEGIN
Print 'StoredProc2 does not exist'
END
END -- This is END for ELSE loop
END TRY
Begin Catch
Print 'Input Validation Catch Block with # '+ Convert(Varchar(5),ERROR_NUMBER())+' Msg: '+ERROR_MESSAGE();
End Catch
END
What you may want to try is to declare a table vairable in your main stored procedure. This table needs to match the output from the sub procedures exactly. The syntax is:
DECLARE #Temp TABLE(
Filed1 INT,
Field2 VARCHAR(100),
etc
)
Then you can execute your sub stored procedure and insert into the table you defined, like this:
INSERT INTO #Temp
EXEC StoredProc1
And finally, select from the #Temp
SELECT * FROM #Temp
You will obviously need to fit your exact requirements into this, but it should assist I hope
You cannot output to a CSV directly from a Stored Procedure without some form of code, so that really depends what you are developing with (ie what will call the stored procedure). You could perhaps build a SQL Job to output to CSV.

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

SQL Create Procedure Abort Logic

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

TSQL - How to use GO inside of a BEGIN .. END block?

I am generating a script for automatically migrating changes from multiple development databases to staging/production. Basically, it takes a bunch of change-scripts, and merges them into a single script, wrapping each script in a IF whatever BEGIN ... END statement.
However, some of the scripts require a GO statement so that, for instance, the SQL parser knows about a new column after it's created.
ALTER TABLE dbo.EMPLOYEE
ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO -- Necessary, or next line will generate "Unknown column: EMP_IS_ADMIN"
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
However, once I wrap that in an IF block:
IF whatever
BEGIN
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
END
It fails because I am sending a BEGIN with no matching END. However, if I remove the GO it complains again about an unknown column.
Is there any way to create and update the same column within a single IF block?
I had the same problem and finally managed to solve it using SET NOEXEC.
IF not whatever
BEGIN
SET NOEXEC ON;
END
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
SET NOEXEC OFF;
GO is not SQL - it is simply a batch separator used in some MS SQL tools.
If you don't use that, you need to ensure the statements are executed separately - either in different batches or by using dynamic SQL for the population (thanks #gbn):
IF whatever
BEGIN
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL;
EXEC ('UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever')
END
You could try sp_executesql, splitting the contents between each GO statement into a separate string to be executed, as demonstrated in the example below. Also, there is a #statementNo variable to track which statement is being executed for easy debugging where an exception occurred. The line numbers will be relative to the beginning of the relevant statement number that caused the error.
BEGIN TRAN
DECLARE #statementNo INT
BEGIN TRY
IF 1=1
BEGIN
SET #statementNo = 1
EXEC sp_executesql
N' ALTER TABLE dbo.EMPLOYEE
ADD COLUMN EMP_IS_ADMIN BIT NOT NULL'
SET #statementNo = 2
EXEC sp_executesql
N' UPDATE dbo.EMPLOYEE
SET EMP_IS_ADMIN = 1'
SET #statementNo = 3
EXEC sp_executesql
N' UPDATE dbo.EMPLOYEE
SET EMP_IS_ADMIN = 1x'
END
END TRY
BEGIN CATCH
PRINT 'Error occurred on line ' + cast(ERROR_LINE() as varchar(10))
+ ' of ' + 'statement # ' + cast(#statementNo as varchar(10))
+ ': ' + ERROR_MESSAGE()
-- error occurred, so rollback the transaction
ROLLBACK
END CATCH
-- if we were successful, we should still have a transaction, so commit it
IF ##TRANCOUNT > 0
COMMIT
You can also easily execute multi-line statements, as demonstrated in the example above, by simply wrapping them in single quotes ('). Don't forget to escape any single quotes contained inside the string with a double single-quote ('') when generating the scripts.
You can enclose the statements in BEGIN and END instead of the GO inbetween
IF COL_LENGTH('Employees','EMP_IS_ADMIN') IS NULL --Column does not exist
BEGIN
BEGIN
ALTER TABLE dbo.Employees ADD EMP_IS_ADMIN BIT
END
BEGIN
UPDATE EMPLOYEES SET EMP_IS_ADMIN = 0
END
END
(Tested on Northwind database)
Edit: (Probably tested on SQL2012)
I ultimately got it to work by replacing every instance of GO on its own line with
END
GO
---Automatic replacement of GO keyword, need to recheck IF conditional:
IF whatever
BEGIN
This is greatly preferable to wrapping every group of statements in a string, but is still far from ideal. If anyone finds a better solution, post it and I'll accept it instead.
You may try this solution:
if exists(
SELECT...
)
BEGIN
PRINT 'NOT RUN'
RETURN
END
--if upper code not true
ALTER...
GO
UPDATE...
GO
I have used RAISERROR in the past for this
IF NOT whatever BEGIN
RAISERROR('YOU''RE ALL SET, and sorry for the error!', 20, -1) WITH LOG
END
ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
You can incorporate a GOTO and LABEL statements to skip over code, thus leaving the GO keywords intact.