diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.Execution.cs b/src/PlanViewer.App/Controls/QuerySessionControl.Execution.cs
index 45f0bd7..4145b51 100644
--- a/src/PlanViewer.App/Controls/QuerySessionControl.Execution.cs
+++ b/src/PlanViewer.App/Controls/QuerySessionControl.Execution.cs
@@ -404,78 +404,8 @@ private async void GetActualPlan_Click(object? sender, RoutedEventArgs e)
///
/// Shows a modal confirmation dialog and returns true if the user clicked OK.
///
- private async Task ShowConfirmationDialog(string title, string message)
- {
- var result = false;
-
- var messageText = new TextBlock
- {
- Text = message,
- TextWrapping = TextWrapping.Wrap,
- FontSize = 13,
- Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
- Margin = new Avalonia.Thickness(0, 0, 0, 16)
- };
-
- var okBtn = new Button
- {
- Content = "OK",
- Height = 32,
- Width = 80,
- Padding = new Avalonia.Thickness(16, 0),
- FontSize = 12,
- HorizontalContentAlignment = HorizontalAlignment.Center,
- VerticalContentAlignment = VerticalAlignment.Center,
- Theme = (Avalonia.Styling.ControlTheme)this.FindResource("AppButton")!
- };
-
- var cancelBtn = new Button
- {
- Content = "Cancel",
- Height = 32,
- Width = 80,
- Padding = new Avalonia.Thickness(16, 0),
- FontSize = 12,
- Margin = new Avalonia.Thickness(8, 0, 0, 0),
- HorizontalContentAlignment = HorizontalAlignment.Center,
- VerticalContentAlignment = VerticalAlignment.Center,
- Theme = (Avalonia.Styling.ControlTheme)this.FindResource("AppButton")!
- };
-
- var buttonPanel = new StackPanel
- {
- Orientation = Avalonia.Layout.Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Right
- };
- buttonPanel.Children.Add(okBtn);
- buttonPanel.Children.Add(cancelBtn);
-
- var content = new StackPanel
- {
- Margin = new Avalonia.Thickness(20),
- Children = { messageText, buttonPanel }
- };
-
- var dialog = new Window
- {
- Title = title,
- Width = 420,
- Height = 200,
- MinWidth = 420,
- MinHeight = 200,
- Icon = GetParentWindow().Icon,
- Background = new SolidColorBrush(Color.Parse("#1A1D23")),
- Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
- Content = content,
- WindowStartupLocation = WindowStartupLocation.CenterOwner
- };
-
- okBtn.Click += (_, _) => { result = true; dialog.Close(); };
- cancelBtn.Click += (_, _) => dialog.Close();
-
- await dialog.ShowDialog(GetParentWindow());
- return result;
- }
+ private Task ShowConfirmationDialog(string title, string message)
+ => Dialogs.ConfirmationDialog.ShowAsync(GetParentWindow(), title, message);
///
/// Extracts the database name from plan XML's StmtSimple DatabaseContext attribute.
diff --git a/src/PlanViewer.App/Dialogs/ConfirmationDialog.cs b/src/PlanViewer.App/Dialogs/ConfirmationDialog.cs
new file mode 100644
index 0000000..ae6dba6
--- /dev/null
+++ b/src/PlanViewer.App/Dialogs/ConfirmationDialog.cs
@@ -0,0 +1,88 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace PlanViewer.App.Dialogs;
+
+///
+/// Modal yes/no confirmation. Shared rather than per-control: this dialog gates
+/// executing SQL against a live server, and it is reached from both the query
+/// session and a loaded plan file. Two copies would be free to drift, which is
+/// exactly how one of those two paths ended up with no confirmation at all.
+///
+public static class ConfirmationDialog
+{
+ public static async Task ShowAsync(Window owner, string title, string message)
+ {
+ var result = false;
+
+ var messageText = new TextBlock
+ {
+ Text = message,
+ TextWrapping = TextWrapping.Wrap,
+ FontSize = 13,
+ Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
+ Margin = new Avalonia.Thickness(0, 0, 0, 16)
+ };
+
+ var okBtn = new Button
+ {
+ Content = "OK",
+ Height = 32,
+ Width = 80,
+ Padding = new Avalonia.Thickness(16, 0),
+ FontSize = 12,
+ HorizontalContentAlignment = HorizontalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ Theme = (Avalonia.Styling.ControlTheme)owner.FindResource("AppButton")!
+ };
+
+ var cancelBtn = new Button
+ {
+ Content = "Cancel",
+ Height = 32,
+ Width = 80,
+ Padding = new Avalonia.Thickness(16, 0),
+ FontSize = 12,
+ Margin = new Avalonia.Thickness(8, 0, 0, 0),
+ HorizontalContentAlignment = HorizontalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ Theme = (Avalonia.Styling.ControlTheme)owner.FindResource("AppButton")!
+ };
+
+ var buttonPanel = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right
+ };
+ buttonPanel.Children.Add(okBtn);
+ buttonPanel.Children.Add(cancelBtn);
+
+ var content = new StackPanel
+ {
+ Margin = new Avalonia.Thickness(20),
+ Children = { messageText, buttonPanel }
+ };
+
+ var dialog = new Window
+ {
+ Title = title,
+ Width = 460,
+ Height = 260,
+ MinWidth = 460,
+ MinHeight = 260,
+ Icon = owner.Icon,
+ Background = new SolidColorBrush(Color.Parse("#1A1D23")),
+ Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
+ Content = content,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner
+ };
+
+ okBtn.Click += (_, _) => { result = true; dialog.Close(); };
+ cancelBtn.Click += (_, _) => dialog.Close();
+
+ await dialog.ShowDialog(owner);
+ return result;
+ }
+}
diff --git a/src/PlanViewer.App/MainWindow.PlanViewer.cs b/src/PlanViewer.App/MainWindow.PlanViewer.cs
index c8f5d47..dc44089 100644
--- a/src/PlanViewer.App/MainWindow.PlanViewer.cs
+++ b/src/PlanViewer.App/MainWindow.PlanViewer.cs
@@ -388,6 +388,25 @@ private async Task GetActualPlanFromFile(PlanViewerControl viewer)
return;
var database = dialog.ResultDatabase ?? ExtractDatabaseFromPlanXml(viewer.RawXml);
+
+ /* Confirm before running it. The query session asks first; this path did
+ not, and it is the riskier of the two: the SQL comes out of a .sqlplan
+ the user opened rather than something they typed, and plan files get
+ emailed around. Name the target so "which server was that?" is answered
+ before the query runs, not after. */
+ var target = string.IsNullOrEmpty(database)
+ ? dialog.ResultConnection.ServerName
+ : $"{dialog.ResultConnection.ServerName}, database [{database}]";
+
+ var confirmed = await Dialogs.ConfirmationDialog.ShowAsync(
+ this,
+ "Get Actual Plan",
+ $"This will execute the query stored in this plan file against:\n\n{target}\n\n" +
+ "The query text comes from the plan file, not from you. Review it first if the file did not originate on this machine.\n\n" +
+ "It runs with SET STATISTICS XML ON and all data results are discarded.\n\nContinue?");
+
+ if (!confirmed) return;
+
var connectionString = dialog.ResultConnection.GetConnectionString(_credentialService, database);
var isAzure = dialog.ResultConnection.ServerName.Contains(".database.windows.net",
StringComparison.OrdinalIgnoreCase) ||