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

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

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

Pokud jste se někdy pokoušeli určit výchozí hodnoty pro parametry uložené procedury, pravděpodobně máte na čele stopy od opakovaného a násilného úderu na váš stůl. Většina článků, které hovoří o získávání informací o parametrech (jako tento tip), ani nezmiňuje slovo výchozí. Je to proto, že kromě surového textu uloženého v definici objektu nejsou informace nikde v zobrazení katalogu. Jsou zde sloupce has_default_value a default_value v sys.parameters ten vzhled slibné, ale jsou vždy obsazeny pouze pro moduly CLR.

Odvozování výchozích hodnot pomocí T-SQL je těžkopádné a náchylné k chybám. Nedávno jsem odpověděl na otázku na Stack Overflow týkající se tohoto problému a vzalo mě to do paměti. V roce 2006 jsem si prostřednictvím více položek Connect stěžoval na neviditelnost výchozích hodnot parametrů v zobrazení katalogu. Problém však stále existuje v SQL Server 2019. (Zde je jediná položka, kterou jsem našel a která se dostala do nového systému zpětné vazby.)

I když je nepohodlné, že výchozí hodnoty nejsou v metadatech vystaveny, s největší pravděpodobností tam nejsou, protože je těžké je analyzovat z textu objektu (v jakémkoli jazyce, ale zejména v T-SQL). Je obtížné dokonce najít začátek a konec seznamu parametrů, protože schopnost analýzy T-SQL je tak omezená a existuje více okrajových případů, než si dokážete představit. Několik příkladů:

  • Nemůžete se spolehnout na přítomnost ( a ) k označení seznamu parametrů, protože jsou volitelné (a lze je nalézt v celém seznamu parametrů)
  • Nemůžete snadno analyzovat první AS označit začátek těla, protože se může objevit z jiných důvodů
  • Na přítomnost BEGIN se nemůžete spolehnout k označení začátku těla, protože je nepovinné
  • Je těžké je dělit na čárky, protože se mohou objevit v komentářích, v řetězcových literálech a jako součást deklarací datových typů (např. (precision, scale) )
  • Je velmi obtížné analyzovat oba typy komentářů, které se mohou objevit kdekoli (včetně uvnitř řetězcových literálů) a mohou být vnořeny
  • Uvnitř řetězcových literálů a komentářů můžete nechtěně najít důležitá klíčová slova, čárky a znaménka
  • Můžete mít výchozí hodnoty, které nejsou čísly nebo řetězcovými literály (například {fn curdate()} nebo GETDATE )

Existuje tolik malých variant syntaxe, že běžné techniky analýzy řetězců jsou neúčinné. Viděl jsem AS již? Bylo to mezi názvem parametru a datovým typem? Bylo to za pravou závorkou, která obklopuje celý seznam parametrů, nebo [jedné?], která se neshodovala, než jsem naposledy viděl parametr? Je ta čárka oddělující dva parametry nebo je součástí přesnosti a měřítka? Když procházíte řetězcem jedno slovo po druhém, jde to dál a dál a je tolik bitů, které musíte sledovat.

Vezměte si tento (záměrně směšný, ale stále syntakticky platný) příklad:

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

Analýza výchozích hodnot z této definice pomocí T-SQL je obtížná. Opravdu těžké . Bez BEGIN správně označit konec seznamu parametrů, všechny nepořádky v komentářích a všechny případy, kdy klíčová slova jako AS může znamenat různé věci, pravděpodobně budete mít složitou sadu vnořených výrazů zahrnujících více SUBSTRING a CHARINDEX vzory, než jste kdy na jednom místě viděli. A pravděpodobně stále skončíte s @d a @e vypadat jako parametry procedury namísto lokálních proměnných.

Když jsem o tomto problému více přemýšlel a hledal, zda se někomu za poslední desetiletí podařilo něco nového, narazil jsem na tento skvělý příspěvek od Michaela Swarta. V tomto příspěvku Michael používá TSqlParser ScriptDom k odstranění jednořádkových i víceřádkových komentářů z bloku T-SQL. Napsal jsem tedy nějaký kód PowerShellu, abych prošel procedurou, abych zjistil, které další tokeny byly identifikovány. Vezměme si jednodušší příklad bez všech záměrných problémů:

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

Otevřete Visual Studio Code (nebo své oblíbené PowerShell IDE) a uložte nový soubor s názvem Test1.ps1. Jediným předpokladem je mít nejnovější verzi Microsoft.SqlServer.TransactSql.ScriptDom.dll (kterou si můžete stáhnout a extrahovat z balíčku sql zde) ve stejné složce jako soubor .ps1. Zkopírujte tento kód, uložte a poté spusťte nebo ladte:

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

Výsledky:

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

Vytvořit :CREATE
WhiteSpace :
Postup :PROCEDURE
WhiteSpace :
Identifikátor :dbo
Tečka :.
Identifikátor :procedure1
WhiteSpace :
WhiteSpace :
Proměnná :@param1
WhiteSpace :
As :AS
WhiteSpace :
Identifikátor :int
WhiteSpace :
As :AS
WhiteSpace :
Tisk :TISK
WhiteSpace :
Celé číslo :1
Středník :;
WhiteSpace :
Přejít:GO
EndOfFile :

Abychom se zbavili některého šumu, můžeme odfiltrovat několik TokenTypů uvnitř poslední smyčky for:

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

