sql >> Base de Datos >  >> RDS >> Database

Analizar los valores predeterminados de los parámetros usando PowerShell - Parte 2

[ Parte 1 | Parte 2 | Parte 3 ]

En mi última publicación, mostré cómo usar TSqlParser y TSqlFragmentVisitor para extraer información importante de un script T-SQL que contiene definiciones de procedimientos almacenados. Con ese script, omití algunas cosas, por ejemplo, cómo analizar la OUTPUT y READONLY palabras clave para parámetros y cómo analizar varios objetos juntos. Hoy, quería proporcionar un script que maneje esas cosas, mencionar algunas otras mejoras futuras y compartir un repositorio de GitHub que creé para este trabajo.

Anteriormente, usé un ejemplo simple como este:

CREATE PROCEDURE dbo.procedure1
  @param1 AS int = /* comment */ -64
AS PRINT 1;
GO

Y con el código de Visitante que proporcioné, la salida a la consola fue:

==========================
Referencia del procedimiento
==========================

dbo.procedimiento1


==========================
Parámetro de procedimiento
==========================

Nombre del parámetro:@param1
Tipo de parámetro:int
Valor predeterminado:-64

Ahora, ¿qué pasaría si la secuencia de comandos pasada se pareciera más a esto? Combina la definición de procedimiento intencionalmente terrible de antes con un par de otros elementos que podría esperar que causen problemas, como nombres de tipos definidos por el usuario, dos formas diferentes de OUT /OUTPUT palabra clave, Unicode en valores de parámetros (¡y en nombres de parámetros!), palabras clave como constantes y literales de escape ODBC.

/* AS BEGIN , @a int = 7, comments can appear anywhere */
CREATE PROCEDURE dbo.some_procedure 
  -- AS BEGIN, @a int = 7 'blat' AS =
  /* AS BEGIN, @a int = 7 'blat' AS = -- */
  @a AS /* comment here because -- chaos */ int = 5,
  @b AS varchar(64) = 'AS = /* BEGIN @a, int = 7 */ ''blat''',
  @c AS int = -- 12 
              6
AS
    -- @d int = 72,
    DECLARE @e int = 5;
    SET @e = 6;
GO
 
CREATE PROCEDURE [dbo].another_procedure
(
  @p1 AS [int] = /* 1 */ 1,
  @p2 datetime = getdate OUTPUT,-- comment,
  @p3 date = {ts '2020-02-01 13:12:49'},
  @p4 dbo.tabletype READONLY,
  @p5 geography OUT, 
  @p6 sysname = N'学中'
)
AS SELECT 5

La secuencia de comandos anterior no maneja varios objetos correctamente y necesitamos agregar algunos elementos lógicos para dar cuenta de OUTPUT y READONLY . Específicamente, Output y ReadOnly no son tipos de token, sino que se reconocen como un Identifier . Entonces, necesitamos algo de lógica adicional para encontrar identificadores con esos nombres explícitos dentro de cualquier ProcedureParameter fragmento. Es posible que detecte algunos otros cambios menores:

    Add-Type -Path "Microsoft.SqlServer.TransactSql.ScriptDom.dll";
 
    $parser = [Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser]($true)::New(); 
 
    $errors = [System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]]::New();
 
    $procedure = @"
    /* AS BEGIN , @a int = 7, comments can appear anywhere */
    CREATE PROCEDURE dbo.some_procedure 
      -- AS BEGIN, @a int = 7 'blat' AS =
      /* AS BEGIN, @a int = 7 'blat' AS = -- */
      @a AS /* comment here because -- chaos */ int = 5,
      @b AS varchar(64) = 'AS = /* BEGIN @a, int = 7 */ ''blat''',
      @c AS int = -- 12 
                  6
    AS
        -- @d int = 72,
        DECLARE @e int = 5;
        SET @e = 6;
    GO
 
    CREATE PROCEDURE [dbo].another_procedure
    (
      @p1 AS [int] = /* 1 */ 1,
      @p2 datetime = getdate OUTPUT,-- comment,
      @p3 date = {ts '2020-02-01 13:12:49'},
      @p4 dbo.tabletype READONLY,
      @p5 geography OUT, 
      @p6 sysname = N'学中'
    )
    AS SELECT 5
