Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Phân tích cú pháp các giá trị mặc định của tham số bằng PowerShell - Phần 1

[Phần 1 | Phần 2 | Phần 3]

Nếu bạn đã từng cố gắng xác định các giá trị mặc định cho các tham số thủ tục được lưu trữ, bạn có thể có vết hằn trên trán do đập nó liên tục và thô bạo trên bàn làm việc. Hầu hết các bài báo nói về truy xuất thông tin tham số (như mẹo này) thậm chí không đề cập đến từ mặc định. Điều này là do, ngoại trừ văn bản thô được lưu trữ trong định nghĩa của đối tượng, thông tin không có ở bất kỳ đâu trong chế độ xem danh mục. Có các cột has_default_valuedefault_value trong sys.parameters cái nhìn đó đầy hứa hẹn, nhưng chúng chỉ được sử dụng cho các mô-đun CLR.

Việc lấy ra các giá trị mặc định bằng T-SQL rất phức tạp và dễ xảy ra lỗi. Gần đây, tôi đã trả lời một câu hỏi trên Stack Overflow về vấn đề này và nó đã khiến tôi mất bộ nhớ. Trở lại năm 2006, tôi đã phàn nàn qua nhiều mục Connect về việc thiếu khả năng hiển thị các giá trị mặc định cho các tham số trong chế độ xem danh mục. Tuy nhiên, sự cố vẫn tồn tại trong SQL Server 2019. (Đây là mục duy nhất tôi tìm thấy đã đưa nó vào hệ thống phản hồi mới.)

Mặc dù có một điều bất tiện là các giá trị mặc định không được hiển thị trong siêu dữ liệu, nhưng rất có thể chúng không có ở đó vì việc phân tích cú pháp chúng ra khỏi văn bản đối tượng (bằng bất kỳ ngôn ngữ nào, đặc biệt là trong T-SQL) là điều khó khăn. Thậm chí, rất khó để tìm thấy đầu và cuối của danh sách tham số vì khả năng phân tích cú pháp của T-SQL rất hạn chế và có nhiều trường hợp phức tạp hơn bạn có thể tưởng tượng. Một vài ví dụ:

  • Bạn không thể dựa vào sự hiện diện của () để chỉ ra danh sách tham số, vì chúng là tùy chọn (và có thể được tìm thấy trong danh sách tham số)
  • Bạn không thể dễ dàng phân tích cú pháp cho AS đầu tiên để đánh dấu phần bắt đầu của phần thân, vì nó có thể xuất hiện vì những lý do khác
  • Bạn không thể dựa vào sự hiện diện của BEGIN để đánh dấu phần đầu của phần nội dung, vì nó là tùy chọn
  • Rất khó để phân tách bằng dấu phẩy, vì chúng có thể xuất hiện bên trong nhận xét, trong chuỗi ký tự và như một phần của khai báo kiểu dữ liệu (suy nghĩ (precision, scale) )
  • Rất khó để phân tích cú pháp cả hai loại nhận xét, có thể xuất hiện ở bất kỳ đâu (bao gồm cả các ký tự bên trong chuỗi) và có thể được lồng vào nhau
  • Bạn có thể vô tình tìm thấy các từ khóa quan trọng, dấu phẩy và dấu bằng bên trong các ký tự và nhận xét của chuỗi
  • Bạn có thể có các giá trị mặc định không phải là số hoặc ký tự chuỗi (nghĩ rằng {fn curdate()} hoặc GETDATE )

Có rất nhiều biến thể cú pháp nhỏ khiến các kỹ thuật phân tích cú pháp chuỗi thông thường không hiệu quả. Tôi đã thấy AS chưa đã sẵn sàng? Nó có nằm giữa tên tham số và kiểu dữ liệu không? Có phải sau dấu ngoặc đơn bên phải bao quanh toàn bộ danh sách tham số hay [một?] Không khớp trước lần cuối cùng tôi nhìn thấy một tham số không? Đó là dấu phẩy phân tách hai tham số hay nó là một phần của độ chính xác và tỷ lệ? Khi bạn lặp lại từng từ một trong một chuỗi, chuỗi đó cứ lặp đi lặp lại và có rất nhiều bit bạn cần theo dõi.

Lấy ví dụ này (cố ý lố bịch, nhưng vẫn hợp lệ về mặt cú pháp):

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

Việc phân tích cú pháp các giá trị mặc định ra khỏi định nghĩa đó bằng T-SQL là một việc khó. Thực sự khó . Không có BEGIN để đánh dấu chính xác phần cuối của danh sách tham số, tất cả các bình luận lộn xộn và tất cả các trường hợp mà các từ khóa như AS có thể có nghĩa khác nhau, bạn có thể sẽ có một tập hợp phức tạp của các biểu thức lồng nhau liên quan đến nhiều SUBSTRINGCHARINDEX mà bạn chưa từng thấy ở một nơi trước đây. Và bạn có thể sẽ vẫn kết thúc với @d@e trông giống như các tham số thủ tục thay vì các biến cục bộ.

