A reliable way to verify T-SQL stored procedures

12,827

Solution 1

You can choose different ways. First of all SQL SERVER 2008 supports dependencies which exist in DB inclusive dependencies of STORED PROCEDURE (see http://msdn.microsoft.com/en-us/library/bb677214%28v=SQL.100%29.aspx, http://msdn.microsoft.com/en-us/library/ms345449.aspx and http://msdn.microsoft.com/en-us/library/cc879246.aspx). You can use sys.sql_expression_dependencies and sys.dm_sql_referenced_entities to see and verify there.

But the most simple way to do verification of all STORED PROCEDURE is following:

  1. export all STORED PROCEDURE
  2. drop old existing STORED PROCEDURE
  3. import just exported STORED PROCEDURE.

If you upgrade DB the existing Stored Procedure will be not verified, but if you create a new one, the procedure will be verified. So after exporting and exporting of all Stored Procedure you receive all existing error reported.

You can also see and export the code of a Stored Procedure with a code like following

SELECT definition
FROM sys.sql_modules
WHERE object_id = (OBJECT_ID(N'spMyStoredProcedure'))

UPDATED: To see objects (like tables and views) referenced by Stored Procedure spMyStoredProcedure you can use following:

SELECT OBJECT_NAME(referencing_id) AS referencing_entity_name 
    ,referenced_server_name AS server_name
    ,referenced_database_name AS database_name
    ,referenced_schema_name AS schema_name
    , referenced_entity_name
FROM sys.sql_expression_dependencies 
WHERE referencing_id = OBJECT_ID(N'spMyStoredProcedure');

UPDATED 2: In the comment to my answer Martin Smith suggested to use sys.sp_refreshsqlmodule instead of recreating a Stored Procedure. So with the code

SELECT 'EXEC sys.sp_refreshsqlmodule ''' + OBJECT_SCHEMA_NAME(object_id) +
              '.' + name + '''' FROM sys.objects WHERE type in (N'P', N'PC')

one receive a script, which can be used for verifying of Stored Procedure dependencies. The output will look like following (example with AdventureWorks2008):

EXEC sys.sp_refreshsqlmodule 'dbo.uspGetManagerEmployees'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetWhereUsedProductID'
EXEC sys.sp_refreshsqlmodule 'dbo.uspPrintError'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeeHireInfo'
EXEC sys.sp_refreshsqlmodule 'dbo.uspLogError'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeeLogin'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeePersonalInfo'
EXEC sys.sp_refreshsqlmodule 'dbo.uspSearchCandidateResumes'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetBillOfMaterials'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetEmployeeManagers'

Solution 2

Here is what worked for me:

-- Based on comment from http://blogs.msdn.com/b/askjay/archive/2012/07/22/finding-missing-dependencies.aspx
-- Check also http://technet.microsoft.com/en-us/library/bb677315(v=sql.110).aspx

select o.type, o.name, ed.referenced_entity_name, ed.is_caller_dependent
from sys.sql_expression_dependencies ed
join sys.objects o on ed.referencing_id = o.object_id
where ed.referenced_id is null

You should get all missing dependencies for your SPs, solving problems with late binding.

Exception: is_caller_dependent = 1 does not necessarily mean a broken dependency. It just means that the dependency is resolved on runtime because the schema of the referenced object is not specified. You can avoid it specifying the schema of the referenced object (another SP for example).

Credits to Jay's blog and the anonymous commenter...

Solution 3

I am fond of using Display Estimated Execution Plan. It highlights many errors reasonably without ever having to really run the proc.

Solution 4

When I came across this question I was interested in finding a safe, non-invasive, and fast technique for validating syntax and object (table, column) references.

While I agree that actually executing each stored procedure will likely turn up more issues than just compiling them, one must exercise caution with the former approach. That is, you need to know that it is, in fact, safe to execute each and every stored procedure (i.e. does it erase some tables, for example?). This safety issue can be addressed by wrapping the execution in a transaction and rolling it back so no changes are permanent, as suggested in devio's answer. Still, this approach could potentially take quite a long time depending on how much data you are manipulating.

The code in the question, and the first portion of Oleg's answer, both suggest re-instantiating each stored procedure, as that action recompiles the procedure and does just such syntactic validation. But this approach is invasive--it's fine for a private test system, but could disrupt the work of other develoeprs on a heavily used test system.

I came across the article Check Validity of SQL Server Stored Procedures, Views and Functions, which presents a .NET solution, but it is the follow-up post at the bottom by "ddblue" that intrigued me more. This approach obtains the text of each stored procedure, converts the create keyword to alter so that it can be compiled, then compiles the proc. And that accurately reports any bad table and column references. The code runs, but I quickly ran into some issues because of the create/alter conversion step.

The conversion from "create" to "alter" looks for "CREATE" and "PROC" separated by a single space. In the real-world, there could spaces or tabs, and there could be one or more than one. I added a nested "replace" sequence (thanks, to this article by Jeff Moden!) to convert all such occurrences to a single space, allowing the conversion to proceed as originally designed. Then, since that needed to be used wherever the original "sm.definition" expression was used, I added a common table expression to avoid massive, unsightly code duplication. So here is my updated version of the code:

DECLARE @Schema NVARCHAR(100),
    @Name NVARCHAR(100),
    @Type NVARCHAR(100),
    @Definition NVARCHAR(MAX),
    @CheckSQL NVARCHAR(MAX)

DECLARE crRoutines CURSOR FOR
WITH System_CTE ( schema_name, object_name, type_desc, type, definition, orig_definition)
AS -- Define the CTE query.
( SELECT    OBJECT_SCHEMA_NAME(sm.object_id) ,
            OBJECT_NAME(sm.object_id) ,
            o.type_desc ,
            o.type,
            REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(REPLACE(sm.definition, char(9), ' '))), '  ', ' ' + CHAR(7)), CHAR(7) + ' ', ''), CHAR(7), '') [definition],
            sm.definition [orig_definition]
  FROM      sys.sql_modules (NOLOCK) AS sm
            JOIN sys.objects (NOLOCK) AS o ON sm.object_id = o.object_id
  -- add a WHERE clause here as indicated if you want to test on a subset before running the whole list.
  --WHERE     OBJECT_NAME(sm.object_id) LIKE 'xyz%'
)
-- Define the outer query referencing the CTE name.
SELECT  schema_name ,
        object_name ,
        type_desc ,
        CASE WHEN type_desc = 'SQL_STORED_PROCEDURE'
             THEN STUFF(definition, CHARINDEX('CREATE PROC', definition), 11, 'ALTER PROC')
             WHEN type_desc LIKE '%FUNCTION%'
             THEN STUFF(definition, CHARINDEX('CREATE FUNC', definition), 11, 'ALTER FUNC')
             WHEN type = 'VIEW'
             THEN STUFF(definition, CHARINDEX('CREATE VIEW', definition), 11, 'ALTER VIEW')
             WHEN type = 'SQL_TRIGGER'
             THEN STUFF(definition, CHARINDEX('CREATE TRIG', definition), 11, 'ALTER TRIG')
        END
FROM    System_CTE
ORDER BY 1 , 2;

OPEN crRoutines

FETCH NEXT FROM crRoutines INTO @Schema, @Name, @Type, @Definition

WHILE @@FETCH_STATUS = 0 
    BEGIN
        IF LEN(@Definition) > 0
            BEGIN
                -- Uncomment to see every object checked.
                -- RAISERROR ('Checking %s...', 0, 1, @Name) WITH NOWAIT
                BEGIN TRY
                    SET PARSEONLY ON ;
                    EXEC ( @Definition ) ;
                    SET PARSEONLY OFF ;
                END TRY
                BEGIN CATCH
                    PRINT @Type + ': ' + @Schema + '.' + @Name
                    PRINT ERROR_MESSAGE() 
                END CATCH
            END
        ELSE
            BEGIN
                RAISERROR ('Skipping %s...', 0, 1, @Name) WITH NOWAIT
            END
        FETCH NEXT FROM crRoutines INTO @Schema, @Name, @Type, @Definition
    END

CLOSE crRoutines
DEALLOCATE crRoutines

Solution 5

Nine years after I first posed this question, and I've just discovered an amazing tool built by Microsoft themselves that not only can reliably verify stored procedure compatibility between SQL Server versions, but all other internal aspects as well. It's been renamed a few times, but they currently call it:

Microsoft® Data Migration Assistant v5.4*

* Version as of 6/17/2021

https://www.microsoft.com/en-us/download/details.aspx?id=53595

Data Migration Assistant (DMA) enables you to upgrade to a modern data platform by detecting compatibility issues that can impact database functionality on your new version of SQL Server. It recommends performance and reliability improvements for your target environment. It allows you to not only move your schema and data, but also uncontained objects from your source server to your target server.

The answers above that use EXEC sys.sp_refreshsqlmodule were a great start, but we ran into one MAJOR problem running it on 2008 R2: any stored procedure or function that was renamed (using sp_rename, and not a DROP/CREATE pattern) REVERTED to its prior definition after running the refresh procedure, because the internal metadata isn't refreshed under the new name. It's a known bug that was fixed in SQL Server 2012, but we had a fun day of recovery afterwards. (One workaround, future readers, is to issue a ROLLBACK if the refresh throws an error.)

Anyway, times have changed, new tools are available -- and good ones at that -- thus the late addition of this answer.

Share:
12,827

Related videos on Youtube

Cᴏʀʏ
Author by

Cᴏʀʏ

I work at Corporate Technology Solutions, Inc., a software consulting firm that specializes in custom software development for the financial services industries in the Greater Milwaukee Area in Wisconsin. I earned a Bachelor of Science degree in Software Engineering from Milwaukee School of Engineering. As a consultant and engineer, typical projects for me revolve around full-stack web application development. My strengths include: gathering functional and technical requirements, database design and administration (ssis, sql-server), development (c#, vb.net, aspnet-mvc, sql, javascript, jquery and more), testing, integration, and training. In my spare time I like to read, relax, sample great Wisconsin beer, and of course, contribute to Stack Overflow! Some achievements I'm proud of (Stack Overflow): Usually in the top 1,000 users for all time rank by reputation 38/40 candidate score (if I were to ever run for moderator) c# – gold badge jquery – gold badge javascript – silver badge sql – silver badge sql-server – silver badge I never intend to give bad advice. If you find that any of my answers are counter-productive or just plain wrong, please leave a comment! I'm more than happy to fix or delete answers if they aren't up to snuff.

Updated on April 21, 2022

Comments

  • Cᴏʀʏ
    Cᴏʀʏ about 2 years

    We're upgrading from SQL Server 2005 to 2008. Almost every database in the 2005 instance is set to 2000 compatibility mode, but we're jumping to 2008. Our testing is complete, but what we've learned is that we need to get faster at it.

    I've discovered some stored procedures that either SELECT data from missing tables or try to ORDER BY columns that don't exist.

    Wrapping the SQL to create the procedures in SET PARSEONLY ON and trapping errors in a try/catch only catches the invalid columns in the ORDER BYs. It does not find the error with the procedure selecting data from the missing table. SSMS 2008's intellisense, however, DOES find the issue, but I can still go ahead and successfully run the ALTER script for the procedure without it complaining.

    So, why can I even get away with creating a procedure that fails when it runs? Are there any tools out there that can do better than what I've tried?

    The first tool I found wasn't very useful: DbValidator from CodeProject, but it finds fewer problems than this script I found on SqlServerCentral, which found the invalid column references.

    -------------------------------------------------------------------------
    -- Check Syntax of Database Objects
    -- Copyrighted work.  Free to use as a tool to check your own code or in 
    --  any software not sold. All other uses require written permission.
    -------------------------------------------------------------------------
    -- Turn on ParseOnly so that we don't actually execute anything.
    SET PARSEONLY ON 
    GO
    
    -- Create a table to iterate through
    declare @ObjectList table (ID_NUM int NOT NULL IDENTITY (1, 1), OBJ_NAME varchar(255), OBJ_TYPE char(2))
    
    -- Get a list of most of the scriptable objects in the DB.
    insert into @ObjectList (OBJ_NAME, OBJ_TYPE)
    SELECT   name, type
    FROM     sysobjects WHERE type in ('P', 'FN', 'IF', 'TF', 'TR', 'V')
    order by type, name
    
    -- Var to hold the SQL that we will be syntax checking
    declare @SQLToCheckSyntaxFor varchar(max)
    -- Var to hold the name of the object we are currently checking
    declare @ObjectName varchar(255)
    -- Var to hold the type of the object we are currently checking
    declare @ObjectType char(2)
    -- Var to indicate our current location in iterating through the list of objects
    declare @IDNum int
    -- Var to indicate the max number of objects we need to iterate through
    declare @MaxIDNum int
    -- Set the inital value and max value
    select  @IDNum = Min(ID_NUM), @MaxIDNum = Max(ID_NUM)
    from    @ObjectList
    
    -- Begin iteration
    while @IDNum <= @MaxIDNum
    begin
      -- Load per iteration values here
      select  @ObjectName = OBJ_NAME, @ObjectType = OBJ_TYPE
      from    @ObjectList
      where   ID_NUM = @IDNum 
    
      -- Get the text of the db Object (ie create script for the sproc)
      SELECT @SQLToCheckSyntaxFor = OBJECT_DEFINITION(OBJECT_ID(@ObjectName, @ObjectType))
    
      begin try
        -- Run the create script (remember that PARSEONLY has been turned on)
        EXECUTE(@SQLToCheckSyntaxFor)
      end try
      begin catch
        -- See if the object name is the same in the script and the catalog (kind of a special error)
        if (ERROR_PROCEDURE() <> @ObjectName)
        begin
          print 'Error in ' + @ObjectName
          print '  The Name in the script is ' + ERROR_PROCEDURE()+ '. (They don''t match)'
        end
        -- If the error is just that this already exists then  we don't want to report that.
        else if (ERROR_MESSAGE() <> 'There is already an object named ''' + ERROR_PROCEDURE() + ''' in the database.')
        begin
          -- Report the error that we got.
          print 'Error in ' + ERROR_PROCEDURE()
          print '  ERROR TEXT: ' + ERROR_MESSAGE() 
        end
      end catch
    
      -- Setup to iterate to the next item in the table
      select  @IDNum = case
                when Min(ID_NUM) is NULL then @IDNum + 1
                else Min(ID_NUM)
              end  
      from    @ObjectList
      where   ID_NUM > @IDNum
    
    end
    -- Turn the ParseOnly back off.
    SET PARSEONLY OFF 
    GO
    
    • Cᴏʀʏ
      Cᴏʀʏ about 14 years
      @John -- HA! Automated regression tests... not for these databases. We've been trying to clean up this client's mess for close to two years now! I agree though, that would be the way to go, but we don't have the time to develop the tests.
    • mbomb007
      mbomb007 almost 8 years
      View this answer on the DBA site. You can use sys.procedures instead of sys.views.
  • Cᴏʀʏ
    Cᴏʀʏ over 12 years
    Seems pretty clever. I'll give it a try when I'm not at work.
  • PhilipD
    PhilipD almost 11 years
    However, there are a couple of minor errors in this code. In the "CASE" statement in the outer query, the last 2 cases should start with "WHEN type_desc =" instead of "WHEN type =".
  • Del Lee
    Del Lee over 6 years
    I used this, although it did generate some false positives. If a stored procedure references an object in another database, for example, it thinks there is a bad reference. Also, it identifies system stored procedures as "problems" as well. sp_send_dbmail is used in some of our stored procedures, for example, and was flagged as a bad reference. Nevertheless, kudos for sharing this.
  • JumpingJezza
    JumpingJezza almost 6 years
    This flagged all my operations on hierarchyid columns (eg GetAncestor, GetLevel) as false positives. They all had is_ambiguous = 1 so that seemed to work as an extra filter.
  • Randy in Marin
    Randy in Marin almost 3 years
    I just gave it a try. The DMA tool did detect some potential issues, but not the one I am looking for. I have some 2008 code that does not compile with a more recent compatibility setting. (It needs an explicit cast from date to datetime.) The sp_refreshsqlmodule does detect the issue while DMA did not. DMA must not be doing a compile. However, there can be issues with a recompile such as a lost signature. I wonder if a rollback would recover the signature....
  • Nitesh
    Nitesh over 2 years
    This is a good start to find errors.