[ Parte 1 | Parte 2 | Parte 3]
No meu último post, mostrei como usar o
TSqlParser
e TSqlFragmentVisitor
para extrair informações importantes de um script T-SQL contendo definições de procedimento armazenado. Com esse script, deixei de fora algumas coisas, como analisar o OUTPUT
e READONLY
palavras-chave para parâmetros e como analisar vários objetos juntos. Hoje, eu queria fornecer um script que lidasse com essas coisas, mencionar algumas outras melhorias futuras e compartilhar um repositório GitHub que criei para este trabalho. Anteriormente, usei um exemplo simples como este:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
E com o código de visitante que forneci, a saída para o console foi:
==========================
ProcedureReference
===========================
dbo.procedure1
==========================
ProcedureParameter
===========================
Nome do parâmetro:@param1
Tipo de parâmetro:int
Padrão:-64
Agora, e se o script passado se parecesse mais com isso? Ele combina a definição de procedimento intencionalmente terrível de antes com alguns outros elementos que você pode esperar causar problemas, como nomes de tipos definidos pelo usuário, duas formas diferentes de
OUT
/OUTPUT
palavra-chave, Unicode em valores de parâmetro (e em nomes de parâmetro!), palavras-chave como constantes e literais de escape 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
O script anterior não lida com vários objetos corretamente e precisamos adicionar alguns elementos lógicos para contabilizar
OUTPUT
e READONLY
. Especificamente, Output
e ReadOnly
não são tipos de token, mas são reconhecidos como um Identifier
. Portanto, precisamos de alguma lógica extra para encontrar identificadores com esses nomes explícitos em qualquer ProcedureParameter
fragmento. Você pode identificar algumas outras pequenas alterações: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; } } }
Este código é apenas para fins de demonstração e não há chance de ser o mais atual. Veja os detalhes abaixo sobre como baixar uma versão mais recente.
A saída neste caso:
==========================
ProcedureReference
===========================
dbo.some_procedure
Nome do parâmetro:@a
Tipo de parâmetro:int
Padrão:5
Nome do parâmetro:@b
Tipo de parâmetro:varchar(64)
Padrão:'AS =/* BEGIN @a, int =7 */ "blat"'
Nome do parâmetro:@c
Tipo de parâmetro:int
Padrão:6
===========================
ProcedureReference
==========================
[dbo].another_procedure
Nome do parâmetro:@p1
Tipo de parâmetro:[int]
Padrão:1
Nome do parâmetro:@p2
Tipo de parâmetro:datetime
Padrão:getdate
Saída:sim
Nome do parâmetro:@p3
Tipo de parâmetro:data
Padrão:{ts '2020-02-01 13:12:49'}
Nome do parâmetro:@p4
Tipo de parâmetro:dbo.tabletype
Somente leitura:sim
Nome do parâmetro:@p5
Tipo de parâmetro:geografia
Saída:sim
Nome do parâmetro:@p6
Tipo de parâmetro:sysname
Padrão:N'学中'
Essa é uma análise bastante poderosa, embora existam alguns casos de borda tediosos e muita lógica condicional. Eu adoraria ver
TSqlFragmentVisitor
expandido para que alguns de seus tipos de token tenham propriedades adicionais (como SchemaObjectName.IsFirstAppearance
e ProcedureParameter.DefaultValue
) e veja os novos tipos de token adicionados (como FunctionReference
). Mas mesmo agora, isso está anos-luz além de um analisador de força bruta que você pode escrever em qualquer linguagem, não importa o T-SQL. Ainda existem algumas limitações que ainda não abordei:
- Isso aborda apenas procedimentos armazenados. O código para lidar com todos os três tipos de funções definidas pelo usuário é semelhante , mas não há
FunctionReference
útil tipo de fragmento, então você precisa identificar o primeiroSchemaObjectName
fragmento (ou o primeiro conjunto deIdentifier
eDot
tokens) e ignore quaisquer instâncias subsequentes. Atualmente, o código neste post vai retornar todas as informações sobre os parâmetros a uma função, mas não retorna o nome da função . Sinta-se à vontade para usá-lo para singletons ou lotes contendo apenas procedimentos armazenados, mas você pode achar a saída confusa para vários tipos de objetos mistos. A versão mais recente no repositório abaixo lida perfeitamente bem com as funções. - Este código não salva o estado. A saída para o console dentro de cada Visit é fácil, mas coletar os dados de várias visitas, para depois canalizar para outro lugar, é um pouco mais complexo, principalmente devido à maneira como o padrão Visitor funciona.
- O código acima não pode aceitar entrada diretamente. Para simplificar a demonstração aqui, é apenas um script bruto onde você cola seu bloco T-SQL como uma constante. O objetivo final é dar suporte à entrada de um arquivo, uma matriz de arquivos, uma pasta, uma matriz de pastas ou extrair definições de módulo de um banco de dados. E a saída pode ser em qualquer lugar:no console, em um arquivo, em um banco de dados… então o céu é o limite. Parte desse trabalho aconteceu nesse meio tempo, mas nada disso foi escrito na versão simples que você vê acima.
- Não há tratamento de erros. Novamente, por brevidade e facilidade de consumo, o código aqui não se preocupa em lidar com exceções inevitáveis, embora a coisa mais destrutiva que pode acontecer em sua forma atual é que um lote não aparecerá na saída se não puder ser corretamente analisado (como
CREATE STUPID PROCEDURE dbo.whatever
). Quando começamos a usar bancos de dados e/ou o sistema de arquivos, o tratamento adequado de erros se tornará muito mais importante.

No próximo segmento, falarei mais sobre como meu amigo e colega Will White me ajudou a passar do script autônomo que você vê acima para o módulo muito mais poderoso que você encontrará no GitHub.
Se você precisar analisar valores padrão de parâmetros nesse meio tempo, sinta-se à vontade para baixar o código e experimentá-lo. E como sugeri antes, experimente por conta própria, porque há muitas outras coisas poderosas que você pode fazer com essas classes e o padrão Visitor.
[ Parte 1 | Parte 2 | Parte 3]