Database
 sql >> Baza danych >  >> RDS >> Database

Parsuj domyślne wartości parametrów za pomocą PowerShell – część 1

[ Część 1 | Część 2 | Część 3 ]

Jeśli kiedykolwiek próbowałeś określić domyślne wartości parametrów procedury składowanej, prawdopodobnie masz ślady na czole po wielokrotnym i gwałtownym uderzaniu w biurko. Większość artykułów, które mówią o pobieraniu informacji o parametrach (takich jak ta wskazówka), nawet nie wspomina słowa default. Dzieje się tak dlatego, że poza surowym tekstem przechowywanym w definicji obiektu, informacji nie ma nigdzie w widokach katalogu. Istnieją kolumny has_default_value i default_value w sys.parameters ten wygląd obiecujące, ale są one zawsze wypełniane tylko dla modułów CLR.

Wyprowadzanie wartości domyślnych za pomocą T-SQL jest kłopotliwe i podatne na błędy. Niedawno odpowiedziałem na pytanie na Stack Overflow dotyczące tego problemu i zabrało mi to pamięć. W 2006 roku skarżyłem się za pośrednictwem wielu elementów Connect na brak widoczności domyślnych wartości parametrów w widokach katalogu. Jednak problem nadal istnieje w SQL Server 2019. (Oto jedyny element, który znalazłem, który trafił do nowego systemu opinii.)

Chociaż niedogodnością jest to, że wartości domyślne nie są ujawniane w metadanych, najprawdopodobniej ich tam nie ma, ponieważ przeanalizowanie ich z tekstu obiektu (w dowolnym języku, ale szczególnie w T-SQL) jest trudne. Trudno jest nawet znaleźć początek i koniec listy parametrów, ponieważ możliwości analizowania T-SQL są tak ograniczone, a przypadków brzegowych jest więcej, niż można sobie wyobrazić. Kilka przykładów:

  • Nie możesz polegać na obecności ( i ) aby wskazać listę parametrów, ponieważ są one opcjonalne (i można je znaleźć w całej liście parametrów)
  • Nie możesz łatwo przeanalizować pierwszego AS aby zaznaczyć początek ciała, ponieważ może pojawić się z innych powodów
  • Nie możesz polegać na obecności BEGIN aby zaznaczyć początek ciała, ponieważ jest to opcjonalne
  • Trudno jest podzielić na przecinki, ponieważ mogą pojawiać się w komentarzach, w literałach łańcuchowych i jako część deklaracji typu danych (pomyśl (precision, scale) )
  • Bardzo trudno jest przeanalizować oba typy komentarzy, które mogą pojawić się w dowolnym miejscu (w tym w literałach ciągu) i mogą być zagnieżdżone
  • Możesz przypadkowo znaleźć ważne słowa kluczowe, przecinki i znaki równości w literałach ciągów i komentarzach
  • Możesz mieć wartości domyślne, które nie są liczbami ani literałami łańcuchowymi (pomyśl {fn curdate()} lub GETDATE )

Istnieje tak wiele małych odmian składni, że zwykłe techniki analizowania ciągów są nieskuteczne. Czy widziałem AS? już? Czy było to między nazwą parametru a typem danych? Czy to po prawym nawiasie otaczającym całą listę parametrów, czy po [jeden?], który nie był zgodny przed ostatnim razem, gdy widziałem parametr? Czy to przecinek oddzielający dwa parametry, czy jest to część precyzji i skali? Kiedy zapętlasz ciąg, jedno słowo po słowie, to trwa i trwa, a jest tak wiele bitów, które musisz śledzić.

Weźmy ten (celowo niedorzeczny, ale nadal poprawny składniowo) przykład:

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

Parsowanie wartości domyślnych z tej definicji przy użyciu T-SQL jest trudne. Naprawdę trudne . Bez BEGIN aby właściwie oznaczyć koniec listy parametrów, cały bałagan z komentarzami i wszystkie przypadki, w których słowa kluczowe, takie jak AS może oznaczać różne rzeczy, prawdopodobnie będziesz mieć złożony zestaw zagnieżdżonych wyrażeń obejmujących więcej SUBSTRING i CHARINDEX wzory niż kiedykolwiek widziałeś w jednym miejscu. I prawdopodobnie nadal będziesz mieć @d i @e wygląda jak parametry procedury zamiast zmiennych lokalnych.

