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

Parse parameterstandardværdier ved hjælp af PowerShell – Del 1

[ Del 1 | Del 2 | Del 3 ]

Hvis du nogensinde har forsøgt at bestemme standardværdierne for lagrede procedureparametre, har du sandsynligvis mærker på din pande fra at slå den på dit skrivebord gentagne gange og voldsomt. De fleste artikler, der taler om at hente parameteroplysninger (som dette tip), nævner ikke engang ordet standard. Dette er fordi, bortset fra den rå tekst, der er gemt i objektets definition, er informationen ikke nogen steder i katalogvisningerne. Der er kolonner has_default_value og default_value i sys.parameters det look lovende, men de er kun udfyldt til CLR-moduler.

At udlede standardværdier ved hjælp af T-SQL er besværligt og udsat for fejl. Jeg besvarede for nylig et spørgsmål på Stack Overflow om dette problem, og det tog mig ned af hukommelsesbanen. Tilbage i 2006 klagede jeg via flere Connect-elementer over manglen på synlighed af standardværdierne for parametre i katalogvisningerne. Problemet eksisterer dog stadig i SQL Server 2019. (Her er det eneste element, jeg har fundet, der kom til det nye feedbacksystem.)

Selvom det er en ulempe, at standardværdierne ikke er eksponeret i metadataene, er de højst sandsynligt ikke der, fordi det er svært at parse dem ud af objektteksten (på ethvert sprog, men især i T-SQL). Det er svært overhovedet at finde starten og slutningen af ​​parameterlisten, fordi T-SQL's parsing-kapacitet er så begrænset, og der er flere edge cases, end du kan forestille dig. Et par eksempler:

  • Du kan ikke stole på tilstedeværelsen af ​​( og ) for at angive parameterlisten, da de er valgfrie (og kan findes i hele parameterlisten)
  • Du kan ikke let parse for den første AS at markere begyndelsen af ​​kroppen, da den kan dukke op af andre årsager
  • Du kan ikke stole på tilstedeværelsen af ​​BEGIN at markere begyndelsen af ​​kroppen, da det er valgfrit
  • Det er svært at opdele på kommaer, da de kan optræde i kommentarer, inden for strenge bogstaver og som en del af datatypeerklæringer (tænk (precision, scale) )
  • Det er meget svært at parse begge typer kommentarer væk, som kan optræde hvor som helst (inklusive inde i strenge bogstaver) og kan indlejres
  • Du kan ved et uheld finde vigtige søgeord, kommaer og lighedstegn inde i strenge bogstaver og kommentarer
  • Du kan have standardværdier, der ikke er tal eller strengliteraler (tænk {fn curdate()} eller GETDATE )

Der er så mange små syntaksvariationer, at normale strengparsingteknikker bliver ineffektive. Har jeg set AS allerede? Var det mellem et parameternavn og en datatype? Var det efter en højre parentes, der omgiver hele parameterlisten, eller [en?], der ikke havde et match før sidste gang, jeg så en parameter? Er det komma, der adskiller to parametre, eller er det en del af præcision og skala? Når du går gennem en streng et ord ad gangen, bliver det ved og ved, og der er så mange bits, du skal spore.

Tag dette (med vilje latterlige, men stadig syntaktisk gyldige) eksempel:

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

Det er svært at analysere standardværdierne ud af denne definition ved hjælp af T-SQL. Virkelig svært . Uden BEGIN for korrekt at markere slutningen af ​​parameterlisten, al kommentarroden og alle de tilfælde, hvor nøgleord som AS kan betyde forskellige ting, vil du sandsynligvis have et komplekst sæt af indlejrede udtryk, der involverer mere SUBSTRING og CHARINDEX mønstre, end du nogensinde har set ét sted før. Og du ender sandsynligvis stadig med @d og @e ligner procedureparametre i stedet for lokale variabler.

Da jeg tænkte lidt mere over problemet og søgte for at se, om nogen havde formået noget nyt i det sidste årti, stødte jeg på dette fantastiske indlæg af Michael Swart. I det indlæg bruger Michael ScriptDom's TSqlParser til at fjerne både enkeltlinje- og flerlinjekommentarer fra en blok af T-SQL. Så jeg skrev noget PowerShell-kode for at gå gennem en procedure for at se, hvilke andre tokens der blev identificeret. Lad os tage et enklere eksempel uden alle de tilsigtede problemer:

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

Åbn Visual Studio Code (eller din foretrukne PowerShell IDE), og gem en ny fil kaldet Test1.ps1. Den eneste forudsætning er at have den seneste version af Microsoft.SqlServer.TransactSql.ScriptDom.dll (som du kan downloade og udtrække fra sqlpackage her) i samme mappe som .ps1-filen. Kopier denne kode, gem, og kør eller fejlfind derefter:

# 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)";
        }
    }
}

Resultaterne:

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

Opret :CREATE
WhiteSpace :
Procedure :PROCEDURE
WhiteSpace :
Identifier :dbo
Punktum :.
Identifier :procedure1
WhiteSpace :
WhiteSpace :
Variabel :@param1
WhiteSpace :
Som :AS
WhiteSpace :
Identifikator :int
WhiteSpace :
As :AS
WhiteSpace :
Udskriv :PRINT
WhiteSpace :
Heltal :1
Semikolon :;
WhiteSpace :
Go :GO
EndOfFile :

