Skip to content

Commit ed5b000

Browse files
Merge pull request #100 from erikdarlingdata/dev
v1.2.0 Release
2 parents bb25d7f + f4f66ec commit ed5b000

30 files changed

Lines changed: 1271 additions & 526 deletions

.coderabbit.yaml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2+
3+
language: "en-US"
4+
early_access: false
5+
enable_free_tier: true
6+
7+
reviews:
8+
profile: "chill"
9+
high_level_summary: true
10+
review_status: true
11+
commit_status: true
12+
collapse_walkthrough: true
13+
sequence_diagrams: false
14+
poem: false
15+
16+
path_filters:
17+
- "!**/*.Designer.cs"
18+
- "!**/bin/**"
19+
- "!**/obj/**"
20+
- "!**/publish/**"
21+
- "!**/*.user"
22+
- "!**/*.suo"
23+
- "!**/screenshots/**"
24+
25+
path_instructions:
26+
- path: "src/PlanViewer.App/**/*.cs"
27+
instructions: >
28+
Avalonia 11.3 desktop app using code-behind pattern (not MVVM).
29+
Watch for: null reference risks, proper disposal of resources,
30+
async/await patterns, and Avalonia-specific UI threading.
31+
- path: "src/PlanViewer.Core/**/*.cs"
32+
instructions: >
33+
Core library with execution plan analysis (PlanAnalyzer), XML parsing,
34+
and shared services. Watch for: XML parsing safety, null handling,
35+
and performance with large execution plans.
36+
- path: "src/PlanViewer.Mcp/**/*.cs"
37+
instructions: >
38+
MCP (Model Context Protocol) server integration for AI tools.
39+
Watch for: input validation, proper error responses, and serialization safety.
40+
- path: "tests/**/*.cs"
41+
instructions: >
42+
Unit and integration tests. Watch for: test isolation,
43+
meaningful assertions, and proper test data setup.
44+
45+
auto_review:
46+
enabled: true
47+
drafts: false
48+
base_branches:
49+
- "dev"
50+
- "main"
51+
52+
tools:
53+
gitleaks:
54+
enabled: true
55+
github-checks:
56+
enabled: true
57+
58+
chat:
59+
auto_reply: true
60+
61+
knowledge_base:
62+
learnings:
63+
scope: "local"
64+
pull_requests:
65+
scope: "local"

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ Pre-built binaries are available on the [Releases](https://github.com/erikdarlin
8787

8888
These are self-contained — no .NET SDK required. Extract the zip and run.
8989

90+
**macOS note:** macOS may block the app because it isn't signed with an Apple Developer certificate. If you see a warning that the app "can't be opened," run this after extracting:
91+
92+
```bash
93+
xattr -cr PerformanceStudio.app
94+
```
95+
96+
Then open the app normally.
97+
9098
## Build from Source
9199

92100
Clone and build:

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ public partial class PlanViewerControl : UserControl
9090
private static readonly SolidColorBrush OrangeBrush = new(Colors.Orange);
9191

9292

93-
// Current property section for collapsible groups
94-
private StackPanel? _currentPropertySection;
9593
// Track all property section grids for synchronized column resize
9694
private readonly List<ColumnDefinition> _sectionLabelColumns = new();
9795
private double _propertyLabelWidth = 140;
@@ -118,7 +116,8 @@ public partial class PlanViewerControl : UserControl
118116
public PlanViewerControl()
119117
{
120118
InitializeComponent();
121-
PlanScrollViewer.PointerWheelChanged += PlanScrollViewer_PointerWheelChanged;
119+
// Use Tunnel routing so Ctrl+wheel zoom fires before ScrollViewer consumes the event
120+
PlanScrollViewer.AddHandler(PointerWheelChangedEvent, PlanScrollViewer_PointerWheelChanged, Avalonia.Interactivity.RoutingStrategies.Tunnel);
122121
// Use Tunnel routing so pan handlers fire before ScrollViewer consumes the events
123122
PlanScrollViewer.AddHandler(PointerPressedEvent, PlanScrollViewer_PointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel);
124123
PlanScrollViewer.AddHandler(PointerMovedEvent, PlanScrollViewer_PointerMoved, Avalonia.Interactivity.RoutingStrategies.Tunnel);
@@ -820,7 +819,6 @@ private async System.Threading.Tasks.Task SetClipboardTextAsync(string text)
820819
private void ShowPropertiesPanel(PlanNode node)
821820
{
822821
PropertiesContent.Children.Clear();
823-
_currentPropertySection = null;
824822
_sectionLabelColumns.Clear();
825823
_currentSectionGrid = null;
826824
_currentSectionRowIndex = 0;
@@ -1771,7 +1769,6 @@ private void AddPropertySection(string title)
17711769
HorizontalContentAlignment = HorizontalAlignment.Stretch
17721770
};
17731771
PropertiesContent.Children.Add(expander);
1774-
_currentPropertySection = null; // No longer used — rows go into _currentSectionGrid
17751772
}
17761773

17771774
private void AddPropertyRow(string label, string value, bool isCode = false, bool indent = false)
@@ -2187,7 +2184,7 @@ private void ShowParameters(PlanStatement statement)
21872184

21882185
if (parameters.Count == 0)
21892186
{
2190-
var localVars = FindUnresolvedVariables(statement.StatementText, parameters);
2187+
var localVars = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
21912188
if (localVars.Count > 0)
21922189
{
21932190
ParametersHeader.Text = "Parameters";
@@ -2303,7 +2300,7 @@ private void ShowParameters(PlanStatement statement)
23032300
}
23042301
}
23052302

2306-
var unresolved = FindUnresolvedVariables(statement.StatementText, parameters);
2303+
var unresolved = FindUnresolvedVariables(statement.StatementText, parameters, statement.RootNode);
23072304
if (unresolved.Count > 0)
23082305
{
23092306
AddParameterAnnotation(
@@ -2350,7 +2347,8 @@ private void AddParameterAnnotation(string text, string color)
23502347
});
23512348
}
23522349

