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);
}
}