functions/private/Invoke-DBSafeShrink-functions.ps1

function GetFreeSpace($SqlCmdArguments, $Database) {
    $sql = "
        SELECT DB_NAME() AS db_name,
            df.[name] AS file_name,
            fn.[size] AS current_size_mb,
            fn.[space_used] AS used_space_mb,
            fn.[size] - fn.[space_used] AS free_space_mb
        FROM [$Database].sys.database_files df
        CROSS APPLY (
            SELECT CAST(CAST(FILEPROPERTY(df.name,'SpaceUsed') AS INT) / 128.0 AS INT) AS [space_used],
                CAST(df.[size] / 128.0 AS INT) AS [size]
 
        ) fn
        WHERE [df].[type_desc] = 'ROWS';"


    Write-Verbose $sql
    return Invoke-Sqlcmd @SqlCmdArguments -Query "$sql" -OutputAs DataRows
}

function PeformFileOperation($SqlCmdArguments, $sql) {
    # A t-log backup could be occuring which would cause this script to break, so lets pause for a bit to try again, if we get that specific error
    # https://blog.sqlauthority.com/2014/11/09/sql-server-fix-error-msg-3023-level-16-state-2-backup-file-manipulation-operations-such-as-alter-database-add-file-and-encryption-changes-on-a-database-must-be-serialized/
    $tryAgain = $false
    $tryAgainCount = 0
    $sleep = 15
    [int]$tryAgainCountMax = (300 / $sleep) # 300 (seconds) == 5 minutes wait, unless it succeeds

    do {
        $tryAgain = $false
        try {
            Write-Verbose "$sql"
            Invoke-Sqlcmd @SqlCmdArguments -Query $sql -ErrorAction Stop
        } catch {
            $msg = $_.Exception.GetBaseException().Message
            if (++$tryAgainCount -lt $tryAgainCountMax -and $msg -imatch "Backup,\s+file\s+manipulation\s+operations\s+\(such\s+as .*?\)\s+and\s+encryption\s+changes\s+on\s+a\s+database\s+must\s+be\s+serialized\.") {
                Write-Warning "BACKUP SERIALIZATION ERROR, PAUSING FOR ($sleep) SECONDS, AND TRYING AGAIN. TRY: $($tryAgainCount + 1)"
                $tryAgain = $true
                Start-Sleep -Seconds $sleep
            } else {
                # not the exception about a backup blocking us, or we are out of retries, so bail
                throw
            }
        }
    } while ($tryAgain)
}


function ShrinkFile($SqlCmdArguments, [string] $fileName, [int]$size, [int]$targetSizeMB = 5, [int]$timeout, [int]$ShrinkIncrementMB = 0) {
    # shrink N-gb at a a time
    [int]$shrinkIncrement = $ShrinkIncrementMB

    if ($shrinkIncrement -lt 50 -or $shrinkIncrement -gt 10000) {
        switch ($size) {
            {$_ -le 10000 } {
                $shrinkIncrement = 1000 # < 10 gb
            }
            {$_ -gt 10000 -and $_ -le 100000 } {
                $shrinkIncrement = 2500 # 10 gb - 100 gb
            }
            {$_ -gt 100000 -and $_ -le 1000000 } {
                $shrinkIncrement = 5000 # 100 gb - 1 tb
            }
            {$_ -gt 1000000 } {
                $shrinkIncrement = 7500 # > 1 tb
            }
            default {
                $shrinkIncrement = 5000
            }
        }
    }

    # set our target size to 75% of the original, to reduce file growths needed. This size should be the smallest of the files if there were multiple
    $targetSize = [Math]::Max(1, $targetSizeMB * 0.75)
    $rawsql = "DBCC SHRINKFILE([$fileName], {0}) WITH NO_INFOMSGS;"

    if ($size -gt 50 -or $size -gt $targetSize) {
        if ($shrinkIncrement -gt 0) {
            for($x = $size; $x -gt $targetSize; $x -= $shrinkIncrement) {
                $size = $x
                $sql = $rawsql -f $x
                Write-Information "[$($sw.Elapsed.ToString($swFormat))] PERFORMING: $sql" -ForegroundColor Green
                Write-Verbose $sql
                Invoke-Sqlcmd @SqlCmdArguments -Query "$sql" -QueryTimeout $timeout
            }
        }
    }

    $sql = $rawsql -f $targetSize
    Write-Information "[$($sw.Elapsed.ToString($swFormat))] PERFORMING FINAL SHRINK: $sql" -ForegroundColor Green
    Write-Verbose $sql
    Invoke-Sqlcmd @SqlCmdArguments -Query "$sql" -QueryTimeout $shrinkTimeOut
}