Database
 sql >> Database >  >> RDS >> Database

Analizza i valori predefiniti dei parametri usando PowerShell – Parte 2

[ Parte 1 | Parte 2 | Parte 3]

Nel mio ultimo post, ho mostrato come usare TSqlParser e TSqlFragmentVisitor per estrarre informazioni importanti da uno script T-SQL contenente le definizioni delle procedure memorizzate. Con quello script, ho tralasciato alcune cose, come come analizzare l'OUTPUT e READONLY parole chiave per i parametri e come analizzare più oggetti insieme. Oggi volevo fornire uno script che gestisse queste cose, menzionare alcuni altri miglioramenti futuri e condividere un repository GitHub che ho creato per questo lavoro.

In precedenza, ho usato un semplice esempio come questo:

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

E con il codice visitatore che ho fornito, l'output sulla console era:

===========================
Riferimento procedura
===========================

dbo.procedure1


==========================
Parametro procedura
============================

Nome parametro:@param1
Tipo parametro:int
Predefinito:-64

E se lo script passato fosse più simile a questo? Combina la definizione di procedura intenzionalmente terribile di prima con un paio di altri elementi che potresti aspettarti di causare problemi, come i nomi dei tipi definiti dall'utente, due diverse forme di OUT /OUTPUT parola chiave, Unicode nei valori dei parametri (e nei nomi dei parametri!), parole chiave come costanti e valori letterali di 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

Lo script precedente non gestisce correttamente più oggetti e dobbiamo aggiungere alcuni elementi logici per tenere conto di OUTPUT e READONLY . In particolare, Output e ReadOnly non sono tipi di token, ma sono riconosciuti come un Identifier . Quindi abbiamo bisogno di una logica extra per trovare identificatori con quei nomi espliciti all'interno di qualsiasi ProcedureParameter frammento. Potresti notare alcune altre modifiche minori:

    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;
        }
      }
    }

Questo codice è solo a scopo dimostrativo e non ci sono possibilità che sia il più aggiornato. Consulta i dettagli di seguito sul download di una versione più recente.

L'output in questo caso:

===========================
Riferimento procedura
===========================
dbo.some_procedure


Nome parametro:@a
Tipo parametro:int
Predefinito:5


Nome parametro:@b
Tipo parametro:varchar(64)
Predefinito:'AS =/* BEGIN @a, int =7 */ "blat"'


Nome parametro:@c
Tipo parametro:int
Predefinito:6



===========================
Riferimento procedura
===========================
[dbo].another_procedure


Nome parametro:@p1
Tipo parametro:[int]
Predefinito:1


Nome parametro:@p2
Tipo parametro:datetime
Default:getdate
Output:sì


Nome parametro:@p3
Tipo parametro:data
Predefinito:{ts '2020-02-01 13:12:49'}


Nome parametro:@p4
Tipo parametro:dbo.tabletype
Sola lettura:sì


Nome parametro:@p5
Tipo parametro:geografia
Output:sì


Nome parametro:@p6
Tipo parametro:sysname
Predefinito:N'学中'

È un'analisi piuttosto potente, anche se ci sono alcuni casi limite noiosi e molta logica condizionale. Mi piacerebbe vedere TSqlFragmentVisitor ampliato in modo che alcuni dei suoi tipi di token abbiano proprietà aggiuntive (come SchemaObjectName.IsFirstAppearance e ProcedureParameter.DefaultValue ) e vedere nuovi tipi di token aggiunti (come FunctionReference ). Ma anche ora, questo è anni luce al di là di un parser di forza bruta che potresti scrivere in qualsiasi lingua, non importa T-SQL.

Tuttavia, ci sono ancora un paio di limitazioni che non ho ancora affrontato:

  • Indirizza solo le procedure archiviate. Il codice per gestire tutti e tre i tipi di funzioni definite dall'utente è simile , ma non c'è un pratico FunctionReference tipo di frammento, quindi devi invece identificare il primo SchemaObjectName frammento (o il primo set di Identifier e Dot token) e ignora eventuali istanze successive. Attualmente il codice in questo post sarà restituire tutte le informazioni sui parametri a una funzione, ma non lo farà restituisce il nome della funzione . Sentiti libero di usarlo per singleton o batch contenenti solo stored procedure, ma potresti trovare l'output confuso per più tipi di oggetti misti. L'ultima versione nel repository sottostante gestisce perfettamente le funzioni.
  • Questo codice non salva lo stato. L'output sulla console all'interno di ogni visita è facile, ma la raccolta dei dati da più visite, per poi pipeline altrove, è un po' più complessa, principalmente a causa del modo in cui funziona il pattern Visitor.
  • Il codice sopra non può accettare input direttamente. Per semplificare la dimostrazione qui, è solo uno script non elaborato in cui incolli il tuo blocco T-SQL come costante. L'obiettivo finale è supportare l'input da un file, un array di file, una cartella, un array di cartelle o estrarre le definizioni dei moduli da un database. E l'output può essere ovunque:sulla console, su un file, su un database... quindi il limite è il cielo. Alcuni di quei lavori sono avvenuti nel frattempo, ma niente di tutto ciò è stato scritto nella versione semplice che vedi sopra.
  • Nessuna gestione degli errori. Ancora una volta, per brevità e facilità di utilizzo, il codice qui non si preoccupa di gestire le inevitabili eccezioni, anche se la cosa più distruttiva che può accadere nella sua forma attuale è che un batch non apparirà nell'output se non può essere correttamente analizzato (come CREATE STUPID PROCEDURE dbo.whatever ). Quando iniziamo a utilizzare i database e/o il file system, la corretta gestione degli errori diventerà molto più importante.

Potresti chiederti, dove manterrò il lavoro in corso su questo e risolverò tutte queste cose? Bene, l'ho messo su GitHub, ho chiamato provvisoriamente il progetto ParamParser e hanno già contributori che aiutano con i miglioramenti. La versione corrente del codice sembra già molto diversa dall'esempio precedente e quando leggerai questo alcune delle limitazioni qui menzionate potrebbero già essere risolte. Voglio solo mantenere il codice in un posto; questo suggerimento riguarda più il mostrare un campione minimo di come può funzionare e l'evidenziazione che esiste un progetto dedicato alla semplificazione di questo compito.

Nel prossimo segmento parlerò di più su come il mio amico e collega, Will White, mi ha aiutato a passare dallo script autonomo che vedi sopra al modulo molto più potente che troverai su GitHub.

Se nel frattempo hai la necessità di analizzare i valori predefiniti dai parametri, sentiti libero di scaricare il codice e provarlo. E come ho suggerito prima, sperimenta da solo, perché ci sono molte altre cose potenti che puoi fare con queste classi e il modello Visitor.

[ Parte 1 | Parte 2 | Parte 3]