sql >> Databasteknik >  >> RDS >> Database

Analysera parameterns standardvärden med PowerShell – Del 2

[ Del 1 | Del 2 | Del 3 ]

I mitt förra inlägg visade jag hur man använder TSqlParser och TSqlFragmentVisitor för att extrahera viktig information från ett T-SQL-skript som innehåller lagrade procedurdefinitioner. Med det skriptet utelämnade jag några saker, till exempel hur man analyserar OUTPUT och READONLY nyckelord för parametrar och hur man analyserar flera objekt tillsammans. Idag ville jag tillhandahålla ett skript som hanterar dessa saker, nämna några andra framtida förbättringar och dela ett GitHub-förråd som jag skapade för detta arbete.

Tidigare använde jag ett enkelt exempel som detta:

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

Och med besökskoden jag angav var utdata till konsolen:

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

dbo.procedure1


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

Paramnamn:@param1
Paramtyp:int
Standard:-64

Tänk om manuset som skickades in såg mer ut så här? Den kombinerar den avsiktligt hemska procedurdefinitionen från tidigare med ett par andra element som du kan förvänta dig att orsaka problem, som användardefinierade typnamn, två olika former av OUT /OUTPUT nyckelord, Unicode i parametervärden (och i parameternamn!), nyckelord som konstanter och ODBC escape-literals.

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

Det tidigare skriptet hanterar inte riktigt flera objekt korrekt, och vi måste lägga till några logiska element för att ta hänsyn till OUTPUT och READONLY . Närmare bestämt Output och ReadOnly är inte tokentyper, utan de känns igen som en Identifier . Så vi behöver lite extra logik för att hitta identifierare med dessa explicita namn inom någon ProcedureParameter fragment. Du kanske ser några andra mindre ändringar:

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

Denna kod är endast avsedd för demonstrationsändamål, och det finns ingen chans att den är den mest aktuella. Se detaljer nedan om hur du laddar ner en nyare version.

Utdata i detta fall:

===========================
Procedurreferens
==========================
dbo.some_procedure


Parameternamn:@a
Parametertyp:int
Standard:5


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


Parameternamn:@c
Parametertyp:int
Standard:6



===========================
Procedurreferens
==========================
[dbo].another_procedur


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


Parametrarnas namn:@p2
Parametrarnas typ:datetime
Standard:getdate
Utdata:ja


Parameternamn:@p3
Parametertyp:datum
Standard:{ts '2020-02-01 13:12:49'}


Parametrarnas namn:@p4
Parametrarnas typ:dbo.tabletype
Skrivskyddat:ja


Parametrarnas namn:@p5
Parametrarnas typ:geografi
Utdata:ja


Param name:@p6
Param type:sysname
Standard:N'学中'

Det är ganska kraftfull analys, även om det finns några tråkiga kantfall och mycket villkorlig logik. Jag skulle gärna se TSqlFragmentVisitor utökats så att några av dess tokentyper har ytterligare egenskaper (som SchemaObjectName.IsFirstAppearance och ProcedureParameter.DefaultValue ), och se nya tokentyper som lagts till (som FunctionReference ). Men även nu är detta ljusår bortom en brute force parser som du kan skriva i vilken som helst språk, strunt i T-SQL.

Det finns fortfarande ett par begränsningar som jag inte har åtgärdat än:

  • Detta adresserar endast lagrade procedurer. Koden för att hantera alla tre typerna av användardefinierade funktioner är liknande , men det finns ingen praktisk FunctionReference fragmenttyp, så istället måste du identifiera den första SchemaObjectName fragment (eller den första uppsättningen Identifier och Dot tokens) och ignorera eventuella efterföljande instanser. För närvarande kommer koden i det här inlägget returnera all information om parametrarna till en funktion, men det kommer inte returnera funktionens namn . Använd den gärna för singletons eller batcher som bara innehåller lagrade procedurer, men du kan tycka att utgången är förvirrande för flera blandade objekttyper. Den senaste versionen i förvaret nedan hanterar funktionerna perfekt.
  • Denna kod sparar inte tillstånd. Utmatning till konsolen inom varje besök är lätt, men att samla in data från flera besök, för att sedan pipeline någon annanstans, är lite mer komplicerat, främst på grund av hur besökarmönstret fungerar.
  • Koden ovan kan inte acceptera indata direkt. För att förenkla demonstrationen här är det bara ett råskript där du klistrar in ditt T-SQL-block som en konstant. Det slutliga målet är att stödja indata från en fil, en uppsättning filer, en mapp, en uppsättning mappar eller att hämta moduldefinitioner från en databas. Och utdata kan vara var som helst:till konsolen, till en fil, till en databas... så himlen är gränsen där. En del av det arbetet har hänt under tiden, men inget av det har skrivits i den enkla versionen som du ser ovan.
  • Det finns ingen felhantering. Återigen, för korthetens skull och för att underlätta konsumtionen, oroar sig koden här inte för att hantera oundvikliga undantag, även om det mest destruktiva som kan hända i sin nuvarande form är att en batch inte kommer att visas i utdata om den inte kan vara korrekt analyserad (som CREATE STUPID PROCEDURE dbo.whatever ). När vi börjar använda databaser och/eller filsystemet blir korrekt felhantering så mycket viktigare.

Du kanske undrar var jag ska fortsätta arbetet med detta och fixa alla dessa saker? Tja, jag har lagt det på GitHub, har preliminärt kallat projektet ParamParser , och har redan bidragsgivare som hjälper till med förbättringar. Den nuvarande versionen av koden ser redan ganska annorlunda ut än exemplet ovan, och när du läser detta kanske några av de begränsningar som nämns här redan är åtgärdade. Jag vill bara behålla koden på ett ställe; Det här tipset handlar mer om att visa ett minimalt exempel på hur det kan fungera, och att lyfta fram att det finns ett projekt där ute för att förenkla denna uppgift.

I nästa segment kommer jag att prata mer om hur min vän och kollega, Will White, hjälpte mig att ta mig från det fristående skriptet som du ser ovan till den mycket kraftfullare modulen du hittar på GitHub.

Om du har behov av att analysera standardvärden från parametrar under tiden, ladda ner koden och prova den. Och som jag föreslog tidigare, experimentera på egen hand, eftersom det finns massor av andra kraftfulla saker du kan göra med dessa klasser och besökarmönstret.

[ Del 1 | Del 2 | Del 3 ]


  1. SQLite Tutorial:Allt du behöver veta

  2. MySQL #1093 - Du kan inte ange måltabellen "giveaways" för uppdatering i FROM-klausulen

  3. Trimma efterföljande utrymmen med PostgreSQL

  4. Hur man extraherar veckonummer i sql