[ Parte 1 | Parte 2 | Parte 3]
Nel mio ultimo post, ho mostrato come usare TSqlParser
e TSqlFragmentVisitor
per estrarre informazioni importanti da uno script T-SQL contenente le definizioni delle procedure memorizzate. Con quello script, ho tralasciato alcune cose, come come analizzare l'OUTPUT
e READONLY
parole chiave per i parametri e come analizzare più oggetti insieme. Oggi volevo fornire uno script che gestisse queste cose, menzionare alcuni altri miglioramenti futuri e condividere un repository GitHub che ho creato per questo lavoro.
In precedenza, ho usato un semplice esempio come questo:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
E con il codice visitatore che ho fornito, l'output sulla console era:
===========================Riferimento procedura
===========================
dbo.procedure1
==========================
Parametro procedura
============================
Nome parametro:@param1
Tipo parametro:int
Predefinito:-64
E se lo script passato fosse più simile a questo? Combina la definizione di procedura intenzionalmente terribile di prima con un paio di altri elementi che potresti aspettarti di causare problemi, come i nomi dei tipi definiti dall'utente, due diverse forme di OUT
/OUTPUT
parola chiave, Unicode nei valori dei parametri (e nei nomi dei parametri!), parole chiave come costanti e valori letterali di 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
Lo script precedente non gestisce correttamente più oggetti e dobbiamo aggiungere alcuni elementi logici per tenere conto di OUTPUT
e READONLY
. In particolare, Output
e ReadOnly
non sono tipi di token, ma sono riconosciuti come un Identifier
. Quindi abbiamo bisogno di una logica extra per trovare identificatori con quei nomi espliciti all'interno di qualsiasi ProcedureParameter
frammento. Potresti notare alcune altre modifiche minori:
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; } } }
Questo codice è solo a scopo dimostrativo e non ci sono possibilità che sia il più aggiornato. Consulta i dettagli di seguito sul download di una versione più recente.
L'output in questo caso:
===========================Riferimento procedura
===========================
dbo.some_procedure
Nome parametro:@a
Tipo parametro:int
Predefinito:5
Nome parametro:@b
Tipo parametro:varchar(64)
Predefinito:'AS =/* BEGIN @a, int =7 */ "blat"'
Nome parametro:@c
Tipo parametro:int
Predefinito:6
===========================
Riferimento procedura
===========================
[dbo].another_procedure
Nome parametro:@p1
Tipo parametro:[int]
Predefinito:1
Nome parametro:@p2
Tipo parametro:datetime
Default:getdate
Output:sì
Nome parametro:@p3
Tipo parametro:data
Predefinito:{ts '2020-02-01 13:12:49'}
Nome parametro:@p4
Tipo parametro:dbo.tabletype
Sola lettura:sì
Nome parametro:@p5
Tipo parametro:geografia
Output:sì
Nome parametro:@p6
Tipo parametro:sysname
Predefinito:N'学中'
È un'analisi piuttosto potente, anche se ci sono alcuni casi limite noiosi e molta logica condizionale. Mi piacerebbe vedere TSqlFragmentVisitor
ampliato in modo che alcuni dei suoi tipi di token abbiano proprietà aggiuntive (come SchemaObjectName.IsFirstAppearance
e ProcedureParameter.DefaultValue
) e vedere nuovi tipi di token aggiunti (come FunctionReference
). Ma anche ora, questo è anni luce al di là di un parser di forza bruta che potresti scrivere in qualsiasi lingua, non importa T-SQL.
Tuttavia, ci sono ancora un paio di limitazioni che non ho ancora affrontato:
- Indirizza solo le procedure archiviate. Il codice per gestire tutti e tre i tipi di funzioni definite dall'utente è simile , ma non c'è un pratico
FunctionReference
tipo di frammento, quindi devi invece identificare il primoSchemaObjectName
frammento (o il primo set diIdentifier
eDot
token) e ignora eventuali istanze successive. Attualmente il codice in questo post sarà restituire tutte le informazioni sui parametri a una funzione, ma non lo farà restituisce il nome della funzione . Sentiti libero di usarlo per singleton o batch contenenti solo stored procedure, ma potresti trovare l'output confuso per più tipi di oggetti misti. L'ultima versione nel repository sottostante gestisce perfettamente le funzioni. - Questo codice non salva lo stato. L'output sulla console all'interno di ogni visita è facile, ma la raccolta dei dati da più visite, per poi pipeline altrove, è un po' più complessa, principalmente a causa del modo in cui funziona il pattern Visitor.
- Il codice sopra non può accettare input direttamente. Per semplificare la dimostrazione qui, è solo uno script non elaborato in cui incolli il tuo blocco T-SQL come costante. L'obiettivo finale è supportare l'input da un file, un array di file, una cartella, un array di cartelle o estrarre le definizioni dei moduli da un database. E l'output può essere ovunque:sulla console, su un file, su un database... quindi il limite è il cielo. Alcuni di quei lavori sono avvenuti nel frattempo, ma niente di tutto ciò è stato scritto nella versione semplice che vedi sopra.
- Nessuna gestione degli errori. Ancora una volta, per brevità e facilità di utilizzo, il codice qui non si preoccupa di gestire le inevitabili eccezioni, anche se la cosa più distruttiva che può accadere nella sua forma attuale è che un batch non apparirà nell'output se non può essere correttamente analizzato (come
CREATE STUPID PROCEDURE dbo.whatever
). Quando iniziamo a utilizzare i database e/o il file system, la corretta gestione degli errori diventerà molto più importante.
Potresti chiederti, dove manterrò il lavoro in corso su questo e risolverò tutte queste cose? Bene, l'ho messo su GitHub, ho chiamato provvisoriamente il progetto ParamParser e hanno già contributori che aiutano con i miglioramenti. La versione corrente del codice sembra già molto diversa dall'esempio precedente e quando leggerai questo alcune delle limitazioni qui menzionate potrebbero già essere risolte. Voglio solo mantenere il codice in un posto; questo suggerimento riguarda più il mostrare un campione minimo di come può funzionare e l'evidenziazione che esiste un progetto dedicato alla semplificazione di questo compito.
Nel prossimo segmento parlerò di più su come il mio amico e collega, Will White, mi ha aiutato a passare dallo script autonomo che vedi sopra al modulo molto più potente che troverai su GitHub.
Se nel frattempo hai la necessità di analizzare i valori predefiniti dai parametri, sentiti libero di scaricare il codice e provarlo. E come ho suggerito prima, sperimenta da solo, perché ci sono molte altre cose potenti che puoi fare con queste classi e il modello Visitor.
[ Parte 1 | Parte 2 | Parte 3]