Skip to content

Commit 99cd86f

Browse files
Merge pull request #54 from erikdarlingdata/feature/issue-53-execute-selection
Execute selection, from cursor, and current batch (#53)
2 parents 424368c + 920bdf6 commit 99cd86f

3 files changed

Lines changed: 109 additions & 14 deletions

File tree

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics;
44
using System.Linq;
55
using System.Text.Json;
6+
using System.Text.RegularExpressions;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using System.Xml.Linq;
@@ -132,9 +133,25 @@ private void SetupEditorContextMenu()
132133
QueryEditor.SelectAll();
133134
};
134135

136+
var executeFromCursorItem = new MenuItem { Header = "Execute from Cursor" };
137+
executeFromCursorItem.Click += async (_, _) =>
138+
{
139+
var text = GetTextFromCursor();
140+
if (!string.IsNullOrWhiteSpace(text))
141+
await CaptureAndShowPlan(estimated: false, queryTextOverride: text);
142+
};
143+
144+
var executeCurrentBatchItem = new MenuItem { Header = "Execute Current Batch" };
145+
executeCurrentBatchItem.Click += async (_, _) =>
146+
{
147+
var text = GetCurrentBatch();
148+
if (!string.IsNullOrWhiteSpace(text))
149+
await CaptureAndShowPlan(estimated: false, queryTextOverride: text);
150+
};
151+
135152
QueryEditor.TextArea.ContextMenu = new ContextMenu
136153
{
137-
Items = { cutItem, copyItem, pasteItem, new Separator(), selectAllItem }
154+
Items = { cutItem, copyItem, pasteItem, new Separator(), selectAllItem, new Separator(), executeFromCursorItem, executeCurrentBatchItem }
138155
};
139156
}
140157

@@ -259,6 +276,47 @@ private void OnTextEntered(object? sender, TextInputEventArgs e)
259276
return (doc.GetText(start, offset - start), start);
260277
}
261278

279+
private string? GetSelectedTextOrNull()
280+
{
281+
var selection = QueryEditor.TextArea.Selection;
282+
if (selection.IsEmpty) return null;
283+
return selection.GetText();
284+
}
285+
286+
private string GetTextFromCursor()
287+
{
288+
var doc = QueryEditor.Document;
289+
var offset = QueryEditor.CaretOffset;
290+
return doc.GetText(offset, doc.TextLength - offset);
291+
}
292+
293+
private string? GetCurrentBatch()
294+
{
295+
var doc = QueryEditor.Document;
296+
var caretOffset = QueryEditor.CaretOffset;
297+
var text = doc.Text;
298+
var goPattern = new Regex(@"^\s*GO\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
299+
var matches = goPattern.Matches(text);
300+
301+
int batchStart = 0;
302+
int batchEnd = text.Length;
303+
304+
foreach (Match m in matches)
305+
{
306+
if (m.Index + m.Length <= caretOffset)
307+
{
308+
batchStart = m.Index + m.Length;
309+
}
310+
else if (m.Index >= caretOffset)
311+
{
312+
batchEnd = m.Index;
313+
break;
314+
}
315+
}
316+
317+
return text[batchStart..batchEnd].Trim();
318+
}
319+
262320
private void SetStatus(string text, bool autoClear = true)
263321
{
264322
_statusClearCts?.Cancel();
@@ -435,15 +493,17 @@ private async void ExecuteEstimated_Click(object? sender, RoutedEventArgs e)
435493
await CaptureAndShowPlan(estimated: true);
436494
}
437495

438-
private async Task CaptureAndShowPlan(bool estimated)
496+
private async Task CaptureAndShowPlan(bool estimated, string? queryTextOverride = null)
439497
{
440498
if (_connectionString == null || _selectedDatabase == null)
441499
{
442500
SetStatus("Connect to a server first", autoClear: false);
443501
return;
444502
}
445503

446-
var queryText = QueryEditor.Text?.Trim();
504+
var queryText = queryTextOverride?.Trim()
505+
?? GetSelectedTextOrNull()?.Trim()
506+
?? QueryEditor.Text?.Trim();
447507
if (string.IsNullOrEmpty(queryText))
448508
{
449509
SetStatus("Enter a query", autoClear: false);

src/PlanViewer.Core/Services/ActualPlanExecutor.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
using System;
10+
using System.Collections.Generic;
1011
using System.Text;
1112
using System.Threading;
1213
using System.Threading.Tasks;
@@ -55,7 +56,7 @@ public static class ActualPlanExecutor
5556
sb.AppendLine("SET STATISTICS XML OFF;");
5657

5758
var fullScript = sb.ToString();
58-
string? capturedPlanXml = null;
59+
var capturedPlanXmls = new List<string>();
5960

6061
/* Override database in connection string */
6162
var builder = new SqlConnectionStringBuilder(connectionString);
@@ -89,7 +90,7 @@ The plan result set has a single row with a single XML column. */
8990
if (value != null && value.TrimStart().StartsWith("<ShowPlanXML", StringComparison.Ordinal))
9091
{
9192
/* This is a plan XML result set — capture it */
92-
capturedPlanXml = value;
93+
capturedPlanXmls.Add(value);
9394
}
9495
else
9596
{
@@ -105,6 +106,8 @@ The plan result set has a single row with a single XML column. */
105106
}
106107
while (await reader.NextResultAsync(cancellationToken));
107108

108-
return capturedPlanXml;
109+
if (capturedPlanXmls.Count == 0) return null;
110+
if (capturedPlanXmls.Count == 1) return capturedPlanXmls[0];
111+
return EstimatedPlanExecutor.MergeShowPlanXmls(capturedPlanXmls);
109112
}
110113
}

src/PlanViewer.Core/Services/EstimatedPlanExecutor.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Threading;
35
using System.Threading.Tasks;
6+
using System.Xml.Linq;
47
using Microsoft.Data.SqlClient;
58

69
namespace PlanViewer.Core.Services;
@@ -36,9 +39,9 @@ public static class EstimatedPlanExecutor
3639
await enableCmd.ExecuteNonQueryAsync(cancellationToken);
3740
}
3841