2353-
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters)
2350+
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters,
2351+
PlanNode? rootNode = null)
23542352
{
23552353
var unresolved = new List<string>();
23562354
if (string.IsNullOrEmpty(queryText))
@@ -2359,6 +2357,11 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23592357
var extractedNames = new HashSet<string>(
23602358
parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);
23612359

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

@@ -2369,6 +2372,8 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23692372
continue;
23702373
if (varName.StartsWith("@@", StringComparison.OrdinalIgnoreCase))
23712374
continue;
2375+
if (tableVarNames.Contains(varName))
2376+
continue;
23722377

23732378
seenVars.Add(varName);
23742379
unresolved.Add(varName);
@@ -2377,6 +2382,19 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
23772382
return unresolved;
23782383
}
23792384

2385+
private static void CollectTableVariableNames(PlanNode node, HashSet<string> names)
2386+
{
2387+
if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@"))
2388+
{
2389+
// ObjectName is like "@t.c" — extract the table variable name "@t"
2390+
var dotIdx = node.ObjectName.IndexOf('.');
2391+
var tvName = dotIdx > 0 ? node.ObjectName[..dotIdx] : node.ObjectName;
2392+
names.Add(tvName);
2393+
}
2394+
foreach (var child in node.Children)
2395+
CollectTableVariableNames(child, names);
2396+
}
2397+
23802398
private static void CollectWarnings(PlanNode node, List<PlanWarning> warnings)
23812399
{
23822400
warnings.AddRange(node.Warnings);
@@ -2961,9 +2979,17 @@ private async void SavePlan_Click(object? sender, RoutedEventArgs e)
29612979

29622980
if (file != null)
29632981
{
2964-
await using var stream = await file.OpenWriteAsync();
2965-
await using var writer = new StreamWriter(stream);
2966-
await writer.WriteAsync(_currentPlan.RawXml);
2982+
try
2983+
{
2984+
await using var stream = await file.OpenWriteAsync();
2985+
await using var writer = new StreamWriter(stream);
2986+
await writer.WriteAsync(_currentPlan.RawXml);
2987+
}
2988+
catch (Exception ex)
2989+
{
2990+
System.Diagnostics.Debug.WriteLine($"SavePlan failed: {ex.Message}");
2991+
CostText.Text = $"Save failed: {(ex.Message.Length > 60 ? ex.Message[..60] + "..." : ex.Message)}";
2992+
}
29672993
}
29682994
}
29692995

0 commit comments

Comments
 (0)