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

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

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

W moim ostatnim poście pokazałem, jak używać TSqlParser i TSqlFragmentVisitor do wyodrębnienia ważnych informacji ze skryptu T-SQL zawierającego definicje procedur składowanych. W tym skrypcie pominąłem kilka rzeczy, takich jak przeanalizowanie OUTPUT i READONLY słowa kluczowe dla parametrów i jak analizować wiele obiektów razem. Dzisiaj chciałem dostarczyć skrypt, który obsłuży te rzeczy, wymienić kilka innych przyszłych ulepszeń i udostępnić repozytorium GitHub, które stworzyłem do tej pracy.

Wcześniej użyłem prostego przykładu takiego:

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

A z podanym przeze mnie kodem gościa, wyjście do konsoli było:

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

dbo.procedure1


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

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

A co by było, gdyby przekazany skrypt wyglądał bardziej tak? Łączy celowo straszną definicję procedury z wcześniejszej wersji z kilkoma innymi elementami, które mogą powodować problemy, takimi jak nazwy typów zdefiniowane przez użytkownika, dwie różne formy OUT /OUTPUT słowo kluczowe, Unicode w wartościach parametrów (i nazwach parametrów!), słowa kluczowe jako stałe i literały ucieczki ODBC.

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

Poprzedni skrypt nie całkiem poprawnie obsługuje wiele obiektów i musimy dodać kilka elementów logicznych, aby uwzględnić OUTPUT i READONLY . W szczególności Output i ReadOnly nie są typami tokenów, ale są rozpoznawane jako Identifier . Potrzebujemy więc dodatkowej logiki, aby znaleźć identyfikatory z tymi jawnymi nazwami w dowolnym ProcedureParameter fragment. Możesz zauważyć kilka innych drobnych zmian:

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

Ten kod służy wyłącznie do celów demonstracyjnych i nie ma szans, że jest najbardziej aktualny. Poniżej znajdziesz szczegółowe informacje na temat pobierania nowszej wersji.

Wynik w tym przypadku:

=======
ProceduraReferencja
====================
dbo.some_procedure


Nazwa parametru:@a
Typ parametru:int
Domyślnie:5


Nazwa parametru:@b
Typ parametru:varchar(64)
Domyślnie:'AS =/* BEGIN @a, int =7 */ "blat"'


Nazwa parametru:@c
Typ parametru:int
Domyślnie:6



=======
ProceduraReferencja
==========================
[dbo].inna_procedura


Nazwa parametru:@p1
Typ parametru:[int]
Domyślnie:1


Nazwa parametru:@p2
Typ parametru:datetime
Domyślnie:getdate
Dane wyjściowe:tak


Nazwa parametru:@p3
Typ parametru:data
Domyślnie:{ts '2020-02-01 13:12:49'}


Nazwa parametru:@p4
Typ parametru:dbo.tabletype
Tylko do odczytu:tak


Nazwa parametru:@p5
Typ parametru:geografia
Dane wyjściowe:tak


Nazwa parametru:@p6
Typ parametru:sysname
Domyślnie:N'学中'

To dość potężne parsowanie, mimo że istnieje kilka żmudnych przypadków brzegowych i dużo logiki warunkowej. Chciałbym zobaczyć TSqlFragmentVisitor rozwinięty, więc niektóre z jego typów tokenów mają dodatkowe właściwości (takie jak SchemaObjectName.IsFirstAppearance i ProcedureParameter.DefaultValue ) i zobacz dodane nowe typy tokenów (takie jak FunctionReference ). Ale nawet teraz jest to o lata świetlne poza parserem brutalnej siły, który możesz napisać w dowolnym język, nieważne T-SQL.

