Technical Article

String-search on objects in a specified database

,

Procedure 'p_find_string' allows users to specify any database on a server in searching for a specified character string.  Supported for string-searching:  table columns, view columns, trigger, function, and procedure code.  Produces a report showing: object type, owner, object name, column id (for tables and views) or what line number (for triggers, functions, and procedures), and the name and definition (for tables and views) or the line of code (for triggers, functions, and procedures).

Example:  Search the pubs database for all columns and lines of code containing the string 'author':

EXEC p_find_string @p_string = 'author'
                 , @p_dbname = 'pubs'

Pre-requisites:  Functions 'f_contains' and 'f_delimited', and stored procedure 'p_get_column_attribute', which I previously submitted.

IF EXISTS (SELECT 1
           FROM   sysobjects
           WHERE  xtype = 'P'
             AND  name = 'p_find_string_in_objects')
  DROP PROCEDURE p_find_string
GO
CREATE PROCEDURE p_find_string
                   @p_string VARCHAR (4000)
                 , @p_dbname SYSNAME
                 , @p_debug  CHAR (1) = NULL
/*  
||======================================================================  
|| Date created:  7/15/2002 (Dise)
||
|| Purpose:  This procedure searches database objects for the
||           presence of a character string, and reports where the
||           string is to be found.
|| 
|| Parameters:
||   @p_string -- string for which to search
||   @p_dbname -- which database to run from
||   @p_debug  -- a non-NULL value turns on certain runtime displays
||======================================================================
*/AS
  DECLARE @TRUE            BIT
        , @FALSE           BIT
        , @done            BIT
        , @colid           INT
        , @len             INT
        , @linenumber      INT
        , @pos             INT
        , @retc            INT
        , @seq             INT
        , @cr              CHAR (1)
        , @lf              CHAR (1)
        , @crlf            CHAR (2)
        , @lfcr            CHAR (2)
        , @q               CHAR (1)
        , @coldef          VARCHAR (100)
        , @delim           VARCHAR (2)
        , @linetext        VARCHAR (4000)
        , @msg             VARCHAR (8000)
        , @objecttype      VARCHAR (9)
        , @otext           VARCHAR (4000)
        , @sqlcmd          VARCHAR (8000)
        , @str             VARCHAR (4000)
        , @dbname          SYSNAME
        , @objectname      SYSNAME
        , @owner           SYSNAME

  /*
  ||====================================================================
  || Initialize...
  ||====================================================================
  */  SELECT @retc  = 0
       , @TRUE  = 1
       , @FALSE = 0
       , @cr    = CHAR (13) -- carriage return
       , @lf    = CHAR (10) -- line feed
       , @q     = CHAR (39) -- single quote
  SELECT @crlf  = @cr + @lf
       , @lfcr  = @lf + @cr
  /*
  ||====================================================================
  || Ensure that the specified database exists on the server...
  ||====================================================================
  */  IF @p_dbname IS NULL
--THEN
    SELECT @dbname = DB_NAME ()
  ELSE
    IF EXISTS (SELECT 1
               FROM   master..sysdatabases
               WHERE  name = @p_dbname)
  --THEN
      SELECT @dbname = @p_dbname
    ELSE
      BEGIN
        SELECT @msg = ' database by name of '       + @p_dbname
                    + ' does not exists on server ' + @@SERVERNAME
        GOTO PRC_ERROR
      END
  --END IF
--END IF

  /*
  ||====================================================================
  || Input string must be non-NULL and not equal to blanks...
  ||====================================================================
  */  IF @p_string IS NULL
  OR @p_string = ''
--THEN
    BEGIN
      SELECT @msg = ' searched-for string "'      + @p_string + '"'
                  + ' must be non-NULL and non-blank'
      GOTO PRC_ERROR
    END
