Showing relationships with PSWriteHTML

Showing relationships with PSWriteHTML

Have you seen the powershell module PSWriteHTML? It’s a neat powershell module that allows you to create all kinds of HTML pages, search around and you’ll loads of really complex examples, try here for a starter.


Anyway, here’s a quick one for you, I’ve created a very basic example extracting from a SQL query and creating a relationship map. The proc called in this case is a custom proc running the same code that SSMS does when you view dependencies, but any data with a relationship would work, from this nodes are created for each object and links added between them in just a few lines.

install-module pswritehtml

import-module pswritehtml 

$Results = Invoke-Sqlcmd -ServerInstance localhost -Database MSDB -Query "EXEC GetDepends" 
#get all the objects that need a node
$objects = $results.objectname
$objects+= $results.relative_name
#now make sure there are no duplicates
$uniquename = $objects|select -Unique

#build the page
New-HTML -TitleText 'My diagram' -Online -FilePath c:\temp\Example-depends.html {
#add the diagram
    New-HTMLDiagram  {
        foreach ($u in $uniquename){
            New-DiagramNode -Label $u
        }
        Foreach($r in ($Results|WHERE {$_.object_id -ine $_.relative_id})){
            New-DiagramLink -From $r.object_name -To $r.relative_name
        }
    }
} -ShowHTML

You then end up with a wild object relationship map like this.

ScriptDOM validate script files

ScriptDOM validate script files

Following on from our last journey in to scriptdom here’s a refresher of the wider script but this time to apply that logic to a group of scripts saved in a folder.

Add-Type -Path "C:\Program Files\Microsoft SQL Server\150\DAC\bin\Microsoft.SqlServer.TransactSql.ScriptDom.dll"
$targetversion = 150

    class MyVisitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlConcreteFragmentVisitor {
        #list tables
        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference] $fragment) {
                WRITE-HOST 'TABLE:' ($fragment.SchemaObject.schemaidentifier.Value + '.'+$fragment.SchemaObject.BaseIdentifier.Value)
        }
        #list procs
        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.ProcedureReference] $fragment) {
                    WRITE-HOST 'PROC:'  (($fragment.name.schemaidentifier.value) +'.'+($fragment.name.baseidentifier.value))
               
            }
    }

$root = "C:\Temp\"        
$files = Get-ChildItem -Path $root -Filter "*.sql" -Recurse|where{$_.PSIsContainer -ieq $false}

foreach($file in $files){
            $file           
            $script = Get-Content -literalpath $file.FullName  -Raw

            $str = -join("New-Object ","Microsoft.SqlServer.TransactSql.ScriptDom.TSql$($targetversion)Parser(`$true)")
            $parser = invoke-expression $str
            #is this valid for the version we want?
            $parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
            $stringReader = New-Object System.IO.StringReader($script)

            $fragment = $parser.Parse($stringReader, [ref]$parseErrors)
            if($parseErrors.Count -gt 0) {
                foreach ($parseErr in $parseErrors)
                {
                    write-host "PARSE ERROR: Line: $($parseErr.line) offset: $($parseErr.offset) message:  $($parseErr.message) "  -ForegroundColor yellow 
                }
            }
            IF($parseErrors.Count -ieq 0)
            { 
                $visitor = [MyVisitor]::new()
                $fragment.Accept($visitor)
            }
}

Where the X are my Extended Events??

Where the X are my Extended Events??

Ever suffer like me from temporary check box blindness?
Ever been running an extended events trace and wonder where on earth that query that you definitely just ran has gone because it isn’t in this extended events session?

If so you might be suffering from single or even multiple event loss. Thankfully we’ve gone a miracle elixir for just that. Check out the advanced properties of the extended events session and you’ll see an option in there to make sure you get the full picture.

You can also specify this when creating your extended events session by adding in the option: EVENT_RETENTION_MODE=NO_EVENT_LOSS

You’ll see there’s a nice warning in there telling you that you’re definitely wrong to be ticking this box, so maybe just do it on your test environment for debugging and not your super important production environment.

There are more details here about those different settings https://docs.microsoft.com/en-us/sql/t-sql/statements/alter-event-session-transact-sql?view=sql-server-ver15 it’s all about those greedy buffers getting full.

ScriptDOM Merge Permissions

ScriptDOM Merge Permissions

Continuing our regularly infrequent series on scriptDOM here’s a script finding the MERGE permissions to add to what we’ve already seen , nice and consistent.

