A record store with N secondary indexes (unique / non-unique / sorted) over stable generational IDs - the N-index generalization of a BiMap
一张记录表,N 个二级索引。用稳定 ID 定位记录,按任意索引 O(1)/O(log n) 查找。
BiMap 是「两个互为逆的唯一索引」的特例。本项目把它泛化成 N 个彼此独立的索引。
| 结构 | 索引数 | 键唯一性 | 记录身份 |
|---|---|---|---|
| indexmap | 1(主键) | 唯一 | 键本身 |
| bimap | 2(互为逆) | 两个方向都唯一 | 左键 |
| multiindex | N(任意组合) | 每索引独立 | 稳定 RowId |
moon new hello-multiindex
cd hello-multiindex
moon add aurasuisui/multiindex@0.3.0import { "aurasuisui/multiindex" }
pkgtype(kind: "executable") // 本页的程序放在模块根包;`moon run .` 需要一个可执行包完整可运行版本见 cmd/leaderboard(独立模块,import 已发布的包); 签名以 docs/SPEC.md 为准(唯一真源)。
struct Player {
id : Int32
name : String
team : Int32
score : Int32
}
fn main {
let t : @multiindex.Table[Int32, Player] = @multiindex.Table::new()
t.register_unique("id", fn(p) { p.id }).unwrap()
t.register_non_unique("team", fn(p) { p.team }).unwrap()
t.register_non_unique_ordered("score", fn(p) { p.score }).unwrap()
let roster : Array[Player] = [
{ id: 7, name: "Alice", team: 1, score: 120 },
{ id: 9, name: "Bob", team: 1, score: 95 },
{ id: 11, name: "Carol", team: 2, score: 150 },
]
match t.insert_many(roster) {
Ok(ids) => println("inserted \{ids.length()} records, len = \{t.len()}")
Err(rejected) => println("refused \{rejected.record.name}: \{rejected.conflict}")
}
let (_, alice) = t.get_by("id", 7).unwrap()
println("id=7 -> \{alice.name}")
for (_, p) in t.iter_by("team", 1) {
println("team=1 -> \{p.name}")
}
for (_, p) in t.range_by("score", 100, 130) {
println("score in [100, 130] -> \{p.name}")
}
for (_, p) in t.iter_desc_by("score") {
println("\{p.name}: \{p.score}")
}
}inserted 3 records, len = 3
id=7 -> Alice
team=1 -> Alice
team=1 -> Bob
score in [100, 130] -> Alice
Carol: 150
Alice: 120
Bob: 95cd cmd/leaderboard
moon run .| 方法 | 说明 |
|---|---|
| Table::new() / Table::with_capacity(cap) | 空表;后者预留槽位 |
| register_unique(name, project) | 哈希唯一(Map[K, Int]) |
| register_unique_ordered(name, project) (需 K : Compare) | 有序唯一(SortedMap[K, Int]) |
| register_non_unique(name, project) | 哈希非唯一(Map[K, SortedSet[Int]]) |
| register_non_unique_ordered(name, project) (需 K : Compare) | 有序非唯一 |
| unregister_index(name) | 删除索引注册:返回 Bool,未注册 false 不 abort;只摘注册表,记录 / 其它索引 / 插入序不变 |
| has_index(name) / index_names() / index_entries() | 注册表查询(名字升序);index_entries() 额外给出每个索引的种类(IndexKind:HashUnique / SortedUnique / HashMulti / SortedMulti) |
| 方法 | 说明 |
|---|---|
| insert(v) | Result[RowId, Rejected[K, V]] |
| insert_many(vs) | Result[Array[RowId], Rejected[K, V]];整批原子,含批内互相冲突 |
| insert_or_replace(v) | Result[(RowId, V?), Rejected[K, V]];覆盖式 upsert:恰好一条记录在新值的某个唯一索引上撞键 → 原地替换、保留它的 RowId,返回被顶替的旧值;无冲突则等价于 insert;两条不同记录分别撞键 → abort(表零变更) |
| update(id, v) / modify(id, f) | Result[Unit, UpdateRejected[K, V]];排除自身的唯一性预检 |
| remove(id) / swap_remove(id) / clear() | 删除单条(旧句柄失效):remove 保插入序、O(len);swap_remove 末位换位、O(1) 但打乱插入序 / 清空记录与索引条目(保留注册) |
| 方法 | 说明 |
|---|---|
| get(id) / contains(id) | 按句柄取记录;陈旧句柄 None / false |
| len() / is_empty() | 记录数 |
| iter() | 插入序,Iter2[RowId, V] |
| get_by(name, key) | (RowId, V)?;非唯一索引返回槽位号最小者 |
| iter_by(name, key) | 该键的全部记录,槽位号升序 |
| range_by(name, lo, hi) / range_half_open_by(name, lo, hi) | 双闭 / 半开区间,仅有序索引 |
| iter_asc_by(name) / iter_desc_by(name) | 按键升 / 降序,仅有序索引 |
| 方法 | 说明 |
|---|---|
| verify_invariants() | Result[Unit, String]:首个违反点,消息含不变量编号与槽位/键 |
| index_stats(name) | IndexStats { buckets, entries, longest_bucket, average_bucket } |
| to_json() / Table::from_json(json) | {"records": [...]},按插入序;索引不参与序列化 |
分支:main 是里程碑视图(每个阶段一个提交),dev 保留完整的逐节点开发历史。
match table.insert(record) {
Ok(id) => id
Err(rejected) => retry_with(rejected.record)
}pub struct Table[K, V] {
// private fields
}Install
Download zipA record store with N secondary indexes (unique / non-unique / sorted) over stable generational IDs - the N-index generalization of a BiMap