-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathTest-SQLScripts.ps1
265 lines (221 loc) · 11.3 KB
/
Test-SQLScripts.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
<#
.SYNOPSIS
Scans a (list of) T-SQL script files(s) and returns information about the operations being performed by them.
.DESCRIPTION
This function utilizes the SQL Server ScriptDom Parser object to parse and return information about each batch and statements within each batch
of T-SQL commands they contain. It will return an object that contains high-level information, as well as a batches object which in turn contains
statement objects.
.PARAMETER Files
The [System.IO.FileInfo] object containing the files you want to scan. This type of object is usually returned from a Get-ChildItem cmdlet, so you can pipe the
results of it to this function. Required.
.PARAMETER PathToScriptDomLibrary
This function requires the use of the Microsoft.SqlServer.TransactSql.ScriptDom object, which is NOT part of the standard SQL Server client libraries. Instead,
it is installed as part of a SQL Server installation. Which means to use this function, you either have to run it on a host that has SQL Server installed, or you
need a copy of the library locally. If you're using the latter, you need to manually provide the path to the Microsoft.SqlServer.TransactSql.ScriptDom.DLL file.
This path will be used as part of Add-Type to load the library which contains all the required namespaces and object code. Defaults to empty.
.PARAMETER UseQuotedIdentifier
Whether or not the quoted identifier option is turned on for the parser. Defaults to true and is passed to the object instantiation.
.NOTES
Out-of-the-box this function will search for:
- DML statements (INSERT, UPDATE, DELETE)
- Certain DDL Statements:
- ALTER TABLE
- DROP INDEX
- CREATE INDEX
- CREATE PROCEDURE
- DROP PROCEDURE
You can extend the tests by adding a new [ParserKey] object to the $ParserKeys array. For now, these defined tests live in the code
but I expect them to be an external json file at some point.
Author: Drew Furgiuele (@pittfurg, port1433.com)
Tags: T-SQL, Parser
.LINK
.EXAMPLE
$Results = Get-ChildItem -Path C:\Scripts | ./Test-SQLScripts.ps1
Execute the parser against a list of files returned from the Get-ChildItem cmdlet and store the returned object in the $Results variable
.EXAMPLE
$Results = Get-ChildItem -Path C:\Scripts | ./Test-SQLScripts.ps1 -PathToScriptDomLibrary "C:\Program Files (x86)\Microsoft SQL Server\130\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll"
Same as above example, but manually point to where the parser library is stored (useful for hosts that don't have SQL Server installed and you manually
copied the library to it).
#>
[cmdletbinding()]
param(
[Parameter(
Mandatory = $true,
Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)
] [System.IO.FileInfo] $Files,
[Parameter(Mandatory=$false)] [string] $PathToScriptDomLibrary = $null,
[Parameter(Mandatory=$false)] [string] $UseQuotedIdentifier = $true
)
begin {
$ParserKeys = @()
Class ParserKey {
[string] $ObjectType
[string] $SchemaSpecification
ParserKey ([string] $ObjectType, [string] $SchemaSpecification) {
$this.ObjectType = $ObjectType
$this.SchemaSpecification = $SchemaSpecification
}
}
$ParserKeys += New-Object Parserkey ("SelectStatement","Queryexpression.Fromclause.Tablereferences.Schemaobject")
$ParserKeys += New-Object Parserkey ("InsertStatement","InsertSpecification.Target.SchemaObject")
$ParserKeys += New-Object Parserkey ("UpdateStatement","UpdateSpecification.Target.SchemaObject")
$ParserKeys += New-Object Parserkey ("DeleteStatement","DeleteSpecification.Target.SchemaObject")
$ParserKeys += New-Object Parserkey ("AlterTableAddTableElementStatement","SchemaObjectName")
$ParserKeys += New-Object Parserkey ("AlterTableDropTableElementStatement","SchemaObjectName")
$ParserKeys += New-Object Parserkey ("DropIndexStatement","DropIndexClauses.Object")
$ParserKeys += New-Object Parserkey ("CreateIndexStatement","OnName")
$ParserKeys += New-Object Parserkey ("CreateProcedureStatement","ProcedureReference.Name")
$ParserKeys += New-Object Parserkey ("CreateTableStatement","SchemaObjectName")
$ParserKeys += New-Object Parserkey ("DropProcedureStatement","Objects")
$ParserKeys += New-Object Parserkey ("DropTableStatement","Objects")
function Get-UpdatedTableFromReferences($TableReference) {
Write-Verbose "Looks like a joined DML statement, need to get into the references..."
if ($TableReference.FirstTableReference) {
Get-UpdatedTableFromReferences $TableReference.FirstTableReference
} else {
Write-Verbose "closing recursion..."
Return $TableReference.SchemaObject
}
}
function Get-Statement ($Statement, $Keys) {
$StatementObject = [PSCustomObject] @{
PSTypeName = "Parser.DOM.Statement"
ScriptName = $f.Name
BatchNumber= $TotalBatches
StatementNumber = $TotalStatements
StatementType = $null
Action = $null
IsQualified = $false
OnObjectSchema = $null
OnObjectName = $null
}
Add-Member -InputObject $StatementObject -Type ScriptMethod -Name ToString -Value { $this.psobject.typenames[0] } -Force
$StatementObject.Action = ($Statement.ScriptTokenStream | Where-Object {$_.Line -eq $Statement.StartLine -and $_.Column -eq $Statement.StartColumn}).Text.ToUpper()
if ($statementObject.Action -eq "If") {
Write-Verbose "Found an an 'IF' statement, looking at the 'THEN' part of the statement..."
if ($Statement.ThenStatement.StatementList.Statements.Count -ge 1) {
$SubStatements = $Statement.ThenStatement.StatementList.Statements
ForEach ($su in $subStatements) {
$StatementObject = Get-Statement $su $keys
}
} else {
$StatementObject = Get-Statement $Statement.ThenStatement $keys
}
$StatementObject.IsQualified = $true
} else {
$Property = $Statement
$ObjectType = ($Keys | Where-Object {$_.ObjectType -eq $Statement.gettype().name}).ObjectType
Write-Verbose "Object type: $ObjectType"
if ($ObjectType -eq "UpdateStatement" -and $statement.UpdateSpecification.WhereClause -ne $null -and $statement.UpdateSpecification.SetClauses -ne $null) {
$SchemaObject = Get-UpdatedTableFromReferences $Statement.UpdateSpecification.FromClause.TableReferences.FirstTableReference
$StatementObject.OnObjectSchema = $SchemaObject.SchemaIdentifier.Value
$StatementObject.OnObjectName = $SchemaObject.BaseIdentifier.Value
} elseif ($ObjectType -eq "SelectStatement" -and $statement.Queryexpression.fromclause.tablereferences.FirstTableReference -ne $null) {
$SchemaObject = Get-UpdatedTableFromReferences $statement.Queryexpression.fromclause.tablereferences.FirstTableReference
$StatementObject.OnObjectSchema = $SchemaObject.SchemaIdentifier.Value
$StatementObject.OnObjectName = $SchemaObject.BaseIdentifier.Value
} else {
try {
$StatementObject.StatementType = $Statement.GetType().Name.ToString()
$SplitDefinition = (($Keys | Where-Object {$_.ObjectType -eq $Statement.gettype().name}).SchemaSpecification).Split(".")
ForEach ($def in $SplitDefinition) {
$Property = $Property | Select-Object -ExpandProperty $def
}
$StatementObject.OnObjectSchema = $Property.SchemaIdentifier.Value
$StatementObject.OnObjectName = $Property.BaseIdentifier.Value
} catch {
Write-Warning "Parsed statement has no descernible statement type. Maybe define one as a parser key?"
}
}
}
return $StatementObject
}
$LibraryLoaded = $false
$ObjectCreated = $false
$LibraryVersions = @(13,12,11)
if ($PathToScriptDomLibrary -ne "") {
try {
Add-Type -Path $PathToScriptDomLibrary -ErrorAction SilentlyContinue
Write-Verbose "Loaded library from path $PathToScriptDomLibrary"
} catch {
throw "Couldn't load the required ScriptDom library from the path specified!"
}
} else {
ForEach ($v in $LibraryVersions)
{
if (!$LibraryLoaded) {
try {
Add-Type -AssemblyName "Microsoft.SqlServer.TransactSql.ScriptDom,Version=$v.0.0.0,Culture=neutral,PublicKeyToken=89845dcd8080cc91" -ErrorAction SilentlyContinue
Write-Verbose "Loaded version $v.0.0.0 of the ScriptDom library."
$LibraryLoaded = $true
} catch {
Write-Verbose "Couldn't load version $v.0.0.0 of the ScriptDom library."
}
}
}
}
ForEach ($v in $LibraryVersions)
{
if (!$ObjectCreated) {
try {
$ParserNameSpace = "Microsoft.SqlServer.TransactSql.ScriptDom.TSql" + $v + "0Parser"
$Parser = New-Object $ParserNameSpace($UseQuotedIdentifier)
$ObjectCreated = $true
Write-Verbose "Created parser object for version $v..."
} catch {
Write-Verbose "Couldn't load version $v.0.0.0 of the ScriptDom library."
}
}
}
if (!$ObjectCreated) {
throw "Unable to create ScriptDom library; did you load the right version of the library?"
}
}
process {
ForEach ($f in $Files) {
$CurrentFileName = $f.FullName
Write-Verbose "Parsing $CurrentFileName..."
$Reader = New-Object System.IO.StreamReader($f.FullName)
$Errors= $null
$Fragment = $Parser.Parse($Reader, [ref] $Errors)
[bool] $HasErrors = $false
if ($Errors -ne $null) {
[bool] $HasErrors = $true
}
$ScriptObject = [PSCustomObject] @{
PSTypeName = "Parser.DOM.Script"
ScriptName = $f.Name
ScriptFilePath = $f.FullName
NumberOfBatches = $Fragment.Batches.Count
HasParseErrors = $HasErrors
Errors = $Errors
Batches = @()
}
Add-Member -InputObject $ScriptObject -Type ScriptMethod -Name ToString -Value { $this.psobject.typenames[0] } -Force
$TotalBatches = 0
ForEach ($b in $Fragment.Batches) {
$TotalBatches++;
$BatchObject = [pscustomobject] @{
PSTypeName = "Parser.DOM.Batch"
ScriptName = $f.Name
BatchNumber = $TotalBatches
Statements = @()
}
Add-Member -InputObject $BatchObject -Type ScriptMethod -Name ToString -Value { $this.psobject.typenames[0] } -Force
$TotalStatements = 0
ForEach ($s in $b.Statements) {
$TotalStatements++
$StatementObject = Get-Statement $s $ParserKeys
$BatchObject.Statements += $StatementObject
}
$ScriptObject.Batches += $BatchObject
}
$Reader.Close()
$ScriptObject
}
}
end {
$Reader.Dispose()
}