Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion public/Copy-DbaAgentJob.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ function Copy-DbaAgentJob {
Overwrites existing jobs on the destination server and automatically sets missing job owners to the 'sa' login.
Use this when you need to replace existing jobs or when source job owners don't exist on the destination server during migrations.

.PARAMETER UseLastModified
When enabled, compares the last modification date (date_modified) from msdb.dbo.sysjobs between source and destination instances.
Jobs are only copied or updated if the source job is newer than the destination job. This provides intelligent synchronization:
- If job doesn't exist on destination: creates it
- If source date_modified is newer: drops and recreates the job
- If dates are equal: skips the job
- If destination is newer: skips with a warning
Use this for incremental synchronization scenarios where you want to keep jobs up-to-date without unconditionally overwriting them.

.PARAMETER EnableException
By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
Expand Down Expand Up @@ -92,6 +101,11 @@ function Copy-DbaAgentJob {
PS C:\> Get-DbaAgentJob -SqlInstance sqlserver2014a | Where-Object Category -eq "Report Server" | Copy-DbaAgentJob -Destination sqlserver2014b

Copies all SSRS jobs (subscriptions) from AlwaysOn Primary SQL instance sqlserver2014a to AlwaysOn Secondary SQL instance sqlserver2014b

.EXAMPLE
PS C:\> Copy-DbaAgentJob -Source sqlserver2014a -Destination sqlserver2014b -UseLastModified

Copies jobs from sqlserver2014a to sqlserver2014b, but only creates new jobs or updates existing jobs where the source job has a newer date_modified timestamp. Jobs with matching timestamps are skipped.
#>
[cmdletbinding(DefaultParameterSetName = "Default", SupportsShouldProcess, ConfirmImpact = "Medium")]
param (
Expand All @@ -105,6 +119,7 @@ function Copy-DbaAgentJob {
[switch]$DisableOnSource,
[switch]$DisableOnDestination,
[switch]$Force,
[switch]$UseLastModified,
[parameter(ValueFromPipeline)]
[Microsoft.SqlServer.Management.Smo.Agent.Job[]]$InputObject,
[switch]$EnableException
Expand Down Expand Up @@ -228,7 +243,82 @@ function Copy-DbaAgentJob {
}

if ($destJobs.name -contains $serverJob.name) {
if ($force -eq $false) {
if ($UseLastModified) {
# Query date_modified from both source and destination using parameterized queries
try {
$splatSourceDate = @{
SqlInstance = $sourceserver
Database = "msdb"
Query = "SELECT date_modified FROM dbo.sysjobs WHERE name = @jobName"
SqlParameter = @{ jobName = $jobName }
}
$sourceDate = (Invoke-DbaQuery @splatSourceDate).date_modified

$splatDestDate = @{
SqlInstance = $destServer
Database = "msdb"
Query = "SELECT date_modified FROM dbo.sysjobs WHERE name = @jobName"
SqlParameter = @{ jobName = $jobName }
}
$destDate = (Invoke-DbaQuery @splatDestDate).date_modified

if ($null -eq $sourceDate -or $null -eq $destDate) {
Write-Message -Level Warning -Message "Could not retrieve date_modified for job $jobName. Skipping date comparison."
if ($force -eq $false) {
if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName exists at destination. Use -Force to drop and migrate.")) {
$copyJobStatus.Status = "Skipped"
$copyJobStatus.Notes = "Already exists on destination"
$copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
Write-Message -Level Verbose -Message "Job $jobName exists at destination. Use -Force to drop and migrate."
}
continue
}
} elseif ($sourceDate -gt $destDate) {
# Source is newer, proceed with drop and recreate
if ($Pscmdlet.ShouldProcess($destinstance, "Source job is newer (modified $sourceDate). Dropping and recreating job $jobName")) {
try {
Write-Message -Message "Source job $jobName is newer. Dropping and recreating." -Level Verbose
$destServer.JobServer.Jobs[$jobName].Drop()
} catch {
$copyJobStatus.Status = "Failed"
$copyJobStatus.Notes = (Get-ErrorMessage -Record $_).Message
$copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
Write-Message -Level Verbose -Message "Issue dropping job $jobName on $destinstance | $PSItem"
continue
}
}
} elseif ($sourceDate -eq $destDate) {
# Dates are equal, skip
if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName has same modification date. Skipping.")) {
$copyJobStatus.Status = "Skipped"
$copyJobStatus.Notes = "Job has same modification date on source and destination"
$copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
Write-Message -Level Verbose -Message "Job $jobName has same modification date ($sourceDate). Skipping."
}
continue
} else {
# Destination is newer, skip with warning
if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName is newer on destination. Skipping.")) {
$copyJobStatus.Status = "Skipped"
$copyJobStatus.Notes = "Destination job is newer than source (dest: $destDate, source: $sourceDate)"
$copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
Write-Message -Level Warning -Message "Job $jobName is newer on destination ($destDate) than source ($sourceDate). Skipping."
}
continue
}
} catch {
Write-Message -Level Warning -Message "Error comparing dates for job $jobName | $PSItem"
if ($force -eq $false) {
if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName exists at destination. Use -Force to drop and migrate.")) {
$copyJobStatus.Status = "Skipped"
$copyJobStatus.Notes = "Already exists on destination"
$copyJobStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject
Write-Message -Level Verbose -Message "Job $jobName exists at destination. Use -Force to drop and migrate."
}
continue
}
}
} elseif ($force -eq $false) {
if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName exists at destination. Use -Force to drop and migrate.")) {
$copyJobStatus.Status = "Skipped"
$copyJobStatus.Notes = "Already exists on destination"
Expand Down
73 changes: 73 additions & 0 deletions tests/Copy-DbaAgentJob.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Describe $CommandName -Tag UnitTests {
"DisableOnSource",
"DisableOnDestination",
"Force",
"UseLastModified",
"InputObject",
"EnableException"
)
Expand Down Expand Up @@ -84,6 +85,7 @@ Describe $CommandName -Tag IntegrationTests {
"DisableOnSource",
"DisableOnDestination",
"Force",
"UseLastModified",
"InputObject",
"EnableException"
)
Expand Down Expand Up @@ -122,4 +124,75 @@ Describe $CommandName -Tag IntegrationTests {
(Get-DbaAgentJob -SqlInstance $TestConfig.instance3 -Job dbatoolsci_copyjob_disabled).Enabled | Should -BeFalse
}
}

Context "UseLastModified parameter" {
BeforeAll {
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true

# Create a test job on both source and destination with same modification date
$testJobModified = "dbatoolsci_copyjob_modified"
$null = New-DbaAgentJob -SqlInstance $TestConfig.instance2 -Job $testJobModified
Start-Sleep -Seconds 2

# Copy to destination first time
$splatInitialCopy = @{
Source = $TestConfig.instance2
Destination = $TestConfig.instance3
Job = $testJobModified
}
$null = Copy-DbaAgentJob @splatInitialCopy

# Ensure both jobs have the exact same date_modified by setting destination to match source
$escapedJobName = $testJobModified.Replace("'", "''")
$sourceDate = Invoke-DbaQuery -SqlInstance $TestConfig.instance2 -Database msdb -Query "SELECT date_modified FROM dbo.sysjobs WHERE name = '$escapedJobName'" | Select-Object -ExpandProperty date_modified
$updateQuery = "UPDATE msdb.dbo.sysjobs SET date_modified = '$($sourceDate.ToString("yyyy-MM-dd HH:mm:ss.fff"))' WHERE name = '$escapedJobName'"
$null = Invoke-DbaQuery -SqlInstance $TestConfig.instance3 -Query $updateQuery

$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
}

AfterAll {
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
$null = Remove-DbaAgentJob -SqlInstance $TestConfig.instance2 -Job dbatoolsci_copyjob_modified -ErrorAction SilentlyContinue
$null = Remove-DbaAgentJob -SqlInstance $TestConfig.instance3 -Job dbatoolsci_copyjob_modified -ErrorAction SilentlyContinue
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
}

It "skips job when dates are equal" {
$splatUseModified = @{
Source = $TestConfig.instance2
Destination = $TestConfig.instance3
Job = "dbatoolsci_copyjob_modified"
UseLastModified = $true
}
$result = Copy-DbaAgentJob @splatUseModified

$result.Name | Should -Be "dbatoolsci_copyjob_modified"
$result.Status | Should -Be "Skipped"
$result.Notes | Should -BeLike "*same modification date*"
}

It "updates job when source is newer" {
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true

# Modify the source job to make it newer
$sourceJob = Get-DbaAgentJob -SqlInstance $TestConfig.instance2 -Job "dbatoolsci_copyjob_modified"
$sourceJob.Description = "Modified description"
$sourceJob.Alter()
Start-Sleep -Seconds 2

$PSDefaultParameterValues.Remove("*-Dba*:EnableException")

$splatUseModified = @{
Source = $TestConfig.instance2
Destination = $TestConfig.instance3
Job = "dbatoolsci_copyjob_modified"
UseLastModified = $true
}
$result = Copy-DbaAgentJob @splatUseModified

$result.Name | Should -Be "dbatoolsci_copyjob_modified"
$result.Status | Should -Be "Successful"
}
}
}