[void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.MergeStatement ] $fragment) {   
                $first =  $fragment.MergeSpecification.target.schemaobject.baseidentifier.value
                $second =  $fragment.MergeSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.baseidentifier.value
                $third = $fragment.MergeSpecification.FromClause.TableReferences.FirstTableReference.alias.value

                $firstschema= $fragment.MergeSpecification.target.schemaobject.SchemaIdentifier.value
                $secondschema= $fragment.MergeSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.SchemaIdentifier.value

                $firstdatabase= ($fragment.MergeSpecification.target.schemaobject.DatabaseIdentifier.value)
                $seconddatabase= ($fragment.MergeSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.DatabaseIdentifier.value)

                 IF($firstschema.length -gt 0 -or $secondschema.length -gt 0)#exclude temp table updates
                 {
                    
                     IF($first -ieq $third )#handle alias
                     {
                     IF($seconddatabase.length -eq "0"){$seconddatabase = "DEFAULT"}
                        write-host "USE [$($seconddatabase)]; GRANT INSERT ON $($secondschema).$($second);"
                        write-host "USE [$($seconddatabase)]; GRANT UPDATE ON $($secondschema).$($second);"
                        write-host "USE [$($seconddatabase)]; GRANT DELETE ON $($secondschema).$($second);"
                     } 
                     ELSE
                     {IF($firstdatabase.length -eq "0"){$firstdatabase = "DEFAULT"}
                        write-host "USE [$($firstdatabase)]; GRANT INSERT ON $($firstschema).$($first);"
                        write-host "USE [$($firstdatabase)]; GRANT UPDATE ON $($firstschema).$($first);"
                        write-host "USE [$($firstdatabase)]; GRANT DELETE ON $($firstschema).$($first);"
                     }
                 }
           
            }
Find Services By User Account

Find Services By User Account

Search your VM estate (Using VMM) to find any services running under a particular user account

$servers =  Get-SCVMHost -VMMServer VMMServerName |select -ExpandProperty VMs|select name , owner

$results = foreach($s in $servers)
{
get-wmiobject win32_service -ComputerName $s.name -ErrorAction SilentlyContinue|WHERE{$_.startname -eq "domain\username"}|select name,startname,startmode,@{n="server";e={$s.name}}
}
$results
The term ‘Â’ is not recognized

The term ‘Â’ is not recognized

The term 'Â' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again."

Anyone else see this message more frequently than they’d like? For me the issue with this one is usually a rogue character somewhere in a PowerShell script that works fine when run manually or in ISE but errors when scheduled and automated to run.

So just remove them right? like they always say;

🎵 You’re writing a script, you’re checking it twice, you’re gonna find those incorrect characters right? 🎵

Maybe not, it’s usually an invisible character so tracking it down isn’t always that easy. I would usually open this file up in a text editor (such as notepad++) and display all characters

Then I would look for any odd spaces usually at the start or end of a line but not always .

I’d remove these and and test again, even if you think the space is needed it can be worth removing and adding again as it might not be just a normal space.

Merry Scriptmas all!

New-Cluster – The parameter is incorrect

New-Cluster – The parameter is incorrect

Recently had an issue trying to create a cluster using the New-Cluster PowerShell command on a Windows Server 2019 target. The error returned was:

The parameter is incorrect

not very helpful and the Cluster report didn’t give any more details either.

Running the same command on the target server worked fine though, it just wouldn’t work remotely.

The issues seems to be because I was running from a Windows Server 2016 machine, once I tried again from another Windows Server 2019 machine this worked remotely too.

ScriptDOM Permissions

ScriptDOM Permissions

Here we are again, hacking our way through scriptDOM analysis.

This section will analyse the scripts to look for tables where a select/insert/update/delete is happening in order to build up a list of permissions which might be required. The script does generate duplicate records so those would need to be stripped out.

If you look at the script you’ll also see some checks being done on whether the perms are in the local database or a cross database query and also trying to exclude temporary tables and handle where the alias is referenced in the update / delete rather than the table directly



