diff --git a/benchmarks/NexusMods.MnemonicDB.Benchmarks/Benchmarks/ReadThenWriteBenchmarks.cs b/benchmarks/NexusMods.MnemonicDB.Benchmarks/Benchmarks/ReadThenWriteBenchmarks.cs index b3cb8b0..a9449e7 100644 --- a/benchmarks/NexusMods.MnemonicDB.Benchmarks/Benchmarks/ReadThenWriteBenchmarks.cs +++ b/benchmarks/NexusMods.MnemonicDB.Benchmarks/Benchmarks/ReadThenWriteBenchmarks.cs @@ -61,7 +61,7 @@ public async Task ReadThenWrite() using var tx = Connection.BeginTransaction(); var mod = Mod.Load(Connection.Db, _modId); var oldHash = mod.OptionalHash; - tx.Add(_modId, Mod.OptionalHash, Hash.From(oldHash.Value + 1)); + tx.Add(_modId, Mod.OptionalHash, Hash.From((ulong)oldHash.Value + 1)); var nextdb = await tx.Commit(); var loadout = Loadout.Load(Connection.Db, mod.LoadoutId); diff --git a/src/NexusMods.MnemonicDB.Abstractions/Attributes/ScalarAttribute.cs b/src/NexusMods.MnemonicDB.Abstractions/Attributes/ScalarAttribute.cs index 28abfe9..512e21e 100644 --- a/src/NexusMods.MnemonicDB.Abstractions/Attributes/ScalarAttribute.cs +++ b/src/NexusMods.MnemonicDB.Abstractions/Attributes/ScalarAttribute.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using DynamicData.Kernel; using JetBrains.Annotations; +using NexusMods.MnemonicDB.Abstractions.IndexSegments; using NexusMods.MnemonicDB.Abstractions.Models; namespace NexusMods.MnemonicDB.Abstractions.Attributes; @@ -32,84 +34,77 @@ public bool Contains(T entity) where T : IHasIdAndIndexSegment } /// - /// Gets the value of the attribute from the entity. + /// Tries to get the value of the attribute from the entity. /// - public TValue Get(IHasIdAndIndexSegment entity) + public bool TryGetValue(T entity, IndexSegment segment, [NotNullWhen(true)] out TValue? value) + where T : IHasEntityIdAndDb { - var segment = entity.IndexSegment; - var dbId = entity.Db.AttributeCache.GetAttributeId(Id); - for (var i = 0; i < segment.Count; i++) + var attributeId = entity.Db.AttributeCache.GetAttributeId(Id); + foreach (var datom in segment) { - var datom = segment[i]; - if (datom.A != dbId) continue; - return ReadValue(datom.ValueSpan, datom.Prefix.ValueTag, entity.Db.Connection.AttributeResolver); + if (datom.A != attributeId) continue; + value = ReadValue(datom.ValueSpan, datom.Prefix.ValueTag, entity.Db.Connection.AttributeResolver); + return true; } - if (DefaultValue.HasValue) - return DefaultValue.Value; - - ThrowKeyNotfoundException(entity); - return default!; + value = default!; + return false; } - + /// - /// Gets the value of the attribute from the entity, this performs a lookup in the database - /// so prefer using the overload with IHasIdAndIndexSegment if you already have the segment. + /// Tries to get the value of the attribute from the entity. /// - protected TValue Get(IHasEntityIdAndDb entity) + public bool TryGetValue(T entity, [NotNullWhen(true)] out TValue? value) + where T : IHasIdAndIndexSegment { - var segment = entity.Db.Get(entity.Id); - var dbId = entity.Db.AttributeCache.GetAttributeId(Id); - for (var i = 0; i < segment.Count; i++) - { - var datom = segment[i]; - if (datom.A != dbId) continue; - return ReadValue(datom.ValueSpan, datom.Prefix.ValueTag, entity.Db.Connection.AttributeResolver); - } - - if (DefaultValue.HasValue) - return DefaultValue.Value; - - ThrowKeyNotfoundException(entity); - return default!; + return TryGetValue(entity, segment: entity.IndexSegment, out value); } /// - /// Retracts the attribute from the entity. + /// Gets the value of the attribute from the entity. /// - public void Retract(IAttachedEntity entityWithTx) + public TValue Get(T entity, IndexSegment segment) + where T : IHasEntityIdAndDb { - Retract(entityWithTx, Get(entityWithTx)); + if (TryGetValue(entity, segment, out var value)) return value; + return ThrowKeyNotfoundException(entity.Id); } - private void ThrowKeyNotfoundException(IHasEntityIdAndDb entity) + /// + /// Gets the value of the attribute from the entity. + /// + public TValue Get(T entity) + where T : IHasIdAndIndexSegment { - throw new KeyNotFoundException($"Entity {entity.Id} does not have attribute {Id}"); + var segment = entity.IndexSegment; + return Get(entity, segment); } /// - /// Try to get the value of the attribute from the entity. + /// Gets the value of the attribute from the entity, , or . /// - public bool TryGet(IHasIdAndIndexSegment entity, out TValue value) + public Optional GetOptional(T entity) + where T : IHasIdAndIndexSegment { - var segment = entity.IndexSegment; - var dbId = entity.Db.AttributeCache.GetAttributeId(Id); - for (var i = 0; i < segment.Count; i++) - { - var datom = segment[i]; - if (datom.A != dbId) continue; - value = ReadValue(datom.ValueSpan, datom.Prefix.ValueTag, entity.Db.Connection.AttributeResolver); - return true; - } + if (TryGetValue(entity, entity.IndexSegment, out var value)) return value; + return DefaultValue.HasValue ? DefaultValue : Optional.None; + } - if (DefaultValue.HasValue) - { - value = DefaultValue.Value; - return true; - } + /// + /// Retracts the attribute from the entity. + /// + public void Retract(IAttachedEntity entityWithTx) + { + Retract(entityWithTx, value: Get(entityWithTx, segment: entityWithTx.Db.Get(entityWithTx.Id))); + } - value = default!; - return false; + [DoesNotReturn] + private TValue ThrowKeyNotfoundException(EntityId entityId) + { + throw new KeyNotFoundException($"Entity `{entityId}` doesn't have attribute {Id}"); +#pragma warning disable CS0162 // Unreachable code detected + return default!; +#pragma warning restore CS0162 // Unreachable code detected } /// diff --git a/src/NexusMods.MnemonicDB.SourceGenerator/Template.weave b/src/NexusMods.MnemonicDB.SourceGenerator/Template.weave index d572f35..91e24b8 100644 --- a/src/NexusMods.MnemonicDB.SourceGenerator/Template.weave +++ b/src/NexusMods.MnemonicDB.SourceGenerator/Template.weave @@ -416,8 +416,12 @@ public partial class {{= model.Name}} : __MODELS__.IModelFactory<{{= model.Name} {{elif attr.IsReference}} public {{= attr.ReferenceType.ToDisplayString()}}Id {{= attr.ContextualName}} => {{= attr.ReferenceType.ToDisplayString()}}Id.From({{= attr.FieldName}}.Get(this)); {{else}} + {{if attr.IsOptional}} + public global::DynamicData.Kernel.Optional<{{= attr.HighLevelType.ToDisplayString()}}> {{= attr.ContextualName }} => {{= attr.FieldName}}.GetOptional(this); + {{else}} public {{= attr.HighLevelType.ToDisplayString()}} {{= attr.ContextualName}} => {{= attr.FieldName}}.Get(this); {{/if}} + {{/if}} {{if attr.IsReference && attr.IsScalar}} diff --git a/tests/NexusMods.MnemonicDB.Tests/DbTests.cs b/tests/NexusMods.MnemonicDB.Tests/DbTests.cs index a85dffa..da5d46c 100644 --- a/tests/NexusMods.MnemonicDB.Tests/DbTests.cs +++ b/tests/NexusMods.MnemonicDB.Tests/DbTests.cs @@ -541,13 +541,13 @@ public async Task CanReadAndWriteOptionalAttributes() var remapped = result.Remap(modWithDescription); remapped.Contains(Mod.Description).Should().BeTrue(); - Mod.Description.TryGet(remapped, out var foundDesc).Should().BeTrue(); + Mod.Description.TryGetValue(remapped, remapped.IndexSegment, out var foundDesc).Should().BeTrue(); foundDesc.Should().Be("Test Description"); - remapped.Description.Should().Be("Test Description"); - + remapped.Description.Value.Should().Be("Test Description"); + var remapped2 = result.Remap(modWithoutDiscription); remapped2.Contains(Mod.Description).Should().BeFalse(); - Mod.Description.TryGet(remapped2, out var foundDesc2).Should().BeFalse(); + Mod.Description.TryGetValue(remapped2, remapped2.IndexSegment, out var foundDesc2).Should().BeFalse(); } [Fact] @@ -1039,12 +1039,10 @@ public async Task CanGetAttributesThatRequireDI() Name = "Test Loadout", GamePath = path }; - + var result = await tx.Commit(); - var loadout = result.Remap(loadout1); - - loadout.GamePath.Should().Be(path); + loadout.GamePath.Value.Should().Be(path); } [Fact]