Database
 sql >> Base de Dados >  >> RDS >> Database

Analisar valores padrão de parâmetro usando o PowerShell – Parte 2


[ Parte 1 | Parte 2 | Parte 3]

No meu último post, mostrei como usar o TSqlParser e TSqlFragmentVisitor para extrair informações importantes de um script T-SQL contendo definições de procedimento armazenado. Com esse script, deixei de fora algumas coisas, como analisar o OUTPUT e READONLY palavras-chave para parâmetros e como analisar vários objetos juntos. Hoje, eu queria fornecer um script que lidasse com essas coisas, mencionar algumas outras melhorias futuras e compartilhar um repositório GitHub que criei para este trabalho.

Anteriormente, usei um exemplo simples como este:
CREATE PROCEDURE dbo.procedure1
  @param1 AS int = /* comment */ -64
AS PRINT 1;
GO

E com o código de visitante que forneci, a saída para o console foi:
==========================
ProcedureReference
===========================

dbo.procedure1


==========================
ProcedureParameter
===========================

Nome do parâmetro:@param1
Tipo de parâmetro:int
Padrão:-64

Agora, e se o script passado se parecesse mais com isso? Ele combina a definição de procedimento intencionalmente terrível de antes com alguns outros elementos que você pode esperar causar problemas, como nomes de tipos definidos pelo usuário, duas formas diferentes de OUT /OUTPUT palavra-chave, Unicode em valores de parâmetro (e em nomes de parâmetro!), palavras-chave como constantes e literais 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

O script anterior não lida com vários objetos corretamente e precisamos adicionar alguns elementos lógicos para contabilizar OUTPUT e READONLY . Especificamente, Output e ReadOnly não são tipos de token, mas são reconhecidos como um Identifier . Portanto, precisamos de alguma lógica extra para encontrar identificadores com esses nomes explícitos em qualquer ProcedureParameter fragmento. Você pode identificar algumas outras pequenas alterações:
    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 é apenas para fins de demonstração e não há chance de ser o mais atual. Veja os detalhes abaixo sobre como baixar uma versão mais recente.

A saída neste caso:
==========================
ProcedureReference
===========================
dbo.some_procedure


Nome do parâmetro:@a
Tipo de parâmetro:int
Padrão:5


Nome do parâmetro:@b
Tipo de parâmetro:varchar(64)
Padrão:'AS =/* BEGIN @a, int =7 */ "blat"'


Nome do parâmetro:@c
Tipo de parâmetro:int
Padrão:6



===========================
ProcedureReference
==========================
[dbo].another_procedure


Nome do parâmetro:@p1
Tipo de parâmetro:[int]
Padrão:1


Nome do parâmetro:@p2
Tipo de parâmetro:datetime
Padrão:getdate
Saída:sim


Nome do parâmetro:@p3
Tipo de parâmetro:data
Padrão:{ts '2020-02-01 13:12:49'}


Nome do parâmetro:@p4
Tipo de parâmetro:dbo.tabletype
Somente leitura:sim


Nome do parâmetro:@p5
Tipo de parâmetro:geografia
Saída:sim


Nome do parâmetro:@p6
Tipo de parâmetro:sysname
Padrão:N'学中'
Essa é uma análise bastante poderosa, embora existam alguns casos de borda tediosos e muita lógica condicional. Eu adoraria ver TSqlFragmentVisitor expandido para que alguns de seus tipos de token tenham propriedades adicionais (como SchemaObjectName.IsFirstAppearance e ProcedureParameter.DefaultValue ) e veja os novos tipos de token adicionados (como FunctionReference ). Mas mesmo agora, isso está anos-luz além de um analisador de força bruta que você pode escrever em qualquer linguagem, não importa o T-SQL.

Ainda existem algumas limitações que ainda não abordei:
  • Isso aborda apenas procedimentos armazenados. O código para lidar com todos os três tipos de funções definidas pelo usuário é semelhante , mas não há FunctionReference útil tipo de fragmento, então você precisa identificar o primeiro SchemaObjectName fragmento (ou o primeiro conjunto de Identifier e Dot tokens) e ignore quaisquer instâncias subsequentes. Atualmente, o código neste post vai retornar todas as informações sobre os parâmetros a uma função, mas não retorna o nome da função . Sinta-se à vontade para usá-lo para singletons ou lotes contendo apenas procedimentos armazenados, mas você pode achar a saída confusa para vários tipos de objetos mistos. A versão mais recente no repositório abaixo lida perfeitamente bem com as funções.
  • Este código não salva o estado. A saída para o console dentro de cada Visit é fácil, mas coletar os dados de várias visitas, para depois canalizar para outro lugar, é um pouco mais complexo, principalmente devido à maneira como o padrão Visitor funciona.
  • O código acima não pode aceitar entrada diretamente. Para simplificar a demonstração aqui, é apenas um script bruto onde você cola seu bloco T-SQL como uma constante. O objetivo final é dar suporte à entrada de um arquivo, uma matriz de arquivos, uma pasta, uma matriz de pastas ou extrair definições de módulo de um banco de dados. E a saída pode ser em qualquer lugar:no console, em um arquivo, em um banco de dados… então o céu é o limite. Parte desse trabalho aconteceu nesse meio tempo, mas nada disso foi escrito na versão simples que você vê acima.
  • Não há tratamento de erros. Novamente, por brevidade e facilidade de consumo, o código aqui não se preocupa em lidar com exceções inevitáveis, embora a coisa mais destrutiva que pode acontecer em sua forma atual é que um lote não aparecerá na saída se não puder ser corretamente analisado (como CREATE STUPID PROCEDURE dbo.whatever ). Quando começamos a usar bancos de dados e/ou o sistema de arquivos, o tratamento adequado de erros se tornará muito mais importante.

Você pode se perguntar, onde vou manter o trabalho contínuo sobre isso e corrigir todas essas coisas? Bem, eu coloquei no GitHub, provisoriamente chamei o projeto de ParamParser , e já tem colaboradores ajudando com melhorias. A versão atual do código já parece bem diferente do exemplo acima e, no momento em que você ler isso, algumas das limitações mencionadas aqui já podem ter sido abordadas. Eu só quero manter o código em um lugar; esta dica é mais sobre mostrar uma amostra mínima de como isso pode funcionar, e destacar que existe um projeto por aí dedicado a simplificar essa tarefa.

No próximo segmento, falarei mais sobre como meu amigo e colega Will White me ajudou a passar do script autônomo que você vê acima para o módulo muito mais poderoso que você encontrará no GitHub.

Se você precisar analisar valores padrão de parâmetros nesse meio tempo, sinta-se à vontade para baixar o código e experimentá-lo. E como sugeri antes, experimente por conta própria, porque há muitas outras coisas poderosas que você pode fazer com essas classes e o padrão Visitor.

[ Parte 1 | Parte 2 | Parte 3]