C# store: delete lay-back, WholeView, SameStoredBytes and the whole-view lay-back tests and contract text (does not build)

_unowned with its rebuild, Owns, Publisher, WholeViewWrite, WholeView, SameStoredBytes and
WroteWholeView are deleted before the write-set apply that replaces them; PushAll and
CommitBatchCore still call them, so this commit does not build. The contract states the
write-set rule that the next commit implements.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 03:01:20 +02:00
commit 60735753dd
5 changed files with 42 additions and 358 deletions

View file

@ -52,14 +52,22 @@ under the store's exclusive lock against what is durably on disk. A failed
condition is a lost race, not an error: the commit reports it and writes
nothing.
**Conditional commit is the only safe multi-process write.** A plain flush and
an unconditional commit are last-writer-wins and write the same bytes: a single
file is replaced by the writer's whole view, a directory manifest takes the
writer's staged keys over the current manifest. An unconditional commit also
persists writes staged earlier and leaves the store clean. Processes sharing a
store must all use conditional commit. The last writer wins only over a file it
can read: a single-file flush or commit refuses a file whose header it cannot
read (truncated, or a format it does not read) and leaves the file as it was.
**A flush, a commit and a single-file document write apply a write set.** Under
the store's exclusive lock the writer reads what is durably in the store now,
applies only what it staged (writes and removals) and, for a commit, its batch,
and replaces the store with the result. Every other record, and every catalog
entry the result references, is copied forward as stored: key, schema id,
storedAt, payload and variant, and a catalog entry's exact bytes, unknown
fields included. No type interprets a copied record. A plain flush and an
unconditional commit write the same bytes and are last-writer-wins only per
staged key; a document changed in place and not staged is not written. An
unconditional commit also persists writes staged earlier and leaves the store
clean. Conditional commit is how a writer learns that another writer moved the
keys it depends on. A writer refuses a file whose header it cannot read
(truncated, or a format it does not read) and leaves the file as it was. C#
applies this to single-file stores, directory stores and CultMesh's single-file
document writes; Rust, TypeScript and Python rewrite the store from their own copy until their
parity cuts land.
A store that fails to hydrate on open (corrupt bytes, unresolvable schema)
makes the open throw and is left byte-identical; it is never silently
@ -142,33 +150,29 @@ names a local type only when no local type has the id, for a store written by a
schema ids this one cannot know (a Rust type's schema id is its entry type). A schema renamed under
a stable id therefore opens in every runtime that holds the id.
Records and catalog entries a runtime does not own survive its writes. A record a cache resolved
through the catalog's schema name, or through an entry pointing at a local id, is readable, but its
id is not one its type owns or declares compatible: a write of another record, a whole-view write
included, lays it back with its id, storedAt, bytes and catalog entry exactly as they were stored.
Only a write of that record itself stores it under the type's own id. A writer that describes
schema ids itself (Rust, whose catalog entries are derived from its entry types) lays back the entry
the file holds for an id it rewrites, and describes an id only when the file has no entry for it or
when its registered type owns the id under another name (a rename).
A write copies forward what it did not stage. A record a cache resolved through the catalog's schema
name, or through an entry pointing at a local id, is readable, but its id is not one its type owns
or declares compatible: a write of another record copies it with its id, storedAt, bytes and
catalog entry exactly as they were stored. Only a write of that record itself stores it under the
type's own id.
A record that resolves to no local type is a foreign record, for example one of a type this build does not
have, or one renamed without declaring its old id. A store never destroys or relabels it: the store carries it
byte for byte under its own id and its own catalog entry, lists it as foreign (`CacheBackingStore.ForeignRecords`
in C#), and the cache never holds it. Writes of other records proceed and lay it back as the file holds it, a
whole-view write included. A write that would replace or remove it is refused with the typed schema conflict
naming its key and id, and nothing is written. A marked header stays marked while a store carries one, because
the writer cannot see whether it holds element ids. Declaring its id on a type claims it at the next load.
in C#), and the cache never holds it. Writes of other records proceed and copy it forward as the file holds it. A write that would replace or remove it is refused with the typed schema conflict
naming its key and id, and nothing is written. A marked header stays marked while a store carries a record the writer cannot show to hold no element id. Declaring its id on a type claims it at the next load.
A reader resolves a record's schema id to the entry that owns it, that entry's own `schemaId`, and
only when no entry owns the id to an entry that lists it as a compatible id (the first such entry
in catalog order).
A writer derives the catalog from the records it writes, and from nothing else. For each distinct
schema id a record carries it chooses exactly one entry that publishes it: the entry that owns the id
(a registered schema's descriptor, else an entry the record arrived with), else an entry that lists
it as compatible (again registered first). When a registered descriptor and an arrived entry share an
own id, the descriptor wins: a rename or a stale content hash takes the descriptor's name, hash,
members and compatible ids as they are. Entries are written as chosen, deduplicated by own id, with
schema id a record carries it chooses exactly one entry that publishes it. For an id the write stages
a record under, that is the entry the writer registered or passed, and a rename or a stale content hash
takes the descriptor's name, hash, members and compatible ids as they are. For every other id it is
the durable entry that owns the id, else one that lists it as compatible, carried as its exact bytes,
unknown trailing fields included. A registered entry never publishes an id the write stages nothing
under. Entries are written as chosen, deduplicated by own id, with
no union of compatible ids and no dependence on the order records arrive in; an entry no record needs
is not written. A write is refused, and the store left as it was, when two entries of one tier share
an own id and disagree on the schema name, when records of different types share a schema id, when a
@ -522,8 +526,9 @@ The v1 concurrent single-file policy is:
3. Writers re-read the current snapshot after taking the lock.
4. Writers using conditional commit evaluate their conditions against the
latest snapshot and write nothing if one fails. A plain flush and an
unconditional commit compare nothing and are last-writer-wins over a file
they can read; one whose header they cannot read is refused untouched.
unconditional commit compare nothing: they apply their staged writes onto the
current snapshot, last-writer-wins per staged key; one whose header they
cannot read is refused untouched.
5. Writers write a temp file, flush it, atomically replace the `.cc` file, and
release the lock.

View file

@ -248,19 +248,16 @@ replaces; writers bump a minted timestamp by one tick when it is not later.
match its content address, at most five times; a pull that has not settled
by then throws.
**A plain flush and an unconditional commit are last-writer-wins: per file for
single-file stores, per key for directory stores.** Both run
under the same lock, so two writers never interleave bytes, but they compare
nothing. Only a conditional commit (`Expect` or `ExpectUnchanged`) protects
against another writer; processes sharing a store must all use it. A writer
wins only over a file it can read: a single-file store refuses to replace a file
whose header it cannot read, and leaves it as it was.
**A plain flush and an unconditional commit are last-writer-wins per staged
key, in both store kinds.** Both run under the same lock, so two writers never
interleave bytes, but they compare nothing. Only a conditional commit
(`Expect` or `ExpectUnchanged`) tells a writer another writer moved the keys it
depends on. A writer wins only over a file it can read: a single-file store
refuses to replace a file whose header it cannot read, and leaves it as it was.
- A single-file store writes this cache's whole view of the file: a record
another writer added since this cache last pulled is gone.
- A directory store writes this cache's staged keys onto the current manifest:
another writer's unrelated keys survive, a key both wrote holds the last
write.
- A single-file store and a directory store apply this cache's staged writes and
removals onto what the store holds now: another writer's unrelated records
survive as stored, a key both wrote holds the last write.
- An unconditional commit writes exactly what a flush of the same staged state
plus the batch would write. It also persists any single writes staged earlier
in that store, and leaves the store clean.

View file

@ -3701,37 +3701,7 @@ namespace GameCult.Caching
Entries[stored.Key.Value] = stored;
SetLastSchemaMigrationReports(reports);
SetForeignRecords(foreign);
_unowned.Clear();
foreach (var record in snapshot.Records)
{
if (!Entries.TryGetValue(record.Key, out var held) || Owns(held.Descriptor, record.SchemaId))
continue;
// An entry under the type's own id that lists the record's id says the record is this schema's: it is re-encoded.
var publisher = Publisher(record.SchemaId, snapshot.SchemaCatalog);
if (!Owns(held.Descriptor, publisher.SchemaId))
_unowned[record.Key] = (held, record, publisher, held.Variant == null ? SerializePayload(held.Document) : null);
}
}
// The records this store loaded under an id their type neither owns nor lists as compatible (another runtime's id, or an
// earlier declaration of the schema, resolved through the catalog's name or its pointer to a local id), each with the
// catalog entry that published it, keyed while the store holds the entry it loaded. A whole-view write lays such a record
// back exactly as it was stored, id and catalog entry included; only a write of the record itself stores it under its
// type's own id. Payload is what a plain document serialized to when it was loaded, so a change made to the document in
// place is told from a record nobody wrote (a variant persists its delta, never its resolved document).
private readonly Dictionary<string, (CultStoredDocument Loaded, CultPersistedRecord Record, CultSchemaCatalogEntry Entry, byte[]? Payload)> _unowned =
new(StringComparer.Ordinal);
private static bool Owns(CultDocumentDescriptor descriptor, string schemaId) =>
string.Equals(descriptor.SchemaId, schemaId, StringComparison.Ordinal) ||
descriptor.CompatibleSchemaIds.Contains(schemaId, StringComparer.Ordinal);
// The entry that publishes a schema id: the one that owns it, else the first that lists it.
private static CultSchemaCatalogEntry Publisher(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog) =>
catalog.FirstOrDefault(entry => string.Equals(entry.SchemaId, schemaId, StringComparison.Ordinal))
?? catalog.First(entry => entry.CompatibleSchemaIds.Contains(schemaId, StringComparer.Ordinal));
public override void Push(CultStoredDocument entry)
{
ThrowIfReadOnly();
@ -3839,22 +3809,6 @@ namespace GameCult.Caching
return CultCommitOutcome.Committed;
}
// A record's (schema id, storedAt) names its stored bytes, and a commit condition trusts that name. So a whole-view write
// that stores a record differently is a new store of it and mints it a later storedAt: under another id than it was loaded
// under (a type re-encoding a record it read under an older id it declares), or with other
// bytes than the file holds at that storedAt. A record written exactly as it is stored keeps its storedAt.
// Records and catalog entries this runtime does not own survive the write: a record loaded under an id its type does not own
// and that nothing has written since is laid back as it was loaded, with the catalog entry that published it, and every
// foreign record the file holds now is laid back as the file holds it, over this cache's clean copy of it; a write this cache staged over one
// was refused before this. Arrived is the catalog the write publishes those records by.
private sealed class WholeViewWrite
{
public CultPersistedRecord[] Records = Array.Empty<CultPersistedRecord>();
public CultSchemaCatalogEntry[] Arrived = Array.Empty<CultSchemaCatalogEntry>();
// Keys written as the file holds or held them, not as this cache holds them.
public HashSet<string> LaidBack = new(StringComparer.Ordinal);
}
private void RefuseForeign(
IEnumerable<string> written,
IReadOnlyDictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> foreign) =>
@ -3867,51 +3821,6 @@ namespace GameCult.Caching
IsDirty = _staged.Count > 0;
});
private WholeViewWrite WholeView(
IEnumerable<CultStoredDocument> view,
CultPersistedStoreSnapshot? disk,
IReadOnlyDictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> foreign)
{
// A record the file holds as foreign is the file's: it is carried, and this cache's clean copy of it is not written.
var entries = view.Where(entry => !foreign.ContainsKey(entry.Key.Value)).ToArray();
var write = new WholeViewWrite();
write.LaidBack.UnionWith(foreign.Keys);
var laidBackEntries = new List<CultSchemaCatalogEntry>();
var onDisk = (disk?.Records ?? Array.Empty<CultPersistedRecord>()).ToDictionary(record => record.Key, StringComparer.Ordinal);
write.Records = entries.Select(entry =>
{
if (_unowned.TryGetValue(entry.Key.Value, out var loaded) && ReferenceEquals(loaded.Loaded, entry) &&
(loaded.Payload == null || SerializePayload(entry.Document).AsSpan().SequenceEqual(loaded.Payload)))
{
// Laid back only while the file still holds what was loaded: after that the file's record is its owner's newer
// write, which stays as it is, and a record the file no longer holds is not written back.
write.LaidBack.Add(entry.Key.Value);
if (!onDisk.TryGetValue(entry.Key.Value, out var current))
return null;
if (current.SchemaId != loaded.Record.SchemaId || current.StoredAt != loaded.Record.StoredAt)
return current;
laidBackEntries.Add(loaded.Entry);
return loaded.Record;
}
var record = ToPersistedRecord(entry, SerializePayload);
var unchanged = onDisk.TryGetValue(record.Key, out var stored) && stored.StoredAt == record.StoredAt
? SameStoredBytes(stored, record)
: entry.StoredSchemaId == record.SchemaId;
if (!unchanged)
record.StoredAt = CultCache.MintStoredAt(record.StoredAt);
return record;
}).OfType<CultPersistedRecord>().Concat(foreign.Values.Select(carried => carried.Record)).ToArray();
// An entry that published a laid-back record is written as it was loaded, whatever the file's entry for that id says now.
var laidBackIds = laidBackEntries.Select(entry => entry.SchemaId).ToHashSet(StringComparer.Ordinal);
write.Arrived = laidBackEntries.Distinct()
.Concat((disk?.SchemaCatalog ?? Array.Empty<CultSchemaCatalogEntry>()).Where(entry => !laidBackIds.Contains(entry.SchemaId)))
.ToArray();
return write;
}
// The records the file holds that no registered type claims, by key.
private Dictionary<string, (CultForeignRecord Foreign, CultPersistedRecord Record)> ForeignOnDisk(CultPersistedStoreSnapshot? disk)
{
@ -3925,36 +3834,6 @@ namespace GameCult.Caching
return foreign;
}
private static bool SameStoredBytes(CultPersistedRecord stored, CultPersistedRecord written) =>
stored.SchemaId == written.SchemaId &&
stored.Payload.AsSpan().SequenceEqual(written.Payload) &&
(stored.Variant == null
? written.Variant == null
: written.Variant != null &&
stored.Variant.BaseKey == written.Variant.BaseKey &&
stored.Variant.Overrides.Count == written.Variant.Overrides.Count &&
stored.Variant.Overrides.Zip(written.Variant.Overrides, (a, b) =>
a.Op == b.Op && a.Id == b.Id && a.Value.AsSpan().SequenceEqual(b.Value) &&
a.Path.Select(step => (step.Slot, step.ElementId)).SequenceEqual(b.Path.Select(step => (step.Slot, step.ElementId))))
.All(same => same));
// The file now holds every record written, under the id and at the storedAt the write gave it: the store's entries and the
// cache's say so. A laid-back or carried record is on disk as the file had it, so the cache learns nothing new about it: in
// particular the ids it minted at load are still in memory only.
private void WroteWholeView(IReadOnlyCollection<CultPersistedRecord> written, HashSet<string> laidBack)
{
var asHeld = written.Where(record => !laidBack.Contains(record.Key)).ToArray();
foreach (var record in asHeld)
{
if (!Entries.TryGetValue(record.Key, out var entry))
continue;
entry.StoredSchemaId = record.SchemaId;
entry.StoredAt = record.StoredAt;
}
Cache?.WroteWholeView(asHeld);
}
// Only a variant makes a merge more than the committer's own judgement: a variant it lands, one another writer wrote
// (read from the record's header, never its payload), or one this cache holds. A plain-only store never decodes
// another writer's records, so a schema this cache does not register cannot fail a commit that involves no variant.