Suy nghĩ thêm về vấn đề và tìm kiếm xem liệu có ai đã quản lý điều gì mới trong thập kỷ qua, tôi bắt gặp bài đăng tuyệt vời này của Michael Swart. Trong bài đăng đó, Michael sử dụng TSqlParser của ScriptDom để xóa cả nhận xét một dòng và nhiều dòng khỏi một khối T-SQL. Vì vậy, tôi đã viết một số mã PowerShell để bước qua một quy trình để xem những mã thông báo nào khác đã được xác định. Hãy lấy một ví dụ đơn giản hơn mà không có tất cả các vấn đề cố ý:

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

Mở Visual Studio Code (hoặc IDE PowerShell yêu thích của bạn) và lưu tệp mới có tên Test1.ps1. Điều kiện tiên quyết duy nhất là có phiên bản mới nhất của Microsoft.SqlServer.TransactSql.ScriptDom.dll (bạn có thể tải xuống và giải nén từ sqlpackage tại đây) trong cùng thư mục với tệp .ps1. Sao chép mã này, lưu, sau đó chạy hoặc gỡ lỗi:

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

Kết quả:

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

Tạo:CREATE
WhiteSpace:
Thủ tục:PROCEDURE
WhiteSpace:
Định danh:dbo
Dấu chấm:.
Định danh:thủ tục1
WhiteSpace:
WhiteSpace:
Biến:@ param1
WhiteSpace:
As:AS
WhiteSpace:
Định danh:int
WhiteSpace:
As :AS
WhiteSpace:
Print:PRINT
WhiteSpace:
Integer:1
Semicolon:;
WhiteSpace:
Go:GO
/> EndOfFile:

Để loại bỏ một số nhiễu, chúng ta có thể lọc ra một số TokenTypes bên trong vòng lặp for cuối cùng:

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

Kết thúc bằng một loạt mã thông báo ngắn gọn hơn:

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

Tạo:CREATE
Thủ tục:PROCEDURE
Định danh:dbo
Dấu chấm:.
Định danh:procedure1
Biến:@ param1
As:AS
Định danh:int
As:AS
In:PRINT
Integer:1

Cách thức này ánh xạ đến một thủ tục một cách trực quan:

Mỗi mã thông báo được phân tích cú pháp từ phần thân thủ tục đơn giản này.

Bạn đã có thể thấy các vấn đề mà chúng tôi sẽ gặp phải khi cố gắng tạo lại tên thông số, kiểu dữ liệu và thậm chí tìm thấy phần cuối của danh sách thông số. Sau khi xem xét kỹ hơn về vấn đề này, tôi đã bắt gặp một bài đăng của Dan Guzman nêu bật một lớp ScriptDom được gọi là TSqlFragmentVisitor, xác định các đoạn của một khối T-SQL được phân tích cú pháp. Nếu chúng tôi chỉ thay đổi chiến thuật một chút, chúng tôi có thể kiểm tra mảnh vỡ thay vì mã thông báo . Một phân mảnh về cơ bản là một tập hợp của một hoặc nhiều mã thông báo và cũng có hệ thống phân cấp kiểu riêng của nó. Theo như tôi biết, không có ScriptFragmentStream để lặp lại qua các đoạn, nhưng chúng tôi có thể sử dụng Khách truy cập để làm điều tương tự về cơ bản. Hãy tạo một tệp mới có tên Test2.ps1, dán mã này vào và chạy nó:

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

Kết quả (những kết quả thú vị cho bài tập này in đậm ):

TSqlScript
TSqlBatch
CreateProcedureStatement
procedureReference Mã định danh
SchemaObjectName
Định danh

SqlDataTypeReference
SchemaObjectName
Định danh
StatementList
PrintStatement
IntegerLiteral

Nếu chúng tôi cố gắng ánh xạ trực quan sơ đồ này với sơ đồ trước đó của chúng tôi, nó sẽ phức tạp hơn một chút. Bản thân mỗi đoạn này là một dòng của một hoặc nhiều mã thông báo và đôi khi chúng sẽ chồng chéo lên nhau. Một số mã thông báo và từ khóa thậm chí không được nhận dạng riêng như một phần của phân đoạn, như CREATE , PROCEDURE , ASGO . Điều thứ hai có thể hiểu được vì nó thậm chí hoàn toàn không phải là T-SQL, nhưng trình phân tích cú pháp vẫn phải hiểu rằng nó phân tách các lô.