[void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference] $fragment) {                 
                IF($fragment.schemaobject.BaseIdentifier.value -inotlike "*#*" -and $fragment.schemaobject.SchemaIdentifier.value.Length -gt 0 ){
                    $database = $($fragment.schemaobject.DatabaseIdentifier.value)
                    IF($database.length -eq "0"){$database = "DEFAULT"}
                    Write-Host "USE [$($database)]; GRANT SELECT ON $($fragment.schemaobject.SchemaIdentifier.value).$($fragment.SchemaObject.BaseIdentifier.Value) to [INSERTUSER];"                 
               }
            }

        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.InsertStatement] $fragment) {   
                $database= ($fragment.InsertSpecification.target.SchemaObject.DatabaseIdentifier.value)
                $first =  $fragment.InsertSpecification.target.schemaobject.baseidentifier.value
                $firstschema= $fragment.InsertSpecification.target.schemaobject.SchemaIdentifier.value
                 IF($firstschema.length -gt 0) 
                {
                    IF($database.length -eq "0"){$database = "DEFAULT"}
                    write-host "USE [$($database)]; GRANT INSERT ON $($firstschema).$($first) to [INSERTUSER];"
                    
                }    
                
                      
            }

 
        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.UpdateStatement ] $fragment) {   
                $first =  $fragment.UpdateSpecification.target.schemaobject.baseidentifier.value
                $second =  $fragment.UpdateSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.baseidentifier.value
                $third = $fragment.UpdateSpecification.FromClause.TableReferences.FirstTableReference.alias.value

                $firstschema= $fragment.UpdateSpecification.target.schemaobject.SchemaIdentifier.value
                $secondschema= $fragment.UpdateSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.SchemaIdentifier.value

                $firstdatabase= ($fragment.UpdateSpecification.target.schemaobject.DatabaseIdentifier.value)
                $seconddatabase= ($fragment.UpdateSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.DatabaseIdentifier.value)

                 IF($firstschema.length -gt 0 -or $secondschema.length -gt 0)#exclude temp table updates
                 {
                    
                     IF($first -ieq $third )#handle alias
                     {
                     IF($seconddatabase.length -eq "0"){$seconddatabase = "DEFAULT"}
                        write-host "USE [$($seconddatabase)]; GRANT UPDATE ON $($secondschema).$($second) to [INSERTUSER];"
                     } 
                     ELSE
                     {IF($firstdatabase.length -eq "0"){$firstdatabase = "DEFAULT"}
                        write-host "USE [$($firstdatabase)]; GRANT UPDATE ON $($firstschema).$($first) to [INSERTUSER];"
                     }
                 }
           
            }

        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.DeleteStatement] $fragment) {   
                
                $first =  $fragment.DeleteSpecification.target.schemaobject.baseidentifier.value
                $second =  $fragment.DeleteSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.baseidentifier.value
                $third = $fragment.DeleteSpecification.FromClause.TableReferences.FirstTableReference.alias.value

                $firstschema= $fragment.DeleteSpecification.target.schemaobject.SchemaIdentifier.value
                $secondschema= $fragment.DeleteSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.SchemaIdentifier.value
                $thirdschema= $fragment.DeleteSpecification.FromClause.TableReferences.FirstTableReference.SchemaIdentifier.schemaidentifier.value

                $firstdatabase= ($fragment.DeleteSpecification.target.schemaobject.DatabaseIdentifier.value)
                $seconddatabase= ($fragment.DeleteSpecification.FromClause.TableReferences.FirstTableReference.schemaobject.DatabaseIdentifier.value)
               
                IF($firstschema.length -gt 0 -or $secondschema.length -gt 0 -or $thirdschema.length -gt 0) #exclude temp table updates
                {
                        IF($first -ieq $third )
                        {
                        IF($seconddatabase.length -eq "0"){$seconddatabase = "DEFAULT"}
                        write-host "USE [$($seconddatabase)]; GRANT DELETE ON $($secondschema).$($second) to [INSERTUSER];"
                        }
                        
                        ELSE
                        {
                        IF($firstdatabase.length -eq "0"){$firstdatabase = "DEFAULT"}
                        write-host "USE [$($firstdatabase)]; GRANT DELETE ON $($firstschema).$($first) to [INSERTUSER];"
                       
                        }
                    }
                }
SQL CPU By OS Thread

SQL CPU By OS Thread

Trying to work out which one of the queries is using up most of that CPU currently on your server?

Try this out, this isn’t foolproof and I have seen it miss queries at times but it’s another option for you to try to track it down.

Enter the hostname and sqlinstance name when prompted and the script will find the SQL processes running, get the threads associated with that process and then try to match these up to SQL sessions and output the results to a grid.

param
(
	[string] $varHostName = "$(READ-HOST 'Host name')",
	[string] $varSQLInstance = "$(READ-HOST 'SQL Server Name [server\instance]')"
)
#$VerbosePreference = "continue"

IF($varhostname -ieq "localhost" -or $varHostName -ieq $env:COMPUTERNAME)
{
    Write-host "running on local machine switching to local variations where appropriate"
}

[int]$varproc =  IF($varhostname -ieq "localhost" -or $varHostName -ieq $varenv:COMPUTERNAME){(Get-WmiObject -class Win32_ComputerSystem -ErrorAction stop).NumberOfLogicalProcessors}ELSE{(Get-WmiObject -ComputerName $varHostName -class Win32_ComputerSystem -ErrorAction stop).NumberOfLogicalProcessors}

IF($varnull -ieq $varproc -or $varproc -le 0)
{
    $varproc = Read-host {-Prompt "Unable to comnfirm number of logical processors, plese confirm how many there are:" }
}

Write-host "Found Processors: $($varProc)"

TRY{
    $varid = IF($varhostname -ieq "localhost" -or $varHostName -ieq $varenv:COMPUTERNAME){GET-PROCESS -Name "sqlservr" -ErrorAction Stop -WarningAction Stop | select -ExpandProperty id}ELSE{GET-PROCESS -Name "sqlservr" -ComputerName $varHostName -ErrorAction Stop -WarningAction Stop | select -ExpandProperty id}

}
CATCH
{
throw $_
}
Write-host "Found SQL PID: $($varID)"

$varMaxAttempts = 20
$varcount = 1
#run until records returned sometime 0 percent found if queries in sleeping/runnable state
While($varthreads.count -eq 0 -and $varcount -ile $varMaxAttempts)
{
    $varthreads = Get-WmiObject Win32_PerfFormattedData_PerfProc_Thread -ComputerName $varHostName | WHERE-OBJECT {$_.Name -notmatch '_Total' -and $_.IDProcess -in $varid -and $_.PercentProcessorTime -igt 0} |  SELECT-OBJECT Name, IDProcess, IDThread,@{e={($_.PercentProcessorTime) / $varproc};n="Percent"}

    Write-host "Attempt :  $($varcount)"
    $varcount =$varcount+1

}

