sql >> Database >  >> RDS >> Database

Standaardwaarden van parameters parseren met PowerShell - Deel 1

[ Deel 1 | Deel 2 | Deel 3 ]

Als u ooit hebt geprobeerd de standaardwaarden voor opgeslagen procedureparameters te bepalen, heeft u waarschijnlijk vlekken op uw voorhoofd door herhaaldelijk en met geweld op uw bureau te slaan. In de meeste artikelen die spreken over het ophalen van parameterinformatie (zoals deze tip) wordt het woord standaard niet eens genoemd. Dit komt omdat, behalve de onbewerkte tekst die is opgeslagen in de objectdefinitie, de informatie nergens in de catalogusweergaven staat. Er zijn kolommen has_default_value en default_value in sys.parameters die look veelbelovend, maar ze zijn alleen gevuld voor CLR-modules.

Het afleiden van standaardwaarden met T-SQL is omslachtig en foutgevoelig. Ik heb onlangs een vraag over Stack Overflow over dit probleem beantwoord en het kostte me geheugen. In 2006 klaagde ik via meerdere Connect-items over het gebrek aan zichtbaarheid van de standaardwaarden voor parameters in de catalogusweergaven. Het probleem bestaat echter nog steeds in SQL Server 2019. (Dit is het enige item dat ik heb gevonden dat het nieuwe feedbacksysteem heeft gehaald.)

Hoewel het vervelend is dat de standaardwaarden niet worden weergegeven in de metagegevens, zijn ze er hoogstwaarschijnlijk niet omdat het moeilijk is om ze uit de objecttekst te ontleden (in elke taal, maar vooral in T-SQL). Het is zelfs moeilijk om het begin en einde van de parameterlijst te vinden, omdat de ontledingscapaciteit van T-SQL zo beperkt is en er meer randgevallen zijn dan u zich kunt voorstellen. Een paar voorbeelden:

  • Je kunt niet vertrouwen op de aanwezigheid van ( en ) om de parameterlijst aan te geven, aangezien ze optioneel zijn (en overal in de parameterlijst te vinden zijn)
  • Je kunt de eerste AS niet gemakkelijk ontleden om het begin van het lichaam te markeren, omdat het om andere redenen kan verschijnen
  • Je kunt niet vertrouwen op de aanwezigheid van BEGIN om het begin van het lichaam te markeren, omdat het optioneel is
  • Het is moeilijk te splitsen op komma's, omdat ze kunnen verschijnen in opmerkingen, in letterlijke tekenreeksen en als onderdeel van gegevenstypedeclaraties (denk aan (precision, scale) )
  • Het is erg moeilijk om beide typen opmerkingen te ontleden, die overal kunnen verschijnen (inclusief binnen letterlijke tekenreeksen) en genest kunnen worden
  • Je kunt per ongeluk belangrijke trefwoorden, komma's en gelijktekens vinden in letterlijke tekenreeksen en opmerkingen
  • U kunt standaardwaarden hebben die geen getallen of letterlijke tekenreeksen zijn (denk aan {fn curdate()} of GETDATE )

Er zijn zoveel kleine syntaxisvariaties dat normale tekenreeks-ontledingstechnieken ondoeltreffend worden. Heb ik AS gezien? al? Was het tussen een parameternaam en een gegevenstype? Was het na een rechter haakje dat de hele parameterlijst omringt, of [één?] die geen overeenkomst had voor de laatste keer dat ik een parameter zag? Is die komma die twee parameters scheidt of maakt het deel uit van precisie en schaal? Wanneer je woord voor woord een string doorloopt, gaat het maar door en door, en er zijn zoveel bits die je moet volgen.

Neem dit (opzettelijk belachelijke, maar nog steeds syntactisch geldige) voorbeeld:

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

Het ontleden van de standaardwaarden uit die definitie met behulp van T-SQL is moeilijk. Heel moeilijk . Zonder BEGIN om het einde van de parameterlijst correct te markeren, alle opmerkingen en alle gevallen waarin trefwoorden zoals AS verschillende dingen kan betekenen, zult u waarschijnlijk een complexe set geneste uitdrukkingen hebben met meer SUBSTRING en CHARINDEX patronen dan je ooit eerder op één plek hebt gezien. En je zult waarschijnlijk nog steeds eindigen met @d en @e eruit zien als procedureparameters in plaats van lokale variabelen.

Toen ik nog wat over het probleem nadacht, en zoekend of iemand het afgelopen decennium iets nieuws had voorgeschoteld, kwam ik deze geweldige post van Michael Swart tegen. In die post gebruikt Michael de TSqlParser van het ScriptDom om zowel enkelregelige als meerregelige opmerkingen uit een blok T-SQL te verwijderen. Dus schreef ik wat PowerShell-code om een ​​procedure te doorlopen om te zien welke andere tokens werden geïdentificeerd. Laten we een eenvoudiger voorbeeld nemen zonder alle opzettelijke problemen:

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

Open Visual Studio Code (of uw favoriete PowerShell IDE) en sla een nieuw bestand op met de naam Test1.ps1. De enige vereiste is om de nieuwste versie van Microsoft.SqlServer.TransactSql.ScriptDom.dll (die u hier kunt downloaden en uitpakken uit sqlpackage) in dezelfde map als het .ps1-bestand te hebben. Kopieer deze code, sla hem op en voer hem uit of debug hem:

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

De resultaten:

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

Creëren:CREATE
WhiteSpace:
Procedure:PROCEDURE
WhiteSpace:
Identifier:dbo
Punt:.
Identifier:procedure1
WhiteSpace:
WhiteSpace:
Variabele:@param1
WhiteSpace:
As:AS
WhiteSpace:
Identifier:int
WhiteSpace:
As :AS
WhiteSpace :
Afdrukken :PRINT
WhiteSpace :
Geheel getal :1
Puntkomma :;
WhiteSpace :
Ga :GO
EndOfFile :