--END IF

  /*
  ||====================================================================
  || Main logic...
  ||====================================================================
  */  /*
  ||====================================================================
  || Create a table to hold database object info...
  ||====================================================================
  */  CREATE TABLE #objects
    (seq        INT IDENTITY
   , objecttype VARCHAR (9)
   , owner      VARCHAR (60)
   , objectname VARCHAR (60)
   , colid      INT
   , otext      VARCHAR (4000))

  /*
  ||====================================================================
  || Create a table to hold the number of carriage-return characters
  || contained in each row of syscomments for stored procedures, for
  || figuring out linenumbers within the procedure...
  ||====================================================================
  */  CREATE TABLE #crcounts
    (colid      INT
   , crcount    INT)

  /*
  ||====================================================================
  || Create a table to hold the report...
  ||====================================================================
  */  CREATE TABLE #report
    (seq                    INT IDENTITY
   , objecttype             VARCHAR (9)
   , owner                  VARCHAR (60)
   , objectname             VARCHAR (60)
   , columnid_or_linenumber INT
   , columndef_or_linetext  VARCHAR (4000))

  /*
  ||====================================================================
  || Search table and view definitions for column names matching the
  || searched-for string...
  ||====================================================================
  */  SELECT @sqlcmd =
       'SELECT CASE o.xtype'
    +          ' WHEN ' + @q + 'U'     + @q
    +          ' THEN ' + @q + 'TABLE' + @q
    +          ' ELSE ' + @q + 'VIEW'  + @q
    +        ' END'
    +        ', u.name'
    +        ', o.name'
    +        ', c.colid'
    +        ', c.name'
    +  ' FROM '   + @dbname + '..syscolumns c'
    +    ' JOIN ' + @dbname + '..systypes   t ON c.xtype = t.xtype'
    +    ' JOIN ' + @dbname + '..sysobjects o ON c.id    = o.id'
    +    ' JOIN ' + @dbname + '..sysusers   u ON o.uid   = u.uid'
    +  ' WHERE  o.xtype IN (' + @q + 'U' + @q
    +                    ', ' + @q + 'V' + @q + ')'
    +    ' AND  c.name LIKE ' + @q + '%' + @p_string + '%' + @q

  IF @p_debug IS NOT NULL
--THEN
    PRINT @sqlcmd
--END IF

  INSERT INTO #objects (objecttype
                      , owner
                      , objectname
                      , colid
                      , otext)
    EXEC (@sqlcmd)

  /*
  ||====================================================================
  || Search procedure and function definitions for column names matching
  || the searched-for string...
  ||====================================================================
  */  SELECT @sqlcmd =
       'SELECT CASE o.xtype'
    +          ' WHEN ' + @q + 'P'         + @q
    +          ' THEN ' + @q + 'PROCEDURE' + @q
    +          ' ELSE ' + @q + 'FUNCTION'  + @q
    +        ' END'
    +       ', u.name'
    +       ', o.name'
    +       ', c.colid'
    +       ', c.text'
    + ' FROM '    + @dbname + '..syscomments c'
    +    ' JOIN ' + @dbname + '..sysobjects  o ON c.id    = o.id'
    +    ' JOIN ' + @dbname + '..sysusers    u ON o.uid   = u.uid'
    + ' WHERE c.id IN (SELECT c2.id'
    +                ' FROM ' + @dbname + '..syscomments c2'
    +                ' WHERE  c2.text LIKE '
    +                    @q + '%' + @p_string + '%' + @q + ')'
    +   ' AND o.xtype IN (' + @q + 'FN' + @q
    +                  ', ' + @q + 'P'  + @q
    +                  ', ' + @q + 'TF' + @q
    +                  ', ' + @q + 'TR' + @q + ')'

  IF @p_debug IS NOT NULL
--THEN
    PRINT @sqlcmd
