package main import ( "git.ycl.pub/yl32940/TsDB2/DB" "git.ycl.pub/yl32940/TsDB2/DB/core" "fmt" "os" "time" ) // Record 测试表结构 — 通过 vrtx 标签声明索引,UseTable 自动创建 type Record struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 `vrtx:"index:idx_hash_age,type:hash"` Score int64 `vrtx:"index:idx_btree_score"` Dept string `vrtx:"maxlen:16"` } // PerfRecord 性能测试用 — 同一字段同时建 Hash 和 B+树 做对比 type PerfRecord struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 `vrtx:"index:idx_hash_age,type:hash"` Score int64 `vrtx:"index:idx_btree_age"` Dept string `vrtx:"maxlen:16"` } var dbPath string var passed, failed int func check(msg string, ok bool, detail ...string) { if ok { passed++ fmt.Printf(" ✅ %s\n", msg) } else { failed++ d := "" if len(detail) > 0 { d = " — " + detail[0] } fmt.Printf(" ❌ %s%s\n", msg, d) } } func section(title string) { fmt.Printf("\n%s %s\n", "==========", title) } // searchAge 用引擎的键编码查询 Age 索引, 返回查找错误 (nil 表示命中)。 // // 不能手拼 uint64(age) 大端: int64 列现在落 FieldType_Int64, 用保序编码 (符号位翻转), // 手拼的键在 v7 之后与盘上键不匹配, SearchByIndex 会一律 not found。 func searchAge(ct *core.Table, idxName string, age int64) error { key, err := ct.BuildIndexKey(idxName, age) if err != nil { return err } _, err = ct.SearchByIndex(idxName, key) return err } // countFindable 统计可被 Hash 索引查找到的 Age 值数量 func countFindable(db *DB.DB, tableName string, idxName string, minAge, maxAge int64) int { coreTbl, _ := db.CoreDB().FindTable(tableName) count := 0 for age := minAge; age < maxAge; age++ { if searchAge(coreTbl, idxName, age) == nil { count++ } } return count } func main() { dbPath = fmt.Sprintf("hash_test_%d.db", time.Now().UnixNano()) os.Remove(dbPath) fmt.Println("============================================================") fmt.Println(" TsDB2 Hash 桶式索引 — 完整测试") fmt.Println(" 覆盖: CRUD/分页/桶分裂(Split)/桶合并(Merge)/持久化/性能") fmt.Println("============================================================") // ============================================================ // 1. 建表 (索引由 vrtx 标签自动创建) // ============================================================ section("1. 建表 + 自动索引") db, err := DB.Open(dbPath) if err != nil { panic(err) } db.SetAutoCommitDelay(0) db.SetBufferPoolCapacity(4000) tbl, err := DB.UseTable[Record](db, "hash_test") check("UseTable[Record] 建表 (自动创建索引)", err == nil) coreTbl, _ := db.CoreDB().FindTable("hash_test") // 验证索引已自动创建 err = searchAge(coreTbl, "idx_hash_age", 25) check("idx_hash_age (Hash) 已自动创建", err == nil || err.Error() == "key not found") // ============================================================ // 2. 大批量插入 — 触发多页存储 (3000行, 每桶~187条目) // ============================================================ section("2. 大批量插入 (3000 行, 触发多页)") N1 := 3000 t0 := time.Now() for i := 1; i <= N1; i++ { err := tbl.Insert(&Record{ Name: fmt.Sprintf("User%05d", i), Age: int64(20 + i%100), Score: int64(i % 500), Dept: fmt.Sprintf("D%d", i%20), }) if err != nil { check(fmt.Sprintf("Insert ID=%d", i), false, err.Error()) break } } insertTime := time.Since(t0) check(fmt.Sprintf("插入 %d 行 耗时 %.2fs (%.0f rows/s)", N1, insertTime.Seconds(), float64(N1)/insertTime.Seconds()), true) if err := db.Commit(); err != nil { panic(err) } check("Commit 持久化", true) // 验证数据可查 found1 := countFindable(db, "hash_test", "idx_hash_age", 20, 120) check(fmt.Sprintf("Hash 查找: %d/100 个 Age 值可查", found1), found1 > 50) u100, err := tbl.Find(100) check("PK 查找 ID=100", err == nil && u100 != nil && u100.Name == "User00100") // 验证行数 cnt, _ := tbl.Count() check(fmt.Sprintf("行数: %d (应=%d)", cnt, N1), cnt == int64(N1)) // ============================================================ // 3. 桶分裂测试 — 集中插入触发 Split (阈值=800) // ============================================================ section("3. 桶分裂测试 (Split)") splitN := 1000 fmt.Printf(" 集中插入 %d 行 Age=25 (全部 hash 到同一桶, 阈值=800)...\n", splitN) t0 = time.Now() for i := N1 + 1; i <= N1+splitN; i++ { if err := tbl.Insert(&Record{ Name: fmt.Sprintf("Split%05d", i), Age: 25, Score: int64(i % 300), Dept: fmt.Sprintf("S%d", i%5), }); err != nil { check(fmt.Sprintf("Split Insert ID=%d", i), false, err.Error()) break } } check(fmt.Sprintf("集中插入 %d 行 耗时 %.2fs", splitN, time.Since(t0).Seconds()), true) if err := db.Commit(); err != nil { panic(err) } // 验证分裂后数据完整性 err25 := searchAge(coreTbl, "idx_hash_age", 25) check("分裂后 Hash 查找 Age=25", err25 == nil) splitFound := countFindable(db, "hash_test", "idx_hash_age", 20, 120) check(fmt.Sprintf("分裂后 Hash 查找: %d/100 个 Age 值可查", splitFound), splitFound > 50) uSplit, err := tbl.Find(uint64(N1 + 1)) check("分裂后 PK 查找", err == nil && uSplit != nil) cnt2, _ := tbl.Count() check(fmt.Sprintf("分裂后行数: %d (应=%d)", cnt2, N1+splitN), cnt2 == int64(N1+splitN)) // ============================================================ // 4. 删除测试 // ============================================================ section("4. Hash 索引删除测试") delN := 500 for i := 1; i <= delN; i++ { if err := tbl.Delete(uint64(i)); err != nil { check(fmt.Sprintf("Delete ID=%d", i), false, err.Error()) break } } check(fmt.Sprintf("删除 %d 行 (ID 1~%d)", delN, delN), true) if err := db.Commit(); err != nil { panic(err) } _, err = tbl.Find(1) check("删除后 PK 查找 ID=1 (应不存在)", err != nil) _, err = tbl.Find(uint64(delN + 1)) check(fmt.Sprintf("删除后 PK 查找 ID=%d (应存在)", delN+1), err == nil) err = searchAge(coreTbl, "idx_hash_age", 25) check("删除后 Hash 查找 Age=25", err == nil) // ============================================================ // 5. 桶合并测试 — 大量删除触发 Merge // ============================================================ section("5. 桶合并测试 (Merge)") deleteCount := 0 for i := uint64(delN + 1); i <= uint64(N1+splitN); i++ { if err := tbl.Delete(i); err == nil { deleteCount++ } } check(fmt.Sprintf("删除 %d 行", deleteCount), deleteCount > 0) if err := db.Commit(); err != nil { panic(err) } remaining, _ := tbl.Count() check(fmt.Sprintf("剩余行数: %d (已删除全部 %d 行)", remaining, N1+splitN), remaining == 0) // 验证全删后哈希索引仍可安全查询 err = searchAge(coreTbl, "idx_hash_age", 25) check("合并后 Hash 索引不崩溃", true) // ============================================================ // 6. 持久化测试 (关闭重开) // ============================================================ section("6. 持久化 + 重开验证") countBefore := remaining db.Close() check("关闭数据库", true) db, err = DB.Open(dbPath) if err != nil { panic(err) } db.SetBufferPoolCapacity(4000) tbl, err = DB.UseTable[Record](db, "hash_test") check("重开 UseTable[Record]", err == nil) coreTbl, _ = db.CoreDB().FindTable("hash_test") countAfter, _ := tbl.Count() check(fmt.Sprintf("重开后行数: %d (应=%d)", countAfter, countBefore), countAfter == countBefore, fmt.Sprintf("expected=%d actual=%d", countBefore, countAfter)) db.Close() // ============================================================ // 7. 性能对比: Hash vs B+树 (用 PerfRecord 同一字段两种索引) // ============================================================ section("7. 性能对比: Hash vs B+树") os.Remove(dbPath) db, _ = DB.Open(dbPath) db.SetAutoCommitDelay(0) db.SetBufferPoolCapacity(4000) perfTbl, _ := DB.UseTable[PerfRecord](db, "perf_test") perfCoreTbl, _ := db.CoreDB().FindTable("perf_test") N := 5000 t0 = time.Now() for i := 1; i <= N; i++ { perfTbl.Insert(&PerfRecord{ Name: fmt.Sprintf("Perf%06d", i), Age: int64(20 + i%200), Score: int64(i % 1000), Dept: fmt.Sprintf("D%d", i%30), }) } db.Commit() insertT := time.Since(t0) fmt.Printf(" 插入 %d 行: %.2fs (%.0f rows/s)\n", N, insertT.Seconds(), float64(N)/insertT.Seconds()) lookupN := 1000 t0 = time.Now() for i := 0; i < lookupN; i++ { age := int64(20 + i%200) searchAge(perfCoreTbl, "idx_hash_age", age) } hashTime := time.Since(t0) hashQPS := float64(lookupN) / hashTime.Seconds() fmt.Printf(" Hash 查找 %d 次: %.3fs (%.0f qps)\n", lookupN, hashTime.Seconds(), hashQPS) t0 = time.Now() for i := 0; i < lookupN; i++ { age := int64(20 + i%200) searchAge(perfCoreTbl, "idx_btree_age", age) } btreeTime := time.Since(t0) btreeQPS := float64(lookupN) / btreeTime.Seconds() fmt.Printf(" B+树 查找 %d 次: %.3fs (%.0f qps)\n", lookupN, btreeTime.Seconds(), btreeQPS) if hashQPS > 0 && btreeQPS > 0 { ratio := hashQPS / btreeQPS check(fmt.Sprintf("Hash/B+树 性能比: %.2fx", ratio), true) } db.Close() // ============================================================ // 总结 // ============================================================ section("测试总结") total := passed + failed fmt.Printf(" 总计: %d 项测试, ✅ 通过: %d, ❌ 失败: %d\n", total, passed, failed) if failed == 0 { fmt.Println(" 🎉 全部通过!Hash 桶式索引工作正常!") } else { fmt.Printf(" ⚠️ 有 %d 项测试失败,请检查\n", failed) } // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除 // os.Remove(dbPath) }