From 49c551291eb923a41a226a263e8477c1ef9b1d35 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Wed, 11 Jun 2025 11:06:16 +0700 Subject: [PATCH 1/4] Summary of changes - add new method CreateSpanCollation Fixes #35236 --- .../SqliteConnection.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs index 88efdbeb07f..32d3ec86254 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs @@ -10,6 +10,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Data.Sqlite.Properties; using SQLitePCL; using static SQLitePCL.raw; @@ -31,6 +32,7 @@ public partial class SqliteConnection : DbConnection private readonly List> _commands = []; private Dictionary? _collations; + private Dictionary? _collationsSpan; private Dictionary<(string name, int arity), (int flags, object? state, delegate_function_scalar? func)>? _functions; @@ -278,6 +280,15 @@ public override void Open() } } + if (_collationsSpan != null) + { + foreach (var item in _collationsSpan) + { + rc = sqlite3__create_collation_utf8(Handle, item.Key, item.Value.state, item.Value.collation); + SqliteException.ThrowExceptionForRC(rc, Handle); + } + } + if (_functions != null) { foreach (var item in _functions) @@ -372,6 +383,15 @@ internal void Deactivate() } } + if (_collationsSpan != null) + { + foreach (var item in _collationsSpan.Keys) + { + rc = sqlite3__create_collation_utf8(Handle, item, null, null); + SqliteException.ThrowExceptionForRC(rc, Handle); + } + } + if (_functions != null) { foreach (var (name, arity) in _functions.Keys) @@ -493,6 +513,62 @@ public virtual void CreateCollation(string name, T state, Func + /// Create custom collation. + /// + /// The type of the state object. + /// Name of the collation. + /// State object passed to each invocation of the collation. + /// Method that compares two char spans, using additional state. + /// Collation + public virtual void CreateSpanCollation(string name, T state, SpanDelegateCollation? comparison) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } +#if NET5_0_OR_GREATER + delegate_collation? collation = comparison != null ? (v, s1, s2) => + { + Span s1Span = stackalloc char[Encoding.UTF8.GetCharCount(s1)]; + Span s2Span = stackalloc char[Encoding.UTF8.GetCharCount(s2)]; + Encoding.UTF8.GetChars(s1, s1Span); + Encoding.UTF8.GetChars(s2, s2Span); + return comparison((T)v, s1Span, s2Span); + } + : null; +#else + delegate_collation? collation = comparison != null ? (v, s1, s2) => + { + return comparison((T)v, Encoding.UTF8.GetChars(s1.ToArray()), Encoding.UTF8.GetChars(s2.ToArray())); + } + : null; +#endif + if (State == ConnectionState.Open) + { + var rc = sqlite3__create_collation_utf8(Handle, name, state, collation); + SqliteException.ThrowExceptionForRC(rc, Handle); + } + + _collationsSpan ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + _collationsSpan[name] = (state, collation); + } + + /// + /// Represents a method signature for a custom collation delegate that compares two read-only spans of characters. + /// + /// An optional user-defined state object to be passed to the comparison function. + /// The first read-only span of characters to compare. + /// The second read-only span of characters to compare. + /// + /// A signed integer indicating the relative order of the strings being compared: + /// Less than zero if precedes ; + /// Zero if is equal to ; + /// Greater than zero if follows . + /// + public delegate int SpanDelegateCollation(in object? state, in ReadOnlySpan s1, in ReadOnlySpan s2); + /// /// Begins a transaction on the connection. /// From b91907b725efcb44209526ca364fcd7772bc80a7 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Jun 2025 11:51:11 +0700 Subject: [PATCH 2/4] use generic for collation callback and remove in modifiers from ReadOnlySpan --- src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs index 32d3ec86254..8dd897686f3 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs @@ -522,7 +522,7 @@ public virtual void CreateCollation(string name, T state, FuncState object passed to each invocation of the collation. /// Method that compares two char spans, using additional state. /// Collation - public virtual void CreateSpanCollation(string name, T state, SpanDelegateCollation? comparison) + public virtual void CreateSpanCollation(string name, T state, SpanDelegateCollation? comparison) { if (string.IsNullOrEmpty(name)) { @@ -561,13 +561,14 @@ public virtual void CreateSpanCollation(string name, T state, SpanDelegateCol /// An optional user-defined state object to be passed to the comparison function. /// The first read-only span of characters to compare. /// The second read-only span of characters to compare. + /// state type /// /// A signed integer indicating the relative order of the strings being compared: /// Less than zero if precedes ; /// Zero if is equal to ; /// Greater than zero if follows . /// - public delegate int SpanDelegateCollation(in object? state, in ReadOnlySpan s1, in ReadOnlySpan s2); + public delegate int SpanDelegateCollation(T state, ReadOnlySpan s1, ReadOnlySpan s2); /// /// Begins a transaction on the connection. From 887578d6b45bacdfaf84d174fcfa9c80f8b9c707 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Jun 2025 12:12:32 +0700 Subject: [PATCH 3/4] create tests --- .../SqliteConnectionTest.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.Data.Sqlite.Tests/SqliteConnectionTest.cs b/test/Microsoft.Data.Sqlite.Tests/SqliteConnectionTest.cs index 3e894bb46c2..27317a0f48d 100644 --- a/test/Microsoft.Data.Sqlite.Tests/SqliteConnectionTest.cs +++ b/test/Microsoft.Data.Sqlite.Tests/SqliteConnectionTest.cs @@ -604,6 +604,70 @@ public void CreateCollation_works_with_state() Assert.Equal("Invoked", item); } + [Fact] + public void CreateCollationSpan_works() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + connection.CreateSpanCollation("MY_NOCASE", null, (_, s1, s2) => s1.CompareTo(s2, StringComparison.OrdinalIgnoreCase)); + + Assert.Equal(1L, connection.ExecuteScalar("SELECT 'Νικοσ' = 'ΝΙΚΟΣ' COLLATE MY_NOCASE;")); + } + + [Fact] + public void CreateCollationSpan_with_null_comparer_works() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + connection.CreateSpanCollation("MY_NOCASE", null, (_, s1, s2) => s1.CompareTo(s2, StringComparison.OrdinalIgnoreCase)); + connection.CreateSpanCollation("MY_NOCASE", null, null); + + var ex = Assert.Throws( + () => connection.ExecuteScalar("SELECT 'Νικοσ' = 'ΝΙΚΟΣ' COLLATE MY_NOCASE;")); + + Assert.Equal(Resources.SqliteNativeError(SQLITE_ERROR, "no such collation sequence: MY_NOCASE"), ex.Message); + } + + [Fact] + public void CreateCollationSpan_works_when_closed() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.CreateSpanCollation("MY_NOCASE", null, (_, s1, s2) => s1.CompareTo(s2, StringComparison.OrdinalIgnoreCase)); + connection.Open(); + + Assert.Equal(1L, connection.ExecuteScalar("SELECT 'Νικοσ' = 'ΝΙΚΟΣ' COLLATE MY_NOCASE;")); + } + + [Fact] + public void CreateCollationSpan_throws_with_empty_name() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var ex = Assert.Throws(() => connection.CreateSpanCollation(null!, null, null)); + + Assert.Equal("name", ex.ParamName); + } + + [Fact] + public void CreateCollationSpan_works_with_state() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + var list = new List(); + connection.CreateSpanCollation( + "MY_NOCASE", + list, + (l, s1, s2) => + { + l.Add("Invoked"); + return s1.CompareTo(s2, StringComparison.OrdinalIgnoreCase); + }); + + Assert.Equal(1L, connection.ExecuteScalar("SELECT 'Νικοσ' = 'ΝΙΚΟΣ' COLLATE MY_NOCASE;")); + var item = Assert.Single(list); + Assert.Equal("Invoked", item); + } + [Fact] public void CreateFunction_works_when_closed() { @@ -1321,7 +1385,7 @@ public void Open_releases_handle_when_constructor_fails() } else { - // On Unix-like systems, we can still delete the file but cannot + // On Unix-like systems, we can still delete the file but cannot // reliably detect handle leaks this way. File.Delete(dbPath); } From 77899c1ff6a4aab640dea98a551f9fa30a36b01b Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Thu, 12 Jun 2025 12:21:52 +0700 Subject: [PATCH 4/4] correct lint error in docs --- src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs index 8dd897686f3..9916cd11061 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs @@ -558,10 +558,10 @@ public virtual void CreateSpanCollation(string name, T state, SpanDelegateCol /// /// Represents a method signature for a custom collation delegate that compares two read-only spans of characters. /// + /// The type of the state object. /// An optional user-defined state object to be passed to the comparison function. /// The first read-only span of characters to compare. /// The second read-only span of characters to compare. - /// state type /// /// A signed integer indicating the relative order of the strings being compared: /// Less than zero if precedes ;