"@
 
    $fragment = $parser.Parse([System.IO.StringReader]::New($procedure), [ref]$errors);
 
    $visitor = [Visitor]::New();
 
    $fragment.Accept($visitor);
 
    class Visitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragmentVisitor 
    {
      [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragment] $fragment)
      {
        $fragmentType = $fragment.GetType().Name;
        if ($fragmentType -in ("ProcedureParameter", "ProcedureReference"))
        {
          if ($fragmentType -eq "ProcedureReference")
          {
            Write-Host "`n==========================";
            Write-Host "  $($fragmentType)";
            Write-Host "==========================";
          }
          $output     = "";
          $param      = ""; 
          $type       = "";
          $default    = "";
          $extra      = "";
          $isReadOnly = $false;
          $isOutput   = $false;
          $seenEquals = $false;
 
          for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
          {
            $token = $fragment.ScriptTokenStream[$i];
            if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As"))
            {
              if ($fragmentType -eq "ProcedureParameter")
              {
                if ($token.TokenType -eq "Identifier" -and 
                    ($token.Text.ToUpper -in ("OUT", "OUTPUT", "READONLY"))
                {
                  $extra = $token.Text.ToUpper();
                  if ($extra -eq "READONLY")
                  {
                    $isReadOnly = $true;
                  }
                  else 
                  {
                    $isOutput = $true;
                  }
                }
 
                if (!$seenEquals)
                {
                  if ($token.TokenType -eq "EqualsSign") 
                  { 
                    $seenEquals = $true; 
                  }
                  else 
                  { 
                    if ($token.TokenType -eq "Variable") 
                    {
                      $param += $token.Text; 
                    }
                    else
                    {
                      if (!$isOutput -and !$isReadOnly)
                      {
                        $type += $token.Text; 
                      }
                    }
                  }
                }
                else
                { 
                  if ($token.TokenType -ne "EqualsSign" -and !$isOutput -and !$isReadOnly)
                  {
                    $default += $token.Text;
                  }
                }
              }
              else 
              {
                $output += $token.Text.Trim(); 
              }
            }
          }
 
          if ($param.Length   -gt 0) { $output  = "`nParam name: " + $param.Trim(); }
          if ($type.Length    -gt 0) { $type    = "`nParam type: " + $type.Trim(); }
          if ($default.Length -gt 0) { $default = "`nDefault:    " + $default.TrimStart(); }
          if ($isReadOnly) { $extra = "`nRead Only:  yes"; }
          if ($isOutput)   { $extra = "`nOutput:     yes"; }
 
          Write-Host $output $type $default $extra;
        }
      }
    }

Este código es solo para fines de demostración y no hay posibilidad de que sea el más actual. Consulte los detalles a continuación sobre cómo descargar una versión más reciente.

La salida en este caso:

==========================
Referencia del procedimiento
==========================
dbo.some_procedure


Nombre del parámetro:@a
Tipo de parámetro:int
Predeterminado:5


Nombre del parámetro:@b
Tipo de parámetro:varchar(64)
Valor predeterminado:'AS =/* BEGIN @a, int =7 */ "blat"'


Nombre del parámetro:@c
Tipo de parámetro:int
Predeterminado:6



=========================
ProcedimientoReferencia
==========================
[dbo].otro_procedimiento


Nombre del parámetro:@p1
Tipo de parámetro:[int]
Valor predeterminado:1


Nombre del parámetro:@p2
Tipo de parámetro:fechahora
Predeterminado:getdate
Salida:sí


Nombre del parámetro:@p3
Tipo de parámetro:fecha
Valor predeterminado:{ts '2020-02-01 13:12:49'}


Nombre del parámetro:@p4
Tipo de parámetro:dbo.tabletype
Solo lectura:sí