write-host "thread details"
Write-host $varthreads

#check if records returned if so build sql query
IF($varthreads.count -gt 0)
{
    #set sql statement
    $varsql = "SELECT 
                    r.session_id, os_thread_id,sub.[Percent],st.text, s.login_name, s.host_name
            FROM 
                    sys.dm_os_threads AS ot
                    JOIN sys.dm_os_tasks AS t     ON t.worker_address = ot.worker_address
                    JOIN sys.dm_exec_requests AS r      ON t.session_id = r.session_id
					JOIN sys.dm_exec_sessions as s on s.session_id = t.session_id
                    outer APPLY sys.dm_exec_sql_text(r.sql_handle) AS st
                    outer APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp
                    INNER JOIN (
            "
        

    FOREACH ($varthr IN $varthreads)
    {
    # build up threads to loop through

            $varsql = -join ($varSQL,"SELECT "+$varthr.IDThread.ToString() +" as [Thread], " +$varthr.percent +" as [Percent] UNION ALL ")

    }
    #remove last union all
        $varsql = $varsql.Substring(0,($varsql.Length -10))
    #add where clause
        $varsql =-join($varsql,") as sub  ON sub.Thread = ot.os_thread_id	WHERE r.session_id <>@@spid	")
    #run sql and display
        TRY{
        
        Write-host "running the following sql on $($varsqlinstance)"
        Write-host $varsql

        INVOKE-SQLCMD -Query $varsql -ServerInstance $varSQLInstance -Database "Master" |OUT-GRIDVIEW
        }
        CATCH{
            Throw $_
        }




    

}
ELSE
{
Write-host "no threads found no further action taken"
}
  
  Remove-Variable var* -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
ScriptDOM (again) list tables

ScriptDOM (again) list tables

Recently while investigating a poorly performing query which had a lot of code in I was able to use the core code from here and just list out all the tables being referenced so I could use these to identify stats / indexes that might need to be tweaked just by using this in the class

          [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference] $fragment) {
                    WRITE-HOST $fragment.SchemaObject.BaseIdentifier.Value
                }
Reporting on SQL version compliance

Reporting on SQL version compliance


The script below can be used to generate a report to show if your servers violate your company policy on being within CU compliance. This uses the following DBATools commands to get the latest CU info and compare to your estate

Test-DBABuild
Get-DbaBuildReference

IF (!(get-module dbatools)) {
    Import-Module dbatools
}     
#get the instances either from  from a list of registered servers or just a static list
$instances = (Get-DbaRegisteredServer -SqlInstance RegServername | select-object ServerName -Unique).servername  # @("Server01","Server02\instance","Server03") 
  
#Get the build details for the instances    
$instancesBuildReference = Get-DbaBuildReference -SqlInstance $instances -Update
    
   
($instancesBuildReference.foreach{
        $i = $psitem
        #check if compliant in this case we must be within 3CUs of the most recent one
        $t = Test-dbabuild -Build $i.build -MaxBehind "3CU"
        #merge the results together 
        $Output = New-Object -TypeName PSObject -Property @{
            'SqlInstance' = $i.sqlinstance
            'Edition' = $i.namelevel
            'Build' = $i.build
            'SPLevel' = $i.SPLevel
            'CULevel'= $i.CULevel
            'TargetSupportedUntil' = $t.supporteduntil
            'Compliant' =  $t.Compliant
            'SPTarget' = $t.sptarget
            'CuTarget' = $t.cutarget
            

            }
        $output
        #pass it in to a variable and add some nice colours for the HTML version
    } |Tee-Object -Variable teeresults | select-object SqlInstance, Edition,Build,SPLevel,CULevel,@{N="Compliant";e={IF($_.compliant -ieq "true"){-join('<font color="green">',$_.compliant,'</font>')}ELSE{-join('<font color="red">',$_.compliant,'</font>')}}},SPTarget,CUTarget,TargetSupportedUntil|ConvertTo-Html).replace("&lt;","<").replace("&quot;",'"').replace("&gt;",">") |Out-File C:\temp\versions.html
    
#take that variable and output it to CSV because some people won't want your coloured HTML version.    
$teeresults|select-object  SqlInstance,Edition,Build,SPLevel,CULevel,Compliant,SPTarget,CuTarget,TargetSupportedUntil|Export-Csv C:\temp\versions.csv -Force -NoTypeInformation
    

and from this you’ll get a .csv output and a page like below to distribute to stakeholders in your business including whether the intended target is still in support with Microsoft which may help you get sign off for those upgrades you’ve been asking about? yeah? maybe? worth a try.

Search for columns in all databases

Search for columns in all databases

nothing fancy, just looking for some columns with particular data types (dates) when you don’t know where it is, column name or data type optional remove as needed

		EXEC sp_msforeachdb 'USE [?]
		select ''?'' as [databasename],
		object_Schema_name(o.object_id) as [schemaname]
		,o.name as [objectname]
		,c.name as [columnname]
		,t.name as [type]
		from sys.columns as c
		INNER JOIN sys.objects as o
		ON o.object_id = c.object_id
		INNER JOIN sys.types as t
		ON c.user_type_id = t.user_type_id
		WHERE o.type = ''U''
		and t.name like ''%date%''
		and c.name like ''%somenameorother%''			
		'
