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

Parameter-Standardwerte mit PowerShell analysieren – Teil 1

[ Teil 1 | Teil 2 | Teil 3 ]

Wenn Sie jemals versucht haben, die Standardwerte für Parameter gespeicherter Prozeduren zu bestimmen, haben Sie wahrscheinlich Spuren auf Ihrer Stirn, weil Sie wiederholt und heftig damit auf Ihren Schreibtisch geschlagen haben. Die meisten Artikel, die sich mit dem Abrufen von Parameterinformationen befassen (wie dieser Tipp), erwähnen nicht einmal das Wort Standard. Dies liegt daran, dass sich die Informationen außer dem in der Objektdefinition gespeicherten Rohtext nirgendwo in den Katalogansichten befinden. Es gibt Spalten has_default_value und default_value in sys.parameters dieses Aussehen vielversprechend, aber sie werden immer nur für CLR-Module aufgefüllt.

Das Ableiten von Standardwerten mit T-SQL ist umständlich und fehleranfällig. Ich habe kürzlich eine Frage zu Stack Overflow zu diesem Problem beantwortet, und es hat mich in die Vergangenheit zurückversetzt. Bereits 2006 habe ich mich über mehrere Connect-Elemente über die mangelnde Sichtbarkeit der Standardwerte für Parameter in den Katalogansichten beschwert. Das Problem besteht jedoch weiterhin in SQL Server 2019. (Hier ist das einzige Element, das ich gefunden habe und das es in das neue Feedbacksystem geschafft hat.)

Obwohl es ein Nachteil ist, dass die Standardwerte nicht in den Metadaten angezeigt werden, sind sie höchstwahrscheinlich nicht vorhanden, da es schwierig ist, sie aus dem Objekttext (in jeder Sprache, aber insbesondere in T-SQL) zu analysieren. Es ist schwierig, überhaupt den Anfang und das Ende der Parameterliste zu finden, da die Parsing-Fähigkeit von T-SQL so begrenzt ist und es mehr Grenzfälle gibt, als Sie sich vorstellen können. Ein paar Beispiele:

  • Sie können sich nicht auf das Vorhandensein von ( verlassen und ) um die Parameterliste anzuzeigen, da sie optional sind (und in der gesamten Parameterliste zu finden sind)
  • Sie können nicht einfach nach dem ersten AS parsen um den Anfang des Körpers zu markieren, da es aus anderen Gründen erscheinen kann
  • Sie können sich nicht auf das Vorhandensein von BEGIN verlassen um den Anfang des Körpers zu markieren, da es optional ist
  • Es ist schwierig, Kommas zu trennen, da sie in Kommentaren, in Zeichenfolgenliteralen und als Teil von Datentypdeklarationen auftreten können (denken Sie an (precision, scale) )
  • Es ist sehr schwierig, beide Arten von Kommentaren wegzuparsen, die überall erscheinen können (auch innerhalb von String-Literalen) und verschachtelt sein können
  • Sie können versehentlich wichtige Schlüsselwörter, Kommas und Gleichheitszeichen in Zeichenfolgenliteralen und Kommentaren finden
  • Sie können Standardwerte haben, die keine Zahlen oder Zeichenfolgenliterale sind (denken Sie an {fn curdate()} oder GETDATE )

Es gibt so viele kleine Syntaxvariationen, dass normale String-Parsing-Techniken unwirksam werden. Habe ich AS gesehen bereits? War es zwischen einem Parameternamen und einem Datentyp? War es nach einer rechten Klammer, die die gesamte Parameterliste umgibt, oder [eine?] die keine Übereinstimmung hatte, bevor ich das letzte Mal einen Parameter gesehen habe? Trennt dieses Komma zwei Parameter oder ist es Teil von Genauigkeit und Skalierung? Wenn Sie eine Zeichenfolge Wort für Wort durchlaufen, geht es weiter und weiter, und es gibt so viele Bits, die Sie verfolgen müssen.

Nehmen Sie dieses (absichtlich lächerliche, aber dennoch syntaktisch gültige) Beispiel:

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

