[パート1|パート2|パート3]
前回の投稿では、 TSqlParserの使用方法を示しました およびTSqlFragmentVisitor ストアドプロシージャの定義を含むT-SQLスクリプトから重要な情報を抽出します。そのスクリプトでは、 OUTPUT を解析する方法など、いくつかのことを省略しました。 およびREADONLY パラメータのキーワード、および複数のオブジェクトを一緒に解析する方法。今日、私はそれらを処理するスクリプトを提供し、他のいくつかの将来の拡張機能について言及し、この作業のために作成したGitHubリポジトリを共有したいと思いました。
以前は、次のような簡単な例を使用しました:
CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO
そして、私が提供したビジターコードを使用すると、コンソールへの出力は次のようになります。
==========================ProcedureReference
==========================
dbo.procedure1
==========================
ProcedureParameter
==========================
パラメータ名:@ param1
パラメータタイプ:int
デフォルト:-64
さて、渡されたスクリプトがこのようになったらどうなるでしょうか。これは、以前の意図的にひどいプロシージャ定義と、ユーザー定義の型名、2つの異なる形式の OUT など、問題を引き起こす可能性のある他のいくつかの要素を組み合わせたものです。 /出力コード> キーワード、パラメータ値(およびパラメータ名!)のUnicode、定数としてのキーワード、および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
前のスクリプトは複数のオブジェクトを正しく処理していないため、 OUTPUTを説明するためにいくつかの論理要素を追加する必要があります およびREADONLY 。具体的には、 Output およびReadOnly トークンタイプではなく、識別子として認識されます 。したがって、 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;
}
}
} このコードはデモンストレーションのみを目的としており、最新である可能性はありません。最新バージョンのダウンロードについては、以下の詳細をご覧ください。
この場合の出力:
==========================ProcedureReference
==========================
dbo.some_procedure
パラメータ名:@a
パラメータタイプ:int
デフォルト:5
パラメータ名:@b
パラメータタイプ:varchar(64)
デフォルト:'AS =/ * BEGIN @a、int =7 * / "blat"'
パラメータ名:@c
パラメータタイプ:int
デフォルト:6
==========================
ProcedureReference
==========================
[dbo] .another_procedure
パラメータ名:@ p1
パラメータタイプ:[int]
デフォルト:1
パラメータ名:@ p2
パラメータタイプ:datetime
デフォルト:getdate
出力:yes
パラメータ名:@ p3
パラメータタイプ:日付
デフォルト:{ts '2020-02-01 13:12:49'}
パラメータ名:@ p4
パラメータタイプ:dbo.tabletype
読み取り専用:はい
パラメータ名:@ p5
パラメータタイプ:地理
出力:はい
パラメータ名:@ p6
パラメータタイプ:sysname
デフォルト:N'学中'
面倒なエッジケースと多くの条件付きロジックがありますが、これはかなり強力な解析です。 TSqlFragmentVisitorが見たいです 一部のトークンタイプに追加のプロパティ( SchemaObjectName.IsFirstAppearance など)が含まれるように拡張されました およびProcedureParameter.DefaultValue )、追加された新しいトークンタイプ( FunctionReference など)を確認します )。しかし、今でも、これはあなたが anyで書くかもしれないブルートフォースパーサーをはるかに超えています。 言語、T-SQLを気にしないでください。
ただし、まだ対処していない制限がいくつかあります。
- これはストアドプロシージャのみに対応します。 3種類すべてのユーザー定義関数を処理するコードは類似です。 、しかし便利な
FunctionReferenceはありません フラグメントタイプなので、代わりに最初のSchemaObjectNameを識別する必要があります フラグメント(または識別子の最初のセット およびDotトークン)、後続のインスタンスを無視します。現在、この投稿のコードは パラメータに関するすべての情報を返します 関数に変換しますが、実行しません 関数の名前を返します 。ストアドプロシージャのみを含むシングルトンまたはバッチに自由に使用できますが、複数の混合オブジェクトタイプの出力が混乱する場合があります。以下のリポジトリの最新バージョンは、機能を完全に正常に処理します。 - このコードは状態を保存しません。 各Visit内でコンソールに出力するのは簡単ですが、複数のVisitからデータを収集し、他の場所にパイプラインすることは、主にVisitorパターンの動作方法のために、もう少し複雑です。
- 上記のコードは直接入力を受け入れることができません。 ここでのデモンストレーションを簡略化するために、これはT-SQLブロックを定数として貼り付ける生のスクリプトです。最終的な目標は、ファイル、ファイルの配列、フォルダー、フォルダーの配列からの入力をサポートすること、またはデータベースからモジュール定義をプルすることです。また、出力は、コンソール、ファイル、データベースなど、どこにでも配置できます。つまり、空が限界です。その作業の一部はその間に行われていますが、上記の単純なバージョンではそのいずれも記述されていません。
- エラー処理はありません。 繰り返しになりますが、簡潔さと消費のしやすさのために、ここのコードは避けられない例外の処理について心配していませんが、現在の形式で発生する可能性のある最も破壊的なことは、バッチが適切にできない場合は出力に表示されないことです解析済み(
CREATE STUPID PROCEDURE dbo.whateverなど )。データベースやファイルシステムの使用を開始すると、適切なエラー処理がさらに重要になります。
これについて継続的な作業をどこで維持し、これらすべてを修正するのか疑問に思われるかもしれません。さて、私はそれをGitHubに配置し、暫定的にプロジェクトを ParamParserと呼びました。 、およびすでに改善を支援する貢献者がいます。コードの現在のバージョンは、上記のサンプルとはかなり異なって見えます。これを読むまでに、ここで説明した制限のいくつかはすでに解決されている可能性があります。コードを1か所にまとめたいだけです。このヒントは、それがどのように機能するかについての最小限のサンプルを示し、このタスクを単純化することに専念するプロジェクトがそこにあることを強調することに関するものです。
次のセグメントでは、友人であり同僚であるWill Whiteが、上記のスタンドアロンスクリプトから、GitHubにあるはるかに強力なモジュールに移行するのにどのように役立ったかについて詳しく説明します。
その間にパラメータからデフォルト値を解析する必要がある場合は、コードをダウンロードして試してみてください。そして、前に提案したように、これらのクラスとビジターパターンで実行できる強力なことが他にもたくさんあるので、自分で実験してください。
[パート1|パート2|パート3]