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

Parsing nilai default parameter menggunakan PowerShell – Bagian 2

[ Bagian 1 | Bagian 2 | Bagian 3 ]

Dalam posting terakhir saya, saya menunjukkan cara menggunakan TSqlParser dan TSqlFragmentVisitor untuk mengekstrak informasi penting dari skrip T-SQL yang berisi definisi prosedur tersimpan. Dengan skrip itu, saya meninggalkan beberapa hal, seperti cara mengurai OUTPUT dan READONLY kata kunci untuk parameter, dan cara mengurai beberapa objek bersama-sama. Hari ini, saya ingin memberikan skrip yang menangani hal-hal tersebut, menyebutkan beberapa peningkatan lainnya di masa mendatang, dan membagikan repositori GitHub yang saya buat untuk pekerjaan ini.

Sebelumnya, saya menggunakan contoh sederhana seperti ini:

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

Dan dengan kode Pengunjung yang saya berikan, output ke konsol adalah:

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

dbo.procedure1


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

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

Sekarang, bagaimana jika skrip yang diteruskan terlihat lebih seperti ini? Ini menggabungkan definisi prosedur yang sengaja mengerikan dari sebelumnya dengan beberapa elemen lain yang mungkin Anda duga akan menyebabkan masalah, seperti nama tipe yang ditentukan pengguna, dua bentuk OUT yang berbeda /OUTPUT kata kunci, Unicode dalam nilai parameter (dan dalam nama parameter!), kata kunci sebagai konstanta, dan ODBC escape literal.

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

Skrip sebelumnya tidak cukup menangani banyak objek dengan benar, dan kita perlu menambahkan beberapa elemen logis untuk memperhitungkan OUTPUT dan READONLY . Khususnya, Output dan ReadOnly bukan jenis token, melainkan dikenali sebagai Identifier . Jadi kita memerlukan beberapa logika tambahan untuk menemukan pengidentifikasi dengan nama eksplisit tersebut dalam ProcedureParameter any pecahan. Anda mungkin melihat beberapa perubahan kecil lainnya:

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

Kode ini hanya untuk tujuan demonstrasi, dan tidak ada kemungkinan itu yang terbaru. Silakan lihat detail di bawah tentang mengunduh versi yang lebih baru.

Output dalam hal ini:

==========================
Rujukan Prosedur
==========================
dbo.some_procedure


Nama param:@a
Jenis param:int
Default:5


Nama param:@b
Jenis param:varchar(64)
Default:'AS =/* BEGIN @a, int =7 */ "blat"'


Nama param:@c
Jenis param:int
Default:6



==========================
Rujukan Prosedur
==========================
[dbo].another_procedure


Nama param:@p1
Jenis param:[int]
Default:1


Nama param:@p2
Jenis param:datetime
Default:getdate
Output:yes


Nama param:@p3
Jenis param:tanggal
Default:{ts '2020-02-01 13:12:49'}


Nama param:@p4
Jenis param:dbo.tabletype
Hanya Baca:yes


Nama param:@p5
Jenis param:geografi
Output:ya


Nama param:@p6
Jenis param:sysname
Default:N'学中'

Itu beberapa penguraian yang cukup kuat, meskipun ada beberapa kasus tepi yang membosankan dan banyak logika bersyarat. Saya ingin melihat TSqlFragmentVisitor diperluas sehingga beberapa jenis tokennya memiliki properti tambahan (seperti SchemaObjectName.IsFirstAppearance dan ProcedureParameter.DefaultValue ), dan lihat jenis token baru ditambahkan (seperti FunctionReference ). Tetapi bahkan sekarang, ini adalah tahun cahaya di luar pengurai brute force yang mungkin Anda tulis di apa pun bahasa, apalagi T-SQL.

