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() { 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 7b3646a..20eb442 100644 --- a/UtilsTest/Reflection/MultiDelegateInvokerTests.cs +++ b/UtilsTest/Reflection/MultiDelegateInvokerTests.cs @@ -1,17 +1,50 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Utils.Reflection; 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 +56,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,38 +70,58 @@ 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 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; + }); - Stopwatch sw = Stopwatch.StartNew(); - int[] results = await invoker.InvokeParallelAsync(3); - sw.Stop(); + Task invocation = invoker.InvokeParallelAsync(3); + bool bothStarted = SpinWait.SpinUntil(() => Volatile.Read(ref started) == 2, 1000); + gate.Set(); - CollectionAssert.AreEqual(new[] { 4, 5 }, results); - Assert.IsTrue(sw.ElapsedMilliseconds < 190); + int[] parallelResults = await invocation; + int[] sequentialResults = await invoker.InvokeAsync(3); + + CollectionAssert.AreEqual(new[] { 4, 5 }, parallelResults); + CollectionAssert.AreEqual(new[] { 4, 5 }, sequentialResults); + Assert.IsTrue(bothStarted, "Delegates did not start concurrently."); } [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); - 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); } }