Zastanawiając się trochę nad tym problemem i szukając, czy komuś udało się coś nowego w ciągu ostatniej dekady, natknąłem się na ten wspaniały post Michaela Swarta. W tym poście Michael używa TSqlParser ScriptDom, aby usunąć zarówno jedno-, jak i wielowierszowe komentarze z bloku T-SQL. Napisałem więc trochę kodu PowerShell, aby przejść przez procedurę, aby zobaczyć, które inne tokeny zostały zidentyfikowane. Weźmy prostszy przykład bez wszystkich celowych problemów:

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

Otwórz Visual Studio Code (lub swoje ulubione środowisko IDE PowerShell) i zapisz nowy plik o nazwie Test1.ps1. Jedynym warunkiem wstępnym jest posiadanie najnowszej wersji Microsoft.SqlServer.TransactSql.ScriptDom.dll (którą można pobrać i wyodrębnić z pakietu sql tutaj) w tym samym folderze, co plik .ps1. Skopiuj ten kod, zapisz, a następnie uruchom lub debuguj:

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

Wyniki:

=============================
UtwórzOświadczenie o procedurze
=============================

Utwórz :CREATE
WhiteSpace :
Procedura :PROCEDURE
WhiteSpace :
Identyfikator :dbo
Kropka :.
Identyfikator :procedure1
WhiteSpace :
WhiteSpace :
Zmienna :@param1
WhiteSpace :
As :AS
WhiteSpace :
Identyfikator :int
WhiteSpace :
As :AS
WhiteSpace :
Drukuj :DRUKUJ
WhiteSpace :
Liczba całkowita :1
Średnik :;
WhiteSpace :
Idź :GO
Koniec pliku :

Aby pozbyć się niektórych szumów, możemy odfiltrować kilka TokenTypes w ostatniej pętli for:

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

Kończąc z bardziej zwięzłą serią tokenów:

=============================
UtwórzOświadczenie o procedurze
=============================

Utwórz :CREATE
Procedura :PROCEDURE
Identyfikator :dbo
Kropka :.
Identyfikator :procedura1
Zmienna :@param1
Jako :AS
Identyfikator :int
As :AS
Drukuj :PRINT
Liczba całkowita :1

Sposób, w jaki to mapuje się wizualnie na procedurę:

Każdy token przeanalizowany z treści tej prostej procedury.

Możesz już zobaczyć problemy, które będziemy mieć, próbując zrekonstruować nazwy parametrów, typy danych, a nawet znaleźć koniec listy parametrów. Po dokładniejszym zapoznaniu się z tym tematem natknąłem się na post Dana Guzmana, który podkreślił klasę ScriptDom o nazwie TSqlFragmentVisitor, która identyfikuje fragmenty bloku przeanalizowanego T-SQL. Jeśli choć trochę zmienimy taktykę, możemy sprawdzić fragmenty zamiast tokenów . Fragment jest zasadniczo zestawem jednego lub więcej tokenów, a także ma własną hierarchię typów. O ile wiem, nie ma ScriptFragmentStream do iteracji przez fragmenty, ale możemy użyć Odwiedzającego wzorzec, aby zrobić zasadniczo to samo. Stwórzmy nowy plik o nazwie Test2.ps1, wklejmy ten kod i uruchommy go:

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

Wyniki (interesujące dla tego ćwiczenia pogrubione ):

TSqlScript
TSqlBatch
CreateProcedureStatement
ProcedureReference
NazwaObiektuSchematu
Identyfikator
Identyfikator
ParametrProcedury
Identyfikator
SqlDataTypeReference
NazwaObiektuSchematu
Identyfikator
ListaWyrażeń
InstrukcjaWydruku
Literał całkowity

Jeśli spróbujemy zmapować to wizualnie do naszego poprzedniego diagramu, stanie się to trochę bardziej złożone. Każdy z tych fragmentów sam w sobie jest strumieniem jednego lub więcej tokenów, a czasami będą się one nakładać. Kilka tokenów instrukcji i słów kluczowych nie jest nawet rozpoznawanych samodzielnie jako część fragmentu, np. CREATE , PROCEDURE , AS i GO . To ostatnie jest zrozumiałe, ponieważ w ogóle nie jest to nawet T-SQL, ale parser nadal musi zrozumieć, że rozdziela partie.

