Skip to content

Leave the sp_executesql declaration list out of substituted statement text - #560

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/sp-executesql-declaration-list
Sep 24, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/sp-executesql-declaration-list

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

A plan from the plan cache or Query Store records an sp_executesql statement with its declaration list in front of it: (@p1 int, @p2 int)SELECT .... Parameter substitution (#467, #482) read the names in that list as parameter references. So the statement text became (10 int, 20 int)SELECT ... WHERE t.A IN (10, 20). That text is not what the plan records, and it does not run.

This text appears in these places:

  • The advice for humans and the advice for robots
  • The HTML export and the comparison report
  • The MCP tools
  • "Copy Query Text (with values)" in the app
  • The statement header of planview analyze -o text

Auto-parameterized plans from the plan cache have the same problem. (@1 tinyint,@2 int)SELECT became (52 tinyint,2 int)SELECT.

Now only the statement after the list gets values. When it gets at least one value, the list is dropped. This text is meant to run, and a declaration list is not T-SQL on its own. When no value is substituted, the text stays as the plan recorded it, with its list. parameterized_statement_text still carries the original text and its list.

A plan cuts statement text off at 4,000 characters. The declarations for a long IN list, such as the ones Dapper writes, can fill all of them. Then the list never closes, and the text holds no statement at all. That text now stays as it is. Before, it became (1 int,2 int,....

ReproScriptBuilder already stripped the same list for the repro script, with its own copy of the parser. The two now share one parser, ParameterSubstitution.DeclarationListEnd. The web project compiles ParameterSubstitution.cs without ReproScriptBuilder.cs, so the shared parser is in ParameterSubstitution.

I found this while working on #558. The plan in that issue came from the plan cache, and planview analyze printed its statement as (10 int, 20 int)SELECT ....

Tests

  • New ParameterSubstitution tests:
  • New ReproScriptBuilder tests:
    • The query text in the repro script has no declaration list. That strip had no test before.
    • A list cut off by the limit stays as it is, as it did before.
  • A mutation check applied six wrong versions of the code. The test written for each one caught it:
    • No list skipping
    • The list ends at the first closing parenthesis
    • The list is dropped when nothing is substituted
    • The repro script keeps the list
    • A list that never closes is read as no list
    • The repro script slices the text at the -1 that marks a list that never closes
  • ComparisonBaseline.txt changes by 4 lines. They are the statement text of the two fixtures that start with a declaration list: (10 int, 20 int)SELECT and (52 tinyint,2 int)SELECT lose their lists. No other line changes. WarningBaseline.txt does not change.
  • Full Core test suite: 743 tests, 742 passed, 1 skipped, 0 failed. The Web project builds with no warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6

A plan from the plan cache or Query Store records an sp_executesql
statement with its declaration list in front: "(@p1 int, @p2 int)SELECT".
Substitution read those names as references, so the advice, exports, MCP
tools, "Copy Query Text (with values)" and planview's text output showed
"(10 int, 20 int)SELECT ...", which neither matches the plan nor runs.

Only the statement after the list gets values now, and when it gets any,
the list is dropped. With no values the text stays as the plan recorded
it. A list that never closes (the plan cut the text off at 4,000
characters inside it) is left alone. parameterized_statement_text still
carries the original.

ReproScriptBuilder had its own copy of the list parser; both now use
ParameterSubstitution.DeclarationListEnd, which the web project can see.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
@claude

claude Bot commented Sep 24, 2026

Copy link
Copy Markdown

Reviewed. The DeclarationListEnd parser is bounds-safe (short-circuits before indexing text[i+1]), the depth-counting correctly handles parenthesized types like decimal(18,2), and the truncated/no-substitution paths correctly preserve the original text as documented. Callers (ResultMapper, ReproScriptBuilder, the app's statement text) are consistent, and the shared-parser split respects the existing PlanViewer.Web.csproj linked-file convention (only ParameterSubstitution.cs is linked, not ReproScriptBuilder.cs). Test coverage matches the documented edge cases including the #556 fixture. No issues found.

@erikdarlingdata
erikdarlingdata merged commit 381f0e0 into dev Sep 24, 2026
3 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/sp-executesql-declaration-list branch September 24, 2026 03:49
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