For thine is the ScriptDOM, the power, and the glory forever

For thine is the ScriptDOM, the power, and the glory forever

Now I am nothing if not late to the party and so here I am rocking up to the ScriptDOM party trying to figure out where the food is and what just 5? 10 years too late? that’s good going.

DISCLAIMER: the examples below will lean heavily on some of the awesome work done here https://www.dbdelta.com/microsoft-sql-server-script-dom and here

https://www.mssqltips.com/sqlservertip/6936/find-problematic-sql-nolock-patterns-update-delete/ and without these 2 sources I don’t think I’d have got anywhere because the documentation is sparse and for someone who doesn’t have c#/.NET expertise it can be a bit daunting.


So, what is it?

scriptDOM is a code library that allows you to do analysis of T-SQL scripts to look for specific elements or just general parsing / formatting errors.

what sort of elements?

almost anything you might do in a TSQL script has a method for it https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.transactsql.scriptdom?view=sql-dacfx-150

right and how does it work?

the scripts passed in to it are broken down in to your all their individual components and then traversed to pull out the required elements

ok but why would I want to do that?


we’re only scratching the surface here but some of the key points are

and more.

couldn’t I just buy a 3rd party tool to do that?

well, yeah probably I think a lot of the tools which offer code analysis / formatting are built using the scriptDOM functionality underneath but maybe you just don’t have the budget for that or it’s not quite flexible enough for what you need.

well I’m a little intrigued is it just a button I click somewhere then?


not exactly, you’ll need to build up some scripts in these examples using powershell and .net

I’m out.

no, wait , come back it’s not that bad, stick with me we’ll get it sorted

…..

good, so the fist thing you’ll need to do if you don’t already have it is download and install the latest sqlpackage version https://docs.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-download?view=sql-server-ver15 this will allow you to run the scripts in isolation without the need for a SQL Server connection at all

that’s pretty neat.


yeah and now you’ve installed it you’ll need to load the type into your powershell session, make sure you do this first or you’ll not get very far

Add-Type -Path "C:\Program Files\Microsoft SQL Server\150\DAC\bin\Microsoft.SqlServer.TransactSql.ScriptDom.dll"

then as shown on https://www.dbdelta.com/microsoft-sql-server-script-dom/ you can use this to verify the script syntax by passing it into a parser object

$script = @"
SELECT TOP 1 column1 FROM table1
"@
$parser = New-Object Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser($true)
$parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
$stringReader = New-Object System.IO.StringReader($script)
$parser.Parse($stringReader, [ref]$parseErrors) |out-null
if($parseErrors.Count -gt 0) {
    foreach ($parseErr in $parseErrors)
    {
        write-host "PARSE ERROR: Line: $($parseErr.line) offset: $($parseErr.offset) message:  $($parseErr.message) "  -ForegroundColor yellow 
    }
}
ELSE{
        write-host "PARSE SUCCESS: Successfully parsed script" -ForegroundColor green
}

PARSE SUCCESS: Successfully parsed script

hooray!

now one really neat thing to note in there is this part “Microsoft.SqlServer.TransactSql.ScriptDom.TSql150Parser” in this instance we’ve targeted it against 150 = SQL2019

well that’s no good, I’m not on 2019

not surprising, luckily you can change that to target the version you need so if you’re still on SQL2012 you can just change that part to Microsoft.SqlServer.TransactSql.ScriptDom.TSql110Parser and if you then run that with a different script

$script = @"
CREATE OR ALTER PROC newproc
AS
BEGIN
SELECT 1
END
"@
$parser = New-Object Microsoft.SqlServer.TransactSql.ScriptDom.TSql110Parser($true)
$parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
$stringReader = New-Object System.IO.StringReader($script)
$parser.Parse($stringReader, [ref]$parseErrors)|out-null
if($parseErrors.Count -gt 0) {
    foreach ($parseErr in $parseErrors)
    {
        write-host "PARSE ERROR: Line: $($parseErr.line) offset: $($parseErr.offset) message:  $($parseErr.message) "  -ForegroundColor yellow 
    }
}
ELSE{
        write-host "PARSE SUCCESS: Successfully parsed script" -ForegroundColor green
}

you’ll see this as the CREATE OR ALTER statement isn’t valid

PARSE ERROR: Line: 1 offset: 0 message: Incorrect syntax near CREATE.
PARSE ERROR: Line: 1 offset: 7 message: Incorrect syntax near OR.


what you can do then is you can parametrize the script so you can declare the version you should run as

$targetversion = 150
#$parser = New-Object Microsoft.SqlServer.TransactSql.ScriptDom.TSql110Parser($true)
$str = -join("New-Object ","Microsoft.SqlServer.TransactSql.ScriptDom.TSql$($targetversion)Parser(`$true)")
$parser = invoke-expression $str

