diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
index 75fb26f..2db8609 100644
--- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
+++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
@@ -495,13 +495,45 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
var iconBitmap = IconHelper.LoadIcon(node.IconName);
if (iconBitmap != null)
{
- iconRow.Children.Add(new Image
+ var iconImage = new Image
{
Source = iconBitmap,
Width = 32,
Height = 32,
Margin = new Thickness(0, 0, 0, 2)
- });
+ };
+
+ // Distinguish Parallelism subtypes (Repartition / Distribute / Gather Streams)
+ // by overlaying a small letter on the shared parallelism.png icon.
+ var parallelismGlyph = GetParallelismGlyph(node);
+ if (parallelismGlyph != null)
+ {
+ var iconGrid = new Grid { Margin = new Thickness(0, 0, 0, 2) };
+ iconGrid.Children.Add(iconImage);
+ iconImage.Margin = default;
+ iconGrid.Children.Add(new Border
+ {
+ Width = 14, Height = 14,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ Background = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)),
+ CornerRadius = new CornerRadius(7),
+ Child = new TextBlock
+ {
+ Text = parallelismGlyph,
+ FontSize = 9,
+ FontWeight = FontWeight.Bold,
+ Foreground = Brushes.White,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ });
+ iconRow.Children.Add(iconGrid);
+ }
+ else
+ {
+ iconRow.Children.Add(iconImage);
+ }
}
// Warning indicator badge (orange triangle with !)
@@ -3062,6 +3094,24 @@ private void UpdateInsightsHeader()
InsightsHeader.Text = " Plan Insights";
}
+ ///
+ /// For Parallelism nodes, returns a single-letter glyph distinguishing
+ /// the three subtypes: R(epartition) / D(istribute) / G(ather) Streams.
+ /// Returns null for non-parallelism operators.
+ ///
+ private static string? GetParallelismGlyph(PlanNode node)
+ {
+ if (!string.Equals(node.PhysicalOp, "Parallelism", StringComparison.Ordinal))
+ return null;
+ return node.LogicalOp switch
+ {
+ "Repartition Streams" => "R",
+ "Distribute Streams" => "D",
+ "Gather Streams" => "G",
+ _ => null
+ };
+ }
+
private static string GetWaitCategory(string waitType)
{
if (waitType.StartsWith("SOS_SCHEDULER_YIELD") ||
diff --git a/src/PlanViewer.Core/Services/PlanIconMapper.cs b/src/PlanViewer.Core/Services/PlanIconMapper.cs
index d7d0580..d329d7d 100644
--- a/src/PlanViewer.Core/Services/PlanIconMapper.cs
+++ b/src/PlanViewer.Core/Services/PlanIconMapper.cs
@@ -178,9 +178,36 @@ public static class PlanIconMapper
///
/// Returns the icon file name (without extension) for a given physical operator.
+ /// When is "ColumnStore" on a *Index Scan,
+ /// routes to the columnstore variant.
///
- public static string GetIconName(string physicalOp)
+ public static string GetIconName(string physicalOp, string? storageType = null)
{
+ // Columnstore scans surface as PhysicalOp="Clustered Index Scan" / "Index Scan"
+ // with Storage="ColumnStore" on the Object element. Route to the columnstore icon.
+ if (string.Equals(storageType, "ColumnStore", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (physicalOp)
+ {
+ case "Clustered Index Scan":
+ case "Index Scan":
+ case "Columnstore Index Scan":
+ return "columnstore_index_scan";
+ case "Clustered Index Delete":
+ case "Index Delete":
+ return "columnstore_index_delete";
+ case "Clustered Index Insert":
+ case "Index Insert":
+ return "columnstore_index_insert";
+ case "Clustered Index Update":
+ case "Index Update":
+ return "columnstore_index_update";
+ case "Clustered Index Merge":
+ case "Index Merge":
+ return "columnstore_index_merge";
+ }
+ }
+
if (IconMap.TryGetValue(physicalOp, out var iconName))
return iconName;
diff --git a/src/PlanViewer.Core/Services/ShowPlanParser.cs b/src/PlanViewer.Core/Services/ShowPlanParser.cs
index db1a374..35194c5 100644
--- a/src/PlanViewer.Core/Services/ShowPlanParser.cs
+++ b/src/PlanViewer.Core/Services/ShowPlanParser.cs
@@ -667,8 +667,9 @@ private static PlanNode ParseRelOp(XElement relOpEl)
}
- // Map to icon
- node.IconName = PlanIconMapper.GetIconName(node.PhysicalOp);
+ // Icon mapping is deferred until after StorageType is parsed below,
+ // so columnstore scans (which surface as Clustered/Index Scan with
+ // Storage="ColumnStore") can be routed to the columnstore icon.
// Handle operator-specific element
var physicalOpEl = GetOperatorElement(relOpEl);
@@ -1368,6 +1369,11 @@ private static PlanNode ParseRelOp(XElement relOpEl)
}
}
+ // Map to icon — done here so columnstore scans (which surface as
+ // Clustered/Index Scan with Storage="ColumnStore") can be routed to
+ // the columnstore icon.
+ node.IconName = PlanIconMapper.GetIconName(node.PhysicalOp, node.StorageType);
+
// Recurse into child RelOps
foreach (var childRelOp in FindChildRelOps(relOpEl))
{