pf-Sql.ps1

function Import-Module_sqlserver {
    if ( Get-Module -Name sqlserver ) {
        return
    }
    $sqlModule = Get-Module -ListAvailable | Where-Object name -eq sqlserver
    if ( -not $sqlModule ) { 
        Write-Warning 'sqlserver Module not available'
        return
    }
 
    # module sqlserver can change the current drive to sqlserver: ,
    # therefore this ensure the operation returns to the original location
    Push-Location
    try {
        $sqlModule | Import-Module -DisableNameChecking
    }
    finally {
        Pop-Location
    }
}

function New-SqlCommand($query, $inputfile, $QueryTimeout = [TimeSpan]::FromMinutes(10)) {
    if ( $inputfile ) {
        if ( -not ( Test-Path $inputfile ) ) {
            throw "File not found : '$inputfile' "
        }

        if ($query) {
            throw "Query will be overriden by : '$inputfile' "
        }

        Write-Host "SQL Executing : '$InputFile'"
        $query = Get-Content $inputfile -Raw
    }

    $cmd = New-Object System.Data.SqlClient.SqlCommand
    $cmd.connection = $conn
    $cmd.commandtext = $query
    $cmd.CommandTimeout = $QueryTimeout.TotalSeconds
    return $cmd
}

function Invoke-SqlcmdAdoNetScalar ($dbServer, $db, $query) {
    
    $conn = New-Object System.Data.SqlClient.SqlConnection
    $conn.ConnectionString = "Data Source=$dbServer;Initial Catalog=$db;Integrated Security=SSPI;"
    try {
        $cmd = New-SqlCommand @PSBoundParameters
        $conn.open()
        $res = $cmd.ExecuteScalar()
        $res
    } finally {
        $conn.close()
    }
}
function Invoke-SqlcmdAdoNetScalar:::Example ($dbServer, $db, $query) {
    Invoke-SqlcmdAdoNetScalar -dbServer $SERVER.DB -db 'master' -query 'select count(*) from INFORMATION_SCHEMA.COLUMNS'
}

function Invoke-SqlcmdAdoNet ($dbServer, $db, $query, $inputfile) {
    
    $conn = New-Object System.Data.SqlClient.SqlConnection
    $conn.ConnectionString = "Data Source=$dbServer;Initial Catalog=$db;Integrated Security=SSPI;"
    try {
        $cmd = New-SqlCommand @PSBoundParameters
        $conn.open()
        $reader = $cmd.ExecuteReader()
        try {
            while ($reader.Read()) {
                $res = New-Object System.Object
                for ($i = 0; $i -lt $reader.FieldCount; $i++){
                    $res | Add-Member -type NoteProperty -name $reader.GetName($i) -value $reader.GetValue($i)
                }
                $res
            }
        }
        finally {
            $reader.Close()
        }
    } finally {
        $conn.close()
    }
}
function Invoke-SqlcmdAdoNet:::Example ($dbServer, $db, $query) {
    Invoke-SqlcmdAdoNet -dbServer $SERVER.DB -db 'master' -query 'select * from INFORMATION_SCHEMA.COLUMNS'
}

function Invoke-Sqlcmd_New ($Query, $Database, $ServerInstance, $InputFile, [switch]$scalar ) {
    if ($scalar) {
        Invoke-SqlcmdAdoNetScalar --dbServer $SERVER.DB -db $Database -query $Query 
    }
    else { 
        Invoke-SqlcmdAdoNet -dbServer $SERVER.DB -db $Database -query $Query -inputfile $InputFile
    }
}

