Promote dev to main: Updates_20260801 - #847
Merged
Merged
Conversation
Change @query_duration_ms from integer to decimal(18,3) so a sub-1ms query-capture floor can be expressed (e.g. 0.5 = 500 microseconds) instead of only whole milliseconds or 0 (no floor). The ms-to-microsecond conversion is wrapped in CONVERT(bigint, ...) so the event-session duration predicate always receives a clean integer literal. Whole-millisecond inputs behave exactly as before; 0 still removes the floor entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audited every procedure's parameter table -- both the per-proc folder READMEs and the aggregated root README -- against each procedure's declared parameters and @help output, and corrected them. Fixes: - Wrong data types: sp_QuickieCache @top (bigint), sp_HumanEvents @custom_name (sysname), sp_HumanEvents @query_duration_ms (decimal). - Swapped valid inputs: sp_PressureDetector @skip_perfmon and @sample_seconds. - Wrong default: sp_QueryReproBuilder @end_date (NULL). - Stale/incomplete valid inputs: @query_sort_order (event_time), @execution_type_desc (failed), @sort_order (average prefix), @target_type (table mode), @cleanup_targets (comma-separated combos). - ~25 undocumented parameters added across sp_QuickieStore, sp_QueryReproBuilder, sp_QuickieCache, sp_HumanEventsBlockViewer (@skip_execution_plans), sp_IndexCleanup, sp_PerfCheck (@help), TestBackupPerformance (@encryption_list). - Restored dropped detail (observer-overhead / UTC / type-constraint warnings, log default parentheticals, other qualifiers) while preserving intentional house-style wording. - TestBackupPerformance: corrected the defaults combination count (24 -> 72) and the compression+encryption result-set description. sp_LogHunter was already accurate. The Install-All bundle is left untouched (CI regenerates it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parameter-table accuracy sweep + fractional-ms @query_duration_ms
…PC calls Query mode keeps only events with a non-zero query_hash_signed. That filter is load-bearing -- it strips hash-less noise -- but it also hides the sp_prepare / sp_prepexec / sp_execute / sp_unprepare plumbing client drivers emit, since all of it carries a zero query hash. That makes prepared-statement workloads impossible to tell apart: pyodbc's fast_executemany on vs off look identical because only the inner INSERT survives the filter, differing only by the parameter datatype. @keep_prepare_rpc = 1 adds a result set (interactive/sampling path only) listing those RPC calls chronologically with object_name, statement, sql_text, and per-call timing, with no duration floor. It inherits whatever session filters are already in effect. Default 0 leaves output byte-for-byte unchanged. The long-term logging-to-table path is intentionally left alone to avoid bloating collection tables under load. Verified end-to-end against pyodbc + ODBC Driver 18: fast_executemany off shows sp_prepexec per row; on shows sp_describe_undeclared_parameters + one sp_prepare + sp_execute per row. Compiles on SQL 2016/2019/2022. No version bump (not a release). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sys.dm_exec_query_stats is a streaming view over the plan cache with nothing seekable in it. The statement path picked its sample handles with an UPDATE that CROSS APPLY'd back into the DMV correlated on query_hash, which nested-loops: the optimizer spools the whole cache, then rewinds, refilters and re-sorts every cached statement once per collected hash, so the work grew with (hashes x cached statements). The plan cache health checks scanned the same DMV four more times, each with a per-row sys.dm_exec_plan_attributes call. Materialize it once into #dm_exec_query_stats, apply the database filters on the way in, and let every consumer read the temp table. The sample handles now fall out of the statement aggregation itself (totals rolled up, joined to the winning plan row), so the UPDATE is gone rather than merely faster. 2x faster on a 22k-plan cache. Fixes carried in the same change, each verified against a frozen cache snapshot rather than two reads of a moving one: - Sample plans could come from the wrong database. The old CROSS APPLY carried no database or date predicates, so with @database_name set it returned the most-executed plan for a hash from ANY database, then showed that plan and its text under the filtered database's name. Filtering at materialization makes that impossible. - "Null value is eliminated by an aggregate" on every real run. The plan-age buckets used COUNT_BIG(DISTINCT CASE WHEN ... THEN plan_handle END), which hands COUNT a NULL for every plan outside the window - so, any cache holding plans older than an hour. Recast as a count over rows pre-deduplicated on MAX(creation_time), preserving the original "any statement of this plan is in the window" semantics exactly. creation_time is a STATEMENT property, not a plan property: a statement that recompiles restamps its own while the batch keeps its plan_handle, so rows sharing a plan_handle routinely disagree. - Procedure, function and trigger stats are materialized too, into a shared #dm_exec_object_stats. The three DMVs are shaped identically across every column used, so one staging table and one aggregate replace three near-identical paths. Staging also keeps the rollup and the winning-plan lookup on one stable snapshot: reading a DMV twice in a single statement is two independent reads of live plan cache memory, and anything evicted in between would vanish from the results. - Drops the "Optimize for Ad Hoc Workloads" suggestion from the single-use plan finding; the Forced Parameterization advice stays. Verified on SQL Server 2012, 2016, 2017, 2019, 2022 and 2025: installs and runs clean in every mode with no errors and no warnings, and returns byte-identical output to the previous version against a live 22,245-plan cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-rpc sp_HumanEvents: add @keep_prepare_rpc to surface prepared-statement RPC calls
… and logging
A multi-agent bug hunt with adversarial verification and live-server
validation found ten real defects; every fix here was reproduced
against SQL Server 2012-2025 before and after.
Collection and routing:
- The scheduler section had never returned a row. Both collection
paths filtered on 'scheduler_monitor_system_health', but the event
is named scheduler_monitor_system_health_ring_buffer_recorded -
zero matches out of 60k+ events on live servers. The parse also
read fields the event has never carried (scheduler_id, status,
is_online, ...), so the section is rebuilt around its real payload:
sql_cpu_utilization, other_process_cpu, system_idle,
memory_utilization, page_faults, working_set_delta_mb, with
@warnings_only flagging pegged SQL CPU, heavy other-process CPU,
or a trimmed working set. Old-shape log tables are dropped and
recreated on sight - provably lossless, since the name bug meant
nothing was ever inserted into them.
- The @what_to_check routing table and its readers had drifted:
'cpu' never collected sp_server_diagnostics_component_result (so
worker-thread and pending-task findings only appeared under 'all'),
'memory' missed it on the file-target path, the ring-buffer path
missed it for 'cpu' and 'waits', and the scheduler output ran under
'system' while collection only fired under 'cpu'. All aligned.
Parsing:
- error_reported carries database_id as an action, not a data
element; reading the data axis left database_name and database_id
NULL for every severe error. Now read from the action axis.
- Blocked-process and deadlock query_text went blank when a stored
procedure's database or object no longer resolves - the unguarded
OBJECT_SCHEMA_NAME + OBJECT_NAME concatenation nulled the whole
column. Both sites now fall back to the raw inputbuf text, which
still exposes the database and object ids.
Logging:
- QUOTENAME'd table names were re-embedded inside single-quoted
string literals (14 RAISERROR messages and the OBJECT_ID probe
template) without doubling quotes, so a legal apostrophe in
@log_database_name / @log_schema_name / @log_table_name_prefix
killed the entire logging run with a compile error. Literal-position
embeds now double their quotes; the probe template gained a
{table_check_literal} placeholder so the identifier and literal
positions no longer share a value.
- @log_to_table = 1 now honors its documented log-instead-of-return
contract: the blocking/deadlock no-results messages and the
available-plans result sets are suppressed while logging.
- The @log_table_* holders were sysname but store fully-qualified,
QUOTENAME'd three-part names, silently truncating long-but-legal
names; widened to nvarchar(776). A prefix over 111 characters now
fails loudly up front instead of QUOTENAME returning NULL and the
table creation becoming a silent no-op.
Verified: installs and runs clean on SQL Server 2012, 2016, 2017,
2019, 2022, and 2025 across all @what_to_check modes and both
targets; the scheduler section returns and logs real data for the
first time; a severe error raised in a user database now names that
database; an apostrophe-bearing prefix creates all 14 log tables;
@log_retention_days = 0 retains rows; logging runs return zero
result sets. A second adversarial review pass of these fixes
returned no findings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…put tables, and cleanup A multi-agent bug hunt with adversarial verification and live-server validation confirmed ten defects by reproduction; every fix here was re-verified live, and the fixes themselves went through a second adversarial audit that caught and corrected a regression in one of them before it shipped. Filters and math: - @blocking_duration_ms over ~36 minutes overflowed int arithmetic in the session predicate builder and killed the procedure with a misleading arithmetic error before any session was created. The multiplication now converts to bigint first. The query duration filter never had this problem - its parameter is decimal(18,3), so its arithmetic was already decimal. - avg_ms_per_wait used bigint integer division at three sites, truncating fractional milliseconds and under-reporting averages. Now decimal. - The executions count doubled whenever plans were collected (@skip_plans = 0): both branches of the query aggregation carry a plan_handle, and COUNT_BIG counted showplan events as executions. An is_execution discriminator now counts only real execution events. Harvest routing: - All three harvest-loop routing CASEs (view naming, table creation, insert building) matched event types as floating substrings of the whole session name, letting @custom_name contaminate the routing: a waits session named with @custom_name = 'deadlock' could have its view pointed at a waits-shaped table, and the resulting CREATE VIEW failure aborted the entire harvest for every session. All patterns are now anchored to the event-type segment of the session name (keeper_HumanEvents_<event_type>...), which also closes the original reported gap: lock/locks/locking aliases never got the blocking view, because one CASE matched %block% where everything else matched %lock%. Output tables: - The _parameterization suffix was appended OUTSIDE the QUOTENAME brackets in both the CREATE TABLE and INSERT builders, producing an invalid object name that broke ALL compiles-to-table logging on SQL Server 2017+ with a syntax error. The suffix now joins the table name before quoting. - The create-table loop re-executed the previous iteration's CREATE TABLE when a later table already existed, aborting the harvest with "There is already an object". The execute is now guarded and the variable reset. - A pure collector run (@output_database_name set, @keep_alive = 0) created and started a throwaway session the harvest loop never reads - it only polls keeper sessions - leaving it running uselessly until a later invocation's startup cleanup reaped it. Collector runs no longer create a session; @keep_alive = 1 with an output database still does, since that is the single-call create-and-collect path. Cleanup and files: - @cleanup dropped ANY table or view whose name merely contained 'HumanEvents', in every schema of the output database - silently destroying user objects. Patterns are now anchored to the tool's own names and scoped to @output_schema_name. The accumulator also used SCHEMA_NAME(), which runs in the calling database's context and NULL-poisoned the whole DROP list for cross-database cleanup, silently cleaning nothing; it now joins the output database's sys.schemas. - Throwaway event_file runs leaked one .xel file into the LOG directory per run, forever: DROP EVENT SESSION removes only metadata, and the per-run GUID filename means max_rollover_files never prunes across runs. After the target is read and dropped, the files are now deleted via sys.xp_delete_files where it exists (present on 2017+, documented on 2022+; checked by existence, not version), with an honest message on versions that cannot delete files from T-SQL. Verified: installs and runs clean on SQL Server 2016 through 2025 across event types, targets, and output-table mode; a decoy user table and view containing 'HumanEvents' in their names survive @cleanup while tool objects drop; the poison-named waits keeper scenario that aborted the harvest now logs rows and creates the correct views; compiles-to-table logging works end to end including the parameterization table; event_file runs leave zero files behind on servers that can delete them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es, rollups, and logging A multi-agent bug hunt with two-skeptic adversarial verification reproduced sixteen defects live; two more came from a completeness critic and were verified by hand. The fixes passed a second adversarial audit, which also caught a routing mismatch in one of them before it shipped. Table-source mode: - The @max_blocking_events cap had no ORDER BY, so reading logged blocked process reports back kept the OLDEST N events in scan order and silently discarded the newest - while the summary text claimed "limited to most recent". The extraction now orders by event timestamp descending before the cap, matching the ring buffer and event file paths. - The schema/table/column/timestamp validation ran as dynamic SQL whose RAISERROR + RETURN only exited the dynamic batch - the procedure kept going with rejected parameters, cascading raw engine errors or returning results after the rejection message. The batch now reports through an OUTPUT flag the procedure checks. A return code cannot work there: RETURN with a value is illegal in an sp_executesql batch. - The blocked-process-threshold config gates refused table-source runs over a server setting that has no bearing on reading captured XML from a table. The gates now exempt calls that will actually route to table mode - the exemption mirrors the routing condition exactly, so a partial table-parameter call that falls back to an XE session still hits the gate. Rollup correctness: - The Top Lead Blocker finding filtered on an object name re-derived from the event-level object_id, which is unreliable for KEY, RID, and PAGE locks - so drilling into the exact object the tool itself reported made the root-cause finding vanish. It now filters on the same wait_resource-decoded object every other check uses. - One physical contention was double-counted in the per-object rollups: the victim row resolved to the real object while its paired lead-blocker row fell to the Unresolved sentinel. Resolved objects now propagate onto their paired unresolved rows. - Total lock wait time could report a blocker's own unrelated wait (a WAITFOR, an I/O) as lock wait, because both sides of a report share the event's transaction_id. The rollups now aggregate victim rows only. - The Top Lead Blocker percentage divided by a windowed total computed after the HAVING filter, overstating every share. The denominator is now the true pre-HAVING grand total. Decoding and output: - One deallocated page voided PAGE/RID object resolution for an entire database: sys.dm_db_page_info raises rather than returning NULL, so the set-based apply died on the first stale page and the surrounding CATCH swallowed everything. Resolution now runs one page per TRY/CATCH, so a bad page loses only itself. - Twelve finding texts went entirely blank when DB_NAME() returned NULL; a missing monitorLoop attribute dropped the lead-blocker analysis; an empty capture window blanked the header finding; and the @help introduction split into two result sets on a missing UNION ALL. All guarded or corrected. Logging: - A long @log_table_name_prefix made QUOTENAME return NULL, which turned every logging statement into a silent no-op - no table, no rows, no error. Prefixes are now validated up front, by DATALENGTH so trailing spaces cannot sneak past the identifier limit. The name holder was also sysname, truncating fully-qualified names; now nvarchar(776). An apostrophe in the logging identifiers broke the DDL batch; literal-position embeds now double their quotes, and the watermark probe template separates its literal and identifier placeholders. - The first-run dedup watermark was UTC-shifted while the event_time column it stands in for is stored in local time, and it hardcoded a one-day floor - so a first logging run could silently drop part or all of the caller's requested window. The fallback now rides in as a parameter carrying the caller's local-domain window start. Verified: installs and runs clean on SQL Server 2016 through 2025; a ten-event fixture with a cap of three returns the three newest events where it returned the three oldest; a rejected table-source call halts with zero leaked output; decoy user objects survive while tool objects drop; the help introduction is one result set again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nerated DDL This procedure emits cleanup scripts that users execute against production, so a wrong recommendation is not a cosmetic bug - it destroys indexes. A multi-agent hunt with adversarial verification reproduced fifteen defects by building fixtures, running the proc, executing the emitted DDL, and diffing the index metadata. Twelve are fixed here. A second adversarial audit of these fixes caught two HIGH regressions the fixes themselves introduced; both are corrected below. Recommendations that destroyed data: - A filtered index could be promoted to replace a UNIQUE CONSTRAINT. The emitted script dropped the constraint and created a filtered unique index in its place, which enforces uniqueness only over rows matching its predicate - a duplicate outside the filter then inserted cleanly. Filtered indexes are now excluded from constraint replacement. - A constraint-backed index could be chosen as the wider target of an include merge. The emitted CREATE INDEX ... DROP_EXISTING against a constraint fails outright (Msg 1907), but the paired DISABLE of the subset still ran, silently removing a covering index nothing replaced. Constraints are now excluded as merge targets, with a backstop on the script generator. - For partitioned indexes, the partitioning column rides along in sys.index_columns at key_ordinal = 0 when it is not a real key, and the key-column aggregations swept it in as a phantom LEADING key. The emitted DROP_EXISTING rebuild therefore re-keyed the very index the tool told the user to KEEP - (a, b) became (pcol, a, b), so it could no longer seek on a. It also poisoned duplicate and subset hashing, producing false matches. Five aggregations now filter on key_ordinal > 0, matching the guard the proc already used elsewhere. - Removing that phantom key exposed a second hazard it had been accidentally masking: a non-aligned UNIQUE CONSTRAINT paired with a partition-aligned index on the same keys now matched, and the emitted CREATE UNIQUE failed with Msg 1908 (a unique index's partitioning column must be part of its key) while the paired DROP CONSTRAINT succeeded - leaving no uniqueness at all. Any index whose partitioning column is absent from its key is now excluded from unique promotion. The aligned case still works. Filters and parameters: - @min_reads and @min_writes were inert unless both were set: each was OR'd against the other, and the unset one defaulted to zero, so SUM(...) >= 0 was always true. Each term is now gated on its own threshold. - Bit parameters were not NULL-guarded, and the defaults were applied too late to matter. A NULL @dedupe_only slipped past the low-uptime safety check (NULL = 0 is UNKNOWN, so it never fired), and the procedure went on to recommend dropping unused indexes based on a few days of usage statistics. The defaults now run ahead of every consumer, including that guard. - Database names containing &, < or > crashed the whole run: the comma-separated include and exclude lists are split by building an XML document, and those characters made it malformed. They are now escaped before the document is built (ampersand first, or the escaping escapes itself). - PARSENAME was used to strip brackets from @database_name, but it also parses dotted names, so a database called Foo.Bar resolved to Bar. Replaced with a true surrounding-bracket strip. - @min_rows compared each partition individually while the object pre-filter summed them, so the two disagreed about what the threshold meant. Both now sum. - A partition column needing brackets produced invalid DDL in generated ON clauses; it is now quoted. The SUMMARY row rendered NULL instead of 0 for an empty scope. Verified: installs clean on SQL Server 2016 through 2025; a partitioned keeper now emits ([a], [b]) INCLUDE ([c]) ... ON [scheme]([pcol]) instead of ([pcol], [a], [b]); both unique-constraint hazards emit nothing while legitimate merges, subset disables, and aligned constraint replacements still emit; a NULL @dedupe_only now trips the seven-day uptime protection instead of bypassing it; & in a database list no longer crashes the run. Not changed, by author's decision: rebuild scripts continue to set FILLFACTOR = 100 and omit PAD_INDEX / ALLOW_PAGE_LOCKS / STATISTICS_NORECOMPUTE, and IGNORE_DUP_KEY remains outside the duplicate hash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t wasn't This procedure is a detector, so its dangerous failure mode is silence: an empty result set reads like good news, and nobody investigates. Every fix here closes a path where it found less than it should have. Silent no-op: - @days_back = NULL with no date range made DATEADD return NULL, which made every generated command NULL, and sys.sp_executesql runs a NULL batch without complaint. The procedure opened no log files, matched nothing, and returned a clean empty result. It now falls back to the documented -7 default. A large negative @days_back is also clamped, so reaching for "everything" no longer overflows DATEADD and aborts. The date window: - Supplying only @start_date (or only @end_date) left @days_back alive at -7. The check that retires it required BOTH dates, but the procedure fills in whichever one is missing immediately afterwards - so the check ran too early and always lost. The log file pruner then deleted every archive older than a week, meaning a search for an incident three months ago quietly threw away the only file containing it. The check now runs after the fixups and fires if either date is supplied. - The pruner itself deleted any archive whose timestamp fell outside the window. But sys.sp_enumerrorlogs reports each file's LAST write, so a file stamped after @end_date still holds entries from before it - and that file is exactly the one straddling the end of the window, the most likely place for the incident being investigated. An archive's start is the previous archive's last write, so the pruner now computes the real span of each file and drops one only if it ended before the window opened or began after it closed. - #enum.log_date was typed date, flooring each archive's last write to midnight. It is now datetime - but sp_enumerrorlogs also truncates that value to the minute, and the file's true final entry lands in the following 59 seconds, so the start bound carries a one-minute grace. Without it, exact comparison prunes archives that really do reach into the window: on a test server that dropped a "significant part of sql server process memory has been paged out" warning. Searching: - Every generated command passed a literal " " as xp_readerrorlog's second search string, which the extended procedure ANDs with the first. Every search therefore silently required the matched line to contain a space, including @custom_message. It now passes NULL. - @first_log_only kept archives 0 and 1. Archive 0 is the current log, so "only the first log" was searching two of them. - @start_date and @end_date were handed to xp_readerrorlog with CONVERT style 0, which drops seconds and lost entries in the window's final partial minute. Now style 121. Honesty: - @language_id is validated against sys.messages and then never used: all 88 search strings are English literals matched against raw log text. Pointed at a non-English server it found nothing and explained nothing. It now warns that the corpus will not translate and suggests @custom_message, rather than handing back an empty result set that reads like a clean bill of health. Verified on SQL Server 2016 through 2025 across every parameter mode. The search corpus itself is unchanged: a few strings that cannot match, and a few that match too much, are editorial calls left to the author. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…check Every fix here closes a way the tool either lost logged data or told a DBA there was no pressure when there was. A multi-agent hunt with adversarial verification reproduced each one live; a second adversarial review of the fixes themselves confirmed them and caught two of my own mistakes before this commit (an over-doubled quote level and a comment left inside a dynamic SQL literal that truncated a generated batch). Logging subsystem: - @log_table_name_prefix had no NULL guard, unlike the database and schema names. A NULL (an automation variable that resolved to NULL) made QUOTENAME(NULL + N'_Waits') NULL, which nulled every CREATE / INSERT / DELETE command string — and sp_executesql runs a NULL batch as a silent no-op. The proc returned success having created no tables and logged nothing, with every result set suppressed because it was in logging mode. Now defaults to 'PressureDetector'. - An apostrophe in a log identifier aborted the run at parse time (Msg 102, even at @debug = 0): the fully-qualified name was re-embedded into the debug RAISERROR's single-quoted literal without doubling its quotes. Fixed at all nine embed sites. - The nine assembled-name variables were sysname, but they hold three-part QUOTENAME'd names; a long-but-legal name truncated and the CREATE failed partway through setup. Widened to nvarchar(1024), plus an up-front cap so a prefix too long to fit its suffix in a 128-char identifier fails loudly instead of half-creating. Reports "no pressure" when there is some: - @skip_plan_xml = 1 silently discarded the entire "queries with memory grants" result set: the outer CASE that builds the plan column had no ELSE, so skipping the plan XML made the whole @mem_sql NULL. Added the ELSE the CPU builder already has. - The perfmon panel filtered on total_per_second <> '0', but that value is cntr_value / uptime-in-seconds in integer arithmetic, so every counter smaller than the server's uptime floored to '0' and was dropped — including the point-in-time pressure gauges (Memory Grants Pending, Processes Blocked, deadlocks). Filters on the raw counter now. - A lock timeout while fetching a query plan aborted the whole procedure. The plan-fetch batches SET LOCK_TIMEOUT to fail fast, but with XACT_ABORT ON and no handler the resulting error 1222 — plausible under the very plan-cache churn being diagnosed — took down every section after it. The four plan-fetch executes are now wrapped in TRY/CATCH and degrade to a warning. Wrong numbers: - Sample-mode disk latency divided bigint stalls by bigint counts inside a decimal CONVERT, so the integer division truncated every latency to a whole millisecond (and to 0 on fast disks). Forced decimal division. - avg_runnable_tasks_count averaged an integer column, truncating fractional CPU queuing to a whole number; and the runnable percentage rounded the ratio to two decimals before multiplying by 100, zeroing every percentage below ~0.5%. Both now compute in decimal. New check: - A separate external-CPU signal: the box saturated by processes OTHER than SQL Server. The scheduler-monitor panel already computed other-process and total CPU but only ever filtered on SQL's own utilization, so a machine pegged by another instance / AV / backup agent reported "no significant CPU usage" while SQL was scheduled off the CPU. Surfaced as its own result column, and unioned into the _CPUEvents log table so headless/logged deployments get it too. Verified on SQL Server 2016 through 2025 across all @what_to_check modes, sample mode, blocking mode, and logging mode; an apostrophe-bearing log prefix now creates all nine tables; a NULL prefix defaults instead of silently logging nothing; low-value perfmon counters that were dropped by integer underflow now surface; a lock timeout during plan fetch degrades to a warning instead of aborting the run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This procedure emits server health/perf findings a DBA acts on, so a check that misses a real problem, fires falsely, or recommends the impossible is worse than no check. A multi-agent hunt reproduced each of these live; a second adversarial review of the fixes then caught four more defects in the same code, corrected here too. Silent false negatives: - Check 7011 (Query Store state) filtered actual_state IN (0, 3), which excludes READ_ONLY (state 1). A Query Store that fills to its max size auto-flips to READ_ONLY and stops capturing new data - its single most common operational failure - and no Query Store check fired for it, so the report came back clean. Now IN (0, 1, 3); Availability Group secondaries and intentional read-only are still excluded by the existing predicates. - @database_name pointing at a database that does not exist (a typo) or is not ONLINE (mid-restore, RECOVERY_PENDING, SUSPECT, a log-shipping standby) matched no rows and produced no database findings and no error, reading as a clean report on a database that was never looked at. It is now validated up front, before any check runs, and fails loudly. Crashes and false positives: - wait_time_percent_of_uptime was decimal(6, 2). It is cumulative wait time across all schedulers as a percentage of wall-clock uptime, so on a many-core server it runs to thousands of percent, overflowed 9999.99, and threw out of the whole procedure - no findings at all - on exactly the busy servers that most need checking. Widened to decimal(38, 2). - The SQL Server 2025 database-scoped-configuration checks (7020) used wrong configuration_ids: 40/41 are READABLE_SECONDARY_TEMPORARY_STATS_* (no matching evaluation, so flagged non-default on every 2025 database), 43 does not exist, and the three options actually evaluated (OPTIMIZED_SP_EXECUTESQL, FULLTEXT_INDEX_VERSION, OPTIONAL_PARAMETER_OPTIMIZATION) live at 42/44/47 and were never pulled. Corrected against the live 2025 catalog; those ids do not exist on 2016-2022 so no earlier version is affected. PREVIEW_FEATURES was dropped (its coded default disagreed with the shipped default). - DOP_FEEDBACK's default flipped from 0 (2019-2022) to 1 (2025), so the fixed default flagged it non-default on every 2025 database. The default is now version-gated. Impossible advice: - Check 7009 recommended enabling Accelerated Database Recovery on SQL Server 2016/2017, where ADR does not exist. The absent column is hardcoded to 0 there, so the check fired for any database with snapshot isolation or RCSI - most of them. Now gated on the column existing. - The Lock Pages in Memory finding fires at 32GB of RAM but its text said the recommendation was for more than 64GB. Text aligned to 32GB. Also: check 7011 now renders a clear message for an explicitly read-only database instead of "Unknown reason code: 1". Verified on SQL Server 2016 through 2025: a full/read-only Query Store is now flagged; a bad or offline @database_name errors clearly instead of returning a false clean report; the wait-percent overflow is gone; 7020 produces no false positives on 2025 and still catches genuinely non-default settings; and 7009 no longer recommends ADR where it does not exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. Ignore-list shred referenced an ambiguous column x inside the generated dynamic SQL (a.x is the whole <x>..</x> document, b.x is each shredded node). It raised error 209 at compile time, so @ignore_plan_ids and @ignore_query_ids silently filtered nothing. Qualify as b.x.value. 2. The "fill in parameters from query text" step split the auto-parameterized prefix on every comma, tearing scaled types (numeric(10,2)) in half and blowing up LEFT with error 537, aborting the whole procedure for any query with a scaled-type parameter. Split on the ',@' parameter boundary instead. 3. @procedure_name_quoted was bound to sp_executesql as sysname while its variable is nvarchar(1024). A quoted three-part name over 128 chars was truncated, OBJECT_ID failed, and a valid procedure was falsely reported as absent. Bind as nvarchar(1024), matching the four other bindings. 4. In @query_plan_xml mode, a document with no StmtSimple node produced a silent blank repro. Raise an informational error and return instead. 5. Schema parsing only matched fully bracketed [schema].[proc]; an unbracketed dbo.proc fell through and was QUOTENAME'd whole into a single mangled [dbo.proc] identifier. Use PARSENAME, which handles both forms, guarded to exactly two parts so a stray three-part name doesn't drop its database. Verified: compiles clean on SQL 2016-2025; unit and end-to-end tests confirm each fix on all five versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Object names: 1. The schema.procedure overload only matched a fully-bracketed [s].[p]; an unbracketed dbo.proc fell through and was QUOTENAME'd whole into a mangled [dbo.proc] that never resolved. Use PARSENAME (handles both forms), run it before the @database_name default, and adopt a three-part paste's own database instead of dropping it (wrong-database lookup) or failing. 2. @procedure_name_quoted was bound to sp_executesql as sysname (128 chars) at three sites though its variable is nvarchar(1024); a long quoted three-part name truncated, OBJECT_ID failed, and a real procedure was reported absent. The nine @log_table_* name variables had the same sysname truncation. Crashes: 3. The reusable @string_split_ints/@string_split_strings lacked the empty-element filter the @include_databases splitter has, so a leading/trailing/doubled comma produced a NULL row that aborted the INSERT into NOT NULL key columns with Msg 515. Added WHERE ids.ids IS NOT NULL. 4. @include_databases/@exclude_databases built XML by raw concatenation without escaping & < >, so a legal database name like R&D threw Msg 9411 and aborted the procedure. Escape entities before wrapping in tags. Silent wrong results: 5/11. Three instance-level validations (hints/feedback/variants pre-2022, wait stats pre-2017, invalid wait category) only returned when @get_all_databases = 0, so in all-databases mode they warned then produced unfiltered results or NULL'd the wait SQL to zero rows. They now halt in both modes; the per-database lenient checks are unchanged. 6. @sort_order varchar(20) truncated the documented 'average <metric>' forms before normalization, silently reverting to cpu. Widened to varchar(50). 7. The regression metric for 'executions' averaged the bigint count_executions before the float cast, truncating the per-interval mean. Cast to float inside AVG (and the same for the waits fallback), at both metric sites. 8. @find_high_impact weekend bucketing used DATEPART(WEEKDAY,...) IN (1,7), correct only under @@DATEFIRST = 7; on DATEFIRST = 1 it mislabeled Monday as Weekend and Saturday as a weekday. Use DATEDIFF(DAY, 0, local) % 7 IN (5, 6), which is DATEFIRST-independent, at both the SELECT and GROUP BY. 10. @find_high_impact avg_rows was bigint, so total_rows/total_executions integer-divided and truncated (3/4 -> 0) unlike every decimal sibling. Made it decimal(38,6). 12. The primary-window percentage was CONVERT(integer,...) then tested '> 50', so a real 50.1-50.99% majority truncated to 50 and was mislabeled 'Spread'. Use decimal(5,1). Logging: G1. With @log_to_table = 1 and the default @expert_mode = 0, only 1 of 9 log tables was populated; the other 8 sat behind expert-mode output gates. Added OR @log_to_table = 1 to the seven gates so all tables capture when logging; the display/empty paths are @log_to_table = 0 guarded so no result sets leak. Verified: compiles clean on SQL 2016-2025; unit tests, a six-scenario live battery, and an end-to-end @log_to_table run confirm each fix; independent review clean. Also verified (no fix needed): the log INSERT/SELECT/CREATE column lists align on every version, and no sp_executesql references an undeclared variable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y search-text docs #9 (@workdays weekday): the filter computed the weekday from the raw UTC last_execution_time while the work-hour window was shifted into local-relative UTC, so a query run on a local weekday morning that lands on the previous UTC day (far-east time zones, e.g. Sydney Monday 09:30 = Sunday 22:30 UTC) was dropped as the wrong day. It also only supported @@DATEFIRST 1 and 7, skipping the weekday filter with a warning on any other server. Now the weekday is derived from the local execution time (UTC shifted back by -@utc_minutes_difference, embedded as a literal since it isn't in the shared @parameters list) using DATEDIFF(DAY, 0, local) % 7 BETWEEN 0 AND 4, which is @@DATEFIRST-independent (0 = Monday .. 4 = Friday) and works on every server. G5 (docs): @query_text_search / @query_text_search_not are matched with LIKE, so _ % [ ] act as wildcards by default. Clarified the parameter comments and the help output to say so, and corrected the @escape_brackets description (it makes [, ], and _ literal; % stays a wildcard). Behavior unchanged. Verified: compiles clean on SQL 2016-2025; a cross-time-zone unit test confirms the local-weekday predicate (Sydney Monday-morning case now included); @workdays=1 runs clean with no Msg 137 for the embedded offset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eStore When the search window contained nothing, the main output selected from an empty #repro_queries and returned a silent, unexplained empty result set. Match sp_QuickieStore's behavior: guard the output with IF EXISTS and, when the table is empty, return a single 'result = #repro_queries is empty' row so the caller knows nothing was found (e.g. widen @start_date/@end_date) instead of guessing. Verified: compiles clean on SQL 2016-2025; a future-dated (empty) window now returns the diagnostic and a populated window still returns the normal results. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four defects, all sharing one failure mode: a merge script that cannot execute
paired with a DISABLE that can, so the loser's covering column disappears with
nothing absorbing it.
Single-winner key duplicates never merged their includes. Rule 5 settles a
unique-vs-non-unique pair in one pass, marking the unique index MERGE INCLUDES
and the loser DISABLE. That leaves one MERGE INCLUDES row, and
#key_duplicate_dedupe only collects groups holding more than one, so nothing
merged the loser's includes in. The winner was then downgraded to KEEP for
having no merged includes while the loser's DISABLE still ran. A plain unique
index accepts CREATE UNIQUE INDEX ... INCLUDE (...) WITH (DROP_EXISTING = ON) --
unlike a constraint-backed one, which fails with Msg 1907 -- so the merge is
legal and now happens.
FOR XML PATH('') re-encoded entities on the way out at six sites. A column named
[A&B] came back [A&B] and the script died with Msg 1911. Added TYPE plus
.value(), matching the pattern already used elsewhere in the procedure, and
escaped &, < and > before the CONVERT(xml, ...) at the three sites that split
include lists, which otherwise fail on the way in with Msg 9411.
XML node values were extracted as sysname. A column name does max out at 128
characters, but these values are already QUOTENAME'd, and QUOTENAME returns
nvarchar(258): a 128-character name arrives 130 characters long and lost its
closing bracket, so the script failed with Msg 103. Widened all ten extractions
to nvarchar(258).
@min_reads and @min_writes only ever applied per object. The filter behind
#filtered_objects groups sys.dm_db_index_usage_stats by object_id and sums usage
across every index on the table, so a busy table dragged in all of its indexes
regardless of the floor. They now also apply per index, via a meets_usage_floor
flag the dedupe rules check.
The flag is marked rather than deleted, and set after Rule 1, both deliberately.
Nonclustered compression reaches indexes through #index_analysis, so deleting the
row would take its compression recommendation with it, and compression
eligibility has nothing to do with usage. Rule 1 does not read the flag, so
unused-index detection still sees every index; screening ahead of it would make
it unreachable, since reads = 0 is a strict subset of everything a reads floor
removes. Help text updated to describe both levels.
Tests: added Erik's fixture scripts, previously kept outside the repo, plus a
README covering prerequisites, running order, and the baseline-diff technique.
The Expected: comments in them are the specification -- two of them caught the
bugs above. Bounded the WHILE 1 = 1 read loops, dropped the sp_BlitzIndex
dependency, and untracked a committed .pyc.
Corrected two run_tests.py assertions that encoded the pre-Msg-1907 behavior:
they asserted a merge into a unique constraint that SQL Server refuses to run.
Verified on 2016/2017/2019/2022/2025: compiles clean; run_tests.py 28/28;
no_access_test.py 4/4; generated merge scripts execute against real fixtures with
the includes landing; baseline diff versus HEAD shows only intended changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lds, trace-flag interpretation Health-check depth on the cumulative/config signals (live problems remain sp_PressureDetector's job). Wait stats (6001): stop labeling every wait a flat "High Impact Wait Type". Name the finding by category so the list reads as a diagnosis (Storage / Lock / Memory / TempDB / CPU / Transaction Log / Parallelism ...), put the wait type and its meaning in object_name, and calibrate severity by category: resource-pressure waits earn High by dominating uptime OR by long average waits; parallelism tops out at Medium (it's a cost-threshold/MAXDOP symptom); the rest stay Low. Idle-box CXCONSUMER drops from a flagged wait to a Low "Parallelism Waits" note. Storage (3001/3002/3003): the slow-read/write thresholds defaulted to 500ms, which is catastrophic-storage territory, so real latency never surfaced. Drop to 20ms (data-file I/O should be under 20ms), with High at 5x (100ms). Memory grants (4101/4103): these are cumulative counts since startup, so firing High on any count > 0 over-reacts to a handful of transient events. Scale severity with magnitude instead. Trace flags (1012, new): the global flags were only listed in server-info. Interpret the notable ones with per-flag meaning and severity -- 1211/3608/ 3609 High, 1224/834 Medium, redundant-on-2016+ ones (1117/1118/2371) and behavior-changers (4199/8048) Low -- leaving benign flags in the list only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inaccessible-database tests took a database OFFLINE without ROLLBACK IMMEDIATE, so any open connection made the ALTER wait indefinitely and the suite hung (300s subprocess timeout). Force it, so the transition is immediate and the test can't hang in CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The values calibrated in Phase 2 were still hardcoded. Expose the tunable ones as parameters (defaults = the Phase 2 values, so default behavior is unchanged), letting a caller adjust to their environment: @slow_read_ms / @slow_write_ms storage latency (ms) @significant_wait_threshold_pct floor for a wait to be reported @wait_high_pct / @wait_medium_pct resource-wait severity bands @memory_grant_warning / _critical forced-grant severity bands Each defaults NULL or negative back to its documented value, so a caller can override just the ones they care about. Help text (description / valid inputs / defaults) covers all seven. The secondary avg-ms wait bands and the grant- timeout bands stay internal for now. Compiles on all five versions; the assertion harness passes 39/39. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sp_QuickieStore had no automated coverage at all, and it is the proc where compiling proves least: ~16,000 lines that assemble a large dynamic SQL statement whose shape changes with almost every one of its 59 parameters. @sort_order alone accepts 39 values, each building a different ORDER BY (the wait sorts additionally join query_store_wait_stats), and @wait_filter / @execution_type_desc / @query_type / @expert_mode / @format_output each rewrite the statement again. A combination nobody has run assembles SQL that fails only at EXECUTION. So the harness is a parameter matrix that executes what each combination built: all 39 sort orders, 9 wait filters, 4 execution types, 4 query types, and the expert/format grid. On top of that, bidirectional assertions prove the filters actually filter rather than being ignored (@query_text_search on a known string vs nonsense, @query_type partitioning proc from ad hoc, @top, @execution_count set impossibly high), each absence paired with a completion check so an errored run cannot pass vacuously. 134 assertions; green on 2017, 2022 and 2025. It builds its own Query Store scratch database with a varied workload and drops it in a finally block. Nothing outside that database is touched. @debug = 1 is exercised against a database with no Query Store data on purpose: with data, debug mode returns the generated SQL as an XML column, and go-sqlcmd renders XML columns slowly enough that the server parks in ASYNC_NETWORK_IO and it looks exactly like a hang. That is a client limitation - the same run is instant in SSMS and a non-debug run at full width returns in under a second. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dd harness
sp_LogHunter builds an xp_readerrorlog command string per search term and
executes each through sp_executesql, swallowing any that throw into #errors
rather than raising them. A failing search therefore returns a clean-looking
empty result set rather than an error.
Search arguments were wrapped in double quotes, which under QUOTED_IDENTIFIER
ON parse as IDENTIFIERS -- capped at 128 characters. A @custom_message longer
than that failed with Msg 103 ("The identifier that starts with ... is too
long"), went into #errors, and handed the caller an empty result set that reads
exactly like a clean bill of health. A 150-character search is not exotic;
pasting an error message fragment gets you there.
Emit N'...' instead. Both halves matter, and the second is why double quotes
were used originally: xp_readerrorlog is an extended stored procedure and does
not implicitly convert, so a bare single-quoted (varchar) argument fails with
Msg 22004 "Invalid Parameter Type" at severity 12 -- quiet enough to look like
finding nothing. Quoted identifiers happened to satisfy the Unicode
requirement; N'...' satisfies it deliberately, with no length limit.
Also widens #search.current_date from nvarchar(10) to nvarchar(12), since
N'20260720' is 11 characters and would have truncated to an unterminated
literal.
Adds sp_LogHunter/tests/run_tests.py (78 assertions) and wires it into CI:
- #errors must stay empty across the parameter matrix, which regression-tests
the four generated-command bugs already documented in the source
- a real positive control: RAISERROR(marker, 10, 1) WITH LOG writes a known
string to the error log, so "found when searched for" / "not found when never
written" / "not found by a default run" are all bidirectional
- date ranges and @custom_message_only proven the same way
The harness checks for Level 12 and above rather than the Level 16 its siblings
use, because Msg 22004 is severity 12 and a Level-16 filter walks past it.
Verified 78/78 on 2016, 2017, 2019, 2022 and 2025. Mutation-checked against the
pre-fix procedure: exactly the two long-message assertions fail there and the
other 76 still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Azure SQL DB, sys.databases.database_id is scoped to the logical server while DB_ID(), DB_NAME(), and the database_id column in every DMV are scoped to the database/elastic pool, and the two id spaces do not have to agree (documented in the DB_ID() remarks). The procedure stored the sys.databases id in #databases, so on Hyperscale DB_NAME(@database_id) resolved to NULL and the first insert into a NOT NULL database_name column died with Msg 515. The same mismatch made fo.database_id = os.database_id match nothing, silently emptying #operational_stats. Fix, direction-proof on both counts: - #databases now stores ISNULL(DB_ID(d.name), d.database_id) - the local-space id, identical on box product - so every DMV predicate (dm_db_index_operational_stats, dm_db_index_usage_stats, the operational stats join) compares within one id space. - Names are never derived from ids anymore: every DB_NAME(@database_id) / DB_NAME(os.database_id) / QUOTENAME(DB_NAME(@current_database_id)) site now uses @current_database_name, parameterized into the dynamic SQL. Verified: compiles on SQL 2017/2022/2025; full local harness on 2022 passes (adversarial 32/32, fixture-cases 31/31 including the execute check, rule-coverage 40/40 covering @get_all_databases, no-access 4/4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atabase-id sp_IndexCleanup: fix NULL database_name on Azure SQL DB
…locking data The contentious-object inheritance UPDATE added in ae881f6 self-joins #blocks on monitor_loop/blocking_desc/blocked_desc, but the SELECT INTO #blocks projection never carried those columns, so the statement failed to compile whenever the session had actually captured blocking. CI missed it because with no blocking data the proc exits before that statement compiles. Add the three chain identity keys to the #blocks projection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs a query or procedure and writes every result set to CSV, spilling execution plans, deadlock graphs, blocked process reports, and long query text or scripts to correctly-typed files (.sqlplan/.xdl/.xml/.sql). Classifies values by XML root element, strips the processing-instruction wrappers the toolkit procs use for clickable SSMS output, dedupes identical artifacts by content hash, captures RAISERROR WITH NOWAIT progress to messages.log, and honors GO batches. No module dependencies - System.Data.SqlClient only - works in Windows PowerShell 5.1 and PowerShell 7. Tested against all ten procs on SQL Server 2025. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds automatic Claude code reviews on pull requests into dev, plus an interactive workflow that responds to @claude mentions in issues and PR comments (also the way to request reviews on fork PRs, which cannot see repo secrets). Both authenticate with the CLAUDE_CODE_OAUTH_TOKEN secret. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Claude Code automatic PR reviews
Finds plan shapes (query_hash + query_plan_hash) whose runtime metrics swing wildly across executions of the same plan. Ranks shapes by an execution-weighted coefficient of variation built from the previously unused stdev columns, feeds statistics from regular executions only (aborted/exception counted separately), and annotates signals: parameter sensitive, row driven, intermittent waits, memory and tempdb swings, timeouts, PSP variants, and cross-shape divergence. Takeover mode like @find_high_impact: summary result set, one row per shape, plan XML, top waits on 2017+, variant flags on 2022+. Also adds primary keys to both modes' staging temp tables and makes object-name resolution deterministic in both. Tested: 157-assertion matrix on SQL Server 2022 (23 new assertions, including a skewed-workload fixture where the parameter sensitive signal must fire); clean installs and version-gated runs on 2016/2017/2019/2022/2025; real-data validation against StackOverflow databases on 2019, 2022, and 2025. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ameter-sensitive sp_QuickieStore: add @find_parameter_sensitive mode
Help no longer pretends @language_id translates anything: the description and valid inputs now match the runtime warning that the search strings are English literals. #errors gains an error_message column populated from ERROR_MESSAGE() in the CATCH, so failed searches explain themselves instead of reading like a clean bill of health. The custom-message quoting comment now states the real remaining ceiling (the nvarchar(4000) command column). Six comments rewritten to describe the code as it is instead of narrating the previous revision. Verified on SQL Server 2022: default run, help output, apostrophe custom message, and an induced over-length failure that lands in #errors with its error message attached. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An explicit NULL @log_table_name_prefix now defaults to HealthParser instead of walking past the length guard into QUOTENAME(NULL), which silently no-opped every logging table (verified: 14 tables created where before there were zero). Help text documents the 111-character prefix limit. Two query_text fallbacks switch ISNULL to COALESCE so the fallback is not typed down to nvarchar(257) and truncated, with comments explaining why COALESCE is load-bearing. The two logging validation errors now say the procedure stops instead of claiming logging will be disabled. Header gains the standard @help/@debug blocks and the help intro now carries proper support URLs, matching every sibling proc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CPU logging insert no longer double-logs every sample: both CPU XML documents now stamp sample_time from a single frozen SYSDATETIME() and ms_ticks reading, so the UNION actually collapses samples that tripped both thresholds (verified: 256 logged rows with 256 distinct sample times, where the same run previously wrote 512). The unused dm_os_sys_info join drops out of both builders. Help documents the 112-character @log_table_name_prefix cap, three user-visible utilization typos are fixed, and five comments are rewritten to state their constraints instead of narrating the previous revision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The help section now prints the MIT license block like every sibling proc, instead of promising licensing details and returning without them, and the help footer points at the support URL rather than an email address. The fill-in parameter splitter handles the comma-space form hand-written sp_executesql calls store in Query Store, so a second parameter no longer vanishes from the value list; the harness assertion is tightened to the placeholder count that actually discriminates. @end_date_original now gets the same seven days as @end_date when the date correction fires, mirroring the sp_QuickieStore fix. #parameter_shred joins the @debug dumps, and one newly written nvarchar(MAX) drops to lowercase. Harness: 239 passed, 0 failed on SQL Server 2022. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@end_date_original now gets the same seven days as @end_date when the date-order correction fires, so the logging tables and debug output record the window the queries actually used instead of understating it by six days. Help gains the ranked_on column in the shape volatility summary guide, matches the timed out or cancelled signal text the code emits, and both takeover modes now say exactly which parameters they honor so silently ignored filters stop reading as filtered results. Matrix: 157 passed, 0 failed on SQL Server 2022. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two logging validation errors now say the procedure stops instead of claiming logging will be disabled while RETURNing, and both become proper Unicode literals. The blocked-process-threshold exemption comment now explains the auto-promotion that makes its condition correct, instead of describing a fallback path that does not exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Startup session cleanup anchors its LIKE with an escaped underscore so a user session named HumanEventsAudit is no longer dropped on every run; keeper sessions are deliberately still exempt at startup, since dropping them would break @keep_alive. The three logged-path waits views no longer truncate avg_ms_per_wait with bigint integer division: each hex-encoded view body is re-encoded with the same decimal(38,2) conversion the interactive path uses, verified by decoding the new blobs, byte-comparing everything but the fixed line, and reading the resulting column metadata back off created views. The shutdown CATCH guards its stop and drop statements so under XACT_ABORT ON the original error reaches THROW instead of being lost entirely. Also: @keep_prepare_rpc documents that results require lowering @query_duration_ms and lists the full RPC filter, megabyte columns get their _mb suffixes, the parameterization view UPDATE excludes its own output row, nine keeper LIKE patterns gain underscore escapes, and the executions aggregate returns bigint to match the logged views. Harness: 195 passed, 0 failed on SQL Server 2022. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README is rebuilt against the code: all twelve parameters documented (seven tuning thresholds were missing), four removed checks deleted (1000, 4107, 7005, 7007), four undocumented checks added (1002, 1012, 5000, 5004), and every stale threshold and priority corrected, including the renamed cost-threshold and per-category wait findings. The stray UTF-8 BOM is stripped. #results.object_name widens to nvarchar(500) so a blob-storage file URL plus its type suffix cannot abort the whole run with a truncation error. The three wait threshold parameters widen to decimal(38, 2) to match the column they compare against, since percent-of-uptime runs to thousands on many-core servers. Check 9998 gets priority 50 so it renders as Informational instead of Unknown. The tests README now describes the harness that exists, including its CI wiring. New-code style is brought in line and five past-tense comments now state their constraints in present tense. Harness: 39 passed, 0 failed on SQL Server 2022. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every combined-install proc changed since Updates_20260701 moves to the August minor and @version_date = 20260801: HealthParser 3.8, HumanEvents 7.8, HumanEventsBlockViewer 5.8, IndexCleanup 2.8, LogHunter 3.8, PerfCheck 2.8, PressureDetector 6.8, QueryReproBuilder 1.8, QuickieCache 1.8, QuickieStore 6.8. sp_QueryStoreCleanup is not in the combined install and keeps its stamp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Updates_20260801 release prep: version bumps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
August 2026 release promotion. Everything on dev since Updates_20260701:
New feature
Release audits — every proc changed this window was audited by a dedicated agent plus a cross-proc consistency sweep; all blockers and warnings fixed and verified:
Version bumps (#846): all ten combined-install procs to .8, @version_date = 20260801
Also riding along: July fix train (sp_PerfCheck phases 1-3, sp_IndexCleanup rules fixes, sp_LogHunter hardening, BlockViewer Msg 207 fix), four proc test harnesses wired into CI, Claude PR review workflows, Export-SqlResults script
Install-All/DarlingData.sql: not regenerated here; CI rebuilds it on main after merge.
Harness state: QuickieStore 157/157, ReproBuilder 239/239, HumanEvents 195/195, PerfCheck 39/39, all on SQL Server 2022.
🤖 Generated with Claude Code