Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Parsing nilai default parameter menggunakan PowerShell – Bagian 1

[ Bagian 1 | Bagian 2 | Bagian 3 ]

Jika Anda pernah mencoba untuk menentukan nilai default untuk parameter prosedur tersimpan, Anda mungkin memiliki tanda di dahi Anda karena memukulnya di meja Anda berulang kali dan keras. Sebagian besar artikel yang berbicara tentang mengambil informasi parameter (seperti tip ini) bahkan tidak menyebutkan kata default. Ini karena, kecuali teks mentah yang disimpan dalam definisi objek, informasinya tidak ada di tampilan katalog. Ada kolom has_default_value dan default_value di sys.parameters tampilan itu menjanjikan, tetapi hanya diisi untuk modul CLR.

Mendapatkan nilai default menggunakan T-SQL tidak praktis dan rawan kesalahan. Saya baru-baru ini menjawab pertanyaan tentang Stack Overflow tentang masalah ini, dan itu membawa saya ke jalur memori. Kembali pada tahun 2006, saya mengeluh melalui beberapa item Connect tentang kurangnya visibilitas nilai default untuk parameter dalam tampilan katalog. Namun, masalahnya masih ada di SQL Server 2019. (Ini satu-satunya item yang saya temukan yang berhasil masuk ke sistem umpan balik baru.)

Meskipun merupakan ketidaknyamanan bahwa nilai default tidak diekspos dalam metadata, kemungkinan besar nilai tersebut tidak ada karena menguraikannya dari teks objek (dalam bahasa apa pun, tetapi khususnya dalam T-SQL) sulit dilakukan. Sulit bahkan untuk menemukan awal dan akhir daftar parameter karena kemampuan penguraian T-SQL sangat terbatas, dan ada lebih banyak kasus tepi daripada yang dapat Anda bayangkan. Beberapa contoh:

  • Anda tidak dapat mengandalkan keberadaan ( dan ) untuk menunjukkan daftar parameter, karena mereka opsional (dan dapat ditemukan di seluruh daftar parameter)
  • Anda tidak dapat dengan mudah menguraikan AS pertama untuk menandai awal dari tubuh, karena dapat muncul karena alasan lain
  • Anda tidak dapat mengandalkan kehadiran BEGIN untuk menandai awal tubuh, karena itu opsional
  • Sulit untuk memisahkan koma, karena koma dapat muncul di dalam komentar, dalam literal string, dan sebagai bagian dari deklarasi tipe data (pikirkan (precision, scale) )
  • Sangat sulit untuk mengurai kedua jenis komentar, yang dapat muncul di mana saja (termasuk literal string di dalam), dan dapat disarangkan
  • Anda dapat secara tidak sengaja menemukan kata kunci penting, koma, dan tanda sama dengan di dalam literal string dan komentar
  • Anda dapat memiliki nilai default yang bukan angka atau literal string (pikirkan {fn curdate()} atau GETDATE )

Ada begitu banyak variasi sintaksis kecil sehingga teknik penguraian string normal menjadi tidak efektif. Pernahkah saya melihat AS sudah? Apakah itu antara nama parameter dan tipe data? Apakah setelah tanda kurung kanan yang mengelilingi seluruh daftar parameter, atau [satu?] yang tidak memiliki kecocokan sebelum terakhir kali saya melihat parameter? Apakah koma yang memisahkan dua parameter atau itu bagian dari presisi dan skala? Saat Anda mengulang string satu kata pada satu waktu, itu terus berlanjut, dan ada begitu banyak bit yang perlu Anda lacak.

Ambil contoh (sengaja konyol, tetapi masih valid secara sintaksis):

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

Mengurai nilai default dari definisi itu menggunakan T-SQL itu sulit. Sangat sulit . Tanpa BEGIN untuk menandai akhir daftar parameter dengan benar, semua kekacauan komentar, dan semua kasus di mana kata kunci seperti AS dapat berarti hal yang berbeda, Anda mungkin akan memiliki sekumpulan ekspresi kompleks yang melibatkan lebih banyak SUBSTRING dan CHARINDEX pola daripada yang pernah Anda lihat di satu tempat sebelumnya. Dan Anda mungkin masih akan berakhir dengan @d dan @e tampak seperti parameter prosedur alih-alih variabel lokal.

