[ Partie 1 | Partie 2 | Partie 3 ]
Dans mon dernier post, j'ai montré comment utiliser TSqlParser
et TSqlFragmentVisitor
pour extraire des informations importantes d'un script T-SQL contenant des définitions de procédures stockées. Avec ce script, j'ai omis quelques éléments, tels que la façon d'analyser le OUTPUT
et READONLY
mots-clés pour les paramètres et comment analyser plusieurs objets ensemble. Aujourd'hui, je voulais fournir un script qui gère ces choses, mentionner quelques autres améliorations futures et partager un référentiel GitHub que j'ai créé pour ce travail.
Auparavant, j'utilisais un exemple simple comme celui-ci :
CRÉER PROCÉDURE dbo.procedure1 @param1 AS int =/* commentaire */ -64AS PRINT 1;GO
Et avec le code visiteur que j'ai fourni, la sortie vers la console était :
=========================ProcédureRéférence
==========================
dbo.procedure1
========================
ParamètreProcédure
==========================
Nom du paramètre :@param1
Type de paramètre :int
Par défaut :-64
Maintenant, et si le script passé ressemblait plus à ça ? Il combine la définition de procédure intentionnellement terrible d'avant avec quelques autres éléments dont vous pourriez vous attendre à causer des problèmes, comme les noms de type définis par l'utilisateur, deux formes différentes de OUT
/OUTPUT
mot-clé, Unicode dans les valeurs de paramètres (et dans les noms de paramètres !), Mots-clés en tant que constantes et littéraux d'échappement ODBC.
/* AS BEGIN , @a int =7, les commentaires peuvent apparaître n'importe où */CREATE PROCEDURE dbo.some_procedure -- AS BEGIN, @a int =7 'blat' AS =/* AS BEGIN, @a int =7 'blat' AS =-- */ @a AS /* commentez ici car -- chaos */ int =5, @b AS varchar(64) ='AS =/* BEGIN @a, int =7 */ '' blat''', @c AS int =-- 12 6AS -- @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 géographie OUT, @p6 sysname =N'学中')AS SELECT 5
Le script précédent ne gère pas correctement plusieurs objets, et nous devons ajouter quelques éléments logiques pour tenir compte de OUTPUT
et READONLY
. Plus précisément, Output
et ReadOnly
ne sont pas des types de jetons, mais plutôt ils sont reconnus comme un Identifier
. Nous avons donc besoin d'une logique supplémentaire pour trouver des identifiants avec ces noms explicites dans n'importe quel ProcedureParameter
fragment. Vous remarquerez peut-être quelques autres modifications mineures :
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, les commentaires peuvent apparaître n'importe où */ CREATE PROCEDURE dbo.some_procedure -- AS BEGIN, @a int =7 'blat' AS =/* AS BEGIN, @a int =7 'blat' AS =-- */ @a AS /* commentez ici car -- 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,-- commentaire, @p3 date ={ts '2020-02-01 13:12:49'}, @p4 dbo.tabletype READONLY, @ p5 géographie OUT, @p6 sysname =N'学中' ) AS SELECT 5"@ $fragment =$parser.Parse([System.IO.StringReader]::New($procedure), [ref]$errors); $visiteur =[Visiteur] ::Nouveau(); $fragment.Accept($visiteur); Visiteur de classe :Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragmentVisitor { [void]Visite ([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 "==========================" ; } $sortie ="" ; $param ="" ; $type ="" ; $par défaut ="" ; $extra ="" ; $isReadOnly =$false ; $isOutput =$false ; $vuEquals =$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; } sinon { 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 ="`nLecture seule :oui" ; } if ($isOutput) { $extra ="`nOutput :oui" ; } Write-Host $output $type $default $extra ; } } }Ce code est uniquement à des fins de démonstration, et il n'y a aucune chance qu'il soit le plus récent. Veuillez consulter les détails ci-dessous concernant le téléchargement d'une version plus récente.
La sortie dans ce cas :
=========================
ProcédureRéférence
==========================
dbo.some_procedure
Nom du paramètre :@a
Type de paramètre :int
Par défaut :5
Nom du paramètre :@b
Type de paramètre :varchar(64)
Par défaut :'AS =/* BEGIN @a, int =7 */ "blat"'
Nom du paramètre :@c
Type de paramètre :int
Par défaut :6
=========================
ProcédureRéférence
==========================
[dbo].another_procedure
Nom du paramètre :@p1
Type de paramètre :[int]
Par défaut :1
Nom du paramètre :@p2
Type de paramètre :datetime
Par défaut :getdate
Sortie :oui
Nom du paramètre :@p3
Type de paramètre :date
Par défaut :{ts '2020-02-01 13:12:49'}
Nom du paramètre :@p4
Type de paramètre :dbo.tabletype
Lecture seule :oui
Nom du paramètre :@p5
Type de paramètre :géographie
Sortie :oui
Nom du paramètre :@p6
Type de paramètre :sysname
Par défaut :N'学中'C'est une analyse assez puissante, même s'il y a des cas fastidieux et beaucoup de logique conditionnelle. J'aimerais voir
TSqlFragmentVisitor
étendu afin que certains de ses types de jetons aient des propriétés supplémentaires (commeSchemaObjectName.IsFirstAppearance
etProcedureParameter.DefaultValue
), et voir les nouveaux types de jetons ajoutés (commeFunctionReference
). Mais même maintenant, c'est à des années-lumière au-delà d'un analyseur de force brute que vous pourriez écrire dans n'importe quel langage, sans parler de T-SQL.Il y a encore quelques limitations que je n'ai pas encore abordées :
- Cela ne concerne que les procédures stockées. Le code pour gérer les trois types de fonctions définies par l'utilisateur est similaire , mais il n'y a pas de
FunctionReference
pratique type de fragment, vous devez donc à la place identifier le premierSchemaObjectName
fragment (ou le premier ensemble deIdentifier
etDot
jetons) et ignorez toutes les instances suivantes. Actuellement, le code dans ce message sera renvoie toutes les informations sur les paramètres à une fonction, mais elle ne le fera pas renvoie le nom de la fonction . N'hésitez pas à l'utiliser pour les singletons ou les lots contenant uniquement des procédures stockées, mais vous pourriez trouver la sortie déroutante pour plusieurs types d'objets mixtes. La dernière version du référentiel ci-dessous gère parfaitement les fonctions. - Ce code n'enregistre pas l'état. La sortie vers la console au sein de chaque visite est facile, mais la collecte des données de plusieurs visites, pour ensuite les canaliser ailleurs, est un peu plus complexe, principalement en raison du fonctionnement du modèle de visiteur.
- Le code ci-dessus ne peut pas accepter d'entrée directement. Pour simplifier la démonstration ici, il s'agit simplement d'un script brut dans lequel vous collez votre bloc T-SQL en tant que constante. L'objectif final est de prendre en charge l'entrée d'un fichier, d'un tableau de fichiers, d'un dossier, d'un tableau de dossiers ou d'extraire des définitions de module d'une base de données. Et la sortie peut être n'importe où :vers la console, vers un fichier, vers une base de données… donc le ciel est la limite là-bas. Une partie de ce travail a eu lieu entre-temps, mais rien de tout cela n'a été écrit dans la version simple que vous voyez ci-dessus.
- Il n'y a pas de gestion des erreurs. Encore une fois, pour des raisons de brièveté et de facilité d'utilisation, le code ici ne se soucie pas de la gestion des exceptions inévitables, bien que la chose la plus destructrice qui puisse arriver dans sa forme actuelle est qu'un lot n'apparaîtra pas dans la sortie s'il ne peut pas être correctement analysé (comme
CREATE STUPID PROCEDURE dbo.whatever
). Lorsque nous commençons à utiliser des bases de données et/ou le système de fichiers, une bonne gestion des erreurs deviendra d'autant plus importante.
Vous vous demandez peut-être où vais-je continuer à travailler sur ce sujet et résoudre tous ces problèmes ? Eh bien, je l'ai mis sur GitHub, j'ai provisoirement appelé le projet ParamParser , et ont déjà des contributeurs qui contribuent aux améliorations. La version actuelle du code semble déjà assez différente de l'exemple ci-dessus, et au moment où vous lirez ceci, certaines des limitations mentionnées ici pourraient déjà être résolues. Je veux seulement conserver le code à un seul endroit ; cette astuce consiste davantage à montrer un échantillon minimal de la façon dont cela peut fonctionner et à souligner qu'il existe un projet dédié à la simplification de cette tâche.
Dans le segment suivant, je parlerai davantage de la façon dont mon ami et collègue, Will White, m'a aidé à passer du script autonome que vous voyez ci-dessus au module beaucoup plus puissant que vous trouverez sur GitHub.
Si vous avez besoin d'analyser les valeurs par défaut des paramètres entre-temps, n'hésitez pas à télécharger le code et à l'essayer. Et comme je l'ai déjà suggéré, expérimentez par vous-même, car il y a beaucoup d'autres choses puissantes que vous pouvez faire avec ces classes et le modèle Visitor.
[ Partie 1 | Partie 2 | Partie 3 ]