Skip to content

sp_Blitz: make it install and run on Azure SQL Database (#4040) - #4045

Open
BrentOzar wants to merge 16 commits into
devfrom
claude/issue-4040-azure-sp_blitz
Open

sp_Blitz: make it install and run on Azure SQL Database (#4040)#4045
BrentOzar wants to merge 16 commits into
devfrom
claude/issue-4040-azure-sp_blitz

Conversation

@BrentOzar

@BrentOzar BrentOzar commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #4040.

The bug

Azure SQL DB refuses to compile a module that names another database. The ALTER PROCEDURE aborted 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 the ALTER fails. Azure users were left with an sp_Blitz that runs, returns nothing, and raises no error — a silent clean bill of health.

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

Count
wrap 32 statements reading master/msdb/model/rdsadmin now run through sp_executesql
hoist 19 cross-DB sub-queries in IF predicates evaluated first into @CrossDBExists / @CrossDBCount / @CrossDBDate
localise 3 master.sys.all_objects DMV probes → plain sys.all_objects
skip list 28 CheckIDs added to the Azure skip block

A few decisions worth reviewing:

  • The 3 localised probes need no dynamic SQL. They are version gates (dm_os_memory_nodes, change_tracking_databases, dm_exec_query_resource_semaphores). System objects come from the resource database and are visible in sys.all_objects from any database, so dropping the master. prefix is identical semantics — and it is what the file already does for sys.all_columns at line 1739.
  • Every dynamic cross-DB call is gated on EngineEdition <> 5, and every hoisted probe that feeds a skippable check also carries that check's #SkipChecks predicate (see review fixes below). 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 now that it can compile past them.
  • @TempDBfiles became #BlitzTempDBfiles. A table variable is not visible inside sp_executesql, so it had to become a temp table; the Blitz prefix keeps it from colliding with a caller's own temp table.
  • CheckID 191 reads sys.master_files in 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 <> 0 arm turns the check off on Azure.
  • The 28 new skips each reach an object Azure lacks (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.

  • CheckID 93 (8b9c783) referenced @IsWindowsOperatingSystem from 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 an sp_executesql parameter.
  • CheckID 212 (8b9c783) ended its dynamic string immediately after IF (SELECT COUNT(*) FROM #Instances) > 1, leaving an IF with no statement inside the batch and an unconditional BEGIN outside it. Only the xp_regread call is dynamic now; the count is evaluated in the outer batch.
  • CheckID 271 (6ed18f9) captured a SUM(1) file count into @CrossDBDate through a DATETIME output parameter. It uses the @CrossDBCount INT helper that already exists for this.
  • CheckID 232 (5023786) was added to the skip list by mistake, silently turning off the "Data Size" finding that sp_Blitz: add Azure SQL Database support #3951 deliberately added for Azure. The check already branches on @IsAzureSQLDB and reads sys.database_files there, 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.
  • Hoisted probes escaped their skip guards (ee2e7b7). Moving 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 what the skip list is for — it exists so an account without access to msdb or master metadata 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 the TRY/CATCH permission probes that set @SkipModel / @SkipMSDB_objs / @SkipMSDB_jobs, and 2 are the RDS master.sys.all_objects reads, visible to every login.
  • #TempDBfiles could destroy a caller's temp table (ee2e7b7). Caller temp tables are visible inside nested procedures, so DROP TABLE #TempDBfiles would 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.

Result
SQL Server 2017 (oldest supported) 39/39 smoke-test steps passed, no SQL errors
SQL Server 2025 (newest) 39/39 smoke-test steps passed, no SQL errors
Azure SQL Database (EngineEdition 5) installs, runs clean, returns findings

Boxed coverage comes from the smoke-test overhaul in #4047, which executes the kit instead of installing it with @VersionCheckMode = 1 — an unconditional early RETURN that meant no check body ever ran. Every rewritten sp_executesql block now binds and executes. Both of the first two bugs above would have failed this loudly: CheckID 93 with Msg 137 at bind time, CheckID 212 with a syntax error when sp_executesql parsed the stranded IF.

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":

  • 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

Latest 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 embeds GETDATE() in its text, 185 compares uptime against live counters, and 152/153 are a pair gated on wait_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

  • Windows-only paths. The CI containers are Linux, so @IsWindowsOperatingSystem = 0 and xp_regread — one of the two spots review found a bug in — is not exercised. A Windows host is the only coverage for those.
  • Restricted-permission behaviour. The findings comparison ran as 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 to msdb or master.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FMPBMMANLaFgSQ73GTgBW7

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>
Comment thread sp_Blitz.sql
Comment thread sp_Blitz.sql Outdated
@BrentOzar

Copy link
Copy Markdown
Member Author

Review/test summary for head 4d41439:

  • Azure SQL Database (EngineEdition 5): the exact PR script installed successfully. The default sp_Blitz run completed without errors and returned 29 findings. An expanded run with user-database checks and server info enabled also completed without errors and returned 37 findings.
  • The existing GitHub SQL Server smoke check is green, but it only installs the procedure and exercises VersionCheckMode; it does not execute the normal check path.
  • Boxed-SQL static review found two P1 execution blockers, posted inline: CheckID 93 loses @IsWindowsOperatingSystem across the dynamic-SQL scope, and CheckID 212 leaves an IF without a body inside the dynamic batch.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_executesql so 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 #SkipChecks block 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.

Comment thread sp_Blitz.sql Outdated
…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
BrentOzar added a commit that referenced this pull request Aug 22, 2026
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.
pull Bot pushed a commit to asleekgeek/SQL-Server-First-Responder-Kit that referenced this pull request Aug 22, 2026
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 BrentOzar left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread sp_Blitz.sql Outdated
BrentOzar and others added 7 commits August 22, 2026 19:12
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 #TempDBfiles table created by the session that invoked sp_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 #SkipChecks predicate, so globally skipping 202 no longer prevents the msdb access. 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 #SkipChecks guard. A caller that globally skips 178 still scans msdb.dbo.backupset, and under an account without access to backup history this can abort sp_Blitz even 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 access master metadata.
				IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */

sp_Blitz.sql:4425

  • This msdb feature probe is outside CheckID 116's #SkipChecks guard, so requesting that 116 be skipped still performs cross-database work and can fail under restricted permissions. Apply the skip predicate before invoking sp_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 IF already does.
		IF CONVERT(INT, SERVERPROPERTY('EngineEdition')) <> 5 /* not Azure SQL DB */

Comment thread .github/workflows/azure-sql-smoke-test.yml Outdated
Comment thread .github/scripts/run-azure-smoke-test.sh Outdated
…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

Copy link
Copy Markdown
Member Author

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 guards

The important one. Hoisting a cross-database sub-query out of an IF predicate also moved it out from behind that check's #SkipChecks test. On dev the sub-query sat after the skip test in the same predicate, so skipping the check skipped the cross-database read with it. After the rewrite the read happened first, unconditionally:

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. @SkipChecksTable exists so an account without access to msdb or master metadata can turn off the checks that would fail on it — and after this change, asking for that no longer prevented the access. CheckID 191 makes the scenario explicit in its own comment: "User may have no permissions to see tempdb files in sys.master_files."

Each probe now carries the same skip predicate as the check it feeds. When skipped, the helper keeps its initialised value (0 / NULL), which makes the downstream condition false — the same outcome as before.

Swept all 20 hoisted probes rather than only the 5 reported. Copilot's list was complete; the other 15 are already correct:

Count Why they are fine
Inside their check's skip block 9 the hoist is nested within IF NOT EXISTS(#SkipChecks …) BEGIN, so the guard already applies
TRY/CATCH permission probes 4 these are the permission-detection mechanism — they set @SkipModel / @SkipMSDB_objs / @SkipMSDB_jobs, and a failure is caught
RDS master.sys.all_objects reads 2 visible to every login, and the lost short-circuit is already disclosed in the PR description

Worth noting that this is a defect class my findings comparison could not have caught: it ran as sa, where a cross-database read never fails, so both sides produced identical output. It only bites a restricted account.

#TempDBfiles could destroy a caller's temp table

Turning the @TempDBfiles table variable into a temp table gave sp_Blitz a side effect it did not previously have. 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 — and #TempDBfiles is a plausible name for someone poking at tempdb to have created. Renamed to #BlitzTempDBfiles, matching #BlitzResults.

Retry budget

Fixed and replied on the thread.

Left open deliberately

The credentials-on-pull_request thread. Forks are already excluded, and anyone who can push a same-repo branch can read a repository secret through any trigger anyway, so an environment gate alone does not change the trust boundary. The real residual — long-lived shared credentials against a shared database — needs a repository-owner decision, so I have not tried to settle it in the diff.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread .github/scripts/run-azure-smoke-test.sh
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with VIEW SERVER STATE but no msdb access, supply skips for the hoisted checks, and assert that sp_Blitz completes 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 )

Copy link
Copy Markdown
Member Author

Ready for review at 5fad8de. Final state:

SQL Server 2017 (oldest supported) 39/39 steps, no SQL errors
SQL Server 2025 (newest, 17.0.4075.5) 39/39 steps, no SQL errors
Azure SQL Database (EngineEdition 5) installs at 510,178 chars, runs clean, 30 findings across 20 CheckIDs
Copilot "reviewed 3 out of 3 changed files and generated no new comments"

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 sa and Azure skips those checks entirely. That is a real gap — it is why the skip-guard defect survived the boxed tests, the Azure test, a findings comparison, and three rounds of review. Filed as #4052 rather than bolted on here, because a naive version of that test is brittle: sp_Blitz does plenty as a non-sysadmin that will complain for unrelated reasons, and getting the skip list wide enough to be quiet but narrow enough to still fail when a guard is removed is the actual work.

Two other things a human reviewer should weigh in on rather than take from me:

  1. Azure credentials on pull_requestthread left open deliberately. Forks are excluded and anyone who can push a same-repo branch can read a repository secret through any trigger anyway, so an environment gate alone does not move the trust boundary. The long-lived shared credential is the part worth deciding on.
  2. Windows-only paths remain untested. The CI containers are Linux, so xp_regread — one of the two places review found a bug — is never exercised. Only a Windows host covers that.

Generated by Claude Code

@BrentOzar BrentOzar left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/azure-sql-smoke-test.yml
Comment thread .github/scripts/run-azure-smoke-test.sh Outdated
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install-Azure.sql: sp_Blitz fails to create on Azure SQL Database (Msg 40515)

3 participants