Memikirkan masalahnya lagi, dan mencari untuk melihat apakah ada orang yang mengelola sesuatu yang baru dalam dekade terakhir, saya menemukan posting hebat ini oleh Michael Swart. Dalam posting itu, Michael menggunakan TSqlParser ScriptDom untuk menghapus komentar satu baris dan multi-baris dari blok T-SQL. Jadi saya menulis beberapa kode PowerShell untuk melangkah melalui prosedur untuk melihat token lain yang diidentifikasi. Mari kita ambil contoh yang lebih sederhana tanpa semua masalah yang disengaja:

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

Buka Visual Studio Code (atau PowerShell IDE favorit Anda) dan simpan file baru bernama Test1.ps1. Satu-satunya prasyarat adalah memiliki versi terbaru Microsoft.SqlServer.TransactSql.ScriptDom.dll (yang dapat Anda unduh dan ekstrak dari sqlpackage di sini) di folder yang sama dengan file .ps1. Salin kode ini, simpan, lalu jalankan atau debug:

# 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)";
        }
    }
}

Hasilnya:

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

Create :CREATE
WhiteSpace :
Prosedur :PROSEDUR
WhiteSpace :
Identifier :dbo
Dot :.
Identifier :procedure1
WhiteSpace :
WhiteSpace :
Variabel :@param1
WhiteSpace :
As :AS
WhiteSpace :
Identifier :int
WhiteSpace :
As :AS
WhiteSpace :
Cetak :PRINT
WhiteSpace :
Integer :1
Titik koma :;
WhiteSpace :
Go :GO
EndOfFile :

Untuk menghilangkan beberapa noise, kita dapat memfilter beberapa TokenType di dalam for loop terakhir:

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

Berakhir dengan rangkaian token yang lebih ringkas:

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

Create :CREATE
Procedure :PROCEDURE
Identifier :dbo
Dot :.
Identifier :procedure1
Variabel :@param1
As :AS
Identifier :int
As :AS
Print :PRINT
Integer :1

Cara ini memetakan ke prosedur secara visual:

Setiap token diuraikan dari badan prosedur sederhana ini.

Anda sudah dapat melihat masalah yang akan kami coba untuk merekonstruksi nama parameter, tipe data, dan bahkan menemukan akhir daftar parameter. Setelah melihat ini lagi, saya menemukan posting oleh Dan Guzman yang menyoroti kelas ScriptDom yang disebut TSqlFragmentVisitor, yang mengidentifikasi fragmen dari blok T-SQL yang diurai. Jika kita mengubah taktik sedikit saja, kita dapat memeriksa fragmen bukannya token . Fragmen pada dasarnya adalah kumpulan satu atau lebih token, dan juga memiliki hierarki tipenya sendiri. Sejauh yang saya tahu, tidak ada ScriptFragmentStream untuk beralih melalui fragmen, tetapi kita dapat menggunakan Pengunjung pola untuk melakukan hal yang pada dasarnya sama. Mari kita buat file baru bernama Test2.ps1, rekatkan kode ini, dan jalankan:

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;
   }
}

Hasil (yang menarik untuk latihan ini dicetak tebal ):

TSqlScript
TSqlBatch
CreateProcedureStatement
ProcedureReference
SchemaObjectName
Identifier
Identifier
ProcedureParameter
Identifier
SqlDataTypeReference
SchemaObjectName
Identifier
StatementList
PrintStatement
IntegerLiteral

Jika kita mencoba memetakan ini secara visual ke diagram kita sebelumnya, ini akan menjadi sedikit lebih rumit. Masing-masing fragmen ini sendiri merupakan aliran dari satu atau lebih token, dan terkadang mereka akan tumpang tindih. Beberapa token pernyataan dan kata kunci bahkan tidak dikenali sendiri sebagai bagian dari sebuah fragmen, seperti CREATE , PROCEDURE , AS , dan GO . Yang terakhir ini dapat dimengerti karena bahkan bukan T-SQL sama sekali, tetapi pengurai masih harus memahami bahwa itu memisahkan kumpulan.

