From 3ef89b0fe7b715f45d17e51976d5864eff4173e8 Mon Sep 17 00:00:00 2001 From: warny Date: Fri, 19 Dec 2025 11:59:10 +0100 Subject: [PATCH 1/3] Add database-specific SQL syntax options --- Utils.Data/Sql/SqlParser.cs | 24 ++++++++++- Utils.Data/Sql/SqlSyntaxOptions.cs | 35 ++++++++++++++-- UtilsTest/Data/SqlQueryAnalyzerTests.cs | 55 +++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/Utils.Data/Sql/SqlParser.cs b/Utils.Data/Sql/SqlParser.cs index 7ba282b..810499f 100644 --- a/Utils.Data/Sql/SqlParser.cs +++ b/Utils.Data/Sql/SqlParser.cs @@ -928,14 +928,34 @@ private void SkipBlockComment() _ => false, }; + /// + /// Determines whether the provided character can start an identifier or parameter token. + /// + /// The character to evaluate. + /// true when the character can begin an identifier; otherwise, false. private bool IsIdentifierStart(char c) { - return char.IsLetter(c) || c == '_' || c == '$' || syntaxOptions.IsIdentifierPrefix(c); + return char.IsLetter(c) || c == '_' || IsIdentifierPrefix(c); } + /// + /// Determines whether the provided character can appear within an identifier or parameter token. + /// + /// The character to evaluate. + /// true when the character can appear in an identifier; otherwise, false. private bool IsIdentifierPart(char c) { - return char.IsLetterOrDigit(c) || c == '_' || c == '$' || syntaxOptions.IsIdentifierPrefix(c); + return char.IsLetterOrDigit(c) || c == '_' || IsIdentifierPrefix(c); + } + + /// + /// Checks whether a character is configured as an identifier prefix or matches a common parameter prefix. + /// + /// The character to evaluate. + /// true when the character is treated as an identifier prefix; otherwise, false. + private bool IsIdentifierPrefix(char c) + { + return syntaxOptions.IsIdentifierPrefix(c); } private char Peek() => sql[index]; diff --git a/Utils.Data/Sql/SqlSyntaxOptions.cs b/Utils.Data/Sql/SqlSyntaxOptions.cs index 976117e..15c6c9f 100644 --- a/Utils.Data/Sql/SqlSyntaxOptions.cs +++ b/Utils.Data/Sql/SqlSyntaxOptions.cs @@ -8,7 +8,11 @@ namespace Utils.Data.Sql; /// public sealed class SqlSyntaxOptions { - private static readonly char[] DefaultIdentifierPrefixes = { '@', '#', '$' }; + private static readonly char[] SqlServerIdentifierPrefixes = { '@', '#', '$' }; + private static readonly char[] OracleIdentifierPrefixes = { ':' }; + private static readonly char[] MySqlIdentifierPrefixes = { '@' }; + private static readonly char[] SqliteIdentifierPrefixes = { '@', ':', '$', '?' }; + private static readonly char[] PostgreSqlIdentifierPrefixes = { '$' }; private readonly HashSet identifierPrefixes; @@ -20,7 +24,7 @@ public sealed class SqlSyntaxOptions public SqlSyntaxOptions(IEnumerable? identifierPrefixes = null, char autoParameterPrefix = '@') { var resolvedPrefixes = identifierPrefixes == null - ? new HashSet(DefaultIdentifierPrefixes) + ? new HashSet(SqlServerIdentifierPrefixes) : new HashSet(identifierPrefixes); if (resolvedPrefixes.Count == 0) @@ -34,10 +38,35 @@ public SqlSyntaxOptions(IEnumerable? identifierPrefixes = null, char autoP AutoParameterPrefix = autoParameterPrefix; } + /// + /// Gets syntax options configured for Microsoft SQL Server. + /// + public static SqlSyntaxOptions SqlServer { get; } = new SqlSyntaxOptions(SqlServerIdentifierPrefixes, '@'); + + /// + /// Gets syntax options configured for Oracle databases. + /// + public static SqlSyntaxOptions Oracle { get; } = new SqlSyntaxOptions(OracleIdentifierPrefixes, ':'); + + /// + /// Gets syntax options configured for MySQL databases. + /// + public static SqlSyntaxOptions MySql { get; } = new SqlSyntaxOptions(MySqlIdentifierPrefixes, '@'); + + /// + /// Gets syntax options configured for SQLite databases. + /// + public static SqlSyntaxOptions Sqlite { get; } = new SqlSyntaxOptions(SqliteIdentifierPrefixes, '@'); + + /// + /// Gets syntax options configured for PostgreSQL databases. + /// + public static SqlSyntaxOptions PostgreSql { get; } = new SqlSyntaxOptions(PostgreSqlIdentifierPrefixes, '$'); + /// /// Gets the default syntax options supporting common SQL Server style prefixes. /// - public static SqlSyntaxOptions Default { get; } = new SqlSyntaxOptions(DefaultIdentifierPrefixes, '@'); + public static SqlSyntaxOptions Default { get; } = SqlServer; /// /// Gets the characters that can prefix identifiers. diff --git a/UtilsTest/Data/SqlQueryAnalyzerTests.cs b/UtilsTest/Data/SqlQueryAnalyzerTests.cs index 8126ba9..3eabb81 100644 --- a/UtilsTest/Data/SqlQueryAnalyzerTests.cs +++ b/UtilsTest/Data/SqlQueryAnalyzerTests.cs @@ -134,6 +134,61 @@ public void ParseSupportsCustomParameterPrefixes() Assert.AreEqual(sql, query.ToSql()); } + [TestMethod] + public void ParseSqlServerPrefixesWithDedicatedOptions() + { + const string sql = "SELECT * FROM #temp WHERE Id = @id"; + + SqlQuery query = SqlQueryAnalyzer.Parse(sql, SqlSyntaxOptions.SqlServer); + + Assert.AreSame(SqlSyntaxOptions.SqlServer, query.SyntaxOptions); + Assert.AreEqual(sql, query.ToSql()); + } + + [TestMethod] + public void ParseOraclePrefixesWithDedicatedOptions() + { + const string sql = "SELECT * FROM accounts WHERE id = :account_id"; + + SqlQuery query = SqlQueryAnalyzer.Parse(sql, SqlSyntaxOptions.Oracle); + + Assert.AreSame(SqlSyntaxOptions.Oracle, query.SyntaxOptions); + Assert.AreEqual(sql, query.ToSql()); + } + + [TestMethod] + public void ParseMySqlPrefixesWithDedicatedOptions() + { + const string sql = "SELECT * FROM users WHERE id = @user_id"; + + SqlQuery query = SqlQueryAnalyzer.Parse(sql, SqlSyntaxOptions.MySql); + + Assert.AreSame(SqlSyntaxOptions.MySql, query.SyntaxOptions); + Assert.AreEqual(sql, query.ToSql()); + } + + [TestMethod] + public void ParseSqlitePrefixesWithDedicatedOptions() + { + const string sql = "SELECT * FROM accounts WHERE id = ?1"; + + SqlQuery query = SqlQueryAnalyzer.Parse(sql, SqlSyntaxOptions.Sqlite); + + Assert.AreSame(SqlSyntaxOptions.Sqlite, query.SyntaxOptions); + Assert.AreEqual(sql, query.ToSql()); + } + + [TestMethod] + public void ParsePostgreSqlPrefixesWithDedicatedOptions() + { + const string sql = "SELECT * FROM users WHERE id = $1"; + + SqlQuery query = SqlQueryAnalyzer.Parse(sql, SqlSyntaxOptions.PostgreSql); + + Assert.AreSame(SqlSyntaxOptions.PostgreSql, query.SyntaxOptions); + Assert.AreEqual(sql, query.ToSql()); + } + [TestMethod] public void ToSqlSupportsFormattingModes() { From 413f450ae441f36132d49c2fb1ad050970ba7768 Mon Sep 17 00:00:00 2001 From: warny Date: Fri, 19 Dec 2025 16:11:08 +0100 Subject: [PATCH 2/3] Stabilize parallel invocation test --- .../Reflection/MultiDelegateInvokerTests.cs | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/UtilsTest/Reflection/MultiDelegateInvokerTests.cs b/UtilsTest/Reflection/MultiDelegateInvokerTests.cs index 7b3646a..eb7d561 100644 --- a/UtilsTest/Reflection/MultiDelegateInvokerTests.cs +++ b/UtilsTest/Reflection/MultiDelegateInvokerTests.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Diagnostics; using System.Threading.Tasks; @@ -5,13 +6,44 @@ namespace UtilsTest.Reflection; +/// +/// Tests for behavior. +/// [TestClass] public class MultiDelegateInvokerTests { + /// + /// Adds one to the provided integer value. + /// + /// The input value. + /// The input value incremented by one. private static int AddOne(int i) => i + 1; + + /// + /// Adds two to the provided integer value. + /// + /// The input value. + /// The input value incremented by two. private static int AddTwo(int i) => i + 2; + /// + /// Measures the execution time of a delegate invoker call. + /// + /// The invocation to measure. + /// The invocation results and elapsed time in milliseconds. + private static async Task<(int[] Results, long ElapsedMilliseconds)> MeasureInvocationAsync(Func> invocation) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + int[] results = await invocation(); + stopwatch.Stop(); + + return (results, stopwatch.ElapsedMilliseconds); + } + [TestMethod] + /// + /// Ensures that synchronous invocation returns every delegate result in order. + /// public void Invoke_Returns_All_Results() { var invoker = new MultiDelegateInvoker(); @@ -23,6 +55,9 @@ public void Invoke_Returns_All_Results() } [TestMethod] + /// + /// Ensures that asynchronous invocation returns every delegate result in order. + /// public async Task InvokeAsync_Returns_All_Results() { var invoker = new MultiDelegateInvoker(); @@ -34,21 +69,27 @@ public async Task InvokeAsync_Returns_All_Results() } [TestMethod] + /// + /// Verifies that parallel invocation completes faster than sequential execution while returning all results. + /// public async Task InvokeParallelAsync_Executes_In_Parallel() { var invoker = new MultiDelegateInvoker(); invoker.Add(i => { System.Threading.Thread.Sleep(100); return i + 1; }); invoker.Add(i => { System.Threading.Thread.Sleep(100); return i + 2; }); - Stopwatch sw = Stopwatch.StartNew(); - int[] results = await invoker.InvokeParallelAsync(3); - sw.Stop(); + (int[] parallelResults, long parallelDuration) = await MeasureInvocationAsync(() => invoker.InvokeParallelAsync(3)); + (int[] sequentialResults, long sequentialDuration) = await MeasureInvocationAsync(() => invoker.InvokeAsync(3)); - CollectionAssert.AreEqual(new[] { 4, 5 }, results); - Assert.IsTrue(sw.ElapsedMilliseconds < 190); + CollectionAssert.AreEqual(new[] { 4, 5 }, parallelResults); + CollectionAssert.AreEqual(new[] { 4, 5 }, sequentialResults); + Assert.IsTrue(parallelDuration + 30 < sequentialDuration); } [TestMethod] + /// + /// Confirms that the smart invocation strategy switches between sequential and parallel execution based on the configured threshold. + /// public async Task InvokeSmartAsync_Switches_Based_On_Threshold() { var sequential = new MultiDelegateInvoker(3); From 104ae2e0124e3ca25f0d1b7c87dd5c5616e94e1b Mon Sep 17 00:00:00 2001 From: warny Date: Fri, 19 Dec 2025 16:23:59 +0100 Subject: [PATCH 3/3] Stabilize range and invoker tests --- UtilsTest/Objects/RangeTests.cs | 8 +-- .../Reflection/MultiDelegateInvokerTests.cs | 51 ++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/UtilsTest/Objects/RangeTests.cs b/UtilsTest/Objects/RangeTests.cs index 495448b..aa99eb2 100644 --- a/UtilsTest/Objects/RangeTests.cs +++ b/UtilsTest/Objects/RangeTests.cs @@ -150,10 +150,10 @@ public void RangeTestReverse() { int[] table = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] expected = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; - table.Reverse(); - Assert.AreEqual(expected[0], table[0]); - Assert.AreEqual(expected[1], table[1]); - Assert.IsTrue(comparer.Equals(expected, table)); + var range = ((IReadOnlyList)table).Reverse(); + Assert.AreEqual(expected[0], range[0]); + Assert.AreEqual(expected[1], range[1]); + Assert.IsTrue(comparer.Equals(expected, range)); } } diff --git a/UtilsTest/Reflection/MultiDelegateInvokerTests.cs b/UtilsTest/Reflection/MultiDelegateInvokerTests.cs index eb7d561..20eb442 100644 --- a/UtilsTest/Reflection/MultiDelegateInvokerTests.cs +++ b/UtilsTest/Reflection/MultiDelegateInvokerTests.cs @@ -1,7 +1,8 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Utils.Reflection; namespace UtilsTest.Reflection; @@ -74,16 +75,32 @@ public async Task InvokeAsync_Returns_All_Results() /// public async Task InvokeParallelAsync_Executes_In_Parallel() { + var gate = new ManualResetEventSlim(false); + int started = 0; var invoker = new MultiDelegateInvoker(); - invoker.Add(i => { System.Threading.Thread.Sleep(100); return i + 1; }); - invoker.Add(i => { System.Threading.Thread.Sleep(100); return i + 2; }); + invoker.Add(i => + { + Interlocked.Increment(ref started); + gate.Wait(); + return i + 1; + }); + invoker.Add(i => + { + Interlocked.Increment(ref started); + gate.Wait(); + return i + 2; + }); + + Task invocation = invoker.InvokeParallelAsync(3); + bool bothStarted = SpinWait.SpinUntil(() => Volatile.Read(ref started) == 2, 1000); + gate.Set(); - (int[] parallelResults, long parallelDuration) = await MeasureInvocationAsync(() => invoker.InvokeParallelAsync(3)); - (int[] sequentialResults, long sequentialDuration) = await MeasureInvocationAsync(() => invoker.InvokeAsync(3)); + int[] parallelResults = await invocation; + int[] sequentialResults = await invoker.InvokeAsync(3); CollectionAssert.AreEqual(new[] { 4, 5 }, parallelResults); CollectionAssert.AreEqual(new[] { 4, 5 }, sequentialResults); - Assert.IsTrue(parallelDuration + 30 < sequentialDuration); + Assert.IsTrue(bothStarted, "Delegates did not start concurrently."); } [TestMethod] @@ -93,20 +110,18 @@ public async Task InvokeParallelAsync_Executes_In_Parallel() public async Task InvokeSmartAsync_Switches_Based_On_Threshold() { var sequential = new MultiDelegateInvoker(3); - sequential.Add(i => { System.Threading.Thread.Sleep(100); return i + 1; }); - sequential.Add(i => { System.Threading.Thread.Sleep(100); return i + 2; }); - Stopwatch sw1 = Stopwatch.StartNew(); - await sequential.InvokeSmartAsync(3); - sw1.Stop(); - Assert.IsTrue(sw1.ElapsedMilliseconds >= 190); + sequential.Add(i => { System.Threading.Thread.Sleep(150); return i + 1; }); + sequential.Add(i => { System.Threading.Thread.Sleep(150); return i + 2; }); + (int[] sequentialResults, long sequentialDuration) = await MeasureInvocationAsync(() => sequential.InvokeSmartAsync(3)); var parallel = new MultiDelegateInvoker(1); - parallel.Add(i => { System.Threading.Thread.Sleep(100); return i + 1; }); - parallel.Add(i => { System.Threading.Thread.Sleep(100); return i + 2; }); - Stopwatch sw2 = Stopwatch.StartNew(); - await parallel.InvokeSmartAsync(3); - sw2.Stop(); - Assert.IsTrue(sw2.ElapsedMilliseconds < 190); + parallel.Add(i => { System.Threading.Thread.Sleep(150); return i + 1; }); + parallel.Add(i => { System.Threading.Thread.Sleep(150); return i + 2; }); + (int[] parallelResults, long parallelDuration) = await MeasureInvocationAsync(() => parallel.InvokeSmartAsync(3)); + + CollectionAssert.AreEqual(new[] { 4, 5 }, sequentialResults); + CollectionAssert.AreEqual(new[] { 4, 5 }, parallelResults); + Assert.IsTrue(parallelDuration + 40 < sequentialDuration); } }