ForEach-DB.psm1
<#
.Synopsis This script executes all commands in a .sql script file in all databases present in the specified SQL instance. .Description If any error occurs the script reports the database that failed and the process continues. If -dbErrorsFileOutput or -dbWarningsFileOutput are provided, the database names of the databases that caused the errors or warnings are written the files specified in these params. If the parameter -dbExceptionFileInput is provided file specified in this parameter should contains a list of database names to be skipped when processing. This is used to avoid processing databases known not to be incompatible with the script. Authentication method used current user in Active Directory. Default level of parallelism if -parallelLevel is not provided is 4 meaning there will be 4 concurrent jobs executing the script. In case Format is not specified while a result set file output is specified, the script will attempt to derive the format type from the file extension. .Parameter Server Name of the MSSQL server to connect to. .Parameter File Filename with a .SQL script to execute against the matched databases. .Parameter dbExceptionFileInput File name containing databases to ignore when processing the command provided in -Query or -File parameter. .Parameter ParallelLevel Level of parallelism used to execute the parameters. Bu default the value of this parameter is 4, meaning 4 parallel processes are going to be spawned and the outout of this jobs is going to be collected and reported in a single pipeline. .Parameter dbWarningsFileOutput Filename where to output database names of databases where the provided script errored out with special RAISERROR used to denote a warning. The type of exception raised is as follows: RAISERROR('Not a compatible database', 20, -1) with log .Parameter dbErrorsFileOutput Filename where to output database names of databases where the provided script errored out when executing the provided statements. .Parameter DetailedErrorLogFileOutput If provided all errors resulting from SQL commands execution will be logged to this file in JSON format. The objects will contain the database name where the error was captured and the resulting error message. .Parameter Query Query/command to execute against matching databases .Parameter ResultSetFormat Format to use when query returns data (SELECT statement). Valid formats are: CSV - Comma delimited output TAB - TAB character delimited output JSON - JSON array of objects .Parameter ResultSetFileOutput File name of target file where to output result sets returned from SELECT calls. Result set will also be shown on the console. .Parameter OpenExcel If specified and if output format type is CSV or XLSX Excel will be opened with the target file specified in ResultSetFileOutput .Parameter ReRunDBFromErrorsFile If this switch is specified the script will re-run all databases contained in the file provided in DbErrorsFileOutput parameter .Parameter BindVariables Pass here an array of key value pairs of SQLCMD variables and their corresponding values. The format for each element in the array is as follows: VARIABLE=VALUE. See examples for more details .Parameter DBDriverQueryFile File name of file containing a driver query returning the server names, database names and buckets to split the list of target databases to execute the query or script. The query must return the following fields: * BUCKET * DBNAME * SERVERNAME This is the query used by default pulling databases from MSSQL metadata: SELECT (ROW_NUMBER() OVER (ORDER BY NAME) - 1) % {parallelLevel} BUCKET, NAME AS DBNAME, 'vm-pc-sql02' SERVERNAME FROM sys.databases When using a custom query make sure to use the variable $parallelLevel in order to create matching buckets for the list of databases. .Parameter ConsoleDuplex When this switch is specified together with ResultSetFileOutput the result set generated will be also output to console. This will slow down processing when using parallel level > 1. .Parameter ShowEachExecSummary This switch controls if showing summary after every database job is processed. .Parameter ShowParams This switch enables showing the values of all parameters passed to this cmdlet .Parameter ConsoleSilence Controls if displaying error messages, final summary per job and total time elapsed. If ConsoleDuplex us enabled result sets will be shown in the console ignoring ConsoleSilence. If not ResultSetFileOutput is specified result sets will also be output to console. .Example # Executes the default query SELECT DB_NAME() DBNAME in each database of server vm-pc-sql02 ForEach-DB -Server vm-pc-sql02 .Example # When using a SELECT statement it's possible to output directly to an Excel compatible .csv file. # The recommended way to do it by using Out-File in order to control the encoding. ForEach-DB -server vm-pc-sql02 -query "select db_name(), getdate()" -ResultSetFormat CSV | Out-File -FilePath .\Output.csv -Encoding UTF8 .Example # The following example runs query SELECT DB_NAME() DBNAME, GETDATE() DATE in all databases, outputs warnings, errors and detailed error messages # to specified files. After completing the generation of CSV output file will call Excel and open the target file ForEach-DB -server vm-pc-sql02 -out output.csv -format CSV -OpenExcel -Warn Warnings.log -Err Errors.log -Query "SELECT DB_NAME() DBNAME, GETDATE() DATE" -Deterror DetError.json .Example # This command binds a variable in the query body passed using the BindVariables parameter ForEach-DB -server vm-pc-sql02 -out output.csv -format CSV -Query "SELECT DB_NAME() DBNAME, `$(A) AS A" -BindVar @("A='1'") #> function IsEmptyString ($str) { $isNull = [string]::IsNullOrEmpty($str) if ($isNull) { return $true } else { return $false } } function resultSetToDelimited ($result, $columnsFile, $delimiter) { if (!(Test-Path -Path $columnsFile)) { try { Out-File -FilePath $columnsFile -InputObject ($result | ConvertTo-Csv -NoTypeInformation -Delimiter $delimiter | Select-Object -First 1 | ForEach-Object {$_ -replace '"', ""}) -Encoding utf8 } catch { # It's possible for two or more threads to try to output the header portion of a delimited file at the same time # we will ignore this errors and assume the file that has the file locked will output the headers successfully if (!$_.Exception.Message.Contains("The process cannot access the file")) { throw } } } return ($result | ConvertTo-Csv -NoTypeInformation -Delimiter $delimiter | Select-Object -Skip 1 | ForEach-Object {$_ -replace '"', ""}) } function elapsedTime($startTime) { $localElapsedTime = $startTime.Elapsed $localTotalTime = $([string]::Format("{0:d2}:{1:d2}:{2:d2}.{3:d2}", $localElapsedTime.hours, $localElapsedTime.minutes, $localElapsedTime.seconds, $localElapsedTime.milliseconds)) return $localTotalTime } function output($content, $consoleDuplex, $consoleSilence, $resultSetFileOutput) { if (($consoleDuplex -and $resultSetFileOutput) -or (!$consoleSilence -and !$resultSetFileOutput)) { Write-Output $content } if ($resultSetFileOutput) { Out-File -FilePath $resultSetFileOutput -InputObject $content -Append -Encoding utf8 } } function ForEach-DB { [CmdletBinding(PositionalBinding = $false)] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage="Enter the MSSQL server name")][Alias("Srv")][string]$server, [Parameter(Position = 1)][Alias("Q")][string]$query = 'SELECT DB_NAME() DBNAME, GETDATE() DATE', [Parameter(Position = 2)][Alias("Out")][string]$resultSetFileOutput, [Alias("Script")][string]$file, [Alias("Except", "Ignore")][string]$dbExceptionFileInput, [Alias("Para")][ValidateRange(1, 16)][int]$parallelLevel = 4, [Alias("Warn")][string]$dbWarningsFileOutput, [Alias("Err")][string]$dbErrorsFileOutput, [Alias("DetError")][string]$detailedErrorLogFileOutput, [Alias("Format")][string][ValidateSet('TAB', 'CSV', 'JSON', 'XLSX', 'PIPE')]$resultSetFormat, [Alias("OpEx")][switch]$openExcel, [Alias("ReRun")][switch]$reRunDBFromErrorsFile, [Alias("Vars", "BindVars")][string[]]$bindVariables, [Alias("Duplex", "Con")][switch]$consoleDuplex, [Alias("DBDriver")][string]$dbDriverQueryFile, [Alias("EachJobSumm", "EachJobSummary")][switch]$showEachExecSummary, [Alias("ShowParameters")][switch]$showParams, [Alias("Silent", "Silence")][switch]$consoleSilence ) function CoalesceFile($fileName) { if ((IsEmptyString $fileName) -eq $false) { try { Get-Content "${fileName}.Job-*" | Out-File "${fileName}" -Append -Encoding utf8 Remove-Item "${fileName}.Job-*" } catch { # If there's no warnings or errors other than source file not existing we will silence the exception if (!$_.Exception.Message.Contains('does not exist, or has been filtered')) { throw } # No sense creating a target blank file. Let's remove the zero bytes file created by Out-File above if ((Test-Path -Path $fileName) -and ((Get-Item $fileName).Length -le 0)) { Remove-Item $fileName } } } } # If Excel has kept XLSX files open, we will add a numeric suffix to the filename right before the extension $global:fileNameSuffix = 0 function PrepareOutputFile($fileName, [ref]$returnFileContents) { if ((IsEmptyString $fileName) -eq $false) { $fileName = [System.IO.Path]::GetFullPath($fileName) if ($global:fileNameSuffix -gt 0) { $filename = $fileName -replace '(\.)([a-zA-Z0-9]*)$', "_${global:fileNameSuffix}.`$2" } $pathExists = Test-Path -Path $fileName if ($pathExists) { if ($returnFileContents) { $returnFileContents.Value = Get-Content -Path $fileName } try { Remove-Item $fileName } catch { # If we have already increased fileNameSuffix to a value > 0 the slot MUST be open # for all ancillary files such as error log, warning log, etc. # In summary, logic to increase fileNameSuffix should only execute for the file file # which happens to be the output file. Other calls simply inherit the pre-set suffix if ($global:fileNameSuffix -gt 0) { throw } while ($true) { try { $global:fileNameSuffix++ $tmpFileName = $filename -replace '(\.)([a-zA-Z0-9]*)$', "_${global:fileNameSuffix}.`$2" if (Test-Path -Path $tmpFileName) { Remove-Item $tmpFileName } $filename = $tmpFileName break } catch { # we will stop trying new filenames at 100. Who will have 100 open Excel windows? if ($global:fileNameSuffix -gt 100) { throw } } } } } } return $fileName } function PrintParameterValues() { if($showParams) { Write-Color -Text "Starting SQL jobs execution with params:" Write-Host "" Write-color -Text "Server: ", "${server}" -Color White, Cyan Write-Color -Text "File: ", "${file}" -Color White, Cyan Write-Color -Text "Query: ", "${query}" -Color White, Cyan Write-Color -Text "ResultSetFileOutput: ", "${resultSetFileOutput}" -Color White, Cyan Write-Color -Text "DB exceptions file input: ", "$dbExceptionFileInput" -Color White, Cyan Write-Color -Text "ParallelLevel: ", "${parallelLevel}" -Color White, Cyan Write-Color -Text "WarningsFileOutput: ", "${dbWarningsFileOutput}" -Color White, Cyan Write-Color -Text "ErrorsFileOutput: ", "${dbErrorsFileOutput}" -Color White, Cyan Write-Color -Text "DetailedErrorLogFileOutput: ", "${detailedErrorLogFileOutput}" -Color White, Cyan Write-Color -Text "ResultSetFormat: ", "${resultSetFormat}" -Color White, Cyan Write-Color -Text "OpenExcel: ", "${openExcel}" -Color White, Cyan Write-Color -Text "ReRunDBFromErrorsFile: ", "${reRunDBFromErrorsFile}" -Color White, Cyan Write-Color -Text "DBDriverQueryFile: ", "${dbDriverQueryFile}" -Color White, Cyan Write-Color -Text "BindVariables: ", "${bindVariables}" -Color White, Cyan Write-Color -Text "ConsoleDuplex: ", "${consoleDuplex}" -Color White, Cyan Write-Color -Text "ShowEachExecSummary: ", "${showEachExecSummary}" -Color White, Cyan Write-Color -Text "ConsoleSilence: ", "${consoleSilence}" -Color White, Cyan Write-Color -Text "ShowParams: ", "${showParams}" -Color White, Cyan Write-Host "" } } function InitializeFileOutput() { # So far only file type requiring initialization is JSON type # In order to build a correct JSON file we enclose all returned objects in an array if ($resultSetFormat -eq "JSON") { output "[" $consoleDuplex $consoleSilence $resultSetFileOutput } } function ReencodeCSVFileAsUTF8() { # Default encoding for CSV files not liked by Excel # After completing our output to CSV we need to re-encode the file # $out = Get-Content $resultSetFileOutput # Set-Content -Value $out -Path $resultSetFileOutput -Encoding UTF8 } function FinalizeFileOutput() { switch ($resultSetFormat) { 'JSON' { # We will close the JSON array of objects and we will add an empty object at the end # given the fact workers add a comma after every object returned from the result sets output "{}]" $consoleDuplex $consoleSilence $resultSetFileOutput } 'CSV' { if (!(Test-Path -Path $resultSetFileOutput)) { return; } ReencodeCSVFileAsUTF8 if ($openExcel) { $excel = New-Object -comobject Excel.Application $workbook = $excel.Workbooks.Open($resultSetFileOutput) $excel.Visible = $true } } 'XLSX' { if (!(Test-Path -Path $resultSetFileOutput)) { return; } ReencodeCSVFileAsUTF8 $xlWorkbookDefault = 51 $xlNoChange = 1 $xlLocalSessionChanges = 2 $excel = New-Object -comobject Excel.Application if (Test-Path -Path "${resultSetFileOutput}.csv") { Remove-Item "${resultSetFileOutput}.csv" } Rename-Item -Path $resultSetFileOutput -NewName "${resultSetFileOutput}.csv" $workbook = $excel.Workbooks.Open($resultSetFileOutput + '.csv') try { $workbook.Worksheets[1].Columns["A:Z"].AutoFit() > $null $workbook.SaveAs($resultSetFileOutput, $xlWorkbookDefault, [Type]::Missing, [Type]::Missing, $false, $false, $xlNoChange, $xlLocalSessionChanges) Remove-Item "${resultSetFileOutput}.csv" } finally { if ($openExcel) { $excel.Visible = $true } else { $workbook.Close() } } } } } # The following command set is going to be executed in parallel using Start-Job $cmd = { param( $jobID ) $file = $using:file $query = $using:query $resultSetFileOutput = $using:resultSetFileOutput $dbWarningsFileOutput = $using:dbWarningsFileOutput $dbErrorsFileOutput = $using:dbErrorsFileOutput $detailedErrorLogFileOutput = $using:detailedErrorLogFileOutput $resultSetFormat = $using:resultSetFormat $dbs = $using:dbs $dbExceptionDBs = $using:dbExceptionDBs $bindVariables = $using:bindVariables $showEachExecSummary = $using:showEachExecSummary $columnsFile = $using:columnsFile $consoleSilence = $using:consoleSilence $consoleDuplex = $using:consoleDuplex $global:dbProcessedCount = 0 $global:dbSkipped = 0 $global:dbWarnings = 0 $global:dbErrors = 0 $delimitedExtensions = @{'CSV' = ','; 'XLSX' = ','; 'TAB' = '`t'; 'PIPE' = '|'} function processScript($dbs) { Write-Progress -Id $jobID -Activity "Job-${jobID}" -Status "Starting..." -PercentComplete 0 $lastPercentComplete = 0 foreach($item in $dbs) { $localStartTime = [System.Diagnostics.Stopwatch]::StartNew() $db = $item | Select-Object -exp DBNAME $server = $item | Select-Object -exp SERVERNAME $percentComplete = (100 * ($global:dbProcessedCount + $global:dbSkipped) / $dbs.Count) if ($percentComplete -ge $lastPercentComplete + 5) { $lastPercentComplete = $percentComplete Write-Progress -Id $jobID -Activity "Job-${jobID}" -Status "Processing ${db}..." -PercentComplete $percentComplete } if ($dbExceptionDBs.Contains($db)) { $global:dbSkipped++ continue } $global:dbProcessedCount++ $ErrorActionPreference = 'SilentlyContinue' try { if($query) { $result = Invoke-SQLCmd -ServerInstance $server -Database $db -Query $query -AbortOnError -MaxCharLength 65535 -Variable $bindVariables } else { $result = Invoke-SQLCmd -ServerInstance $server -Database $db -InputFile $file -AbortOnError -MaxCharLength 65535 -Variable $bindVariables } if($result) { if ($delimitedExtensions[$resultSetFormat]) { $resultTable = resultSetToDelimited $result $columnsFile $delimitedExtensions[$resultSetFormat] } elseif ($resultSetFormat -eq "JSON") { $resultTable = $result | Select-Object * -ExcludeProperty ItemArray, Table, RowError, RowState, HasErrors | ConvertTo-Json $resultTable = $resultTable -replace '(^\[[ \r\n]*)|([ \r\n]*\]$)', '' $resultTable += "," } output $resultTable $consoleDuplex $consoleSilence "${resultSetFileOutput}.Job-${jobID}" } $localTotalTime = elapsedTime $localStartTime if ($showEachExecSummary) { if (!$result) { Write-Color -Text "Job-${jobID}: ", "${db}", "... OK. Elapsed: ${localTotalTime}" -Color White, Cyan, White } else { $recCount = ($result | Measure).Count Write-Color -Text "Job-${jobID}: ", "${db}", "... Retrieved ", "${recCount}", " records. Elapsed: ${localTotalTime}" -Color White, Cyan, White, Green, White } } } catch { $localTotalTime = elapsedTime $localStartTime if ($_.Exception.Message.Contains('user error 50000, severity 20')) { $global:dbWarnings++ if ($showEachExecSummary) { Write-Color -Text "Job-${jobID}: ", "${db}...", " WARNING", ": Database incompatible. Elapsed: ${localTotalTime}" -Color White, Cyan, Yellow, White if ((IsEmptyString $dbWarningsFileOutput) -eq $false) { Out-File -FilePath "${dbWarningsFileOutput}.Job-${jobID}" -InputObject "${db}" -Append -Encoding utf8 } } } else { $global:dbErrors++ if (!$consoleSilence) { Write-Color -Text "Job-${jobID}: ", "${db}...", " ERROR", ": script failed. Elapsed: ${localTotalTime}" -Color White, Cyan, Red, White } if ((IsEmptyString $dbErrorsFileOutput) -eq $false) { Out-File -FilePath "${dbErrorsFileOutput}.Job-${JobID}" -InputObject "${db}" -Append -Encoding utf8 } if ((IsEmptyString $detailedErrorLogFileOutput) -eq $false) { $props = @{ Database = $db ErrorMessage = $_.Exception.Message } $ErrorMsgObj = New-Object psobject -Property $props $errorAsJson = ConvertTo-Json -InputObject $ErrorMsgObj Out-File -FilePath "${detailedErrorLogFileOutput}.Job-${JobID}" -InputObject "${errorAsJson}," -Append -Encoding utf8 } } } finally { $ErrorActionPreference = 'Stop' } } } processScript $dbs Write-Progress -Id $jobID -Activity "Job-${jobID}" -Status "Completed" -PercentComplete 100 -Completed if (!$consoleSilence) { Write-Color -Text "Job-${jobID} finished. ", "Summary: ", "processed ", "${dbProcessedCount}", ", Skipped ", "${dbSkipped}", ", Warnings ", "${dbWarnings}", ", Errrors ", "${dbErrors}", "." -Color White, Green, White, Green, White, Yellow, White, Yellow, White, Red, White } } # Environment initialization $ErrorActionPreference = "Stop" # Set the current directory on the .NET layer so we can normalize file names when calling PrepareOutputFile [System.IO.Directory]::SetCurrentDirectory(((Get-Location -PSProvider FileSystem).ProviderPath)) # Paramaters setup and verification if ($consoleSilence -and ($consoleDuplex -or $showEachExecSummary)) { throw "-ConsoleSilence can't be specified together with -ConsoleDuplex or -ShowEachExecSummary parameters" } if ((IsEmptyString $query) -eq $false -and (IsEmptyString $file) -eq $false) { throw "-Query and -File parameters can be specified at the same time" } if ((IsEmptyString $file) -eq $false) { $file = [System.IO.Path]::GetFullPath($file) } $resultSetFileOutput = PrepareOutputFile $resultSetFileOutput $dbWarningsFileOutput = PrepareOutputFile $dbWarningsFileOutput [string[]]$erroredOutDBs = @() $dbErrorsFileOutput = PrepareOutputFile $dbErrorsFileOutput ([ref]$erroredOutDBs) if (((IsEmptyString $dbErrorsFileOutput -eq $false) -or !(Test-Path -Path $dbErrorsFileOutput)) -and $reRunDBFromErrorsFile) { throw "-DBErrorsFileOutput not provided or error file not found. -ReRunDBFromErrorsFile was set but no error file was loaded. Can't run." } $detailedErrorLogFileOutput = PrepareOutputFile $detailedErrorLogFileOutput $columnsFile = PrepareOutputFile "output_columns.csv" if ((IsEmptyString $resultSetFormat) -eq $true) { if ((IsEmptyString $resultSetFileOutput) -eq $false) { if (($resultSetFileOutput -match '\.([a-zA-Z0-9]*)$') -and (@('TAB', 'CSV', 'XLSX', 'JSON', 'PIPE')) -contains $Matches[1]) { $resultSetFormat = $Matches[1] } else { $resultSetFormat = 'CSV' } } else { $resultSetFormat = 'TAB' } } if (!(@('XLSX', 'CSV') -contains $resultSetFormat) -and $openExcel) { throw "-OpenExcel option can be used only when -ResultSetFormat is equal to XLSX or CSV" } # Finished parameters setup and verification # Detailed error file will be an array of JSON objects. Let's open the array if ((IsEmptyString $detailedErrorLogFileOutput) -eq $false) { Out-File -FilePath $detailedErrorLogFileOutput -InputObject "[" -Encoding utf8 } if ((IsEmptyString $dbExceptionFileInput) -eq $false) { $dbExceptionFileInput = [System.IO.Path]::GetFullPath($dbExceptionFileInput) [string[]]$dbExceptionDBs = Get-Content -Path $dbExceptionFileInput } else { $dbExceptionDBs = @() } # Let's retrieve the list of databases in the server if ((IsEmptyString $dbDriverQueryFile) -eq $false) { if (Test-Path -Path $dbDriverQueryFile) { $dbsQuery = Get-Content -Path $dbDriverQueryFile -Raw # User should use the token {parallelLevel} to create buckets matching the number of parallelism $dbsQuery = $dbsQuery -replace '{parallelLevel}', "${parallelLevel}" } else { throw "Could not find driver file ${dbDriverQueryFile}" } } else { $dbsQuery = "SELECT (ROW_NUMBER() OVER (ORDER BY NAME) - 1) % ${parallelLevel} BUCKET, NAME AS DBNAME, '${server}' SERVERNAME FROM sys.databases" } $databases = Invoke-Sqlcmd -ServerInstance ${server} -Database master -query ${dbsQuery} if ($reRunDBFromErrorsFile -and $erroredOutDBs.Count -gt 0) { $databases = $databases.Where({($erroredOutDBs -contains ($_ | Select-Object -exp DBNAME)) -eq $true}) } $db_splits = @(0..($parallelLevel - 1)) $db_splits[0] = $databases for ($i = 0; $i -lt $parallelLevel - 1; $i++) { $db_splits[$i], $db_splits[$i + 1] = $db_splits[$i].Where({($_ | Select-Object -exp BUCKET) -eq $i}, 'Split') } $startTime = [System.Diagnostics.Stopwatch]::StartNew() PrintParameterValues InitializeFileOutput $consoleDuplex $consoleSilence $resultSetFileOutput # Let's start and process the parallel jobs output [console]::TreatControlCAsInput = $true try { # Let's stop abandoned jobs that never properly started Get-Job -State NotStarted | Stop-Job Get-Job | Where({($_ | Select-Object -exp State) -ne 'Running'}) | Remove-Job $jobID = 1 # env:\MODULE_PATH environment variable is used in order to be able to import functions # needed within the codeblock used below when callint Start-Job. Essentially this trick # allows the parallel workers to use specific functions from the main module (this module) if (Test-Path -Path env:\MODULE_PATH) { Remove-Item -Path env:\MODULE_PATH } New-Item -Name MODULE_PATH -Value $MyInvocation.MyCommand.Module.Path -Path env:\ > $null try { foreach($dbs in $db_splits) { Start-Job -ScriptBlock $cmd -ArgumentList ($jobID++) -InitializationScript { Import-Module -Name $env:MODULE_PATH -DisableNameChecking } > $null } $runningJobs = $true while ($runningJobs) { $runningJobs = Get-Job -State Running # Randomize the list so screen updates is not always from top to bottom $runningJobs = $runningJobs | Sort-Object {Get-Random} if($runningJobs) { if ($resultSetFileOutput -and !$consoleDuplex) { Receive-Job $runningJobs > $null } else { Receive-Job $runningJobs } Wait-Job $runningJobs -Timeout 0 > $null # if Ctrl-C is pressed we will cancel all of our child jobs if ([console]::KeyAvailable) { $key = [system.console]::readkey($true) if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) { Write-Color -Text "Terminating..." -Color Red $runningJobs | Stop-Job break } } } } } finally { Remove-Item -Path env:\MODULE_PATH } } finally { [console]::TreatControlCAsInput = $false } # Even though our execution run has completed we may not have fetched all output from the child jobs $jobs = Get-Job -HasMoreData $true if ($jobs) { if ($consoleDuplex) { $jobs | Receive-Job } else { $jobs | Receive-Job > $null } } if ($resultSetFileOutput -and (Test-Path -Path $columnsFile)) { Get-Content -Path $columnsFile | Out-File -FilePath $resultSetFileOutput -Encoding utf8 Remove-Item $columnsFile } CoalesceFile $resultSetFileOutput CoalesceFile $dbWarningsFileOutput CoalesceFile $dbErrorsFileOutput CoalesceFile $detailedErrorLogFileOutput if ((IsEmptyString $detailedErrorLogFileOutput) -eq $false) { # We will close our JSON array of detailed errors. Note the need for {} # due to the extra comma after the last error object logged Out-File -FilePath $detailedErrorLogFileOutput -InputObject "{}]" -Append -Encoding utf8 } FinalizeFileOutput # Final step: display total elapsed time $elapsedTime = $startTime.Elapsed $totalTime = $([string]::Format("{0:d2}:{1:d2}:{2:d2}.{3:d2}", $elapsedTime.hours, $elapsedTime.minutes, $elapsedTime.seconds, $elapsedTime.milliseconds)) if (!$consoleSilence) { Write-Host "" Write-Color -Text "Total elapsed ", "${totalTime}" -Color White, Cyan } } Export-ModuleMember -Function ForEach-DB # The following functions are used from within the codeblock used when calling Start-Job Export-ModuleMember -Function IsEmptyString Export-ModuleMember -Function resultSetToDelimited Export-ModuleMember -Function elapsedTime Export-ModuleMember -Function output |