I'm trying to understand what specifically is happening in SQL that means the following syntax is not allowed (and I'm finding it hard to search for):
IF (OBJECT_ID('..sp_cake', 'P') is not null)
ALTER PROC sp_cake
as
select 1
I would expect the ALTER to be valid because T-SQL is wrapping it up in its own BEGIN-END block and nothing bad could happen with the rest of the script block.
This is what T-SQL is doing, wrapping everything up and keeping it cleanly separated:
IF (OBJECT_ID('..sp_cake', 'P') is not null)
BEGIN
ALTER PROC [dbo].[sp_cake]
as
BEGIN
select 1
END
END
And these examples would be the simplest expression of what I think I'm doing (and these are syntactically correct)
IF (OBJECT_ID('..sp_cake', 'P') is not null)
select 1
IF (OBJECT_ID('..sp_cake', 'P') is null)
select 1 -- i.e. this works and 1 is the output
I have read that the CREATE or ALTER must be the first statement in a query block, but I don't understand why.
I know that I can get around this problem by either:
creating a dummy sproc and then altering it outside of an IF block, or;
creating a string of the entire sproc and executing it as a statement;
but I don't see why it is not valid to test for existence and then ALTER.
I'm not sure what you mean by this statement:
I have read that the CREATE or ALTER must be the first statement in a
query block, but I don't understand why.
You are correct that these need to be the first statements in a batch. That is a property of the T-SQL language -- not something whose cause needs to be understood but something that you need to know to use the language properly. Typically, the structure to do what you want in SQL Server is:
IF (OBJECT_ID('..sp_cake', 'P') is not null)
BEGIN
DROP PROCEDURE dbo.sp_code
END;
GO
CREATE PROC [dbo].[sp_cake] as
BEGIN
select 1
END;
I do agree that it would be nice to have a create procedure if not exists or create or alter procedure. Getting that functionality requires lobbying Microsoft.
Related
I have a a number of sp's that create a temporary table #TempData with various fields. Within these sp's I call some processing sp that operates on #TempData. Temp data processing depends on sp input parameters. SP code is:
CREATE PROCEDURE [dbo].[tempdata_proc]
#ID int,
#NeedAvg tinyint = 0
AS
BEGIN
SET NOCOUNT ON;
if #NeedAvg = 1
Update #TempData set AvgValue = 1
Update #TempData set Value = -1;
END
Then, this sp is called in outer sp with the following code:
USE [BN]
--GO
--DBCC FREEPROCCACHE;
GO
Create table #TempData
(
tele_time datetime
, Value float
--, AvgValue float
)
Create clustered index IXTemp on #TempData(tele_time);
insert into #TempData(tele_time, Value ) values( GETDATE(), 50 ); --sample data
declare
#ID int,
#UpdAvg int;
select
#ID = 1000,
#UpdAvg = 1
;
Exec dbo.tempdata_proc #ID, #UpdAvg ;
select * from #TempData;
drop table #TempData
This code throws an error: Msg 207, Level 16, State 1, Procedure tempdata_proc, Line 8: Invalid column name "AvgValue".
But if only I uncomment declaration AvgValue float - everything works OK.
The question: is there any workaround letting the stored proc code remain the same and providing a tip to the optimizer - skip this because AvgValue column will not be used by the sp due to params passed.
Dynamic SQL is not a welcomed solution BTW. Using alternative to #TempData tablename is undesireable solution according to existing tsql code (huge modifications necessary for that).
Tried SET FMTONLY, tempdb.tempdb.sys.columns, try-catch wrapping without any success.
The way that stored procedures are processed is split into two parts - one part, checking for syntactical correctness, is performed at the time that the stored procedure is created or altered. The remaining part of compilation is deferred until the point in time at which the store procedure is executed. This is referred to as Deferred Name Resolution and allows a stored procedure to include references to tables (not just limited to temp tables) that do not exist at the point in time that the procedure is created.
Unfortunately, when it comes to the point in time that the procedure is executed, it needs to be able to compile all of the individual statements, and it's at this time that it will discover that the table exists but that the column doesn't - and so at this time, it will generate an error and refuse to run the procedure.
The T-SQL language is unfortunately a very simplistic compiler, and doesn't take runtime control flow into account when attempting to perform the compilation. It doesn't analyse the control flow or attempt to defer the compilation in conditional paths - it just fails the compilation because the column doesn't (at this time) exist.
Unfortunately, there aren't any mechanisms built in to SQL Server to control this behaviour - this is the behaviour you get, and anything that addresses it is going to be perceived as a workaround - as evidenced already by the (valid) suggestions in the comments - the two main ways to deal with it are to use dynamic SQL or to ensure that the temp table always contains all columns required.
One way to workaround your concerns about maintenance if you go down the "all uses of the temp table should have all columns" is to move the column definitions into a separate stored procedure, that can then augment the temporary table with all of the required columns - something like:
create procedure S_TT_Init
as
alter table #TT add Column1 int not null
alter table #TT add Column2 varchar(9) null
go
create procedure S_TT_Consumer
as
insert into #TT(Column1,Column2) values (9,'abc')
go
create procedure S_TT_User
as
create table #TT (tmp int null)
exec S_TT_Init
insert into #TT(Column1) values (8)
exec S_TT_Consumer
select Column1 from #TT
go
exec S_TT_User
Which produces the output 8 and 9. You'd put your temp table definition in S_TT_Init, S_TT_Consumer is the inner query that multiple stored procedures call, and S_TT_User is an example of one such stored procedure.
Create the table with the column initially. If you're populating the TEMP table with SPROC output just make it an IDENTITY INT (1,1) so the columns line up with your output.
Then drop the column and re-add it as the appropriate data type later on in the SPROC.
The only (or maybe best) way i can thing off beyond dynamic SQL is using checks for database structure.
if exists (Select 1 From tempdb.sys.columns Where object_id=OBJECT_ID('tempdb.dbo.#TTT') and name = 'AvgValue')
begin
--do something AvgValue related
end
maybe create a simple function that takes table name and column or only column if its always #TempTable and retursn 1/0 if the column exists, would be useful in the long run i think
if dbo.TempTableHasField('AvgValue')=1
begin
-- do something AvgValue related
end
EDIT1: Dang, you are right, sorry about that, i was sure i had ... this.... :( let me thing a bit more
When making a SQL script to create a trigger on a table, I wanted to check that the trigger doesn't already exist before I create it. Otherwise the script cannot be run multiple times.
So I added a statement to first check whether the trigger exists. After adding that statement, the CREATE TRIGGER statement no longer works.
IF NOT EXISTS (SELECT name FROM sysobjects
WHERE name = 'tr_MyTable1_INSERT' AND type = 'TR')
BEGIN
CREATE TRIGGER tr_MyTable1_INSERT
ON MyTable1
AFTER INSERT
AS
BEGIN
...
END
END
GO
This gives:
Msg 156, Level 15, State 1, Line 5
Incorrect syntax near the keyword 'TRIGGER'.
The solution would be to drop the existing trigger and then create the new one:
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'tr_MyTable1_INSERT' AND type = 'TR')
DROP TRIGGER tr_MyTable1_INSERT
GO
CREATE TRIGGER tr_MyTable1_INSERT
ON MyTable1
AFTER INSERT
AS
BEGIN
...
END
GO
My question is: why is the first example failing? What is so wrong with checking the trigger exists?
Certain statements need to be the first in a batch (as in, group of statements separated by GO ).
Quote:
CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE SCHEMA, CREATE TRIGGER, and CREATE VIEW statements cannot be combined with other statements in a batch. The CREATE statement must start the batch. All other statements that follow in that batch will be interpreted as part of the definition of the first CREATE statement.
It's simply one of the rules for SQL Server batches (see):
http://msdn.microsoft.com/en-us/library/ms175502.aspx
Otherwise you could change an object, say a table, and then refer to the change in the same batch, before the change was actually made.
Schema changes should always be seperate batch calls...I am guessing they do it to gaurantee your SELECT will succeed, if you modify schema in the same batch they may not be able to gaurantee that. Just a guess...
Microsoft SQL Server seems to check column name validity, but not table name validity when defining stored procedures. If it detects that a referenced table name exists currently, it validates the column names in a statement against the columns in that table. So, for example, this will run OK:
CREATE PROCEDURE [dbo].[MyProcedure]
AS
BEGIN
SELECT
Col1, Col2, Col3
FROM
NonExistentTable
END
GO
... as will this:
CREATE PROCEDURE [dbo].[MyProcedure]
AS
BEGIN
SELECT
ExistentCol1, ExistentCol2, ExistentCol3
FROM
ExistentTable
END
GO
... but this fails, with 'Invalid column name':
CREATE PROCEDURE [dbo].[MyProcedure]
AS
BEGIN
SELECT
NonExistentCol1, NonExistentCol2, NonExistentCol3
FROM
ExistentTable
END
GO
Why does SQL Server check columns, but not tables, for existence? Surely it's inconsistent; it should do both, or neither. It's useful for us to be able to define SPs which may refer to tables AND/OR columns which don't exist in the schema yet, so is there a way to turn off SQL Server's checking of column existence in tables which currently exist?
This is called deferred name resolution.
There is no way of turning it off. You can use dynamic SQL or (a nasty hack!) add a reference to a non existent table so that compilation of that statement is deferred.
CREATE PROCEDURE [dbo].[MyProcedure]
AS
BEGIN
CREATE TABLE #Dummy (c int)
SELECT
NonExistantCol1, NonExistantCol2, NonExistantCol3
FROM
ExistantTable
WHERE NOT EXISTS(SELECT * FROM #Dummy)
DROP TABLE #Dummy
END
GO
This article in MSDN should answer your question.
From the article:
When a stored procedure is executed for the first time, the query
processor reads the text of the stored procedure from the
sys.sql_modules catalog view and checks that the names of the objects
used by the procedure are present. This process is called deferred
name resolution because table objects referenced by the stored
procedure need not exist when the stored procedure is created, but
only when it is executed.
I'm developing a simple database architecture in VisualParadigm and lately ran over next code excerpt.
IF EXISTS (SELECT * FROM sys.objects
WHERE object_id = OBJECT_ID(N'getType') AND type in (N'P', N'PC'))
DROP PROCEDURE getType;
Next goes my stored procedure:
CREATE PROCEDURE getType #typeId int
AS
SELECT * FROM type t WHERE t.type_id = #typeId;
Can anyone explain what does it mean/do (the former one)?
P.S.: It would be great, if you may also check for any syntax errors as I'm totally new to SQL Server and stored procedures.
The IF EXISTS part first checks if a stored procedure with the same name exists. if it does it drops it before creating it. Without this check you'd get an error that the stored procedure already exists.
Adding to Raj's post, there is no means to do an "upsert" with stored procedures. The Create Procedure statement must be the first statement of the batch. Thus, the following would not work:
If Not Exists(Select 1 From sys.procedures Where Name = 'getType')
Create Procedure...
Else
Alter Procedure...
The only means to "update" a procedure and not have it throw an error if it already exists is to drop it and re-create it.
ADDITION
To address a specific question you made in comments, sys.objects is a catalog view which contains a list of all objects (tables, constraints, columns, indexes etc. Every "thing" in the database) of which procedures are one of them. Thus, this is checking whether the procedure object (based on filters on type) exist. The primary key of the sys.objects table/view is object_id which is an integer. In your example, they are using the OBJECT_ID function to find the id of the object getType and determine if it is a procedure. (It probably would have been safe to just use If OBJECT_ID(N'getType') is not null but just in case there is another object with that name that isn't a procedure, they added the check on the object type).
a CREATE PROCEDURE getType... will fail if the object already exists. by including the IF EXISTS... code which will drop the object if it exists first, you eliminate the error and the CREATE... will run.
The OBJECT_ID(N'getType') just returns the numeric ID of an object named N'getType' and the AND type in (N'P', N'PC')) makes sure that the object is a P=stored procedure or a PC=Assembly (CLR).
You could try using something like this, so you can use ALTER to keep permissions (DROP+CREATE removes any):
BEGIN TRY EXEC ('CREATE PROCEDURE YourProcedureName AS SELECT ''ERROR'' RETURN 999') END TRY BEGIN CATCH END CATCH
GO
ALTER PROCEDURE YourProcedureName
AS
SELECT 'WORKS!2'
GO
EXEC YourProcedureName
OUTPUT:
-------
WORKS!2
(1 row(s) affected)
It looks like this is part of a script to generate your DB. The first statement looks to see if your sproc called "getType" exists. If it does then it will drop it. Why? Because the next line is going to create it.
The only other way it could create it and make sure it matches the current version of your procedure is to change create to alter. That would make for longer code because it would have to list the sproc twice. Or it could generate dynamic sql which is not nearly as clean.
It's doing a drop and recreate
if a database object called getType exists:
WHERE object_id = OBJECT_ID(N'getType')
and it's a stored procedure:
AND type in (N'P', N'PC'))
then drop it before adding your stored procedure:
DROP PROCEDURE getType;
The first query drops procedure if it exists. The second creates a new procedure that takes integer parameter and returns resultset.
I'm after a simple stored procedure to drop tables. Here's my first attempt:
CREATE PROC bsp_susf_DeleteTable (#TableName char)
AS
IF EXISTS (SELECT name FROM sysobjects WHERE name = #TableName)
BEGIN
DROP TABLE #TableName
END
When I parse this in MS Query Analyser I get the following error:
Server: Msg 170, Level 15, State 1, Procedure bsp_susf_DeleteTable, Line 6
Line 6: Incorrect syntax near '#TableName'.
Which kind of makes sense because the normal SQL for a single table would be:
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'tbl_XYZ')
BEGIN
DROP TABLE tbl_XYZ
END
Note the first instance of tbl_XYZ (in the WHERE clause) has single quotes around it, while the second instance in the DROP statement does not. If I use a variable (#TableName) then I don't get to make this distinction.
So can a stored procedure be created to do this? Or do I have to copy the IF EXISTS ... everywhere?
You should be able to use dynamic sql:
declare #sql varchar(max)
if exists (select name from sysobjects where name = #TableName)
BEGIN
set #sql = 'drop table ' + #TableName
exec(#sql)
END
Hope this helps.
Update: Yes, you could make #sql smaller, this was just a quick example. Also note other comments about SQL Injection Attacks
Personally I would be very wary of doing this. If you feel you need it for administrative purposes, please make sure the rights to execute this are extremely limited. Further, I would have the proc copy the table name and the date and the user executing it to a logging table. That way at least you will know who dropped the wrong table. You may want other protections as well. For instance you may want to specify certain tables that cannot be dropped ever using this proc.
Further this will not work on all tables in all cases. You cannot drop a table that has a foreign key associated with it.
Under no circumstances would I allow a user or anyone not the database admin to execute this proc. If you havea a system design where users can drop tables, there is most likely something drastically wrong with your design and it should be rethought.
Also, do not use this proc unless you have a really, really good backup schedule in place and experience restoring from backups.
You'll have to use EXEC to execute that query as a string. In other words, when you pass in the table name, define a varchar and assign the query and tablename, then exec the variable you created.
Edit: HOWEVER, I don't recommend that because someone could pass in sql rather than a TableName and cause all kinds of wonderful problems. See Sql injection for more information.
Your best bet is to create a parameterized query on the client side for this. For example, in C# I would do something like:
// EDIT 2: on second thought, ignore this code; it probably won't work
SqlCommand sc = new SqlCommand();
sc.Connection = someConnection;
sc.CommandType = Command.Text;
sc.CommandText = "drop table #tablename";
sc.Parameters.AddWithValue("#tablename", "the_table_name");
sc.ExecuteNonQuery();