Das Analysieren der Standardwerte aus dieser Definition mit T-SQL ist schwierig. Wirklich schwer . Ohne BEGIN um das Ende der Parameterliste richtig zu markieren, das ganze Kommentar-Chaos und alle Fälle, in denen Schlüsselwörter wie AS verschiedene Dinge bedeuten können, werden Sie wahrscheinlich einen komplexen Satz verschachtelter Ausdrücke haben, die mehr SUBSTRING beinhalten und CHARINDEX Muster, als Sie jemals zuvor an einem Ort gesehen haben. Und Sie werden wahrscheinlich immer noch mit @d enden und @e sieht aus wie Prozedurparameter anstelle von lokalen Variablen.

Als ich weiter über das Problem nachdachte und nachschaute, ob jemand in den letzten zehn Jahren etwas Neues geschafft hatte, stieß ich auf diesen großartigen Beitrag von Michael Swart. In diesem Beitrag verwendet Michael den TSqlParser von ScriptDom, um sowohl einzeilige als auch mehrzeilige Kommentare aus einem T-SQL-Block zu entfernen. Also habe ich PowerShell-Code geschrieben, um eine Prozedur schrittweise zu durchlaufen und zu sehen, welche anderen Token identifiziert wurden. Nehmen wir ein einfacheres Beispiel ohne all die absichtlichen Probleme:

CREATE PROCEDURE dbo.procedure1
  @param1 int
AS PRINT 1;
GO

Öffnen Sie Visual Studio Code (oder Ihre bevorzugte PowerShell-IDE) und speichern Sie eine neue Datei mit dem Namen Test1.ps1. Die einzige Voraussetzung ist, dass sich die neueste Version von Microsoft.SqlServer.TransactSql.ScriptDom.dll (die Sie hier herunterladen und aus sqlpackage extrahieren können) im selben Ordner wie die .ps1-Datei befindet. Kopieren Sie diesen Code, speichern Sie ihn und führen Sie ihn dann aus oder debuggen Sie ihn:

# need to extract this DLL from latest sqlpackage; place it in same folder
# https://docs.microsoft.com/en-us/sql/tools/sqlpackage-download
Add-Type -Path "Microsoft.SqlServer.TransactSql.ScriptDom.dll";
 
# set up a parser object using the most recent version available 
$parser = [Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser]($true)::New(); 
 
# and an error collector
$errors = [System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]]::New();
 
# this ultimately won't come from a constant - think file, folder, database
# can be a batch or multiple batches, just keeping it simple to start
 
$procedure = @"
CREATE PROCEDURE dbo.procedure1
  @param1 AS int
AS PRINT 1;
GO
"@
 
# now we need to try parsing
$block = $parser.Parse([System.IO.StringReader]::New($procedure), [ref]$errors);
 
# parse the whole thing, which is a set of one or more batches
foreach ($batch in $block.Batches)
{
    # each batch contains one or more statements
    # (though a valid create procedure statement is also always just one batch)
    foreach ($statement in $batch.Statements)
    {
        # output the type of statement
        Write-Host "  ====================================";
        Write-Host "    $($statement.GetType().Name)";
        Write-Host "  ====================================";        
 
        # each statement has one or more tokens in its token stream
        foreach ($token in $statement.ScriptTokenStream)
        {
            # those tokens have properties to indicate the type
            # as well as the actual text of the token
            Write-Host "  $($token.TokenType.ToString().PadRight(16)) : $($token.Text)";
        }
    }
}

Die Ergebnisse:

===================================
CreateProcedureStatement
===================================

Erstellen :CREATE
WhiteSpace :
Procedure :PROCEDURE
WhiteSpace :
Identifier :dbo
Dot :.
Identifier :procedure1
WhiteSpace :
WhiteSpace :
Variable :@param1
WhiteSpace :
As :AS
WhiteSpace :
Identifier :int
WhiteSpace :
As :AS
WhiteSpace :
Print :PRINT
WhiteSpace :
Integer :1
Semikolon :;
WhiteSpace :
Go :GO
EndOfFile :

