Caching: one claimant precedence; a cacheless writer picks the claimant its registry picks
CultDocumentRegistry.Claimant is now the only precedence that decides which entry reads a persisted catalog entry, static over a lookup (a registry's indexes, or the entries a registry-less cacheless write writes). ClaimantEntry is deleted. A cacheless WriteDocument or WriteRecord lent a registry is never more permissive than a cache registering the same types; an unpublished id is carried, not owned, through Owns' catch. Fixes the four Low findings of variants:verdict:cut-restamp-on-write.s2. Not yet verified on Yggdrasil when committed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0e57bf113e
commit
bf29ddb33d
3 changed files with 168 additions and 60 deletions
|
|
@ -206,10 +206,10 @@ The rule is checked again where the write lands, under the store's lock: a write
|
|||
whose durable record is not owned, because another writer rewrote it after this cache
|
||||
loaded, is refused typed, and the record is reloaded as the store holds it, read-only when a type can
|
||||
read it. This holds for every write through the store, the store's own `Push`, `Delete` and
|
||||
`CommitBatch` included. A CultMesh single-file write asks the same rule with the one catalog entry it writes in
|
||||
place of a registry: it owns that entry's own id and the ids it lists compatible, and an id whose publishing
|
||||
entry that entry reads whole (the entry lists the writer's id compatible, or has its schema name, and leaves no
|
||||
persisted slot unread); it is refused the same way otherwise.
|
||||
`CommitBatch` included. A CultMesh single-file write asks the same rule: it owns its entry's own id and the ids that entry lists
|
||||
compatible, and an id whose publishing entry its claimant reads whole (no persisted slot unread). The claimant is
|
||||
the one a cache with the call's registry would pick, so a cacheless write is never more permissive than a cache;
|
||||
with no registry the call's one entry is the claimant. It is refused the same way otherwise.
|
||||
|
||||
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 under a new schema name without declaring its old id. A store never destroys or relabels it: the store carries it
|
||||
|
|
|
|||
|
|
@ -569,6 +569,38 @@ namespace GameCult.Caching
|
|||
new(Key, StoredAt, Descriptor, delta, document, codec) { IdsInMemoryOnly = idsInMemoryOnly, StoredSchemaId = StoredSchemaId, StoredVersion = StoredVersion };
|
||||
}
|
||||
|
||||
// What the one claimant precedence (CultDocumentRegistry.Claimant) asks of the entries a reader holds: the entry owning an id,
|
||||
// the entries listing an id compatible, the entries of a schema name, and how to name one in a refusal.
|
||||
internal interface IClaimantLookup<T> where T : class
|
||||
{
|
||||
T? Owner(string schemaId);
|
||||
IReadOnlyList<T> Listing(string schemaId);
|
||||
IReadOnlyList<T> Named(string schemaName);
|
||||
ClaimantFacts Describe(T claimant);
|
||||
}
|
||||
|
||||
internal readonly record struct ClaimantFacts(string SchemaName, string SchemaVersion, string TypeLabel);
|
||||
|
||||
// The lookup of a cacheless writer without a registry: the catalog entries the call writes.
|
||||
internal sealed class CatalogEntryLookup : IClaimantLookup<CultSchemaCatalogEntry>
|
||||
{
|
||||
private readonly IReadOnlyCollection<CultSchemaCatalogEntry> _entries;
|
||||
|
||||
public CatalogEntryLookup(IReadOnlyCollection<CultSchemaCatalogEntry> entries) => _entries = entries;
|
||||
|
||||
public CultSchemaCatalogEntry? Owner(string schemaId) =>
|
||||
_entries.FirstOrDefault(entry => string.Equals(entry.SchemaId, schemaId, StringComparison.Ordinal));
|
||||
|
||||
public IReadOnlyList<CultSchemaCatalogEntry> Listing(string schemaId) =>
|
||||
_entries.Where(entry => entry.CompatibleSchemaIds.Contains(schemaId, StringComparer.Ordinal)).ToArray();
|
||||
|
||||
public IReadOnlyList<CultSchemaCatalogEntry> Named(string schemaName) =>
|
||||
_entries.Where(entry => string.Equals(entry.SchemaName, schemaName, StringComparison.Ordinal)).ToArray();
|
||||
|
||||
public ClaimantFacts Describe(CultSchemaCatalogEntry claimant) =>
|
||||
new(claimant.SchemaName, claimant.SchemaVersion, claimant.SchemaId);
|
||||
}
|
||||
|
||||
public sealed class CultDocumentRegistry
|
||||
{
|
||||
private static readonly Lazy<CultDocumentRegistry> SharedRegistry =
|
||||
|
|
@ -703,11 +735,21 @@ namespace GameCult.Caching
|
|||
internal CultDocumentDescriptor? Claimant(
|
||||
string schemaId,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> catalog,
|
||||
out CultSchemaCatalogEntry? persisted) =>
|
||||
Claimant(schemaId, catalog, _indexes, out persisted);
|
||||
|
||||
// The only claimant precedence: which entry reads a persisted catalog entry, over a lookup of the entries a reader holds (a
|
||||
// registry's types, or the entries a cacheless writer writes). Exact id, then the one entry listing the id, then a local id the
|
||||
// persisted entry lists, then the schema name. Every caller, cache or cacheless, asks it; none writes a precedence of its own.
|
||||
internal static T? Claimant<T>(
|
||||
string schemaId,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> catalog,
|
||||
IClaimantLookup<T> lookup,
|
||||
out CultSchemaCatalogEntry? persisted)
|
||||
where T : class
|
||||
{
|
||||
var indexes = _indexes;
|
||||
persisted = null;
|
||||
if (indexes.BySchemaId.TryGetValue(schemaId, out var exact))
|
||||
if (lookup.Owner(schemaId) is { } exact)
|
||||
return exact;
|
||||
|
||||
persisted = PublishedBy(schemaId, catalog);
|
||||
|
|
@ -717,33 +759,35 @@ namespace GameCult.Caching
|
|||
}
|
||||
|
||||
// No type owns the id, so the one type that lists it is the record's type. Two listers and no owner cannot say which.
|
||||
if (indexes.ByCompatibleSchemaId.TryGetValue(schemaId, out var listers))
|
||||
var listers = lookup.Listing(schemaId);
|
||||
if (listers.Count > 0)
|
||||
{
|
||||
if (listers.Length > 1)
|
||||
if (listers.Count > 1)
|
||||
throw new CultSchemaConflictException(
|
||||
$"CultCache schema id '{schemaId}' is owned by no registered type and declared compatible by several " +
|
||||
$"({string.Join(", ", listers.Select(lister => lister.DocumentType.FullName))}), so a record under it resolves to none. " +
|
||||
$"({string.Join(", ", listers.Select(lister => lookup.Describe(lister).TypeLabel))}), so a record under it resolves to none. " +
|
||||
"Register the type that owns the id, or declare it on one type.",
|
||||
schemaId,
|
||||
listers.Select(lister => lister.SchemaName).ToArray(),
|
||||
listers.Select(lister => lookup.Describe(lister).SchemaName).ToArray(),
|
||||
string.Empty);
|
||||
return listers[0];
|
||||
}
|
||||
|
||||
foreach (var compatibleSchemaId in persisted.CompatibleSchemaIds.Where(candidate => !string.IsNullOrWhiteSpace(candidate)))
|
||||
{
|
||||
if (indexes.BySchemaId.TryGetValue(compatibleSchemaId, out var compatibleLocal))
|
||||
if (lookup.Owner(compatibleSchemaId) is { } compatibleLocal)
|
||||
return compatibleLocal;
|
||||
}
|
||||
|
||||
if (indexes.BySchemaName.TryGetValue(persisted.SchemaName, out var localVersions))
|
||||
var localVersions = lookup.Named(persisted.SchemaName);
|
||||
if (localVersions.Count > 0)
|
||||
{
|
||||
if (localVersions.Length == 1)
|
||||
if (localVersions.Count == 1)
|
||||
return localVersions[0];
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"CultCache schema name '{persisted.SchemaName}' is ambiguous across local versions " +
|
||||
$"[{string.Join(", ", localVersions.Select(candidate => candidate.SchemaVersion).OrderBy(version => version, StringComparer.Ordinal))}]. " +
|
||||
$"[{string.Join(", ", localVersions.Select(candidate => lookup.Describe(candidate).SchemaVersion).OrderBy(version => version, StringComparer.Ordinal))}]. " +
|
||||
"Persisted schema compatibility must identify an explicit schema id.");
|
||||
}
|
||||
|
||||
|
|
@ -761,43 +805,36 @@ namespace GameCult.Caching
|
|||
// next write. A record read only by dropping slots is not owned: declaring its id compatible is the consent to the loss.
|
||||
// Header only: the persisted catalog entry that publishes the id is compared with the claimant's entry, never a payload. A
|
||||
// claimant that cannot be named (ambiguous, id missing from the catalog, shapes incompatible) owns nothing.
|
||||
internal bool Owns(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog) =>
|
||||
Owns(
|
||||
schemaId,
|
||||
catalog,
|
||||
Declared,
|
||||
_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return Claimant(schemaId, catalog, out _)?.ToCatalogEntry();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
internal bool Owns(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog) => Owns(schemaId, catalog, Declared);
|
||||
|
||||
// The rule itself, for any writer: declared, or the entry that reads the record's published entry (the writer's claimant)
|
||||
// reads it whole. A registry supplies Declared and Claimant; a cacheless writer supplies its one entry through the same two.
|
||||
internal static bool Owns(
|
||||
// The same rule with the declared half supplied: a cacheless writer lent this registry declares what it writes, and claims
|
||||
// with the types this registry would.
|
||||
internal bool Owns(string schemaId, IReadOnlyCollection<CultSchemaCatalogEntry> catalog, Func<string, bool> declared) =>
|
||||
Owns(schemaId, catalog, declared, _indexes, descriptor => descriptor.ToCatalogEntry());
|
||||
|
||||
// The rule itself, for any writer: declared, or the claimant (picked by the one Claimant over a lookup) reads the record's
|
||||
// published entry whole. The lookup is a registry's indexes, or the entries a registry-less cacheless writer writes.
|
||||
internal static bool Owns<T>(
|
||||
string schemaId,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> catalog,
|
||||
Func<string, bool> declared,
|
||||
Func<CultSchemaCatalogEntry, CultSchemaCatalogEntry?> claimantOf)
|
||||
IClaimantLookup<T> lookup,
|
||||
Func<T, CultSchemaCatalogEntry> entryOf)
|
||||
where T : class
|
||||
{
|
||||
if (declared(schemaId))
|
||||
return true;
|
||||
var persisted = PublishedBy(schemaId, catalog);
|
||||
return persisted != null && claimantOf(persisted) is { } claimant && ReadsWhole(persisted, claimant);
|
||||
try
|
||||
{
|
||||
var claimant = Claimant(schemaId, catalog, lookup, out var persisted);
|
||||
return persisted != null && claimant != null && ReadsWhole(persisted, entryOf(claimant));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The entry a cacheless writer reads a published entry with: the one that lists the writer's id compatible, else the one of
|
||||
// the same schema name, as Claimant picks a type.
|
||||
internal static CultSchemaCatalogEntry? ClaimantEntry(IEnumerable<CultSchemaCatalogEntry> writes, CultSchemaCatalogEntry persisted) =>
|
||||
writes.FirstOrDefault(entry => persisted.CompatibleSchemaIds.Contains(entry.SchemaId, StringComparer.Ordinal))
|
||||
?? writes.FirstOrDefault(entry => string.Equals(entry.SchemaName, persisted.SchemaName, StringComparison.Ordinal));
|
||||
|
||||
// Owns' declared half: a registered type owns the id or lists it compatible.
|
||||
internal bool Declared(string schemaId)
|
||||
{
|
||||
|
|
@ -879,7 +916,7 @@ namespace GameCult.Caching
|
|||
new[] { existing.SchemaName, claimed.SchemaName },
|
||||
string.Empty);
|
||||
|
||||
private sealed class RegistryIndexes
|
||||
private sealed class RegistryIndexes : IClaimantLookup<CultDocumentDescriptor>
|
||||
{
|
||||
public RegistryIndexes()
|
||||
{
|
||||
|
|
@ -902,6 +939,17 @@ namespace GameCult.Caching
|
|||
// Every type declaring the id compatible, in registration order; resolution never depends on that order.
|
||||
public Dictionary<string, CultDocumentDescriptor[]> ByCompatibleSchemaId { get; }
|
||||
public Dictionary<string, CultDocumentDescriptor[]> BySchemaName { get; }
|
||||
|
||||
public CultDocumentDescriptor? Owner(string schemaId) => BySchemaId.GetValueOrDefault(schemaId);
|
||||
|
||||
public IReadOnlyList<CultDocumentDescriptor> Listing(string schemaId) =>
|
||||
ByCompatibleSchemaId.GetValueOrDefault(schemaId) ?? Array.Empty<CultDocumentDescriptor>();
|
||||
|
||||
public IReadOnlyList<CultDocumentDescriptor> Named(string schemaName) =>
|
||||
BySchemaName.GetValueOrDefault(schemaName) ?? Array.Empty<CultDocumentDescriptor>();
|
||||
|
||||
public ClaimantFacts Describe(CultDocumentDescriptor claimant) =>
|
||||
new(claimant.SchemaName, claimant.SchemaVersion, claimant.DocumentType.FullName ?? string.Empty);
|
||||
}
|
||||
|
||||
private static CultDocumentDescriptor BuildDescriptor(Type type)
|
||||
|
|
@ -4398,7 +4446,7 @@ namespace GameCult.Caching
|
|||
/// <param name="record">The record to store.</param>
|
||||
/// <param name="catalogEntry">The schema catalog entry for the record's schema id.</param>
|
||||
/// <param name="holdsIds">Whether the record's payload holds element ids, or null when the caller cannot tell.</param>
|
||||
/// <param name="registry">The registry the payload was encoded with. Given, the write decides the header for copied records that registry can read; omitted, it decides nothing for them. It serves this call only: the store keeps no registry.</param>
|
||||
/// <param name="registry">The registry the payload was encoded with. Given, the write decides the header for copied records that registry can read, and the registry also picks the type that reads a durable record, as a cache registering it would; omitted, it decides nothing for them and the one catalog entry is the reader. It serves this call only: the store keeps no registry.</param>
|
||||
/// <remarks>The call holds the store's gate while it runs. User code it runs must not enter a cache or another store; that throws <see cref="InvalidOperationException"/>.</remarks>
|
||||
/// <exception cref="InvalidOperationException">The store is attached to a cache; write through the cache.</exception>
|
||||
/// <exception cref="CultStoreUnreadableException">The file at the path is not a store this runtime can read.</exception>
|
||||
|
|
@ -4651,21 +4699,21 @@ namespace GameCult.Caching
|
|||
_staged.Concat(upserts.Select(staged => staged.Record.Key)).Concat(removals).Concat(checks).Concat(StagedChecks),
|
||||
OwnsOnDisk(upserts));
|
||||
|
||||
// The owns predicate a write over the durable catalog asks. A cacheless writer has no registry: it owns what its one entry
|
||||
// declares, or reads whole in the entry that publishes the id.
|
||||
// The owns predicate a write over the durable catalog asks. A cacheless writer declares what the entries it writes declare
|
||||
// (variants:ruling:mesh-writes-bound-by-declaration), and reads a published entry with the claimant a cache registering the
|
||||
// same types would pick: its lent registry's, else the entries it writes. It is never more permissive than that cache.
|
||||
private Func<string, IReadOnlyCollection<CultSchemaCatalogEntry>, bool> OwnsOnDisk(IReadOnlyCollection<CultStagedRecord>? upserts = null) =>
|
||||
Cache != null || upserts == null
|
||||
? Registry.Owns
|
||||
: (id, catalog) =>
|
||||
{
|
||||
var writes = upserts.Select(staged => staged.Entry).ToArray();
|
||||
return CultDocumentRegistry.Owns(
|
||||
id,
|
||||
catalog,
|
||||
declared => writes.Any(entry =>
|
||||
string.Equals(entry.SchemaId, declared, StringComparison.Ordinal) ||
|
||||
entry.CompatibleSchemaIds.Contains(declared, StringComparer.Ordinal)),
|
||||
persisted => CultDocumentRegistry.ClaimantEntry(writes, persisted));
|
||||
Func<string, bool> declared = candidate => writes.Any(entry =>
|
||||
string.Equals(entry.SchemaId, candidate, StringComparison.Ordinal) ||
|
||||
entry.CompatibleSchemaIds.Contains(candidate, StringComparer.Ordinal));
|
||||
return AttachedRegistry is { } lent
|
||||
? lent.Owns(id, catalog, declared)
|
||||
: CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes), entry => entry);
|
||||
};
|
||||
|
||||
// A refused key's durable version is what this store has now read of it.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ namespace GameCult.Caching.Tests
|
|||
[Key(1)] public int Value;
|
||||
}
|
||||
|
||||
// Reads a Narrow-shaped entry only by dropping Value.
|
||||
[CultDocument("tests.restamp_short", "tests.restamp_short.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class NoteShort
|
||||
{
|
||||
[Key(0)] [CultName] public string Name = string.Empty;
|
||||
}
|
||||
|
||||
// Grown with the lossy id declared: declaration is the consent to the loss.
|
||||
[CultDocument("tests.restamp_note", "tests.restamp_note.v4", CompatibleSchemaIds = new[] { WideId })]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
|
|
@ -344,7 +352,7 @@ namespace GameCult.Caching.Tests
|
|||
return path;
|
||||
}
|
||||
|
||||
// Variants live in a single file (the directory store refuses them). The staging half asks the flag ToStoredDocument set on a loaded variant, and Resolved and Flatten carry it on: an owned
|
||||
// Variants live in a single file (the directory store refuses them). The staging half asks the flag ToStoredDocument set on a loaded variant; Resolved and Flatten copies are never asked (only the store's loaded view carries the read-whole flag): an owned
|
||||
// drift variant is rewritten, flattened and removed like a plain drift record, restamping only what is written.
|
||||
[Test]
|
||||
public async Task AnOwnedDriftVariantIsRewritten()
|
||||
|
|
@ -390,16 +398,19 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
// A store whose entry is a newer writer's rename of Narrow's schema, with identical members and the given compatible ids.
|
||||
private string SeedRenamed(bool listsNarrow)
|
||||
private string SeedRenamed(bool listsNarrow, string? alsoLists = null, string name = "tests.restamp_note_renamed")
|
||||
{
|
||||
var path = SeedNarrow(directory: false);
|
||||
var snapshot = Snapshot(path);
|
||||
var narrowId = NarrowRegistry.GetRequired<NoteNarrow>().SchemaId;
|
||||
var entry = snapshot.SchemaCatalog.Single();
|
||||
entry.SchemaId = entry.ContentHash = "tests.restamp.renamed";
|
||||
entry.SchemaName = "tests.restamp_note_renamed";
|
||||
entry.SchemaName = name;
|
||||
entry.SchemaVersion = "tests.restamp_note_renamed.v2";
|
||||
entry.CompatibleSchemaIds = listsNarrow ? new[] { "tests.restamp.renamed", narrowId } : new[] { "tests.restamp.renamed" };
|
||||
entry.CompatibleSchemaIds = new[] { "tests.restamp.renamed" }
|
||||
.Concat(listsNarrow ? new[] { narrowId } : Array.Empty<string>())
|
||||
.Concat(alsoLists == null ? Array.Empty<string>() : new[] { alsoLists })
|
||||
.ToArray();
|
||||
foreach (var record in snapshot.Records)
|
||||
record.SchemaId = entry.SchemaId;
|
||||
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
|
|
@ -407,15 +418,16 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
|
||||
// One writer of NoteNarrow, with a cache or without one: the same Owns answer for the same record.
|
||||
private void WriteNarrow(string path, bool cacheless)
|
||||
private void WriteNarrow(string path, bool cacheless, CultDocumentRegistry? registry = null)
|
||||
{
|
||||
registry ??= NarrowRegistry;
|
||||
if (cacheless)
|
||||
{
|
||||
new SingleFileMessagePackBackingStore(path).WriteDocument(Note, new NoteNarrow { Name = "note", Value = 2 }, registry: NarrowRegistry);
|
||||
new SingleFileMessagePackBackingStore(path).WriteDocument(Note, new NoteNarrow { Name = "note", Value = 2 }, registry: registry);
|
||||
return;
|
||||
}
|
||||
|
||||
using var cache = Open(path, false, NarrowRegistry);
|
||||
using var cache = Open(path, false, registry);
|
||||
cache.PullAllBackingStoresAsync().GetAwaiter().GetResult();
|
||||
Assert.That(cache.Commit(batch => batch.Upsert(new NoteNarrow { Name = "note", Value = 2 }, new CultRecordHandle<NoteNarrow>(Note))), Is.True);
|
||||
}
|
||||
|
|
@ -455,6 +467,54 @@ namespace GameCult.Caching.Tests
|
|||
Assert.That(Fingerprint(path), Is.EqualTo(before));
|
||||
}
|
||||
|
||||
private static readonly CultDocumentRegistry NarrowShortRegistry = CultDocumentRegistry.ForTypes(new[] { typeof(NoteNarrow), typeof(NoteShort) });
|
||||
|
||||
// Which entry reads a persisted entry is one precedence: a cacheless writer lent a registry picks the claimant that registry
|
||||
// picks, never one by schema name over its own entry. Here the entry is named and shaped like Narrow but lists Short's id.
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void ACachelessWriterPicksTheClaimantItsRegistryPicks(bool cacheless)
|
||||
{
|
||||
var path = SeedRenamed(listsNarrow: false, alsoLists: NarrowShortRegistry.GetRequired<NoteShort>().SchemaId, name: "tests.restamp_note");
|
||||
var before = Fingerprint(path);
|
||||
|
||||
Assert.Throws<CultSchemaConflictException>(() => WriteNarrow(path, cacheless, NarrowShortRegistry));
|
||||
Assert.That(Fingerprint(path), Is.EqualTo(before));
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void ACachelessWriterOwnsWhatItsRegistryOwns(bool cacheless)
|
||||
{
|
||||
var path = SeedRenamed(listsNarrow: false, alsoLists: NarrowShortRegistry.GetRequired<NoteShort>().SchemaId, name: "tests.restamp_note");
|
||||
|
||||
WriteNarrow(path, cacheless, NarrowRegistry);
|
||||
|
||||
Assert.That(Snapshot(path).Records.Single(record => record.Key == "note").SchemaId, Is.EqualTo(NarrowRegistry.GetRequired<NoteNarrow>().SchemaId));
|
||||
}
|
||||
|
||||
// A durable record under an id no catalog entry publishes belongs to nobody a writer can name: another key's write carries it
|
||||
// untouched, a write at its own key is refused.
|
||||
[Test]
|
||||
public void AnUnpublishedIdIsCarriedByACachelessWrite()
|
||||
{
|
||||
var path = SeedNarrow(directory: false);
|
||||
var snapshot = Snapshot(path);
|
||||
var stray = new CultPersistedRecord { Key = "stray", SchemaId = "tests.restamp.unpublished", StoredAt = snapshot.Records[0].StoredAt, Payload = new byte[] { 0x90 } };
|
||||
snapshot.Records = snapshot.Records.Concat(new[] { stray }).ToArray();
|
||||
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
|
||||
new SingleFileMessagePackBackingStore(path).WriteDocument(Other, new NoteNarrow { Name = "other", Value = 3 }, registry: NarrowRegistry);
|
||||
|
||||
var carried = Snapshot(path).Records.Single(record => record.Key == "stray");
|
||||
Assert.That(carried.SchemaId, Is.EqualTo("tests.restamp.unpublished"));
|
||||
Assert.That(carried.Payload, Is.EqualTo(new byte[] { 0x90 }));
|
||||
var before = Fingerprint(path);
|
||||
Assert.Throws<CultSchemaConflictException>(() =>
|
||||
new SingleFileMessagePackBackingStore(path).WriteDocument(new CultRecordKey("stray"), new NoteNarrow { Name = "stray", Value = 1 }, registry: NarrowRegistry));
|
||||
Assert.That(Fingerprint(path), Is.EqualTo(before));
|
||||
}
|
||||
|
||||
private static string VectorPath(string name)
|
||||
{
|
||||
for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent)
|
||||
|
|
|
|||
Loading…
Reference in a new issue