once you know that the script is valid you can start to dig in to it for the code you are looking for. As pointed out here https://www.dbdelta.com/microsoft-sql-server-script-dom/ trying to do analysis of complex statements with multiple code blocks / paths in it can be a problem and if you follow their example you’ll see that the nested select statements get missed but thankfully they have provided an example of a class using Microsoft.SqlServer.TransactSql.ScriptDom.TSqlConcreteFragmentVisitor which you can declare in your scripts to make this much more usable so we’ll use that framework to help us start to dig in to the scripts. In fact if you didn’t at the start of this read the links, go do that now it’s going to help you for the next section. I’ll wait.


OK, BRB

now we’re going to carry on with some more examples and for this we’re going to use a longer script and look for the following

  • cursor
  • transaction without a commit
  • select from a VERY large table
  • select *

OMG, WHO WROTE THIS? IS THIS IN PRODUCTION??

no, it’s just an example

phew, ok

let’s crack on, to start with we’re going to look for the cursor in there, we’ll declare the class, assign the parse results to a variable and then let the two collide

Add-Type -Path "C:\Program Files\Microsoft SQL Server\150\DAC\bin\Microsoft.SqlServer.TransactSql.ScriptDom.dll"
$script = @"
SELECT * FROM dbo.tableo with (nolock);
IF 1 = 1
BEGIN
    SELECT * FROM dbo.superbigtable;
END; 
DECLARE @name VARCHAR(50)
DECLARE db_cursor CURSOR FOR 
SELECT name 
FROM MASTER.dbo.sysdatabases 
OPEN db_cursor  
FETCH NEXT FROM db_cursor INTO @name  
WHILE @@FETCH_STATUS = 0  
BEGIN  
      SELECT @name
END 
CLOSE db_cursor  
DEALLOCATE db_cursor 
BEGIN TRANSACTION
select 1
IF get-date <getdate
BEGIN
--COMMIT
SELECT 'no commit'
END
ELSE
BEGIN
ROLLBACK
END
"@
$targetversion = 150
class MyVisitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlConcreteFragmentVisitor {
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.CursorDefinition] $fragment) {
        
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }
    
}
$str = -join("New-Object ","Microsoft.SqlServer.TransactSql.ScriptDom.TSql$($targetversion)Parser(`$true)")
$parser = invoke-expression $str
$parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
$stringReader = New-Object System.IO.StringReader($script)
$fragment = $parser.Parse($stringReader, [ref]$parseErrors)
if($parseErrors.Count -gt 0) {
    foreach ($parseErr in $parseErrors)
    {
        write-host "PARSE ERROR: Line: $($parseErr.line) offset: $($parseErr.offset) message:  $($parseErr.message) "  -ForegroundColor yellow 
    }
}
ELSE{
        write-host "PARSE SUCCESS: Successfully parsed script" -ForegroundColor green
}
IF($parseErrors.Count -ieq 0)
    { 
    $visitor = [MyVisitor]::new()
    $fragment.Accept($visitor)
    }

PARSE SUCCESS: Successfully parsed script
WARNING: CursorDefinition found at line 10, column 1, length 42

You can see in this example it’s flagged up the cursor because we used the Microsoft.SqlServer.TransactSql.ScriptDom.CursorDefinition to identify it. Let’s add the Select * example from https://www.dbdelta.com/microsoft-sql-server-script-dom/ just to show you can use the two together

class MyVisitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlConcreteFragmentVisitor {
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.SelectStarExpression] $fragment) {
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }
    
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.CursorDefinition] $fragment) {
        
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }
    
}

PARSE SUCCESS: Successfully parsed script
WARNING: SelectStarExpression found at line 1, column 8, length 1
WARNING: SelectStarExpression found at line 4, column 12, length 1
WARNING: CursorDefinition found at line 10, column 1, length 42


that has confirmed the parsing but warned on both select * statements and the cursor, so you can see how you can quickly build up a list of your troubles.

I guess so yeah


let’s take a look at identifying the transactions next as this caused me some headaches. I had hoped that using some combination of
Microsoft.SqlServer.TransactSql.ScriptDom.BeginTransactionStatement
Microsoft.SqlServer.TransactSql.ScriptDom.CommitTransactionStatement
Microsoft.SqlServer.TransactSql.ScriptDom.RollbackTransactionStatement
to identify the script elements would do it and whilst in isolation they all work

        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.BeginTransactionStatement] $fragment) {            
            Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
           }
        
        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.CommitTransactionStatement] $fragment) {   
            Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
        }
        [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.RollbackTransactionStatement] $fragment) {   
            Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
        }

WARNING: BeginTransactionStatement found at line 27, column 1, length 17
WARNING: RollbackTransactionStatement found at line 36, column 1, length 8


note that there is no commit in the script we used so it doesn’t find it which is what I’d expect but I haven’t found a way to nest the checks successfully to check that ‘yes I found a begin transaction’ but ‘no I didn’t find a commit’, if you’ve managed this one then I’d be really interested to see how that’s working.

BOOOOO!!HISSSSS!

You can’t do that you said you’d tell me how to find them?!

