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