[ 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
FunctionReferencetyp fragmentu, więc zamiast tego musisz zidentyfikować pierwszySchemaObjectNamefragment (lub pierwszy zestawIdentifieriDottokeny) 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 ]