WHY HAVE I EVEN READ THIS FAR?!


I do have a workaround though.

oh, sorry,


if you did your homework you’ll have seen here https://www.mssqltips.com/sqlservertip/6936/find-problematic-sql-nolock-patterns-update-delete/ how you can interrogate the types and statements within a fragment and use these to find things such as NOLOCKs well if we add in this section to look at the fragments.

    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.BeginTransactionStatement] $fragment) { 
        write-host ($fragment.ScriptTokenStream|select-object tokentype)
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }

you’ll get someting like this , not very useful right?

{TokenType=Select} @{TokenType=WhiteSpace} @{TokenType=Star} @{TokenType=WhiteSpace} @{TokenType=From} @{TokenType=WhiteSpace} @{TokenType=Identifier} @{TokenType=Dot} @{TokenType=Identifier} @{TokenType=WhiteSpace} @{TokenType=With} @{TokenType=WhiteSpace} @{TokenType=LeftParenthesis} @{TokenType=Identifier} @{TokenType=RightParenthesis} @{TokenType=Semicolon} @{TokenType=WhiteSpace} @{TokenType=If} @{TokenType=WhiteSpace} @{TokenType=Integer} @{TokenType=WhiteSpace}……

too much token is a bad thing

yeah not ideal but buried in there you’ll find these ones

@{TokenType=Transaction}
@{TokenType=Rollback}

and when there is a commit in the script you’ll also get

@{TokenType=Commit}

keep going….

so now we know these are there we have something to work with and we can check that a COMMIT comes after a BEGIN TRANSACTION when a transaction is found


    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.BeginTransactionStatement] $fragment) { 
        [int]$start = ($fragment.ScriptTokenStream|WHERE-object{$_.TokenType -in ("Transaction")}).offset
        [int]$end =($fragment.ScriptTokenStream|WHERE-object{$_.TokenType -in ("COMMIT")}).offset
        IF($null -eq $end){$end = 0}
        #$start
        IF($end -lt $start){
        Write-Host "WARNING: $($Fragment.GetType().Name) BEGIN TRANSACTION without a COMMIT" -ForegroundColor Yellow}
    }
    

PARSE SUCCESS: Successfully parsed script
WARNING: SelectStarExpression found at line 1, column 8, length 1
WARNING: SelectStarExpression found at line 4, column 12, length 1
WARNING: CursorDefinition found at line 10, column 1, length 42
WARNING: BeginTransactionStatement BEGIN TRANSACTION without a COMMIT

Ta-da, now if you remove the comment from the commit in the script

–COMMIT

and run it again you’ll see that we no longer get a warning about the transaction

PARSE SUCCESS: Successfully parsed script
WARNING: SelectStarExpression found at line 1, column 8, length 1
WARNING: SelectStarExpression found at line 4, column 12, length 1
WARNING: CursorDefinition found at line 10, column 1, length 42

that’s more like it

the last section we’re going to look at is how to pick out those scripts that come in that will touch that VERY large table you have, the one you kept saying you really needed to put partitioning on but now is so big it has it’s own orbit. You need to keep an eye on the Selects from that and definitely the Updates.

For that we’ll use the Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference which will allow us to see the tables referenced in the script and look for those known problems

          [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference] $fragment) {
                   IF($fragment.SchemaObject.BaseIdentifier.Value -ieq "superbigtable")
                     {
                     Write-Host "WARNING: Large table. Previously identified troublesome table identified $($fragment.SchemaObject.BaseIdentifier.Value)" -ForegroundColor Yellow
                     }
                }

WARNING: Large table. Previously identified troublesome table identified superbigtable

that’s handy

as you can see there’s a lot to it and and we haven’t even scratched the surface here, it’s really good for opt in analysis where you are looking for specific things.

I’ll add the full script of these examples below for reference and as a CHALLENGE I haven’t added in the check to find the NOLOCK, see if you can update the script to alert for that too.

more homework?

yeah sorry but I’d be really keen to know where you’ve found a use for scriptDOM and how you are using it

Add-Type -Path "C:\Program Files\Microsoft SQL Server\150\DAC\bin\Microsoft.SqlServer.TransactSql.ScriptDom.dll"
 
 
 
$script = @"
SELECT * FROM dbo.tableo with (nolock);
IF 1 = 1
BEGIN
    SELECT * FROM dbo.superbigtable;
END; 
 
DECLARE @name VARCHAR(50)
 
DECLARE db_cursor CURSOR FOR 
SELECT name 
FROM MASTER.dbo.sysdatabases 
 
OPEN db_cursor  
FETCH NEXT FROM db_cursor INTO @name  
 
WHILE @@FETCH_STATUS = 0  
BEGIN  
      SELECT @name
END 
 
CLOSE db_cursor  
DEALLOCATE db_cursor 
 
