Copy-DbaDatabase - Fix empty -NewName destination sweep and piped multi-database failures - #10514
Copy-DbaDatabase - Fix empty -NewName destination sweep and piped multi-database failures#10514potatoqualitee wants to merge 9 commits into
Conversation
…ti-database failures (#10512) Fixes for the four defects behind #10512, reproduced live on 2.8.3-identical code: - Reject bound-but-empty -NewName/-Prefix in begin{}: an empty destination name passes every downstream -Database filter, which treats "" as "all databases" - Set-DbaDbOwner then silently rewrote the owner of every updateable database on the destination. - Re-arm the "Cannot use NewName when copying multiple databases" guard for pipelines: process{} sees one database per invocation, so the old per-invocation count never exceeded 1 and the guard never fired. - Containment invariant after name construction: an empty resolved destination name now fails that database instead of reaching the restore and the post-restore owner/state/property commands. - Restore failures now use Stop-Function -Continue: the interrupt flag set by the old catch persists across process blocks and silently discarded every database piped in after one failed restore. - Test-DbaBackupInformation: handle the bare-boolean return Test-DbaPath produces for a single scalar path - a missing single backup file used to pass verification unnoticed. - Test-DbaPath: surface xp_fileexist batch failures as a warning instead of Verbose, so batch-level errors are no longer indistinguishable from files that genuinely do not exist. (do Copy-DbaDatabase, Test-DbaPath, Test-DbaBackupInformation) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r destination The pipeline guard added for #10512 accumulated inside the destination loop, so a single database copied to two destinations with -NewName counted twice and falsely tripped "Cannot use NewName when copying multiple databases". Lab-verified: single db to two destinations renames on both; piped multi-db with -NewName still stops. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iled row on containment, SMB visibility retry Review rework of the #10512 fix: - The piped-NewName guard now rejects in process{} before any database is copied, using $MyInvocation.ExpectingInput to detect pipeline mode where the total count is unknowable up front. The cumulative-counter approach (which only fired after the first database had already been copied under the new name) is gone; parameter-bound -InputObject arrays fall through to the original count guard because their full count is known. - The empty -Prefix rejection is removed: an empty prefix is a harmless no-op and rejecting it broke existing splats. Empty -NewName is still rejected because it is what detonates the owner sweep. - The empty-destination-name containment invariant now emits a Failed MigrationObject before Stop-Function -Continue, so the caller sees the skipped database in the results instead of it silently vanishing. - A bounded 8-second visibility retry from the destination bridges the 5-second SMB FileNotFoundCache window between a fresh backup and its restore, handling both the scalar-bool and object shapes Test-DbaPath returns. This keeps the intermittent OS-error-2 restores fixed without touching machine-wide SMB configuration. - Tests: the sentinel database owner is a dedicated login (a sweep sets owners to the source owner, so sa-to-sa would go undetected); the piped copy test asserts nonblank DestinationDatabase per database; the rename rejection test asserts zero destination changes; a new test proves one failed restore yields exactly one Failed row and the next piped database still copies. Lab verified: unit 12/12, integration 5/5, param-mode rename live check. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both reviews addressed in 03cace2. Point by point: 1. Piped- 2. Empty 3. Containment invariant emitted no Failed result (review 1 point 3, review 2 "smaller"). Fixed - 4. Scope of "Fixes #10512" vs the SMB trigger (review 1 point 4). Took the bounded-retry option the review favored: after a fresh backup (skipped for 5. Failed-restore continuation test (review 1 "missing regression test", review 2 point 3). Added: "Continues to the next piped database when one restore fails". It backs up both databases to the share, deletes the first backup file (history intact, so 6. Sentinel-owner false negative (review 2 point 4). Fixed - the sentinel database owner is now a dedicated Lab verification after the rework: 12 unit + 5 integration tests green, plus a live parameter-mode 🤖 Generated with Claude Code |
…ic continuation test, NewName pipeline help Round-2 review fixes for the #10512 PR: - The SMB visibility retry no longer treats an empty or incomplete Test-DbaPath result set as success. Test-DbaPath emits nothing when its own connection attempt fails, so the retry now requires one conclusive result for every backup path before proceeding; indeterminate probes keep retrying until the negative-cache deadline. - The failed-restore continuation test pipes the failing database explicitly first. sys.databases order is not a contract, and a failure on the last record could not have detected the old discard bug. - New test: a visibility probe returning no results keeps retrying and emits the timeout warning instead of proceeding as if visible. - NewName help documents the pipeline-input restriction introduced by the atomic rejection; to rename, specify the database with -Database and provide -NewName. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Round-2 review feedback addressed in 4212762. Point by point: Empty/incomplete visibility probe treated as success — fixed. The retry now materializes the Retry state-machine coverage — added Continuation test ordering — fixed. The failing database is now piped explicitly first via two separate NewName help — updated to state the pipeline-input restriction and the escape hatch: specify the database with Single-database piped rename (the P1 in one review) — kept the atomic rejection, deliberately. The other two reviews endorse it as the right implementation, and buffering pipeline input to allow the one-record case was considered and rejected in the previous round: it would restructure the streaming 🤖 Generated with Claude Code |
Project rule: integration tests only, no Pester mocks. The module-scoped mock intercepted unrelated internal Test-DbaPath calls (a Pester 5 filtered mock has no real-command fallback for non-matching calls) and broke the COPY matrix. The conclusive-result guard in the retry stays in production code; the no-output probe path cannot be driven from real instances, so it ships untested by design. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The CI failure on 4212762 was the new mock-based retry test — the module-scoped |
…ecting it Unbinding the parameter in begin makes every later Test-Bound check see it as not specified, so a pipeline that passes -NewName unconditionally and leaves it empty copies the databases under their original names (#10512). The original sweep hazard stays closed: the parameter never reaches $destinationDbName, and the empty-destination guard before the restore remains as backstop. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex review round 1: the help documents whitespace as equivalent to empty, so the blank-NewName regression test is now data-driven over both spellings. Cleanup stays inline rather than AfterEach because a Context-scoped AfterEach would fire after the unrelated tests in this Context too. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipeline pattern Documents the #10512 flexibility: an empty or whitespace -NewName is treated as not specified, so the reporter's pattern of passing one variable unconditionally now has a discoverable example. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
github actions are broken :( |
Fixes #10512
What was going wrong
Reproduced in a lab (sql2017 -> sql2019, UNC share) against the reported 2.8.3 symptoms. Four distinct defects compound into the reported behavior:
A bound-but-empty
-NewNamesilently sweeps the destination.[string]$NewNamebinds$null/""without validation,$destinationDbNameends up empty, and every downstream-Database ""filter means "all databases". Since 2.8.3 replaced the post-restoreALTER DATABASEwithSet-DbaDbOwner, that empty filter rewrites the owner of every updateable database on the destination instance - the reported owner warnings are just the databases it could not touch. The only guard ($dbCount -gt 1) never fires for piped input becauseprocess{}sees one database per record.One failed restore silently discards every remaining piped database. The restore-failure path called
Stop-Functionwithout-Continue, which sets the module's persistent interrupt flag; all later pipeline records are dropped with no output at all - the "databases just vanish" flavor in the report.A missing single backup file passes verification.
Test-DbaPathreturns a bare[bool]for a single scalar path on a single instance, butTest-DbaBackupInformationalways assumes objects:$path.FileExists -eq $falseon a bool is never true, so a missing single-file backup adds no verification error and the engine's OS error 2 becomes the first (confusing) signal.Path-check batch failures are reported as "file not found". When the
xp_fileexistbatch throws,Test-DbaPathfabricatedFileExists = $falsefor every path and demoted the real exception to Verbose - making transient access failures look like missing backups.The remaining trigger - backups intermittently "not found" although they exist - reproduced as the destination host's SMB client FileNotFoundCache (default 5 s): a single early miss pins "not found" for the restore that follows (control 0/30 failures, cache primed 30/30, cache disabled 0/15).
The fixes
-NewNameup front with a clear message (it is what detonates the owner sweep). An empty-Prefixremains a harmless no-op and is deliberately not rejected.-NewName: with piped databases the total count is unknowable until the pipeline ends - by which time the first database would already have been copied under the new name. The command now rejects-NewNamewith pipeline input inprocess{}before any work happens ($MyInvocation.ExpectingInput), directing the caller to-Database+-NewNamefor a single-database rename. A parameter-bound-InputObjectarray still goes through the original count guard because its full count is known up front.-Continue, so later piped databases still process.Set-SmbClientConfiguration -FileNotFoundCacheLifetime 0on the destination host remains a workable environment-level mitigation.Test-DbaBackupInformationhandles the scalar-bool contract ofTest-DbaPathfor single-file backups.Test-DbaPathsurfaces batch execution failures as a warning stating existence could not be determined, instead of silently reporting not-found.Verification
-NewNamenow stops loudly with zero copies and untouched owners; piped multi-database copies complete; piped-NewNameis rejected with zero databases created on the destination; a missing backup mid-pipeline yields exactly one Failed row and the next database still copies; parameter-mode single-database rename still works.DestinationDatabaseand an unrelated destination owner preserved - the sentinel owner is a dedicated login, since the sweep sets owners to the source owner and sa-to-sa would go undetected), atomic piped--NewNamerejection, and failed-restore continuation. 12 unit + 5 integration tests green in the lab.🤖 Generated with Claude Code