sql >> Databáze >  >> RDS >> Database

Analyzujte výchozí hodnoty parametrů pomocí prostředí PowerShell – část 2

[ Část 1 | Část 2 | Část 3 ]

V mém posledním příspěvku jsem ukázal, jak používat TSqlParser a TSqlFragmentVisitor extrahovat důležité informace ze skriptu T-SQL obsahujícího definice uložených procedur. S tímto skriptem jsem vynechal několik věcí, například jak analyzovat OUTPUT a READONLY klíčová slova pro parametry a jak analyzovat více objektů dohromady. Dnes jsem chtěl poskytnout skript, který tyto věci zvládne, zmínit několik dalších budoucích vylepšení a sdílet úložiště GitHub, které jsem pro tuto práci vytvořil.

Dříve jsem použil jednoduchý příklad, jako je tento:

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

A s kódem návštěvníka, který jsem poskytl, byl výstup do konzole:

==========================
Reference procedur
==========================

dbo.procedure1


==========================
Parametr procedury
===========================

Název parametru:@param1
Typ parametru:int
Výchozí:-64

A teď, co když předaný scénář vypadal spíš takto? Kombinuje záměrně hroznou definici procedury z dřívějška s několika dalšími prvky, u kterých byste mohli očekávat, že způsobí problémy, jako jsou uživatelsky definované názvy typů, dvě různé formy OUT /OUTPUT klíčové slovo, Unicode v hodnotách parametrů (a v názvech parametrů!), klíčová slova jako konstanty a ODBC escape literály.

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

Předchozí skript nezpracovává více objektů správně a potřebujeme přidat několik logických prvků, abychom zohlednili OUTPUT a READONLY . Konkrétně Output a ReadOnly nejsou typy tokenů, ale spíše jsou rozpoznávány jako Identifier . Potřebujeme tedy nějakou extra logiku, abychom našli identifikátory s těmito explicitními názvy v jakémkoli ProcedureParameter fragment. Můžete zaznamenat několik dalších menších změn:

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

Tento kód slouží pouze pro demonstrační účely a není pravděpodobné, že by byl nejaktuálnější. Podrobnosti o stažení novější verze naleznete níže.

Výstup v tomto případě:

==========================
Reference procedur
==========================
dbo.some_procedure


Název parametru:@a
Typ parametru:int
Výchozí:5


Název parametru:@b
Typ parametru:varchar(64)
Výchozí:'AS =/* BEGIN @a, int =7 */ "blat"'


Název parametru:@c
Typ parametru:int
Výchozí:6



===========================
Reference procedur
==========================
[dbo].jiný_procedura


Název parametru:@p1
Typ parametru:[int]
Výchozí:1


Název parametru:@p2
Typ parametru:datetime
Výchozí:getdate
Výstup:ano


Název parametru:@p3
Typ parametru:datum
Výchozí:{ts '2020-02-01 13:12:49'}


Název parametru:@p4
Typ parametru:dbo.tabletype
Pouze pro čtení:ano


Název parametru:@p5
Typ parametru:geografie
Výstup:ano


Název parametru:@p6
Typ parametru:sysname
Výchozí:N'学中'

To je docela výkonná analýza, i když existují některé únavné okrajové případy a spousta podmíněné logiky. Rád bych viděl TSqlFragmentVisitor rozšířen, takže některé jeho typy tokenů mají další vlastnosti (jako SchemaObjectName.IsFirstAppearance a ProcedureParameter.DefaultValue ) a podívejte se na přidané nové typy tokenů (například FunctionReference ). Ale i nyní je to světelné roky za analyzátorem hrubé síly, který byste mohli napsat jakýmkoli jazyk, bez ohledu na T-SQL.

Stále však existuje několik omezení, která jsem ještě nevyřešil:

  • Toto se týká pouze uložených procedur. Kód pro obsluhu všech tří typů uživatelsky definovaných funkcí je podobný , ale neexistuje žádný praktický FunctionReference fragment, takže místo toho musíte identifikovat první SchemaObjectName fragment (nebo první sada Identifier a Dot tokeny) a ignorovat všechny následující instance. Aktuálně kód v tomto příspěvku bude vrátit všechny informace o parametrech na funkci, ale nebude vrátit název funkce . Neváhejte jej použít pro jednotlivé typy nebo dávky obsahující pouze uložené procedury, ale výstup může být matoucí pro různé typy smíšených objektů. Nejnovější verze v úložišti níže zvládá funkce naprosto v pořádku.
  • Tento kód neukládá stav. Výstup do konzole v rámci každé návštěvy je snadný, ale shromažďování dat z více návštěv za účelem jejich přenosu jinam je o něco složitější, především kvůli tomu, jak funguje vzor Návštěvník.
  • Výše uvedený kód nemůže přijímat vstup přímo. Pro zjednodušení demonstrace je to jen surový skript, kam vložíte svůj blok T-SQL jako konstantu. Konečným cílem je podpora vstupu ze souboru, pole souborů, složky, pole složek nebo stahování definic modulů z databáze. A výstup může být kdekoli:do konzole, do souboru, do databáze... takže tam je limit nebe. Nějaká ta práce mezitím proběhla, ale nic z toho nebylo napsáno v jednoduché verzi, kterou vidíte výše.
  • Nedochází k žádnému zpracování chyb. Opět, pro stručnost a snadnost použití se zde kód nestará o manipulaci s nevyhnutelnými výjimkami, i když nejničivější věc, která se v jeho současné podobě může stát, je, že se dávka neobjeví ve výstupu, pokud nemůže být správně analyzovat (jako CREATE STUPID PROCEDURE dbo.whatever ). Když začneme používat databáze a/nebo souborový systém, správné zpracování chyb bude mnohem důležitější.

Mohli byste se divit, kde na tom budu pokračovat v práci a všechny tyto věci opravit? No, dal jsem to na GitHub, předběžně jsem nazval projekt ParamParser a již mají přispěvatelé, kteří pomáhají s vylepšeními. Aktuální verze kódu již vypadá zcela odlišně od výše uvedeného příkladu a v době, kdy si toto přečtete, mohou být některá ze zde zmíněných omezení již vyřešena. Chci pouze udržovat kód na jednom místě; tento tip je spíše o ukázce minimální ukázky toho, jak to může fungovat, a zdůraznění, že existuje projekt, který se věnuje zjednodušení tohoto úkolu.

V další části budu mluvit více o tom, jak mi můj přítel a kolega Will White pomohl dostat se ze samostatného skriptu, který vidíte výše, k mnohem výkonnějšímu modulu, který najdete na GitHubu.

Pokud mezitím potřebujete analyzovat výchozí hodnoty z parametrů, můžete si kód stáhnout a vyzkoušet. A jak jsem již naznačil, experimentujte sami, protože s těmito třídami a vzorem Návštěvník můžete dělat spoustu dalších mocných věcí.

[ Část 1 | Část 2 | Část 3 ]


  1. Oznámení obecné dostupnosti SQL Compliance Manager 5.9

  2. Kontrola, zda existuje tabulka postgresql pod pythonem (a pravděpodobně Psycopg2)

  3. Správa uživatelských účtů, role, oprávnění, autentizace PHP a MySQL - 3. část

  4. Pochopení indexů v MySQL:Část třetí