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

Parameter-Standardwerte mit PowerShell analysieren – Teil 2

[ Teil 1 | Teil 2 | Teil 3 ]

In meinem letzten Beitrag habe ich gezeigt, wie man TSqlParser verwendet und TSqlFragmentVisitor um wichtige Informationen aus einem T-SQL-Skript zu extrahieren, das Definitionen gespeicherter Prozeduren enthält. Bei diesem Skript habe ich ein paar Dinge weggelassen, wie zum Beispiel, wie man OUTPUT parst und READONLY Schlüsselwörter für Parameter und wie man mehrere Objekte zusammen parst. Heute wollte ich ein Skript bereitstellen, das diese Dinge handhabt, einige weitere zukünftige Verbesserungen erwähnen und ein GitHub-Repository teilen, das ich für diese Arbeit erstellt habe.

Zuvor habe ich ein einfaches Beispiel wie dieses verwendet:

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

Und mit dem von mir bereitgestellten Besuchercode war die Ausgabe an die Konsole:

=========================
VerfahrenReferenz
==========================

dbo.procedure1


=========================
Prozedurparameter
==========================

Parametername:@param1
Paramtyp:int
Standard:-64

Nun, was wäre, wenn das übergebene Skript eher so aussehen würde? Es kombiniert die absichtlich schreckliche Prozedurdefinition von vorher mit ein paar anderen Elementen, von denen Sie erwarten könnten, dass sie Probleme verursachen, wie benutzerdefinierte Typnamen, zwei verschiedene Formen von OUT /OUTPUT Schlüsselwort, Unicode in Parameterwerten (und in Parameternamen!), Schlüsselwörter als Konstanten und ODBC-Escape-Literale.

/* 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

Das vorherige Skript verarbeitet mehrere Objekte nicht ganz richtig, und wir müssen einige logische Elemente hinzufügen, um OUTPUT zu berücksichtigen und READONLY . Insbesondere Output und ReadOnly sind keine Token-Typen, sondern werden als Identifier erkannt . Wir brauchen also etwas zusätzliche Logik, um Bezeichner mit diesen expliziten Namen in jedem ProcedureParameter zu finden Fragment. Vielleicht entdecken Sie noch ein paar andere kleinere Änderungen:

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

Dieser Code dient nur zu Demonstrationszwecken und es besteht keine Chance, dass er der aktuellste ist. Einzelheiten zum Herunterladen einer neueren Version finden Sie weiter unten.

Die Ausgabe in diesem Fall:

=========================
VerfahrenReferenz
==========================
dbo.some_procedure


Parametername:@a
Parametertyp:int
Standard:5


Parametername:@b
Parametertyp:varchar(64)
Standard:'AS =/* BEGIN @a, int =7 */ "blat"'


Parametername:@c
Parametertyp:int
Standard:6



==========================
Verfahrensreferenz
==========================
[dbo].another_procedure


Parametername:@p1
Parametertyp:[int]
Standard:1


Parametername:@p2
Parametertyp:datetime
Standard:getdate
Ausgabe:ja


Parametername:@p3
Paramtyp:Datum
Standard:{ts '2020-02-01 13:12:49'}


Parametername:@p4
Paramtyp:dbo.tabletype
Nur lesen:ja


Parametername:@p5
Paramtyp:Geografie
Ausgabe:ja


Parametername:@p6
Paramtyp:Systemname
Standard:N'学中'

Das ist ein ziemlich leistungsfähiges Parsing, auch wenn es einige langweilige Randfälle und viel bedingte Logik gibt. Ich würde gerne TSqlFragmentVisitor sehen erweitert, sodass einige seiner Token-Typen zusätzliche Eigenschaften haben (wie SchemaObjectName.IsFirstAppearance und ProcedureParameter.DefaultValue ) und sehen Sie neue hinzugefügte Token-Typen (wie FunctionReference ). Aber selbst jetzt ist dies Lichtjahre jenseits eines Brute-Force-Parsers, den Sie in beliebiger schreiben könnten Sprache, geschweige denn T-SQL.