Končíme stručnější řadou žetonů:

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

Vytvořit :CREATE
Postup :PROCEDURE
Identifikátor :dbo
Tečka :.
Identifikátor :procedure1
Proměnná :@param1
As :AS
Identifikátor :int
Jako :AS
Tisk :PRINT
Celé číslo:1

Způsob, jakým se to vizuálně mapuje na proceduru:

Každý token analyzován z tohoto těla jednoduché procedury.

Již můžete vidět problémy, které budeme mít při pokusu o rekonstrukci názvů parametrů, datových typů a dokonce i při hledání konce seznamu parametrů. Poté, co jsem se na to podíval více, narazil jsem na příspěvek Dana Guzmana, který zdůraznil třídu ScriptDom nazvanou TSqlFragmentVisitor, která identifikuje fragmenty bloku analyzovaného T-SQL. Pokud jen trochu změníme taktiku, můžeme si prohlédnout fragmenty místo tokenů . Fragment je v podstatě sada jednoho nebo více tokenů a má také svou vlastní hierarchii typů. Pokud vím, žádný ScriptFragmentStream neexistuje k iteraci fragmentů, ale můžeme použít Návštěvníka vzor dělat v podstatě to samé. Vytvořme nový soubor s názvem Test2.ps1, vložte tento kód a spusťte jej:

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

Výsledky (zajímavé pro toto cvičenítučně ):

TSqlScript
TSqlBatch
CreateProcedureStatement
ProcedureReference
SchemaObjectName
Identifikátor
Identifikátor
ProcedureParameter
Identifier
SqlDataTypeReference
SchemaObjectName
Identifikátor
StatementList
PrintStatement
IntegerLiteral

Pokud se pokusíme toto vizuálně zmapovat k našemu předchozímu diagramu, bude to trochu složitější. Každý z těchto fragmentů je sám o sobě proudem jednoho nebo více žetonů a někdy se budou překrývat. Některé tokeny příkazů a klíčová slova nejsou ani samy o sobě rozpoznány jako součást fragmentu, například CREATE , PROCEDURE , AS a GO . To je pochopitelné, protože to vůbec není T-SQL, ale analyzátor musí stále chápat, že odděluje dávky.

Porovnání způsobu, jakým jsou rozpoznávány tokeny příkazů a tokeny fragmentů.

Abychom znovu vytvořili jakýkoli fragment v kódu, můžeme během návštěvy tohoto fragmentu iterovat jeho tokeny. To nám umožňuje odvodit věci, jako je název objektu a fragmenty parametrů, s mnohem méně únavnou analýzou a podmínkami, i když stále musíme procházet smyčkou uvnitř tokenu každého fragmentu. Pokud změníme Write-Host $fragment.GetType().Name; v předchozím skriptu k tomuto:

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

Výstup je:

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

dbo.procedure1

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

@param1 AS int

Máme název objektu a schématu pohromadě, aniž bychom museli provádět další iteraci nebo zřetězení. A máme celý řádek zapojený do jakékoli deklarace parametru, včetně názvu parametru, datového typu a jakékoli výchozí hodnoty, která může existovat. Zajímavé je, že návštěvník zpracovává @param1 int a int jako dva odlišné fragmenty, které v podstatě počítají datový typ dvakrát. První z nich je ProcedureParameter a druhý je SchemaObjectName . Opravdu nás zajímá jen to první SchemaObjectName odkaz (dbo.procedure1 ) nebo přesněji pouze ten, který následuje po ProcedureReference . Slibuji, že se s nimi vypořádáme, jen ne se všemi dnes. Pokud změníme $procedure konstantní (přidání komentáře a výchozí hodnoty):

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

Výstup pak bude:

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

dbo.procedure1

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

@param1 AS int =/* komentář */ -64

To stále zahrnuje všechny tokeny ve výstupu, které jsou ve skutečnosti komentáře. Uvnitř cyklu for můžeme odfiltrovat všechny typy tokenů, které chceme ignorovat, abychom to vyřešili (také odstraním nadbytečné AS klíčová slova v tomto příkladu, ale možná to nebudete chtít dělat, pokud rekonstruujete těla modulů):

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

Výstup je čistší, ale stále není dokonalý.

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

dbo.procedure1

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

@param1 int =-64

Pokud chceme oddělit název parametru, datový typ a výchozí hodnotu, bude to složitější. Zatímco procházíme proudem tokenů pro jakýkoli daný fragment, můžeme oddělit název parametru od libovolné deklarace datového typu pouhým sledováním, kdy narazíme na EqualsSign žeton. Nahrazení smyčky for touto další logikou:

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

Nyní je výstup:

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

dbo.procedure1

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

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

To je lepší, ale stále je co řešit. Existují klíčová slova parametrů, která jsem dosud ignoroval, například OUTPUT a READONLY a potřebujeme logiku, když je naším vstupem dávka s více než jednou procedurou. Těmito problémy se budu zabývat v části 2.

Mezitím experimentujte! S ScriptDOM, TSqlParser a TSqlFragmentVisitor můžete dělat spoustu dalších výkonných věcí.

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


  1. Počítejte záznamy pro každý měsíc v roce

  2. Vytvořte cizí klíč v SQLite

  3. Jak rychle odstranit uživatele se stávajícími oprávněními

  4. Zabezpečení databáze 101:Zabezpečení v databázích s otevřeným zdrojovým kódem