diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs index 363d967..3ef8327 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs @@ -2187,7 +2187,7 @@ private void ShowParameters(PlanStatement statement) if (parameters.Count == 0) { - var localVars = FindUnresolvedVariables(statement.StatementText, parameters); + var localVars = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode); if (localVars.Count > 0) { ParametersHeader.Text = "Parameters"; @@ -2303,7 +2303,7 @@ private void ShowParameters(PlanStatement statement) } } - var unresolved = FindUnresolvedVariables(statement.StatementText, parameters); + var unresolved = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode); if (unresolved.Count > 0) { AddParameterAnnotation( @@ -2350,7 +2350,8 @@ private void AddParameterAnnotation(string text, string color) }); } - private static List FindUnresolvedVariables(string queryText, List parameters) + private static List FindUnresolvedVariables(string queryText, List parameters, + PlanNode? rootNode = null) { var unresolved = new List(); if (string.IsNullOrEmpty(queryText)) @@ -2359,6 +2360,11 @@ private static List FindUnresolvedVariables(string queryText, List( parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase); + // Collect table variable names from the plan tree so we don't misreport them as local variables + var tableVarNames = new HashSet(StringComparer.OrdinalIgnoreCase); + if (rootNode != null) + CollectTableVariableNames(rootNode, tableVarNames); + var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase); var seenVars = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -2369,6 +2375,8 @@ private static List FindUnresolvedVariables(string queryText, List FindUnresolvedVariables(string queryText, List names) + { + if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@")) + { + // ObjectName is like "@t.c" — extract the table variable name "@t" + var dotIdx = node.ObjectName.IndexOf('.'); + var tvName = dotIdx > 0 ? node.ObjectName[..dotIdx] : node.ObjectName; + names.Add(tvName); + } + foreach (var child in node.Children) + CollectTableVariableNames(child, names); + } + private static void CollectWarnings(PlanNode node, List warnings) { warnings.AddRange(node.Warnings); diff --git a/src/PlanViewer.Core/Services/PlanAnalyzer.cs b/src/PlanViewer.Core/Services/PlanAnalyzer.cs index f16e976..0d577b3 100644 --- a/src/PlanViewer.Core/Services/PlanAnalyzer.cs +++ b/src/PlanViewer.Core/Services/PlanAnalyzer.cs @@ -388,7 +388,7 @@ private static void AnalyzeStatement(PlanStatement stmt, AnalyzerConfig cfg) var modifiesTableVar = false; CheckForTableVariables(stmt.RootNode, isModification, ref hasTableVar, ref modifiesTableVar); - if (hasTableVar) + if (hasTableVar && !modifiesTableVar) { stmt.PlanWarnings.Add(new PlanWarning { @@ -865,11 +865,17 @@ _ when nonSargableReason.StartsWith("Function call") => if (!cfg.IsRuleDisabled(22) && !string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@")) { + var isModificationOp = node.PhysicalOp.Contains("Insert", StringComparison.OrdinalIgnoreCase) + || node.PhysicalOp.Contains("Update", StringComparison.OrdinalIgnoreCase) + || node.PhysicalOp.Contains("Delete", StringComparison.OrdinalIgnoreCase); + node.Warnings.Add(new PlanWarning { WarningType = "Table Variable", - 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.", - Severity = PlanWarningSeverity.Warning + Message = isModificationOp + ? "Modifying a table variable forces the entire plan to run single-threaded. Replace with a #temp table to allow parallel execution." + : "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.", + Severity = isModificationOp ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning }); }