Um etwas von dem Rauschen loszuwerden, können wir ein paar TokenTypes innerhalb der letzten for-Schleife herausfiltern:

      foreach ($token in $statement.ScriptTokenStream)
      {
         if ($token.TokenType -notin "WhiteSpace", "Go", "EndOfFile", "SemiColon")
         {
           Write-Host "  $($token.TokenType.ToString().PadRight(16)) : $($token.Text)";
         }
      }

Am Ende eine prägnantere Reihe von Tokens:

===================================
CreateProcedureStatement
===================================

Erstellen :CREATE
Prozedur :PROCEDURE
Identifikator :dbo
Punkt :.
Identifikator :procedure1
Variable :@param1
As :AS
Bezeichner :int
As :AS
Print :PRINT
Integer :1

Die Art und Weise, wie dies visuell einer Prozedur zugeordnet wird:

Jedes Token, das aus diesem einfachen Prozedurhauptteil geparst wird.

Sie können bereits die Probleme sehen, die wir haben werden, wenn wir versuchen, Parameternamen und Datentypen zu rekonstruieren und sogar das Ende der Parameterliste zu finden. Nachdem ich mich näher damit befasst hatte, stieß ich auf einen Beitrag von Dan Guzman, der eine ScriptDom-Klasse namens TSqlFragmentVisitor hervorhob, die Fragmente eines Blocks von geparstem T-SQL identifiziert. Wenn wir die Taktik nur ein wenig ändern, können wir Fragmente inspizieren statt Token . Ein Fragment ist im Wesentlichen ein Satz aus einem oder mehreren Token und hat auch seine eigene Typhierarchie. Soweit ich weiß, gibt es keinen ScriptFragmentStream um Fragmente zu durchlaufen, aber wir können einen Visitor verwenden Muster, um im Wesentlichen dasselbe zu tun. Lassen Sie uns eine neue Datei namens Test2.ps1 erstellen, diesen Code einfügen und ausführen:

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 = @"
CREATE PROCEDURE dbo.procedure1
  @param1 AS int
AS PRINT 1;
GO
"@
 
$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)
   {
       Write-Host $fragment.GetType().Name;
   }
}

Ergebnisse (interessante für diese Übung fett gedruckt ):

TSqlScript
TSqlBatch
CreateProcedureStatement
ProcedureReference
SchemaObjectName
Identifier
Identifier
ProcedureParameter
Bezeichner
SqlDataTypeReference
SchemaObjectName
Bezeichner
StatementList
PrintStatement
IntegerLiteral

Wenn wir versuchen, dies visuell auf unser vorheriges Diagramm abzubilden, wird es etwas komplexer. Jedes dieser Fragmente ist selbst ein Strom aus einem oder mehreren Tokens, und manchmal überlappen sie sich. Mehrere Anweisungstoken und Schlüsselwörter werden nicht einmal als Teil eines Fragments erkannt, wie CREATE , PROCEDURE , AS , und GO . Letzteres ist verständlich, da es überhaupt kein T-SQL ist, aber der Parser muss trotzdem verstehen, dass er Batches trennt.

Vergleich der Erkennung von Anweisungstoken und Fragmenttoken.

Um ein beliebiges Fragment im Code neu zu erstellen, können wir seine Token während eines Besuchs dieses Fragments durchlaufen. Dadurch können wir Dinge wie den Namen des Objekts und die Parameterfragmente mit viel weniger mühsamer Analyse und Bedingungen ableiten, obwohl wir immer noch den Token-Stream jedes Fragments durchlaufen müssen. Wenn wir Write-Host $fragment.GetType().Name; ändern im vorherigen Skript dazu:

[void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragment] $fragment)
{
  if ($fragment.GetType().Name -in ("ProcedureParameter", "ProcedureReference"))
  {
    $output = "";
    Write-Host "==========================";
    Write-Host "  $($fragment.GetType().Name)";
    Write-Host "==========================";
 
    for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
    {
      $token = $fragment.ScriptTokenStream[$i];
      $output += $token.Text;
    }
    Write-Host $output;
  }
}

Die Ausgabe ist:

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

dbo.procedure1

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

@param1 AS int

