Skip to content

Commit fd53dc4

Browse files
Fix Rule 22 table variable warnings on modification operators (#80)
- Node-level: Insert/Update/Delete on table variables now shows Critical "forces serial" warning instead of generic "lacks statistics" message - Statement-level: stats warning only fires when reading from a table variable, not modifying it - Parameters pane: exclude table variable names from local variable detection so @t is not misreported as an unresolved local variable Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5a8eb93 commit fd53dc4

2 files changed

Lines changed: 33 additions & 6 deletions

File tree

src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,7 +2187,7 @@ private void ShowParameters(PlanStatement statement)
21872187

21882188
if (parameters.Count == 0)
21892189
{
2190-
var localVars = FindUnresolvedVariables(statement.StatementText, parameters);
2190+
var localVars = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
21912191
if (localVars.Count > 0)
21922192
{
21932193
ParametersHeader.Text = "Parameters";
@@ -2303,7 +2303,7 @@ private void ShowParameters(PlanStatement statement)
23032303
}
23042304
}
23052305

2306-
var unresolved = FindUnresolvedVariables(statement.StatementText, parameters);
2306+
var unresolved = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
23072307
if (unresolved.Count > 0)
23082308
{
23092309
AddParameterAnnotation(
@@ -2350,7 +2350,8 @@ private void AddParameterAnnotation(string text, string color)
23502350
});
23512351
}
23522352

2353-
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters)
2353+
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters,
2354+
PlanNode? rootNode = null)
23542355
{
23552356
var unresolved = new List<string>();
23562357
if (string.IsNullOrEmpty(queryText))
@@ -2359,6 +2360,11 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23592360
var extractedNames = new HashSet<string>(
23602361
parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);
23612362

2363+
// Collect table variable names from the plan tree so we don't misreport them as local variables
2364+
var tableVarNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
2365+
if (rootNode != null)
2366+
CollectTableVariableNames(rootNode, tableVarNames);
2367+
23622368
var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
23632369
var seenVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
23642370

@@ -2369,6 +2375,8 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23692375
continue;
23702376
if (varName.StartsWith("@@", StringComparison.OrdinalIgnoreCase))
23712377
continue;
2378+
if (tableVarNames.Contains(varName))
2379+
continue;
23722380

23732381
seenVars.Add(varName);
23742382
unresolved.Add(varName);
@@ -2377,6 +2385,19 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23772385
return unresolved;
23782386
}
23792387

2388+
private static void CollectTableVariableNames(PlanNode node, HashSet<string> names)
2389+
{
2390+
if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@"))
2391+
{
2392+
// ObjectName is like "@t.c" — extract the table variable name "@t"
2393+
var dotIdx = node.ObjectName.IndexOf('.');
2394+
var tvName = dotIdx > 0 ? node.ObjectName[..dotIdx] : node.ObjectName;
2395+
names.Add(tvName);
2396+
}
2397+
foreach (var child in node.Children)
2398+
CollectTableVariableNames(child, names);
2399+
}
2400+
23802401
private static void CollectWarnings(PlanNode node, List<PlanWarning> warnings)
23812402
{
23822403
warnings.AddRange(node.Warnings);

src/PlanViewer.Core/Services/PlanAnalyzer.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ private static void AnalyzeStatement(PlanStatement stmt, AnalyzerConfig cfg)
388388
var modifiesTableVar = false;
389389
CheckForTableVariables(stmt.RootNode, isModification, ref hasTableVar, ref modifiesTableVar);
390390

391-
if (hasTableVar)
391+
if (hasTableVar && !modifiesTableVar)
392392
{
393393
stmt.PlanWarnings.Add(new PlanWarning
394394
{
@@ -865,11 +865,17 @@ _ when nonSargableReason.StartsWith("Function call") =>
865865
if (!cfg.IsRuleDisabled(22) && !string.IsNullOrEmpty(node.ObjectName) &&
866866
node.ObjectName.StartsWith("@"))
867867
{
868+
var isModificationOp = node.PhysicalOp.Contains("Insert", StringComparison.OrdinalIgnoreCase)
869+
|| node.PhysicalOp.Contains("Update", StringComparison.OrdinalIgnoreCase)
870+
|| node.PhysicalOp.Contains("Delete", StringComparison.OrdinalIgnoreCase);
871+
868872
node.Warnings.Add(new PlanWarning
869873
{
870874
WarningType = "Table Variable",
871-
Message = "Table variable detected. Table variables lack column-level statistics, which causes bad row estimates, join choices, and memory grant decisions. Replace with a #temp table.",
872-
Severity = PlanWarningSeverity.Warning
875+
Message = isModificationOp
876+
? "Modifying a table variable forces the entire plan to run single-threaded. Replace with a #temp table to allow parallel execution."
877+
: "Table variable detected. Table variables lack column-level statistics, which causes bad row estimates, join choices, and memory grant decisions. Replace with a #temp table.",
878+
Severity = isModificationOp ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning
873879
});
874880
}
875881

0 commit comments

Comments
 (0)