Database
 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 вече? Беше ли между име на параметър и тип данни? Дали беше след дясна скоба, която заобикаля целия списък с параметри, или [един?], който нямаше съвпадение преди последния път, когато видях параметър? Тази запетая разделя ли два параметъра или е част от точността и мащаба? Когато прелиствате низ по една дума, това продължава и продължава и има толкова много битове, които трябва да проследите.

Вземете този (умишлено смешен, но все пак синтактично валиден) пример:

/* 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 модели, отколкото сте виждали на едно място преди. И вероятно все още ще се окажете с @d и @e изглежда като параметри на процедурата вместо локални променливи.

Като си помислих още малко за проблема и търсех дали някой е успял нещо ново през последното десетилетие, попаднах на тази страхотна публикация от Майкъл Суорт. В тази публикация Майкъл използва TSqlParser на ScriptDom, за да премахне както едноредовите, така и многоредовите коментари от блок от T-SQL. Така че написах код на PowerShell, за да премина през процедура, за да видя кои други токени са идентифицирани. Нека вземем по-прост пример без всички умишлени проблеми:

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

Отворете Visual Studio Code (или любимата ви IDE PowerShell) и запазете нов файл, наречен 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 :
Бяло пространство :
Променлива :@param1
Бяло пространство :
Като :AS
Бяло пространство :
Идентификатор:int
Бяло пространство :
Като :AS
Бяло пространство :
Печат :PRINT
Бяло пространство :
Цело число :1
Точка и запетая :;
Бяло пространство :
Отиди :GO
Край на файл :

За да се отървем от част от шума, можем да филтрираме няколко TokenType в последния for цикъл:

      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
Цело число:1

Начинът, по който това се съпоставя визуално с процедура:

Всеки маркер е анализиран от тази проста процедура.

Вече можете да видите проблемите, които ще имаме, опитвайки се да реконструираме имена на параметри, типове данни и дори да намерим края на списъка с параметри. След като разгледах това още малко, попаднах на публикация от Дан Гузман, която подчерта клас ScriptDom, наречен TSqlFragmentVisitor, който идентифицира фрагменти от блок от анализиран T-SQL. Ако променим малко тактиката, можем да инспектираме фрагменти вместо жетони . Фрагментът е по същество набор от един или повече токени и също има своя собствена йерархия на типове. Доколкото знам, няма 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

Ако се опитаме да съпоставим това визуално с предишната ни диаграма, става малко по-сложно. Всеки от тези фрагменти сам по себе си е поток от един или повече символи и понякога те ще се припокриват. Няколко токена и ключови думи дори не се разпознават сами като част от фрагмент, като CREATE , PROCEDURE , AS и 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;
  }
}

Резултатът е:

==========================
Справка за процедурата
===========================

dbo.procedure1

==========================
Параметър на процедура
==========================

@param1 AS int

Имаме името на обекта и схемата заедно, без да се налага да извършваме допълнителна итерация или конкатенация. И имаме целия ред, включен във всяка декларация на параметър, включително името на параметъра, типа данни и всяка стойност по подразбиране, която може да съществува. Интересното е, че посетителят обработва @param1 int и int като два отделни фрагмента, по същество двойно отчитащи типа данни. Първият е ProcedureParameter фрагмент, а последният е SchemaObjectName . Ние наистина се интересуваме само от първото SchemaObjectName справка (dbo.procedure1 ) или по-конкретно само този, който следва ProcedureReference . Обещавам, че ще се справим с тях, но не всички днес. Ако променим $procedure константа към това (добавяне на коментар и стойност по подразбиране):

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

Тогава изходът става:

==========================
Справка за процедурата
===========================

dbo.procedure1

==========================
Параметър на процедура
==========================

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

Резултатът е по-чист, но все още не е перфектен.

==========================
Справка за процедурата
===========================

dbo.procedure1

==========================
Параметър на процедура
==========================

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

Сега изходът е:

==========================
Справка за процедурата
===========================

dbo.procedure1

==========================
Параметър на процедура
==========================

Име на параметър:@param1
Тип на параметър:int
По подразбиране:-64

Така е по-добре, но има още за решаване. Има ключови думи с параметри, които съм игнорирал досега, като OUTPUT и READONLY , и имаме нужда от логика, когато нашият вход е партида с повече от една процедура. Ще се занимавам с тези проблеми в част 2.

Междувременно експериментирайте! Има много други мощни неща, които можете да правите със ScriptDOM, TSqlParser и TSqlFragmentVisitor.

[ Част 1 | Част 2 | Част 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. Работа с API на JavaFX Chart

  2. Проектиране на база данни за онлайн портал за работа

  3. Преминете към стартиране на разработване на база данни, управлявано от тестове (TDDD)

  4. Как да създадете таблица от SQL заявка

  5. Как да намерите максимални стойности в редове