[ Část 1 | Část 2 | Část 3 ]
V mém posledním příspěvku jsem ukázal, jak používat TSqlParser
a TSqlFragmentVisitor
extrahovat důležité informace ze skriptu T-SQL obsahujícího definice uložených procedur. S tímto skriptem jsem vynechal několik věcí, například jak analyzovat OUTPUT
a READONLY
klíčová slova pro parametry a jak analyzovat více objektů dohromady. Dnes jsem chtěl poskytnout skript, který tyto věci zvládne, zmínit několik dalších budoucích vylepšení a sdílet úložiště GitHub, které jsem pro tuto práci vytvořil.
Dříve jsem použil jednoduchý příklad, jako je tento:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
A s kódem návštěvníka, který jsem poskytl, byl výstup do konzole:
==========================Reference procedur
==========================
dbo.procedure1
==========================
Parametr procedury
===========================
Název parametru:@param1
Typ parametru:int
Výchozí:-64
A teď, co když předaný scénář vypadal spíš takto? Kombinuje záměrně hroznou definici procedury z dřívějška s několika dalšími prvky, u kterých byste mohli očekávat, že způsobí problémy, jako jsou uživatelsky definované názvy typů, dvě různé formy OUT
/OUTPUT
klíčové slovo, Unicode v hodnotách parametrů (a v názvech parametrů!), klíčová slova jako konstanty a ODBC escape literály.
/* 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
Předchozí skript nezpracovává více objektů správně a potřebujeme přidat několik logických prvků, abychom zohlednili OUTPUT
a READONLY
. Konkrétně Output
a ReadOnly
nejsou typy tokenů, ale spíše jsou rozpoznávány jako Identifier
. Potřebujeme tedy nějakou extra logiku, abychom našli identifikátory s těmito explicitními názvy v jakémkoli ProcedureParameter
fragment. Můžete zaznamenat několik dalších menších změn:
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; } } }
Tento kód slouží pouze pro demonstrační účely a není pravděpodobné, že by byl nejaktuálnější. Podrobnosti o stažení novější verze naleznete níže.
Výstup v tomto případě:
==========================Reference procedur
==========================
dbo.some_procedure
Název parametru:@a
Typ parametru:int
Výchozí:5
Název parametru:@b
Typ parametru:varchar(64)
Výchozí:'AS =/* BEGIN @a, int =7 */ "blat"'
Název parametru:@c
Typ parametru:int
Výchozí:6
===========================
Reference procedur
==========================
[dbo].jiný_procedura
Název parametru:@p1
Typ parametru:[int]
Výchozí:1
Název parametru:@p2
Typ parametru:datetime
Výchozí:getdate
Výstup:ano
Název parametru:@p3
Typ parametru:datum
Výchozí:{ts '2020-02-01 13:12:49'}
Název parametru:@p4
Typ parametru:dbo.tabletype
Pouze pro čtení:ano
Název parametru:@p5
Typ parametru:geografie
Výstup:ano
Název parametru:@p6
Typ parametru:sysname
Výchozí:N'学中'
To je docela výkonná analýza, i když existují některé únavné okrajové případy a spousta podmíněné logiky. Rád bych viděl TSqlFragmentVisitor
rozšířen, takže některé jeho typy tokenů mají další vlastnosti (jako SchemaObjectName.IsFirstAppearance
a ProcedureParameter.DefaultValue
) a podívejte se na přidané nové typy tokenů (například FunctionReference
). Ale i nyní je to světelné roky za analyzátorem hrubé síly, který byste mohli napsat jakýmkoli jazyk, bez ohledu na T-SQL.
Stále však existuje několik omezení, která jsem ještě nevyřešil:
- Toto se týká pouze uložených procedur. Kód pro obsluhu všech tří typů uživatelsky definovaných funkcí je podobný , ale neexistuje žádný praktický
FunctionReference
fragment, takže místo toho musíte identifikovat prvníSchemaObjectName
fragment (nebo první sadaIdentifier
aDot
tokeny) a ignorovat všechny následující instance. Aktuálně kód v tomto příspěvku bude vrátit všechny informace o parametrech na funkci, ale nebude vrátit název funkce . Neváhejte jej použít pro jednotlivé typy nebo dávky obsahující pouze uložené procedury, ale výstup může být matoucí pro různé typy smíšených objektů. Nejnovější verze v úložišti níže zvládá funkce naprosto v pořádku. - Tento kód neukládá stav. Výstup do konzole v rámci každé návštěvy je snadný, ale shromažďování dat z více návštěv za účelem jejich přenosu jinam je o něco složitější, především kvůli tomu, jak funguje vzor Návštěvník.
- Výše uvedený kód nemůže přijímat vstup přímo. Pro zjednodušení demonstrace je to jen surový skript, kam vložíte svůj blok T-SQL jako konstantu. Konečným cílem je podpora vstupu ze souboru, pole souborů, složky, pole složek nebo stahování definic modulů z databáze. A výstup může být kdekoli:do konzole, do souboru, do databáze... takže tam je limit nebe. Nějaká ta práce mezitím proběhla, ale nic z toho nebylo napsáno v jednoduché verzi, kterou vidíte výše.
- Nedochází k žádnému zpracování chyb. Opět, pro stručnost a snadnost použití se zde kód nestará o manipulaci s nevyhnutelnými výjimkami, i když nejničivější věc, která se v jeho současné podobě může stát, je, že se dávka neobjeví ve výstupu, pokud nemůže být správně analyzovat (jako
CREATE STUPID PROCEDURE dbo.whatever
). Když začneme používat databáze a/nebo souborový systém, správné zpracování chyb bude mnohem důležitější.
Mohli byste se divit, kde na tom budu pokračovat v práci a všechny tyto věci opravit? No, dal jsem to na GitHub, předběžně jsem nazval projekt ParamParser a již mají přispěvatelé, kteří pomáhají s vylepšeními. Aktuální verze kódu již vypadá zcela odlišně od výše uvedeného příkladu a v době, kdy si toto přečtete, mohou být některá ze zde zmíněných omezení již vyřešena. Chci pouze udržovat kód na jednom místě; tento tip je spíše o ukázce minimální ukázky toho, jak to může fungovat, a zdůraznění, že existuje projekt, který se věnuje zjednodušení tohoto úkolu.
V další části budu mluvit více o tom, jak mi můj přítel a kolega Will White pomohl dostat se ze samostatného skriptu, který vidíte výše, k mnohem výkonnějšímu modulu, který najdete na GitHubu.
Pokud mezitím potřebujete analyzovat výchozí hodnoty z parametrů, můžete si kód stáhnout a vyzkoušet. A jak jsem již naznačil, experimentujte sami, protože s těmito třídami a vzorem Návštěvník můžete dělat spoustu dalších mocných věcí.
[ Část 1 | Část 2 | Část 3 ]