Es gibt jedoch noch ein paar Einschränkungen, die ich noch nicht angesprochen habe:

  • Dies betrifft nur gespeicherte Prozeduren. Der Code zur Behandlung aller drei Arten von benutzerdefinierten Funktionen ist ähnlich , aber es gibt keine praktische FunctionReference Fragmenttyp, sodass Sie stattdessen den ersten SchemaObjectName identifizieren müssen fragment (oder der erste Satz von Identifier und Dot Token) und ignorieren Sie alle nachfolgenden Instanzen. Derzeit wird der Code in diesem Beitrag wird alle Informationen über die Parameter zurückgeben zu einer Funktion, aber es wird nicht gibt den Namen der Funktion zurück . Sie können es gerne für Singletons oder Batches verwenden, die nur gespeicherte Prozeduren enthalten, aber Sie finden die Ausgabe möglicherweise verwirrend für mehrere gemischte Objekttypen. Die neueste Version im Repository unten funktioniert einwandfrei.
  • Dieser Code speichert den Status nicht. Die Ausgabe an die Konsole innerhalb jedes Besuchs ist einfach, aber das Sammeln der Daten von mehreren Besuchen, um sie dann an anderer Stelle weiterzuleiten, ist etwas komplexer, hauptsächlich aufgrund der Funktionsweise des Besuchermusters.
  • Der obige Code kann Eingaben nicht direkt akzeptieren. Um die Demonstration hier zu vereinfachen, ist es nur ein rohes Skript, in das Sie Ihren T-SQL-Block als Konstante einfügen. Das letztendliche Ziel besteht darin, die Eingabe aus einer Datei, einem Array von Dateien, einem Ordner, einem Array von Ordnern oder das Abrufen von Moduldefinitionen aus einer Datenbank zu unterstützen. Und die Ausgabe kann überall erfolgen:auf der Konsole, in einer Datei, in einer Datenbank … also sind dem Himmel keine Grenzen gesetzt. Ein Teil dieser Arbeit ist in der Zwischenzeit geschehen, aber nichts davon wurde in der einfachen Version geschrieben, die Sie oben sehen.
  • Es gibt keine Fehlerbehandlung. Auch hier kümmert sich der Code aus Gründen der Kürze und Benutzerfreundlichkeit nicht um die Behandlung unvermeidlicher Ausnahmen, obwohl das Zerstörerischste, was in seiner jetzigen Form passieren kann, darin besteht, dass ein Stapel nicht in der Ausgabe erscheint, wenn er nicht richtig sein kann geparst (wie CREATE STUPID PROCEDURE dbo.whatever ). Wenn wir beginnen, Datenbanken und/oder das Dateisystem zu verwenden, wird die richtige Fehlerbehandlung umso wichtiger.

Sie fragen sich vielleicht, wo ich die laufende Arbeit daran aufrechterhalten und all diese Dinge beheben soll? Nun, ich habe es auf GitHub gestellt, habe das Projekt versuchsweise ParamParser genannt , und haben bereits Mitwirkende, die bei Verbesserungen helfen. Die aktuelle Version des Codes sieht bereits ganz anders aus als das obige Beispiel, und wenn Sie dies lesen, sind einige der hier erwähnten Einschränkungen möglicherweise bereits behoben. Ich möchte den Code nur an einer Stelle pflegen; Bei diesem Tipp geht es eher darum, ein minimales Beispiel dafür zu zeigen, wie es funktionieren kann, und hervorzuheben, dass es ein Projekt gibt, das sich der Vereinfachung dieser Aufgabe widmet.

Im nächsten Abschnitt werde ich mehr darüber sprechen, wie mein Freund und Kollege Will White mir geholfen hat, von dem oben gezeigten eigenständigen Skript zu dem viel leistungsfähigeren Modul zu gelangen, das Sie auf GitHub finden.

Wenn Sie in der Zwischenzeit Standardwerte aus Parametern parsen müssen, können Sie den Code herunterladen und ausprobieren. Und wie ich bereits angedeutet habe, experimentieren Sie selbst, denn es gibt viele andere mächtige Dinge, die Sie mit diesen Klassen und dem Visitor-Muster machen können.

[ Teil 1 | Teil 2 | Teil 3 ]