diff --git a/Dashboard/Controls/PlanViewerControl.xaml.cs b/Dashboard/Controls/PlanViewerControl.xaml.cs index 6ea34e78..1b2e2ace 100644 --- a/Dashboard/Controls/PlanViewerControl.xaml.cs +++ b/Dashboard/Controls/PlanViewerControl.xaml.cs @@ -534,7 +534,8 @@ private void ShowPropertiesPanel(PlanNode node) // Header var headerText = node.PhysicalOp; - if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)) + if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp) + && !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase)) headerText += $" ({node.LogicalOp})"; PropertiesHeader.Text = headerText; PropertiesSubHeader.Text = $"Node ID: {node.NodeId}"; @@ -1481,7 +1482,8 @@ private ToolTip BuildNodeTooltip(PlanNode node) // Header var headerText = node.PhysicalOp; - if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)) + if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp) + && !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase)) headerText += $" ({node.LogicalOp})"; stack.Children.Add(new TextBlock { diff --git a/Dashboard/Services/ShowPlanParser.cs b/Dashboard/Services/ShowPlanParser.cs index 21778012..37f367c0 100644 --- a/Dashboard/Services/ShowPlanParser.cs +++ b/Dashboard/Services/ShowPlanParser.cs @@ -832,6 +832,19 @@ private static PlanNode ParseRelOp(XElement relOpEl) node.Lookup = physicalOpEl.Attribute("Lookup")?.Value is "true" or "1"; node.DynamicSeek = physicalOpEl.Attribute("DynamicSeek")?.Value is "true" or "1"; + // Override PhysicalOp, LogicalOp, and icon when Lookup=true. + // SQL Server's XML emits PhysicalOp="Clustered Index Seek" with + // rather than "Key Lookup (Clustered)" — correct the label here so all display + // paths (node card, tooltip, properties panel) show the right operator name. + if (node.Lookup) + { + var isHeap = node.IndexKind?.Equals("Heap", StringComparison.OrdinalIgnoreCase) == true + || node.PhysicalOp.StartsWith("RID Lookup", StringComparison.OrdinalIgnoreCase); + node.PhysicalOp = isHeap ? "RID Lookup (Heap)" : "Key Lookup (Clustered)"; + node.LogicalOp = isHeap ? "RID Lookup" : "Key Lookup"; + node.IconName = isHeap ? "rid_lookup" : "bookmark_lookup"; + } + // Table cardinality and rows to be read (on per XSD) node.TableCardinality = ParseDouble(relOpEl.Attribute("TableCardinality")?.Value); node.EstimatedRowsRead = ParseDouble(relOpEl.Attribute("EstimatedRowsRead")?.Value); diff --git a/Lite/Controls/PlanViewerControl.xaml.cs b/Lite/Controls/PlanViewerControl.xaml.cs index 0603ba59..e6504164 100644 --- a/Lite/Controls/PlanViewerControl.xaml.cs +++ b/Lite/Controls/PlanViewerControl.xaml.cs @@ -550,7 +550,8 @@ private void ShowPropertiesPanel(PlanNode node) // Header var headerText = node.PhysicalOp; - if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)) + if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp) + && !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase)) headerText += $" ({node.LogicalOp})"; PropertiesHeader.Text = headerText; PropertiesSubHeader.Text = $"Node ID: {node.NodeId}"; @@ -1492,7 +1493,8 @@ private ToolTip BuildNodeTooltip(PlanNode node) // Header var headerText = node.PhysicalOp; - if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp)) + if (node.LogicalOp != node.PhysicalOp && !string.IsNullOrEmpty(node.LogicalOp) + && !node.PhysicalOp.Contains(node.LogicalOp, StringComparison.OrdinalIgnoreCase)) headerText += $" ({node.LogicalOp})"; stack.Children.Add(new TextBlock { diff --git a/Lite/Services/ShowPlanParser.cs b/Lite/Services/ShowPlanParser.cs index 11c0f9ec..14625899 100644 --- a/Lite/Services/ShowPlanParser.cs +++ b/Lite/Services/ShowPlanParser.cs @@ -832,6 +832,19 @@ private static PlanNode ParseRelOp(XElement relOpEl) node.Lookup = physicalOpEl.Attribute("Lookup")?.Value is "true" or "1"; node.DynamicSeek = physicalOpEl.Attribute("DynamicSeek")?.Value is "true" or "1"; + // Override PhysicalOp, LogicalOp, and icon when Lookup=true. + // SQL Server's XML emits PhysicalOp="Clustered Index Seek" with + // rather than "Key Lookup (Clustered)" — correct the label here so all display + // paths (node card, tooltip, properties panel) show the right operator name. + if (node.Lookup) + { + var isHeap = node.IndexKind?.Equals("Heap", StringComparison.OrdinalIgnoreCase) == true + || node.PhysicalOp.StartsWith("RID Lookup", StringComparison.OrdinalIgnoreCase); + node.PhysicalOp = isHeap ? "RID Lookup (Heap)" : "Key Lookup (Clustered)"; + node.LogicalOp = isHeap ? "RID Lookup" : "Key Lookup"; + node.IconName = isHeap ? "rid_lookup" : "bookmark_lookup"; + } + // Table cardinality and rows to be read (on per XSD) node.TableCardinality = ParseDouble(relOpEl.Attribute("TableCardinality")?.Value); node.EstimatedRowsRead = ParseDouble(relOpEl.Attribute("EstimatedRowsRead")?.Value);