sp_Blitz: make it install and run on Azure SQL Database (#4040) - #4045
sp_Blitz: make it install and run on Azure SQL Database (#4040)#4045BrentOzar wants to merge 16 commits into
Conversation
Closes #4040. Azure SQL DB refuses to compile a module that names another database, so the ALTER PROCEDURE aborted on the first of 88 cross-database references (model.sys.objects) and sp_Blitz was never created. The create-stub batch succeeded on its own, so users were left with a silent no-op sp_Blitz. The runtime @Skip* / #SkipChecks work from #3951 could never fix this: Msg 40515 fires when Azure binds the module, long before any runtime guard exists. What changed: * 32 statements that read master/msdb/model/rdsadmin now execute through sp_executesql, keeping the names away from the compiler. * 19 cross-database sub-queries inside IF predicates are evaluated first into @CrossDBExists / @CrossDBCount / @CrossDBDate helpers. * 3 master.sys.all_objects probes for resource-database system objects became plain sys.all_objects. Those are DMV version gates, and system objects are visible from every database, so this is identical semantics with no dynamic SQL - and it matches what the file already does for sys.all_columns. * @TempDBfiles became #TempDBfiles; a table variable is not visible inside sp_executesql. * Every dynamic cross-database call is gated on EngineEdition <> 5. Hoisting a sub-query out of an IF costs the short-circuit that used to protect it - the RDS probe sat behind db_id('rdsadmin') IS NOT NULL - and Azure cannot execute these references even though it can now compile past them. * 29 CheckIDs joined the Azure skip list. Each reaches an object Azure does not have (sys.master_files, sys.servers, sys.dm_os_cluster_nodes, and so on) from inside pre-existing dynamic SQL, so it compiled cleanly and only failed on execution. * CheckID 191 reads sys.master_files in its own guard predicate, which no skip entry can rescue, so the count is precomputed instead. Verified on a real Azure SQL Database (EngineEdition 5): installs with no errors, runs with no errors, returns 29 findings. Every generated string literal was checked by unescaping it and diffing against the original text - 51/51 reproduce their source exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Review/test summary for head 4d41439:
Azure support is working as intended, but I would not merge until both boxed-SQL regressions are fixed and the normal execution path is smoke-tested. |
CheckID 93: the WHERE clause references @IsWindowsOperatingSystem, which is not visible inside sys.sp_executesql. Declare and pass it so the batch binds instead of raising Msg 137 and aborting the run. CheckID 212: the dynamic string ended after 'IF (SELECT COUNT(*) FROM #Instances) > 1', leaving an IF with no statement inside the batch and an unconditional BEGIN outside it. Keep only the xp_regread call dynamic and evaluate the count in the outer batch, so the IF/BEGIN/END block is intact and still guards the finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Updates sp_Blitz so it can be created/compiled and run on Azure SQL Database (EngineEdition = 5) by removing static cross-database references that Azure rejects at module-bind time, and by expanding the Azure skip list for unsupported objects.
Changes:
- Wraps cross-database metadata reads (master/msdb/model/rdsadmin) in
sp_executesqlso Azure SQL DB can compile the module. - Hoists some cross-DB predicates into
@CrossDB*helper variables to avoid static 3-part naming. - Expands the Azure SQL DB
#SkipChecksblock to skip additional CheckIDs that would otherwise fail at execution time.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…rossDBDate The hoisted SUM(1) over master.sys.master_files is a count, but it was captured into @CrossDBDate via a DATETIME output parameter and then compared against SUM(data_files) from #TempDBfiles. That leaned on implicit INT-to-DATETIME conversion and read as a date comparison. Use the INT helper that already exists for this purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
The value in this harness is executing the scripts at all. That alone found three real pre-existing bugs (#4048, #4049, #4050) and would have caught both of PR #4045's runtime breakages. It worked from the first commit and has needed almost no repair since. The base-vs-head comparison is the opposite story. Installing the base branch's copies, running everything twice, and classifying each failure as new or pre-existing needed error signatures, step-body comparison, harness-file comparison and replay-on-mismatch -- about 155 lines whose only job was answering 'is this failure the PR's fault?'. Nearly every defect found while reviewing this harness lived in that code, and several were regressions introduced while fixing others. It also never took a decision. Every run so far logged 'harness differs from base; every step must pass on its own merits', because the harness files themselves keep changing. Grandfathering only matters when the baseline is dirty, and the baseline is clean: 0 failing steps on both engine versions. So it is gone, along with the findings diff it fed. The runner drops from 792 lines to 281, CI drops from two passes to one, and what remains is: seed a database, install all 12 non-deprecated scripts, verify each procedure exists, run 39 labelled steps, fail on any SQL error. Git history keeps a working implementation of the comparison if a dirty baseline ever makes it necessary. Unchanged and still earning their place: sqlcmd -I (QUOTED_IDENTIFIER), per-step attribution so one round surfaces every failure, the uptime wait for #4048, the CheckID 106 skip for #4050, and both engine versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
Brings in the smoke-test overhaul from #4047, so this branch's 51 rewritten cross-database blocks are executed against real SQL Server 2017 and 2025 for the first time. That is the boxed-SQL verification this PR's description says is missing, and the gap that let its Msg 137 and stranded-IF bugs through.
The smoke test installed only the changed sp_*.sql and ran it with @VersionCheckMode = 1, which is an unconditional early RETURN. No check body ever executed, so runtime breakage passed green -- PR BrentOzarULTD#4045 had two such bugs reach human review. Now, per issue BrentOzarULTD#4046: - Runs all 12 non-deprecated scripts through a 39-step parameter matrix covering the @output* table paths, sp_BlitzIndex modes 0-4, sp_kill's execute path and sp_DatabaseRestore. sp_BlitzUpdate is excluded because it rewrites the procs mid-run. - Installs the base branch's copy of every script, runs the same matrix, and compares. SQL errors this branch introduces fail the build; errors already present on base are reported and do not. Differences in sp_Blitz findings are printed for review and never fail, since changing a finding is often the point of the change. - Seeds a database, indexes, backup history and plan cache activity so the scripts have something real to read instead of an empty server. - Runs against SQL Server 2017 (oldest in support since 2016 aged out on 2026-07-14) and SQL Server 2025. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
BrentOzar
left a comment
There was a problem hiding this comment.
Reviewed the current head (abe4996) after the three earlier review fixes. I found no additional actionable issues.
I specifically rechecked the 53 sp_executesql calls for caller-local variables that were not passed into the dynamic batch, compared the wrapped statement bodies with the original dev text, checked the @CrossDBExists / @CrossDBCount / @CrossDBDate reset and output types, and scanned the resulting procedure for cross-database names left outside string literals. The only remaining static three-part names are the existing tempdb.sys.database_files references. The SQL Server smoke-test workflow is also green.
Residual coverage gap: I did not execute the Windows-only xp_regread path; that limitation is already called out clearly in the PR description.
Answers the question code review cannot: does the rewritten sp_Blitz still report the same findings as the released one? Three reviewers have now read this change; nobody has run it and compared what it finds. Installs dev's sp_Blitz, runs it, records every (CheckID, DatabaseName, Finding). Installs this branch's, runs the same thing, records again, diffs. The Azure rewrite is meant to change how the code is written, not what it reports, so on boxed SQL the two lists should be identical. Runs on SQL Server 2017 and 2025. Excludes CheckIDs 156 and 185, whose text embeds a timestamp and a live wait counter respectively, so they differ between any two runs minutes apart regardless of the code. Deliberately not part of the permanent smoke tests -- on every PR this was noisy and never decided anything, which is why #4047 removed it. Gated to this branch, and both files come out once the answer is recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
CheckID 232 (Data Size) branches on @IsAzureSQLDB and reads sys.database_files instead of sys.master_files when it is on Azure -- support #3951 added deliberately. Its only mentions of sys.master_files are inside a comment and a quoted string, so Azure never binds them and the check runs fine. This PR added a skip entry for it anyway, which silently turned that check off on Azure. dev does not skip 232, so this was a regression the PR introduced, and one nothing would have reported: a skipped check produces no error, just a finding that stops appearing. It came from the mechanical sweep used to build the skip list -- find sys.* objects Azure lacks, attribute each to its enclosing #SkipChecks guard -- which matched on text without noticing the check already handled Azure. Re-checked the other 28 added skips for the same false positive; 232 was the only one whose body already had an Azure-specific branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
…diff Azure job. Issue #4040's failure mode is invisible to both a boxed test and a plain did-it-install check: the CREATE stub is its own batch and always succeeds, while only the ALTER carrying the real body hits Msg 40515 and fails. Users were left with an sp_Blitz that ran, returned nothing, and raised no error. So the job asserts four things rather than one: - EngineEdition is 5, so a pass cannot come from accidentally testing something that is not Azure - both scripts install without error - sp_Blitz's definition is longer than 10,000 characters, proving the body landed and not just the RETURN 0 stub - it runs clean AND returns at least one finding, because running silently with nothing to say is the bug, not the goal Serverless Azure SQL DB auto-pauses and rejects the connection that wakes it, so the first failures mean nothing; it retries for up to ten minutes. Runs are serialised, since the target is one shared database this installs into. One-off diff fix: it installed only sp_Blitz, but sp_Blitz calls sp_ineachdb to iterate databases, so every database check died with Msg 2812 and the baseline capture never completed. Both scripts now come from the same revision, so each side of the comparison is internally consistent. sp_validatelogins is the only other proc sp_Blitz calls and it is a SQL Server built-in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
EXEC sp_Blitz @OutputType = 'COUNT' emits sp_Blitz's version-check result set first, so taking the first line of sqlcmd output picked up "Component ... is outdated" and reported that as the finding count. Take the last all-digits line instead, which the warning text can never be, and reuse the same reader for the EngineEdition and definition-length probes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
Two empty captures compare equal, so the diff reported IDENTICAL and exited 0 without having compared anything. That is indistinguishable from a real match in the log. Require each side to record at least one finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
CheckID 152 filters waits with wait_time_ms > .1 * @CpuMsSinceWaitsCleared, and that denominator grows with uptime, so a wait that clears the bar in the first capture falls below it in the second one seconds later. CheckID 153 fires exactly when 152 finds nothing, so the pair flips together: 2017 reported 152 going silent and 153 appearing, on the same two revisions that had compared identical minutes earlier. Same volatility class as 156 and 185. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
It existed to answer what code review could not: does the rewritten sp_Blitz still report the same findings as the released one? It does. On SQL Server 2017 and 2025 the released and rewritten procedures produced identical (CheckID, DatabaseName, Finding) lists, with a guard requiring each capture to be non-empty so an empty comparison could not pass as a match. Answer recorded, so the harness comes out rather than becoming a permanent check that reruns a settled question on every PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
Previously missed (2) — in code that hasn't changed since the last review.
sp_Blitz.sql:7087
- This can drop a
#TempDBfilestable created by the session that invokedsp_Blitz, because caller temp tables are visible inside nested procedures. The previous table variable had no such side effect. Use a procedure-specific temp-table name and update its references rather than dropping this generic caller-visible name.
IF OBJECT_ID('tempdb..#TempDBfiles') IS NOT NULL DROP TABLE #TempDBfiles;
CREATE TABLE #TempDBfiles (config VARCHAR(50), data_files INT);
sp_Blitz.sql:1821
- The CheckID 202 metadata probe runs before its
#SkipCheckspredicate, so globally skipping 202 no longer prevents themsdbaccess. Keep the probe behind the same skip guard to preserve the documented skip behavior and avoid permission failures.
This issue also appears in the following locations of the same file:
- line 1953
- line 4224
- line 4425
- line 6441
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
sp_Blitz.sql:1953
- The hoisted CheckID 178 probe now executes before the
#SkipChecksguard. A caller that globally skips 178 still scansmsdb.dbo.backupset, and under an account without access to backup history this can abortsp_Blitzeven though the check was explicitly skipped. Include the same skip predicate in this guard or move the probe inside the check's guarded block.
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
sp_Blitz.sql:4224
- The CheckID 105 probe executes even when 105 is globally present in
#SkipChecks. Put the dynamic lookup behind the same skip predicate so a skipped check does not accessmastermetadata.
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
sp_Blitz.sql:4425
- This
msdbfeature probe is outside CheckID 116's#SkipChecksguard, so requesting that 116 be skipped still performs cross-database work and can fail under restricted permissions. Apply the skip predicate before invokingsp_executesql.
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
sp_Blitz.sql:6441
- The newly hoisted CheckID 191 count is evaluated before checking whether 191 is skipped. This means a global skip no longer suppresses the catalog access; include the skip predicate in this guard, as the later
IFalready does.
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
…files Three fixes from Copilot's review. Skip guards. Hoisting a cross-database sub-query out of an IF predicate also moved it out from behind that check's #SkipChecks test, so asking to skip CheckID 202, 178, 105, 116 or 191 no longer prevented the cross-database read. That defeats the point of the skip list: it exists so an account without access to msdb or master metadata can turn off the checks that would fail. Each probe now carries the same skip predicate as the check it feeds. When skipped, the helper keeps its initialised value, which makes the downstream condition false -- the same outcome as before. Swept all 20 hoisted probes for this. The other 15 are already correct: nine sit inside their check's skip block, four are the TRY/CATCH permission probes that set @SkipModel / @SkipMSDB_objs / @SkipMSDB_jobs, and two are the RDS master.sys.all_objects reads, which are visible to every login and whose lost short-circuit is already noted in the PR description. #TempDBfiles is now #BlitzTempDBfiles. Turning the table variable into a temp table gave sp_Blitz a side effect it did not have before: caller temp tables are visible inside nested procedures, so DROP TABLE #TempDBfiles would destroy a table of that name belonging to the session that called sp_Blitz. The name is generic enough to collide with one a DBA looking at tempdb might have made. Azure wake-up retry budget. Failed connections inherited the 60-second login timeout, so 30 attempts plus their 20-second sleeps could run about 40 minutes against a 30-minute job timeout -- the runner would kill the job before the loop could report why it gave up. Wake-up attempts now use a 15-second login timeout and a 15-second sleep, bounding the loop at 15 minutes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
|
Copilot's suppressed findings on the last review turned out to be the valuable ones. Both are fixed in ee2e7b7. Recording them here since suppressed comments have no thread to reply on. Hoisted probes escaped their skip guardsThe important one. Hoisting a cross-database sub-query out of an SET @CrossDBExists = 0;
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5
BEGIN
EXEC sys.sp_executesql N'... FROM msdb.INFORMATION_SCHEMA.COLUMNS ...'; -- ran even when 202 was skipped
END;
IF NOT EXISTS (SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 202)That defeats what the skip list is for. Each probe now carries the same skip predicate as the check it feeds. When skipped, the helper keeps its initialised value ( Swept all 20 hoisted probes rather than only the 5 reported. Copilot's list was complete; the other 15 are already correct:
Worth noting that this is a defect class my findings comparison could not have caught: it ran as
|
The count cannot explain itself. It moved from 29 to 30 between two commits and the log gave no way to tell whether a check had been turned on, turned off, or simply reported differently -- the target is a single shared serverless database that auto-pauses, and resuming resets wait stats, so the wait-stats checks come and go on their own. A bare number makes that indistinguishable from a real behaviour change. Lists the CheckIDs alongside the count. Diagnostic only: it never fails the job, and the count remains the assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
Unlike the boxed jobs, which get a fresh container each run, the Azure target is one long-lived database. A previous run's sp_Blitz satisfied every assertion in this job -- it exists, its body is far longer than 10,000 characters, it runs, it returns findings -- even if the current checkout had installed nothing at all. The job would report success having measured the last run's code. That is the same shape as the bug the job exists to catch. #4040 was invisible because something called sp_Blitz was present and answered when asked; a job that can pass on a leftover procedure reproduces the failure mode rather than detecting it. Drop both procedures first and confirm they are gone, so the definition-length check proves this checkout created what it measures. Refusing to continue unless the count is a definite zero, since an unreadable result is not evidence the database is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
sp_Blitz.sql:1824
- These new skip guards are not covered under the permission model they are meant to protect: the boxed matrix connects as
sa, while Azure skips these cross-database checks entirely. A non-sysadmin regression here already occurred during this PR, so please add a smoke case using a login withVIEW SERVER STATEbut nomsdbaccess, supply skips for the hoisted checks, and assert thatsp_Blitzcompletes without attempting those reads.
IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */
AND NOT EXISTS ( SELECT 1
FROM #SkipChecks
WHERE DatabaseName IS NULL AND CheckID = 202 )
|
Ready for review at
Copilot's last pass raised one thing worth naming rather than burying: the new skip guards are not covered by any test, because the boxed matrix connects as Two other things a human reviewer should weigh in on rather than take from me:
Generated by Claude Code |
BrentOzar
left a comment
There was a problem hiding this comment.
Re-reviewed the current head (5fad8de), which is 10 commits ahead of the version I previously reviewed. I found no additional actionable issues.
For the new delta, I checked the Azure workflow and smoke-test script for false-pass paths, verified the new pre-drop/zero-count assertion, and reviewed the sp_Blitz.sql changes that guard hoisted probes behind #SkipChecks, restore CheckID 232 on Azure, and rename #TempDBfiles to avoid caller collisions. I also repeated the full static three-part-name and dynamic-variable-scope scans against the current procedure.
The live Azure run for this head confirmed EngineEdition 5, started from a clean database, installed a 510,178-character sp_Blitz, ran without errors, and returned 30 findings. Both the Azure and boxed SQL Server workflows are green.
I did not duplicate the existing open thread about protecting the long-lived Azure credentials; that remains the only unresolved review concern visible on the current patch.
A pull request from a fork never receives these secrets -- GitHub withholds them, which is exactly what stops a fork's code from reading the password -- so the job is skipped there. That left a hole: an outside contributor's change to sp_Blitz.sql could reach dev having been tested on boxed SQL Server but never against Azure, which is the engine this work exists to protect. Running on push to dev closes it. That is after the fact rather than blocking, but the alternative is giving credentials to code no one has reviewed, which is the attack the fork restriction prevents in the first place. The job condition needed the same change. On a push there is no pull_request in the event payload, so the existing fork test would have compared against nothing, skipped every dev run, and made the new trigger a silent no-op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4bb07c5ac6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The finding assertion could not fail. Every successful run of sp_Blitz unconditionally inserts two CheckID -1 credit/version rows and the CheckID 156 rundate row, and CheckID 223 "Some Checks Skipped" fires whenever the login is not sysadmin, which on Azure is always. @OutputType = 'COUNT' counts all of them, so the count was never below four and "returned at least one finding" stayed green no matter what. That is the bug this job exists to detect, rebuilt inside the job: output that looks like proof of life while proving nothing. #4040 was invisible precisely because sp_Blitz answered when asked. Assert instead on CheckIDs that are not sentinels, read from CSV output so the IDs are visible rather than summed away. Verified the new form fails on sentinel-only output and on empty output, and that a comma inside a finding cannot shift the CheckID field. Found by Codex in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7
Closes #4040.
The bug
Azure SQL DB refuses to compile a module that names another database. The
ALTER PROCEDUREaborted on the first of 88 cross-database references (model.sys.objects, sp_Blitz.sql:392) and sp_Blitz was never created.Worse than the issue reported: the
IF OBJECT_ID(...) IS NULL EXEC('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0')stub is a separate batch with no cross-DB reference, so it succeeds. Only theALTERfails. Azure users were left with ansp_Blitzthat runs, returns nothing, and raises no error — a silent clean bill of health.The runtime
@Skip*/#SkipCheckswork from #3951 could never fix this. Msg 40515 fires when Azure binds the module, long before any runtime guard exists.What changed
wrapsp_executesqlhoistIFpredicates evaluated first into@CrossDBExists/@CrossDBCount/@CrossDBDatelocalisemaster.sys.all_objectsDMV probes → plainsys.all_objectsA few decisions worth reviewing:
dm_os_memory_nodes,change_tracking_databases,dm_exec_query_resource_semaphores). System objects come from the resource database and are visible insys.all_objectsfrom any database, so dropping themaster.prefix is identical semantics — and it is what the file already does forsys.all_columnsat line 1739.EngineEdition <> 5, and every hoisted probe that feeds a skippable check also carries that check's#SkipCheckspredicate (see review fixes below). Hoisting a sub-query out of anIFcosts the short-circuit that used to protect it — the RDS probe sat behinddb_id('rdsadmin') IS NOT NULL— and Azure cannot execute these references even now that it can compile past them.@TempDBfilesbecame#BlitzTempDBfiles. A table variable is not visible insidesp_executesql, so it had to become a temp table; theBlitzprefix keeps it from colliding with a caller's own temp table.sys.master_filesin its own guard predicate, which no skip-list entry can rescue, since the predicate still has to bind. Its count is precomputed instead, starting at 0 so the<> 0arm turns the check off on Azure.sys.master_files,sys.servers,sys.dm_os_cluster_nodes,sys.server_permissions, …) from inside pre-existing dynamic SQL. They compiled fine and only failed on execution.Review fixes
Six defects caught in review. The last three came from Copilot's suppressed comments, which turned out to be the valuable ones.
@IsWindowsOperatingSystemfrom inside the dynamic batch without declaring or passing it. Caller-local variables are not visible there, so every boxed run reaching this check would have raised Msg 137 and aborted. The bit is now passed as ansp_executesqlparameter.IF (SELECT COUNT(*) FROM #Instances) > 1, leaving anIFwith no statement inside the batch and an unconditionalBEGINoutside it. Only thexp_regreadcall is dynamic now; the count is evaluated in the outer batch.SUM(1)file count into@CrossDBDatethrough aDATETIMEoutput parameter. It uses the@CrossDBCount INThelper that already exists for this.@IsAzureSQLDBand readssys.database_filesthere, so it was always Azure-safe. Worth naming because nothing would have reported it: a skipped check raises no error and changes no binding, it just stops finding things.IFpredicate also moved it out from behind that check's#SkipCheckstest, so asking to skip CheckID 202, 178, 105, 116 or 191 no longer prevented the cross-database read. That defeats what the skip list is for — it exists so an account without access tomsdbormastermetadata can turn off the checks that would fail on it, a scenario CheckID 191 spells out in its own comment. Each probe now carries the same skip predicate as the check it feeds; when skipped the helper keeps its initialised value, making the downstream condition false, which is the previous outcome. All 20 hoists were swept: 9 already sit inside their check's skip block, 4 are theTRY/CATCHpermission probes that set@SkipModel/@SkipMSDB_objs/@SkipMSDB_jobs, and 2 are the RDSmaster.sys.all_objectsreads, visible to every login.#TempDBfilescould destroy a caller's temp table (ee2e7b7). Caller temp tables are visible inside nested procedures, soDROP TABLE #TempDBfileswould drop a table of that name belonging to the calling session — a side effect the table variable did not have. Renamed#BlitzTempDBfiles.Verification
Three engines, all executing the procedure rather than only installing it.
Boxed coverage comes from the smoke-test overhaul in #4047, which executes the kit instead of installing it with
@VersionCheckMode = 1— an unconditional earlyRETURNthat meant no check body ever ran. Every rewrittensp_executesqlblock now binds and executes. Both of the first two bugs above would have failed this loudly: CheckID 93 withMsg 137at bind time, CheckID 212 with a syntax error whensp_executesqlparsed the strandedIF.Azure is now a CI job, not a one-off manual run —
.github/workflows/azure-sql-smoke-test.yml. It asserts four things rather than one, because #4040's failure mode is invisible to "did it install":EngineEditionis 5, so a pass cannot come from accidentally testing something that is not Azuresp_Blitz's definition is longer than 10,000 characters — proving the body landed and not just theRETURN 0stubLatest run: definition 510,178 characters, 0 errors, 30 findings across 20 CheckIDs.
The finding count is not stable, because the target is a single shared serverless database whose state moves between runs. Re-running the same commit twenty minutes later took it from 29 to 30, so the count is a liveness assertion, not a fixture. The job therefore also logs which CheckIDs fired, so a number that moves can be explained rather than guessed at.
The checks still find the same things. A throwaway harness installed
dev's sp_Blitz, recorded every(CheckID, DatabaseName, Finding), then did the same for this branch and diffed. On both SQL Server 2017 and 2025 the two lists were identical — 49 findings each on 2017. Excluded four CheckIDs that cannot be stable between two runs minutes apart regardless of the code: 156 embedsGETDATE()in its text, 185 compares uptime against live counters, and 152/153 are a pair gated onwait_time_ms > .1 * @CpuMsSinceWaitsCleared, whose denominator grows with uptime. A guard required each capture to be non-empty, so an empty comparison could not pass as a match. The harness was deleted once it had answered; it is a question about this change, not a check every future PR should rerun.Static analysis: 0 cross-database references remain outside string literals, re-verified with a lexer handling strings,
--and nested/* */together. Every generated string literal was mechanically unescaped and diffed against the original source text; each reproduces its source exactly.What this does not establish
@IsWindowsOperatingSystem = 0andxp_regread— one of the two spots review found a bug in — is not exercised. A Windows host is the only coverage for those.sa, where a cross-database read never fails. That is precisely why it could not have caught the skip-guard defect above, which only bites an account that lacks access tomsdbormaster.🤖 Generated with Claude Code
https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7