Om een ​​deel van de ruis te verwijderen, kunnen we een paar TokenTypes uit de last for-lus filteren:

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

Eindigend met een meer beknopte reeks tokens:

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

Maken:CREATE
Procedure:PROCEDURE
Identificatie:dbo
Punt:.
Identificatie:procedure1
Variabele:@param1
As:AS
Identifier:int
As:AS
Print:PRINT
Integer:1

De manier waarop dit visueel overeenkomt met een procedure:

Elk token is geparseerd uit deze eenvoudige procedure.

U kunt nu al de problemen zien die we zullen hebben bij het reconstrueren van parameternamen, gegevenstypen en zelfs bij het vinden van het einde van de parameterlijst. Na hier wat meer naar te hebben gekeken, kwam ik een bericht van Dan Guzman tegen waarin een ScriptDom-klasse met de naam TSqlFragmentVisitor werd gemarkeerd, die fragmenten van een blok geparseerde T-SQL identificeert. Als we de tactiek een beetje veranderen, kunnen we fragmenten inspecteren in plaats van tokens . Een fragment is in wezen een set van een of meer tokens en heeft ook zijn eigen typehiërarchie. Voor zover ik weet, is er geen ScriptFragmentStream om door fragmenten te bladeren, maar we kunnen een Bezoeker . gebruiken patroon om in wezen hetzelfde te doen. Laten we een nieuw bestand maken met de naam Test2.ps1, plak deze code in en voer het uit:

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

Resultaten (interessante voor deze oefening vetgedrukt ):

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

Als we proberen dit visueel in kaart te brengen in ons vorige diagram, wordt het een beetje ingewikkelder. Elk van deze fragmenten is zelf een stroom van een of meer tokens, en soms zullen ze elkaar overlappen. Verschillende instructietokens en trefwoorden worden op zichzelf niet eens herkend als onderdeel van een fragment, zoals CREATE , PROCEDURE , AS , en GO . Dat laatste is begrijpelijk omdat het helemaal geen T-SQL is, maar de parser moet nog begrijpen dat hij batches scheidt.

De manier waarop instructietokens en fragmenttokens worden herkend, vergelijken.

Om elk fragment in code opnieuw op te bouwen, kunnen we de tokens doorlopen tijdens een bezoek aan dat fragment. Hierdoor kunnen we dingen afleiden zoals de naam van het object en de parameterfragmenten met veel minder vervelende parsing en conditionals, hoewel we nog steeds binnen de tokenstroom van elk fragment moeten lopen. Als we Write-Host $fragment.GetType().Name; . wijzigen in het vorige script naar dit:

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

De uitvoer is:

==========================
ProcedureReferentie
==========================

dbo.procedure1

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

@param1 AS int

We hebben de object- en schemanaam bij elkaar zonder dat we extra iteratie of aaneenschakeling hoeven uit te voeren. En we hebben de hele regel betrokken bij elke parameterdeclaratie, inclusief de parameternaam, het gegevenstype en elke standaardwaarde die mogelijk bestaat. Interessant is dat de bezoeker @param1 int . afhandelt en int als twee verschillende fragmenten, waarbij het gegevenstype in wezen dubbel wordt geteld. De eerste is een ProcedureParameter fragment, en de laatste is een SchemaObjectName . We geven alleen om de eerste SchemaObjectName referentie (dbo.procedure1 ) of, specifieker, alleen degene die volgt op ProcedureReference . Ik beloof dat we die vandaag zullen behandelen, alleen niet allemaal. Als we de $procedure . wijzigen constante hieraan (toevoegen van een opmerking en een standaardwaarde):

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

Dan wordt de uitvoer:

==========================
ProcedureReferentie
==========================

dbo.procedure1

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

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

Dit omvat nog steeds alle tokens in de uitvoer die eigenlijk opmerkingen zijn. Binnen de for-lus kunnen we alle tokentypes die we willen negeren uitfilteren om dit aan te pakken (ik verwijder ook overbodige AS trefwoorden in dit voorbeeld, maar misschien wilt u dat niet doen als u modulelichamen reconstrueert):

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

De output is schoner, maar nog steeds niet perfect.

==========================
ProcedureReferentie
==========================

dbo.procedure1

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

@param1 int =-64

Als we de parameternaam, het gegevenstype en de standaardwaarde willen scheiden, wordt het complexer. Terwijl we de tokenstroom doorlopen voor een bepaald fragment, kunnen we de parameternaam splitsen van alle gegevenstypedeclaraties door gewoon te volgen wanneer we een EqualsSign raken teken. De for-lus vervangen door deze extra logica:

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

Nu is de uitvoer:

==========================
ProcedureReferentie
==========================

dbo.procedure1

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

Paramnaam:@param1
Paramtype:int
Standaard:-64

Dat is beter, maar er moet nog meer worden opgelost. Er zijn parameterzoekwoorden die ik tot nu toe heb genegeerd, zoals OUTPUT en READONLY , en we hebben logica nodig wanneer onze invoer een batch is met meer dan één procedure. Ik zal die problemen in deel 2 behandelen.

Experimenteer in de tussentijd! Er zijn veel andere krachtige dingen die u kunt doen met ScriptDOM, TSqlParser en TSqlFragmentVisitor.

[ Deel 1 | Deel 2 | Deel 3 ]


  1. PASS Summit 2013 :Een succes in Charlotte

  2. Hoe SQRT() werkt in MariaDB

  3. Hoe verander ik de standaardtaal voor SQL Server?

  4. De juiste manier om een ​​unieke beperking te implementeren die meerdere NULL-waarden in SQL Server toestaat