[ Част 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 ]