--END IF

  INSERT INTO #objects (objecttype
                      , owner
                      , objectname
                      , colid
                      , otext)
     EXEC (@sqlcmd)

  /*
  ||====================================================================
  || In preparation for using SEQ as a retrieval key, create an index
  || on it...
  ||====================================================================
  */  CREATE UNIQUE CLUSTERED INDEX objidx ON #objects (seq)

  /*
  ||====================================================================
  || Process the list of objects...
  ||
  || Rather than use a CURSOR, we're going to use the SEQ column
  || as a key...
  ||====================================================================
  */  SELECT @seq  = 0
       , @done = @FALSE
  
  WHILE @done = @FALSE
  BEGIN
    /*
    ||==================================================================
    || Get the next row of data from #objects, if one exists.  If not,
    || set a flag that causes falling out of the loop...
    ||==================================================================
    */    IF EXISTS (SELECT 1
               FROM  #objects
               WHERE  seq > @seq)
  --THEN
      SELECT @seq        = seq
           , @objecttype = objecttype
           , @owner      = owner
           , @objectname = objectname
           , @colid      = colid
           , @otext      = otext
      FROM   #objects
      WHERE  seq = (SELECT MIN (seq)
                    FROM  #objects
                    WHERE  seq > @seq)
    ELSE
      BEGIN
        SELECT @done = @TRUE

        GOTO ENDOFLOOP
      END
  --END IF

    /*
    ||==================================================================
    || If the objects is a table or view, then the only additional
    || information needed is the column definition, which is acquired
    || thru the procedure 'p_get_column_attribute'...
    ||==================================================================
    */    IF @objecttype IN ('TABLE', 'VIEW')
  --THEN
      BEGIN
        EXEC p_get_column_attribute
               @p_which_attrib = 'TYPEDEF'
             , @p_tablename    = @objectname
             , @p_columnname   = @otext
             , @p_out_msg      = @msg    OUTPUT
             , @p_out_attrib   = @coldef OUTPUT
             , @p_dbname       = @dbname
             , @p_owner        = @owner

        /*
        ||==============================================================
        || All accumulated info about the table or view now goes into
        || the #report table...
        ||==============================================================
        */        INSERT INTO #report (objecttype
                           , owner
                           , objectname
                           , columnid_or_linenumber
                           , columndef_or_linetext)
          VALUES (@objecttype
                , @owner
                , @objectname
                , @colid
                , @otext + ' ' + @coldef)
      END

    /*
    ||==================================================================
    || If the objects is a function or a procedure, then the needed
    || information includes the linenumber and the text of the line
    || in which the string occurs...
    ||==================================================================
    */    ELSE IF @objecttype IN ('FUNCTION', 'PROCEDURE', 'TRIGGER')
  --THEN
      BEGIN
        /*
        ||==============================================================
        || We want to allow for any possible combination of carriage
        || return or line feeds, changing them all to simple carriage
        || returns...
        ||==============================================================
        */        SELECT @otext = REPLACE (@otext, @crlf, @cr)
        SELECT @otext = REPLACE (@otext, @lfcr, @cr)
        SELECT @otext = REPLACE (@otext, @lf,   @cr)

        /*
        ||==============================================================
        || Some procedures and functions require more than one row in
        || 'syscomments' to be stored in.  By keeping a count of the
        || number of carriage-return values in each 'syscomments' row,
        || we can compute the number of lines up to any point in the
        || module...
        ||==============================================================
        */        IF @colid = 1
      --THEN
          BEGIN
            TRUNCATE TABLE #crcounts

            INSERT INTO #crcounts (colid, crcount)
              VALUES (0, 0)
          END
      --END IF

        INSERT INTO #crcounts (colid
                             , crcount)
          SELECT @colid
               , dbo.f_contains (@cr, @otext)

        /*
        ||==============================================================
        || We have retrieved all rows in 'syscomments' for any procedure
        || or function containing the string; therefore, not every row
        || retrieved may have the string inside of it.  (However, even
        || so, we may need to know how many lines of code are contained
        || in this row, so it gets processed anyway.)
        ||
        || The following returns the address of the string inside of
        || the current 'syscomments.text' column, if the string exists
        || in it, and also the length of the text...
        ||==============================================================
        */        SELECT @pos = CHARINDEX (@p_string, @otext)
             , @len = LEN (@otext)

        /*
        ||==============================================================
        || The following 'inner' loop is designed to examine the
        || 'syscomments.text' value to find occurrences of the string.
        || There may be one or many such occurrences; if there were
        || zero, the looping criteria are not met (i.e., @pos = 0)...
        ||==============================================================
        */        WHILE @pos >  0
          AND @pos <= @len
        BEGIN
          /*
          ||============================================================
          || The part of the text occurring before the string starts
          || needs to be examined for carriage-return characters, which
          || indicate what line number the string occurs within.  The
          || 'f_contains' function counts how many such
          || characters are within this segment, which the 'f_delimited'
          || function returns the text of this line...
          ||============================================================
          */          IF @pos = 1
        --THEN
            SELECT @str = ''
          ELSE
            SELECT @str = SUBSTRING (@otext, 1, @pos - 1)
        --END IF
          
          SELECT @linetext   = dbo.f_delimited (@otext, @cr, @pos)
               , @linenumber = dbo.f_contains (@cr, @str)
                             + SUM (crcount)
                             + 1
          FROM  #crcounts
          WHERE  colid < @colid

          /*
          ||============================================================
          || The assembled information is put into the #report table...
          ||============================================================
          */          INSERT INTO #report (objecttype
                             , owner
                             , objectname
                             , columnid_or_linenumber
                             , columndef_or_linetext)
            VALUES (@objecttype
                  , @owner
                  , @objectname
                  , @linenumber
                  , @linetext)

          /*
          ||============================================================
          || We want to skip past other occurrences of the string
          || within the current line of procedure/function code, since
          || this line has already been reported on, but still want
          || to pick up other occurrences on subsequent lines...
          ||============================================================
          */          IF @pos < @len
        --THEN
            BEGIN
              SELECT @pos = CHARINDEX (@cr, @otext, @pos)

              IF  @pos > 0
              AND @pos < @len
            --THEN
                SELECT @pos = CHARINDEX (@p_string, @otext, @pos + 1)
              ELSE
                SELECT @pos = 0
            --END IF
            END
          ELSE
            SELECT @pos = 0
        --END IF

        /*
        ||==============================================================
        || End of 'inner' loop...
        ||==============================================================
        */        END

        /*
        ||==============================================================
        || If the next row in #objects is a continuation of the current
        || procedure or function, the number of lines already counted
        || up until now needs to be saved to preserve accuracy...
        ||==============================================================
        */        SELECT @linenumber = @linenumber
                           + dbo.f_contains (@cr, @otext)
      END
  --END IF

    /*
    ||==================================================================
    || When no more rows exist in the #objects table, processing falls
    || out of the loop here...
    ||==================================================================
    */ENDOFLOOP:
  END

  /*
  ||====================================================================
  || Final query...
  ||====================================================================
  */  /*
  ||====================================================================
  || Print the banner...
  ||====================================================================
  */  PRINT 'Executing procedure ' + OBJECT_NAME (@@PROCID) + '...'
  PRINT 'Searching for string: '    + @p_string
  PRINT REPLICATE ('-', 72)

  SELECT objecttype
       , owner
       , objectname
       , columnid_or_linenumber
       , columndef_or_linetext
  FROM #report
  ORDER BY objecttype
         , owner
         , objectname
         , columnid_or_linenumber

  /*
  ||====================================================================
  || When all has gone well, step around the error trap...
  ||====================================================================
  */  GOTO PRC_EXIT

PRC_ERROR:
  /*
  ||====================================================================
  || Error trap...
  ||====================================================================
  */  SELECT @retc = 1

  RAISERROR (@msg, 16, 1)

PRC_EXIT:
  /*
  ||====================================================================
  || Exit the procedure...
  ||====================================================================
  */  RETURN @retc
GO

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating