diff --git a/Dashboard/Converters/QueryTextCleanupConverter.cs b/Dashboard/Converters/QueryTextCleanupConverter.cs
index e1ae8481..764131d0 100644
--- a/Dashboard/Converters/QueryTextCleanupConverter.cs
+++ b/Dashboard/Converters/QueryTextCleanupConverter.cs
@@ -12,7 +12,7 @@
namespace PerformanceMonitorDashboard.Converters
{
- public class QueryTextCleanupConverter : IValueConverter
+ public partial class QueryTextCleanupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
@@ -28,7 +28,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
text = text.Replace("\t", " ", StringComparison.Ordinal);
// Replace multiple spaces with single space
- text = Regex.Replace(text, @"\s+", " ");
+ text = MultipleSpacesRegExp().Replace(text, " ");
// Trim leading/trailing whitespace
text = text.Trim();
@@ -40,5 +40,8 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
{
throw new NotImplementedException();
}
+
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex MultipleSpacesRegExp();
}
}
diff --git a/Dashboard/Helpers/DateFilterHelper.cs b/Dashboard/Helpers/DateFilterHelper.cs
index b9b5e4f4..def0f459 100644
--- a/Dashboard/Helpers/DateFilterHelper.cs
+++ b/Dashboard/Helpers/DateFilterHelper.cs
@@ -11,7 +11,7 @@
namespace PerformanceMonitorDashboard.Helpers
{
- public static class DateFilterHelper
+ public static partial class DateFilterHelper
{
public static bool MatchesFilter(object? value, string? filterText)
{
@@ -148,7 +148,7 @@ private static bool TryConvertToDateTime(object value, out DateTime result)
}
// "last N hours/days/weeks" expressions
- var lastMatch = Regex.Match(expressionLower, @"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)");
+ var lastMatch = LastNHoursDaysWeeksMonthsRegExp().Match(expressionLower);
if (lastMatch.Success)
{
int count = int.Parse(lastMatch.Groups[1].Value, CultureInfo.InvariantCulture);
@@ -231,5 +231,8 @@ private static bool IsRelativeExpression(string expression)
expression == "tomorrow" ||
Regex.IsMatch(expression, @"last\s+\d+\s+(hour|hours|day|days|week|weeks|month|months)");
}
+
+ [GeneratedRegex(@"last\s+(\d+)\s+(hour|hours|day|days|week|weeks|month|months)")]
+ private static partial Regex LastNHoursDaysWeeksMonthsRegExp();
}
}
diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs
index ec03090e..6734c901 100644
--- a/Dashboard/Services/PlanAnalyzer.cs
+++ b/Dashboard/Services/PlanAnalyzer.cs
@@ -10,24 +10,16 @@ namespace PerformanceMonitorDashboard.Services;
/// Post-parse analysis pass that walks a parsed plan tree and adds warnings
/// for common performance anti-patterns. Called after ShowPlanParser.Parse().
///
-public static class PlanAnalyzer
+public static partial class PlanAnalyzer
{
- private static readonly Regex FunctionInPredicateRegex = new(
- @"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex FunctionInPredicateRegex = FunctionInPredicateRegExp();
- private static readonly Regex LeadingWildcardLikeRegex = new(
- @"\blike\b[^'""]*?N?'%",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex LeadingWildcardLikeRegex = LeadingWildcardLikeRegExp();
- private static readonly Regex CaseInPredicateRegex = new(
- @"\bCASE\s+(WHEN\b|$)",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex CaseInPredicateRegex = CaseInPredicateRegExp();
// Matches CTE definitions: WITH name AS ( or , name AS (
- private static readonly Regex CteDefinitionRegex = new(
- @"(?:\bWITH\s+|\,\s*)(\w+)\s+AS\s*\(",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex CteDefinitionRegex = CteDefinitionRegExp();
public static void Analyze(ParsedPlan plan)
{
@@ -686,7 +678,7 @@ _ when nonSargableReason.StartsWith("Function call") =>
// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
- node.ObjectName.StartsWith("@"))
+ node.ObjectName.StartsWith('@'))
{
node.Warnings.Add(new PlanWarning
{
@@ -890,7 +882,7 @@ private static bool IsScanOperator(PlanNode node)
return "Implicit conversion (CONVERT_IMPLICIT)";
// ISNULL / COALESCE wrapping column
- if (Regex.IsMatch(predicate, @"\b(isnull|coalesce)\s*\(", RegexOptions.IgnoreCase))
+ if (IsNullCoalesceRegExp().IsMatch(predicate))
return "ISNULL/COALESCE wrapping column";
// Common function calls on columns
@@ -930,7 +922,7 @@ private static void DetectMultiReferenceCte(PlanStatement stmt)
var refPattern = new Regex(
$@"\b(FROM|JOIN)\s+{Regex.Escape(cteName)}\b",
RegexOptions.IgnoreCase);
- var refCount = refPattern.Matches(text).Count;
+ var refCount = refPattern.Count(text);
if (refCount > 1)
{
@@ -1243,4 +1235,15 @@ private static string Truncate(string value, int maxLength)
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
}
+
+ [GeneratedRegex(@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex FunctionInPredicateRegExp();
+ [GeneratedRegex(@"\blike\b[^'""]*?N?'%", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex LeadingWildcardLikeRegExp();
+ [GeneratedRegex(@"\bCASE\s+(WHEN\b|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex CaseInPredicateRegExp();
+ [GeneratedRegex(@"(?:\bWITH\s+|\,\s*)(\w+)\s+AS\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex CteDefinitionRegExp();
+ [GeneratedRegex(@"\b(isnull|coalesce)\s*\(", RegexOptions.IgnoreCase)]
+ private static partial Regex IsNullCoalesceRegExp();
}
diff --git a/Dashboard/Services/ReproScriptBuilder.cs b/Dashboard/Services/ReproScriptBuilder.cs
index 3605db3e..f008c549 100644
--- a/Dashboard/Services/ReproScriptBuilder.cs
+++ b/Dashboard/Services/ReproScriptBuilder.cs
@@ -20,7 +20,7 @@ namespace PerformanceMonitorDashboard.Services;
/// Builds paste-ready T-SQL reproduction scripts from query text and plan XML.
/// Extracts parameters from plan XML ParameterList (same approach as sp_QueryReproBuilder).
///
-public static class ReproScriptBuilder
+public static partial class ReproScriptBuilder
{
///
/// Builds a complete reproduction script from available query data.
@@ -399,7 +399,7 @@ private static List FindUnresolvedVariables(string queryText, List(parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);
/* Find all @variable references in the query text */
- var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
+ var matches = AtVariableRegExp().Matches(queryText);
var seenVars = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (Match match in matches)
@@ -429,6 +429,9 @@ private static List FindUnresolvedVariables(string queryText, List
diff --git a/Dashboard/TracePatternHistoryWindow.xaml.cs b/Dashboard/TracePatternHistoryWindow.xaml.cs
index 71ce17c3..fd1de4e8 100644
--- a/Dashboard/TracePatternHistoryWindow.xaml.cs
+++ b/Dashboard/TracePatternHistoryWindow.xaml.cs
@@ -59,7 +59,7 @@ public TracePatternHistoryWindow(
_toDate = toDate;
// Collapse newlines/tabs to spaces and truncate for a clean single-line header
- var displayPattern = System.Text.RegularExpressions.Regex.Replace(queryPattern, @"\s+", " ").Trim();
+ var displayPattern = MultipleSpacesRegExp().Replace(queryPattern, " ").Trim();
if (displayPattern.Length > 120)
displayPattern = displayPattern.Substring(0, 120) + "...";
QueryIdentifierText.Text = $"Trace Pattern History: [{databaseName}] — {displayPattern}";
@@ -406,6 +406,9 @@ private void ExportToCsv_Click(object sender, RoutedEventArgs e)
}
}
+ [System.Text.RegularExpressions.GeneratedRegex(@"\s+")]
+ private static partial System.Text.RegularExpressions.Regex MultipleSpacesRegExp();
+
#endregion
}
}
diff --git a/Installer/Program.cs b/Installer/Program.cs
index d16303ae..69f2776d 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -19,14 +19,12 @@
namespace PerformanceMonitorInstaller
{
- class Program
+ partial class Program
{
/*
Pre-compiled regex patterns for performance
*/
- private static readonly Regex GoBatchPattern = new Regex(
- @"^\s*GO\s*(?:--[^\r\n]*)?\s*$",
- RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase);
+ private static readonly Regex GoBatchPattern = GoBatchRegExp();
private static readonly Regex SqlFileNamePattern = new Regex(
@"^\d{2}[a-z]?_.*\.sql$",
@@ -1800,5 +1798,8 @@ Write file
return reportPath;
}
+
+ [GeneratedRegex(@"^\s*GO\s*(?:--[^\r\n]*)?\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled)]
+ private static partial Regex GoBatchRegExp();
}
}
diff --git a/InstallerGui/Services/InstallationService.cs b/InstallerGui/Services/InstallationService.cs
index 25ccc85e..97a5f0e3 100644
--- a/InstallerGui/Services/InstallationService.cs
+++ b/InstallerGui/Services/InstallationService.cs
@@ -62,7 +62,7 @@ public class InstallationResult
///
/// Service for installing the Performance Monitor database
///
- public class InstallationService : IDisposable
+ public partial class InstallationService : IDisposable
{
private readonly HttpClient _httpClient;
private bool _disposed;
@@ -70,9 +70,7 @@ public class InstallationService : IDisposable
/*
Compiled regex patterns for better performance
*/
- private static readonly Regex SqlFilePattern = new(
- @"^\d{2}[a-z]?_.*\.sql$",
- RegexOptions.Compiled);
+ private static readonly Regex SqlFilePattern = SqlFileRegExp();
private static readonly Regex SqlCmdDirectivePattern = new(
@"^:r\s+.*$",
@@ -1429,5 +1427,8 @@ INSERT INTO PerformanceMonitor.config.installation_history
/*Don't let history logging failure break the installation*/
}
}
+
+ [GeneratedRegex(@"^\d{2}[a-z]?_.*\.sql$", RegexOptions.Compiled)]
+ private static partial Regex SqlFileRegExp();
}
}
diff --git a/Lite.Tests/Lite.Tests.csproj b/Lite.Tests/Lite.Tests.csproj
index 86268874..874fd31f 100644
--- a/Lite.Tests/Lite.Tests.csproj
+++ b/Lite.Tests/Lite.Tests.csproj
@@ -9,7 +9,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs
index 8031874d..b3d56074 100644
--- a/Lite/Services/PlanAnalyzer.cs
+++ b/Lite/Services/PlanAnalyzer.cs
@@ -10,11 +10,9 @@ namespace PerformanceMonitorLite.Services;
/// Post-parse analysis pass that walks a parsed plan tree and adds warnings
/// for common performance anti-patterns. Called after ShowPlanParser.Parse().
///
-public static class PlanAnalyzer
+public static partial class PlanAnalyzer
{
- private static readonly Regex FunctionInPredicateRegex = new(
- @"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex FunctionInPredicateRegex = FunctionInPredicateRegExp();
private static readonly Regex LeadingWildcardLikeRegex = new(
@"\blike\b[^'""]*?N?'%",
@@ -686,7 +684,7 @@ _ when nonSargableReason.StartsWith("Function call") =>
// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
- node.ObjectName.StartsWith("@"))
+ node.ObjectName.StartsWith('@'))
{
node.Warnings.Add(new PlanWarning
{
@@ -930,7 +928,7 @@ private static void DetectMultiReferenceCte(PlanStatement stmt)
var refPattern = new Regex(
$@"\b(FROM|JOIN)\s+{Regex.Escape(cteName)}\b",
RegexOptions.IgnoreCase);
- var refCount = refPattern.Matches(text).Count;
+ var refCount = refPattern.Count(text);
if (refCount > 1)
{
@@ -1243,4 +1241,7 @@ private static string Truncate(string value, int maxLength)
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
}
+
+ [GeneratedRegex(@"\b(CONVERT_IMPLICIT|CONVERT|CAST|isnull|coalesce|datepart|datediff|dateadd|year|month|day|upper|lower|ltrim|rtrim|trim|substring|left|right|charindex|replace|len|datalength|abs|floor|ceiling|round|reverse|stuff|format)\s*\(", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex FunctionInPredicateRegExp();
}
diff --git a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
index b85500c1..93f5e363 100644
--- a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
+++ b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
@@ -8,6 +8,7 @@
using System;
using System.Diagnostics;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DuckDB.NET.Data;
@@ -19,7 +20,8 @@ namespace PerformanceMonitorLite.Services;
public partial class RemoteCollectorService
{
- private const string QuerySnapshotsBase = @"
+ private const string QuerySnapshotsBase = """
+
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET LOCK_TIMEOUT 1000;
@@ -80,7 +82,9 @@ WHERE der.session_id <> @@SPID
AND dest.text IS NOT NULL
AND der.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
ORDER BY der.cpu_time DESC, der.parallel_worker_count DESC
-OPTION(MAXDOP 1, RECOMPILE);";
+OPTION(MAXDOP 1, RECOMPILE);
+""";
+ private readonly static CompositeFormat QuerySnapshotsBaseFormat = CompositeFormat.Parse(QuerySnapshotsBase);
///
/// Builds the query snapshots SQL with or without live query plan support.
@@ -89,8 +93,8 @@ AND dest.text IS NOT NULL
internal static string BuildQuerySnapshotsQuery(bool supportsLiveQueryPlan)
{
return supportsLiveQueryPlan
- ? string.Format(QuerySnapshotsBase, "live_query_plan = deqs.query_plan,", "OUTER APPLY sys.dm_exec_query_statistics_xml(der.session_id) AS deqs")
- : string.Format(QuerySnapshotsBase, "live_query_plan = CONVERT(xml, NULL),", "");
+ ? string.Format(null, QuerySnapshotsBaseFormat, "live_query_plan = deqs.query_plan,", "OUTER APPLY sys.dm_exec_query_statistics_xml(der.session_id) AS deqs")
+ : string.Format(null, QuerySnapshotsBaseFormat, "live_query_plan = CONVERT(xml, NULL),", "");
}
///
@@ -126,44 +130,42 @@ private async Task CollectQuerySnapshotsAsync(ServerConnection server, Canc
{
await duckConnection.OpenAsync(cancellationToken);
- using (var appender = duckConnection.CreateAppender("query_snapshots"))
+ using var appender = duckConnection.CreateAppender("query_snapshots");
+ while (await reader.ReadAsync(cancellationToken))
{
- while (await reader.ReadAsync(cancellationToken))
- {
- var row = appender.CreateRow();
- row.AppendValue(GenerateCollectionId())
- .AppendValue(collectionTime)
- .AppendValue(serverId)
- .AppendValue(server.ServerName)
- .AppendValue(Convert.ToInt32(reader.GetValue(0))) /* session_id */
- .AppendValue(reader.IsDBNull(1) ? (string?)null : reader.GetString(1)) /* database_name */
- .AppendValue(reader.IsDBNull(2) ? (string?)null : reader.GetString(2)) /* elapsed_time_formatted */
- .AppendValue(reader.IsDBNull(3) ? (string?)null : reader.GetString(3)) /* query_text */
- .AppendValue(reader.IsDBNull(4) ? (string?)null : reader.GetString(4)) /* query_plan */
- .AppendValue(reader.IsDBNull(5) ? (string?)null : reader.GetValue(5)?.ToString()) /* live_query_plan (xml) */
- .AppendValue(reader.IsDBNull(6) ? (string?)null : reader.GetString(6)) /* status */
- .AppendValue(reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7))) /* blocking_session_id */
- .AppendValue(reader.IsDBNull(8) ? (string?)null : reader.GetString(8)) /* wait_type */
- .AppendValue(reader.IsDBNull(9) ? 0L : Convert.ToInt64(reader.GetValue(9))) /* wait_time_ms */
- .AppendValue(reader.IsDBNull(10) ? (string?)null : reader.GetString(10)) /* wait_resource */
- .AppendValue(reader.IsDBNull(11) ? 0L : Convert.ToInt64(reader.GetValue(11))) /* cpu_time_ms */
- .AppendValue(reader.IsDBNull(12) ? 0L : Convert.ToInt64(reader.GetValue(12))) /* total_elapsed_time_ms */
- .AppendValue(reader.IsDBNull(13) ? 0L : Convert.ToInt64(reader.GetValue(13))) /* reads */
- .AppendValue(reader.IsDBNull(14) ? 0L : Convert.ToInt64(reader.GetValue(14))) /* writes */
- .AppendValue(reader.IsDBNull(15) ? 0L : Convert.ToInt64(reader.GetValue(15))) /* logical_reads */
- .AppendValue(reader.IsDBNull(16) ? 0m : reader.GetDecimal(16)) /* granted_query_memory_gb */
- .AppendValue(reader.IsDBNull(17) ? (string?)null : reader.GetString(17)) /* transaction_isolation_level */
- .AppendValue(reader.IsDBNull(18) ? 0 : Convert.ToInt32(reader.GetValue(18))) /* dop */
- .AppendValue(reader.IsDBNull(19) ? 0 : Convert.ToInt32(reader.GetValue(19))) /* parallel_worker_count */
- .AppendValue(reader.IsDBNull(20) ? (string?)null : reader.GetString(20)) /* login_name */
- .AppendValue(reader.IsDBNull(21) ? (string?)null : reader.GetString(21)) /* host_name */
- .AppendValue(reader.IsDBNull(22) ? (string?)null : reader.GetString(22)) /* program_name */
- .AppendValue(reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23))) /* open_transaction_count */
- .AppendValue(reader.IsDBNull(24) ? 0m : Convert.ToDecimal(reader.GetValue(24))) /* percent_complete */
- .EndRow();
-
- rowsCollected++;
- }
+ var row = appender.CreateRow();
+ row.AppendValue(GenerateCollectionId())
+ .AppendValue(collectionTime)
+ .AppendValue(serverId)
+ .AppendValue(server.ServerName)
+ .AppendValue(Convert.ToInt32(reader.GetValue(0))) /* session_id */
+ .AppendValue(reader.IsDBNull(1) ? (string?)null : reader.GetString(1)) /* database_name */
+ .AppendValue(reader.IsDBNull(2) ? (string?)null : reader.GetString(2)) /* elapsed_time_formatted */
+ .AppendValue(reader.IsDBNull(3) ? (string?)null : reader.GetString(3)) /* query_text */
+ .AppendValue(reader.IsDBNull(4) ? (string?)null : reader.GetString(4)) /* query_plan */
+ .AppendValue(reader.IsDBNull(5) ? (string?)null : reader.GetValue(5)?.ToString()) /* live_query_plan (xml) */
+ .AppendValue(reader.IsDBNull(6) ? (string?)null : reader.GetString(6)) /* status */
+ .AppendValue(reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7))) /* blocking_session_id */
+ .AppendValue(reader.IsDBNull(8) ? (string?)null : reader.GetString(8)) /* wait_type */
+ .AppendValue(reader.IsDBNull(9) ? 0L : Convert.ToInt64(reader.GetValue(9))) /* wait_time_ms */
+ .AppendValue(reader.IsDBNull(10) ? (string?)null : reader.GetString(10)) /* wait_resource */
+ .AppendValue(reader.IsDBNull(11) ? 0L : Convert.ToInt64(reader.GetValue(11))) /* cpu_time_ms */
+ .AppendValue(reader.IsDBNull(12) ? 0L : Convert.ToInt64(reader.GetValue(12))) /* total_elapsed_time_ms */
+ .AppendValue(reader.IsDBNull(13) ? 0L : Convert.ToInt64(reader.GetValue(13))) /* reads */
+ .AppendValue(reader.IsDBNull(14) ? 0L : Convert.ToInt64(reader.GetValue(14))) /* writes */
+ .AppendValue(reader.IsDBNull(15) ? 0L : Convert.ToInt64(reader.GetValue(15))) /* logical_reads */
+ .AppendValue(reader.IsDBNull(16) ? 0m : reader.GetDecimal(16)) /* granted_query_memory_gb */
+ .AppendValue(reader.IsDBNull(17) ? (string?)null : reader.GetString(17)) /* transaction_isolation_level */
+ .AppendValue(reader.IsDBNull(18) ? 0 : Convert.ToInt32(reader.GetValue(18))) /* dop */
+ .AppendValue(reader.IsDBNull(19) ? 0 : Convert.ToInt32(reader.GetValue(19))) /* parallel_worker_count */
+ .AppendValue(reader.IsDBNull(20) ? (string?)null : reader.GetString(20)) /* login_name */
+ .AppendValue(reader.IsDBNull(21) ? (string?)null : reader.GetString(21)) /* host_name */
+ .AppendValue(reader.IsDBNull(22) ? (string?)null : reader.GetString(22)) /* program_name */
+ .AppendValue(reader.IsDBNull(23) ? 0 : Convert.ToInt32(reader.GetValue(23))) /* open_transaction_count */
+ .AppendValue(reader.IsDBNull(24) ? 0m : Convert.ToDecimal(reader.GetValue(24))) /* percent_complete */
+ .EndRow();
+
+ rowsCollected++;
}
}
diff --git a/Lite/Services/ReproScriptBuilder.cs b/Lite/Services/ReproScriptBuilder.cs
index 6a9a35a9..a1fef754 100644
--- a/Lite/Services/ReproScriptBuilder.cs
+++ b/Lite/Services/ReproScriptBuilder.cs
@@ -19,7 +19,7 @@ namespace PerformanceMonitorLite.Services;
/// Builds paste-ready T-SQL reproduction scripts from query text and plan XML.
/// Extracts parameters from plan XML ParameterList (same approach as sp_QueryReproBuilder).
///
-public static class ReproScriptBuilder
+public static partial class ReproScriptBuilder
{
///
/// Builds a complete reproduction script from available query data.
@@ -397,7 +397,7 @@ private static List FindUnresolvedVariables(string queryText, List(parameters.Select(p => p.Name), StringComparer.OrdinalIgnoreCase);
/* Find all @variable references in the query text */
- var matches = Regex.Matches(queryText, @"@\w+", RegexOptions.IgnoreCase);
+ var matches = AtVariableRegExp().Matches(queryText);
var seenVars = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (Match match in matches)
@@ -427,6 +427,9 @@ private static List FindUnresolvedVariables(string queryText, List