sql >> Databasteknik >  >> RDS >> Database

Analysera parameterns standardvärden med PowerShell – Del 1

[ Del 1 | Del 2 | Del 3 ]

Om du någonsin har försökt fastställa standardvärdena för lagrade procedurparametrar, har du förmodligen märken i pannan från att du slår den på skrivbordet upprepade gånger och våldsamt. De flesta artiklar som talar om att hämta parameterinformation (som detta tips) nämner inte ens ordet standard. Detta beror på att informationen inte finns någonstans i katalogvyerna, förutom den råtext som lagras i objektets definition. Det finns kolumner has_default_value och default_value i sys.parameters det utseendet lovande, men de är bara alltid befolkade för CLR-moduler.

Att härleda standardvärden med T-SQL är besvärligt och felbenäget. Jag svarade nyligen på en fråga på Stack Overflow om det här problemet, och det tog mig ner i minnet. Redan 2006 klagade jag via flera Connect-objekt över bristen på synlighet av standardvärdena för parametrar i katalogvyerna. Problemet finns dock fortfarande i SQL Server 2019. (Här är det enda föremål jag har hittat som kom till det nya feedbacksystemet.)

Även om det är ett besvär att standardvärdena inte exponeras i metadata, är de troligen inte där eftersom det är svårt att analysera dem från objekttexten (på vilket språk som helst, men särskilt i T-SQL). Det är svårt att ens hitta början och slutet av parameterlistan eftersom T-SQL:s analysförmåga är så begränsad, och det finns fler kantfall än du kan föreställa dig. Några exempel:

  • Du kan inte lita på närvaron av ( och ) för att indikera parameterlistan, eftersom de är valfria (och kan finnas i hela parameterlistan)
  • Du kan inte enkelt analysera för den första AS för att markera början av kroppen, eftersom det kan dyka upp av andra skäl
  • Du kan inte lita på närvaron av BEGIN för att markera början av kroppen, eftersom det är valfritt
  • Det är svårt att dela på kommatecken, eftersom de kan visas i kommentarer, inom strängliteraler och som en del av datatypsdeklarationer (tänk (precision, scale) )
  • Det är mycket svårt att tolka bort båda typerna av kommentarer, som kan visas var som helst (inklusive inuti strängliteraler) och kan kapslas
  • Du kan oavsiktligt hitta viktiga nyckelord, kommatecken och likhetstecken inuti strängbokstavar och kommentarer
  • Du kan ha standardvärden som inte är siffror eller strängliteraler (tänk {fn curdate()} eller GETDATE )

Det finns så många små syntaxvariationer att normala stränganalystekniker blir ineffektiva. Har jag sett AS redan? Var det mellan ett parameternamn och en datatyp? Var det efter en högerparentes som omger hela parameterlistan, eller [en?] som inte hade en matchning innan jag såg en parameter senast? Skiljer det kommatecken åt två parametrar eller är det en del av precision och skala? När du går igenom en sträng ett ord i taget, fortsätter det och fortsätter, och det finns så många bitar du behöver spåra.

Ta detta (avsiktligt löjliga, men fortfarande syntaktiskt giltiga) exempel:

