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

Parse parameter standardværdier ved hjælp af PowerShell – Del 2

[ Del 1 | Del 2 | Del 3 ]

I mit sidste indlæg viste jeg, hvordan man bruger TSqlParser og TSqlFragmentVisitor at udtrække vigtig information fra et T-SQL-script, der indeholder lagrede proceduredefinitioner. Med det script udelod jeg et par ting, såsom hvordan man analyserer OUTPUT og READONLY nøgleord til parametre, og hvordan man parser flere objekter sammen. I dag ønskede jeg at levere et script, der håndterer disse ting, nævne et par andre fremtidige forbedringer og dele et GitHub-lager, jeg har oprettet til dette arbejde.

Tidligere brugte jeg et simpelt eksempel som dette:

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

Og med den besøgskode, jeg angav, var outputtet til konsollen:

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

dbo.procedure1


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

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

Hvad nu hvis manuskriptet, der blev sendt ind, så mere sådan ud? Den kombinerer den forsætlige forfærdelige proceduredefinition fra før med et par andre elementer, som du måske forventer vil forårsage problemer, såsom brugerdefinerede typenavne, to forskellige former for OUT /OUTPUT nøgleord, Unicode i parameterværdier (og i parameternavne!), nøgleord som konstanter og ODBC escape-literaler.

/* 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 forrige script håndterer ikke helt flere objekter korrekt, og vi skal tilføje et par logiske elementer for at tage højde for OUTPUT og READONLY . Specifikt Output og ReadOnly er ikke tokentyper, men snarere genkendes de som en Identifier . Så vi har brug for noget ekstra logik for at finde identifikatorer med disse eksplicitte navne i enhver ProcedureParameter fragment. Du kan muligvis se et par andre mindre ændringer:

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

Denne kode er kun til demonstrationsformål, og der er ingen chance for, at den er den mest aktuelle. Se venligst detaljer nedenfor om at downloade en nyere version.

Outputtet i dette tilfælde:

===========================
Procedurehenvisning
==========================
dbo.some_procedure


Param navn:@a
Param type:int
Standard:5


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


Param navn:@c
Param type:int
Standard:6



===========================
Procedurehenvisning
==========================
[dbo].another_procedure


Param navn:@p1
Param type:[int]
Standard:1


Param navn:@p2
Param type:datetime
Standard:getdate
Output:ja


Param navn:@p3
Param type:date
Standard:{ts '2020-02-01 13:12:49'}


Param navn:@p4
Param type:dbo.tabletype
Skrivebeskyttet:ja


Param navn:@p5
Param type:geografi
Output:ja


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

Det er noget ret kraftfuldt parsing, selvom der er nogle kedelige kantsager og en masse betinget logik. Jeg ville elske at se TSqlFragmentVisitor udvidet, så nogle af dens tokentyper har yderligere egenskaber (såsom SchemaObjectName.IsFirstAppearance og ProcedureParameter.DefaultValue ), og se nye tokentyper tilføjet (såsom FunctionReference ). Men selv nu er dette lysår hinsides en brute force parser, du kan skrive i hvilken som helst sprog, pyt med T-SQL.

Der er dog stadig et par begrænsninger, som jeg ikke har behandlet endnu:

  • Dette omhandler kun lagrede procedurer. Koden til at håndtere alle tre typer brugerdefinerede funktioner er lignende , men der er ingen praktisk FunctionReference fragmenttype, så i stedet skal du identificere den første SchemaObjectName fragment (eller det første sæt af Identifier og Dot tokens) og ignorer eventuelle efterfølgende tilfælde. I øjeblikket vil koden i dette indlæg returnere alle oplysninger om parametrene til en funktion, men den vil ikke returner funktionens navn . Du er velkommen til at bruge den til singletons eller batches, der kun indeholder lagrede procedurer, men du kan finde outputtet forvirrende for flere blandede objekttyper. Den seneste version i depotet nedenfor håndterer funktionerne helt fint.
  • Denne kode gemmer ikke tilstand. Output til konsollen inden for hvert besøg er let, men at indsamle data fra flere besøg, for derefter at pipeline andre steder, er lidt mere kompleks, primært på grund af den måde, besøgsmønsteret fungerer på.
  • Koden ovenfor kan ikke acceptere input direkte. For at forenkle demonstrationen her, er det bare et råscript, hvor du indsætter din T-SQL-blok som en konstant. Det endelige mål er at understøtte input fra en fil, en række filer, en mappe, en række mapper eller at trække moduldefinitioner fra en database. Og outputtet kan være hvor som helst:til konsollen, til en fil, til en database ... så himlen er grænsen der. Noget af det arbejde er sket i mellemtiden, men intet af det er skrevet i den simple version, du ser ovenfor.
  • Der er ingen fejlhåndtering. Igen, for kortheds skyld og for at lette forbruget, bekymrer koden her ikke om at håndtere uundgåelige undtagelser, selvom det mest ødelæggende, der kan ske i sin nuværende form, er, at en batch ikke vises i outputtet, hvis den ikke kan være korrekt parset (som CREATE STUPID PROCEDURE dbo.whatever ). Når vi begynder at bruge databaser og/eller filsystemet, bliver korrekt fejlhåndtering så meget vigtigere.

Du spekulerer måske på, hvor skal jeg fortsætte det igangværende arbejde med dette og rette alle disse ting? Nå, jeg har lagt det på GitHub, har foreløbigt kaldt projektet ParamParser , og har allerede bidragydere, der hjælper med forbedringer. Den nuværende version af koden ser allerede meget anderledes ud end eksemplet ovenfor, og når du læser dette, er nogle af de begrænsninger, der er nævnt her, måske allerede behandlet. Jeg vil kun bevare koden ét sted; dette tip handler mere om at vise et minimalt eksempel på, hvordan det kan fungere, og fremhæve, at der er et projekt derude dedikeret til at forenkle denne opgave.

I det næste segment vil jeg fortælle mere om, hvordan min ven og kollega, Will White, hjalp mig med at komme fra det selvstændige script, du ser ovenfor, til det meget mere kraftfulde modul, du finder på GitHub.

Hvis du har behov for at parse standardværdier fra parametre i mellemtiden, er du velkommen til at downloade koden og prøve den. Og som jeg foreslog før, eksperimenter på egen hånd, for der er mange andre kraftfulde ting, du kan gøre med disse klasser og Besøgsmønsteret.

[ Del 1 | Del 2 | Del 3 ]


  1. Opret et forhold i SQL

  2. TYPE Definition Ændring i Oracle 21c

  3. Vores mest populære database blogindlæg i 2017

  4. Følgerklynger – 3 store brugssager til synkronisering af SQL- og NoSQL-implementeringer