Unique-index vectors: pin holder identity by case (rows 23-24)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled

Two holders of one value whose keys differ only by case are two holders; a case-folding dedupe would see one and miss the refusal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Libby 2026-11-11 10:08:41 +00:00
commit d02b56a692
4 changed files with 14 additions and 7 deletions

View file

@ -351,6 +351,7 @@ namespace GameCult.Caching.Tests
private static readonly Rec[] RepullBefore = Items_(("item:a1", "A"), ("item:b1", "C"));
private static readonly Rec[] RepullAfter = Items_(("item:a1", "A"), ("item:b1", "B"), ("item:b2", "B"), ("item:c1", "A"));
private static readonly Rec[] KeyCaseRecords = Items_(("a", "S"), ("z", "S"), ("B", "T"), ("C", "T"));
private static readonly Rec[] HolderCaseRecords = Items_(("a", "S"), ("A", "S"));
private static readonly Rec[] KeyPunctuationRecords = Items_(("_", "S"), ("a", "S"), ("Z", "T"), ("b", "T"));
private static readonly Rec[] SurrogateGroupRecords = Items_(("\uE000", "S"), ("\uE000b", "S"), ("\U0001F600", "T"), ("\U0001F600b", "T"));
@ -360,6 +361,7 @@ namespace GameCult.Caching.Tests
("repull-before.cc", RepullBefore), ("repull-after.cc", RepullAfter), ("alias-store.cc", AliasRecords),
("key-case-store.cc", KeyCaseRecords), ("key-punctuation-store.cc", KeyPunctuationRecords),
("surrogate-groups-store.cc", SurrogateGroupRecords),
("holder-case-store.cc", HolderCaseRecords),
};
private static readonly Refusal SurrogateRefusal = new("k\U0001F600", "k\uE000", Item, "Sku");
@ -368,6 +370,7 @@ namespace GameCult.Caching.Tests
private static readonly Refusal AliasRefusal = new("item:a", "item:b", Alias, "Sku");
private static readonly Refusal RepullRefusal = new("item:a1", "item:c1", Item, "Sku");
private static readonly Refusal KeyCaseRefusal = new("B", "C", Item, "Sku");
private static readonly Refusal HolderCaseRefusal = new("A", "a", Item, "Sku");
private static readonly Refusal KeyPunctuationRefusal = new("Z", "b", Item, "Sku");
private static readonly Refusal SurrogateGroupRefusal = new("\U0001F600", "\U0001F600b", Item, "Sku");
@ -402,6 +405,9 @@ namespace GameCult.Caching.Tests
new("load-key-case", new[] { Open("key-case-store.cc") }, KeyCaseRefusal),
new("load-key-punctuation", new[] { Open("key-punctuation-store.cc") }, KeyPunctuationRefusal),
new("load-surrogate-groups", new[] { Open("surrogate-groups-store.cc") }, SurrogateGroupRefusal),
// Rows 23-24: two holders of one value that differ only by case are two holders; a case-folding dedupe would see one.
new("batch-holders-by-case", new[] { Open(), Commit(HolderCaseRecords) }, HolderCaseRefusal),
new("load-holders-by-case", new[] { Open("holder-case-store.cc") }, HolderCaseRefusal),
};
private static byte[] RefusalStoreBytes(Rec[] records)
@ -589,7 +595,7 @@ namespace GameCult.Caching.Tests
}
}
Assert.That(walked, Is.EqualTo(22));
Assert.That(walked, Is.EqualTo(24));
}
}
}

View file

@ -19,7 +19,7 @@ reviewed commit. Other runtimes are held to the same canonical text and the same
`" "` and `item:d` holds Sku `U+0085`. Written through a role Sku.
- `blank-store.cc`: the same, only `item:c` and `item:d`.
- `pair-store.cc`, `surrogate-store.cc`, `types-store.cc`, `repull-before.cc`, `repull-after.cc`, `alias-store.cc`,
`key-case-store.cc`, `key-punctuation-store.cc`, `surrogate-groups-store.cc`: single-file stores written like
`key-case-store.cc`, `key-punctuation-store.cc`, `surrogate-groups-store.cc`, `holder-case-store.cc`: single-file stores written like
`duplicate-store.cc`, through roles, for the rows of `refusals.msgpack` that open or pull them. Schemas
`tests.unique_item`, `tests.uq_pair` (Sku and Alt both unique), `tests.uq_alias` (Sku and `alt` both unique),
`tests.uq_alpha` and `tests.uq_beta`, each slots `[0: Sku string, 1: second string]`, schema version the name plus
@ -51,19 +51,20 @@ last succeeds and the last refuses with exactly the RULE text built from those f
byte-identical. Schemas are those of the store files above; `second` is `Category` (plain) except in `tests.uq_pair`
and `tests.uq_alias`, where it is `Alt` (unique, alias `Alt` and `alt` respectively).
The 13 rows of the spec are a floor; rows 14-22 extend it with orderings that a code-point, case-insensitive or
The 13 rows of the spec are a floor; rows 14-24 extend it with orderings that a code-point, case-insensitive or
culture compare would flip. Rows, in file order: 1 batch-later-value-staged-first, 2 batch-surrogate-keys, 3
batch-interleaved-types, 4 batch-two-aliases, 5 held-then-landing, 6 held-in-the-least-group, 7 swap-off-a-held-value,
8 load-two-aliases, 9 load-surrogate-keys, 10 load-interleaved-types, 11 repull-held-pair, 12 load-repull-after, 13
commit-onto-a-loaded-holder, 14 batch-key-case (`B` before `a`), 15 batch-key-punctuation (`Z` before `_` before `a`),
16 batch-key-case-in-a-group, 17 batch-surrogate-groups (U+1F600 before U+E000, the reverse of code-point order), 18
batch-alias-case (alias `Sku` before `alt`), 19 load-alias-case, 20 load-key-case, 21 load-key-punctuation, 22
load-surrogate-groups. Rows 19-22 open a store holding the records of rows 18, 14, 15 and 17, so a runtime with no batch
load-surrogate-groups, 23 batch-holders-by-case (`a` and `A` holding one value: `A` before `a`), 24 load-holders-by-case.
Rows 19-22 and 24 open a store holding the records of rows 18, 14, 15, 17 and 23, so a runtime with no batch
commit still walks alias and cross-group key order.
A runtime performs every row its API can perform and asserts the count it walked:
- C# and Rust: all 22.
- Python: 21, every row but 3 (`put_envelopes` takes one type).
- TypeScript: 11, rows 5, 8, 9, 10, 11, 12, 13, 19, 20, 21 and 22 (no batch commit, so no row 1-4, 6, 7 or 14-18;
- C# and Rust: all 24.
- Python: 23, every row but 3 (`put_envelopes` takes one type).
- TypeScript: 12, rows 5, 8, 9, 10, 11, 12, 13, 19, 20, 21, 22 and 24 (no batch commit, so no row 1-4, 6, 7, 14-18 or 23;
`variants:idea:unique-index-runtimes.ts-batch-commit`).

Binary file not shown.