Membandingkan cara token pernyataan dan token fragmen dikenali.

Untuk membangun kembali setiap fragmen dalam kode, kita dapat mengulangi tokennya selama kunjungan ke fragmen itu. Ini memungkinkan kita memperoleh hal-hal seperti nama objek dan fragmen parameter dengan penguraian dan persyaratan yang jauh lebih mudah, meskipun kita masih harus mengulang di dalam aliran token setiap fragmen. Jika kita mengubah Write-Host $fragment.GetType().Name; dalam skrip sebelumnya untuk ini:

[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;
  }
}

Outputnya adalah:

==========================
Rujukan Prosedur
==========================

dbo.procedure1

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

@param1 AS int

Kami memiliki objek dan nama skema bersama-sama tanpa harus melakukan iterasi atau penggabungan tambahan. Dan kami memiliki seluruh baris yang terlibat dalam deklarasi parameter apa pun, termasuk nama parameter, tipe data, dan nilai default apa pun yang mungkin ada. Menariknya, pengunjung menangani @param1 int dan int sebagai dua fragmen yang berbeda, pada dasarnya menghitung dua kali tipe data. Yang pertama adalah ProcedureParameter fragmen, dan yang terakhir adalah SchemaObjectName . Kami benar-benar hanya peduli tentang pertama SchemaObjectName referensi (dbo.procedure1 ) atau, lebih khusus, hanya yang mengikuti ProcedureReference . Saya berjanji kita akan menanganinya, hanya saja tidak semuanya hari ini. Jika kita mengubah $procedure konstan untuk ini (menambahkan komentar dan nilai default):

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

Maka outputnya menjadi:

==========================
Rujukan Prosedur
==========================

dbo.procedure1

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

@param1 AS int =/* komentar */ -64

Ini masih menyertakan token apa pun dalam output yang sebenarnya berupa komentar. Di dalam for loop, kita dapat memfilter semua jenis token yang ingin kita abaikan untuk mengatasi hal ini (saya juga menghapus AS yang berlebihan kata kunci dalam contoh ini, tetapi Anda mungkin tidak ingin melakukannya jika Anda merekonstruksi badan modul):

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

Outputnya lebih bersih, tapi masih belum sempurna.

==========================
Rujukan Prosedur
==========================

dbo.procedure1

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

@param1 int =-64

Jika kita ingin memisahkan nama parameter, tipe data, dan nilai default, itu menjadi lebih kompleks. Saat kami mengulang aliran token untuk setiap fragmen tertentu, kami dapat memisahkan nama parameter dari deklarasi tipe data apa pun hanya dengan melacak saat kami menekan EqualsSign token. Mengganti perulangan for dengan logika tambahan ini:

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;
}

Sekarang outputnya adalah:

==========================
Rujukan Prosedur
==========================

dbo.procedure1

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

Nama param:@param1
Jenis param:int
Default:-64

Itu lebih baik, tetapi masih ada lagi yang harus dipecahkan. Ada kata kunci parameter yang sejauh ini saya abaikan, seperti OUTPUT dan READONLY , dan kita membutuhkan logika ketika input kita adalah kumpulan dengan lebih dari satu prosedur. Saya akan menangani masalah tersebut di bagian 2.

Sementara itu, bereksperimenlah! Ada banyak hal hebat lainnya yang dapat Anda lakukan dengan ScriptDOM, TSqlParser, dan TSqlFragmentVisitor.

[ Bagian 1 | Bagian 2 | Bagian 3 ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Menggunakan Langkah Unpivot untuk membuat Tabel Tabular dari Tabel Crosstab

  2. ScaleGrid Meluncurkan Dukungan Google Cloud Platform (GCP) untuk Hosting Database Terkelola

  3. Perbedaan Antara Pernyataan JDBC dan Pernyataan yang Disiapkan

  4. Kompleksitas NULL – Bagian 2

  5. Spool Indeks Eager dan Pengoptimal