39-
// Execute the query — with SHOWPLAN XML ON, this returns the plan
40-
// as a single-row, single-column result set (no actual execution)
41-
string? planXml = null;
42+
// Execute the query — with SHOWPLAN XML ON, this returns one result set
43+
// per statement, each containing a ShowPlanXML document.
44+
var planXmls = new List<string>();
4245
using (var queryCmd = new SqlCommand(queryText, connection))
4346
{
4447
queryCmd.CommandTimeout = timeoutSeconds;
@@ -49,12 +52,16 @@ public static class EstimatedPlanExecutor
4952
});
5053

5154
using var reader = await queryCmd.ExecuteReaderAsync(cancellationToken);
52-
if (await reader.ReadAsync(cancellationToken))
55+
do
5356
{
54-
var value = reader.GetValue(0)?.ToString();
55-
if (value != null && value.TrimStart().StartsWith("<ShowPlanXML", StringComparison.Ordinal))
56-
planXml = value;
57+
if (await reader.ReadAsync(cancellationToken))
58+
{
59+
var value = reader.GetValue(0)?.ToString();
60+
if (value != null && value.TrimStart().StartsWith("<ShowPlanXML", StringComparison.Ordinal))
61+
planXmls.Add(value);
62+
}
5763
}
64+
while (await reader.NextResultAsync(cancellationToken));
5865
}
5966

6067
// Disable SHOWPLAN XML (best effort — connection is about to close)
@@ -66,6 +73,31 @@ public static class EstimatedPlanExecutor
6673
}
6774
catch { /* connection cleanup */ }
6875

69-
return planXml;
76+
if (planXmls.Count == 0) return null;
77+
if (planXmls.Count == 1) return planXmls[0];
78+
return MergeShowPlanXmls(planXmls);
79+
}
80+
81+
/// <summary>
82+
/// Merges multiple ShowPlanXML documents into one by combining all Batch elements.
83+
/// </summary>
84+
internal static string MergeShowPlanXmls(List<string> planXmls)
85+
{
86+
XNamespace ns = "http://schemas.microsoft.com/sqlserver/2004/07/showplan";
87+
var baseDoc = XDocument.Parse(planXmls[0]);
88+
var batchSequence = baseDoc.Root!.Element(ns + "BatchSequence")!;
89+
90+
for (int i = 1; i < planXmls.Count; i++)
91+
{
92+
var doc = XDocument.Parse(planXmls[i]);
93+
var batches = doc.Root!.Element(ns + "BatchSequence")?.Elements(ns + "Batch");
94+
if (batches != null)
95+
{
96+
foreach (var batch in batches)
97+
batchSequence.Add(batch);
98+
}
99+
}
100+
101+
return baseDoc.ToString(SaveOptions.DisableFormatting);
70102
}
71103
}

0 commit comments

Comments
 (0)