/* 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 är svårt att analysera standardvärdena från den definitionen med T-SQL. Riktigt svårt . Utan BEGIN för att korrekt markera slutet på parameterlistan, all kommentarsröset och alla fall där nyckelord som AS kan betyda olika saker, kommer du förmodligen att ha en komplex uppsättning kapslade uttryck som involverar mer SUBSTRING och CHARINDEX mönster än du någonsin sett på ett ställe tidigare. Och du kommer förmodligen fortfarande att sluta med @d och @e ser ut som procedurparametrar istället för lokala variabler.

När jag funderade lite mer på problemet och letade efter om någon hade lyckats med något nytt under det senaste decenniet, kom jag över detta fantastiska inlägg av Michael Swart. I det inlägget använder Michael ScriptDoms TSqlParser för att ta bort både enradiga och flerradiga kommentarer från ett block av T-SQL. Så jag skrev lite PowerShell-kod för att gå igenom en procedur för att se vilka andra tokens som identifierades. Låt oss ta ett enklare exempel utan alla avsiktliga problem:

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

Öppna Visual Studio Code (eller din favorit PowerShell IDE) och spara en ny fil som heter Test1.ps1. Den enda förutsättningen är att ha den senaste versionen av Microsoft.SqlServer.TransactSql.ScriptDom.dll (som du kan ladda ner och extrahera från sqlpackage här) i samma mapp som .ps1-filen. Kopiera den här koden, spara och kör eller felsök sedan:

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

Resultaten:

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

Skapa :CREATE
WhiteSpace :
Procedur :PROCEDURE
WhiteSpace :
Identifier :dbo
Punkt :.
Identifier :procedure1
WhiteSpace :
WhiteSpace :
Variabel :@param1
WhiteSpace :
Som :AS
WhiteSpace :
Identifier :int
WhiteSpace :
As :AS
WhiteSpace :
Skriv ut :PRINT
WhiteSpace :
Heltal :1
Semikolon :;
WhiteSpace :
Go :GO
EndOfFile :

För att bli av med en del av bruset kan vi filtrera bort några TokenTypes inuti den sista for-loopen:

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

Avslutar med en mer kortfattad serie tokens:

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

Skapa :CREATE
Procedur :PROCEDURE
Identifier :dbo
Punkt :.
Identifier :procedure1
Variabel :@param1
As :AS
Identifierare :int
As :AS
Print :PRINT
Heltal :1

Hur detta mappar till en procedur visuellt:

Varje token tolkas från denna enkla procedurtext.

Du kan redan se problemen vi kommer att ha när vi försöker rekonstruera parameternamn, datatyper och till och med hitta slutet på parameterlistan. Efter att ha tittat på detta lite mer kom jag över ett inlägg av Dan Guzman som lyfte fram en ScriptDom-klass som heter TSqlFragmentVisitor, som identifierar fragment av ett block av analyserad T-SQL. Om vi ​​bara ändrar taktiken lite kan vi inspektera fragment istället för tokens . Ett fragment är i huvudsak en uppsättning av en eller flera tokens och har också sin egen typhierarki. Så vitt jag vet finns det ingen ScriptFragmentStream att iterera genom fragment, men vi kan använda en Besökare mönster för att göra i princip samma sak. Låt oss skapa en ny fil som heter Test2.ps1, klistra in den här koden och köra 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;
   }
}

Resultat (intressanta för den här övningeni fetstil ):

TSqlScript
TSqlBatch
CreateProcedureStatement
ProcedureReference
SchemaObjectName
Identifier
Identifier
ProcedurParameter
Identifier
SqlDataTypeReference
SchemaObjectName
Identifier
StatementList
PrintStatement
IntegerLiteral

Om vi ​​försöker kartlägga detta visuellt till vårt tidigare diagram blir det lite mer komplext. Vart och ett av dessa fragment är i sig en ström av en eller flera tokens, och ibland överlappar de varandra. Flera uttalandetokens och nyckelord känns inte ens igen av sig själva som en del av ett fragment, som CREATE , PROCEDURE , AS och GO . Det senare är förståeligt eftersom det inte ens är T-SQL alls, men parsern måste fortfarande förstå att den separerar batcher.

Jämför hur satstokens och fragmenttokens känns igen.

För att bygga om vilket fragment som helst i koden kan vi iterera genom dess tokens under ett besök på det fragmentet. Detta låter oss härleda saker som namnet på objektet och parameterfragmenten med mycket mindre tråkig analys och villkor, även om vi fortfarande måste loopa inuti varje fragments tokenström. Om vi ​​ändrar Write-Host $fragment.GetType().Name; i föregående skript till detta:

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

Utdata är:

===========================
Procedurreferens
==========================

dbo.procedure1

===========================
ProcedurParameter
==========================

@param1 AS int

Vi har objektet och schemanamnet tillsammans utan att behöva utföra någon ytterligare iteration eller sammanlänkning. Och vi har hela raden involverad i alla parameterdeklarationer, inklusive parameternamnet, datatypen och alla standardvärden som kan finnas. Intressant nog hanterar besökaren @param1 int och int som två distinkta fragment, i huvudsak dubbelräknande datatypen. Den förra är en ProcedureParameter fragment, och det senare är ett SchemaObjectName . Vi bryr oss egentligen bara om det första SchemaObjectName referens (dbo.procedure1 ) eller, mer specifikt, bara den som följer ProcedureReference . Jag lovar att vi kommer att ta itu med dem, bara inte alla idag. Om vi ​​ändrar $procedure konstant till detta (lägger till en kommentar och ett standardvärde):

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

Då blir utgången:

===========================
Procedurreferens
==========================

dbo.procedure1

===========================
ProcedurParameter
==========================

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

Detta inkluderar fortfarande alla tokens i utgången som faktiskt är kommentarer. Inuti for-loopen kan vi filtrera bort alla tokentyper vi vill ignorera för att åtgärda detta (jag tar också bort överflödig AS nyckelord i det här exemplet, men du kanske inte vill göra det om du rekonstruerar modulkroppar):

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

Utgången är renare, men fortfarande inte perfekt.

===========================
Procedurreferens
==========================

dbo.procedure1

===========================
ProcedurParameter
==========================

@param1 int =-64

Om vi ​​vill separera parameternamn, datatyp och standardvärde blir det mer komplext. Medan vi går igenom tokenströmmen för ett givet fragment kan vi dela ut parameternamnet från alla datatypdeklarationer genom att bara spåra när vi träffar en EqualsSign tecken. Ersätter for-slingan med denna ytterligare 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 är utdata:

===========================
Procedurreferens
==========================

dbo.procedure1

===========================
ProcedurParameter
==========================

Parametrarnas namn:@param1
Parametrarnas typ:int
Standard:-64

Det är bättre, men det finns fortfarande mer att lösa. Det finns parameternyckelord som jag har ignorerat hittills, som OUTPUT och READONLY , och vi behöver logik när vår input är en batch med mer än en procedur. Jag kommer att ta itu med dessa frågor i del 2.

Under tiden, experimentera! Det finns många andra kraftfulla saker du kan göra med ScriptDOM, TSqlParser och TSqlFragmentVisitor.

[ Del 1 | Del 2 | Del 3 ]


  1. Säkerhetskopiera en enda tabell med dess data från en databas i sql server 2008

  2. Start RAC-databasen misslyckas med fel ORA-12547

  3. PostgreSQL Autoincrement

  4. Oracle MINUS Operatör förklaras