| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- package btree
- import (
- "bytes"
- "encoding/binary"
- "math/rand"
- "sort"
- "testing"
- "TsDB2/DB/core/page"
- )
- // ============================================================================
- // 复合分隔键 (key, pkID) 回归测试。
- //
- // 二级树的全局不变量: 所有叶条目按复合键 (key, pkID) 有序; 内部条目的前
- // EntryLen-4 字节 = 其子树的最小复合键。一旦破坏:
- // - sbtChildIdxPK 的点路由会落到错误子树 → (key,pkID) 精确删除漏删;
- // - sbtLeafLowerBoundPK 的叶内二分会漏条目 → SBTSearchAll 返回不全。
- // 本测试用「乱序插入 → 乱序删除 → 重插入 → 删空」全流程 + 全树不变量校验钉死它。
- // ============================================================================
- // checkSubtree 递归校验以 pid 为根的子树:
- // - 叶内/内部条目按复合键严格递增;
- // - 内部条目复合键 == 其子树的最小复合键;
- // - 不存在 "空的非根子树" (删除路径的下溢处理应消掉它们);
- // - 叶子链 (PrePage/NextPage) 与 DFS 顺序一致。
- // 返回该子树的最小复合键; 空树返回 nil。
- func checkSubtree(t *testing.T, h page.HandleDb, pid uint32) []byte {
- t.Helper()
- n := sbtLoad(h, pid)
- if n == nil {
- t.Fatalf("checkSubtree: 读不出节点 pid=%d", pid)
- }
- cnt := int(n.info.Count)
- if n.isLeaf() {
- for i := 1; i < cnt; i++ {
- a, b := sbtEntry(n, i-1), sbtEntry(n, i)
- if sbtCmpKeyPK(sbtLeafKey(a), sbtPKID(a), sbtLeafKey(b), sbtPKID(b)) >= 0 {
- t.Fatalf("叶 %d 内复合键乱序 @%d", pid, i)
- }
- }
- if cnt == 0 {
- return nil
- }
- return sbtEntry(n, 0) // 叶条目本身就是 [key|pkID]
- }
- if cnt == 0 {
- return nil // 仅允许根为空内部节点 (上层归一化会换掉它)
- }
- var first []byte
- for i := 0; i < cnt; i++ {
- e := sbtEntry(n, i)
- if i > 0 {
- p := sbtEntry(n, i-1)
- if sbtCmpKeyPK(sbtIntKey(p), sbtIntPKID(p), sbtIntKey(e), sbtIntPKID(e)) >= 0 {
- t.Fatalf("内部节点 %d 复合键乱序 @%d", pid, i)
- }
- }
- childPID := sbtChildPID(e)
- child := sbtLoad(h, childPID)
- if child == nil {
- t.Fatalf("内部节点 %d 条目 %d 指向读不出的子页 %d", pid, i, childPID)
- }
- if child.info.Count == 0 {
- t.Fatalf("存在空的非根子树: 父 %d 条目 %d → 子 %d (下溢处理漏了它)", pid, i, childPID)
- }
- cm := checkSubtree(t, h, childPID)
- if !bytes.Equal(cm, e[:len(e)-4]) {
- t.Fatalf("父条目复合键 != 子树最小复合键: 父 %d 条目 %d → 子 %d\n父=%v 子=%v",
- pid, i, childPID, e[:len(e)-4], cm)
- }
- if i == 0 {
- first = append([]byte(nil), cm...)
- }
- }
- return first
- }
- // dfsLeafOrder DFS 收集叶页号 (复合键序)。
- func dfsLeafOrder(h page.HandleDb, pid uint32) []uint32 {
- n := sbtLoad(h, pid)
- if n == nil {
- return nil
- }
- if n.isLeaf() {
- return []uint32{pid}
- }
- var out []uint32
- for i := 0; i < int(n.info.Count); i++ {
- out = append(out, dfsLeafOrder(h, sbtChildPID(sbtEntry(n, i)))...)
- }
- return out
- }
- func checkLeafChain(t *testing.T, h page.HandleDb, root uint32) {
- t.Helper()
- leaves := dfsLeafOrder(h, root)
- if len(leaves) == 0 {
- return
- }
- for i, pid := range leaves {
- n := sbtLoad(h, pid)
- if n == nil || !n.isLeaf() {
- t.Fatalf("DFS 叶序里出现非叶/读不出的页 %d", pid)
- }
- wantPre, wantNext := uint32(0), uint32(0)
- if i > 0 {
- wantPre = leaves[i-1]
- }
- if i+1 < len(leaves) {
- wantNext = leaves[i+1]
- }
- if n.info.Hdr.PrePage != wantPre || n.info.Hdr.NextPage != wantNext {
- t.Fatalf("叶子链断裂: 叶[%d]=%d want(pre=%d,next=%d) got(pre=%d,next=%d)",
- i, pid, wantPre, wantNext, n.info.Hdr.PrePage, n.info.Hdr.NextPage)
- }
- }
- }
- // TestSecondaryCompositeOrderStress 乱序插入/删除/重插入全程校验复合键不变量。
- func TestSecondaryCompositeOrderStress(t *testing.T) {
- const (
- keyLen = 8
- keys = 50 // 每个 key 400 条重复 → 单 key 区段 6400B, 必然跨多个叶
- perKey = 400
- )
- total := keys * perKey
- m := newMemHandle()
- h := m.handle()
- type pair struct {
- key uint64
- pk uint64
- }
- all := make([]pair, 0, total)
- for k := uint64(1); k <= keys; k++ {
- for p := uint64(1); p <= perKey; p++ {
- all = append(all, pair{key: k, pk: p*k + p%97}) // pkID 乱序于插入序列
- }
- }
- rng := rand.New(rand.NewSource(20260914))
- rng.Shuffle(len(all), func(i, j int) { all[i], all[j] = all[j], all[i] })
- mkEntry := func(p pair) []byte {
- e := make([]byte, keyLen+8)
- binary.BigEndian.PutUint64(e[:keyLen], p.key)
- binary.BigEndian.PutUint64(e[keyLen:], p.pk)
- return e
- }
- mkKey := func(k uint64) []byte {
- b := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b, k)
- return b
- }
- // 活跃集合 (期望状态): key → 有序 pkID 列表
- live := make(map[uint64][]uint64)
- var root uint32
- // ---- 阶段 1: 乱序全量插入 ----
- for _, p := range all {
- var err error
- root, err = SBTInsert(root, mkEntry(p), h)
- if err != nil {
- t.Fatalf("插入 (%d,%d) 失败: %v", p.key, p.pk, err)
- }
- live[p.key] = append(live[p.key], p.pk)
- }
- checkSubtree(t, h, root)
- checkLeafChain(t, h, root)
- // live[k] 保持有序, 供 sort.Search 精确移除
- for k := range live {
- sort.Slice(live[k], func(a, b int) bool { return live[k][a] < live[k][b] })
- }
- // ---- 阶段 2: 乱序删除一半 ----
- rng.Shuffle(len(all), func(i, j int) { all[i], all[j] = all[j], all[i] })
- deleted := make(map[pair]bool)
- for i, p := range all[:total/2] {
- newRoot, err := SBTDeleteWithPKID(root, mkKey(p.key), p.pk, h)
- if err != nil {
- t.Fatalf("删除 (%d,%d) 失败: %v", p.key, p.pk, err)
- }
- root = newRoot
- pks := live[p.key]
- idx := sort.Search(len(pks), func(x int) bool { return pks[x] >= p.pk })
- live[p.key] = append(pks[:idx], pks[idx+1:]...)
- deleted[p] = true
- if i%500 == 499 {
- checkSubtree(t, h, root)
- checkLeafChain(t, h, root)
- // 抽查被删 key 与随机 key 的 SBTSearchAll
- for _, k := range []uint64{p.key, uint64(rng.Intn(keys) + 1), 1, uint64(keys)} {
- want := append([]uint64(nil), live[k]...)
- sort.Slice(want, func(a, b int) bool { return want[a] < want[b] })
- got, ok := SBTSearchAll(root, mkKey(k), h)
- if len(want) == 0 {
- if ok {
- t.Fatalf("SBTSearchAll(key=%d) 应为空, 返回 %v", k, got)
- }
- continue
- }
- if !ok || !equalU64(got, want) {
- t.Fatalf("SBTSearchAll(key=%d) 不符: want %d 条, got %v (ok=%v)", k, len(want), got, ok)
- }
- }
- }
- }
- checkSubtree(t, h, root)
- // ---- 阶段 3: 重插入部分已删条目 (混合分裂/合并后的结构) ----
- reins := 0
- for _, p := range all[:total/2] {
- if reins >= total/8 {
- break
- }
- if !deleted[p] {
- continue
- }
- var err error
- root, err = SBTInsert(root, mkEntry(p), h)
- if err != nil {
- t.Fatalf("重插入 (%d,%d) 失败: %v", p.key, p.pk, err)
- }
- live[p.key] = append(live[p.key], p.pk)
- sort.Slice(live[p.key], func(a, b int) bool { return live[p.key][a] < live[p.key][b] })
- delete(deleted, p)
- reins++
- }
- checkSubtree(t, h, root)
- checkLeafChain(t, h, root)
- // ---- 阶段 4: 全量删除至空 ----
- remaining := make([]pair, 0, total)
- for k := uint64(1); k <= keys; k++ {
- for _, pk := range live[k] {
- remaining = append(remaining, pair{key: k, pk: pk})
- }
- }
- rng.Shuffle(len(remaining), func(i, j int) { remaining[i], remaining[j] = remaining[j], remaining[i] })
- for _, p := range remaining {
- newRoot, err := SBTDeleteWithPKID(root, mkKey(p.key), p.pk, h)
- if err != nil {
- t.Fatalf("清空阶段删除 (%d,%d) 失败: %v", p.key, p.pk, err)
- }
- root = newRoot
- }
- // 删空后: 根必须是 (空) 叶, 全树只剩 1 页
- n := sbtLoad(h, root)
- if n == nil || !n.isLeaf() || n.info.Count != 0 {
- t.Fatalf("删空后根不是空叶: %+v", n.info)
- }
- // 页收支平衡: 曾分配的每一页, 要么是最终根, 要么在下溢/合并中已归还
- freedSoFar := sortedSet(m.freed)
- if len(freedSoFar)+1 != len(m.allocated()) {
- t.Fatalf("页收支不平衡: 分配 %d 页, 已归还 %d, 最终根 1 页",
- len(m.allocated()), len(freedSoFar))
- }
- // 释放单页空根应恰好只归还根本身 (其余 34 页已在途中归还, 不得二次归还)
- m.freed = nil
- btReleaseSubtree(h, root)
- if freed := sortedSet(m.freed); !sameSet(freed, []uint32{root}) {
- t.Fatalf("释放单页空根应只归还根 %d, 实际归还 %v", root, freed)
- }
- }
- func equalU64(a, b []uint64) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
- }
- // TestSecondaryBuildFromSortedComposite 批量建树要求条目按复合键 (key, pkID) 有序;
- // 同 key 条目 pkID 无序时, 复合路由/二分都会静默漏条目 —— 这里验证有序输入的正确性,
- // 并抽查精确删除落在叶中部 (非区段端点) 的场景。
- func TestSecondaryBuildFromSortedComposite(t *testing.T) {
- const keyLen = 8
- m := newMemHandle()
- h := m.handle()
- // 3 个 key, 每个 800 条, 按 (key, pkID) 排序
- var entries [][]byte
- for k := uint64(1); k <= 3; k++ {
- for p := uint64(1); p <= 800; p++ {
- e := make([]byte, keyLen+8)
- binary.BigEndian.PutUint64(e[:keyLen], k)
- binary.BigEndian.PutUint64(e[keyLen:], p)
- entries = append(entries, e)
- }
- }
- root := SBTBuildFromSorted(entries, h)
- if root == 0 {
- t.Fatal("SBTBuildFromSorted 返回 0")
- }
- checkSubtree(t, h, root)
- // SBTSearchAll 应按 pkID 升序返回完整区段
- for k := uint64(1); k <= 3; k++ {
- b := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b, k)
- got, ok := SBTSearchAll(root, b, h)
- if !ok || len(got) != 800 {
- t.Fatalf("key=%d: want 800 条, got %d (ok=%v)", k, len(got), ok)
- }
- for i := 1; i < len(got); i++ {
- if got[i-1] >= got[i] {
- t.Fatalf("key=%d: 返回未按 pkID 有序", k)
- }
- }
- }
- // 删掉中间一段 pkID, 区段两端不受影响
- for p := uint64(300); p <= 500; p++ {
- b := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b, 2)
- var err error
- root, err = SBTDeleteWithPKID(root, b, p, h)
- if err != nil {
- t.Fatalf("删除 (2,%d) 失败: %v", p, err)
- }
- }
- checkSubtree(t, h, root)
- b2 := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b2, 2)
- got, ok := SBTSearchAll(root, b2, h)
- if !ok || len(got) != 599 {
- t.Fatalf("删除中段 (300..500 共 201 条) 后 key=2 应剩 599 条, got %d", len(got))
- }
- }
- // TestSecondaryDeleteRoutingExact 点删除不再向左试探兄弟子树:
- // 构造 "同 key 区段横跨多个叶" 后, 删除区段最左侧的条目必须一次命中。
- // (旧实现从 key 路由落点向左扫, 删左端条目要试完整段; 若复合路由错位, 这里会漏删报
- // ErrNotFound 或删错条目。)
- func TestSecondaryDeleteRoutingExact(t *testing.T) {
- const keyLen = 8
- m := newMemHandle()
- h := m.handle()
- // key=7 的 3000 条 + 两侧各 1 条异键哨兵
- var entries [][]byte
- add := func(k, p uint64) {
- e := make([]byte, keyLen+8)
- binary.BigEndian.PutUint64(e[:keyLen], k)
- binary.BigEndian.PutUint64(e[keyLen:], p)
- entries = append(entries, e)
- }
- add(6, 1)
- for p := uint64(1); p <= 3000; p++ {
- add(7, p)
- }
- add(8, 1)
- sort.Slice(entries, func(i, j int) bool {
- c := bytes.Compare(entries[i][:keyLen], entries[j][:keyLen])
- if c != 0 {
- return c < 0
- }
- return binary.BigEndian.Uint64(entries[i][keyLen:]) < binary.BigEndian.Uint64(entries[j][keyLen:])
- })
- root := SBTBuildFromSorted(entries, h)
- // 从区段最左 (pkID=1) 删到最右 (pkID=3000): 任何一步 ErrNotFound 都是路由错位
- for p := uint64(1); p <= 3000; p++ {
- b := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b, 7)
- var err error
- root, err = SBTDeleteWithPKID(root, b, p, h)
- if err != nil {
- t.Fatalf("顺序删除 (7,%d) 失败: %v", p, err)
- }
- }
- // 区段删空, 哨兵仍在
- for k, want := range map[uint64]int{6: 1, 7: 0, 8: 1} {
- b := make([]byte, keyLen)
- binary.BigEndian.PutUint64(b, k)
- got, ok := SBTSearchAll(root, b, h)
- if want == 0 && ok {
- t.Fatalf("key=%d 应已删空, got %v", k, got)
- }
- if want > 0 && (!ok || len(got) != want) {
- t.Fatalf("key=%d want %d 条, got %v (ok=%v)", k, want, got, ok)
- }
- }
- checkSubtree(t, h, root)
- }
|