Wir haben den Objekt- und den Schemanamen zusammen, ohne eine zusätzliche Iteration oder Verkettung durchführen zu müssen. Und wir haben die gesamte Zeile in jeder Parameterdeklaration, einschließlich des Parameternamens, des Datentyps und aller möglicherweise vorhandenen Standardwerte. Interessanterweise handhabt der Besucher @param1 int und int als zwei unterschiedliche Fragmente, wodurch der Datentyp im Wesentlichen doppelt gezählt wird. Ersteres ist ein ProcedureParameter fragment, und letzteres ist ein SchemaObjectName . Uns interessiert wirklich nur das erste SchemaObjectName Referenz (dbo.procedure1 ) oder genauer gesagt nur der, der auf ProcedureReference folgt . Ich verspreche, dass wir uns mit denen befassen werden, nur nicht mit allen heute. Wenn wir die $procedure ändern Konstante dazu (Hinzufügen eines Kommentars und eines Standardwerts):

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

Dann wird die Ausgabe:

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

dbo.procedure1

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

@param1 AS int =/* Kommentar */ -64

Dies schließt immer noch alle Token in der Ausgabe ein, die eigentlich Kommentare sind. Innerhalb der for-Schleife können wir alle Token-Typen herausfiltern, die wir ignorieren möchten, um dies zu beheben (ich entferne auch überflüssiges AS Schlüsselwörter in diesem Beispiel, aber Sie möchten das vielleicht nicht tun, wenn Sie Modulkörper rekonstruieren):

for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
{
  $token = $fragment.ScriptTokenStream[$i];
  if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As"))
  {
    $output += $token.Text;
  }
}

Die Ausgabe ist sauberer, aber immer noch nicht perfekt.

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

dbo.procedure1

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

@param1 int =-64

Wenn wir Parametername, Datentyp und Standardwert trennen wollen, wird es komplexer. Während wir den Token-Stream für ein beliebiges Fragment durchlaufen, können wir den Parameternamen von allen Datentypdeklarationen trennen, indem wir einfach nachverfolgen, wann wir auf ein EqualsSign treffen Zeichen. Ersetzen der for-Schleife durch diese zusätzliche Logik:

if ($fragment.GetType().Name -in ("ProcedureParameter","SchemaObjectName"))
{
    $output  = "";
    $param   = ""; 
    $type    = "";
    $default = "";
    $seenEquals = $false;
 
      for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
      {
        $token = $fragment.ScriptTokenStream[$i];
        if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As"))
        {
          if ($fragment.GetType().Name -eq "ProcedureParameter")
          {
            if (!$seenEquals)
            {
              if ($token.TokenType -eq "EqualsSign") 
              { 
                $seenEquals = $true; 
              }
              else 
              { 
                if ($token.TokenType -eq "Variable") 
                {
                  $param += $token.Text; 
                }
                else 
                {
                  $type += $token.Text; 
                }
              }
            }
            else
            { 
              if ($token.TokenType -ne "EqualsSign")
              {
                $default += $token.Text; 
              }
            }
          }
          else 
          {
            $output += $token.Text.Trim(); 
          }
        }
      }
 
      if ($param.Length   -gt 0) { $output  = "Param name: "   + $param.Trim(); }
      if ($type.Length    -gt 0) { $type    = "`nParam type: " + $type.Trim(); }
      if ($default.Length -gt 0) { $default = "`nDefault:    " + $default.TrimStart(); }
      Write-Host $output $type $default;
}

Jetzt ist die Ausgabe:

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

dbo.procedure1

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

Parametername:@param1
Paramtyp:int
Standard:-64

Das ist besser, aber es gibt noch mehr zu lösen. Es gibt Parameterschlüsselwörter, die ich bisher ignoriert habe, wie OUTPUT und READONLY , und wir brauchen Logik, wenn unsere Eingabe ein Stapel mit mehr als einer Prozedur ist. Ich werde diese Probleme in Teil 2 behandeln.

Experimentieren Sie in der Zwischenzeit! Es gibt viele andere leistungsstarke Dinge, die Sie mit ScriptDOM, TSqlParser und TSqlFragmentVisitor tun können.

[ Teil 1 | Teil 2 | Teil 3 ]