function Invoke-SqlScript ($Query, $Database, $ServerInstance, $InputFile, [switch]$scalar ) {
    Import-Module_sqlserver
    if ($Database -is [Microsoft.SqlServer.Management.Smo.Database] ) {
        # Temporary Fix to SQLProvider WMI issue
        $PsBoundParameters.Database = $Database.Name
    }
    if ($SERVER.DB) {
        $PsBoundParameters.ServerInstance = $SERVER.DB
    }
    if ($InputFile) {
        Write-Host "Executing : '$InputFile'"
    }
    $timeout = [TimeSpan]'00:10:00'
    $result = sqlserver\Invoke-Sqlcmd @PsBoundParameters -QueryTimeout $timeout.TotalSeconds -Verbose -ErrorAction 'stop'

    if ($InputFile) {
        $msg = ConvertTo-Json -Depth 1 -InputObject $PsBoundParameters
        "$(Get-Date)`n$msg" >> ( $InputFile + ".log" )
    }

    if ( $scalar -and $result ) {
        return $result[0]
    }
    return $result
}

function Enable-SqlDevOptions {
    $sql = "
        EXEC sp_configure 'show advanced options', 1;
        RECONFIGURE;
        "

    Invoke-Sqlcmd_New -Query $sql
}

function Get-SqlInstance_Default {
    if ($SERVER.DB) {
        $servInstance = if ( $SERVER.DB.Contains('\') ) { $SERVER.DB } else { $SERVER.DB + '\DEFAULT' }
        return "SQLSERVER:\sql\" + $servInstance
    }
    throw 'Required SQL Server name and instance in $SERVER.DB'
}

function Get-SqlPathParts($path) {
    $path = Get-Path $path
    $path = $path | Update-Prefix "SQLSERVER:\"
    $path = $path.Split('\')
    function GetPathPart($i) {
        if ($path.Count -gt $i) {
            $path[$i]
        }
    }
    [PSCustomObject]@{
        Service   = GetPathPart 0
        Server    = GetPathPart 1
        Instance  = GetPathPart 2
        Area      = GetPathPart 3
        Container = GetPathPart 4
        Type      = GetPathPart 5
        Object    = GetPathPart 6
    }
}
function Get-SqlPathParts:::Test {
    Get-SqlPathParts "SQLSERVER:\sql\Cts-db-tst.MYTMBC.C360.PRIVATE\server\databases\appdb\tables"
    Get-SqlPathParts "SQLSERVER:\service\server"
}

function Get-SqlDatabaseList ([string[]]$dbNames = $UserDatabaseNamePatterns) {
    $dbList = ( Get-SqlInstance_Default ) + "\Databases" | Get-ChildItem
    $result = @()
    foreach ($dbName in $dbNames) {
        $result += $dbList | Where-Object name -like $dbName
    }
    return $result
}

function Get-SqlProcess ($dbName) {

    $sql = 'SELECT d.name as dbname, d.dbid ,spid ,login_time ,nt_domain as domain ,nt_username as username,loginame as loginName
            FROM master.dbo.sysprocesses p
            INNER JOIN master.dbo.sysdatabases d ON p.dbid = d.dbid '

    if ($dbName) { 
        $sql += "WHERE d.name = '$dbName' "
    }

    Invoke-Sqlcmd_New -query $sql
}
function Get-SqlProcess:::Example {
    $sqlProcessList  = Get-SqlProcess -db 'CCPDiagnostics'
    Write-Verbose $sqlProcessList
}

function Stop-SqlProcess {
    param ( 
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
        [int]$spID
    )
    process {
        if ($spID) {
            Invoke-Sqlcmd_New -query "kill $spID"
        }
    }
}
function Stop-SqlProcess:::Example {
    $sqlProcessList  = Get-SqlProcess -db 'CCPDiagnostics'
    $sqlProcessList.spid | Stop-SqlProcess
}

function Get-SqlBackUpFolder {
    $result = $SERVER.DBBackupFolder ?? "c:\backups"
    $result = Join-Path $result $SERVER.DB
    return $result
}

function CheckPoint-SqlDatabase ($label, [string[]]$dbNames = $UserDatabaseNamePatterns) {
    if (-not $label) {
        $label = Get-TimeStamp
    }
    
    $dbList = Get-SqlDatabaseList $dbNames

    $backUpBasePath = Get-SqlBackUpFolder
    $backUpBasePath = "$backUpBasePath\$label"
    New-Folder_EnsureExists $backUpBasePath

    foreach ($db in $DbList) {
        $backupPath = "$backUpBasePath\$($db.Name).bak"
        if (Test-Path $backupPath) {
            Remove-Item $backupPath -Force -Verbose
        }
        Backup-SqlDatabase -DatabaseObject $db -BackupFile $backupPath -CompressionOption On -BackupAction Database
    }
}
function CheckPoint-SqlDatabase:::Example {
    CheckPoint-SqlDatabase -label INIT_TBC_SP
}

function Restore-SqlCheckpoint ($label) {
    $backUpBasePath = Get-SqlBackUpFolder
    if (-not $label) {
        $lastBackupFolder = Get-ChildItem $backUpBasePath -directory | 
            Sort-Object LastWriteTime -Descending | Select-Object -First 1
        if (-not $lastBackupFolder) {
            Write-Warning 'No backups availables'
            return
        }
        $label = $lastBackupFolder.Name
    }
    $backUpBasePath = "$backUpBasePath\$label"
    if (-not (Test-Path $backUpBasePath) ) {
        Write-Warning "Backup not found '$backUpBasePath'"
    }
    $sqlInstancePath = Get-SqlInstance_Default

    $pathParts = Get-SqlPathParts $sqlInstancePath
    $sqlInstanceName = $pathParts.Server + "\" + $pathParts.Instance

    $dbNameList = Get-ChildItem $backUpBasePath -filter *.bak | 
        ForEach-Object { $_.Name | Update-Suffix '.bak' }

    foreach ( $dbName in $dbNameList ) {
        $backUpPath = "$backUpBasePath\$dbName.bak"
        $dbPath = "$sqlInstancePath\Databases\$dbName"

        Invoke-SqlSingleUser $dbPath -script {
            Restore-SqlDatabase -ServerInstance $sqlInstanceName -Database $dbName -BackupFile $backUpPath -ReplaceDatabase -Verbose -RestoreAction Database -RestrictedUser
        }
    }
}

function Invoke-SqlSingleUser($dbPath, [ScriptBlock]$script) {
    $dbPath = get-path $dbPath
    $dbname = Split-Path $dbPath -Leaf
    $sqlProcessList  = Get-SqlProcess -dbName $dbName
    $sqlProcessList.spid | Stop-SqlProcess

    $dbExists = Test-Path $dbPath
    if ($dbExists) {
        Invoke-Sqlcmd_New "ALTER DATABASE [$dbName] SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE"
    }
    try {
        Invoke-Command -ScriptBlock $script
    }
    finally {
        if (Test-Path $dbPath) {
            Invoke-Sqlcmd_New "ALTER DATABASE [$dbName] SET MULTI_USER"
        }
    }
}

function Remove-CorpAppDatabases {
    $dbList = Get-SqlDatabaseList
    foreach ($db in $dbList) {
        Invoke-SqlSingleUser $db.PSPath -script { 
            Invoke-Sqlcmd_New -Query "DROP DATABASE [$($db.Name)] "
         }
    }
}

function Import-Module_SqlServer {
    Invoke-InLocation -path $env:TEMP -script {
        Import-Module sqlserver -Global
    }
}

function Compress-CorpAppDatabases {
    Import-Module_SqlServer
    $dbList = Get-SqlDatabaseList
    $dbList | Compress-Database
}
function Compress-CorpAppDatabases:::Example {
    Import-Module .\PSModules\Common -DisableNameChecking
    Import-Module VMProvision -DisableNameChecking
    Import-AppDeploySettings
    $PSDefaultParameterValues = @{ "New-SqlCommand:QueryTimeout" = [Timespan]::FromMinutes(100) }
    Compress-CorpAppDatabases
}

function Compress-Database {
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $dbname
    )
    process {
        if (-not $dbname) { return }

        $dbnameRaw = $dbname | Update-Prefix '[' | Update-Suffix ']'
        $dbname = $dbnameRaw | Update-String_Enclose -prefix '[' -suffix ']' -conditional

        Write-Host "Compress-Database $dbname"
        
        $recoveryModel = Invoke-Sqlcmd_New -Query "SELECT name, recovCORP_model_desc FROM sys.databases WHERE name = '$dbnameRaw' ;" 
        $dblogfiles =  Invoke-Sqlcmd_New -Query "select name from $dbname.sys.database_files --where type_desc = 'LOG'"
        
        if ($dblogfiles) {
            $shrinkQuery = @("USE $dbname") + ($dblogfiles.name | 
                ForEach-Object { "DBCC SHRINKFILE(N'$_', 1 );" } ) -join "`n"
        
            if ($recoveryModel.recovCORP_model_desc -ne 'SIMPLE') {
                $shrinkQuery = "
                    ALTER DATABASE $dbname SET RECOVERY SIMPLE WITH NO_WAIT;
                    $shrinkQuery
                    ALTER DATABASE $dbname SET RECOVERY $($recoveryModel.recovCORP_model_desc) WITH NO_WAIT; "

            }

            $ignoredOutput = Invoke-Sqlcmd_New -Query $shrinkQuery;
            Write-Verbose $ignoredOutput
        }

    }
}

function Set-DatabaseRecoveryModel ($database, $recoveryModel = 'SIMPLE') {
    Write-Host "Set-DatabaseRecoveryModel $database to $recoveryModel"
    $sql = "
USE master ;
ALTER DATABASE $database SET RECOVERY $recoveryModel WITH NO_WAIT;
USE $database
DBCC SHRINKDATABASE ($database);
"

    Invoke-Sqlcmd_New $sql
}

function Set-DatabaseRecoveryModelBatch ($label, [string[]]$dbNames = $UserDatabaseNamePatterns) {
    $dbList = Get-SqlDatabaseList $dbNames
    foreach ($db in $DbList) {
        Set-DatabaseRecoveryModel -Database $db
    }
}
function Set-DatabaseRecoveryModelBatch:::Example {
    Set-DatabaseRecoveryModelBatch
}

function Get-SqlCmd_Variables {
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $file
    ) 
    process {
        $lines = get-content $file
        $result = $lines | Get-Regex_Match "^\s*(?<comment>--)?\s*:Setvar\s+(?<variable>\w+)\s+(?<value>.+)"
        $result
    }
}
function Get-SqlCmd_Variables:::Example {
    $file = "pf-sql\puml.sql"
    $result = Get-SqlCmd_Variables -file $file
    $result[0].0 | assert -eq "??????????"
}

function Set-SqlCmd_VariableOverride {
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $file,
        [hashtable]$Variables,
        $destination 
    ) 
    process {
        $currentVariables = Get-SqlCmd_Variables -file $file
        $missing = @()

        $replacements = $Variables.GetEnumerator() | ForEach-Object {  
            $varName = $_.Key
            $newLine = ":setVAR $varName $($_.Value)"
            $sqlcmdMatchList = $currentVariables | Where-Object { $_.variable -eq $varName -and -not $_.comment }
            if ($sqlcmdMatchList) {
                foreach ($sqlcmdCurrentVariable in $sqlcmdMatchList) {
                    $oldline = $sqlcmdCurrentVariable.0
                    [PSCustomObject]@{
                        ToReplace   = $oldline
                        Replacement = "--$oldline`n$newLine"
                    }
                }
            }
            else {
                $missing += $newLine
            }
        }
        $replacementsMap = $replacements | ConvertTo-HashTable -keyProp ToReplace -valueProp Replacement
        Update-FileContent_Replace_Multiple -All -replacements $replacementsMap `
            -Path $file -destination $destination
        
        if ($missing) {
            $missing +  (Get-Content $destination) | Set-Content $destination
        } 
    }
}

function Set-SqlCmd_VariableOverride:::Example {
    Set-SqlCmd_VariableOverride -file  "pf-sql\puml.sql" `
        -destination  "pf-sql\puml.generated.sql" `
        -variables @{ResultType = '"XXX"'; MisVar = "YYY"; MaxLevels=3}
}

function Invoke-Puml_Generation() {
    $sqlPwd = '1qaz.2WSX.'
    $sqluser = 'SA'
    $databaseName = 'AdventureWorks2017'
    $ConnectionString = "Server=localhost,1433;Database=$databaseName;User Id=$sqluser; Password=$sqlPwd";

    $fileSqlGenerated = "pf-sql\puml.generated.sql"
    
    Set-SqlCmd_VariableOverride -file  "pf-sql\puml.sql" `
        -destination $fileSqlGenerated `
        -variables @{
            ResultType = '"BiDirectionalRelationWithColumnsAndLabels"'
            MaxLevels = 3
        }

    $sqlResult = Invoke-Sqlcmd -ConnectionString $ConnectionString `
                    -InputFile $fileSqlGenerated `
                    -MaxCharLength ([int]::Maxvalue) -Variable $StringArray -OutputAs DataSet

    $tableSchemaJson = $sqlResult.Tables[0].Rows | ForEach-Object { $_[0] } | Join-String
    $tableSchema = $tableSchemaJson | ConvertFrom-Json
    $tableSchemaPuml = $tableSchema | ForEach-Object {
        "package $($_.TABLE_SCHEMA) { "
            $_.tables | ForEach-Object {
                "`tclass $($_.TABLE_NAME) { "
                $_.Columns | ForEach-Object {
                    "`t`t$($_.DATA_TYPE) $($_.COLUMN_NAME)"
                   }
                "`t}"
            }
        "}"
     } | Join-String -separator "`n"

    $outputlines = $sqlResult.Tables[1].Rows | ForEach-Object { $_.TableOne + $_.LabelOne + ' --|{ ' + $_.LabelMany + $_.TableMany }
    $tableRelationsPuml = $outputlines | Join-String -separator "`n"

    $templateContext = @{ 
        Schema = $tableSchemaPuml 
        Relations = $tableRelationsPuml 
    }

    Update-File_Replace_PSTemplate -file pf-Sql\Tests\diagram-puml.md.PSTemplate -TemplateContext $templateContext
}