Nombre del parámetro:@p5
Tipo de parámetro:geografía
Salida:sí


Nombre del parámetro:@p6
Tipo de parámetro:sysname
Valor predeterminado:N'学中'

Ese es un análisis bastante poderoso, a pesar de que hay algunos casos extremos tediosos y mucha lógica condicional. Me encantaría ver TSqlFragmentVisitor expandido para que algunos de sus tipos de token tengan propiedades adicionales (como SchemaObjectName.IsFirstAppearance y ProcedureParameter.DefaultValue ), y ver nuevos tipos de token agregados (como FunctionReference ). Pero incluso ahora, esto está a años luz más allá de un analizador de fuerza bruta que podría escribir en cualquier lenguaje, no importa T-SQL.

Sin embargo, todavía hay un par de limitaciones que aún no he abordado:

  • Esto solo aborda los procedimientos almacenados. El código para manejar los tres tipos de funciones definidas por el usuario es similar , pero no hay una práctica FunctionReference tipo de fragmento, por lo que en su lugar debe identificar el primer SchemaObjectName fragmento (o el primer conjunto de Identifier y Dot tokens) e ignorar cualquier instancia posterior. Actualmente, el código de esta publicación será devolver toda la información sobre los parámetros a una función, pero no devuelve el nombre de la función . Siéntase libre de usarlo para singletons o lotes que contengan solo procedimientos almacenados, pero puede encontrar el resultado confuso para múltiples tipos de objetos mixtos. La última versión en el repositorio a continuación maneja las funciones perfectamente bien.
  • Este código no guarda el estado. La salida a la consola dentro de cada visita es fácil, pero recopilar los datos de varias visitas, para luego canalizarlos a otro lugar, es un poco más complejo, principalmente debido a la forma en que funciona el patrón Visitor.
  • El código anterior no puede aceptar entradas directamente. Para simplificar la demostración aquí, es solo un script en bruto donde pega su bloque T-SQL como una constante. El objetivo final es admitir la entrada de un archivo, una matriz de archivos, una carpeta, una matriz de carpetas o extraer definiciones de módulos de una base de datos. Y la salida puede estar en cualquier lugar:a la consola, a un archivo, a una base de datos... así que el cielo es el límite allí. Parte de ese trabajo se ha realizado mientras tanto, pero nada de eso se ha escrito en la versión simple que ves arriba.
  • No hay manejo de errores. Nuevamente, por brevedad y facilidad de consumo, el código aquí no se preocupa por manejar excepciones inevitables, aunque lo más destructivo que puede suceder en su forma actual es que un lote no aparecerá en la salida si no puede ser correctamente analizado (como CREATE STUPID PROCEDURE dbo.whatever ). Cuando comencemos a usar bases de datos y/o el sistema de archivos, el manejo adecuado de errores será mucho más importante.

Puede preguntarse, ¿dónde voy a mantener el trabajo continuo en esto y solucionar todas estas cosas? Bueno, lo puse en GitHub, tentativamente llamé al proyecto ParamParser , y ya tienen colaboradores que ayudan con las mejoras. La versión actual del código ya se ve bastante diferente del ejemplo anterior, y para cuando lea esto, es posible que ya se hayan abordado algunas de las limitaciones mencionadas aquí. Solo quiero mantener el código en un solo lugar; este consejo se trata más de mostrar una muestra mínima de cómo puede funcionar y resaltar que existe un proyecto dedicado a simplificar esta tarea.

En el próximo segmento, hablaré más sobre cómo mi amigo y colega, Will White, me ayudó a pasar del script independiente que ve arriba al módulo mucho más poderoso que encontrará en GitHub.

Si necesita analizar los valores predeterminados de los parámetros mientras tanto, no dude en descargar el código y probarlo. Y como sugerí antes, experimente por su cuenta, porque hay muchas otras cosas poderosas que puede hacer con estas clases y el patrón Visitante.

[ Parte 1 | Parte 2 | Parte 3 ]