For at slippe af med noget af støjen kan vi filtrere nogle få TokenTypes fra i den sidste for-løkke:

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

Ender med en mere kortfattet serie af tokens:

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

Opret :OPRET
Procedure :PROCEDURE
Identifikator :dbo
Punktum :.
Identifikator :procedure1
Variabel :@param1
Som :AS
Identifikator :int
As :AS
Udskriv :PRINT
Heltal :1

Måden, hvorpå dette knytter sig til en procedure visuelt:

Hvert token er parset fra denne enkle proceduretekst.

Du kan allerede se de problemer, vi har med at prøve at rekonstruere parameternavne, datatyper og endda finde slutningen af ​​parameterlisten. Efter at have set nærmere på dette, stødte jeg på et indlæg af Dan Guzman, der fremhævede en ScriptDom-klasse kaldet TSqlFragmentVisitor, som identificerer fragmenter af en blok af parset T-SQL. Hvis vi ændrer taktikken en smule, kan vi inspicere fragmenter i stedet for tokens . Et fragment er i det væsentlige et sæt af et eller flere tokens og har også sit eget typehierarki. Så vidt jeg ved, er der ingen ScriptFragmentStream at iterere gennem fragmenter, men vi kan bruge en Visitor mønster til at gøre stort set det samme. Lad os oprette en ny fil kaldet Test2.ps1, indsætte denne kode og køre den:

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

Resultater (interessante for denne øvelsei fed skrift ):

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

Hvis vi forsøger at kortlægge dette visuelt til vores tidligere diagram, bliver det lidt mere komplekst. Hvert af disse fragmenter er i sig selv en strøm af et eller flere tokens, og nogle gange vil de overlappe hinanden. Adskillige erklæringstokens og nøgleord genkendes ikke engang alene som en del af et fragment, som f.eks. CREATE , PROCEDURE , AS og GO . Sidstnævnte er forståeligt, da det slet ikke er T-SQL, men parseren skal stadig forstå, at den adskiller batches.

Sammenligning af den måde, sætningstokens og fragmenttokens genkendes på.

For at genopbygge ethvert fragment i kode, kan vi iterere gennem dets tokens under et besøg på det fragment. Dette lader os udlede ting som navnet på objektet og parameterfragmenterne med meget mindre kedelige parsing og betingelser, selvom vi stadig er nødt til at sløjfe inde i hvert fragments token-strøm. Hvis vi ændrer Write-Host $fragment.GetType().Name; i det forrige script til dette:

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

Outputtet er:

===========================
Procedurehenvisning
==========================

dbo.procedure1

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

@param1 AS int

Vi har objektet og skemanavnet sammen uden at skulle udføre yderligere iteration eller sammenkædning. Og vi har hele linjen involveret i enhver parametererklæring, inklusive parameternavnet, datatypen og enhver standardværdi, der måtte eksistere. Interessant nok håndterer den besøgende @param1 int og int som to adskilte fragmenter, der i det væsentlige dobbelttæller datatypen. Førstnævnte er en ProcedureParameter fragment, og sidstnævnte er et SchemaObjectName . Vi bekymrer os egentlig kun om den første SchemaObjectName reference (dbo.procedure1 ) eller mere specifikt kun den, der følger ProcedureReference . Jeg lover, at vi vil håndtere dem, bare ikke dem alle i dag. Hvis vi ændrer $procedure konstant til dette (tilføje en kommentar og en standardværdi):

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

Så bliver outputtet:

===========================
Procedurehenvisning
==========================

dbo.procedure1

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

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

Dette inkluderer stadig alle tokens i outputtet, der faktisk er kommentarer. Inde i for-løkken kan vi bortfiltrere alle token-typer, vi ønsker at ignorere for at løse dette (jeg fjerner også overflødig AS søgeord i dette eksempel, men du ønsker måske ikke at gøre det, hvis du rekonstruerer modulkroppe):

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

Outputtet er renere, men stadig ikke perfekt.

===========================
Procedurehenvisning
==========================

dbo.procedure1

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

@param1 int =-64

Hvis vi vil adskille parameternavnet, datatypen og standardværdien, bliver det mere komplekst. Mens vi går gennem tokenstrømmen for et givet fragment, kan vi opdele parameternavnet fra alle datatypedeklarationer ved blot at spore, hvornår vi rammer et EqualsSign polet. Udskiftning af for-løkken med denne ekstra 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;
}

Nu er outputtet:

===========================
Procedurehenvisning
==========================

dbo.procedure1

===========================
Procedureparameter
==========================

Param navn:@param1
Param type:int
Standard:-64

Det er bedre, men der er stadig mere at løse. Der er parameternøgleord, jeg har ignoreret indtil videre, såsom OUTPUT og READONLY , og vi har brug for logik, når vores input er en batch med mere end én procedure. Jeg vil behandle disse problemer i del 2.

I mellemtiden, eksperimenter! Der er en masse andre kraftfulde ting, du kan gøre med ScriptDOM, TSqlParser og TSqlFragmentVisitor.

[ Del 1 | Del 2 | Del 3 ]


  1. Sådan automatiseres databasefailover med ClusterControl

  2. Forstå SQL-datatyper – alt hvad du behøver at vide om SQL-datatyper

  3. Sådan fungerer CAST() i SQL Server

  4. Brug af Dapper med Oracle