sql >> Database >  >> RDS >> Database

Standaardwaarden van parameters parseren met PowerShell - Deel 2

[ Deel 1 | Deel 2 | Deel 3 ]

In mijn laatste bericht liet ik zien hoe je TSqlParser . gebruikt en TSqlFragmentVisitor om belangrijke informatie te extraheren uit een T-SQL-script dat opgeslagen proceduredefinities bevat. Met dat script heb ik een paar dingen weggelaten, zoals het ontleden van de OUTPUT en READONLY trefwoorden voor parameters en hoe u meerdere objecten samen kunt ontleden. Vandaag wilde ik een script leveren dat deze dingen afhandelt, een paar andere toekomstige verbeteringen noemen en een GitHub-repository delen die ik voor dit werk heb gemaakt.

Eerder gebruikte ik een eenvoudig voorbeeld zoals dit:

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

En met de bezoekerscode die ik heb verstrekt, was de uitvoer naar de console:

==========================
ProcedureReferentie
==========================

dbo.procedure1


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

Paramnaam:@param1
Paramtype:int
Standaard:-64

Nu, wat als het ingevoerde script er meer zo uitzag? Het combineert de opzettelijk verschrikkelijke proceduredefinitie van vroeger met een aantal andere elementen waarvan je zou verwachten dat ze problemen veroorzaken, zoals door de gebruiker gedefinieerde typenamen, twee verschillende vormen van de OUT /OUTPUT trefwoord, Unicode in parameterwaarden (en in parameternamen!), trefwoorden als constanten en ODBC-escape-letterwoorden.

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

Het vorige script verwerkt meerdere objecten niet helemaal correct, en we moeten een paar logische elementen toevoegen om rekening te houden met OUTPUT en READONLY . In het bijzonder Output en ReadOnly zijn geen tokentypes, maar worden eerder herkend als een Identifier . We hebben dus wat extra logica nodig om identifiers met die expliciete namen te vinden binnen een ProcedureParameter fragment. Misschien zie je nog een paar andere kleine veranderingen:

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

Deze code is alleen voor demonstratiedoeleinden en er is geen kans dat deze de meest recente is. Zie details hieronder over het downloaden van een recentere versie.

De uitvoer in dit geval:

==========================
ProcedureReferentie
==========================
dbo.some_procedure


Param naam:@a
Param type:int
Standaard:5


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


Param naam:@c
Param type:int
Standaard:6



==========================
ProcedureReferentie
==========================
[dbo].een andere_procedure


Param-naam:@p1
Param-type:[int]
Standaard:1


Param naam:@p2
Param type:datetime
Standaard:getdate
Uitvoer:ja


Param-naam:@p3
Param-type:datum
Standaard:{ts '2020-02-01 13:12:49'}


Param-naam:@p4
Param-type:dbo.tabletype
Alleen-lezen:ja


Param naam:@p5
Param type:geografie
Uitvoer:ja


Param naam:@p6
Param type:sysname
Standaard:N'学中'

Dat is behoorlijk krachtig ontleden, ook al zijn er enkele vervelende randgevallen en veel voorwaardelijke logica. Ik zou graag TSqlFragmentVisitor . willen zien uitgebreid zodat sommige van zijn tokentypes extra eigenschappen hebben (zoals SchemaObjectName.IsFirstAppearance en ProcedureParameter.DefaultValue ), en zie nieuwe tokentypes toegevoegd (zoals FunctionReference ). Maar zelfs nu is dit lichtjaren verder dan een brute force-parser die je zou kunnen schrijven in elke taal, laat staan ​​T-SQL.

Er zijn echter nog een aantal beperkingen die ik nog niet heb aangepakt:

  • Dit betreft alleen opgeslagen procedures. De code om alle drie de typen door de gebruiker gedefinieerde functies af te handelen is vergelijkbaar , maar er is geen handige FunctionReference fragmenttype, dus in plaats daarvan moet u de eerste SchemaObjectName . identificeren fragment (of de eerste set van Identifier en Dot tokens) en negeer eventuele volgende instanties. Momenteel is de code in dit bericht zal retourneer alle informatie over de parameters naar een functie, maar het niet retourneer de functie naam . Voel je vrij om het te gebruiken voor singletons of batches die alleen opgeslagen procedures bevatten, maar je zou de uitvoer verwarrend kunnen vinden voor meerdere gemengde objecttypes. De nieuwste versie in de onderstaande repository kan de functies prima aan.
  • Deze code slaat de status niet op. Uitvoer naar de console binnen elk bezoek is eenvoudig, maar het verzamelen van de gegevens van meerdere bezoeken om vervolgens ergens anders naartoe te leiden, is iets ingewikkelder, voornamelijk vanwege de manier waarop het bezoekerspatroon werkt.
  • De bovenstaande code kan geen directe invoer accepteren. Om de demonstratie hier te vereenvoudigen, is het slechts een onbewerkt script waarin u uw T-SQL-blok als een constante plakt. Het uiteindelijke doel is om invoer uit een bestand, een reeks bestanden, een map, een reeks mappen of het ophalen van moduledefinities uit een database te ondersteunen. En de uitvoer kan overal zijn:naar de console, naar een bestand, naar een database ... dus de lucht is daar de limiet. Een deel van dat werk is in de tussentijd gebeurd, maar niets daarvan is geschreven in de eenvoudige versie die u hierboven ziet.
  • Er is geen foutafhandeling. Nogmaals, voor de beknoptheid en het gebruiksgemak maakt de code zich hier geen zorgen over het omgaan met onvermijdelijke uitzonderingen, hoewel het meest destructieve dat in zijn huidige vorm kan gebeuren, is dat een batch niet in de uitvoer verschijnt als deze niet correct kan worden ontleed (zoals CREATE STUPID PROCEDURE dbo.whatever ). Wanneer we databases en/of het bestandssysteem gaan gebruiken, wordt een goede foutafhandeling des te belangrijker.

Je vraagt ​​je misschien af, waar blijf ik hier aan blijven werken en al deze dingen oplossen? Nou, ik heb het op GitHub gezet, heb het project voorlopig ParamParser genoemd , en hebben al bijdragers die helpen met verbeteringen. De huidige versie van de code ziet er al heel anders uit dan het voorbeeld hierboven, en tegen de tijd dat u dit leest, zijn enkele van de hier genoemde beperkingen mogelijk al verholpen. Ik wil de code maar op één plek bewaren; deze tip gaat meer over het tonen van een minimaal voorbeeld van hoe het kan werken, en benadrukken dat er een project is dat is gericht op het vereenvoudigen van deze taak.

In het volgende segment zal ik meer vertellen over hoe mijn vriend en collega, Will White, me heeft geholpen om van het zelfstandige script dat je hierboven ziet naar de veel krachtigere module te gaan die je op GitHub zult vinden.

Als u in de tussentijd de standaardwaarden van parameters moet ontleden, kunt u de code downloaden en uitproberen. En zoals ik al eerder suggereerde, experimenteer zelf, want er zijn veel andere krachtige dingen die je kunt doen met deze klassen en het bezoekerspatroon.

[ Deel 1 | Deel 2 | Deel 3 ]


  1. MINUTE() Voorbeelden – MySQL

  2. Introductie van nieuwe functie - Spotlight Cloud-rapporten

  3. SqlCommand hergebruiken?

  4. Postgres UTC-datumnotatie &tijdperk gegoten, tekeninversie