Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

language: "en-US"
early_access: false
enable_free_tier: true

reviews:
profile: "chill"
high_level_summary: true
review_status: true
commit_status: true
collapse_walkthrough: true
sequence_diagrams: false
poem: false

path_filters:
- "!**/*.Designer.cs"
- "!**/bin/**"
- "!**/obj/**"
- "!**/publish/**"
- "!**/*.user"
- "!**/*.suo"
- "!**/screenshots/**"

path_instructions:
- path: "src/PlanViewer.App/**/*.cs"
instructions: >
Avalonia 11.3 desktop app using code-behind pattern (not MVVM).
Watch for: null reference risks, proper disposal of resources,
async/await patterns, and Avalonia-specific UI threading.
- path: "src/PlanViewer.Core/**/*.cs"
instructions: >
Core library with execution plan analysis (PlanAnalyzer), XML parsing,
and shared services. Watch for: XML parsing safety, null handling,
and performance with large execution plans.
- path: "src/PlanViewer.Mcp/**/*.cs"
instructions: >
MCP (Model Context Protocol) server integration for AI tools.
Watch for: input validation, proper error responses, and serialization safety.
- path: "tests/**/*.cs"
instructions: >
Unit and integration tests. Watch for: test isolation,
meaningful assertions, and proper test data setup.

auto_review:
enabled: true
drafts: false
base_branches:
- "dev"
- "main"

tools:
gitleaks:
enabled: true
github-checks:
enabled: true

chat:
auto_reply: true

knowledge_base:
learnings:
scope: "local"
pull_requests:
scope: "local"
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ Pre-built binaries are available on the [Releases](https://github.com/erikdarlin

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

**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:

```bash
xattr -cr PerformanceStudio.app
```

Then open the app normally.

## Build from Source

Clone and build:
Expand Down
48 changes: 37 additions & 11 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ public partial class PlanViewerControl : UserControl
private static readonly SolidColorBrush OrangeBrush = new(Colors.Orange);


// Current property section for collapsible groups
private StackPanel? _currentPropertySection;
// Track all property section grids for synchronized column resize
private readonly List<ColumnDefinition> _sectionLabelColumns = new();
private double _propertyLabelWidth = 140;
Expand All @@ -118,7 +116,8 @@ public partial class PlanViewerControl : UserControl
public PlanViewerControl()
{
InitializeComponent();
PlanScrollViewer.PointerWheelChanged += PlanScrollViewer_PointerWheelChanged;
// Use Tunnel routing so Ctrl+wheel zoom fires before ScrollViewer consumes the event
PlanScrollViewer.AddHandler(PointerWheelChangedEvent, PlanScrollViewer_PointerWheelChanged, Avalonia.Interactivity.RoutingStrategies.Tunnel);
// Use Tunnel routing so pan handlers fire before ScrollViewer consumes the events
PlanScrollViewer.AddHandler(PointerPressedEvent, PlanScrollViewer_PointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel);
PlanScrollViewer.AddHandler(PointerMovedEvent, PlanScrollViewer_PointerMoved, Avalonia.Interactivity.RoutingStrategies.Tunnel);
Expand Down Expand Up @@ -820,7 +819,6 @@ private async System.Threading.Tasks.Task SetClipboardTextAsync(string text)
private void ShowPropertiesPanel(PlanNode node)
{
PropertiesContent.Children.Clear();
_currentPropertySection = null;
_sectionLabelColumns.Clear();
_currentSectionGrid = null;
_currentSectionRowIndex = 0;
Expand Down Expand Up @@ -1771,7 +1769,6 @@ private void AddPropertySection(string title)
HorizontalContentAlignment = HorizontalAlignment.Stretch
};
PropertiesContent.Children.Add(expander);
_currentPropertySection = null; // No longer used — rows go into _currentSectionGrid
}

private void AddPropertyRow(string label, string value, bool isCode = false, bool indent = false)
Expand Down Expand Up @@ -2187,7 +2184,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";
Expand Down Expand Up @@ -2303,7 +2300,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(
Expand Down Expand Up @@ -2350,7 +2347,8 @@ private void AddParameterAnnotation(string text, string color)
});
}

private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters)
private static List<string> FindUnresolvedVariables(string queryText, List<PlanParameter> parameters,
PlanNode? rootNode = null)
{
var unresolved = new List<string>();
if (string.IsNullOrEmpty(queryText))
Expand All @@ -2359,6 +2357,11 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
var extractedNames = new HashSet<string>(
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<string>(StringComparer.OrdinalIgnoreCase);
if (rootNode != null)
CollectTableVariableNames(rootNode, tableVarNames);

var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
var seenVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

Expand All @@ -2369,6 +2372,8 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
continue;
if (varName.StartsWith("@@", StringComparison.OrdinalIgnoreCase))
continue;
if (tableVarNames.Contains(varName))
continue;

seenVars.Add(varName);
unresolved.Add(varName);
Expand All @@ -2377,6 +2382,19 @@ private static List<string> FindUnresolvedVariables(string queryText, List<PlanP
return unresolved;
}

private static void CollectTableVariableNames(PlanNode node, HashSet<string> 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<PlanWarning> warnings)
{
warnings.AddRange(node.Warnings);
Expand Down Expand Up @@ -2961,9 +2979,17 @@ private async void SavePlan_Click(object? sender, RoutedEventArgs e)

if (file != null)
{
await using var stream = await file.OpenWriteAsync();
await using var writer = new StreamWriter(stream);
await writer.WriteAsync(_currentPlan.RawXml);
try
{
await using var stream = await file.OpenWriteAsync();
await using var writer = new StreamWriter(stream);
await writer.WriteAsync(_currentPlan.RawXml);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"SavePlan failed: {ex.Message}");
CostText.Text = $"Save failed: {(ex.Message.Length > 60 ? ex.Message[..60] + "..." : ex.Message)}";
}
}
}

Expand Down
Loading
Loading