Masih ada beberapa batasan yang belum saya atasi:

  • Ini hanya membahas prosedur tersimpan. Kode untuk menangani ketiga jenis fungsi yang ditentukan pengguna mirip , tetapi tidak ada FunctionReference yang praktis jenis fragmen, jadi sebagai gantinya Anda perlu mengidentifikasi SchemaObjectName pertama fragmen (atau set pertama Identifier dan Dot token) dan abaikan instance berikutnya. Saat ini kode dalam postingan ini akan kembalikan semua informasi tentang parameter ke suatu fungsi, tetapi tidak akan kembalikan nama fungsi tersebut . Jangan ragu untuk menggunakannya untuk singleton atau batch yang hanya berisi prosedur tersimpan, tetapi Anda mungkin menemukan output yang membingungkan untuk beberapa jenis objek campuran. Versi terbaru dalam repositori di bawah ini menangani fungsi dengan sangat baik.
  • Kode ini tidak menyimpan status. Mengeluarkan ke konsol dalam setiap Kunjungan itu mudah, tetapi mengumpulkan data dari beberapa kunjungan, untuk kemudian disalurkan ke tempat lain, sedikit lebih rumit, terutama karena cara kerja pola Pengunjung.
  • Kode di atas tidak dapat menerima masukan secara langsung. Untuk menyederhanakan demonstrasi di sini, ini hanya skrip mentah tempat Anda menempelkan blok T-SQL Anda sebagai konstanta. Tujuan akhirnya adalah untuk mendukung input dari file, array file, folder, array folder, atau menarik definisi modul dari database. Dan hasilnya bisa di mana saja:ke konsol, ke file, ke database… jadi batasnya di sana. Beberapa dari pekerjaan itu telah terjadi sementara itu, tetapi tidak satu pun dari itu telah ditulis dalam versi sederhana yang Anda lihat di atas.
  • Tidak ada penanganan kesalahan. Sekali lagi, untuk singkatnya dan kemudahan konsumsi, kode di sini tidak khawatir tentang penanganan pengecualian yang tak terelakkan, meskipun hal yang paling merusak yang dapat terjadi dalam bentuknya yang sekarang adalah bahwa batch tidak akan muncul di output jika tidak benar. diurai (seperti CREATE STUPID PROCEDURE dbo.whatever ). Ketika kita mulai menggunakan database dan/atau sistem file, penanganan kesalahan yang tepat akan menjadi jauh lebih penting.

Anda mungkin bertanya-tanya, di mana saya akan melanjutkan pekerjaan ini dan memperbaiki semua hal ini? Yah, saya telah meletakkannya di GitHub, untuk sementara saya menyebut proyek ini ParamParser , dan sudah memiliki kontributor yang membantu peningkatan. Versi kode saat ini sudah terlihat sangat berbeda dari contoh di atas, dan pada saat Anda membaca ini, beberapa batasan yang disebutkan di sini mungkin sudah diatasi. Saya hanya ingin menyimpan kode di satu tempat; tip ini lebih tentang menunjukkan contoh minimal tentang cara kerjanya, dan menyoroti bahwa ada proyek di luar sana yang didedikasikan untuk menyederhanakan tugas ini.

Di segmen berikutnya, saya akan berbicara lebih banyak tentang bagaimana teman dan kolega saya, Will White, membantu saya beralih dari skrip mandiri yang Anda lihat di atas ke modul yang jauh lebih hebat yang akan Anda temukan di GitHub.

Jika Anda perlu mengurai nilai default dari parameter untuk sementara, silakan unduh kodenya dan coba. Dan seperti yang saya sarankan sebelumnya, bereksperimenlah sendiri, karena ada banyak hal hebat lainnya yang dapat Anda lakukan dengan kelas-kelas ini dan pola Pengunjung.

[ 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. Paket Hosting di Chocolatey

  2. Tips untuk Desain Database yang Lebih Baik

  3. Berbagai Cara untuk Menghapus Duplikat dari Tabel SQL

  4. Cara Menginstal dan Mengonfigurasi Zabbix di Ubuntu 20.04

  5. Cara Mendapatkan Tanggal Saat Ini (Tanpa Waktu) di T-SQL