From cec09806b007af656b0f95bf07c127ad56946ebc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 12 Oct 2026 05:06:19 +0200 Subject: [PATCH] Caching: an empty direct flush still applies as before; only a write that checked keys and has nothing else to write writes no file. Tests: the delisting writer declares the widget; the checked-key flush also writes Co-Authored-By: Claude Sonnet 5.5 --- src/GameCult.Caching/CultCache.cs | 4 ++-- tests/GameCult.Caching.Tests/ForeignRecordTests.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index ede033dc..dd88c346 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -4164,7 +4164,7 @@ namespace GameCult.Caching // Flush, commit and Mesh's write all end here, with the durable store they read under the lock they hold. First, a key the write // names or checks whose durable record is under an id the writer does not declare refuses the write (RefuseUndeclaredOnDisk); a - // write that then has nothing to write (it only checked keys) writes no file. Then a store that holds + // write that then has nothing to write but keys it checked writes no file. Then a store that holds // a variant, or would after this write, is written only while its record versions are what this store last read: a variant // resolves against other records, and a write decodes none it did not stage, so it cannot judge another writer's. Versions // only: a write hashes other records' bytes, never decodes them. Otherwise the whole write is refused and what it refused is @@ -4175,7 +4175,7 @@ namespace GameCult.Caching Durable? durable, IReadOnlyCollection upserts, IReadOnlyCollection removals, IEnumerable checks) { RefuseUndeclaredOnDisk(durable, upserts, removals, checks); - if (upserts.Count == 0 && removals.Count == 0) + if (upserts.Count == 0 && removals.Count == 0 && (checks.Any() || StagedChecks.Count > 0)) return Array.Empty<(CultStagedRecord, string)>(); RefuseWriteIntoMovedVariantStore(durable, upserts, removals); var durableRecords = durable?.Snapshot.Records ?? Array.Empty(); diff --git a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs index 63835ee4..10742ad0 100644 --- a/tests/GameCult.Caching.Tests/ForeignRecordTests.cs +++ b/tests/GameCult.Caching.Tests/ForeignRecordTests.cs @@ -1125,6 +1125,7 @@ namespace GameCult.Caching.Tests using (var seed = Open(path, Full, directory)) seed.Commit(batch => batch.Upsert(Deck, DeckOf("d"), D)); using var a = Open(path, DeckOnly, directory); + a.UpsertAsync(Deck, DeckOf("e"), E).GetAwaiter().GetResult(); Assert.That(a.Remove(W), Is.False); using (var b = Open(path, Full, directory)) b.Commit(batch => batch.Upsert(Deck, DeckOf("theirs"), W)); @@ -1132,6 +1133,7 @@ namespace GameCult.Caching.Tests Assert.DoesNotThrow(() => a.FlushAsync().GetAwaiter().GetResult()); + Assert.That(Read(path).Records.Select(record => record.Key), Is.EquivalentTo(new[] { "d", "e", "w" }), "the staged write landed beside the checked key"); Assert.That(Stored(path, W), Is.EqualTo(theirs), "the record at the checked key is still the other writer's bytes"); using (var c = Open(path, Full, directory)) c.Commit(batch => batch.Upsert(Widget, WidgetOf("now a widget"), W)); @@ -1173,7 +1175,9 @@ namespace GameCult.Caching.Tests var path = Seeded("delist.cc", directory); using var a = Open(path, DeckOnly, directory); Assert.That(StoreOf(a).ForeignRecords.Select(record => record.Key), Is.EqualTo(new[] { "w" })); - Move(path, W, "moved", directory); + // The writer declares the widget's id, so it may replace the widget with a deck record under an id a reads but does not declare. + using (var other = Open(path, CultDocumentRegistry.ForTypes(new[] { MovingDeck, Widget }), directory)) + Assert.That(other.Commit(batch => batch.Upsert(MovingDeck, New(MovingDeck, ("Name", "moved")), W)), Is.True); Assert.That(a.Remove(W), Is.False); var refusal = Assert.Throws(() => a.FlushAsync().GetAwaiter().GetResult())!;