Why we should use QUOTENAME function? - sql-server

I get acquainted with QUOTENAME function. But I don't understand for what I can use it? Why it is so widely used?
select quotename('[abc]') -- '[[abc]]]'
select quotename('abc') -- '[abc]'
select '[' + 'abc' +']' -- why it is not so good as previous?

Imagine the following script is scheduled to run regularly to clean up tables in schemas other than the dbo schema.
DECLARE #TABLE_SCHEMA SYSNAME,
#TABLE_NAME SYSNAME
DECLARE #C1 AS CURSOR;
SET #C1 = CURSOR FAST_FORWARD
FOR SELECT TABLE_SCHEMA,
TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA <> 'dbo'
OPEN #C1;
FETCH NEXT FROM #C1 INTO #TABLE_SCHEMA, #TABLE_NAME;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT 'DROP TABLE [' + #TABLE_SCHEMA + '].[' + #TABLE_NAME + ']';
EXEC ('DROP TABLE [' + #TABLE_SCHEMA + '].[' + #TABLE_NAME + ']');
FETCH NEXT FROM #C1 INTO #TABLE_SCHEMA, #TABLE_NAME;
END
If you create the following and run the script then all works as expected despite using the manual string concatenation approach. The table foo.bar is dropped.
CREATE SCHEMA foo
CREATE TABLE foo.bar(x int)
Now create the following and try
CREATE TABLE foo.[[abc]]](x int)
The script fails with an error
DROP TABLE [foo].[[abc]]
Msg 105, Level 15, State 1, Line 6
Unclosed quotation mark after the character string '[abc]'.
Msg 102, Level 15, State 1, Line 6
Incorrect syntax near '[abc]'.
So not using QUOTENAME has caused the script to fail. The closing bracket was not escaped properly by doubling it up. The correct syntax should have been
DROP TABLE [foo].[[abc]]]
Even worse news is that a malicious developer has come to know of the script's existence. They execute the following just before the script is scheduled to run.
CREATE TABLE [User supplied name]];
EXEC sp_addsrvrolemember 'SomeDomain\user2216', 'sysadmin'; --]
(
x int
)
Now the script that ends up being executed is
DROP TABLE [foo].[User supplied name];
EXEC sp_addsrvrolemember 'SomeDomain\user2216', 'sysadmin'; --]
The ] was interpreted as closing off the object name and the remainder as a new statement. The first statement returned an error message but not a scope terminating one and the second one was still executed. By not using QUOTENAME you have opened yourself up to SQL injection and the developer has successfully escalated their privileges

QUOTENAME can be used when generating dynamic SQL statements. It'll indeed place your column name between brackets but will also escape characters which would break your quoted column name and could cause SQL injection.
For example:
SELECT QUOTENAME('abc[]def');
Will return:
[abc[]]def]
For more info: QUOTENAME (MSDN)

Related

Get database name dynamically