View file

@ -212,26 +212,6 @@ namespace GameCult.Caching.Tests
Assert.That(reader.MintElementIds(), Is.EqualTo(0));
}
// A record from before ids, under another runtime's id for the same schema, is laid back by a whole-view write exactly as it
// was stored. The ids it minted at load are still in memory only, so MintElementIds still rewrites it, under the type's own id.
[Test]
public void AWholeViewWriteLeavesTheLoadMintedIdsOfARecordItLaysBackInMemoryOnly()
{
var path = PathOf("unowned-pre-id.cc");
WritePreIdStore(path, null, ("old", "o", 1));
var ownId = DiskRecord(path, "old").SchemaId;
RelabelAsOtherRuntime(path);
var stored = DiskRecord(path, "old");
using var cache = Open(path);
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var laidBack = DiskRecord(path, "old");
Assert.That((laidBack.SchemaId, laidBack.StoredAt, laidBack.Payload), Is.EqualTo((stored.SchemaId, stored.StoredAt, stored.Payload)));
Assert.That(cache.MintElementIds(), Is.EqualTo(1), "its ids are still in memory only");
Assert.That(DiskRecord(path, "old").SchemaId, Is.EqualTo(ownId), "a write of the record stores it under the type's own id");
}
// The store's one record, stored as another runtime stores that schema: under an id the type does not own, published by a
// catalog entry of its own.
private static void RelabelAsOtherRuntime(string path)
@ -252,147 +232,6 @@ namespace GameCult.Caching.Tests
return path;
}
// The record's owner wrote it after this cache loaded it: the file holds the owner's newer record, and this cache's write of
// another record leaves it as it is, storedAt included (a record's storedAt never moves backwards).
[Test]
public void AWholeViewWriteDoesNotRevertARecordItsOwnerWroteAfterTheLoad([Values] bool flush)
{
var path = UnownedStore("owner-wrote.cc");
using var cache = Open(path);
var loadedAt = DiskRecord(path, "old").StoredAt;
using (var owner = Open(path))
owner.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("owner"), new CultRecordKey("old")));
var owned = DiskRecord(path, "old");
if (flush)
{
cache.UpsertAsync(typeof(IdDeck), Deck("other"), new CultRecordKey("other")).GetAwaiter().GetResult();
cache.FlushAsync().GetAwaiter().GetResult();
}
else
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var after = DiskRecord(path, "old");
Assert.That((after.SchemaId, after.StoredAt, after.Payload), Is.EqualTo((owned.SchemaId, owned.StoredAt, owned.Payload)));
Assert.That(string.CompareOrdinal(after.StoredAt, loadedAt), Is.GreaterThan(0));
Assert.That(MessagePackSerializer.Deserialize<IdDeck>(after.Payload).Name, Is.EqualTo("owner"));
}
// The owner removed it: this cache's stale copy is not written back.
[Test]
public void AWholeViewWriteDoesNotWriteBackARecordItsOwnerRemovedAfterTheLoad()
{
var path = UnownedStore("owner-removed.cc");
using var cache = Open(path);
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
snapshot.Records = Array.Empty<CultPersistedRecord>();
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Select(record => record.Key), Is.EqualTo(new[] { "other" }));
}
// The file's record counts as moved when either half of its name (schema id, storedAt) is no longer the loaded one.
[TestCase(true, false)]
[TestCase(false, true)]
public void AWholeViewWriteCarriesTheFilesRecordWhenOnlyItsStoredAtOrOnlyItsIdMoved(bool newerStoredAt, bool ownersId)
{
var path = UnownedStore("moved.cc");
using var cache = Open(path);
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
var moved = snapshot.Records.Single();
if (newerStoredAt)
moved.StoredAt = DateTimeOffset.UtcNow.AddMinutes(5).ToString("O");
if (ownersId)
{
var owner = Registry.GetRequired(typeof(IdDeck));
moved.SchemaId = owner.SchemaId;
snapshot.SchemaCatalog = snapshot.SchemaCatalog.Append(owner.ToCatalogEntry()).ToArray();
}
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var after = DiskRecord(path, "old");
Assert.That((after.SchemaId, after.StoredAt), Is.EqualTo((moved.SchemaId, moved.StoredAt)));
}
// A record that carried no ids when it was laid back does not mark the store, though the ids it minted at load are in memory.
[Test]
public void ALaidBackRecordWithoutIdsDoesNotMarkTheStore()
{
var path = UnownedStore("laid-back-v1.cc");
using var cache = Open(path);
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("empty", reels: 0), new CultRecordKey("empty")));
Assert.That(DiskRecord(path, "old").SchemaId, Is.EqualTo("other.runtime.deck"), "laid back");
Assert.That(CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).FormatVersion, Is.EqualTo(CultPersistedStoreSnapshot.FormatV1));
}
// A change made to a laid-back document in place is a write of the record, as it is for a record the type owns: it is stored
// under the type's own id, not dropped.
[Test]
public void AnInPlaceChangeToALaidBackDocumentIsWritten()
{
var path = UnownedStore("in-place.cc");
using var cache = Open(path);
cache.Get<IdDeck>(new CultRecordKey("old"))!.Name = "changed";
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var written = DiskRecord(path, "old");
Assert.That(written.SchemaId, Is.Not.EqualTo("other.runtime.deck"));
Assert.That(MessagePackSerializer.Deserialize<IdDeck>(written.Payload).Name, Is.EqualTo("changed"));
}
// The catalog entry that published a laid-back record is written as it was loaded, whatever the file's entry for the id says now.
[Test]
public void ALaidBackRecordIsPublishedByTheCatalogEntryItWasLoadedWith()
{
var path = UnownedStore("laid-back-entry.cc");
using var cache = Open(path);
var loadedHash = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).SchemaCatalog.Single().ContentHash;
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
snapshot.SchemaCatalog.Single().ContentHash = "changed-by-another-writer";
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var published = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).SchemaCatalog
.Where(entry => entry.SchemaId == "other.runtime.deck").ToArray();
Assert.That(published, Has.Length.EqualTo(1));
Assert.That(published[0].ContentHash, Is.EqualTo(loadedHash));
}
// A whole-view write that persists the ids a record minted at load stores other bytes under the same id: a new store of
// the record, so it takes a later storedAt, and the cache that wrote it holds that storedAt. A record it stores exactly as
// the file holds it keeps its own.
[Test]
public void AWholeViewWriteThatPersistsLoadMintedIdsGivesTheRecordALaterStoredAt()
{
var path = PathOf("restamp.cc");
WritePreIdStore(path, "kept", ("old", "o", 1));
var old = DiskRecord(path, "old");
var kept = DiskRecord(path, "kept");
using var cache = Open(path);
var current = cache.Get<IdDeck>(new CultRecordKey("old"))!;
cache.Commit(batch => batch.Upsert(typeof(IdDeck), Deck("other"), new CultRecordKey("other")));
var written = DiskRecord(path, "old");
Assert.That(written.SchemaId, Is.EqualTo(old.SchemaId), "the same id");
Assert.That(written.Payload, Is.Not.EqualTo(old.Payload), "other bytes: the ids are on disk now");
Assert.That(string.CompareOrdinal(written.StoredAt, old.StoredAt), Is.GreaterThan(0), "so a later storedAt");
Assert.That(DiskRecord(path, "kept").StoredAt, Is.EqualTo(kept.StoredAt), "a record stored unchanged keeps its storedAt");
Assert.That(cache.TryCommit(batch =>
{
batch.Expect(new CultRecordKey("old"), current);
batch.Upsert(typeof(IdDeck), Deck("after"), new CultRecordKey("after"));
}), Is.EqualTo(CultCommitOutcome.Committed), "the cache holds the storedAt it wrote");
}
[Test]
public void TheByteCostOfAnIdIsMeasured()
{

View file

@ -603,23 +603,6 @@ namespace GameCult.Caching.Tests
return path;
}
// The record and the entry that described it when it loaded travel together: another writer re-describing the id since
// does not change what a whole view lays back.
[Test]
public void AWholeViewLaysBackTheCatalogEntryItLoadedWhateverTheFileSaysNow([Values(Write.Flush, Write.Commit)] Write write)
{
var path = OtherRuntimeStore("redescribed.cc");
var loaded = CatalogEntryBytes(path, OtherRuntimeId);
using var cache = Open(path, DeckOnly, directory: false);
var snapshot = Read(path);
snapshot.SchemaCatalog.Single(entry => entry.SchemaId == OtherRuntimeId).SchemaVersion = "tests.foreign_deck.redescribed";
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
Land(cache, write, E, DeckOf("e"));
Assert.That(CatalogEntryBytes(path, OtherRuntimeId), Is.EqualTo(loaded));
}
// A store whose file is gone holds nothing, foreign records included.
[Test]
public void AStoreWhoseFileIsGoneListsNoForeignRecords()
@ -651,25 +634,6 @@ namespace GameCult.Caching.Tests
SchemaCatalog = Read(path).SchemaCatalog.Where(entry => entry.SchemaId == schemaId).ToArray()
});
// Records and catalog entries a runtime does not own survive its writes: a write of another record lays the record back with
// its id, storedAt, bytes and catalog entry as they were stored.
[Test]
public void AWriteOfAnotherRecordLaysBackARecordUnderAnIdThisBuildDoesNotOwn([Values] Write write)
{
var path = OtherRuntimeStore("other-runtime.cc");
var before = Stored(path, D);
var entry = CatalogEntryBytes(path, OtherRuntimeId);
using (var cache = Open(path, DeckOnly, directory: false))
{
Assert.That(EmittedDocumentTypes.Read(cache.Get(D)!, "Name"), Is.EqualTo("d"), "this build reads it");
Land(cache, write, E, DeckOf("e"));
}
Assert.That(Stored(path, D), Is.EqualTo(before));
Assert.That(CatalogEntryBytes(path, OtherRuntimeId), Is.EqualTo(entry));
}
// A write of the record itself stores what the cache holds, under the type's own id.
[Test]
public void AWriteOfTheRecordItselfStoresItUnderTheTypesOwnId([Values(Write.Flush, Write.Commit)] Write write)