diff --git a/src/Hydrogen/Crypto/Hashing/Hashers.cs b/src/Hydrogen/Crypto/Hashing/Hashers.cs index 1b231ba9..1aa6c17f 100644 --- a/src/Hydrogen/Crypto/Hashing/Hashers.cs +++ b/src/Hydrogen/Crypto/Hashing/Hashers.cs @@ -52,7 +52,7 @@ public static int GetDigestSizeBytes(CHF algorithm) public static byte[] Hash(CHF algorithm, TItem item, IItemSerializer serializer, Endianness endianness = HydrogenDefaults.Endianness) { var bytes = serializer.SerializeToBytes(item, endianness); return Hash(algorithm, bytes); -} + } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Hydrogen/ObjectSpaces/Builder/ObjectSpaceDimensionBuilder.cs b/src/Hydrogen/ObjectSpaces/Builder/ObjectSpaceDimensionBuilder.cs index c5d3efa6..c5be1019 100644 --- a/src/Hydrogen/ObjectSpaces/Builder/ObjectSpaceDimensionBuilder.cs +++ b/src/Hydrogen/ObjectSpaces/Builder/ObjectSpaceDimensionBuilder.cs @@ -60,28 +60,31 @@ public ObjectSpaceDimensionBuilder WithIdentifier(Expression WithIndexOn(Expression> memberExpression, string indexName = null) { + public ObjectSpaceDimensionBuilder WithIndexOn(Expression> memberExpression, string indexName = null, IndexNullPolicy nullPolicy = IndexNullPolicy.IgnoreNull) { var member = memberExpression.ToMember(); var index = new ObjectSpaceDefinition.IndexDefinition { Type = ObjectSpaceDefinition.IndexType.Index, Name = indexName ?? member.Name, Member = member, + NullPolicy = nullPolicy }; _indexes.Add(index); return this; } - public ObjectSpaceDimensionBuilder WithUniqueIndexOn(Expression> memberExpression, string indexName = null) { + public ObjectSpaceDimensionBuilder WithUniqueIndexOn(Expression> memberExpression, string indexName = null, IndexNullPolicy nullPolicy = IndexNullPolicy.IgnoreNull) { var member = memberExpression.ToMember(); var index = new ObjectSpaceDefinition.IndexDefinition { Type = ObjectSpaceDefinition.IndexType.UniqueKey, Name = indexName ?? member.Name, Member = member, + NullPolicy = nullPolicy }; _indexes.Add(index); return this; diff --git a/src/Hydrogen/ObjectSpaces/ObjectSpace.cs b/src/Hydrogen/ObjectSpaces/ObjectSpace.cs index cea5f219..c4e8f982 100644 --- a/src/Hydrogen/ObjectSpaces/ObjectSpace.cs +++ b/src/Hydrogen/ObjectSpaces/ObjectSpace.cs @@ -382,7 +382,7 @@ protected virtual IClusteredStreamsAttachment BuildIdentifier(ObjectStream dimen return keySerializer.IsConstantSize ? IndexFactory.CreateUniqueMemberIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, keyComparer) : - IndexFactory.CreateUniqueMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer); + IndexFactory.CreateUniqueMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer, indexDefinition.NullPolicy); } protected virtual IClusteredStreamsAttachment BuildUniqueKey(ObjectStream dimension, ObjectSpaceDefinition.DimensionDefinition dimensionDefinition, ObjectSpaceDefinition.IndexDefinition indexDefinition) { @@ -391,7 +391,7 @@ protected virtual IClusteredStreamsAttachment BuildUniqueKey(ObjectStream dimens return keySerializer.IsConstantSize ? IndexFactory.CreateUniqueMemberIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, keyComparer) : - IndexFactory.CreateUniqueMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer); + IndexFactory.CreateUniqueMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer, indexDefinition.NullPolicy); } protected virtual IClusteredStreamsAttachment BuildIndex(ObjectStream dimension, ObjectSpaceDefinition.DimensionDefinition dimensionDefinition, ObjectSpaceDefinition.IndexDefinition indexDefinition) { @@ -400,7 +400,7 @@ protected virtual IClusteredStreamsAttachment BuildIndex(ObjectStream dimension, return keySerializer.IsConstantSize ? IndexFactory.CreateMemberIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, keyComparer) : - IndexFactory.CreateMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer); + IndexFactory.CreateMemberChecksumIndex(dimension, indexDefinition.Name, indexDefinition.Member, keySerializer, null, null, keyComparer, indexDefinition.NullPolicy); } protected virtual IClusteredStreamsAttachment BuildRecyclableIndexStore(ObjectStream dimension, ObjectSpaceDefinition.DimensionDefinition dimensionDefinition, ObjectSpaceDefinition.IndexDefinition indexDefinition) { diff --git a/src/Hydrogen/ObjectSpaces/ObjectSpaceDefinition.cs b/src/Hydrogen/ObjectSpaces/ObjectSpaceDefinition.cs index 364dcd40..0a9443a8 100644 --- a/src/Hydrogen/ObjectSpaces/ObjectSpaceDefinition.cs +++ b/src/Hydrogen/ObjectSpaces/ObjectSpaceDefinition.cs @@ -128,6 +128,8 @@ public class IndexDefinition { public Member Member { get; set; } + public IndexNullPolicy NullPolicy { get; set; } + } public enum IndexType { diff --git a/src/Hydrogen/ObjectStream/Indexes/IndexFactory.cs b/src/Hydrogen/ObjectStream/Indexes/IndexFactory.cs index 892902c3..26a97632 100644 --- a/src/Hydrogen/ObjectStream/Indexes/IndexFactory.cs +++ b/src/Hydrogen/ObjectStream/Indexes/IndexFactory.cs @@ -86,7 +86,8 @@ internal static IClusteredStreamsAttachment CreateMemberChecksumIndex( IItemSerializer keySerializer = null, object keyChecksummer = null, object keyFetcher = null, - object keyComparer = null + object keyComparer = null, + IndexNullPolicy indexNullPolicy = IndexNullPolicy.IgnoreNull ) { Guard.Ensure(objectStream.GetType() == typeof(ObjectStream<>).MakeGenericType(member.DeclaringType)); @@ -101,7 +102,7 @@ internal static IClusteredStreamsAttachment CreateMemberChecksumIndex( .GetMethod(nameof(CreateProjectionChecksumIndex), BindingFlags.NonPublic | BindingFlags.Static) .MakeGenericMethod(member.DeclaringType, member.PropertyType); - return (IClusteredStreamsAttachment)method.Invoke(null, new object[] { genericContainer, indexName, projection, keySerializer, keyChecksummer, keyFetcher, keyComparer }); + return (IClusteredStreamsAttachment)method.Invoke(null, new object[] { genericContainer, indexName, projection, keySerializer, keyChecksummer, keyFetcher, keyComparer, indexNullPolicy }); } @@ -112,7 +113,9 @@ internal static ProjectionChecksumIndex CreateProjectionChecksumInd IItemSerializer keySerializer = null, IItemChecksummer keyChecksummer = null, Func keyFetcher = null, - IEqualityComparer keyComparer= null) { + IEqualityComparer keyComparer= null, + IndexNullPolicy indexNullPolicy = IndexNullPolicy.IgnoreNull + ) { keySerializer ??= ItemSerializer.Default; keyChecksummer ??= new ItemDigestor(keySerializer, objectStream.Streams.Endianness); @@ -124,7 +127,8 @@ internal static ProjectionChecksumIndex CreateProjectionChecksumInd projection, keyChecksummer, keyFetcher, - keyComparer + keyComparer, + indexNullPolicy ); return keyChecksumKeyIndex; @@ -141,7 +145,8 @@ internal static IClusteredStreamsAttachment CreateUniqueMemberChecksumIndex( IItemSerializer keySerializer = null, object keyChecksummer = null, object keyFetcher = null, - object keyComparer = null + object keyComparer = null, + IndexNullPolicy indexNullPolicy = IndexNullPolicy.IgnoreNull ) { Guard.Ensure(objectStream.GetType() == typeof(ObjectStream<>).MakeGenericType(member.DeclaringType)); @@ -156,7 +161,7 @@ internal static IClusteredStreamsAttachment CreateUniqueMemberChecksumIndex( .GetMethod(nameof(CreateUniqueProjectionChecksumIndex), BindingFlags.NonPublic | BindingFlags.Static) .MakeGenericMethod(member.DeclaringType, member.PropertyType); - return (IClusteredStreamsAttachment)method.Invoke(null, new object[] { genericContainer, indexName, projection, keySerializer, keyChecksummer, keyFetcher, keyComparer }); + return (IClusteredStreamsAttachment)method.Invoke(null, new object[] { genericContainer, indexName, projection, keySerializer, keyChecksummer, keyFetcher, keyComparer, indexNullPolicy }); } @@ -167,7 +172,8 @@ internal static UniqueProjectionChecksumIndex CreateUniqueProjectio IItemSerializer keySerializer = null, IItemChecksummer keyChecksummer = null, Func keyFetcher = null, - IEqualityComparer keyComparer= null + IEqualityComparer keyComparer = null, + IndexNullPolicy indexNullPolicy = IndexNullPolicy.IgnoreNull ) { keySerializer ??= ItemSerializer.Default; keyChecksummer ??= new ItemDigestor(keySerializer, objectStream.Streams.Endianness); @@ -179,7 +185,8 @@ internal static UniqueProjectionChecksumIndex CreateUniqueProjectio projection, keyChecksummer, keyFetcher, - keyComparer + keyComparer, + indexNullPolicy ); return uniqueKeyChecksumIndex; diff --git a/src/Hydrogen/ObjectStream/Indexes/IndexNullPolicy.cs b/src/Hydrogen/ObjectStream/Indexes/IndexNullPolicy.cs new file mode 100644 index 00000000..7f6785ae --- /dev/null +++ b/src/Hydrogen/ObjectStream/Indexes/IndexNullPolicy.cs @@ -0,0 +1,7 @@ +namespace Hydrogen; + +public enum IndexNullPolicy { + IgnoreNull, + IndexNullValue, + ThrowOnNull, +} diff --git a/src/Hydrogen/ObjectStream/Indexes/ProjectionChecksumIndex.cs b/src/Hydrogen/ObjectStream/Indexes/ProjectionChecksumIndex.cs index 47b63bf2..5a29a038 100644 --- a/src/Hydrogen/ObjectStream/Indexes/ProjectionChecksumIndex.cs +++ b/src/Hydrogen/ObjectStream/Indexes/ProjectionChecksumIndex.cs @@ -32,10 +32,12 @@ public ProjectionChecksumIndex( Func projection, IItemChecksummer projectionChecksummer, Func projectionHydrator, - IEqualityComparer keyComparer + IEqualityComparer keyComparer, + IndexNullPolicy nullPolicy ) : base( objectStream, - new IndexStorageAttachment(objectStream.Streams, indexName, PrimitiveSerializer.Instance, EqualityComparer.Default) + new IndexStorageAttachment(objectStream.Streams, indexName, PrimitiveSerializer.Instance, EqualityComparer.Default), + nullPolicy ) { Guard.ArgumentNotNull(projection, nameof(projection)); Guard.ArgumentNotNull(projectionChecksummer, nameof(projectionChecksummer)); @@ -96,6 +98,8 @@ protected override void OnContainerCleared() { Store.Attach(); } + protected override bool IsNullValue((TProjection, int) projection) => projection.Item1 is null; + private class ChecksumBasedLookup : ILookup { private readonly ProjectionChecksumIndex _parent; diff --git a/src/Hydrogen/ObjectStream/Indexes/ProjectionIndex.cs b/src/Hydrogen/ObjectStream/Indexes/ProjectionIndex.cs index 0d2d1599..a3b86908 100644 --- a/src/Hydrogen/ObjectStream/Indexes/ProjectionIndex.cs +++ b/src/Hydrogen/ObjectStream/Indexes/ProjectionIndex.cs @@ -21,9 +21,11 @@ internal sealed class ProjectionIndex : ProjectionIndexBase< public ProjectionIndex(ObjectStream objectStream, string indexName, Func projection, IItemSerializer projectionSerializer, IEqualityComparer projectionComparer) : base( objectStream, - new IndexStorageAttachment(objectStream.Streams, indexName, projectionSerializer, projectionComparer) + new IndexStorageAttachment(objectStream.Streams, indexName, projectionSerializer, projectionComparer), + IndexNullPolicy.ThrowOnNull ) { Guard.ArgumentNotNull(projection, nameof(projection)); + Guard.Argument(projectionSerializer.IsConstantSize, nameof(projectionSerializer), "Must be a constant size serializer"); _projection = projection; } diff --git a/src/Hydrogen/ObjectStream/Indexes/ProjectionIndexBase.cs b/src/Hydrogen/ObjectStream/Indexes/ProjectionIndexBase.cs index d3ed993d..fe3ffbe1 100644 --- a/src/Hydrogen/ObjectStream/Indexes/ProjectionIndexBase.cs +++ b/src/Hydrogen/ObjectStream/Indexes/ProjectionIndexBase.cs @@ -6,6 +6,8 @@ // // This notice must not be removed when duplicating this file or its contents, in whole or in part. +using System; + namespace Hydrogen; /// @@ -16,19 +18,25 @@ namespace Hydrogen; /// Type of store used to store item member values public abstract class ProjectionIndexBase : IndexBase where TStore : IClusteredStreamsAttachment { protected new ObjectStream Objects; - protected ProjectionIndexBase(ObjectStream objectStream, TStore store) + + protected ProjectionIndexBase(ObjectStream objectStream, TStore store, IndexNullPolicy nullPolicy) : base(objectStream, store) { Guard.Ensure(!store.IsAttached, "Store must not be attached already"); Objects = (ObjectStream)base.Objects; + NullPolicy = nullPolicy; } + public IndexNullPolicy NullPolicy { get; } + public abstract TProjection ApplyProjection(TItem item); protected sealed override void OnAdding(object item, long index) { base.OnAdding(item, index); var itemT = (TItem)item; - OnAdding(itemT, index, ApplyProjection(itemT)); + var projection = ApplyProjection(itemT); + if (ApplyNullPolicy(itemT, projection)) + OnAdding(itemT, index, projection); } protected sealed override void OnAdded(object item, long index) { @@ -40,7 +48,9 @@ protected sealed override void OnAdded(object item, long index) { protected sealed override void OnInserting(object item, long index) { base.OnInserting(item, index); var itemT = (TItem)item; - OnInserting(itemT, index, ApplyProjection(itemT)); + var projection = ApplyProjection(itemT); + if (ApplyNullPolicy(itemT, projection)) + OnInserting(itemT, index, projection); } protected sealed override void OnInserted(object item, long index) { @@ -52,7 +62,9 @@ protected sealed override void OnInserted(object item, long index) { protected sealed override void OnUpdating(object item, long index) { base.OnUpdating(item, index); var itemT = (TItem)item; - OnUpdating(itemT, index, ApplyProjection(itemT)); + var projection = ApplyProjection(itemT); + if (ApplyNullPolicy(itemT, projection)) + OnUpdating(itemT, index, projection); } protected sealed override void OnUpdated(object item, long index) { @@ -85,4 +97,23 @@ protected override void OnRemoved(long index) { protected override void OnReaped(long index) { } -} + + protected virtual bool IsNullValue(TProjection projection) => projection is null; + + protected virtual bool ApplyNullPolicy(TItem item, TProjection projection) { + if (!IsNullValue(projection)) + return true; + + switch(NullPolicy) { + case IndexNullPolicy.IgnoreNull: + return false; + case IndexNullPolicy.IndexNullValue: + return true; + case IndexNullPolicy.ThrowOnNull: + throw new InvalidOperationException($"Unable to apply index {AttachmentID} for {item.GetType().ToStringCS()} {item} as it resulted in NULL projection"); + default: + throw new ArgumentOutOfRangeException(); + } + } + +} \ No newline at end of file diff --git a/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionChecksumIndex.cs b/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionChecksumIndex.cs index 473f73a3..9e0ff8a9 100644 --- a/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionChecksumIndex.cs +++ b/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionChecksumIndex.cs @@ -31,10 +31,12 @@ public UniqueProjectionChecksumIndex( Func projection, IItemChecksummer keyChecksummer, Func keyHydrator, - IEqualityComparer keyComparer + IEqualityComparer keyComparer, + IndexNullPolicy indexNullPolicy ) : base( objectStream, - new IndexStorageAttachment(objectStream.Streams, indexName, PrimitiveSerializer.Instance, EqualityComparer.Default) + new IndexStorageAttachment(objectStream.Streams, indexName, PrimitiveSerializer.Instance, EqualityComparer.Default), + indexNullPolicy ) { Guard.ArgumentNotNull(projection, nameof(projection)); Guard.ArgumentNotNull(keyChecksummer, nameof(keyChecksummer)); @@ -63,7 +65,7 @@ public override (TKey, int) ApplyProjection(TItem item) { protected override void OnAdding(TItem item, long index, (TKey, int) keyChecksum) { if (!IsUnique(keyChecksum, null, out var clashIndex)) - throw new InvalidOperationException($"Unable to add {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1}' ({keyChecksum.Item2}) with index {clashIndex}"); + throw new InvalidOperationException($"Unable to add {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1?.ToString() ?? "NULL"}' ({keyChecksum.Item2}) with index {clashIndex}"); } protected override void OnAdded(TItem item, long index, (TKey, int) keyChecksum) { @@ -73,7 +75,7 @@ protected override void OnAdded(TItem item, long index, (TKey, int) keyChecksum) protected override void OnUpdating(TItem item, long index, (TKey, int) keyChecksum) { if (!IsUnique(keyChecksum, index, out var clashIndex)) - throw new InvalidOperationException($"Unable to update {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1}' ({keyChecksum.Item2}) with index {clashIndex}"); + throw new InvalidOperationException($"Unable to update {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1?.ToString() ?? "NULL"}' ({keyChecksum.Item2}) with index {clashIndex}"); } protected override void OnUpdated(TItem item, long index, (TKey, int) keyChecksum) { @@ -83,7 +85,7 @@ protected override void OnUpdated(TItem item, long index, (TKey, int) keyChecksu protected override void OnInserting(TItem item, long index, (TKey, int) keyChecksum) { if (!IsUnique(keyChecksum, index, out var clashIndex)) - throw new InvalidOperationException($"Unable to insert {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1}' ({keyChecksum.Item2}) with index {clashIndex}"); + throw new InvalidOperationException($"Unable to insert {typeof(TItem).ToStringCS()} as a unique projection (checksummed) violation occurs with projected key '{keyChecksum.Item1?.ToString() ?? "NULL"}' ({keyChecksum.Item2}) with index {clashIndex}"); } protected override void OnInserted(TItem item, long index, (TKey, int) keyChecksum) { @@ -108,6 +110,8 @@ protected override void OnContainerCleared() { Store.Attach(); } + protected override bool IsNullValue((TKey, int) projection) => projection.Item1 is null; + private bool IsUnique((TKey, int) keyChecksum, long? exemptIndex, out long clashIndex) { if (_keyDictionary.TryGetValue(keyChecksum, out var foundIndex)) { if (foundIndex != exemptIndex) { diff --git a/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionIndex.cs b/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionIndex.cs index 333fa790..7a2a35cd 100644 --- a/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionIndex.cs +++ b/src/Hydrogen/ObjectStream/Indexes/UniqueProjectionIndex.cs @@ -17,8 +17,11 @@ internal sealed class UniqueProjectionIndex : ProjectionIndexBase objectStream, string indexName, Func projection, IItemSerializer keySerializer, IEqualityComparer keyComparer) : base( objectStream, - new UniqueKeyStorageAttachment(objectStream.Streams, indexName, keySerializer, keyComparer) - ) { + new UniqueKeyStorageAttachment(objectStream.Streams, indexName, keySerializer, keyComparer), + IndexNullPolicy.ThrowOnNull + ) { + Guard.ArgumentNotNull(projection, nameof(projection)); + Guard.Argument(keySerializer.IsConstantSize, nameof(keySerializer), "Must be a constant size serializer"); _projection = projection; } @@ -34,7 +37,7 @@ public IReadOnlyDictionary Values { protected override void OnAdding(TItem item, long index, TKey key) { if (!IsUnique(key, null, out var clashIndex)) - throw new InvalidOperationException($"Unable to add {typeof(TItem).ToStringCS()} as a unique keyChecksum violation occurs with item at {clashIndex}"); + throw new InvalidOperationException($"Unable to add {typeof(TItem).ToStringCS()} as a unique projection violation occurs with projected key '{key?.ToString() ?? "NULL"}' with index {clashIndex}"); } protected override void OnAdded(TItem item, long index, TKey keyChecksum) { @@ -43,7 +46,7 @@ protected override void OnAdded(TItem item, long index, TKey keyChecksum) { protected override void OnUpdating(TItem item, long index, TKey key) { if (!IsUnique(key, index, out var clashIndex)) - throw new InvalidOperationException($"Unable to update {typeof(TItem).ToStringCS()} at {index} as a unique keyChecksum violation occurs with item at {clashIndex}"); + throw new InvalidOperationException($"Unable to update {typeof(TItem).ToStringCS()} as a unique projection violation occurs with projected key '{key?.ToString() ?? "NULL"}' with index {clashIndex}"); } protected override void OnUpdated(TItem item, long index, TKey keyChecksum) { @@ -52,7 +55,7 @@ protected override void OnUpdated(TItem item, long index, TKey keyChecksum) { protected override void OnInserting(TItem item, long index, TKey key) { if (!IsUnique(key, index, out var clashIndex)) - throw new InvalidOperationException($"Unable to insert {typeof(TItem).ToStringCS()} at {index} as a unique keyChecksum violation occurs with item at {clashIndex}"); + throw new InvalidOperationException($"Unable to insert {typeof(TItem).ToStringCS()} as a unique projection violation occurs with projected key '{key?.ToString() ?? "NULL"}' with index {clashIndex}"); } protected override void OnInserted(TItem item, long index, TKey keyChecksum) { diff --git a/tests/Hydrogen.Tests/ObjectSpaces/MerkleizedObjectSpacesTest.cs b/tests/Hydrogen.Tests/ObjectSpaces/MerkleizedObjectSpacesTest.cs index 350651dc..26ca7c23 100644 --- a/tests/Hydrogen.Tests/ObjectSpaces/MerkleizedObjectSpacesTest.cs +++ b/tests/Hydrogen.Tests/ObjectSpaces/MerkleizedObjectSpacesTest.cs @@ -18,8 +18,8 @@ namespace Hydrogen.Tests; [TestFixture, Timeout(60000)] public class MerkleizedObjectSpacesTest : ObjectSpacesTestBase { - protected override ObjectSpace CreateObjectSpace(string filePath) - => PrepareObjectSpaceBuilder() + protected override ObjectSpace CreateObjectSpace(string filePath, IndexNullPolicy nullValuePolicy = IndexNullPolicy.IgnoreNull) + => PrepareObjectSpaceBuilder(nullValuePolicy) .UseFile(filePath) .Merkleized() .Build(); @@ -41,7 +41,6 @@ public void CheckRootsChanged() { var dim1 = objectSpace.Dimensions[0]; var dim2 = objectSpace.Dimensions[1]; - // Verify account dimension has single item root using var dim1Scope = dim1.ObjectStream.EnterAccessScope(); var accountRoot = dim1.ObjectStream.Streams.Header.MapExtensionProperty(0, digestSize, new ConstantSizeByteArraySerializer(digestSize)).Value; diff --git a/tests/Hydrogen.Tests/ObjectSpaces/NonMerkleizedObjectSpacesTest.cs b/tests/Hydrogen.Tests/ObjectSpaces/NonMerkleizedObjectSpacesTest.cs index c273756c..e3456d25 100644 --- a/tests/Hydrogen.Tests/ObjectSpaces/NonMerkleizedObjectSpacesTest.cs +++ b/tests/Hydrogen.Tests/ObjectSpaces/NonMerkleizedObjectSpacesTest.cs @@ -16,8 +16,8 @@ namespace Hydrogen.Tests; [TestFixture, Timeout(60000)] public class NonMerkleizedObjectSpacesTest : ObjectSpacesTestBase { - protected override ObjectSpace CreateObjectSpace(string filePath) - => PrepareObjectSpaceBuilder() + protected override ObjectSpace CreateObjectSpace(string filePath, IndexNullPolicy nullValuePolicy = IndexNullPolicy.IgnoreNull) + => PrepareObjectSpaceBuilder(nullValuePolicy) .UseFile(filePath) .Build(); } diff --git a/tests/Hydrogen.Tests/ObjectSpaces/ObjectSpacesTestBase.cs b/tests/Hydrogen.Tests/ObjectSpaces/ObjectSpacesTestBase.cs index d1c8ca6c..e5ce5521 100644 --- a/tests/Hydrogen.Tests/ObjectSpaces/ObjectSpacesTestBase.cs +++ b/tests/Hydrogen.Tests/ObjectSpaces/ObjectSpacesTestBase.cs @@ -18,7 +18,7 @@ namespace Hydrogen.Tests; [TestFixture, Timeout(60000)] public abstract class ObjectSpacesTestBase { - protected abstract ObjectSpace CreateObjectSpace(string filePath); + protected abstract ObjectSpace CreateObjectSpace(string filePath, IndexNullPolicy nullValuePolicy = IndexNullPolicy.IgnoreNull); #region Activation @@ -281,6 +281,45 @@ public void UniqueMember_Checksummed_SaveThenDeleteThenSave_ThrowsNothing() { Assert.That( () => objectSpace.Save(account3), Throws.Nothing); } + [Test] + public void UniqueMember_Checksummed_IgnoreNullPolicy() { + // note: string based property will use a checksum-based index since not constant length key + using var scope = CreateObjectSpaceScope(nullValuePolicy: IndexNullPolicy.IgnoreNull); + var objectSpace = scope.Item; + var rng = new Random(); + var account1 = CreateAccount(rng); + var account2 = CreateAccount(rng); + account1.Name = null; + account2.Name = null; + objectSpace.Save(account1); + Assert.That( () => objectSpace.Save(account2), Throws.Nothing); + } + + [Test] + public void UniqueMember_Checksummed_IndexNullValue() { + // note: string based property will use a checksum-based index since not constant length key + using var scope = CreateObjectSpaceScope(nullValuePolicy: IndexNullPolicy.IndexNullValue); + var objectSpace = scope.Item; + var rng = new Random(); + var account1 = CreateAccount(rng); + var account2 = CreateAccount(rng); + account1.Name = null; + account2.Name = null; + objectSpace.Save(account1); + Assert.That( () => objectSpace.Save(account2), Throws.InvalidOperationException); + } + + [Test] + public void UniqueMember_Checksummed_ThrowOnNullValue() { + // note: string based property will use a checksum-based index since not constant length key + using var scope = CreateObjectSpaceScope(nullValuePolicy: IndexNullPolicy.ThrowOnNull); + var objectSpace = scope.Item; + var rng = new Random(); + var account1 = CreateAccount(rng); + account1.Name = null; + Assert.That(() => objectSpace.Save(account1), Throws.InvalidOperationException); + } + #endregion #region Unique Member @@ -388,13 +427,13 @@ public void UniqueMember_SaveThenDeleteThenSave_ThrowsNothing() { #region Aux - protected static ObjectSpaceBuilder PrepareObjectSpaceBuilder() { + protected static ObjectSpaceBuilder PrepareObjectSpaceBuilder(IndexNullPolicy nullValuePolicy) { var builder = new ObjectSpaceBuilder(); builder .AutoLoad() .AddDimension() - .WithUniqueIndexOn(x => x.Name) - .WithUniqueIndexOn(x => x.UniqueNumber) + .WithUniqueIndexOn(x => x.Name, nullPolicy: nullValuePolicy) + .WithUniqueIndexOn(x => x.UniqueNumber, nullPolicy: nullValuePolicy) .UsingEqualityComparer(CreateAccountComparer()) .Done() .AddDimension() @@ -441,11 +480,11 @@ protected static IEqualityComparer CreateIdentityComparer() .By(x => x.DSS) .ThenBy(x => x.Key, ByteArrayEqualityComparer.Instance); - protected IScope CreateObjectSpaceScope(string folder = null, bool keepFolder = false) { + protected IScope CreateObjectSpaceScope(string folder = null, bool keepFolder = false, IndexNullPolicy nullValuePolicy = IndexNullPolicy.IgnoreNull) { folder ??= Tools.FileSystem.GetTempEmptyDirectory(true); var filePath = Path.Combine(folder, "app.db"); - var objectSpace = CreateObjectSpace(filePath); + var objectSpace = CreateObjectSpace(filePath, nullValuePolicy); var disposables = new Disposables(); disposables.Add(objectSpace); if (!keepFolder)