diff --git a/ArrowDbCore.sln b/ArrowDbCore.sln deleted file mode 100644 index 679eb26..0000000 --- a/ArrowDbCore.sln +++ /dev/null @@ -1,52 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{822210FC-B851-4C2C-AEAE-250F17687CC3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArrowDbCore", "src\ArrowDbCore\ArrowDbCore.csproj", "{23F42F88-1579-4087-ABF2-814EDBD53F59}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4ED1B77D-F425-487C-B32C-53F92A8E5A2E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArrowDbCore.Tests.Unit", "tests\ArrowDbCore.Tests.Unit\ArrowDbCore.Tests.Unit.csproj", "{CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{9844EA79-5000-4276-A2C4-D7BA430F18B4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArrowDbCore.Benchmarks", "benchmarks\ArrowDbCore.Benchmarks\ArrowDbCore.Benchmarks.csproj", "{419CA340-26F0-4FC1-83AC-D06A93AAB190}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArrowDbCore.Tests.Integrity", "tests\ArrowDbCore.Tests.Integrity\ArrowDbCore.Tests.Integrity.csproj", "{39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {23F42F88-1579-4087-ABF2-814EDBD53F59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23F42F88-1579-4087-ABF2-814EDBD53F59}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23F42F88-1579-4087-ABF2-814EDBD53F59}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23F42F88-1579-4087-ABF2-814EDBD53F59}.Release|Any CPU.Build.0 = Release|Any CPU - {CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D}.Release|Any CPU.Build.0 = Release|Any CPU - {419CA340-26F0-4FC1-83AC-D06A93AAB190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {419CA340-26F0-4FC1-83AC-D06A93AAB190}.Debug|Any CPU.Build.0 = Debug|Any CPU - {419CA340-26F0-4FC1-83AC-D06A93AAB190}.Release|Any CPU.ActiveCfg = Release|Any CPU - {419CA340-26F0-4FC1-83AC-D06A93AAB190}.Release|Any CPU.Build.0 = Release|Any CPU - {39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {23F42F88-1579-4087-ABF2-814EDBD53F59} = {822210FC-B851-4C2C-AEAE-250F17687CC3} - {CDBBF9DF-5F8B-41C0-AAE7-2EC157C3BA1D} = {4ED1B77D-F425-487C-B32C-53F92A8E5A2E} - {419CA340-26F0-4FC1-83AC-D06A93AAB190} = {9844EA79-5000-4276-A2C4-D7BA430F18B4} - {39B1435C-B9E0-40A8-ABA9-7BB2F2CCF787} = {4ED1B77D-F425-487C-B32C-53F92A8E5A2E} - EndGlobalSection -EndGlobal diff --git a/ArrowDbCore.slnx b/ArrowDbCore.slnx new file mode 100644 index 0000000..f97cfca --- /dev/null +++ b/ArrowDbCore.slnx @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d5105..70a9713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog (Sorted by Date in Descending Order) +## 1.4.0.0 + +* `GetOrAddAsync` and `Upsert` (which has the `updateCondition` argument) now both have overloads that accept a `TArg` parameter as well as a modified factory function that can use it, in order to avoid closures. + ## 1.3.0.0 * Added overloads of `Upsert` that accept a `string` key, while the `ReadOnlySpan` overloads are amazing in specific cases where its use can prevent a string allocation for the lookup, in other places where the input was originally a `string` that was implicitly converted to a `ReadOnlySpan` for the parameter, this would've caused a copy to be allocated for the key when the key did not exist. The same scenario will now use the `string` overload and use it for the key directly, avoiding the intermediate copy. diff --git a/README.md b/README.md index e07666c..b9d876f 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,15 @@ bool db.TryGetValue(ReadOnlySpan key, JsonTypeInfo jsonTyp Notice that all APIs accept keys as `ReadOnlySpan` to avoid unnecessary allocations. This means that if you check for a key by some slice of a string, there is no need to allocate a string just for the lookup. -Upserting (adding or updating) is done via 4 overloads: +Upserting (adding or updating) is done via 6 overloads: ```csharp bool db.Upsert(string key, TValue value, JsonTypeInfo jsonTypeInfo); +bool db.Upsert(string, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition); +bool Upsert(string key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition, TArg updateConditionArgument); bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo); -bool db.Upsert(string, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition = null); -bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition = null); +bool db.Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition); +bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition, TArg updateConditionArgument); ``` ### Upsert Overloads Best Practices @@ -161,6 +163,30 @@ do { As the example shows retries is the usual way to resolve these conflicts, but custom logic can also be used, you can simply reject the operation, and also use other loops or even `goto` statements if you are brave enough. +In this example, `referenceDate` is a local value, and when used inside the lambda of the `updateCondition` it allocates a [Closure](https://www.youtube.com/watch?v=h3MsnBRqzcY), which depending on whether is a performance critical code section, could be sub-optimal. To address this, a secondary overload is available: + +```csharp +bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition, TArg updateConditionArgument) + +// adapt example to use this +bool noteUpdated = false; // track if conflict was resolved +do { + if (!db.TryGetValue("shopping list", MyJsonContext.Default.Note, out Note? note)) { + // note does not exist, I am skipping this condition as it is not part of the example + } + // we are here, so previous note was found + var referenceDate = note.LastUpdatedUTC; // locally store the reference + note!.Content += "Pizza"; // modify the note + note.LastUpdatedUTC = TimeProvider.System.UtcNow; // update note timestamp + // update on condition that the stored reference is still the same, by checking the timestamp + if (db.Upsert("shopping list", note, MyJsonContext.Default.Note, (reference, date) => reference.LastUpdatedUTC == date), referenceDate) { + noteUpdated = true; // note was updated - this will break out of the loop + } +} while (!noteUpdated); +``` + +Using the overload we created a different lambda, in which there are no 2 input arguments, one of which is the date to check against, and the scope of the lambda only uses its parameters, which in turn means that no class has to be allocated for the closure, and instead the compiler will generate the lambda as a static method, the argument would then be forwarded from `Upsert` into the lambda during runtime. Avoiding the performance penalty of allocating a closure class for each call. + ## `ReadOnlySpan` Key Generation `ArrowDb` APIs use `ReadOnlySpan` for keys to minimize unnecessary string allocations. Usually using the API with `Upsert` doesn't require specific logic as string can also be interpreted as `ReadOnlySpan`, however when checking if a key exists or removing keys, usually you don't have pre-existing reference to the key, which means you have to use rather low level APIs to efficiently generate a `ReadOnlySpan` key. @@ -210,12 +236,15 @@ A common code pattern for caching usually consists of some `GetOrAdd` method, th ```csharp async ValueTask GetOrAddAsync(string key, JsonTypeInfo jsonTypeInfo, Func> valueFactory); +async ValueTask GetOrAddAsync(string key, JsonTypeInfo jsonTypeInfo, Func> valueFactory, TArg factoryArgument); ``` If the value exists, the asynchronous factory method is not called, and the value is returned synchronously. Otherwise the factory will produce the value, `Upsert` it, then return it. Since `ArrowDb` was not made specifically to cache, it doesn't store time metadata for values, because of this, there will not be a method that accepts "cache expiration" or similar options in the foreseen future. Such scenarios will need to implemented client-side, best done with a pattern that splits read and write, by called `TryGetValue` which will also check the inner time reference, if false and out of date, will generate the value and use `Upsert`. +Similarly to `Upsert` - `GetOrAddAsync` also has an overload that accepts `TArg` and and enables closure free execution for optimal performance. + ## Encryption As seen earlier, the default recommended serializer is `FileSerializer`, which serializes the db to a file on disk. In addition to it, `ArrowDb` also features a similar serializer that encrypts the db to a file on disk (the `AesFileSerializer`), for that the serializer requires an `Aes` instance to be passed along with the path to the file. diff --git a/src/ArrowDbCore/ArrowDb.GetOrAdd.cs b/src/ArrowDbCore/ArrowDb.GetOrAdd.cs index 2e496a2..138178a 100644 --- a/src/ArrowDbCore/ArrowDb.GetOrAdd.cs +++ b/src/ArrowDbCore/ArrowDb.GetOrAdd.cs @@ -5,7 +5,7 @@ namespace ArrowDbCore; public partial class ArrowDb { /// - /// Tries to retrieve a value stored in the database under , if doesn't exist, it uses the factory to create and add it, then returns it. + /// Tries to retrieve a value stored in the database under , if it doesn't exist, it uses the factory to create and add it, then returns it. /// /// The type of the value to get or add /// The key at which to find or add the value @@ -22,4 +22,25 @@ public async ValueTask GetOrAddAsync(string key, JsonTypeInfo + /// Tries to retrieve a value stored in the database under , if it doesn't exist, it uses the factory to create and add it, then returns it. + /// + /// The type of the value to get or add + /// The type of the argument for the updateCondition function + /// The key at which to find or add the value + /// The json type info for the value type + /// The function used to generate a value for the key + /// An argument that could be provided to the valueFactory function to avoid a closure + /// The value after finding or adding it + /// + /// + public async ValueTask GetOrAddAsync(string key, JsonTypeInfo jsonTypeInfo, Func> valueFactory, TArg factoryArgument) { + if (Lookup.TryGetValue(key, out var source)) { + return JsonSerializer.Deserialize(new ReadOnlySpan(source), jsonTypeInfo)!; + } + var val = await valueFactory(key, factoryArgument); + Upsert(key, val, jsonTypeInfo); + return val; + } } diff --git a/src/ArrowDbCore/ArrowDb.Upsert.cs b/src/ArrowDbCore/ArrowDb.Upsert.cs index 86fdc97..80cbbbb 100644 --- a/src/ArrowDbCore/ArrowDb.Upsert.cs +++ b/src/ArrowDbCore/ArrowDb.Upsert.cs @@ -67,6 +67,32 @@ public bool Upsert(string key, TValue value, JsonTypeInfo jsonTy return Upsert(key, value, jsonTypeInfo); } + /// + /// Tries to upsert the specified key with the specified value into the database + /// + /// The type of the value to upsert + /// The type of the argument for the updateCondition function + /// The key at which to upsert the value + /// The value to upsert + /// The json type info for the value type + /// A conditional check that determines whether this update should be performed + /// An argument that could be provided to the updateCondition function to avoid a closure + /// True if the value was upserted, false otherwise + /// + /// + /// can be used to resolve write conflicts, the update will be rejected only if both conditions are met: + /// + /// 1. A value for the specified key exists and successfully deserialized to + /// 2. on the reference value returns false + /// + public bool Upsert(string key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition, TArg updateConditionArgument) { + if (TryGetValue(key, jsonTypeInfo, out TValue existingReference) && + !updateCondition(existingReference, updateConditionArgument)) { + return false; + } + return Upsert(key, value, jsonTypeInfo); + } + /// /// Tries to upsert the specified key with the specified value into the database /// @@ -93,4 +119,33 @@ public bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo + /// Tries to upsert the specified key with the specified value into the database + /// + /// The type of the value to upsert + /// The type of the argument for the updateCondition function + /// The key at which to upsert the value + /// The value to upsert + /// The json type info for the value type + /// A conditional check that determines whether this update should be performed + /// An argument that could be provided to the updateCondition function to avoid a closure + /// True if the value was upserted, false otherwise + /// + /// + /// can be used to resolve write conflicts, the update will be rejected only if both conditions are met: + /// + /// 1. A value for the specified key exists and successfully deserialized to + /// 2. on the reference value returns false + /// + /// This method overload which uses ReadOnlySpan{char} will not allocate a new string for the key if it already exists, instead it will directly replace the value + /// + /// + public bool Upsert(ReadOnlySpan key, TValue value, JsonTypeInfo jsonTypeInfo, Func updateCondition, TArg updateConditionArgument) { + if (TryGetValue(key, jsonTypeInfo, out TValue existingReference) && + !updateCondition(existingReference, updateConditionArgument)) { + return false; + } + return Upsert(key, value, jsonTypeInfo); + } } diff --git a/src/ArrowDbCore/ArrowDb.cs b/src/ArrowDbCore/ArrowDb.cs index 05a09ad..6217234 100644 --- a/src/ArrowDbCore/ArrowDb.cs +++ b/src/ArrowDbCore/ArrowDb.cs @@ -78,7 +78,7 @@ private ArrowDb(ConcurrentDictionary source, IDbSerializer seria /// ~ArrowDb() { Interlocked.Decrement(ref s_runningInstances); - Semaphore?.Dispose(); + Semaphore.Dispose(); } /// diff --git a/src/ArrowDbCore/ArrowDbCore.csproj b/src/ArrowDbCore/ArrowDbCore.csproj index 20f8a0b..384fff5 100644 --- a/src/ArrowDbCore/ArrowDbCore.csproj +++ b/src/ArrowDbCore/ArrowDbCore.csproj @@ -4,8 +4,9 @@ net9.0 enable enable - 1.3.0.0 + 1.4.0.0 true + true diff --git a/src/ArrowDbCore/ArrowDbJsonContext.cs b/src/ArrowDbCore/ArrowDbJsonContext.cs index 3573c77..f993317 100644 --- a/src/ArrowDbCore/ArrowDbJsonContext.cs +++ b/src/ArrowDbCore/ArrowDbJsonContext.cs @@ -8,4 +8,4 @@ namespace ArrowDbCore; /// [JsonSourceGenerationOptions(WriteIndented = false, AllowTrailingCommas = true, NumberHandling = JsonNumberHandling.AllowReadingFromString, UseStringEnumConverter = true)] [JsonSerializable(typeof(ConcurrentDictionary))] -public partial class ArrowDbJsonContext : JsonSerializerContext {} +public partial class ArrowDbJsonContext : JsonSerializerContext; diff --git a/src/ArrowDbCore/Serializers/AesFileSerializer.cs b/src/ArrowDbCore/Serializers/AesFileSerializer.cs index 3cfdc8a..2d68024 100644 --- a/src/ArrowDbCore/Serializers/AesFileSerializer.cs +++ b/src/ArrowDbCore/Serializers/AesFileSerializer.cs @@ -51,9 +51,9 @@ public ValueTask> DeserializeAsync() { /// public ValueTask SerializeAsync(ConcurrentDictionary data) { using var fileStream = File.Create(_path); - using var encryptor = _aes.CreateEncryptor(); - using var cryptoStream = new CryptoStream(fileStream, encryptor, CryptoStreamMode.Write); - JsonSerializer.Serialize(cryptoStream, data, _jsonTypeInfo); - return ValueTask.CompletedTask; + using var encryptor = _aes.CreateEncryptor(); + using var cryptoStream = new CryptoStream(fileStream, encryptor, CryptoStreamMode.Write); + JsonSerializer.Serialize(cryptoStream, data, _jsonTypeInfo); + return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Analyzers/ArrowDbCore.Tests.Analyzers.csproj b/tests/ArrowDbCore.Tests.Analyzers/ArrowDbCore.Tests.Analyzers.csproj new file mode 100644 index 0000000..965e2df --- /dev/null +++ b/tests/ArrowDbCore.Tests.Analyzers/ArrowDbCore.Tests.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + Exe + net9.0 + enable + enable + true + true + + + + + + + + diff --git a/tests/ArrowDbCore.Tests.Analyzers/Program.cs b/tests/ArrowDbCore.Tests.Analyzers/Program.cs new file mode 100644 index 0000000..837131c --- /dev/null +++ b/tests/ArrowDbCore.Tests.Analyzers/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Unit/GetOrAddAsync.cs b/tests/ArrowDbCore.Tests.Unit/GetOrAddAsync.cs index 7af0365..22f5c9c 100644 --- a/tests/ArrowDbCore.Tests.Unit/GetOrAddAsync.cs +++ b/tests/ArrowDbCore.Tests.Unit/GetOrAddAsync.cs @@ -17,6 +17,22 @@ public async Task GetOrAddAsync_ReturnsSynchronously_WhenExists() { Assert.Equal(1, task.GetAwaiter().GetResult()); } + + [Fact] + public async Task GetOrAddAsync_WithTArg_ReturnsSynchronously_WhenExists() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + db.Upsert("1", 1, JContext.Default.Int32); // add before + // using a static delegate ensures that closure cannot be allocated + var task = db.GetOrAddAsync("1", JContext.Default.Int32, static async (_, value) => { + await Task.Delay(1000); + return value; + }, 1); + Assert.True(task.IsCompletedSuccessfully); + + Assert.Equal(1, task.GetAwaiter().GetResult()); + + } #pragma warning restore xUnit1031 // Do not use blocking task operations in test method [Fact] @@ -31,4 +47,18 @@ public async Task GetOrAddAsync_ReturnsAsynchronously_WhenNotExists() { Assert.False(task.IsCompletedSuccessfully); Assert.Equal(1, await task); } + + [Fact] + public async Task GetOrAddAsync_WithTArg_ReturnsAsynchronously_WhenNotExists() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + // doesn't exist + // using a static delegate ensures that closure cannot be allocated + var task = db.GetOrAddAsync("1", JContext.Default.Int32, static async (_, value) => { + await Task.Delay(1000); + return value; + }, 1); + Assert.False(task.IsCompletedSuccessfully); + Assert.Equal(1, await task); + } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Unit/JContext.cs b/tests/ArrowDbCore.Tests.Unit/JContext.cs index aad06c7..1981d93 100644 --- a/tests/ArrowDbCore.Tests.Unit/JContext.cs +++ b/tests/ArrowDbCore.Tests.Unit/JContext.cs @@ -4,4 +4,4 @@ namespace ArrowDbCore.Tests.Unit; [JsonSourceGenerationOptions(WriteIndented = false, NumberHandling = JsonNumberHandling.AllowReadingFromString, UseStringEnumConverter = true)] [JsonSerializable(typeof(int))] -public partial class JContext : JsonSerializerContext { } \ No newline at end of file +public partial class JContext : JsonSerializerContext; \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Unit/Upserts.Spans.cs b/tests/ArrowDbCore.Tests.Unit/Upserts.Spans.cs index 3f72936..7be1f34 100644 --- a/tests/ArrowDbCore.Tests.Unit/Upserts.Spans.cs +++ b/tests/ArrowDbCore.Tests.Unit/Upserts.Spans.cs @@ -53,4 +53,37 @@ public async Task Conditional_Update_When_Found_And_Invalid_Returns_False() { Assert.True(db.TryGetValue(key, JContext.Default.Int32, out var value)); Assert.Equal(1, value); } + + [Fact] + public async Task Conditional_Update_TArg_When_Not_Found_Inserts() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + ReadOnlySpan key = "1"; + // using a static delegate ensures that closure cannot be allocated + Assert.True(db.Upsert(key, 1, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "1")); + } + + [Fact] + public async Task Conditional_Update_TArg_When_Found_And_Valid_Updates() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + ReadOnlySpan key = "1"; + db.Upsert(key, 1, JContext.Default.Int32); + // using a static delegate ensures that closure cannot be allocated + Assert.True(db.Upsert(key, 3, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "1")); + Assert.True(db.TryGetValue(key, JContext.Default.Int32, out var value)); + Assert.Equal(3, value); + } + + [Fact] + public async Task Conditional_Update_TArg_When_Found_And_Invalid_Returns_False() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + ReadOnlySpan key = "1"; + db.Upsert(key, 1, JContext.Default.Int32); + // using a static delegate ensures that closure cannot be allocated + Assert.False(db.Upsert(key, 3, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "3")); + Assert.True(db.TryGetValue(key, JContext.Default.Int32, out var value)); + Assert.Equal(1, value); + } } \ No newline at end of file diff --git a/tests/ArrowDbCore.Tests.Unit/Upserts.cs b/tests/ArrowDbCore.Tests.Unit/Upserts.cs index 597492d..256497d 100644 --- a/tests/ArrowDbCore.Tests.Unit/Upserts.cs +++ b/tests/ArrowDbCore.Tests.Unit/Upserts.cs @@ -48,4 +48,34 @@ public async Task Conditional_Update_When_Found_And_Invalid_Returns_False() { Assert.True(db.TryGetValue("1", JContext.Default.Int32, out var value)); Assert.Equal(1, value); } + + [Fact] + public async Task Conditional_Update_TArg_When_Not_Found_Inserts() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + // using a static delegate ensures that closure cannot be allocated + Assert.True(db.Upsert("1", 1, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "1")); + } + + [Fact] + public async Task Conditional_Update_TArg_When_Found_And_Valid_Updates() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + db.Upsert("1", 1, JContext.Default.Int32); + // using a static delegate ensures that closure cannot be allocated + Assert.True(db.Upsert("1", 3, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "1")); + Assert.True(db.TryGetValue("1", JContext.Default.Int32, out var value)); + Assert.Equal(3, value); + } + + [Fact] + public async Task Conditional_Update_TArg_When_Found_And_Invalid_Returns_False() { + var db = await ArrowDb.CreateInMemory(); + Assert.Equal(0, db.Count); + db.Upsert("1", 1, JContext.Default.Int32); + // using a static delegate ensures that closure cannot be allocated + Assert.False(db.Upsert("1", 3, JContext.Default.Int32, static (reference, digit) => reference == int.Parse(digit), "3")); + Assert.True(db.TryGetValue("1", JContext.Default.Int32, out var value)); + Assert.Equal(1, value); + } } \ No newline at end of file