function Start-SQL_Docker {
    $dw = '"C:\Program Files\Docker\Docker\frontend\Docker Desktop.exe" --name=settings'
    Test-Path $dw
    get-service -name com.docker.service
    get-service -name *docker*
    
    # Ensure you have a docker login first and it has been setup using the Docker Desktop App
    # Might need to sing out maually
    # $sqlDockerImage = 'microsoft/mssql-server-windows-developer'
    $sqlDockerImage = 'mcr.microsoft.com/mssql/server:2019-latest'
    $sqlContainerName = 'SQL19'
    $sqlPwd = '1qaz.2WSX.'
    $sqluser = 'SA' 
    
    docker run `
        --name $sqlContainerName `
        -p 1433:1433 `
        -e "ACCEPT_EULA=Y" `
        -e "SA_PASSWORD=$sqlPwd" `
        -v C:\Docker\SQL:/sql `
        -d $sqlDockerImage

    $sqlContainerIp = docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $sqlContainerName
    $sqlContainerId = docker inspect --format="{{.Id}}" $sqlContainerName
    

    docker kill $sqlContainerName
    docker rm $sqlContainerName

    install-module sqlserver -Scope AllUsers
    Import-Module sqlserver -Global

    $databaseName = 'master'

    $ConnectionString = "Server=localhost,1433;Database=$databaseName;User Id=$sqluser; Password=$sqlPwd";
    $sql = 'SELECT @@SERVERNAME AS ServerName;';   
    Invoke-Sqlcmd -ConnectionString $ConnectionString -Query $sql;

    # docker exec $sqlContainerName sudo yum install -y mssql-server-fts
    # docker exec $sqlContainerName sudo hostname
}

#Import-Module_sqlserver
$UserDatabaseNamePatterns = @('QA*', 'PROD*', 'DEV*','ReportServer*')