I am trying to get the database name dynamically but, showing some errors.
What I have tried:
Declare #dbname varchar(40) = (select name from sys.databases where name like '%worksdw2019')
select db_name() as [Database], *
from #dbname.dbo.DimCustomer with (readuncommitted)
How end result should look like
select db_name() as [Database], *
from AdventureWorksdw2019.dbo.dimcustomer
These are the errors I am getting:
Msg 102, Level 15, State 1, Line 3
Incorrect syntax near '.'.
Msg 319, Level 15, State 1, Line 3
Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon.
You can't parameterize database names, so you'll need dynamic SQL.
DECLARE #context nvarchar(1000);
SELECT #context = QUOTENAME(name) + N'.sys.sp_executesql'
from sys.databases
where name like N'%worksdw2019';
-- what if there is more than one?
DECLARE #sql nvarchar(max) = N'select db_name() as [Database], *
from dbo.DimCustomer;';
EXECUTE #context #sql;
Uh, with (readuncommitted) -> why? sqlblog.org/nolock
You have invalid SQL written.
You cannot write use a variable like that in SQL Server. If the database name is going to be variable as seen in your example, you will need to execute dynamic SQL using the EXEC command such as this example below:
Declare #dbname varchar(40) = (select name from sys.databases where name like '%HelloWorld')
EXEC('select db_name() as [Database], * from ' + #dbname + '.dbo.DimCustomer with (readuncommitted);')
-- you may additionally need to escape your database name
-- if you are using special characters in your name

Is it possible to issue CREATE statements using sp_executesql with parameters?

I'm trying to dynamically create triggers, but ran into a confusing issue around using sp_executesql and passing parameters into the dynamic SQL. The following simple test case works:
DECLARE #tableName sysname = 'MyTable';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT 1
END';
EXEC sp_executesql #sql
However, I want to be able to use #tableName (and other values) as variables within the script, so I passed it along to the sp_executesql call:
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
When running the above, I get an error:
Msg 156, Level 15, State 1, Line 2
Incorrect syntax near the keyword 'TRIGGER'.
After trying I few things, I've discovered that even if I don't use #tableName in the dynamic SQL at all, I still get this error. And I also get this error trying to create a PROCEDURE (except, obviously, the message is Incorrect syntax near the keyword 'PROCEDURE'.)
Since the SQL runs fine either directly or when not supplying parameters to sp_executesql, this seems like I'm running into a true limitation in the SQL engine, but I don't see it documented anywhere. Does anyone know if there is a way to accept to a dynamic CREATE script, or at least have insight into the underlying limitation that's being run into?
Update
I can add a PRINT statement, and get the below SQL, which is valid, and runs successfully (when run directly). I still get the error if there's nothing dynamic in the SQL (it's just a single string with no concatenation).
CREATE TRIGGER TR_ContentItems ON ContentItems FOR INSERT
AS
BEGIN
PRINT #tableName
END
I also get the same error whether using sysname or nvarchar(max) for the parameter.
If you execute your create trigger statement that you said you printed... you will find that it does not work. The print statement in the body of the trigger is trying to output #tablename, but is never defined, so you will get an error:
Must declare the scalar variable "#tableName".
But that is not your main issue. As for why you can't seem to execute a DDL statement with execute_sql with parameters, I couldn't find any documentation to explain why... but your experience and others proves that it's troublesome. I believe this post has a pretty good theory: sp_executesql adds statements to executed dynamic script?
You can however execute dynamic sql with DDL statements using the EXECUTE statement. So what you could do is create a parameterized sp_executesql statement that validates your table name and then creates a dynamic sql string to execute with the EXECUTE statement.
It doesn't look pretty, but it works:
DECLARE #tableName sysname = 'MyTable';
DECLARE #sql nvarchar(max) =
N'
set #tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(#tableName)) --validate table
DECLARE #CreateTriggerSQL as varchar(max) =
''
CREATE TRIGGER '' + QUOTENAME(''TR_'' + #tableName) + '' ON '' + QUOTENAME( #tableName) + '' FOR INSERT
AS
BEGIN
PRINT '''''' + #tableName + ''''''
END
''
print isnull(#CreateTriggerSQL, ''INVALID TABLE'')
exec (#CreateTriggerSQL)
';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName;
You could also convert this into a stored procedure with parameters instead of running sp_executesql if that were more convenient. It looks a bit cleaner:
CREATE PROCEDURE sp_AddTriggerToTable (#TableName AS sysname) AS
set #tableName = (SELECT name FROM sys.tables WHERE OBJECT_ID = OBJECT_ID(#tableName)) --validate table
DECLARE #CreateTriggerSQL as varchar(max) =
'
CREATE TRIGGER ' + QUOTENAME('TR_' + #tableName) + ' ON ' + QUOTENAME( #tableName) + ' FOR INSERT
AS
BEGIN
PRINT ''' + #tableName + '''
END
'
print isnull(#CreateTriggerSQL, 'INVALID TABLE')
exec (#CreateTriggerSQL)
GO
I would strongly caution against using Dynamic SQL with table names. You are setting yourself up for some serious SQL Injection issues. You should validate anything that goes into the #tableName variable.
That said, in your example...
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
... you are trying to input your declared #tableName into the text you're creating for #sql, and then you're trying to pass a parameter through spexecutesql. This makes your #sql invalid when trying to call it.
You can try:
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_'' + #tableName + N'' ON '' + #tableName + N'' FOR INSERT
AS
BEGIN
PRINT #tableName
END';
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
... which will give you the string ...
'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
PRINT #tableName
END'
... which can then accept the parameter you pass through ...
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName ;
Again, I'd use some heavy validation (and white-listing) before passing anything into dynamic SQL that will use a dynamic table name.
NOTE: As noted below, I believe you are limited on DML statements that can be executed with sp_executesql(), and I think parameterization is limited also. And based on your other comments, it doesn't sound like you're really needing a dynamic process but a way to repeat a specific task for a handful of elements. If that's the case, my recommendation is to do it manually with a copy/paste then execute the statements.
Since the SQL runs fine either directly or when not supplying
parameters to sp_executesql, this seems like I'm running into a true
limitation in the SQL engine, but I don't see it documented anywhere.
This behavior is documented, albeit not intuitive. The relevant excerpt from the documentation under the trigger limitations topic:
CREATE TRIGGER must be the first statement in the batch
When you execute a parameterized query, the parameter declarations are counted as being part of the batch. Consequently, a CREATE TRIGGER batch (and other CREATE statements for programmability objects like procs, functions, etc.) cannot be executed as a parameterized query.
The invalid syntax error message you get when you attempt to run CREATE TRIGGER as a parameterized query isn't particularly helpful. Below is an simplified version of your code using the undocumented and unsupported internal parameterized query syntax.
EXECUTE(N'(#tableName sysname = N''MyTable'')CREATE TRIGGER TR_MyTable ON dbo.MyTable FOR INSERT AS');
This at least yields an error calling out the CREATE TRIGGER limitation:
Msg 1050, Level 15, State 1, Line 73 This syntax is only allowed for
parameterized queries. Msg 111, Level 15, State 1, Line 73 'CREATE
TRIGGER' must be the first statement in a query batch.
Similarly executing another parameterized statement with this method runs successfully:
EXECUTE (N'(#tableName sysname = N''MyTable'')PRINT #tableName');
But if you don't actually use the parameter in the batch, an error results
EXECUTE (N'(#tableName sysname = N''MyTable'')PRINT ''done''');
Msg 1050, Level 15, State 1, Line 75 This syntax is only allowed for
parameterized queries.
The bottom line is that you need to build the CREATE TRIGGER statement as a string without parameters and execute the statement as a non-parameterized query to create a trigger.
Is it possible to issue CREATE statements using sp_executesql with
parameters?
Simple answer is "No", you can't
According to MSDN
Generally, parameters are valid only in Data Manipulation Language
(DML) statements, and not in Data Definition Language (DDL) statements
You can check more details about this Statement Parameters
What is the issue?
Parameters are only allowed in place of scalar literals, like quoted strings or dates, or numeric values. You can't parameterise a DDL operation.
What can be done?
I believe that you want to use parameterized sp_executesql is to avoid any SQL Injection Attack. To achieve this for the DDL operations you can do following thing to minimize the possibility of attack.
Use Delimiters : You can use QUOTENAME() for SYSNAME parameters like Trigger Name, Table Names and Column names.
Limiting Permissions : User Account you are using to run the dynamic DDL, should have only limited permission. Like on a
specific schema with only CREATE permission.
Hiding Error Message : Don't throw the actual error to the user. SQL Injection are mainly performed by trial and error approach. If
you hide the actual error message, it will become tough to crack it.
Input Validation : You can always have a function which validates the input string, escape the required characters, check
for specific keywords like DROP.
Any workaround?
If you want to parameterized your statement using sp_executesql, in that case you can get the query to be executed in a OUTPUT variable and run the query in next statement like following.
By this, the first call to sp_executesql will parameterized your query, and the actual execution will be performed by the second call to sp_executesql
For example.
DECLARE #TableName VARCHAR(100) = 'MyTable'
DECLARE #returnStatement NVARCHAR(max);
DECLARE #sql1 NVARCHAR(max)=
N'SELECT #returnStatement = ''CREATE TRIGGER TR_''
+ #TableName + '' ON '' + #TableName + '' FOR INSERT AS BEGIN PRINT 1 END'''
EXEC Sp_executesql
#sql1,
N'#returnStatement VARCHAR(MAX) OUTPUT, #TableName VARCHAR(100)',
#returnStatement output,
#TableName
EXEC Sp_executesql #returnStatement
Is it possible to issue CREATE statements using sp_executesql with
parameters?
The answer is "Yes", but with small adjustment:
USE msdb
DECLARE #tableName sysname = 'sysjobsteps';
DECLARE #sql nvarchar(max) = N'
EXECUTE ('' -- Added nested EXECUTE()
CREATE TRIGGER [TR_'' + #tableName + N''] ON ['' + #tableName + N''] FOR INSERT
AS
BEGIN
PRINT '''''+#tableName+'''''
END''
)' -- End of EXECUTE()
EXEC sp_executesql #sql, N'#tableName sysname', #tableName=#tableName
Adjsutments list:
Extra EXECUTE involved, comment below explains why
Extra square brackets added to make SQL Injections slightly harder
I'm looking for specific (ideally, documented) restrictions of
sp_executesql with parameters and if there are any workarounds for
those specific restrictions (beyond not using parameters)
in this case it is a limitation of DDL commands, not sp_executesql. DDL statements cannot be parametrized using variables. Microsoft documentation says:
Variables can be used only in expressions, not in place of object
names or keywords. To construct dynamic SQL statements, use EXECUTE.
source: DECLARE (Transact-SQL)
Therefore, the solution with EXECUTE is provided by me as a workaround
Personally I hate triggers and try to avoid them most of the time ;)
However if you really, really need this dynamic stuff you should use sp_MSforeachtable and avoid injection (as pointed out by Shawn) at any cost:
EXEC sys.sp_MSforeachtable
#command1 = '
DECLARE #sql NVARCHAR(MAX)
SET #sql = CONCAT(''CREATE TRIGGER TR_''
, REPLACE(REPLACE(REPLACE(''?'', ''[dbo].'', ''''),''['',''''),'']'','''')
, '' ON ? FOR INSERT
AS
BEGIN
PRINT ''''?'''';
END;'');
EXEC sp_executesql #sql;'
, #whereand = ' AND object_id IN (SELECT object_id FROM sys.objects
WHERE name LIKE ''%ContentItems%'')';
If you want to use the parameter as string, add double ' before and after the parameter name
like this :
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
print ''' + #tableName
+''' END';
EXEC sp_executesql #sql
And if you want to use it as table name, use select instead of print ,
like this :
DECLARE #tableName sysname = 'ContentItems';
DECLARE #sql nvarchar(max) = N'
CREATE TRIGGER TR_' + #tableName + N' ON ' + #tableName + N' FOR INSERT
AS
BEGIN
select * from ' + #tableName
+' END';
EXEC sp_executesql #sql

Copy a view definition from one database to another one in SQL Server

I am developping a script that allows me to create all the views present in database1 in another database (database2).
I am trying to do that using a cursor that loop on all the views of database1 and then try to execute the definition of that view on the second database.
Unfortunately it doesn't work. I get the following error:
Syntaxe incorrecte vers 'go'.
Msg 111, Niveau 15, État 1, Ligne 14
'CREATE VIEW' doit être la première instruction d'un traitement de requêtes.
This is my code
declare #database2 varchar(50), #database1 varchar(50)
set #database2 = 'Local'
set #database1 = 'prod'
declare #Query nvarchar(max), #view_definition nvarchar(max), #count int
set #count = 0
declare curseur cursor for SELECT top 1 view_definition FROM prod.information_schema.VIEWS
open curseur
fetch curseur into #view_definition
While ##FETCH_STATUS = 0
Begin
set #count = #count + 1
--Print 'Vue N° ' + cast(#count as varchar) + ':'
set #Query = N'Use ' + #database2 +CHAR(13)+CHAR(10)+ 'go' + #view_definition +CHAR(13)+CHAR(10)+ 'go'
print #Query
exec sp_executesql #Query
fetch curseur into #view_definition
End
Close curseur
deallocate curseur
This code was executed from database1.
However when I execute the result of the 'print #Query' instruction, it works!!
Does anyone can help me to resolve the problem?
Thanks in advance
There are two things here:
You can't USE another database in a stored procedure, although is the other database is on the same server you can use CREATE VIEW databasename.schemaname.viewname (.... If it's on another server, yiu could try setting it up as a linked server and using servername.database.schema.viewname.
sp_executesql expects one statement, and doesn't accept GO to my
knowledge (which is what the error is telling you). You could try to
put in ';' instead (and you don't need the CHAR(13)'s if you do
although they make it easier to read).
In case this helps someone in the future, here's my solution to this problem. The full history of how I came up with it is over at Stored Procedure to Copy Views from Current Database to Another
CREATE PROCEDURE [dbo].[usp_Copy_View_To_Database]
#ViewName SYSNAME, -- The name of the view to copy over
#DatabaseName SYSNAME, -- The name of the database to copy the view to
#overwrite bit = 1 -- Whether to overwrite any existing view
AS
IF DB_ID(#DatabaseName) IS NULL -- Validate the database name exists
BEGIN
RAISERROR('Invalid Destination Database Name passed',16,1)
RETURN
END
SET NOCOUNT ON
IF #overwrite = 1 -- If set to overwrite, try to drop the remote view
BEGIN
DECLARE #DropViewStatement NVARCHAR(MAX) =
'EXEC ' + QUOTENAME(#DatabaseName) + '.sys.sp_executesql N''DROP VIEW IF EXISTS ' + QUOTENAME(#ViewName) + ';'';'
EXEC (#DropViewStatement);
END
-- Extract the saved definition of the view
DECLARE #ViewDefinition NVARCHAR(MAX);
SELECT #ViewDefinition = definition FROM sys.sql_modules WHERE [object_id] = OBJECT_ID(#ViewName);
-- Check for a mismatch between the internal view name and the expected name (TODO: Resolve this automatically?)
IF #ViewDefinition NOT LIKE ('%' + #ViewName + '%')
BEGIN
DECLARE #InternalName NVARCHAR(MAX) = SUBSTRING(#ViewDefinition, 3, CHARINDEX(char(10), #ViewDefinition, 3)-4);
PRINT ('Warning: The view named '+#ViewName+' has an internal definition name that is different ('+#InternalName+'). This may have been caused by renaming the view after it was created. You will have to drop and recreate it with the correct name.')
END
-- Substitute any hard-coded references to the current database with the destination database
SET #ViewDefinition = REPLACE(#ViewDefinition, db_name(), #DatabaseName);
-- Generate the dynamic SQL that will create the view on the remote database
DECLARE #CreateViewStatement NVARCHAR(MAX) =
'EXEC ' + QUOTENAME(#DatabaseName) + '.sys.sp_executesql N''' + REPLACE(#ViewDefinition,'''','''''') + ''';'
--PRINT '#CreateViewStatement: ' + #CreateViewStatement -- Can be used for debugging
-- Execute the create statement
EXEC (#CreateViewStatement);
In short, you need two layers of nested dynamic SQL:
Inner layer to execute the "CREATE VIEW" statement, which must be on its own. This is executed using EXEC SomeDatabase.sys.sp_executesql #CreateViewSQL
One more layer to dynamically specify "SomeDatabase" via a parameter (assuming you require the flexibility of copying it to a database not known at scripting time).
Calling the above stored proc in the inner-loop of the original poster's tentative solution should solve the problem of copying a view to another database.
Note that simply looping over all views might pose a challenge if views depend on one another. There may be some additional complexity involving resolving the dependency tree of views an copying them in the correct order. Alternatively, a "dumb and easy" approach might be to loop over all views, ignore failures, and keep repeating the process until all views have been created.

Give DROP PROCEDURE a parameter

I'm using SqlServer for the first time, and in every single one of our create procedure scripts there is a block of code like below to remove the procedure if it already exists:
IF EXISTS (SELECT *
FROM information_schema.routines
WHERE routine_name = 'SomeProcedureName'
AND routine_type = 'PROCEDURE'
BEGIN
DROP PROCEDURE SomeProcedureName
END
//then the procedure definition
To stop cutting and pasting this boilerplate code in every file I would like to put this code in its own stored procedure so that instead the scripts would look like this:
DropIfRequired('SomeProcedureName')
//then the procedure definition
My attempt at a solution is:
CREATE PROCEDURE DropIfRequired
(
#procedureName varchar
)
AS
IF EXISTS (SELECT * FROM information_schema.routines
WHERE routine_name = #procedureName
AND routine_type = 'PROCEDURE')
BEGIN
DROP PROCEDURE #procedureName
END
But I then get the following error:
Msg 102, Level 15, State 1, Procedure DeleteProcedure, Line 10
Incorrect syntax near '#procedureName'.
Any ideas how to do what I want?
The full answer is:
DECLARE #SQL VARCHAR(8000)
SELECT #SQL = 'USE ' + DB_NAME() + CHAR(10)
SET #SQL = #SQL + 'DROP PROCEDURE ' + #procName
--PRINT #SQL
EXEC(#SQL)
The one given by Andrew will only work if the default database for your login is set to the database you want. When using dynamic sql you get a new database context. So if you do not have a default database set you will execute the command from master.
One thing to note is that in the DropIfRequired procedure, you have defined the procedure name as follows:
CREATE PROCEDURE DropIfRequired
(
#procedureName varchar
)
You need to define a length of the varchar parameter, otherwise SQL will assume a length of one character. Instead, do something like as follows (1000 should be more than enough for most procedure names)
CREATE PROCEDURE DropIfRequired
(
#procedureName varchar(1000)
)
its missing quotes, try adding them in with an exec statement.
EXEC( 'DROP PROCEDURE ''' + #procName + '''') ( all single quotes)

Syntax check all stored procedures?

i want to ensure that all stored procedures are still syntactically valid. (This can happen if someone renames/deletes a table/column).
Right now my solution to check the syntax of all stored procedures is to go into Enterprise Manager, select the first stored procedure in the list, and use the procedure:
Enter
Alt+C
Escape
Escape
Down Arrow
Goto 1
It works, but it's pretty tedious. i'd like a stored procedure called
SyntaxCheckAllStoredProcedures
like the other stored procedure i wrote that does the same thing for views:
RefreshAllViews
For everyone's benefit, RefreshAllViews:
RefreshAllViews.prc
CREATE PROCEDURE dbo.RefreshAllViews AS
-- This sp will refresh all views in the catalog.
-- It enumerates all views, and runs sp_refreshview for each of them
DECLARE abc CURSOR FOR
SELECT TABLE_NAME AS ViewName
FROM INFORMATION_SCHEMA.VIEWS
OPEN abc
DECLARE #ViewName varchar(128)
-- Build select string
DECLARE #SQLString nvarchar(2048)
FETCH NEXT FROM abc
INTO #ViewName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQLString = 'EXECUTE sp_RefreshView '+#ViewName
PRINT #SQLString
EXECUTE sp_ExecuteSQL #SQLString
FETCH NEXT FROM abc
INTO #ViewName
END
CLOSE abc
DEALLOCATE abc
For everyone's benefit, a stored procedure to mark all stored procedure as needing a recompile (marking a stored procedure for recompile will not tell you if it's syntactically valid):
RecompileAllStoredProcedures.prc
CREATE PROCEDURE dbo.RecompileAllStoredProcedures AS
DECLARE abc CURSOR FOR
SELECT ROUTINE_NAME
FROM INFORMATION_SCHEMA.routines
WHERE ROUTINE_TYPE = 'PROCEDURE'
OPEN abc
DECLARE #RoutineName varchar(128)
-- Build select string once
DECLARE #SQLString nvarchar(2048)
FETCH NEXT FROM abc
INTO #RoutineName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQLString = 'EXECUTE sp_recompile '+#RoutineName
PRINT #SQLString
EXECUTE sp_ExecuteSQL #SQLString
FETCH NEXT FROM abc
INTO #RoutineName
END
CLOSE abc
DEALLOCATE abc
For completeness sake, the UpdateAllStatistics procedure. This will update all statistics in the database by doing a full data scan:
RefreshAllStatistics.prc
CREATE PROCEDURE dbo.RefreshAllStatistics AS
EXECUTE sp_msForEachTable 'UPDATE STATISTICS ? WITH FULLSCAN'
You can also do this "in-place" - without getting all the create statements.
In addition to setting NOEXEC ON, you will also need to set your favorite SHOWPLAN_* ON (I use SHOWPLAN_TEXT). Now you can get rid of your step 2 and just execute each procedure you retrieved in step 1.
Here is a sample using an individual stored procedure. You can work it into your favorite loop:
create procedure tests #bob int as
select * from missing_table_or_view
go
set showplan_text on;
go
set noexec on
exec tests
set noexec off
go
set showplan_text off;
go
drop procedure tests
go
The above sample should generate the following output:
Msg 208, Level 16, State 1, Procedure tests, Line 2
Invalid object name 'missing_table_or_view'.
The check suggested by KenJ is definitely the best one, since the recreate/alter-approaches does not find all errors. E.g.
impossible execution plans due to query-hints
I even had an SP referencing a non-existing table that went through without the error being detected.
Please find my version that checks all existing SPs at once with KenJ's method below. AFAIK, it will detect every error that will keep the SP from being executed.
--Forces the creation of execution-plans for all sps.
--To achieve this, a temporary SP is created that calls all existing SPs.
--It seems like the simulation of the parameters is not necessary. That makes things a lot easier.
DECLARE #stmt NVARCHAR(MAX) = 'CREATE PROCEDURE pTempCompileTest AS ' + CHAR(13) + CHAR(10)
SELECT #stmt = #stmt + 'EXEC [' + schemas.name + '].[' + procedures.name + '];'
FROM sys.procedures
INNER JOIN sys.schemas ON schemas.schema_id = procedures.schema_id
WHERE schemas.name = 'dbo'
ORDER BY procedures.name
EXEC sp_executesql #stmt
GO
--Here, the real magic happens.
--In order to display as many errors as possible, XACT_ABORT is turned off.
--Unfortunately, for some errors, the execution stops anyway.
SET XACT_ABORT OFF
GO
--Showplan disables the actual execution, but forces t-sql to create execution-plans for every statement.
--This is the core of the whole thing!
SET SHOWPLAN_ALL ON
GO
--You cannot use dynamic SQL in here, since sp_executesql will not be executed, but only show the string passed in in the execution-plan
EXEC pTempCompileTest
GO
SET SHOWPLAN_ALL OFF
GO
SET XACT_ABORT ON
GO
--drop temp sp again
DROP PROCEDURE pTempCompileTest
--If you have any errors in the messages-window now, you should fix these...
If you are using sql 2008 r2 or below then do not use
SET NOEXEC ON
It only checks the syntax and not for potential errors like the existence of tables or columns.
Instead use:
SET FMTONLY ON
it will do a full compile as it tries to return the meta data of the stored procedure.
For 2012 and you will need to use stored procedure:
sp_describe_first_result_set
Also you can do a complete script in Tsql that checks all sp and views, its just a bit of work.
UPDATE
I wrote a complete solution for in tsql that goes through all user defined stored proceedures and checks there syntax. the script is long winded but can be found here http://chocosmith.wordpress.com/2012/12/07/tsql-recompile-all-views-and-stored-proceedures-and-check-for-error/
In addition you might want to consider using Visual Studio Team System 2008 Database Edition which, among other things, does a static verification of all stored procedures in the project on build, thus ensuring that all are consistent with the current schema.
I know this is way old, but I created a slightly different version that actually re-creates all stored procedures, thus throwing errors if they cannot compile. This is something you do not achieve by using the SP_Recompile command.
CREATE PROCEDURE dbo.UTL_ForceSPRecompilation
(
#Verbose BIT = 0
)
AS
BEGIN
--Forces all stored procedures to recompile, thereby checking syntax validity.
DECLARE #SQL NVARCHAR(MAX)
DECLARE #SPName NVARCHAR(255)
DECLARE abc CURSOR FOR
SELECT NAME, OBJECT_DEFINITION(o.[object_id])
FROM sys.objects AS o
WHERE o.[type] = 'P'
ORDER BY o.[name]
OPEN abc
FETCH NEXT FROM abc
INTO #SPName, #SQL
WHILE ##FETCH_STATUS = 0
BEGIN
--This changes "CREATE PROCEDURE" to "ALTER PROCEDURE"
SET #SQL = 'ALTER ' + RIGHT(#SQL, LEN(#SQL) - (CHARINDEX('CREATE', #SQL) + 6))
IF #Verbose <> 0 PRINT #SPName
EXEC(#SQL)
FETCH NEXT FROM abc
INTO #SPName, #SQL
END
CLOSE abc
DEALLOCATE abc
END
I know this is a old question but this is my solution when I could not find any suiting.
I required to validate my stored procedures and views after alot of changes in the database.
Basicly what i wanted was to try to do a ALTER PROCEDURE and ALTER VIEW using the current procedures and view (not actually changing them).
I have written this that works fairly well.
Note! Do not perform on live database, make a copy to validate and then fix the things need fixing. Also sys.sql_modules can be inconsistent so take extra care. I do not use this to actually make the changes, only to check which are not working properly.
DECLARE #scripts TABLE
(
Name NVARCHAR(MAX),
Command NVARCHAR(MAX),
[Type] NVARCHAR(1)
)
DECLARE #name NVARCHAR(MAX), -- Name of procedure or view
#command NVARCHAR(MAX), -- Command or part of command stored in syscomments
#type NVARCHAR(1) -- Procedure or view
INSERT INTO #scripts(Name, Command, [Type])
SELECT P.name, M.definition, 'P' FROM sys.procedures P
JOIN sys.sql_modules M ON P.object_id = M.object_id
INSERT INTO #scripts(Name, Command, [Type])
SELECT V.name, M.definition, 'V' FROM sys.views V
JOIN sys.sql_modules M ON V.object_id = M.object_id
DECLARE curs CURSOR FOR
SELECT Name, Command, [Type] FROM #scripts
OPEN curs
FETCH NEXT FROM curs
INTO #name, #command, #type
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
IF #type = 'P'
SET #command = REPLACE(#command, 'CREATE PROCEDURE', 'ALTER PROCEDURE')
ELSE
SET #command = REPLACE(#command, 'CREATE VIEW', 'ALTER VIEW')
EXEC sp_executesql #command
PRINT #name + ' - OK'
END TRY
BEGIN CATCH
PRINT #name + ' - FAILED: ' + CAST(ERROR_NUMBER() AS NVARCHAR(MAX)) + ' ' + ERROR_MESSAGE()
--PRINT #command
END CATCH
FETCH NEXT FROM curs
INTO #name, #command, #type
END
CLOSE curs
A bit of a drawn-out option:
Create a copy of the database
(backup and restore). You could do this on the target database, if your confidence level is high.
Use SSMS to script out all the
stored procedures into a single script file
DROP all the procedures
Run the script to recreate them. Any that can't be created will error out.
Couple of fussy gotchas in here, such as:
You want to have the "if proc exists
then drop proc GO create proc ... GO"
syntax to separte each procedure.
Nested procedures will fail if they
call a proc that has not yet been
(re)created. Running the script several
times should catch that (since
ordering them properly can be a real
pain).
Other and more obscure issues might crop up, so be wary.
To quickly drop 10 or 1000 procedures, run
SELECT 'DROP PROCEDURE ' + schema_name(schema_id) + '.' + name
from sys.procedures
select the output, and run it.
This assumes you're doing a very infrequent task. If you have to do this regularly (daily, weekly...), please let us know why!
There is no way to do it from T-SQL, or Enterprise Manager, so i had to write something from client code. i won't post all the code here, but the trick is to:
1) Get a list of all stored procedures
SELECT ROUTINE_NAME AS StoredProcedureName
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_TYPE = 'PROCEDURE' --as opposed to a function
ORDER BY ROUTINE_NAME
2) Get the stored procedure create T-SQL:
select
c.text
from dbo.syscomments c
where c.id = object_id(N'StoredProcedureName')
order by c.number, c.colid
option(robust plan)
3) Run the create statement with NOEXEC on, so that the syntax is checked, but it doesn't actually try to create the stored procedure:
connection("SET NOEXEC ON", ExecuteNoRecords);
connection(StoredProcedureCreateSQL, ExecuteNoRecords);
connection("SET NOEXEC ON", ExecuteNoRecords);
Here is an amendment which deals with multiple schemas
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[RefreshAllViews] AS
-- This sp will refresh all views in the catalog.
-- It enumerates all views, and runs sp_refreshview for each of them
DECLARE abc CURSOR FOR
SELECT TABLE_SCHEMA+'.'+TABLE_NAME AS ViewName
FROM INFORMATION_SCHEMA.VIEWS
OPEN abc
DECLARE #ViewName varchar(128)
-- Build select string
DECLARE #SQLString nvarchar(2048)
FETCH NEXT FROM abc
INTO #ViewName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQLString = 'EXECUTE sp_RefreshView ['+#ViewName+']'
PRINT #SQLString
EXECUTE sp_ExecuteSQL #SQLString
FETCH NEXT FROM abc
INTO #ViewName
END
CLOSE abc
DEALLOCATE abc
GO

Resources