Wciąż jest jednak kilka ograniczeń, których jeszcze nie rozwiązałem:

  • Dotyczy to tylko procedur składowanych. Kod do obsługi wszystkich trzech typów funkcji zdefiniowanych przez użytkownika jest podobny , ale nie ma przydatnego FunctionReference typ fragmentu, więc zamiast tego musisz zidentyfikować pierwszy SchemaObjectName fragment (lub pierwszy zestaw Identifier i Dot tokeny) i zignoruj ​​kolejne wystąpienia. Obecnie kod w tym poście będzie zwróć wszystkie informacje o parametrach do funkcji, ale nie zwróć nazwę funkcji . Można go używać w przypadku pojedynczych lub partii zawierających tylko procedury składowane, ale dane wyjściowe mogą być mylące w przypadku wielu mieszanych typów obiektów. Najnowsza wersja w repozytorium poniżej doskonale radzi sobie z funkcjami.
  • Ten kod nie zapisuje stanu. Wyprowadzanie danych do konsoli w ramach każdej wizyty jest łatwe, ale zbieranie danych z wielu wizyt, a następnie przesyłanie ich w inne miejsce, jest nieco bardziej złożone, głównie ze względu na sposób działania wzorca Visitor.
  • Powyższy kod nie może bezpośrednio akceptować danych wejściowych. Aby uprościć demonstrację tutaj, jest to po prostu surowy skrypt, w którym wklejasz swój blok T-SQL jako stałą. Ostatecznym celem jest obsługa danych wejściowych z pliku, tablicy plików, folderu, tablicy folderów lub ściąganie definicji modułów z bazy danych. A dane wyjściowe mogą być wszędzie:do konsoli, do pliku, do bazy danych… więc nie ma ograniczeń. Niektóre z tych prac miały miejsce w międzyczasie, ale żadna z nich nie została napisana w prostej wersji, którą widzisz powyżej.
  • Brak obsługi błędów. Ponownie, dla zwięzłości i łatwości użytkowania, kod tutaj nie martwi się obsługą nieuniknionych wyjątków, chociaż najbardziej destrukcyjną rzeczą, jaka może się zdarzyć w obecnej formie, jest to, że partia nie pojawi się w danych wyjściowych, jeśli nie może być poprawnie parsowane (np. CREATE STUPID PROCEDURE dbo.whatever ). Kiedy zaczniemy korzystać z baz danych i/lub systemu plików, właściwa obsługa błędów stanie się o wiele ważniejsza.

Możesz się zastanawiać, gdzie będę kontynuował prace nad tym i naprawił wszystkie te rzeczy? Cóż, umieściłem to na GitHub, wstępnie nazwałem projekt ParamParser i mają już współtwórców pomagających w ulepszaniu. Obecna wersja kodu już wygląda zupełnie inaczej niż przykład powyżej, a do czasu, gdy to przeczytasz, niektóre z wymienionych tutaj ograniczeń mogą już zostać rozwiązane. Chcę tylko utrzymać kod w jednym miejscu; ta wskazówka dotyczy raczej pokazania minimalnej próbki tego, jak może działać, i podkreślenia, że ​​istnieje projekt poświęcony uproszczeniu tego zadania.

W następnym odcinku opowiem więcej o tym, jak mój przyjaciel i kolega, Will White, pomógł mi przejść od samodzielnego skryptu, który widzisz powyżej, do znacznie potężniejszego modułu, który znajdziesz na GitHub.

Jeśli w międzyczasie musisz przeanalizować wartości domyślne z parametrów, pobierz kod i wypróbuj go. I jak sugerowałem wcześniej, eksperymentuj na własną rękę, ponieważ jest wiele innych potężnych rzeczy, które możesz zrobić z tymi klasami i wzorcem Visitor.

[ 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. Wprowadzenie do statystyk oczekiwania

  2. Pythonowe interfejsy API REST z Flask, Connexion i SQLAlchemy — część 2

  3. Uruchamiaj skrypty SQL w środowisku Multitenant z catcon.pl

  4. Brudne tajemnice wyrażenia CASE

  5. Zintegruj Firebase z PHP do komunikacji w czasie rzeczywistym