sql >> データベース >  >> RDS >> Database

PowerShellを使用してパラメーターのデフォルト値を解析する–パート1

    [パート1|パート2|パート3]

    ストアドプロシージャパラメータのデフォルト値を決定しようとしたことがある場合は、額が机に繰り返し激しく当たったことによる痕跡がある可能性があります。 (このヒントのように)パラメータ情報の取得について説明しているほとんどの記事では、デフォルトという言葉すら言及されていません。これは、オブジェクトの定義に格納されている生のテキストを除いて、情報がカタログビューのどこにも存在しないためです。 has_default_value列があります およびdefault_value sys.parameters内 その見た目 有望ですが、CLRモジュールにのみ入力されます。

    T-SQLを使用してデフォルト値を取得するのは面倒で、エラーが発生しやすくなります。私は最近、この問題に関するStack Overflowの質問に答えましたが、それは私を記憶の道に連れて行きました。 2006年に、私は複数のConnectアイテムを介して、カタログビューのパラメーターのデフォルト値が表示されないことについて不満を述べました。ただし、SQL Server 2019にはまだ問題があります。(これが、新しいフィードバックシステムに到達した唯一のアイテムです。)

    デフォルト値がメタデータに公開されていないのは不便ですが、オブジェクトテキスト(どの言語でも、特にT-SQL)からデフォルト値を解析するのは難しいため、デフォルト値が公開されていない可能性があります。 T-SQLの解析機能は非常に限られており、想像以上に多くのエッジケースがあるため、パラメータリストの開始と終了を見つけることさえ困難です。いくつかの例:

    • (の存在に依存することはできません および) パラメータリストはオプションであるため(パラメータリスト全体に表示される場合があります)、パラメータリストを示します。
    • 最初のASを簡単に解析することはできません それは他の理由で現れる可能性があるので、体の始まりをマークするために
    • BEGINの存在に依存することはできません オプションなので、ボディの始まりをマークします
    • コンマはコメント内、文字列リテラル内、およびデータ型宣言の一部として表示される可能性があるため、コンマで分割するのは困難です((precision, scale)を考えてください。 )。
    • 両方のタイプのコメントを解析するのは非常に困難です。これらのコメントはどこにでも表示され(文字列リテラル内を含む)、ネストできます。
    • 文字列リテラルとコメント内で、重要なキーワード、コンマ、等号を誤って見つける可能性があります
    • 数値や文字列リテラルではないデフォルト値を使用できます({fn curdate()}を考えてください)。 またはGETDATE

    構文のバリエーションが非常に多いため、通常の文字列解析手法は無効になります。 ASを見たことがありますか すでに?パラメータ名とデータ型の間にありましたか?パラメータリスト全体を囲む右括弧の後でしたか、それとも最後にパラメータを表示する前に一致しなかった[1つ?]でしたか?そのコンマは2つのパラメーターを区切っていますか、それとも精度とスケールの一部ですか?一度に1単語ずつ文字列をループしていると、文字列はどんどん増えていき、追跡する必要のあるビットが非常に多くなります。

    この(意図的にばかげているが、構文的には有効な)例を見てください:

    /* 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;

    T-SQLを使用して、その定義からデフォルト値を解析するのは困難です。 本当に難しいBEGINなし パラメータリストの終わり、すべてのコメントの混乱、およびASなどのキーワードのすべてのケースを適切にマークするため さまざまな意味を持つ可能性があります。おそらく、より多くのSUBSTRINGを含む複雑なネストされた式のセットがあります。 およびCHARINDEX これまでに1か所で見たよりも多くのパターン。そして、おそらくまだ@dで終わるでしょう および@e ローカル変数ではなく、プロシージャパラメータのように見えます。

    問題についてもう少し考えて、過去10年間に誰かが何か新しいことを管理したかどうかを調べて、MichaelSwartによるこの素晴らしい投稿に出くわしました。その投稿では、MichaelはScriptDomのTSqlParserを使用して、T-SQLのブロックから1行と複数行の両方のコメントを削除しています。そこで、他のどのトークンが識別されたかを確認するための手順を実行するためのPowerShellコードをいくつか作成しました。意図的な問題をすべて発生させずに、より簡単な例を見てみましょう。

    CREATE PROCEDURE dbo.procedure1
      @param1 int
    AS PRINT 1;
    GO

    Visual Studio Code(またはお気に入りのPowerShell IDE)を開き、Test1.ps1という名前の新しいファイルを保存します。唯一の前提条件は、最新バージョンのMicrosoft.SqlServer.TransactSql.ScriptDom.dll(ここからダウンロードしてsqlpackageから抽出できます)を.ps1ファイルと同じフォルダーに置くことです。このコードをコピーして保存し、実行またはデバッグします。

    # need to extract this DLL from latest sqlpackage; place it in same folder
    # https://docs.microsoft.com/en-us/sql/tools/sqlpackage-download
    Add-Type -Path "Microsoft.SqlServer.TransactSql.ScriptDom.dll";
     
    # set up a parser object using the most recent version available 
    $parser = [Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser]($true)::New(); 
     
    # and an error collector
    $errors = [System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]]::New();
     
    # this ultimately won't come from a constant - think file, folder, database
    # can be a batch or multiple batches, just keeping it simple to start
     
    $procedure = @"
    CREATE PROCEDURE dbo.procedure1
      @param1 AS int
    AS PRINT 1;
    GO
    "@
     
    # now we need to try parsing
    $block = $parser.Parse([System.IO.StringReader]::New($procedure), [ref]$errors);
     
    # parse the whole thing, which is a set of one or more batches
    foreach ($batch in $block.Batches)
    {
        # each batch contains one or more statements
        # (though a valid create procedure statement is also always just one batch)
        foreach ($statement in $batch.Statements)
        {
            # output the type of statement
            Write-Host "  ====================================";
            Write-Host "    $($statement.GetType().Name)";
            Write-Host "  ====================================";        
     
            # each statement has one or more tokens in its token stream
            foreach ($token in $statement.ScriptTokenStream)
            {
                # those tokens have properties to indicate the type
                # as well as the actual text of the token
                Write-Host "  $($token.TokenType.ToString().PadRight(16)) : $($token.Text)";
            }
        }
    }

    結果:

    ====================================
    CreateProcedureStatement
    ====================================

    作成:CREATE
    WhiteSpace:
    プロシージャ:PROCEDURE
    WhiteSpace:
    識別子:​​dbo
    ドット:。
    識別子:​​procedure1
    WhiteSpace:
    WhiteSpace:
    変数:@ param1
    WhiteSpace:
    As:AS
    WhiteSpace:
    識別子:​​int
    WhiteSpace:
    As :AS
    WhiteSpace:
    Print:PRINT
    WhiteSpace:
    Integer:1
    Semicolon:;
    WhiteSpace:
    Go:GO
    EndOfFile:

    ノイズの一部を取り除くために、最後のforループ内のいくつかのTokenTypeを除外できます:

          foreach ($token in $statement.ScriptTokenStream)
          {
             if ($token.TokenType -notin "WhiteSpace", "Go", "EndOfFile", "SemiColon")
             {
               Write-Host "  $($token.TokenType.ToString().PadRight(16)) : $($token.Text)";
             }
          }

    最終的には、より簡潔な一連のトークンになります:

    ====================================
    CreateProcedureStatement
    ====================================

    作成:CREATE
    プロシージャ:PROCEDURE
    識別子:​​dbo
    ドット:。
    識別子:​​procedure1
    変数:@ param1
    As:AS
    識別子:int
    As:AS
    Print:PRINT
    Integer:1

    これを視覚的に手順にマッピングする方法:

    この単純なプロシージャ本体から解析された各トークン。

    パラメータ名やデータ型を再構築したり、パラメータリストの最後を見つけたりする際に発生する問題をすでに確認できます。これをもう少し調べた後、解析されたT-SQLのブロックのフラグメントを識別するTSqlFragmentVisitorと呼ばれるScriptDomクラスを強調したDanGuzmanによる投稿に出くわしました。戦術を少し変更すれば、フラグメントを調べることができます。 トークンの代わりに 。フラグメントは、基本的に1つ以上のトークンのセットであり、独自のタイプ階層もあります。私の知る限り、ScriptFragmentStreamはありません。 フラグメントを反復処理しますが、ビジターを使用できます 本質的に同じことをするパターン。 Test2.ps1という名前の新しいファイルを作成し、このコードを貼り付けて実行しましょう:

    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 = @"
    CREATE PROCEDURE dbo.procedure1
      @param1 AS int
    AS PRINT 1;
    GO
    "@
     
    $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)
       {
           Write-Host $fragment.GetType().Name;
       }
    }

    結果(この演習で興味深いもの太字 ):

    TSqlScript
    TSqlBatch
    CreateProcedureStatement
    ProcedureReference
    SchemaObjectName
    識別子
    識別子
    ProcedureParameter
    識別子
    SqlDataTypeReference
    SchemaObjectName
    識別子
    StatementList
    PrintStatement
    IntegerLiteral

    これを前の図に視覚的にマッピングしようとすると、もう少し複雑になります。これらの各フラグメントは、それ自体が1つ以上のトークンのストリームであり、オーバーラップする場合があります。 CREATEのように、いくつかのステートメントトークンとキーワードは、フラグメントの一部としてそれ自体では認識されません。 、PROCEDUREAS 、およびGO 。後者はT-SQLでさえないため理解できますが、パーサーはバッチを分離することを理解する必要があります。

    ステートメントトークンとフラグメントトークンの認識方法の比較。

    コード内のフラグメントを再構築するために、そのフラグメントへのアクセス中にトークンを反復処理できます。これにより、オブジェクトの名前やパラメータフラグメントなどを、面倒な解析や条件付きで導出できますが、各フラグメントのトークンストリーム内でループする必要があります。 Write-Host $fragment.GetType().Name;を変更した場合 これに対する前のスクリプトで:

    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.TSqlFragment] $fragment)
    {
      if ($fragment.GetType().Name -in ("ProcedureParameter", "ProcedureReference"))
      {
        $output = "";
        Write-Host "==========================";
        Write-Host "  $($fragment.GetType().Name)";
        Write-Host "==========================";
     
        for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
        {
          $token = $fragment.ScriptTokenStream[$i];
          $output += $token.Text;
        }
        Write-Host $output;
      }
    }

    出力は次のとおりです。

    ==========================
    ProcedureReference
    ==========================

    dbo.procedure1

    ==========================
    ProcedureParameter
    ==========================

    @ param1 AS int

    追加の反復や連結を実行することなく、オブジェクトとスキーマ名を一緒に取得できます。また、パラメータ名、データ型、存在する可能性のあるデフォルト値など、パラメータ宣言に関係する行全体があります。興味深いことに、訪問者は@param1 intを処理します およびint 2つの異なるフラグメントとして、基本的にデータ型を二重にカウントします。前者はProcedureParameter フラグメントであり、後者はSchemaObjectName 。私たちは本当に最初だけを気にします SchemaObjectName 参照(dbo.procedure1 )または、より具体的には、ProcedureReferenceに続くもののみ 。今日はすべてではなく、それらに対処することを約束します。 $procedureを変更した場合 これに定数(コメントとデフォルト値を追加):

    $procedure = @"
    CREATE PROCEDURE dbo.procedure1
      @param1 AS int = /* comment */ -64
    AS PRINT 1;
    GO
    "@

    次に、出力は次のようになります。

    ==========================
    ProcedureReference
    ==========================

    dbo.procedure1

    ==========================
    ProcedureParameter
    ==========================

    @ param1 AS int =/*コメント*/-64

    これには、実際にはコメントであるトークンが出力に含まれます。 forループ内で、これに対処するために無視したいトークンタイプを除外できます(余分なASも削除します) この例ではキーワードですが、モジュール本体を再構築する場合は、これを実行したくない場合があります):

    for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
    {
      $token = $fragment.ScriptTokenStream[$i];
      if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As"))
      {
        $output += $token.Text;
      }
    }

    出力はよりクリーンですが、それでも完全ではありません。

    ==========================
    ProcedureReference
    ==========================

    dbo.procedure1

    ==========================
    ProcedureParameter
    ==========================

    @ param1 int =-64

    パラメータ名、データ型、デフォルト値を分離したい場合は、より複雑になります。任意のフラグメントのトークンストリームをループしている間、EqualsSignに到達したときに追跡するだけで、任意のデータ型宣言からパラメータ名を分割できます。 トークン。 forループを次の追加ロジックに置き換えます:

    if ($fragment.GetType().Name -in ("ProcedureParameter","SchemaObjectName"))
    {
        $output  = "";
        $param   = ""; 
        $type    = "";
        $default = "";
        $seenEquals = $false;
     
          for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++)
          {
            $token = $fragment.ScriptTokenStream[$i];
            if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As"))
            {
              if ($fragment.GetType().Name -eq "ProcedureParameter")
              {
                if (!$seenEquals)
                {
                  if ($token.TokenType -eq "EqualsSign") 
                  { 
                    $seenEquals = $true; 
                  }
                  else 
                  { 
                    if ($token.TokenType -eq "Variable") 
                    {
                      $param += $token.Text; 
                    }
                    else 
                    {
                      $type += $token.Text; 
                    }
                  }
                }
                else
                { 
                  if ($token.TokenType -ne "EqualsSign")
                  {
                    $default += $token.Text; 
                  }
                }
              }
              else 
              {
                $output += $token.Text.Trim(); 
              }
            }
          }
     
          if ($param.Length   -gt 0) { $output  = "Param name: "   + $param.Trim(); }
          if ($type.Length    -gt 0) { $type    = "`nParam type: " + $type.Trim(); }
          if ($default.Length -gt 0) { $default = "`nDefault:    " + $default.TrimStart(); }
          Write-Host $output $type $default;
    }

    これで、出力は次のようになります。

    ==========================
    ProcedureReference
    ==========================

    dbo.procedure1

    ==========================
    ProcedureParameter
    ==========================

    パラメータ名:@ param1
    パラメータタイプ:int
    デフォルト:-64

    それは良いことですが、まだ解決すべきことがあります。 OUTPUTのように、これまで無視してきたパラメータキーワードがあります。 およびREADONLY 、および入力が複数のプロシージャを含むバッチである場合は、ロジックが必要です。これらの問題はパート2で扱います。

    それまでの間、実験してください。 ScriptDOM、TSqlParser、TSqlFragmentVisitorを使用して実行できる強力な機能は他にもたくさんあります。

    [パート1|パート2|パート3]


    1. テーブル式の基礎、パート11 –ビュー、変更に関する考慮事項

    2. データベースとテーブルスペース、違いは何ですか?

    3. Oracleは外部キーを取得します

    4. SQLServerの1つの列に基づいて複数の列をピボットする