From 398006d8262996e47709815c0287bbb1e29da98e Mon Sep 17 00:00:00 2001 From: Corey Kaylor Date: Fri, 12 Dec 2025 09:11:18 -0500 Subject: [PATCH 1/3] A couple minor improvements on code style --- src/LightningDB/DatabaseConfiguration.cs | 14 ++------- src/LightningDB/EnvironmentConfiguration.cs | 34 ++++++--------------- src/LightningDB/LightningDB.csproj | 2 +- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/LightningDB/DatabaseConfiguration.cs b/src/LightningDB/DatabaseConfiguration.cs index ca7a3f5..b8b66f5 100644 --- a/src/LightningDB/DatabaseConfiguration.cs +++ b/src/LightningDB/DatabaseConfiguration.cs @@ -37,28 +37,18 @@ internal IDisposable ConfigureDatabase(LightningTransaction tx, LightningDatabas var pinnedComparer = new ComparerKeepAlive(); if (_comparer != null) { - CompareFunction compare = Compare; + CompareFunction compare = (ref left, ref right) => _comparer.Compare(left, right); pinnedComparer.AddComparer(compare); mdb_set_compare(tx._handle, db._handle, compare); } if (_duplicatesComparer == null) return pinnedComparer; - CompareFunction dupCompare = IsDuplicate; + CompareFunction dupCompare = (ref left, ref right) => _duplicatesComparer.Compare(left, right); pinnedComparer.AddComparer(dupCompare); mdb_set_dupsort(tx._handle, db._handle, dupCompare); return pinnedComparer; } - private int Compare(ref MDBValue left, ref MDBValue right) - { - return _comparer.Compare(left, right); - } - - private int IsDuplicate(ref MDBValue left, ref MDBValue right) - { - return _duplicatesComparer.Compare(left, right); - } - /// /// Sets a custom comparer for database operations using the specified comparer. /// diff --git a/src/LightningDB/EnvironmentConfiguration.cs b/src/LightningDB/EnvironmentConfiguration.cs index ae55145..46aead7 100644 --- a/src/LightningDB/EnvironmentConfiguration.cs +++ b/src/LightningDB/EnvironmentConfiguration.cs @@ -10,10 +10,6 @@ /// public class EnvironmentConfiguration { - private long? _mapSize; - private int? _maxReaders; - private int? _maxDatabases; - /// /// Gets or sets the size of the memory map (in bytes) for a LightningEnvironment. /// @@ -27,11 +23,7 @@ public class EnvironmentConfiguration /// Use caution when setting this value in applications running in 32-bit processes, as the effective addressable memory /// space is limited. For such scenarios, auto-adjustments may be applied to ensure compatibility. /// - public long MapSize - { - get => _mapSize ?? 0; - set => _mapSize = value; - } + public long MapSize { get; set; } /// /// Gets or sets the maximum number of reader slots available for a LightningEnvironment instance. @@ -44,11 +36,7 @@ public long MapSize /// property after the environment is already initialized will result in an exception. Adjust this parameter to match /// the requirements of your application's workload and concurrency needs. /// - public int MaxReaders - { - get => _maxReaders ?? 0; - set => _maxReaders = value; - } + public int MaxReaders { get; set; } /// /// Gets or sets the maximum number of databases that can be opened within a LightningEnvironment instance. @@ -63,11 +51,7 @@ public int MaxReaders /// Configuring the appropriate MaxDatabases value is particularly relevant for applications requiring concurrency or /// multiple named databases. /// - public int MaxDatabases - { - get => _maxDatabases ?? 0; - set => _maxDatabases = value; - } + public int MaxDatabases { get; set; } /// /// Gets or sets a value indicating whether the map size should be automatically reduced @@ -86,13 +70,13 @@ public int MaxDatabases internal void Configure(LightningEnvironment env) { - if (_mapSize.HasValue) - env.MapSize = _mapSize.Value; + if (MapSize > 0) + env.MapSize = MapSize; - if (_maxDatabases.HasValue) - env.MaxDatabases = _maxDatabases.Value; + if (MaxDatabases > 0) + env.MaxDatabases = MaxDatabases; - if (_maxReaders.HasValue) - env.MaxReaders = _maxReaders.Value; + if (MaxReaders > 0) + env.MaxReaders = MaxReaders; } } \ No newline at end of file diff --git a/src/LightningDB/LightningDB.csproj b/src/LightningDB/LightningDB.csproj index 26bb92a..44cefc5 100644 --- a/src/LightningDB/LightningDB.csproj +++ b/src/LightningDB/LightningDB.csproj @@ -5,7 +5,7 @@ 0.20.0 Ilya Lukyanov;Corey Kaylor netstandard2.0;net8.0;net9.0;net10.0 - 13 + 14 LightningDB LightningDB lightningdb.png From 7cd373084bf954d48ab985ddb6d945aefdc0a2c9 Mon Sep 17 00:00:00 2001 From: Corey Kaylor Date: Fri, 12 Dec 2025 12:49:54 -0500 Subject: [PATCH 2/3] Added default comparer options for optimized usage --- src/LightningDB.Tests/ComparerTests.cs | 465 ++++++++++++++++++ src/LightningDB/Comparers/BitwiseComparer.cs | 17 + src/LightningDB/Comparers/HashCodeComparer.cs | 69 +++ src/LightningDB/Comparers/LengthComparer.cs | 23 + .../Comparers/LengthOnlyComparer.cs | 17 + .../Comparers/ReverseBitwiseComparer.cs | 17 + .../Comparers/ReverseLengthComparer.cs | 23 + .../Comparers/ReverseSignedIntegerComparer.cs | 30 ++ .../ReverseUnsignedIntegerComparer.cs | 30 ++ .../Comparers/ReverseUtf8StringComparer.cs | 17 + .../Comparers/SignedIntegerComparer.cs | 31 ++ .../Comparers/UnsignedIntegerComparer.cs | 31 ++ .../Comparers/Utf8StringComparer.cs | 17 + 13 files changed, 787 insertions(+) create mode 100644 src/LightningDB.Tests/ComparerTests.cs create mode 100644 src/LightningDB/Comparers/BitwiseComparer.cs create mode 100644 src/LightningDB/Comparers/HashCodeComparer.cs create mode 100644 src/LightningDB/Comparers/LengthComparer.cs create mode 100644 src/LightningDB/Comparers/LengthOnlyComparer.cs create mode 100644 src/LightningDB/Comparers/ReverseBitwiseComparer.cs create mode 100644 src/LightningDB/Comparers/ReverseLengthComparer.cs create mode 100644 src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs create mode 100644 src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs create mode 100644 src/LightningDB/Comparers/ReverseUtf8StringComparer.cs create mode 100644 src/LightningDB/Comparers/SignedIntegerComparer.cs create mode 100644 src/LightningDB/Comparers/UnsignedIntegerComparer.cs create mode 100644 src/LightningDB/Comparers/Utf8StringComparer.cs diff --git a/src/LightningDB.Tests/ComparerTests.cs b/src/LightningDB.Tests/ComparerTests.cs new file mode 100644 index 0000000..821afe2 --- /dev/null +++ b/src/LightningDB.Tests/ComparerTests.cs @@ -0,0 +1,465 @@ +using System; +using System.Runtime.InteropServices; +using LightningDB.Comparers; +using Shouldly; + +namespace LightningDB.Tests; + +public class ComparerTests : TestBase +{ + public void bitwise_comparer_sorts_keys_lexicographically() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(BitwiseComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, new byte[] { 1, 2, 4 }, new byte[] { 1 }); + txn.Put(db, new byte[] { 1, 2, 3 }, new byte[] { 2 }); + txn.Put(db, new byte[] { 1, 2, 5 }, new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 3 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 4 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 5 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void reverse_bitwise_comparer_sorts_keys_in_reverse() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(ReverseBitwiseComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, new byte[] { 1, 2, 4 }, new byte[] { 1 }); + txn.Put(db, new byte[] { 1, 2, 3 }, new byte[] { 2 }); + txn.Put(db, new byte[] { 1, 2, 5 }, new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 5 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 4 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 3 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void signed_integer_comparer_sorts_int32_with_negatives_first() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(SignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50), BitConverter.GetBytes(50)); + txn.Put(db, BitConverter.GetBytes(-10), BitConverter.GetBytes(-10)); + txn.Put(db, BitConverter.GetBytes(100), BitConverter.GetBytes(100)); + txn.Put(db, BitConverter.GetBytes(-50), BitConverter.GetBytes(-50)); + txn.Put(db, BitConverter.GetBytes(0), BitConverter.GetBytes(0)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-50); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void signed_integer_comparer_sorts_int64_with_negatives_first() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(SignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50L), BitConverter.GetBytes(50L)); + txn.Put(db, BitConverter.GetBytes(-10L), BitConverter.GetBytes(-10L)); + txn.Put(db, BitConverter.GetBytes(0L), BitConverter.GetBytes(0L)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10L); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0L); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50L); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void reverse_signed_integer_comparer_sorts_descending() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(ReverseSignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50), BitConverter.GetBytes(50)); + txn.Put(db, BitConverter.GetBytes(-10), BitConverter.GetBytes(-10)); + txn.Put(db, BitConverter.GetBytes(0), BitConverter.GetBytes(0)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(-10); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void unsigned_integer_comparer_sorts_uint32() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(UnsignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50u), BitConverter.GetBytes(50u)); + txn.Put(db, BitConverter.GetBytes(100u), BitConverter.GetBytes(100u)); + txn.Put(db, BitConverter.GetBytes(0u), BitConverter.GetBytes(0u)); + txn.Put(db, BitConverter.GetBytes(uint.MaxValue), BitConverter.GetBytes(uint.MaxValue)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(uint.MaxValue); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void unsigned_integer_comparer_sorts_uint64() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(UnsignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50UL), BitConverter.GetBytes(50UL)); + txn.Put(db, BitConverter.GetBytes(0UL), BitConverter.GetBytes(0UL)); + txn.Put(db, BitConverter.GetBytes(ulong.MaxValue), BitConverter.GetBytes(ulong.MaxValue)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0UL); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50UL); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(ulong.MaxValue); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void reverse_unsigned_integer_comparer_sorts_descending() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(ReverseUnsignedIntegerComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, BitConverter.GetBytes(50u), BitConverter.GetBytes(50u)); + txn.Put(db, BitConverter.GetBytes(100u), BitConverter.GetBytes(100u)); + txn.Put(db, BitConverter.GetBytes(0u), BitConverter.GetBytes(0u)); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(100u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(50u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + MemoryMarshal.Read(cursor.GetCurrent().key.AsSpan()).ShouldBe(0u); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void utf8_string_comparer_sorts_strings() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(Utf8StringComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, "banana"u8.ToArray(), new byte[] { 1 }); + txn.Put(db, "apple"u8.ToArray(), new byte[] { 2 }); + txn.Put(db, "cherry"u8.ToArray(), new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("apple"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("banana"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("cherry"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void reverse_utf8_string_comparer_sorts_descending() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(ReverseUtf8StringComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, "banana"u8.ToArray(), new byte[] { 1 }); + txn.Put(db, "apple"u8.ToArray(), new byte[] { 2 }); + txn.Put(db, "cherry"u8.ToArray(), new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("cherry"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("banana"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + System.Text.Encoding.UTF8.GetString(cursor.GetCurrent().key.AsSpan()).ShouldBe("apple"); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void length_comparer_sorts_by_length_first() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(LengthComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, new byte[] { 255, 255, 255 }, new byte[] { 1 }); + txn.Put(db, new byte[] { 0 }, new byte[] { 2 }); + txn.Put(db, new byte[] { 128, 128 }, new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 0 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 128, 128 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 255, 255, 255 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void reverse_length_comparer_sorts_longer_first() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(ReverseLengthComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, new byte[] { 1, 2, 3 }, new byte[] { 1 }); + txn.Put(db, new byte[] { 1 }, new byte[] { 2 }); + txn.Put(db, new byte[] { 1, 2 }, new byte[] { 3 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 3 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1, 2 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().key.CopyToNewArray().ShouldBe(new byte[] { 1 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void length_only_comparer_treats_same_length_as_equal() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(LengthOnlyComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + txn.Put(db, new byte[] { 1, 2, 3 }, new byte[] { 1 }); + txn.Put(db, new byte[] { 4, 5, 6 }, new byte[] { 2 }); + + using var cursor = txn.CreateCursor(db); + + cursor.Next().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().value.CopyToNewArray().ShouldBe(new byte[] { 2 }); + + cursor.Next().Item1.ShouldBe(MDBResultCode.NotFound); + } + + public void hash_code_comparer_provides_consistent_ordering() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create }; + config.CompareWith(HashCodeComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + var key1 = new byte[] { 1, 2, 3 }; + var key2 = new byte[] { 4, 5, 6 }; + var key3 = new byte[] { 7, 8, 9 }; + + txn.Put(db, key1, key1); + txn.Put(db, key2, key2); + txn.Put(db, key3, key3); + + var order1 = new System.Collections.Generic.List(); + using (var cursor = txn.CreateCursor(db)) + { + while (cursor.Next().Item1 == MDBResultCode.Success) + order1.Add(cursor.GetCurrent().key.CopyToNewArray()); + } + + order1.Count.ShouldBe(3); + + txn.Commit(); + using var txn2 = env.BeginTransaction(TransactionBeginFlags.ReadOnly); + using var db2 = txn2.OpenDatabase(configuration: config); + + var order2 = new System.Collections.Generic.List(); + using (var cursor = txn2.CreateCursor(db2)) + { + while (cursor.Next().Item1 == MDBResultCode.Success) + order2.Add(cursor.GetCurrent().key.CopyToNewArray()); + } + + order1.Count.ShouldBe(order2.Count); + for (var i = 0; i < order1.Count; i++) + order1[i].ShouldBe(order2[i]); + } + + public void reverse_bitwise_comparer_works_with_duplicate_values() + { + using var env = CreateEnvironment(); + env.Open(); + + var config = new DatabaseConfiguration { Flags = DatabaseOpenFlags.Create | DatabaseOpenFlags.DuplicatesSort }; + config.FindDuplicatesWith(ReverseBitwiseComparer.Instance); + + using var txn = env.BeginTransaction(); + using var db = txn.OpenDatabase(configuration: config); + + var key = new byte[] { 1 }; + txn.Put(db, key, new byte[] { 1, 2, 3 }); + txn.Put(db, key, new byte[] { 4, 5, 6 }); + txn.Put(db, key, new byte[] { 2, 3, 4 }); + + using var cursor = txn.CreateCursor(db); + cursor.SetKey(key); + + cursor.GetCurrent().value.CopyToNewArray().ShouldBe(new byte[] { 4, 5, 6 }); + + cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().value.CopyToNewArray().ShouldBe(new byte[] { 2, 3, 4 }); + + cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.Success); + cursor.GetCurrent().value.CopyToNewArray().ShouldBe(new byte[] { 1, 2, 3 }); + + cursor.NextDuplicate().Item1.ShouldBe(MDBResultCode.NotFound); + } +} diff --git a/src/LightningDB/Comparers/BitwiseComparer.cs b/src/LightningDB/Comparers/BitwiseComparer.cs new file mode 100644 index 0000000..97d7d33 --- /dev/null +++ b/src/LightningDB/Comparers/BitwiseComparer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances using lexicographic byte comparison (memcmp-style). +/// +public sealed class BitwiseComparer : IComparer +{ + public static readonly BitwiseComparer Instance = new(); + + private BitwiseComparer() { } + + public int Compare(MDBValue x, MDBValue y) + => x.AsSpan().SequenceCompareTo(y.AsSpan()); +} diff --git a/src/LightningDB/Comparers/HashCodeComparer.cs b/src/LightningDB/Comparers/HashCodeComparer.cs new file mode 100644 index 0000000..a9d50d2 --- /dev/null +++ b/src/LightningDB/Comparers/HashCodeComparer.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +#if !NET6_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances by hash code for faster comparison of large values. +/// Falls back to byte comparison when hashes collide. +/// Sort order is deterministic within a process but may vary across restarts. +/// +public sealed class HashCodeComparer : IComparer +{ + public static readonly HashCodeComparer Instance = new(); + + private HashCodeComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + + var leftHash = ComputeHash(left); + var rightHash = ComputeHash(right); + + var hashCmp = leftHash.CompareTo(rightHash); + return hashCmp != 0 ? hashCmp : left.SequenceCompareTo(right); + } + +#if NET6_0_OR_GREATER + private static int ComputeHash(ReadOnlySpan data) + { + var hc = new HashCode(); + hc.AddBytes(data); + return hc.ToHashCode(); + } +#else + private static ulong ComputeHash(ReadOnlySpan data) + { + const ulong prime = 0x9E3779B97F4A7C15UL; + var hash = (ulong)data.Length; + + while (data.Length >= 8) + { + hash ^= MemoryMarshal.Read(data); + hash *= prime; + hash ^= hash >> 32; + data = data.Slice(8); + } + + if (data.Length >= 4) + { + hash ^= MemoryMarshal.Read(data); + hash *= prime; + data = data.Slice(4); + } + + foreach (var b in data) + { + hash ^= b; + hash *= prime; + } + + return hash; + } +#endif +} diff --git a/src/LightningDB/Comparers/LengthComparer.cs b/src/LightningDB/Comparers/LengthComparer.cs new file mode 100644 index 0000000..f570478 --- /dev/null +++ b/src/LightningDB/Comparers/LengthComparer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances by length first, then by content. +/// Shorter values sort before longer values. +/// +public sealed class LengthComparer : IComparer +{ + public static readonly LengthComparer Instance = new(); + + private LengthComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + var lengthCmp = left.Length.CompareTo(right.Length); + return lengthCmp != 0 ? lengthCmp : left.SequenceCompareTo(right); + } +} diff --git a/src/LightningDB/Comparers/LengthOnlyComparer.cs b/src/LightningDB/Comparers/LengthOnlyComparer.cs new file mode 100644 index 0000000..b217a0f --- /dev/null +++ b/src/LightningDB/Comparers/LengthOnlyComparer.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances by length only, ignoring content. +/// Values of equal length are considered equal regardless of content. +/// +public sealed class LengthOnlyComparer : IComparer +{ + public static readonly LengthOnlyComparer Instance = new(); + + private LengthOnlyComparer() { } + + public int Compare(MDBValue x, MDBValue y) + => x.AsSpan().Length.CompareTo(y.AsSpan().Length); +} diff --git a/src/LightningDB/Comparers/ReverseBitwiseComparer.cs b/src/LightningDB/Comparers/ReverseBitwiseComparer.cs new file mode 100644 index 0000000..63d4cbc --- /dev/null +++ b/src/LightningDB/Comparers/ReverseBitwiseComparer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances using lexicographic byte comparison in descending order. +/// +public sealed class ReverseBitwiseComparer : IComparer +{ + public static readonly ReverseBitwiseComparer Instance = new(); + + private ReverseBitwiseComparer() { } + + public int Compare(MDBValue x, MDBValue y) + => y.AsSpan().SequenceCompareTo(x.AsSpan()); +} diff --git a/src/LightningDB/Comparers/ReverseLengthComparer.cs b/src/LightningDB/Comparers/ReverseLengthComparer.cs new file mode 100644 index 0000000..b525d0e --- /dev/null +++ b/src/LightningDB/Comparers/ReverseLengthComparer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances by length first (descending), then by content (descending). +/// Longer values sort before shorter values. +/// +public sealed class ReverseLengthComparer : IComparer +{ + public static readonly ReverseLengthComparer Instance = new(); + + private ReverseLengthComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + var lengthCmp = right.Length.CompareTo(left.Length); + return lengthCmp != 0 ? lengthCmp : right.SequenceCompareTo(left); + } +} diff --git a/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs b/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs new file mode 100644 index 0000000..df1875f --- /dev/null +++ b/src/LightningDB/Comparers/ReverseSignedIntegerComparer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as signed integers (int or long) in descending order. +/// Supports 4-byte and 8-byte values. Falls back to reverse bitwise comparison for other sizes. +/// +public sealed class ReverseSignedIntegerComparer : IComparer +{ + public static readonly ReverseSignedIntegerComparer Instance = new(); + + private ReverseSignedIntegerComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + + if (left.Length == 4 && right.Length == 4) + return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left)); + + if (left.Length == 8 && right.Length == 8) + return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left)); + + return right.SequenceCompareTo(left); + } +} diff --git a/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs b/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs new file mode 100644 index 0000000..755db65 --- /dev/null +++ b/src/LightningDB/Comparers/ReverseUnsignedIntegerComparer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as unsigned integers (uint or ulong) in descending order. +/// Supports 4-byte and 8-byte values. Falls back to reverse bitwise comparison for other sizes. +/// +public sealed class ReverseUnsignedIntegerComparer : IComparer +{ + public static readonly ReverseUnsignedIntegerComparer Instance = new(); + + private ReverseUnsignedIntegerComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + + if (left.Length == 4 && right.Length == 4) + return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left)); + + if (left.Length == 8 && right.Length == 8) + return MemoryMarshal.Read(right).CompareTo(MemoryMarshal.Read(left)); + + return right.SequenceCompareTo(left); + } +} diff --git a/src/LightningDB/Comparers/ReverseUtf8StringComparer.cs b/src/LightningDB/Comparers/ReverseUtf8StringComparer.cs new file mode 100644 index 0000000..dca5bb6 --- /dev/null +++ b/src/LightningDB/Comparers/ReverseUtf8StringComparer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as UTF-8 encoded strings in descending order. +/// +public sealed class ReverseUtf8StringComparer : IComparer +{ + public static readonly ReverseUtf8StringComparer Instance = new(); + + private ReverseUtf8StringComparer() { } + + public int Compare(MDBValue x, MDBValue y) + => y.AsSpan().SequenceCompareTo(x.AsSpan()); +} diff --git a/src/LightningDB/Comparers/SignedIntegerComparer.cs b/src/LightningDB/Comparers/SignedIntegerComparer.cs new file mode 100644 index 0000000..2dcc74d --- /dev/null +++ b/src/LightningDB/Comparers/SignedIntegerComparer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as signed integers (int or long). +/// Supports 4-byte and 8-byte values. Negative values sort before positive values. +/// Falls back to bitwise comparison for other sizes. +/// +public sealed class SignedIntegerComparer : IComparer +{ + public static readonly SignedIntegerComparer Instance = new(); + + private SignedIntegerComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + + if (left.Length == 4 && right.Length == 4) + return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right)); + + if (left.Length == 8 && right.Length == 8) + return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right)); + + return left.SequenceCompareTo(right); + } +} diff --git a/src/LightningDB/Comparers/UnsignedIntegerComparer.cs b/src/LightningDB/Comparers/UnsignedIntegerComparer.cs new file mode 100644 index 0000000..546d5ee --- /dev/null +++ b/src/LightningDB/Comparers/UnsignedIntegerComparer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as unsigned integers (uint or ulong). +/// Supports 4-byte and 8-byte values. Matches LMDB's MDB_INTEGERKEY behavior. +/// Falls back to bitwise comparison for other sizes. +/// +public sealed class UnsignedIntegerComparer : IComparer +{ + public static readonly UnsignedIntegerComparer Instance = new(); + + private UnsignedIntegerComparer() { } + + public int Compare(MDBValue x, MDBValue y) + { + var left = x.AsSpan(); + var right = y.AsSpan(); + + if (left.Length == 4 && right.Length == 4) + return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right)); + + if (left.Length == 8 && right.Length == 8) + return MemoryMarshal.Read(left).CompareTo(MemoryMarshal.Read(right)); + + return left.SequenceCompareTo(right); + } +} diff --git a/src/LightningDB/Comparers/Utf8StringComparer.cs b/src/LightningDB/Comparers/Utf8StringComparer.cs new file mode 100644 index 0000000..84469ef --- /dev/null +++ b/src/LightningDB/Comparers/Utf8StringComparer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace LightningDB.Comparers; + +/// +/// Compares MDBValue instances as UTF-8 encoded strings using ordinal comparison. +/// +public sealed class Utf8StringComparer : IComparer +{ + public static readonly Utf8StringComparer Instance = new(); + + private Utf8StringComparer() { } + + public int Compare(MDBValue x, MDBValue y) + => x.AsSpan().SequenceCompareTo(y.AsSpan()); +} From 7d0913eb337295c6722d983eb2fe3a63e9c06931 Mon Sep 17 00:00:00 2001 From: Corey Kaylor Date: Fri, 12 Dec 2025 12:56:37 -0500 Subject: [PATCH 3/3] Updated readme for Comparers --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 0845de6..c3adb97 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,40 @@ In this example: - Using a cursor, we iterate over all values associated with the key "fruit" by moving to the next duplicate entry and see the values retrieved are ordered. - Then we demonstrate doing the same thing with IEnumerable instead. +## Custom Key Ordering + +LightningDB provides built-in, allocation-free comparers for custom key sorting and duplicate ordering. Use them with `CompareWith()` for keys or `FindDuplicatesWith()` for duplicate values: + +```csharp +var config = new DatabaseConfiguration +{ + Flags = DatabaseOpenFlags.Create | DatabaseOpenFlags.DuplicatesSort +}; + +// Sort keys as signed integers (negative values sort before positive) +config.CompareWith(SignedIntegerComparer.Instance); + +// Sort duplicate values in reverse order +config.FindDuplicatesWith(ReverseBitwiseComparer.Instance); + +using var db = tx.OpenDatabase(configuration: config); +``` + +**Available comparers in `LightningDB.Comparers`:** + +| Comparer | Description | +|----------|-------------| +| `BitwiseComparer` | Lexicographic byte comparison (default LMDB behavior) | +| `ReverseBitwiseComparer` | Lexicographic descending | +| `SignedIntegerComparer` | 4/8-byte signed integers with proper negative ordering | +| `UnsignedIntegerComparer` | 4/8-byte unsigned integers | +| `Utf8StringComparer` | Ordinal UTF-8 string comparison | +| `LengthComparer` | Sort by length first, then content | +| `LengthOnlyComparer` | Sort by length only | +| `HashCodeComparer` | Hash-based comparison for large values | + +Reverse variants are available for most comparers (e.g., `ReverseSignedIntegerComparer`). + ## Additional Resources For more detailed examples and advanced usage, refer to the unit tests in the [Lightning.NET](https://github.com/CoreyKaylor/Lightning.NET) repository.