Specialized B+Tree Key-Size Variants
One cache-aligned B+Tree node layout per key width, picked automatically from the field's type — invisible to application code.
Status: ✅ Implemented · Visibility: Internal · Category: Indexing
🎯 What it solves
Every indexed field — primary key or secondary index — needs an ordered, concurrent lookup structure, but indexed key types span an 8×-wide range (1-byte byte up to 64-byte String64). A single one-size-fits-all B+Tree would either waste node space on narrow keys (fewer keys per cache-line-sized node, deeper trees, more cache misses) or be unable to address wide keys at all. Typhon avoids this by giving each key-width tier its own specialized tree implementation, so a byte-keyed index and a long-keyed index both get a node layout packed for their key size, without the application ever picking one.
⚙️ How it works (in brief)
Four concrete tree types — L16BTree (keys ≤ 16-bit), L32BTree (32-bit), L64BTree (64-bit), String64BTree (fixed 64-byte string) — share one generic algorithm (BTree<TKey, TStore>) but each defines its own fixed-size node struct, sized so more keys fit in a narrow-key node than a wide-key one. Which variant backs a given index is decided automatically when the schema is built, from the indexed field's declared type — application code never names or constructs a tree class directly. Every variant is further specialized over TStore: PersistentStore (durable, WAL-protected, backs Versioned/SingleVersion tables) or TransientStore (in-memory, backs Transient tables) — again chosen by the table's storage discipline, not by the caller.
💻 Usage
[Component("Game.Player", 1)]
public struct Player
{
[Index] // unique secondary index -> L32BTree<int, ...> (32-bit key)
public int PlayerId;
[Index(AllowMultiple = true)] // non-unique secondary index -> String64BTree<...>
public String64 Guild;
public float PositionX; // not indexed
}
// Cold path: resolve once, reuse the IndexRef on the hot path.
var idIndex = engine.GetIndexRef<Player, int>(p => p.PlayerId);
// Hot path: range-scan through Transaction — never through the tree directly.
using var tx = engine.CreateQuickTransaction();
using var found = tx.EnumerateIndex<Player, int>(idIndex, minKey: 100, maxKey: 200);
foreach (var entry in found)
{
// entry.EntityPK, entry.Key, entry.Component
}
| Key type(s) | Variant | Capacity (keys/node) |
|---|---|---|
byte, sbyte, short, ushort, char |
L16BTree |
38 |
int, uint, float |
L32BTree |
29 |
long, ulong, double |
L64BTree |
19 |
String64 |
String64BTree |
4 |
⚠️ Guarantees & limits
- Variant selection is fully automatic and type-driven — there is no API to request a specific tree implementation; it follows from the field's declared type and
[Index(AllowMultiple = ...)]. - All four variants expose the identical operation surface and concurrency model (OLC optimistic reads, B-link splits) through
IndexRef/Transaction— no behavioral difference between tiers at the application level. - Node capacity scales inversely with key width (38 → 29 → 19 → 4), so narrower-keyed indexes pack more entries per node and produce shallower trees for the same entry count.
String64keys are fixed at 64 bytes; there is no variable-length string index.- Persistent vs. transient backing follows the owning table's storage discipline (
Versioned/SingleVersion= durable,Transient= in-memory) — not independently selectable per index. - Schema evolution that changes an indexed field's type invalidates previously-resolved
IndexRefs (CapturedLayoutVersioncheck); re-resolve viaGetIndexRef/GetPKIndexRef.
🧪 Tests
- BtreeTests —
ForwardInsertionTest/ForwardFloatInsertionTest/ReverseInsertionTest/ReverseString64InsertionTestexercise all four key-width tiers (int/L32,float/L32,byte/L16,String64) through the sharedBTree<TKey, TStore>algorithm - OlcLatchTests — chunk-size verification (
Index16Chunk_Size_Is256Bytes,Index32Chunk_Size_Is256Bytes,Index64Chunk_Size_Is256Bytes,Index32Chunk_Capacity_Is29) confirms the per-variant node layout the table above documents
🔗 Related
- Sibling: B+Tree Node Layout and Capacity Tuning — per-key-width node capacities this feature's variants use