Database
 sql >> база данни >  >> RDS >> Database

Анализирайте стойностите по подразбиране на параметрите с помощта на PowerShell – част 2

[ Част 1 | Част 2 | Част 3 ]

В последната си публикация показах как се използва TSqlParser и TSqlFragmentVisitor за извличане на важна информация от T-SQL скрипт, съдържащ дефиниции на съхранени процедури. С този скрипт пропуснах няколко неща, като например как да анализирам OUTPUT и READONLY ключови думи за параметри и как да анализирате няколко обекта заедно. Днес исках да предоставя скрипт, който се справя с тези неща, да спомена няколко други бъдещи подобрения и да споделя GitHub хранилище, което създадох за тази работа.

Преди това използвах прост пример като този:

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

И с кода на посетителя, който предоставих, изходът към конзолата беше:

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

dbo.procedure1


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

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

Ами ако предаденият скрипт изглеждаше по-скоро така? Той съчетава преднамерено ужасната дефиниция на процедурата от преди с няколко други елемента, които може да очаквате да причинят проблеми, като имена на дефинирани от потребителя типове, две различни форми на OUT /OUTPUT ключова дума, Unicode в стойностите на параметрите (и в имената на параметри!), ключовите думи като константи и ODBC escape литерали.

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

Предишният скрипт не обработва правилно множество обекти и трябва да добавим няколко логически елемента, за да отчетем OUTPUT и READONLY . По-конкретно, Output и ReadOnly не са типове токени, а по-скоро се разпознават като Identifier . Така че имаме нужда от допълнителна логика, за да намерим идентификатори с тези изрични имена във всеки ProcedureParameter фрагмент. Може да забележите още няколко дребни промени:

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

Този код е само за демонстрационни цели и е нулев шанс да е най-актуалният. Моля, вижте подробности по-долу за изтеглянето на по-нова версия.

Резултатът в този случай:

==========================
Справка за процедурата
===========================
dbo.някаква_процедура


Име на параметър:@a
Тип на параметър:int
По подразбиране:5


Име на параметър:@b
Тип на параметър:varchar(64)
По подразбиране:'AS =/* BEGIN @a, int =7 */ "blat"'


Име на параметър:@c
Тип на параметър:int
По подразбиране:6



===========================
Справка за процедурата
==========================
[dbo].друга_процедура


Име на параметър:@p1
Тип на параметър:[int]
По подразбиране:1


Име на параметър:@p2
Тип на параметър:datetime
По подразбиране:getdate
Изход:да


Име на параметър:@p3
Тип на параметър:дата
По подразбиране:{ts '2020-02-01 13:12:49'}


Име на параметър:@p4
Тип на параметър:dbo.tabletype
Само за четене:да


Име на параметър:@p5
Тип на параметър:география
Изход:да


Име на параметър:@p6
Тип на параметър:sysname
По подразбиране:N'学中'

Това е доста мощен анализ, въпреки че има някои досадни крайни случаи и много условна логика. Бих искал да видя TSqlFragmentVisitor разширено, така че някои от неговите типове токени имат допълнителни свойства (като SchemaObjectName.IsFirstAppearance и ProcedureParameter.DefaultValue ) и вижте добавени нови типове токени (като FunctionReference ). Но дори и сега това са светлинни години отвъд анализатора на груба сила, който може да напишете във всяко език, няма значение T-SQL.

Все още обаче има няколко ограничения, които все още не съм разгледал:

  • Това се отнася само за съхранените процедури. Кодът за обработка на трите типа дефинирани от потребителя функции е подобен , но няма удобен FunctionReference тип фрагмент, така че вместо това трябва да идентифицирате първия SchemaObjectName фрагмент (или първият набор от Identifier и Dot токени) и игнорирайте всички последващи екземпляри. В момента кодът в тази публикация ще връща цялата информация за параметрите към функция, но няма върнете име на функцията . Чувствайте се свободни да го използвате за сингълтон или партиди, съдържащи само съхранени процедури, но може да откриете, че изходът е объркващ за множество смесени типове обекти. Най-новата версия в хранилището по-долу се справя перфектно с функциите.
  • Този код не запазва състоянието. Извеждането на конзолата в рамките на всяко посещение е лесно, но събирането на данни от множество посещения, за да се прехвърлят на друго място, е малко по-сложно, главно поради начина, по който работи моделът на посетителя.
  • Кодът по-горе не може да приема директно въвеждане. За да опростите демонстрацията тук, това е просто необработен скрипт, в който поставяте своя T-SQL блок като константа. Евентуалната цел е да се поддържа въвеждане от файл, масив от файлове, папка, масив от папки или изтегляне на дефиниции на модули от база данни. И изходът може да бъде навсякъде:към конзолата, към файл, към база данни... така че небето е границата там. Част от тази работа се случи междувременно, но нищо от това не е написано в простата версия, която виждате по-горе.
  • Няма обработка на грешки. Отново, за краткост и лекота на консумация, кодът тук не се тревожи за обработката на неизбежни изключения, въпреки че най-разрушителното нещо, което може да се случи в сегашния му вид, е, че партида няма да се появи в изхода, ако не може да бъде правилно анализиран (като CREATE STUPID PROCEDURE dbo.whatever ). Когато започнем да използваме бази данни и/или файлова система, правилното обработване на грешки ще стане много по-важно.

Можете да се чудите къде ще поддържам текущата работа по това и ще поправя всички тези неща? Е, сложих го в GitHub, нарекох условно проекта ParamParser , и вече има сътрудници, които помагат с подобренията. Текущата версия на кода вече изглежда доста по-различно от горната проба и докато прочетете това, някои от споменатите тук ограничения може вече да са адресирани. Искам само да поддържам кода на едно място; този съвет е по-скоро за показване на минимална извадка за това как може да работи и подчертаване, че има проект, посветен на опростяването на тази задача.

В следващия сегмент ще говоря повече за това как моят приятел и колега Уил Уайт ми помогна да премина от самостоятелния скрипт, който виждате по-горе, до много по-мощния модул, който ще намерите в GitHub.

Ако междувременно имате нужда да анализирате стойностите по подразбиране от параметри, не се колебайте да изтеглите кода и да го изпробвате. И както предложих преди, експериментирайте сами, защото има много други мощни неща, които можете да правите с тези класове и модела на посетителя.

[ Част 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. Как да се справяме с разделяне на нула в SQL

  2. Прагове за оптимизиране – групиране и агрегиране на данни, част 3

  3. Производителност на sys.partitions

  4. Инсталирайте и конфигурирайте софтуера XAMPP на Windows Server 2019

  5. Параметър Sniffing, Embedding и опциите RECOMPILE