BEGIN TRANSACTION
select 1
IF get-date <getdate
BEGIN
--COMMIT
SELECT 'no commit'
END
ELSE
BEGIN
ROLLBACK
END
"@
$targetversion = 150
 
 
class MyVisitor: Microsoft.SqlServer.TransactSql.ScriptDom.TSqlConcreteFragmentVisitor {
 
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.SelectStarExpression] $fragment) {
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }
 
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.CursorDefinition] $fragment) {
         
        Write-Host "WARNING: $($Fragment.GetType().Name) found at line $($fragment.StartLine), column $($fragment.StartColumn), length $($fragment.FragmentLength)" -ForegroundColor Yellow
    }
     
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.BeginTransactionStatement] $fragment) { 
        [int]$start = ($fragment.ScriptTokenStream|WHERE-object{$_.TokenType -in ("Transaction")}).offset
        [int]$end =($fragment.ScriptTokenStream|WHERE-object{$_.TokenType -in ("COMMIT")}).offset
        IF($null -eq $end){$end = 0}
        #$start
        IF($end -lt $start){
        Write-Host "WARNING: $($Fragment.GetType().Name) BEGIN TRANSACTION without a COMMIT" -ForegroundColor Yellow
       }
    }
     
    #check for big tables
    [void]Visit ([Microsoft.SqlServer.TransactSql.ScriptDom.NamedTableReference] $fragment) {
             
        #Write-Host "Found $($fragment.GetType().Name) at line $($fragment.StartLine), column $($fragment.StartColumn). Table name: $($fragment.SchemaObject.BaseIdentifier.Value)"
        IF($fragment.SchemaObject.BaseIdentifier.Value -ieq "superbigtable")
        {
        Write-Host "WARNING: Large table. Previously identified troublesome table identified $($fragment.SchemaObject.BaseIdentifier.Value)" -ForegroundColor Yellow
        }
    }
 
}
 
 
$str = -join("New-Object ","Microsoft.SqlServer.TransactSql.ScriptDom.TSql$($targetversion)Parser(`$true)")
$parser = invoke-expression $str
 
$parseErrors = New-Object System.Collections.Generic.List[Microsoft.SqlServer.TransactSql.ScriptDom.ParseError]
$stringReader = New-Object System.IO.StringReader($script)
$fragment = $parser.Parse($stringReader, [ref]$parseErrors)
if($parseErrors.Count -gt 0) {
    foreach ($parseErr in $parseErrors)
    {
        write-host "PARSE ERROR: Line: $($parseErr.line) offset: $($parseErr.offset) message:  $($parseErr.message) "  -ForegroundColor yellow 
    }
}
ELSE{
        write-host "PARSE SUCCESS: Successfully parsed script" -ForegroundColor green
}
 
IF($parseErrors.Count -ieq 0)
    { 
    $visitor = [MyVisitor]::new()
    $fragment.Accept($visitor)
    }

I need a lay down now

Send HTML Email

Send HTML Email

sample base code block to email out SQL results in a formatted HTML output.

DECLARE @HTML_Body VARCHAR(MAX)
DECLARE @HTML_Head VARCHAR(MAX)
DECLARE @HTML_Tail VARCHAR(MAX)
DECLARE @HTML_Headappend VARCHAR(MAX)
DECLARE @HTML_Tailappend VARCHAR(MAX)
DECLARE @EmailSubject VARCHAR(255)
DECLARE @recipients VARCHAR(255)
DECLARE @profile varchar(255)
SET @HTML_Headappend = 'some text to append to top of email  if needed'
SET @HTML_Tailappend = 'some text to append to bottom of email if needed'
SET @EmailSubject = 'the subject goes here'
SET @recipients = 'email@email.com'
SET @profile = 'sql agent profile name'

SET @HTML_Head = '<html>'
SET @HTML_Head = @HTML_Head + '<head>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' <style>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' body{font-family: arial; font-size: 13px;}table{font-family: arial; font-size: 13px; border-collapse: collapse;width:100%} td {padding: 2px;height:15px;border:solid 1px black;} th {padding: 2px;background-color:black;color:white;border:solid 1px black;}' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + '</style>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + '</head>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + '<body> '+@HTML_Headappend+ '<br />' + CHAR(13) + CHAR(10);


SET @HTML_Head = @HTML_Head + '<table>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' <tr>' + CHAR(13) + CHAR(10);
-- column headers should match the order of the select statement below
SET @HTML_Head = @HTML_Head + ' <th>Column1</th>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' <th>Column2</th>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' <th>Column3 etc</th>' + CHAR(13) + CHAR(10);
SET @HTML_Head = @HTML_Head + ' </tr>' + CHAR(13) + CHAR(10);
SET @HTML_Tail = '</table></br></br>'+@HTML_Tailappend+'</body></html>'



-- columns should match the order of the headers above all named [TD]
SET @HTML_Body = @HTML_Head + (
SELECT	column1 AS [TD],
		column2 AS [TD],
		column3etc AS [TD],
FROM table
WHERE records = 1
FOR XML RAW('tr') 
	,ELEMENTS
) + @HTML_Tail

--SELECT @HTML_BODY
--send it on
EXEC msdb.dbo.sp_send_dbmail @recipients = @recipient
,@subject = @EmailSubject
,@body = @HTML_Body
,@body_format = 'HTML'
,@profile_name = @profile;