diff --git a/README.md b/README.md index ba1585e..38efd41 100644 --- a/README.md +++ b/README.md @@ -155,26 +155,26 @@ The library includes specialized support for collections when working with: 1. Protocol Buffer message types 2. Primitive types (int, long, string, etc.) -When using `IList` with these types, the library automatically handles serialization/deserialization without requiring wrapper message types. This is particularly useful for Protocol Buffers, where `RepeatedField` typically cannot be serialized as a standalone entity. +When using `IList` or `ISet` with these types, the library automatically handles serialization/deserialization without requiring wrapper message types. This is particularly useful for Protocol Buffers, where `RepeatedField` typically cannot be serialized as a standalone entity. The serialization format varies depending on the element type: #### Fixed-Size Types (int, long, etc.) ``` -[4 bytes: List length][Contiguous array of serialized elements] +[4 bytes: Collection length][Contiguous array of serialized elements] ``` #### Variable-Size Types (string, protobuf messages) ``` -[4 bytes: List length][For each element: [4 bytes: Element size][N bytes: Element data]] +[4 bytes: Collection length][For each element: [4 bytes: Element size][N bytes: Element data]] ``` Example types that work automatically with this support: -- Protocol Buffer message types: `IList` -- Primitive types: `IList`, `IList`, `IList`, etc. +- Protocol Buffer message types: `IList`, `ISet` +- Primitive types: `IList`, `IList`, `IList`, `ISet`, `ISet`, etc. ## Merge Operators diff --git a/src/RocksDb.Extensions/FixedSizeCollectionSerializer.cs b/src/RocksDb.Extensions/FixedSizeCollectionSerializer.cs new file mode 100644 index 0000000..befac00 --- /dev/null +++ b/src/RocksDb.Extensions/FixedSizeCollectionSerializer.cs @@ -0,0 +1,109 @@ +using System.Buffers; + +namespace RocksDb.Extensions; + +/// +/// Base class for serializing collections of fixed-size elements like primitive types (int, long, etc.) +/// where each element occupies the same number of bytes in memory. +/// +/// +/// The serialized format consists of: +/// - 4 bytes: Collection length (number of elements) +/// - Remaining bytes: Contiguous array of serialized elements +/// +/// The collection type (e.g., IList{T}, ISet{T}) +/// The element type +internal abstract class FixedSizeCollectionSerializer : ISerializer + where TCollection : ICollection +{ + private readonly ISerializer _scalarSerializer; + + protected FixedSizeCollectionSerializer(ISerializer scalarSerializer) + { + _scalarSerializer = scalarSerializer; + } + + /// + /// Creates a new collection instance with the specified capacity. + /// + protected abstract TCollection CreateCollection(int capacity); + + /// + /// Adds an element to the collection. + /// + protected abstract void AddElement(TCollection collection, TElement element); + + public bool TryCalculateSize(ref TCollection value, out int size) + { + size = sizeof(int); // size of the collection + if (value.Count == 0) + { + return true; + } + + var referentialElement = value.First(); + if (_scalarSerializer.TryCalculateSize(ref referentialElement, out var elementSize)) + { + size += value.Count * elementSize; + return true; + } + + return false; + } + + public void WriteTo(ref TCollection value, ref Span span) + { + // Write the size of the collection + var slice = span.Slice(0, sizeof(int)); + BitConverter.TryWriteBytes(slice, value.Count); + + if (value.Count == 0) + { + return; + } + + // Write the elements of the collection + int offset = sizeof(int); + var elementSize = (span.Length - offset) / value.Count; + foreach (var item in value) + { + var element = item; + slice = span.Slice(offset, elementSize); + _scalarSerializer.WriteTo(ref element, ref slice); + offset += elementSize; + } + } + + public void WriteTo(ref TCollection value, IBufferWriter buffer) + { + throw new NotImplementedException(); + } + + public TCollection Deserialize(ReadOnlySpan buffer) + { + // Read the size of the collection + var slice = buffer.Slice(0, sizeof(int)); + var size = BitConverter.ToInt32(slice); + + var collection = CreateCollection(size); + + if (size == 0) + { + return collection; + } + + // Read the elements of the collection + int offset = sizeof(int); + var elementSize = (buffer.Length - offset) / size; + for (int i = 0; i < size; i++) + { + slice = buffer.Slice(offset, elementSize); + var element = _scalarSerializer.Deserialize(slice); + AddElement(collection, element); + offset += elementSize; + } + + return collection; + } +} + diff --git a/src/RocksDb.Extensions/FixedSizeListSerializer.cs b/src/RocksDb.Extensions/FixedSizeListSerializer.cs index 48c9a45..1bc6d20 100644 --- a/src/RocksDb.Extensions/FixedSizeListSerializer.cs +++ b/src/RocksDb.Extensions/FixedSizeListSerializer.cs @@ -1,87 +1,12 @@ -using System.Buffers; - namespace RocksDb.Extensions; -/// -/// Serializes lists of fixed-size elements like primitive types (int, long, etc.) where each element -/// occupies the same number of bytes in memory. This implementation optimizes for performance by -/// pre-calculating buffer sizes based on element count. -/// -/// -/// Use this serializer when working with lists of primitive types or structs where all elements -/// have identical size. The serialized format consists of: -/// - 4 bytes: List length (number of elements) -/// - Remaining bytes: Contiguous array of serialized elements -/// -internal class FixedSizeListSerializer : ISerializer> +internal class FixedSizeListSerializer : FixedSizeCollectionSerializer, T> { - private readonly ISerializer _scalarSerializer; - - public FixedSizeListSerializer(ISerializer scalarSerializer) - { - _scalarSerializer = scalarSerializer; - } - - public bool TryCalculateSize(ref IList value, out int size) + public FixedSizeListSerializer(ISerializer scalarSerializer) : base(scalarSerializer) { - size = sizeof(int); // size of the list - if (value.Count == 0) - { - return true; - } - - var referentialElement = value[0]; - if (_scalarSerializer.TryCalculateSize(ref referentialElement, out var elementSize)) - { - size += value.Count * elementSize; - return true; - } - - return false; } - public void WriteTo(ref IList value, ref Span span) - { - // Write the size of the list - var slice = span.Slice(0, sizeof(int)); - BitConverter.TryWriteBytes(slice, value.Count); - - // Write the elements of the list - int offset = sizeof(int); - var elementSize = (span.Length - offset) / value.Count; - for (int i = 0; i < value.Count; i++) - { - var element = value[i]; - slice = span.Slice(offset, elementSize); - _scalarSerializer.WriteTo(ref element, ref slice); - offset += elementSize; - } - } + protected override IList CreateCollection(int capacity) => new List(capacity); - public void WriteTo(ref IList value, IBufferWriter buffer) - { - throw new NotImplementedException(); - } - - public IList Deserialize(ReadOnlySpan buffer) - { - // Read the size of the list - var slice = buffer.Slice(0, sizeof(int)); - var size = BitConverter.ToInt32(slice); - - var list = new List(size); - - // Read the elements of the list - int offset = sizeof(int); - var elementSize = (buffer.Length - offset) / size; - for (int i = 0; i < size; i++) - { - slice = buffer.Slice(offset, elementSize); - var element = _scalarSerializer.Deserialize(slice); - list.Add(element); - offset += elementSize; - } - - return list; - } + protected override void AddElement(IList collection, T element) => collection.Add(element); } \ No newline at end of file diff --git a/src/RocksDb.Extensions/FixedSizeSetSerializer.cs b/src/RocksDb.Extensions/FixedSizeSetSerializer.cs new file mode 100644 index 0000000..c6a4a15 --- /dev/null +++ b/src/RocksDb.Extensions/FixedSizeSetSerializer.cs @@ -0,0 +1,12 @@ +namespace RocksDb.Extensions; + +internal class FixedSizeSetSerializer : FixedSizeCollectionSerializer, T> +{ + public FixedSizeSetSerializer(ISerializer scalarSerializer) : base(scalarSerializer) + { + } + + protected override ISet CreateCollection(int capacity) => new HashSet(capacity); + + protected override void AddElement(ISet collection, T element) => collection.Add(element); +} diff --git a/src/RocksDb.Extensions/RocksDbBuilder.cs b/src/RocksDb.Extensions/RocksDbBuilder.cs index 9c88392..1ecdb72 100644 --- a/src/RocksDb.Extensions/RocksDbBuilder.cs +++ b/src/RocksDb.Extensions/RocksDbBuilder.cs @@ -124,6 +124,26 @@ private static ISerializer CreateSerializer(IReadOnlyList) Activator.CreateInstance(typeof(VariableSizeListSerializer<>).MakeGenericType(elementType), scalarSerializer)!; } + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ISet<>)) + { + var elementType = type.GetGenericArguments()[0]; + + // Use reflection to call CreateSerializer method with generic type argument + // This is equivalent to calling CreateSerializer(serializerFactories) + var scalarSerializer = typeof(RocksDbBuilder).GetMethod(nameof(CreateSerializer), BindingFlags.NonPublic | BindingFlags.Static) + ?.MakeGenericMethod(elementType) + .Invoke(null, new object[] { serializerFactories }); + + if (elementType.IsPrimitive) + { + // Use fixed size set serializer for primitive types + return (ISerializer) Activator.CreateInstance(typeof(FixedSizeSetSerializer<>).MakeGenericType(elementType), scalarSerializer)!; + } + + // Use variable size set serializer for non-primitive types + return (ISerializer) Activator.CreateInstance(typeof(VariableSizeSetSerializer<>).MakeGenericType(elementType), scalarSerializer)!; + } + // Handle CollectionOperation for the ListMergeOperator if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(MergeOperators.CollectionOperation<>)) { diff --git a/src/RocksDb.Extensions/VariableSizeCollectionSerializer.cs b/src/RocksDb.Extensions/VariableSizeCollectionSerializer.cs new file mode 100644 index 0000000..7a38809 --- /dev/null +++ b/src/RocksDb.Extensions/VariableSizeCollectionSerializer.cs @@ -0,0 +1,119 @@ +using System.Buffers; + +namespace RocksDb.Extensions; + +/// +/// Base class for serializing collections containing variable-size elements like strings or complex objects +/// where each element may occupy a different number of bytes when serialized. +/// +/// +/// The serialized format consists of: +/// - 4 bytes: Collection length (number of elements) +/// - For each element: +/// - 4 bytes: Size of the serialized element +/// - N bytes: Serialized element data +/// +/// The collection type (e.g., IList{T}, ISet{T}) +/// The element type +internal abstract class VariableSizeCollectionSerializer : ISerializer + where TCollection : ICollection +{ + private readonly ISerializer _scalarSerializer; + + protected VariableSizeCollectionSerializer(ISerializer scalarSerializer) + { + _scalarSerializer = scalarSerializer; + } + + /// + /// Creates a new collection instance with the specified capacity. + /// + protected abstract TCollection CreateCollection(int capacity); + + /// + /// Adds an element to the collection. + /// + protected abstract void AddElement(TCollection collection, TElement element); + + public bool TryCalculateSize(ref TCollection value, out int size) + { + size = sizeof(int); // size of the collection + if (value.Count == 0) + { + return true; + } + + foreach (var item in value) + { + var element = item; + if (_scalarSerializer.TryCalculateSize(ref element, out var elementSize)) + { + size += sizeof(int); + size += elementSize; + } + else + { + // Element serializer can't calculate size, so we can't either + size = 0; + return false; + } + } + + return true; + } + + public void WriteTo(ref TCollection value, ref Span span) + { + // Write the size of the collection + var slice = span.Slice(0, sizeof(int)); + BitConverter.TryWriteBytes(slice, value.Count); + + // Write the elements of the collection + int offset = sizeof(int); + foreach (var item in value) + { + var element = item; + if (_scalarSerializer.TryCalculateSize(ref element, out var elementSize)) + { + slice = span.Slice(offset, sizeof(int)); + BitConverter.TryWriteBytes(slice, elementSize); + offset += sizeof(int); + + slice = span.Slice(offset, elementSize); + _scalarSerializer.WriteTo(ref element, ref slice); + offset += elementSize; + } + } + } + + public void WriteTo(ref TCollection value, IBufferWriter buffer) + { + throw new NotImplementedException(); + } + + public TCollection Deserialize(ReadOnlySpan buffer) + { + // Read the size of the collection + var slice = buffer.Slice(0, sizeof(int)); + var size = BitConverter.ToInt32(slice); + + var collection = CreateCollection(size); + + // Read the elements of the collection + int offset = sizeof(int); + for (int i = 0; i < size; i++) + { + slice = buffer.Slice(offset, sizeof(int)); + var elementSize = BitConverter.ToInt32(slice); + offset += sizeof(int); + + slice = buffer.Slice(offset, elementSize); + var element = _scalarSerializer.Deserialize(slice); + AddElement(collection, element); + offset += elementSize; + } + + return collection; + } +} + diff --git a/src/RocksDb.Extensions/VariableSizeListSerializer.cs b/src/RocksDb.Extensions/VariableSizeListSerializer.cs index 0c6e47b..101bbe2 100644 --- a/src/RocksDb.Extensions/VariableSizeListSerializer.cs +++ b/src/RocksDb.Extensions/VariableSizeListSerializer.cs @@ -1,106 +1,12 @@ -using System.Buffers; - namespace RocksDb.Extensions; -/// -/// Serializes lists containing variable-size elements like strings or complex objects where each element -/// may occupy a different number of bytes when serialized. -/// -/// -/// Use this serializer for lists containing elements that may have different sizes (strings, nested objects, etc.). -/// The serialized format consists of: -/// - 4 bytes: List length (number of elements) -/// - For each element: -/// - 4 bytes: Size of the serialized element -/// - N bytes: Serialized element data -/// -internal class VariableSizeListSerializer : ISerializer> +internal class VariableSizeListSerializer : VariableSizeCollectionSerializer, T> { - private readonly ISerializer _scalarSerializer; - - public VariableSizeListSerializer(ISerializer scalarSerializer) - { - _scalarSerializer = scalarSerializer; - } - - public bool TryCalculateSize(ref IList value, out int size) + public VariableSizeListSerializer(ISerializer scalarSerializer) : base(scalarSerializer) { - size = sizeof(int); // size of the list - if (value.Count == 0) - { - return true; - } - - for (int i = 0; i < value.Count; i++) - { - var element = value[i]; - if (_scalarSerializer.TryCalculateSize(ref element, out var elementSize)) - { - size += sizeof(int); - size += elementSize; - } - else - { - // Element serializer can't calculate size, so we can't either - size = 0; - return false; - } - } - - return true; } - public void WriteTo(ref IList value, ref Span span) - { - // Write the size of the list - var slice = span.Slice(0, sizeof(int)); - BitConverter.TryWriteBytes(slice, value.Count); - - // Write the elements of the list - int offset = sizeof(int); - for (int i = 0; i < value.Count; i++) - { - var element = value[i]; - if (_scalarSerializer.TryCalculateSize(ref element, out var elementSize)) - { - slice = span.Slice(offset, sizeof(int)); - BitConverter.TryWriteBytes(slice, elementSize); - offset += sizeof(int); - - slice = span.Slice(offset, elementSize); - _scalarSerializer.WriteTo(ref element, ref slice); - offset += elementSize; - } - } - } + protected override IList CreateCollection(int capacity) => new List(capacity); - public void WriteTo(ref IList value, IBufferWriter buffer) - { - throw new NotImplementedException(); - } - - public IList Deserialize(ReadOnlySpan buffer) - { - // Read the size of the list - var slice = buffer.Slice(0, sizeof(int)); - var size = BitConverter.ToInt32(slice); - - var list = new List(size); - - // Read the elements of the list - int offset = sizeof(int); - for (int i = 0; i < size; i++) - { - slice = buffer.Slice(offset, sizeof(int)); - var elementSize = BitConverter.ToInt32(slice); - offset += sizeof(int); - - slice = buffer.Slice(offset, elementSize); - var element = _scalarSerializer.Deserialize(slice); - list.Add(element); - offset += elementSize; - } - - return list; - } + protected override void AddElement(IList collection, T element) => collection.Add(element); } \ No newline at end of file diff --git a/src/RocksDb.Extensions/VariableSizeSetSerializer.cs b/src/RocksDb.Extensions/VariableSizeSetSerializer.cs new file mode 100644 index 0000000..4f480c6 --- /dev/null +++ b/src/RocksDb.Extensions/VariableSizeSetSerializer.cs @@ -0,0 +1,12 @@ +namespace RocksDb.Extensions; + +internal class VariableSizeSetSerializer : VariableSizeCollectionSerializer, T> +{ + public VariableSizeSetSerializer(ISerializer scalarSerializer) : base(scalarSerializer) + { + } + + protected override ISet CreateCollection(int capacity) => new HashSet(capacity); + + protected override void AddElement(ISet collection, T element) => collection.Add(element); +} diff --git a/test/RocksDb.Extensions.Tests/RocksDbStoreWithJsonSerializerTests.cs b/test/RocksDb.Extensions.Tests/RocksDbStoreWithJsonSerializerTests.cs index 6f593e3..2a2e9ac 100644 --- a/test/RocksDb.Extensions.Tests/RocksDbStoreWithJsonSerializerTests.cs +++ b/test/RocksDb.Extensions.Tests/RocksDbStoreWithJsonSerializerTests.cs @@ -139,6 +139,115 @@ public void should_put_and_retrieve_data_with_lists_from_store() value.ShouldBeEquivalentTo(cacheValue); } + [Test] + public void should_put_and_retrieve_data_with_sets_from_store() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var cacheKey = Enumerable.Range(0, 100) + .Select(x => new ProtoNetCacheKey + { + Id = x, + }) + .ToHashSet(); + + var cacheValue = Enumerable.Range(0, 100) + .Select(x => new ProtoNetCacheValue + { + Id = x, + Value = $"value-{x}", + }) + .ToHashSet(); + + store.Put(cacheKey, cacheValue); + + store.HasKey(cacheKey).ShouldBeTrue(); + store.TryGet(cacheKey, out var value).ShouldBeTrue(); + value.ShouldBeEquivalentTo(cacheValue); + } + + [Test] + public void should_put_and_retrieve_empty_set_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyCacheKey = new HashSet(); + var emptyCacheValue = new HashSet(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_put_and_retrieve_empty_list_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, IList>(); + var store = testFixture.GetStore, IList>>(); + + // Act + var emptyCacheKey = new List(); + var emptyCacheValue = new List(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_handle_multiple_empty_sets_with_different_keys() + { + // Arrange + using var testFixture = CreateTestFixture>(); + var store = testFixture.GetStore>>(); + + // Act + var emptySet1 = new HashSet(); + var emptySet2 = new HashSet(); + var emptySet3 = new HashSet(); + + var key1 = new ProtoNetCacheKey { Id = 1 }; + var key2 = new ProtoNetCacheKey { Id = 2 }; + var key3 = new ProtoNetCacheKey { Id = 3 }; + + store.Put(key1, emptySet1); + store.Put(key2, emptySet2); + store.Put(key3, emptySet3); + + // Assert + store.HasKey(key1).ShouldBeTrue(); + store.TryGet(key1, out var value1).ShouldBeTrue(); + value1.ShouldNotBeNull(); + value1.Count.ShouldBe(0); + + store.HasKey(key2).ShouldBeTrue(); + store.TryGet(key2, out var value2).ShouldBeTrue(); + value2.ShouldNotBeNull(); + value2.Count.ShouldBe(0); + + store.HasKey(key3).ShouldBeTrue(); + store.TryGet(key3, out var value3).ShouldBeTrue(); + value3.ShouldNotBeNull(); + value3.Count.ShouldBe(0); + } + private static TestFixture CreateTestFixture() { var testFixture = TestFixture.Create(rockDb => diff --git a/test/RocksDb.Extensions.Tests/RocksDbStoreWithPrimitiveSerializerTests.cs b/test/RocksDb.Extensions.Tests/RocksDbStoreWithPrimitiveSerializerTests.cs index daafc59..584d8f9 100644 --- a/test/RocksDb.Extensions.Tests/RocksDbStoreWithPrimitiveSerializerTests.cs +++ b/test/RocksDb.Extensions.Tests/RocksDbStoreWithPrimitiveSerializerTests.cs @@ -299,6 +299,213 @@ public void should_put_and_retrieve_data_from_store_using_list_of_strings() retrievedValue.ShouldBe(value); } + [Test] + public void should_put_and_retrieve_data_from_store_using_set_of_ints() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var key = new HashSet { 1, 2, 3 }; + var value = new HashSet { 4, 5, 6 }; + store.Put(key, value); + + // Assert + store.HasKey(key).ShouldBeTrue(); + store.TryGet(key, out var cacheValue).ShouldBeTrue(); + cacheValue.ShouldBeEquivalentTo(value); + } + + [Test] + public void should_put_and_retrieve_data_from_store_using_set_of_strings() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + var key = new HashSet { "key1", "key2", string.Empty, "key3" }; + var value = new HashSet { "value1", string.Empty, "value2", "value3" }; + + // Act + store.Put(key, value); + + // Assert + store.HasKey(key).ShouldBeTrue(); + store.TryGet(key, out var retrievedValue).ShouldBeTrue(); + retrievedValue.ShouldBeEquivalentTo(value); + } + + [Test] + public void should_put_and_retrieve_empty_set_with_int_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyIntSetKey = new HashSet(); + var emptyIntSetValue = new HashSet(); + + store.Put(emptyIntSetKey, emptyIntSetValue); + + // Assert + store.HasKey(emptyIntSetKey).ShouldBeTrue(); + store.TryGet(emptyIntSetKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyIntSetValue); + } + + [Test] + public void should_put_and_retrieve_empty_set_with_string_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyStringSetKey = new HashSet(); + var emptyStringSetValue = new HashSet(); + + store.Put(emptyStringSetKey, emptyStringSetValue); + + // Assert + store.HasKey(emptyStringSetKey).ShouldBeTrue(); + store.TryGet(emptyStringSetKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyStringSetValue); + } + + [Test] + public void should_put_and_retrieve_empty_set_with_long_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyLongSetKey = new HashSet(); + var emptyLongSetValue = new HashSet(); + + store.Put(emptyLongSetKey, emptyLongSetValue); + + // Assert + store.HasKey(emptyLongSetKey).ShouldBeTrue(); + store.TryGet(emptyLongSetKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyLongSetValue); + } + + [Test] + public void should_put_and_retrieve_empty_list_with_int_types() + { + // Arrange + using var testFixture = CreateTestFixture, IList>(); + var store = testFixture.GetStore, IList>>(); + + // Act + var emptyIntListKey = new List(); + var emptyIntListValue = new List(); + + store.Put(emptyIntListKey, emptyIntListValue); + + // Assert + store.HasKey(emptyIntListKey).ShouldBeTrue(); + store.TryGet(emptyIntListKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyIntListValue); + } + + [Test] + public void should_put_and_retrieve_empty_list_with_string_types() + { + // Arrange + using var testFixture = CreateTestFixture, IList>(); + var store = testFixture.GetStore, IList>>(); + + // Act + var emptyStringListKey = new List(); + var emptyStringListValue = new List(); + + store.Put(emptyStringListKey, emptyStringListValue); + + // Assert + store.HasKey(emptyStringListKey).ShouldBeTrue(); + store.TryGet(emptyStringListKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyStringListValue); + } + + [Test] + public void should_handle_multiple_empty_sets_with_different_int_keys() + { + // Arrange + using var testFixture = CreateTestFixture>(); + var store = testFixture.GetStore>>(); + + // Act + var emptySet1 = new HashSet(); + var emptySet2 = new HashSet(); + var emptySet3 = new HashSet(); + + store.Put(1, emptySet1); + store.Put(2, emptySet2); + store.Put(3, emptySet3); + + // Assert + store.HasKey(1).ShouldBeTrue(); + store.TryGet(1, out var value1).ShouldBeTrue(); + value1.ShouldNotBeNull(); + value1.Count.ShouldBe(0); + + store.HasKey(2).ShouldBeTrue(); + store.TryGet(2, out var value2).ShouldBeTrue(); + value2.ShouldNotBeNull(); + value2.Count.ShouldBe(0); + + store.HasKey(3).ShouldBeTrue(); + store.TryGet(3, out var value3).ShouldBeTrue(); + value3.ShouldNotBeNull(); + value3.Count.ShouldBe(0); + } + + [Test] + public void should_handle_multiple_empty_sets_with_different_string_keys() + { + // Arrange + using var testFixture = CreateTestFixture>(); + var store = testFixture.GetStore>>(); + + // Act + var emptySet1 = new HashSet(); + var emptySet2 = new HashSet(); + var emptySet3 = new HashSet(); + + store.Put("key1", emptySet1); + store.Put("key2", emptySet2); + store.Put("key3", emptySet3); + + // Assert + store.HasKey("key1").ShouldBeTrue(); + store.TryGet("key1", out var value1).ShouldBeTrue(); + value1.ShouldNotBeNull(); + value1.Count.ShouldBe(0); + + store.HasKey("key2").ShouldBeTrue(); + store.TryGet("key2", out var value2).ShouldBeTrue(); + value2.ShouldNotBeNull(); + value2.Count.ShouldBe(0); + + store.HasKey("key3").ShouldBeTrue(); + store.TryGet("key3", out var value3).ShouldBeTrue(); + value3.ShouldNotBeNull(); + value3.Count.ShouldBe(0); + } + private static TestFixture CreateTestFixture() { var testFixture = TestFixture.Create(rockDb => diff --git a/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtoBufNetSerializerTests.cs b/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtoBufNetSerializerTests.cs index 09c5f82..65cf477 100644 --- a/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtoBufNetSerializerTests.cs +++ b/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtoBufNetSerializerTests.cs @@ -140,6 +140,36 @@ public void should_put_and_retrieve_data_with_lists_from_store() value.ShouldBeEquivalentTo(cacheValue); } + [Test] + public void should_put_and_retrieve_data_with_sets_from_store() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var cacheKey = Enumerable.Range(0, 100) + .Select(x => new ProtoNetCacheKey + { + Id = x, + }) + .ToHashSet(); + + var cacheValue = Enumerable.Range(0, 100) + .Select(x => new ProtoNetCacheValue + { + Id = x, + Value = $"value-{x}", + }) + .ToHashSet(); + + store.Put(cacheKey, cacheValue); + + store.HasKey(cacheKey).ShouldBeTrue(); + store.TryGet(cacheKey, out var value).ShouldBeTrue(); + value.ShouldBeEquivalentTo(cacheValue); + } + [Test] public void should_return_all_keys_from_store() { @@ -186,6 +216,85 @@ public void should_return_all_values_from_store() } } + [Test] + public void should_put_and_retrieve_empty_set_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyCacheKey = new HashSet(); + var emptyCacheValue = new HashSet(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_put_and_retrieve_empty_list_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, IList>(); + var store = testFixture.GetStore, IList>>(); + + // Act + var emptyCacheKey = new List(); + var emptyCacheValue = new List(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_handle_multiple_empty_sets_with_different_keys() + { + // Arrange + using var testFixture = CreateTestFixture>(); + var store = testFixture.GetStore>>(); + + // Act + var emptySet1 = new HashSet(); + var emptySet2 = new HashSet(); + var emptySet3 = new HashSet(); + + var key1 = new ProtoNetCacheKey { Id = 1 }; + var key2 = new ProtoNetCacheKey { Id = 2 }; + var key3 = new ProtoNetCacheKey { Id = 3 }; + + store.Put(key1, emptySet1); + store.Put(key2, emptySet2); + store.Put(key3, emptySet3); + + // Assert + store.HasKey(key1).ShouldBeTrue(); + store.TryGet(key1, out var value1).ShouldBeTrue(); + value1.ShouldNotBeNull(); + value1.Count.ShouldBe(0); + + store.HasKey(key2).ShouldBeTrue(); + store.TryGet(key2, out var value2).ShouldBeTrue(); + value2.ShouldNotBeNull(); + value2.Count.ShouldBe(0); + + store.HasKey(key3).ShouldBeTrue(); + store.TryGet(key3, out var value3).ShouldBeTrue(); + value3.ShouldNotBeNull(); + value3.Count.ShouldBe(0); + } + private static TestFixture CreateTestFixture() { var testFixture = TestFixture.Create(rockDb => diff --git a/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtobufSerializerTests.cs b/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtobufSerializerTests.cs index 2d5d02e..48caade 100644 --- a/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtobufSerializerTests.cs +++ b/test/RocksDb.Extensions.Tests/RocksDbStoreWithProtobufSerializerTests.cs @@ -170,6 +170,115 @@ public void should_put_and_retrieve_data_with_lists_from_store() value.ShouldBeEquivalentTo(cacheValue); } + [Test] + public void should_put_and_retrieve_data_with_sets_from_store() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var cacheKey = Enumerable.Range(0, 100) + .Select(x => new CacheKey + { + Id = x, + }) + .ToHashSet(); + + var cacheValue = Enumerable.Range(0, 100) + .Select(x => new CacheValue + { + Id = x, + Value = $"value-{x}", + }) + .ToHashSet(); + + store.Put(cacheKey, cacheValue); + + store.HasKey(cacheKey).ShouldBeTrue(); + store.TryGet(cacheKey, out var value).ShouldBeTrue(); + value.ShouldBeEquivalentTo(cacheValue); + } + + [Test] + public void should_put_and_retrieve_empty_set_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, ISet>(); + var store = testFixture.GetStore, ISet>>(); + + // Act + var emptyCacheKey = new HashSet(); + var emptyCacheValue = new HashSet(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_put_and_retrieve_empty_list_with_non_primitive_types() + { + // Arrange + using var testFixture = CreateTestFixture, IList>(); + var store = testFixture.GetStore, IList>>(); + + // Act + var emptyCacheKey = new List(); + var emptyCacheValue = new List(); + + store.Put(emptyCacheKey, emptyCacheValue); + + // Assert + store.HasKey(emptyCacheKey).ShouldBeTrue(); + store.TryGet(emptyCacheKey, out var value).ShouldBeTrue(); + value.ShouldNotBeNull(); + value.Count.ShouldBe(0); + value.ShouldBeEquivalentTo(emptyCacheValue); + } + + [Test] + public void should_handle_multiple_empty_sets_with_different_keys() + { + // Arrange + using var testFixture = CreateTestFixture>(); + var store = testFixture.GetStore>>(); + + // Act + var emptySet1 = new HashSet(); + var emptySet2 = new HashSet(); + var emptySet3 = new HashSet(); + + var key1 = new CacheKey { Id = 1 }; + var key2 = new CacheKey { Id = 2 }; + var key3 = new CacheKey { Id = 3 }; + + store.Put(key1, emptySet1); + store.Put(key2, emptySet2); + store.Put(key3, emptySet3); + + // Assert + store.HasKey(key1).ShouldBeTrue(); + store.TryGet(key1, out var value1).ShouldBeTrue(); + value1.ShouldNotBeNull(); + value1.Count.ShouldBe(0); + + store.HasKey(key2).ShouldBeTrue(); + store.TryGet(key2, out var value2).ShouldBeTrue(); + value2.ShouldNotBeNull(); + value2.Count.ShouldBe(0); + + store.HasKey(key3).ShouldBeTrue(); + store.TryGet(key3, out var value3).ShouldBeTrue(); + value3.ShouldNotBeNull(); + value3.Count.ShouldBe(0); + } + private static TestFixture CreateTestFixture() { var testFixture = TestFixture.Create(rockDb =>