Skip to content

Promote dev to main: Updates_20260801 - #847

Merged
erikdarlingdata merged 100 commits into
mainfrom
dev
Jul 29, 2026
Merged

Promote dev to main: Updates_20260801#847
erikdarlingdata merged 100 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

August 2026 release promotion. Everything on dev since Updates_20260701:

New feature

  • sp_QuickieStore @find_parameter_sensitive (sp_QuickieStore: add @find_parameter_sensitive mode #845): plan-shape volatility mode — groups by query_hash + query_plan_hash, ranks by execution-weighted coefficient of variation from the stdev columns, splits regular from aborted/exception executions, signals parameter sensitivity vs row-driven work vs intermittent waits, flags PSP variants and cross-shape divergence

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:

  • sp_HealthParser: NULL logging-prefix silent no-op, COALESCE truncation fixes, header/help consistency
  • sp_LogHunter: @language_id help honesty, #errors now records ERROR_MESSAGE()
  • sp_PressureDetector: CPU logging no longer double-writes every sample (frozen sample-time inputs)
  • sp_QueryReproBuilder: ships its MIT license block, comma-space parameter splitting, support URL
  • sp_QuickieStore: window-scoped logging metadata, help accuracy
  • sp_HumanEventsBlockViewer: validation message accuracy
  • sp_HumanEvents: startup cleanup no longer drops user XE sessions, logged waits views no longer truncate avg_ms_per_wait, shutdown CATCH preserves the original error under XACT_ABORT
  • sp_PerfCheck: README rebuilt against the code, BOM stripped, blob-URL truncation and threshold-parameter widening

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

erikdarlingdata and others added 30 commits July 5, 2026 15:49
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&amp;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>
erikdarlingdata and others added 29 commits July 18, 2026 09:33
…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>
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
@erikdarlingdata
erikdarlingdata merged commit 860f8ac into main Jul 29, 2026
9 checks passed
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.

1 participant