Skip to content

Commit 3e09e73

Browse files
Start-DbaMigration - Add -SetSourceOffline to set databases offline during migration (#10013)
1 parent 591cb80 commit 3e09e73

4 files changed

Lines changed: 126 additions & 13 deletions

File tree

public/Copy-DbaDatabase.ps1

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ function Copy-DbaDatabase {
9393
Sets source databases to read-only before migration to prevent data changes during the process.
9494
Use this to ensure data consistency when databases must remain accessible at the source during migration.
9595
96+
.PARAMETER SetSourceOffline
97+
Sets source databases offline before migration to prevent any connections during the process.
98+
Use this to ensure complete isolation when databases must be completely inaccessible at the source during migration.
99+
When combined with -Reattach, databases are brought back online after being reattached to the source.
100+
96101
.PARAMETER ReuseSourceFolderStructure
97102
Maintains the exact file path structure from the source instance on the destination.
98103
Use this when destination servers have identical drive layouts or when preserving specific organizational folder structures.
@@ -130,10 +135,6 @@ function Copy-DbaDatabase {
130135
Use this to distinguish migrated databases (e.g., 'DEV_' prefix for development copies).
131136
Cannot be used together with -NewName parameter.
132137
133-
.PARAMETER SetSourceOffline
134-
Sets source databases to offline status after successful migration.
135-
Use this for cutover scenarios where source databases should be unavailable after migration.
136-
137138
.PARAMETER KeepCDC
138139
Preserves Change Data Capture (CDC) configuration and data during migration.
139140
Use this when destination databases need to maintain CDC tracking for auditing or replication.
@@ -258,6 +259,9 @@ function Copy-DbaDatabase {
258259
[parameter(ParameterSetName = "DbBackup")]
259260
[parameter(ParameterSetName = "DbAttachDetach")]
260261
[switch]$SetSourceReadOnly,
262+
[parameter(ParameterSetName = "DbBackup")]
263+
[parameter(ParameterSetName = "DbAttachDetach")]
264+
[switch]$SetSourceOffline,
261265
[Alias("ReuseFolderStructure")]
262266
[parameter(ParameterSetName = "DbBackup")]
263267
[parameter(ParameterSetName = "DbAttachDetach")]
@@ -276,7 +280,6 @@ function Copy-DbaDatabase {
276280
[switch]$KeepCDC,
277281
[parameter(ParameterSetName = "DbBackup")]
278282
[switch]$KeepReplication,
279-
[switch]$SetSourceOffline,
280283
[string]$NewName,
281284
[string]$Prefix,
282285
[switch]$Force,
@@ -1172,6 +1175,7 @@ function Copy-DbaDatabase {
11721175
}
11731176

11741177
$sourceDbReadOnly = $sourceServer.Databases[$dbName].ReadOnly
1178+
$sourceDbOffline = $sourceServer.Databases[$dbName].Status -like "*Offline*"
11751179

11761180
if ($SetSourceReadOnly) {
11771181
If ($Pscmdlet.ShouldProcess($source, "Set $dbName to read-only")) {
@@ -1184,6 +1188,18 @@ function Copy-DbaDatabase {
11841188
}
11851189
}
11861190

1191+
if ($SetSourceOffline -and $DetachAttach) {
1192+
# For DetachAttach, set offline before detach to kill connections
1193+
If ($Pscmdlet.ShouldProcess($source, "Set $dbName to offline")) {
1194+
Write-Message -Level Verbose -Message "Setting database to offline."
1195+
try {
1196+
$result = Set-DbaDbState -SqlInstance $sourceServer -Database $dbName -Offline -EnableException -Force
1197+
} catch {
1198+
Stop-Function -Continue -Message "Couldn't set database to offline. Aborting routine for this database" -ErrorRecord $_
1199+
}
1200+
}
1201+
}
1202+
11871203
if ($BackupRestore) {
11881204
if ($UseLastBackup) {
11891205
$whatifmsg = "Gathering last backup information for $dbName from $Source and restoring"
@@ -1235,6 +1251,17 @@ function Copy-DbaDatabase {
12351251
$backupCollection += $backupTmpResult
12361252
}
12371253
}
1254+
1255+
# For BackupRestore, set source offline after backup completes but before restore
1256+
if ($SetSourceOffline) {
1257+
Write-Message -Level Verbose -Message "Setting source database $dbName to offline after backup."
1258+
try {
1259+
$null = Set-DbaDbState -SqlInstance $sourceServer -Database $dbName -Offline -EnableException -Force
1260+
} catch {
1261+
Stop-Function -Continue -Message "Couldn't set database to offline after backup. Aborting routine for this database" -ErrorRecord $_
1262+
}
1263+
}
1264+
12381265
Write-Message -Level Verbose -Message "Reuse = $ReuseSourceFolderStructure."
12391266
try {
12401267
$msg = $null
@@ -1305,6 +1332,16 @@ function Copy-DbaDatabase {
13051332
}
13061333
}
13071334

1335+
if ($SetSourceOffline) {
1336+
If ($Pscmdlet.ShouldProcess($destServer.Name, "Set $dbName to online after source was set to offline")) {
1337+
try {
1338+
$null = Set-DbaDbState -SqlInstance $destServer -Database $dbName -Online -EnableException -Force
1339+
} catch {
1340+
Stop-Function -Message "Couldn't set $dbName to online on $($destserver.Name)" -ErrorRecord $_
1341+
}
1342+
}
1343+
}
1344+
13081345
$dbFinish = Get-Date
13091346
if ($NoRecovery -eq $false) {
13101347
If ($Pscmdlet.ShouldProcess($destServer.Name, "Setting db owner to $dbowner for $destinationDbName")) {
@@ -1361,6 +1398,14 @@ function Copy-DbaDatabase {
13611398
Stop-Function -Message "Couldn't set database to read-only" -ErrorRecord $_
13621399
}
13631400
}
1401+
1402+
if ($SetSourceOffline -or $sourceDbOffline) {
1403+
try {
1404+
$result = Set-DbaDbState -SqlInstance $sourceServer -Database $dbName -Offline -EnableException -Force
1405+
} catch {
1406+
Stop-Function -Message "Couldn't set database to offline" -ErrorRecord $_
1407+
}
1408+
}
13641409
Write-Message -Level Verbose -Message "Successfully reattached $dbName to $source."
13651410
} else {
13661411
Write-Message -Level Verbose -Message "Could not reattach $dbName to $source."
@@ -1463,12 +1508,6 @@ function Copy-DbaDatabase {
14631508
$copyDatabaseStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
14641509
}
14651510

1466-
if ($SetSourceOffline -and $copyDatabaseStatus.Status -eq "Successful" -and $sourceServer.databases[$dbName].status -notlike '*offline*') {
1467-
if ($Pscmdlet.ShouldProcess($source, "Setting $dbName offline")) {
1468-
Set-DbaDbState -SqlInstance $sourceServer -Database $dbName -Offline -Force
1469-
}
1470-
}
1471-
14721511
$dbTotalTime = $dbFinish - $dbStart
14731512
$dbTotalTime = ($dbTotalTime.ToString().Split(".")[0])
14741513

public/Copy-DbaServerRole.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function Copy-DbaServerRole {
111111
return
112112
}
113113

114-
$sourceRoles = $sourceServer.Roles | Where-Object IsFixedRole -eq $false
114+
$sourceRoles = $sourceServer.Roles | Where-Object { $PSItem.IsFixedRole -eq $false -and $PSItem.Name -ne "public" }
115115

116116
if ($Force) { $ConfirmPreference = "none" }
117117
}

public/Start-DbaMigration.ps1

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ function Start-DbaMigration {
9797
This prevents data changes during migration and helps ensure data consistency.
9898
When combined with -Reattach, databases remain read-only after being reattached to the source.
9999
100+
.PARAMETER SetSourceOffline
101+
Sets migrated databases offline on the source server before migration begins.
102+
This prevents any connections to the source databases during migration, ensuring complete isolation.
103+
When combined with -Reattach, databases are brought back online after being reattached to the source.
104+
100105
.PARAMETER AzureCredential
101106
Specifies the name of a SQL Server credential for accessing Azure Storage when SharedPath points to an Azure Storage account.
102107
The credential must already exist on both source and destination servers with proper access to the Azure Storage container.
@@ -201,6 +206,11 @@ function Start-DbaMigration {
201206
202207
Migrates databases using detach/copy/attach. Reattach at source and set source databases read-only. Also migrates everything else.
203208
209+
.EXAMPLE
210+
PS C:\> Start-DbaMigration -Verbose -Source sqlcluster -Destination sql2016 -BackupRestore -SharedPath "\\fileserver\backups" -SetSourceOffline
211+
212+
Migrates databases using backup/restore method. Sets source databases offline before migration to prevent any connections during the process.
213+
204214
.EXAMPLE
205215
PS C:\> $PSDefaultParameters = @{
206216
>> "dbatools:Source" = "sqlcluster"
@@ -225,6 +235,7 @@ function Start-DbaMigration {
225235
[switch]$WithReplace,
226236
[switch]$NoRecovery,
227237
[switch]$SetSourceReadOnly,
238+
[switch]$SetSourceOffline,
228239
[switch]$ReuseSourceFolderStructure,
229240
[switch]$IncludeSupportDbs,
230241
[PSCredential]$SourceSqlCredential,
@@ -392,6 +403,7 @@ function Start-DbaMigration {
392403
Destination = $Destination
393404
DestinationSqlCredential = $DestinationSqlCredential
394405
SetSourceReadOnly = $SetSourceReadOnly
406+
SetSourceOffline = $SetSourceOffline
395407
ReuseSourceFolderStructure = $ReuseSourceFolderStructure
396408
AllDatabases = $true
397409
Force = $Force
@@ -424,7 +436,9 @@ function Start-DbaMigration {
424436
}
425437
}
426438

427-
Copy-DbaDatabase @CopyDatabaseSplat
439+
Copy-DbaDatabase @CopyDatabaseSplat | ForEach-Object {
440+
$PSItem
441+
}
428442
}
429443

430444
if ($Exclude -notcontains 'Logins') {

tests/Start-DbaMigration.Tests.ps1

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Describe $CommandName -Tag UnitTests {
2020
"WithReplace",
2121
"NoRecovery",
2222
"SetSourceReadOnly",
23+
"SetSourceOffline",
2324
"ReuseSourceFolderStructure",
2425
"IncludeSupportDbs",
2526
"SourceSqlCredential",
@@ -194,4 +195,63 @@ Describe $CommandName -Tag IntegrationTests {
194195
$sourceDbs.Owner | Should -Be $destDbs.Owner
195196
}
196197
}
198+
199+
Context "When using SetSourceOffline parameter" {
200+
BeforeAll {
201+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
202+
203+
# Create a dedicated database for offline testing
204+
$offlineTestDb = "dbatoolsci_offline$random"
205+
206+
# Clean up any existing test database
207+
Remove-DbaDatabase -SqlInstance $TestConfig.instance2, $TestConfig.instance3 -Database $offlineTestDb -ErrorAction SilentlyContinue
208+
209+
# Create test database on source
210+
$splatCreateOfflineDb = @{
211+
SqlInstance = $TestConfig.instance2
212+
Query = "CREATE DATABASE $offlineTestDb; ALTER DATABASE $offlineTestDb SET AUTO_CLOSE OFF WITH ROLLBACK IMMEDIATE"
213+
}
214+
Invoke-DbaQuery @splatCreateOfflineDb
215+
216+
# Create a backup so UseLastBackup can find it
217+
$null = Backup-DbaDatabase -SqlInstance $TestConfig.instance2 -Database $offlineTestDb -BackupDirectory $backupPath
218+
219+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
220+
221+
# Run migration with SetSourceOffline
222+
$splatOfflineMigration = @{
223+
Source = $TestConfig.instance2
224+
Destination = $TestConfig.instance3
225+
BackupRestore = $true
226+
UseLastBackup = $true
227+
SetSourceOffline = $true
228+
Force = $true
229+
Exclude = "Logins", "SpConfigure", "SysDbUserObjects", "AgentServer", "CentralManagementServer", "ExtendedEvents", "PolicyManagement", "ResourceGovernor", "Endpoints", "ServerAuditSpecifications", "Audits", "LinkedServers", "SystemTriggers", "DataCollector", "DatabaseMail", "BackupDevices", "Credentials", "StartupProcedures", "MasterCertificates"
230+
}
231+
$offlineResults = Start-DbaMigration @splatOfflineMigration
232+
}
233+
234+
AfterAll {
235+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
236+
237+
# Bring database back online before cleanup
238+
Set-DbaDbState -SqlInstance $TestConfig.instance2 -Database $offlineTestDb -Online -Force -ErrorAction SilentlyContinue
239+
240+
# Clean up
241+
Remove-DbaDatabase -SqlInstance $TestConfig.instance2, $TestConfig.instance3 -Database $offlineTestDb -ErrorAction SilentlyContinue
242+
243+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
244+
}
245+
246+
It "Should set source database offline after successful migration" {
247+
$sourceDb = Get-DbaDatabase -SqlInstance $TestConfig.instance2 -Database $offlineTestDb
248+
$sourceDb.Status | Should -BeLike "*Offline*"
249+
}
250+
251+
It "Should have destination database online" {
252+
$destDb = Get-DbaDatabase -SqlInstance $TestConfig.instance3 -Database $offlineTestDb
253+
$destDb | Should -Not -BeNullOrEmpty
254+
$destDb.Status | Should -Be "Normal"
255+
}
256+
}
197257
}

0 commit comments

Comments
 (0)