So sánh cách nhận dạng mã thông báo câu lệnh và mã thông báo phân đoạn.

Để xây dựng lại bất kỳ phân đoạn nào trong mã, chúng ta có thể lặp lại các mã thông báo của nó trong quá trình truy cập vào phân đoạn đó. Điều này cho phép chúng tôi lấy được những thứ như tên của đối tượng và các đoạn tham số với việc phân tích cú pháp và điều kiện ít tẻ nhạt hơn nhiều, mặc dù chúng tôi vẫn phải lặp lại bên trong luồng mã thông báo của mỗi đoạn. Nếu chúng ta thay đổi Write-Host $fragment.GetType().Name; trong tập lệnh trước cho cái này:

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

Đầu ra là:

==========================
Thủ tục Tham khảo
============================

dbo.procedure1

==========================
Thủ tục tham số
============================

@ param1 AS int

Chúng ta có đối tượng và tên lược đồ cùng nhau mà không cần phải thực hiện bất kỳ phép lặp hoặc nối bổ sung nào. Và chúng tôi có toàn bộ dòng liên quan đến bất kỳ khai báo tham số nào, bao gồm tên tham số, kiểu dữ liệu và bất kỳ giá trị mặc định nào có thể tồn tại. Điều thú vị là khách truy cập xử lý @param1 intint dưới dạng hai đoạn riêng biệt, về cơ bản là đếm kép kiểu dữ liệu. Cái trước là ProcedureParameter và cái sau là một SchemaObjectName . Chúng tôi thực sự chỉ quan tâm đến đầu tiên SchemaObjectName tham chiếu (dbo.procedure1 ) hoặc, cụ thể hơn, chỉ cái sau ProcedureReference . Tôi hứa chúng ta sẽ giải quyết những vấn đề đó, không phải tất cả chúng hôm nay. Nếu chúng tôi thay đổi $procedure hằng số này (thêm nhận xét và giá trị mặc định):

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

Sau đó, đầu ra trở thành:

==========================
Thủ tục Tham khảo
============================

dbo.procedure1

==========================
Thủ tục tham số
============================

@ param1 AS int =/ * bình luận * / -64

Điều này vẫn bao gồm bất kỳ mã thông báo nào trong đầu ra thực sự là nhận xét. Bên trong vòng lặp for, chúng tôi có thể lọc ra bất kỳ loại mã thông báo nào mà chúng tôi muốn bỏ qua để giải quyết vấn đề này (Tôi cũng loại bỏ AS thừa từ khóa trong ví dụ này, nhưng bạn có thể không muốn làm điều đó nếu bạn đang tạo lại các nội dung mô-đun):

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

Đầu ra sạch hơn, nhưng vẫn chưa hoàn hảo.

==========================
Thủ tục Tham khảo
============================

dbo.procedure1

==========================
Thủ tục tham số
============================

@ param1 int =-64

Nếu chúng ta muốn tách biệt tên tham số, kiểu dữ liệu và giá trị mặc định, nó sẽ trở nên phức tạp hơn. Trong khi chúng tôi đang lặp lại luồng mã thông báo cho bất kỳ phân đoạn nhất định nào, chúng tôi có thể tách tên tham số khỏi bất kỳ khai báo kiểu dữ liệu nào bằng cách chỉ theo dõi khi chúng tôi nhấn EqualsSign mã thông báo. Thay thế vòng lặp for bằng logic bổ sung này:

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

Bây giờ đầu ra là:

==========================
Thủ tục Tham khảo
============================

dbo.procedure1

==========================
Thủ tục tham số
============================

Tên tham số:@ param1
Loại tham số:int
Mặc định:-64

Điều đó tốt hơn, nhưng vẫn còn nhiều điều cần giải quyết. Có những từ khóa tham số mà tôi đã bỏ qua cho đến nay, như OUTPUTREADONLY và chúng ta cần logic khi đầu vào của chúng ta là một lô với nhiều hơn một thủ tục. Tôi sẽ giải quyết những vấn đề đó trong phần 2.

Trong khi chờ đợi, hãy thử nghiệm! Có rất nhiều thứ mạnh mẽ khác mà bạn có thể làm với ScriptDOM, TSqlParser và TSqlFragmentVisitor.

[Phần 1 | Phần 2 | Phần 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. Giảm thiểu tác động của việc mở rộng cột IDENTITY - phần 1

  2. 10 phương pháp hàng đầu để cải thiện hiệu suất ETL bằng SSIS

  3. Khớp Cung với Cầu - Giải pháp, Phần 2

  4. Cách loại bỏ các hàng trùng lặp trong SQL

  5. Ký hiệu UML