[ Del 1 | Del 2 | Del 3 ]
Hvis du nogensinde har forsøgt at bestemme standardværdierne for lagrede procedureparametre, har du sandsynligvis mærker på din pande fra at slå den på dit skrivebord gentagne gange og voldsomt. De fleste artikler, der taler om at hente parameteroplysninger (som dette tip), nævner ikke engang ordet standard. Dette er fordi, bortset fra den rå tekst, der er gemt i objektets definition, er informationen ikke nogen steder i katalogvisningerne. Der er kolonner has_default_value
og default_value
i sys.parameters
det look lovende, men de er kun udfyldt til CLR-moduler.
At udlede standardværdier ved hjælp af T-SQL er besværligt og udsat for fejl. Jeg besvarede for nylig et spørgsmål på Stack Overflow om dette problem, og det tog mig ned af hukommelsesbanen. Tilbage i 2006 klagede jeg via flere Connect-elementer over manglen på synlighed af standardværdierne for parametre i katalogvisningerne. Problemet eksisterer dog stadig i SQL Server 2019. (Her er det eneste element, jeg har fundet, der kom til det nye feedbacksystem.)
Selvom det er en ulempe, at standardværdierne ikke er eksponeret i metadataene, er de højst sandsynligt ikke der, fordi det er svært at parse dem ud af objektteksten (på ethvert sprog, men især i T-SQL). Det er svært overhovedet at finde starten og slutningen af parameterlisten, fordi T-SQL's parsing-kapacitet er så begrænset, og der er flere edge cases, end du kan forestille dig. Et par eksempler:
- Du kan ikke stole på tilstedeværelsen af
(
og)
for at angive parameterlisten, da de er valgfrie (og kan findes i hele parameterlisten) - Du kan ikke let parse for den første
AS
at markere begyndelsen af kroppen, da den kan dukke op af andre årsager - Du kan ikke stole på tilstedeværelsen af
BEGIN
at markere begyndelsen af kroppen, da det er valgfrit - Det er svært at opdele på kommaer, da de kan optræde i kommentarer, inden for strenge bogstaver og som en del af datatypeerklæringer (tænk
(precision, scale)
) - Det er meget svært at parse begge typer kommentarer væk, som kan optræde hvor som helst (inklusive inde i strenge bogstaver) og kan indlejres
- Du kan ved et uheld finde vigtige søgeord, kommaer og lighedstegn inde i strenge bogstaver og kommentarer
- Du kan have standardværdier, der ikke er tal eller strengliteraler (tænk
{fn curdate()}
ellerGETDATE
)
Der er så mange små syntaksvariationer, at normale strengparsingteknikker bliver ineffektive. Har jeg set AS
allerede? Var det mellem et parameternavn og en datatype? Var det efter en højre parentes, der omgiver hele parameterlisten, eller [en?], der ikke havde et match før sidste gang, jeg så en parameter? Er det komma, der adskiller to parametre, eller er det en del af præcision og skala? Når du går gennem en streng et ord ad gangen, bliver det ved og ved, og der er så mange bits, du skal spore.
Tag dette (med vilje latterlige, men stadig syntaktisk gyldige) eksempel:
/* 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;
Det er svært at analysere standardværdierne ud af denne definition ved hjælp af T-SQL. Virkelig svært . Uden BEGIN
for korrekt at markere slutningen af parameterlisten, al kommentarroden og alle de tilfælde, hvor nøgleord som AS
kan betyde forskellige ting, vil du sandsynligvis have et komplekst sæt af indlejrede udtryk, der involverer mere SUBSTRING
og CHARINDEX
mønstre, end du nogensinde har set ét sted før. Og du ender sandsynligvis stadig med @d
og @e
ligner procedureparametre i stedet for lokale variabler.
Da jeg tænkte lidt mere over problemet og søgte for at se, om nogen havde formået noget nyt i det sidste årti, stødte jeg på dette fantastiske indlæg af Michael Swart. I det indlæg bruger Michael ScriptDom's TSqlParser til at fjerne både enkeltlinje- og flerlinjekommentarer fra en blok af T-SQL. Så jeg skrev noget PowerShell-kode for at gå gennem en procedure for at se, hvilke andre tokens der blev identificeret. Lad os tage et enklere eksempel uden alle de tilsigtede problemer:
CREATE PROCEDURE dbo.procedure1 @param1 int AS PRINT 1; GO
Åbn Visual Studio Code (eller din foretrukne PowerShell IDE), og gem en ny fil kaldet Test1.ps1. Den eneste forudsætning er at have den seneste version af Microsoft.SqlServer.TransactSql.ScriptDom.dll (som du kan downloade og udtrække fra sqlpackage her) i samme mappe som .ps1-filen. Kopier denne kode, gem, og kør eller fejlfind derefter:
# 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)"; } } }
Resultaterne:
=====================================CreateProcedureStatement
=====================================
Opret :CREATE
WhiteSpace :
Procedure :PROCEDURE
WhiteSpace :
Identifier :dbo
Punktum :.
Identifier :procedure1
WhiteSpace :
WhiteSpace :
Variabel :@param1
WhiteSpace :
Som :AS
WhiteSpace :
Identifikator :int
WhiteSpace :
As :AS
WhiteSpace :
Udskriv :PRINT
WhiteSpace :
Heltal :1
Semikolon :;
WhiteSpace :
Go :GO
EndOfFile :
For at slippe af med noget af støjen kan vi filtrere nogle få TokenTypes fra i den sidste for-løkke:
foreach ($token in $statement.ScriptTokenStream) { if ($token.TokenType -notin "WhiteSpace", "Go", "EndOfFile", "SemiColon") { Write-Host " $($token.TokenType.ToString().PadRight(16)) : $($token.Text)"; } }
Ender med en mere kortfattet serie af tokens:
=====================================CreateProcedureStatement
=====================================
Opret :OPRET
Procedure :PROCEDURE
Identifikator :dbo
Punktum :.
Identifikator :procedure1
Variabel :@param1
Som :AS
Identifikator :int
As :AS
Udskriv :PRINT
Heltal :1
Måden, hvorpå dette knytter sig til en procedure visuelt:
Hvert token er parset fra denne enkle proceduretekst.
Du kan allerede se de problemer, vi har med at prøve at rekonstruere parameternavne, datatyper og endda finde slutningen af parameterlisten. Efter at have set nærmere på dette, stødte jeg på et indlæg af Dan Guzman, der fremhævede en ScriptDom-klasse kaldet TSqlFragmentVisitor, som identificerer fragmenter af en blok af parset T-SQL. Hvis vi ændrer taktikken en smule, kan vi inspicere fragmenter i stedet for tokens . Et fragment er i det væsentlige et sæt af et eller flere tokens og har også sit eget typehierarki. Så vidt jeg ved, er der ingen ScriptFragmentStream
at iterere gennem fragmenter, men vi kan bruge en Visitor mønster til at gøre stort set det samme. Lad os oprette en ny fil kaldet Test2.ps1, indsætte denne kode og køre den:
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; } }
Resultater (interessante for denne øvelsei fed skrift ):
TSqlScriptTSqlBatch
CreateProcedureStatement
ProcedureReference
SchemaObjectName
Identifikator
Identifikator
ProcedureParameter
Identifier
SqlDataTypeReference
SchemaObjectName
Identifier
StatementList
PrintStatement
IntegerLiteral
Hvis vi forsøger at kortlægge dette visuelt til vores tidligere diagram, bliver det lidt mere komplekst. Hvert af disse fragmenter er i sig selv en strøm af et eller flere tokens, og nogle gange vil de overlappe hinanden. Adskillige erklæringstokens og nøgleord genkendes ikke engang alene som en del af et fragment, som f.eks. CREATE
, PROCEDURE
, AS
og GO
. Sidstnævnte er forståeligt, da det slet ikke er T-SQL, men parseren skal stadig forstå, at den adskiller batches.
Sammenligning af den måde, sætningstokens og fragmenttokens genkendes på.
For at genopbygge ethvert fragment i kode, kan vi iterere gennem dets tokens under et besøg på det fragment. Dette lader os udlede ting som navnet på objektet og parameterfragmenterne med meget mindre kedelige parsing og betingelser, selvom vi stadig er nødt til at sløjfe inde i hvert fragments token-strøm. Hvis vi ændrer Write-Host $fragment.GetType().Name;
i det forrige script til dette:
[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; } }
Outputtet er:
===========================Procedurehenvisning
==========================
dbo.procedure1
===========================
ProcedureParameter
==========================
@param1 AS int
Vi har objektet og skemanavnet sammen uden at skulle udføre yderligere iteration eller sammenkædning. Og vi har hele linjen involveret i enhver parametererklæring, inklusive parameternavnet, datatypen og enhver standardværdi, der måtte eksistere. Interessant nok håndterer den besøgende @param1 int
og int
som to adskilte fragmenter, der i det væsentlige dobbelttæller datatypen. Førstnævnte er en ProcedureParameter
fragment, og sidstnævnte er et SchemaObjectName
. Vi bekymrer os egentlig kun om den første SchemaObjectName
reference (dbo.procedure1
) eller mere specifikt kun den, der følger ProcedureReference
. Jeg lover, at vi vil håndtere dem, bare ikke dem alle i dag. Hvis vi ændrer $procedure
konstant til dette (tilføje en kommentar og en standardværdi):
$procedure = @" CREATE PROCEDURE dbo.procedure1 @param1 AS int = /* comment */ -64 AS PRINT 1; GO "@
Så bliver outputtet:
===========================Procedurehenvisning
==========================
dbo.procedure1
===========================
ProcedureParameter
==========================
@param1 AS int =/* kommentar */ -64
Dette inkluderer stadig alle tokens i outputtet, der faktisk er kommentarer. Inde i for-løkken kan vi bortfiltrere alle token-typer, vi ønsker at ignorere for at løse dette (jeg fjerner også overflødig AS
søgeord i dette eksempel, men du ønsker måske ikke at gøre det, hvis du rekonstruerer modulkroppe):
for ($i = $fragment.FirstTokenIndex; $i -le $fragment.LastTokenIndex; $i++) { $token = $fragment.ScriptTokenStream[$i]; if ($token.TokenType -notin ("MultiLineComment", "SingleLineComment", "As")) { $output += $token.Text; } }
Outputtet er renere, men stadig ikke perfekt.
===========================Procedurehenvisning
==========================
dbo.procedure1
===========================
ProcedureParameter
==========================
@param1 int =-64
Hvis vi vil adskille parameternavnet, datatypen og standardværdien, bliver det mere komplekst. Mens vi går gennem tokenstrømmen for et givet fragment, kan vi opdele parameternavnet fra alle datatypedeklarationer ved blot at spore, hvornår vi rammer et EqualsSign
polet. Udskiftning af for-løkken med denne ekstra logik:
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 er outputtet:
===========================Procedurehenvisning
==========================
dbo.procedure1
===========================
Procedureparameter
==========================
Param navn:@param1
Param type:int
Standard:-64
Det er bedre, men der er stadig mere at løse. Der er parameternøgleord, jeg har ignoreret indtil videre, såsom OUTPUT
og READONLY
, og vi har brug for logik, når vores input er en batch med mere end én procedure. Jeg vil behandle disse problemer i del 2.
I mellemtiden, eksperimenter! Der er en masse andre kraftfulde ting, du kan gøre med ScriptDOM, TSqlParser og TSqlFragmentVisitor.
[ Del 1 | Del 2 | Del 3 ]