7.8 KiB
unique-index vectors
Written only by the C# reference, tests/GameCult.Caching.Tests/UniqueIndexVectorTests.cs, with
CULTCACHE_WRITE_VECTORS=1. Every other run reads these files and compares them byte for byte; a changed vector is a
reviewed commit. Other runtimes are held to the same canonical text and the same verdicts (variants:ruling:unique-indexes).
Files
values.msgpack: an array of[label, kind, value, text-or-nil], kind one ofstring,int64,uint64,double,bool(decode integers 64-bit wide).textis the canonical index text of a member holdingvalue: a record holding it is found byGetByIndex(alias, text). A nil text is a blank value: no value, never indexed, never judged, found by no text, and two records holding it commit. Strings are as is (never trimmed), blank being empty or only C#White_Spacecharacters (U+0009-000D, U+0020, U+0085, U+00A0, U+1680, U+2000-200A, U+2028, U+2029, U+202F, U+205F, U+3000); U+FEFF and U+001F are values. Booleans areTrueandFalse. Integers are their decimal digits. A non-integral finite double is its exact decimal expansion (written by C#RenderExactDouble, never hand-typed),-0.0is0, NaN and infinities are nil.duplicate-store.cc: a single-file store, schematests.unique_itemv1, slots[Sku: string, Category: string], stored-at pinned to2026-01-01T00:00:00Z. Recordsitem:aanditem:bboth hold SkuS-1;item:cholds Sku" "anditem:dholds SkuU+0085. Written through a role Sku.blank-store.cc: the same, onlyitem:canditem: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,holder-case-store.cc,cross-schema-store.cc,distinct-values-store.cc,normal-keys-store.cc,normal-three-store.cc,wide-store.cc: single-file stores written likeduplicate-store.cc, through roles, for the rows ofrefusals.msgpackthat open or pull them. Schemastests.unique_item,tests.uq_pair(Sku and Alt both unique),tests.uq_alias(Sku andaltboth unique),tests.uq_wide(Sku and Alt both unique, aliases U+FF21 and U+10400),tests.uq_alphaandtests.uq_beta, each slots[0: Sku string, 1: second string], schema version the name plus.v1. The schema ids C# writes in the store catalogs and on every record:tests.unique_item:sha256:93bb890de83069061af696b23f94fd32fb3c2dad3c91ee645e8add56085f1882tests.uq_pair:sha256:2538cceafb6dcbce22ef11b932ecb804af5f96bc3aaedff692a52d3640433e60tests.uq_alias:sha256:77a77a394ebd8bf71e14b9b6f2cea4978c25d4e705fd12364f660edbf6c63b1ftests.uq_wide:sha256:b79eecd11eed5e456902f095d2748da7f59125c4c8972b0da7dddc81d07fc03ftests.uq_alpha:sha256:53aeaf2ea9b25e85ade6b15fc04c7653e7b206c87e258f1aa74dd32a840a1389tests.uq_beta:sha256:45ed62a64c91f6d815d3ec975c4a4a1b2ce07b6e7262a32d828673cf10899586
outcomes.msgpack: rows[file, unique alias or nil, open|refused, k1 or nil, k2 or nil]. Openingfilewith that alias a unique index (nil: a role, no unique alias) yields the verdict. A refusal reads, byte for byte,Records <k1> and <k2> (tests.unique_item) both hold unique index '<alias>'; an index value names one record.withk1 < k2ordinal, never contains the value, and leaves the file byte-identical.
refusals.msgpack
RULE: a unique-index refusal is a function of the state the admission would leave, never of staging, file or pull
order. Over every (type, unique alias, non-blank value) that state holds at two or more distinct keys, name the one
least by (alias, its least key, its second-least key, schema name), each compared by UTF-16 code unit (C#
StringComparer.Ordinal): Records <k1> and <k2> (<schema>) both hold unique index '<alias>'; an index value names one record. with k1 < k2 its two least keys. One rule for a single write, a batch, a load, a re-pull and an index
registration.
Rows [label, ops, refusal]. Each op is ["open", file-or-nil] (copy that vector file to a fresh path, or nil for a
fresh path with no file, and open a cache on it), ["pull", file] (overwrite the open cache's file with that vector
file, then pull every backing store again) or ["commit", [[key, schema, sku, second], ...]] (one batch, staged in row
order; schema is the schema name). refusal is nil (every op succeeds) or [k1, k2, schema, alias]: every op but the
last succeeds and the last refuses with exactly the RULE text built from those fields, leaving the file
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-37 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, 23 batch-holders-by-case (a and A holding one value: A before a), 24 load-holders-by-case, 25 batch-replaced-by-case (held a, then a commit of A on its value: A does not replace a),
26 batch-value-in-two-schemas (one value in tests.uq_alpha and tests.uq_beta at two keys: two groups, success), 27
load-value-in-two-schemas, 28 batch-distinct-values (S, s, S , S, U+00E9, e+U+0301, U+FF33 are seven values: success), 29
load-distinct-values, 30 batch-keys-by-normal-form (keys U+00E9 and e+U+0301 holding one value are two holders, e+U+0301 first),
31 load-keys-by-normal-form, 32 put-onto-a-held-pair (held m and landing a share Sku and Alt in tests.uq_pair, one record per
commit: Alt is named before Sku), 33 put-onto-a-held-alias-pair (the same in tests.uq_alias: Sku before alt), 34 batch-three-holders-normal-form
(keys e+U+0302, f and U+00E9 holding one value: the two least by code unit, e+U+0302 and f, are named; an NFC, NFD or culture sort of the keys names another pair), 35
load-three-holders-normal-form, 36 batch-alias-code-unit (two records each holding the same value under both aliases of tests.uq_wide, U+FF21 and U+10400: by UTF-16 code
unit U+10400 (D801 DC00) precedes U+FF21, by code point it follows it, so the refusal names alias U+10400), 37 load-alias-code-unit.
Rows 19-22, 24, 27, 29, 31, 35 and 37 open a store holding the records of rows 18, 14, 15, 17, 23, 26, 28, 30, 34 and 36, 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 walks. These are the counts a runtime must assert, not a statement of which runtimes walk them today:
- C# and Rust: all 37.
- Python: 35, every row but 3 and 26 (
put_envelopestakes one type, so no batch of two types). - TypeScript: 20, the rows with no multi-record commit: 5, 8, 9, 10, 11, 12, 13, 19, 20, 21, 22, 24, 25, 27, 29, 31, 32, 33, 35 and 37
(no batch commit, so no row 1-4, 6, 7, 14-18, 23, 26, 28, 30, 34 or 36;
variants:idea:unique-index-runtimes.ts-batch-commit).
Not pinned: the schema-name tiebreak. Record keys are unique across a whole cache, so two types never hold the same key and
two groups can never tie on alias and key pair; the final schema comparison cannot decide a refusal.