[ Част 1 | Част 2 | Част 3 ]
В последната си публикация показах как се използва TSqlParser и TSqlFragmentVisitor за извличане на важна информация от T-SQL скрипт, съдържащ дефиниции на съхранени процедури. С този скрипт пропуснах няколко неща, като например как да анализирам OUTPUT и READONLY ключови думи за параметри и как да анализирате няколко обекта заедно. Днес исках да предоставя скрипт, който се справя с тези неща, да спомена няколко други бъдещи подобрения и да споделя GitHub хранилище, което създадох за тази работа.
Преди това използвах прост пример като този:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
И с кода на посетителя, който предоставих, изходът към конзолата беше:
==========================Справка за процедурата
===========================
dbo.procedure1
==========================
Параметър на процедура
===========================
Име на параметър:@param1
Тип на параметър:int
По подразбиране:-64
Ами ако предаденият скрипт изглеждаше по-скоро така? Той съчетава преднамерено ужасната дефиниция на процедурата от преди с няколко други елемента, които може да очаквате да причинят проблеми, като имена на дефинирани от потребителя типове, две различни форми на OUT /OUTPUT ключова дума, Unicode в стойностите на параметрите (и в имената на параметри!), ключовите думи като константи и ODBC escape литерали.
/* 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
Предишният скрипт не обработва правилно множество обекти и трябва да добавим няколко логически елемента, за да отчетем OUTPUT и READONLY . По-конкретно, Output и ReadOnly не са типове токени, а по-скоро се разпознават като Identifier . Така че имаме нужда от допълнителна логика, за да намерим идентификатори с тези изрични имена във всеки ProcedureParameter фрагмент. Може да забележите още няколко дребни промени:
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;
}
}
} Този код е само за демонстрационни цели и е нулев шанс да е най-актуалният. Моля, вижте подробности по-долу за изтеглянето на по-нова версия.
Резултатът в този случай:
==========================Справка за процедурата
===========================
dbo.някаква_процедура
Име на параметър:@a
Тип на параметър:int
По подразбиране:5
Име на параметър:@b
Тип на параметър:varchar(64)
По подразбиране:'AS =/* BEGIN @a, int =7 */ "blat"'
Име на параметър:@c
Тип на параметър:int
По подразбиране:6
===========================
Справка за процедурата
==========================
[dbo].друга_процедура
Име на параметър:@p1
Тип на параметър:[int]
По подразбиране:1
Име на параметър:@p2
Тип на параметър:datetime
По подразбиране:getdate
Изход:да
Име на параметър:@p3
Тип на параметър:дата
По подразбиране:{ts '2020-02-01 13:12:49'}
Име на параметър:@p4
Тип на параметър:dbo.tabletype
Само за четене:да
Име на параметър:@p5
Тип на параметър:география
Изход:да
Име на параметър:@p6
Тип на параметър:sysname
По подразбиране:N'学中'
Това е доста мощен анализ, въпреки че има някои досадни крайни случаи и много условна логика. Бих искал да видя TSqlFragmentVisitor разширено, така че някои от неговите типове токени имат допълнителни свойства (като SchemaObjectName.IsFirstAppearance и ProcedureParameter.DefaultValue ) и вижте добавени нови типове токени (като FunctionReference ). Но дори и сега това са светлинни години отвъд анализатора на груба сила, който може да напишете във всяко език, няма значение T-SQL.
Все още обаче има няколко ограничения, които все още не съм разгледал:
- Това се отнася само за съхранените процедури. Кодът за обработка на трите типа дефинирани от потребителя функции е подобен , но няма удобен
FunctionReferenceтип фрагмент, така че вместо това трябва да идентифицирате първияSchemaObjectNameфрагмент (или първият набор отIdentifierиDotтокени) и игнорирайте всички последващи екземпляри. В момента кодът в тази публикация ще връща цялата информация за параметрите към функция, но няма върнете име на функцията . Чувствайте се свободни да го използвате за сингълтон или партиди, съдържащи само съхранени процедури, но може да откриете, че изходът е объркващ за множество смесени типове обекти. Най-новата версия в хранилището по-долу се справя перфектно с функциите. - Този код не запазва състоянието. Извеждането на конзолата в рамките на всяко посещение е лесно, но събирането на данни от множество посещения, за да се прехвърлят на друго място, е малко по-сложно, главно поради начина, по който работи моделът на посетителя.
- Кодът по-горе не може да приема директно въвеждане. За да опростите демонстрацията тук, това е просто необработен скрипт, в който поставяте своя T-SQL блок като константа. Евентуалната цел е да се поддържа въвеждане от файл, масив от файлове, папка, масив от папки или изтегляне на дефиниции на модули от база данни. И изходът може да бъде навсякъде:към конзолата, към файл, към база данни... така че небето е границата там. Част от тази работа се случи междувременно, но нищо от това не е написано в простата версия, която виждате по-горе.
- Няма обработка на грешки. Отново, за краткост и лекота на консумация, кодът тук не се тревожи за обработката на неизбежни изключения, въпреки че най-разрушителното нещо, което може да се случи в сегашния му вид, е, че партида няма да се появи в изхода, ако не може да бъде правилно анализиран (като
CREATE STUPID PROCEDURE dbo.whatever). Когато започнем да използваме бази данни и/или файлова система, правилното обработване на грешки ще стане много по-важно.
Можете да се чудите къде ще поддържам текущата работа по това и ще поправя всички тези неща? Е, сложих го в GitHub, нарекох условно проекта ParamParser , и вече има сътрудници, които помагат с подобренията. Текущата версия на кода вече изглежда доста по-различно от горната проба и докато прочетете това, някои от споменатите тук ограничения може вече да са адресирани. Искам само да поддържам кода на едно място; този съвет е по-скоро за показване на минимална извадка за това как може да работи и подчертаване, че има проект, посветен на опростяването на тази задача.
В следващия сегмент ще говоря повече за това как моят приятел и колега Уил Уайт ми помогна да премина от самостоятелния скрипт, който виждате по-горе, до много по-мощния модул, който ще намерите в GitHub.
Ако междувременно имате нужда да анализирате стойностите по подразбиране от параметри, не се колебайте да изтеглите кода и да го изпробвате. И както предложих преди, експериментирайте сами, защото има много други мощни неща, които можете да правите с тези класове и модела на посетителя.
[ Част 1 | Част 2 | Част 3 ]