[ 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ć pierwszySchemaObjectName
fragment (lub pierwszy zestawIdentifier
iDot
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 ]