This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
PowerShell tooling to migrate SQL Server databases between two Always On Availability Groups (AGs) using the dbatools module. It supports:
- Full migration (steps 1–10): removes from source AG, backs up, restores, joins destination AG, waits for sync.
- Destination cleanup only (
-CleanAGDestination, steps 1–5): removes and drops databases from AG_Destination without touching the source. - Parallel migration:
migrate-agdatabasesinparallel.ps1runs one background job per database for concurrent migrations.
- PowerShell 5.1 (Windows PowerShell — no PS 7+ features like
ForEach-Object -Parallelor null-coalescing??). - The
dbatoolsmodule installed. - Windows Authentication with sysadmin rights on all SQL Server nodes.
- A UNC backup path accessible from all nodes with sufficient free disk space (at least N × average database size when running in parallel).
Sequential migration:
.\Migrate-AgDatabase.ps1 `
-DatabaseName "DB1", "DB2" `
-AG_Source "agsql" `
-AG_Destination "agsql2" `
-Node1_source "sql2" `
-Node2_source "sql3" `
-Node1_destination "sql4" `
-Node2_destination "sql5" `
-BackupPath "\\SQL4\temp"Parallel migration (one background job per database):
.\migrate-agdatabasesinparallel.ps1 `
-DatabaseName "DB1", "DB2", "DB3" `
-MaxParallel 2 `
-AG_Source "agsql" `
-AG_Destination "agsql2" `
-Node1_source "sql2" `
-Node2_source "sql3" `
-Node1_destination "sql4" `
-Node2_destination "sql5" `
-BackupPath "\\SQL4\temp"Optional switches for both scripts: -TakeSourceOffline, -CleanAGDestination, -VerboseLogging.
Logs are written to .\logs\migrate-agdatabase_<timestamp>_<PID>.log (PID suffix prevents filename collisions when jobs start within the same second).
migrate-agdatabase.ps1— sequential migration; processes all databases in a foreach loop.migrate-agdatabasesinparallel.ps1— parallel wrapper; spawns oneStart-Jobper database, each callingmigrate-agdatabase.ps1with a single-DatabaseName. Accepts the same parameters plus-MaxParallel [int](0 = all databases at once).
migrate-agdatabase.ps1 dot-sources shared-functions.ps1 at startup, initialises the log file, then:
- Runs a one-time preflight (AG existence, backup path reachability).
- Loops over each database name, executing steps 1–10 (or 1–5 if
-CleanAGDestination).
Dot-sourced at the top of the main script. Provides:
Write-Log— writes[timestamp] [LEVEL] messageto console (coloured) and to$script:LogFilePath. Levels:INF,WAR,ERR,VRB. UsesWrite-Host(stream 6) so output is captured by$job.ChildJobs[0].Informationin background jobs.Invoke-DbaWithLog— wraps a scriptblock; when$Config.Defaults.VerboseLoggingis true it captures dbatools verbose output and pipes it throughWrite-Log.Format-Elapsed— formats aStopwatchasHH:mm:ss.
- Uses
[System.Collections.Generic.Queue[string]]to throttle job start when-MaxParallelis set. - Streams job output live to console by polling
$job.ChildJobs[0].Informationwith an index tracker ($infoIndex) —Receive-Jobis not used because it drains the stream and prevents re-reading. @()forces arrays onWhere-Objectresults before.Countto avoidSet-StrictMode -Version Latestthrowing on$null.Count.- Wraps each job's script call in try/catch so fatal exceptions are written via
Write-Host(visible viaFlush-JobOutput) before re-throwing. - In the summary, checks both
$job.JobStateInfo.Reasonand$job.ChildJobs[0].Errorto surface the terminating exception.
- Step 7 uses raw T-SQL instead of
Restore-DbaDatabase:Restore-DbaDatabasecallsTest-DbaBackupInformation, which refuses to restore when leftover physical files exist even with-WithReplace. DirectInvoke-DbaQuerybypasses this check. WITH MOVEin restore: handles differences in default data/log paths between source and destination SQL Server versions (e.g. MSSQL16 → MSSQL17). Target paths are read viaGet-DbaDefaultPath; file names come fromRead-DbaBackupHeader.- Orphaned file cleanup: after DROP, physical files are removed over the admin share (
\\server\C$\...) because DROP DATABASE leaves files on disk when the database was in RESTORING state or had been taken offline.Remove-OrphanedDbFilesretries up to 5 times with a 3-second sleep because SQL Server may briefly hold file handles after DROP returns. The call sites are wrapped in try/catch — on failure a WARN is logged with the paths for manual cleanup and the migration continues (the DROP itself succeeded). - Background jobs don't inherit network tokens:
Start-Jobprocesses cannot access admin shares (\\server\C$\...) with the interactive user's credentials. The non-fatal try/catch on file cleanup handles this; manual deletion is the fallback. $PIDin log filename: prevents log file collisions when multiple jobs start within the same second.$Confighashtable: passed implicitly through script scope; currently only holdsDefaults.VerboseLogging.Invoke-DbaWithLogreads it from the caller's scope.Set-StrictMode -Version Latest+$ErrorActionPreference = 'Stop': all unhandled errors terminate immediately; no silent failures.