Porównanie sposobu rozpoznawania tokenów instrukcji i tokenów fragmentów.

Aby odbudować dowolny fragment w kodzie, możemy iterować po jego tokenach podczas wizyty w tym fragmencie. To pozwala nam wyprowadzić takie rzeczy jak nazwa obiektu i fragmenty parametrów przy znacznie mniej żmudnym analizowaniu i warunkowaniu, chociaż nadal musimy zapętlić się w strumieniu tokenów każdego fragmentu. Jeśli zmienimy Write-Host $fragment.GetType().Name; w poprzednim skrypcie do tego:

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

Dane wyjściowe to:

=======
ProceduraReferencja
===========================

dbo.procedure1

====================
Parametrprocedury
==========================

@param1 AS int

Mamy obiekt i nazwę schematu razem, bez konieczności wykonywania dodatkowej iteracji lub konkatenacji. I mamy całą linię zaangażowaną w deklarację parametru, w tym nazwę parametru, typ danych i dowolną wartość domyślną, która może istnieć. Co ciekawe, odwiedzający obsługuje @param1 int i int jako dwa różne fragmenty, w zasadzie podwójnie liczące typ danych. Pierwszy z nich to ProcedureParameter fragment, a ten ostatni to SchemaObjectName . Naprawdę zależy nam tylko na pierwszym SchemaObjectName referencja (dbo.procedure1 ) lub, dokładniej, tylko ten, który następuje po ProcedureReference . Obiecuję, że się tym zajmiemy, ale nie wszystkimi dzisiaj. Jeśli zmienimy $procedure stała do tego (dodanie komentarza i wartości domyślnej):

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

Następnie dane wyjściowe stają się:

=======
ProceduraReferencja
===========================

dbo.procedure1

====================
Parametrprocedury
==========================

@param1 AS int =/* komentarz */ -64

To nadal obejmuje wszystkie tokeny w danych wyjściowych, które w rzeczywistości są komentarzami. Wewnątrz pętli for możemy odfiltrować dowolne typy tokenów, które chcemy zignorować, aby rozwiązać ten problem (usuwam również zbędny AS słowa kluczowe w tym przykładzie, ale możesz nie chcieć tego robić, jeśli rekonstruujesz korpusy modułów):

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

Wyjście jest czystsze, ale nadal nie idealne.

=======
ProceduraReferencja
===========================

dbo.procedure1

====================
Parametrprocedury
==========================

@param1 int =-64

Jeśli chcemy oddzielić nazwę parametru, typ danych i wartość domyślną, staje się to bardziej złożone. Podczas gdy przeglądamy strumień tokenów dla dowolnego fragmentu, możemy oddzielić nazwę parametru od dowolnych deklaracji typu danych, po prostu śledząc, kiedy trafimy EqualsSign znak. Zastąpienie pętli for tą dodatkową logiką:

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

Teraz wynik to:

=======
ProceduraReferencja
===========================

dbo.procedure1

==========================
Parametrprocedury
====================

Nazwa parametru:@param1
Typ parametru:int
Domyślnie:-64

Tak jest lepiej, ale jest jeszcze więcej do rozwiązania. Istnieją parametry, które do tej pory ignorowałem, takie jak OUTPUT i READONLY i potrzebujemy logiki, gdy nasze dane wejściowe to wsad z więcej niż jedną procedurą. Zajmę się tymi problemami w części 2.

W międzyczasie eksperymentuj! Istnieje wiele innych potężnych rzeczy, które możesz zrobić za pomocą ScriptDOM, TSqlParser i TSqlFragmentVisitor.

[ Część 1 | Część 2 | Część 3 ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Używanie OAuth do uwierzytelniania połączenia ODBC z Salesforce.com

  2. Zaktualizowane opcje warstwy bazy danych Azure SQL

  3. Język definicji danych SQL

  4. Dowiedz się, jak używać SQL SELECT z przykładami

  5. Czy przedrostek sp_ nadal jest nie-nie?