// TsDB2 ORM 全量特性验证 // // 覆盖 4 种索引形态, 全部通过 ORM 完成 增 / 删 / 改 / 查: // // 1. 主键表 BaseModel(_id) → 一级索引 IndexTypePrimary (B+ 树, 叶子存 DataPage) // 2. Hash 二级 Age type:hash → IndexTypeHash (等值 O(1), 范围 O(N) 全桶扫描) // 3. B+ 二级 Score (默认) → IndexTypeBTree (等值 + 范围 + 有序) // 4. 时序表 TimeSeriesModel(_ts) → IndexTypeTime (扁平时间索引链 + 分段 DataPage) // // 另外验证三条现在必须成立的约定: // // 5. 对象头部增删改查: 内嵌基类后, 记录自己带 Insert/Update/Delete/Save/Load/Reload // 6. 表必须内嵌基类: 没有基类的结构体不是本框架的表, UseTable 报 ErrNotModel; // 主键列可由每张表用 vrtx:"pk" 覆盖基类默认的 _id / _ts // 7. 同一时间戳多行: _ts 只决定落在哪个分段, 行号 (pkID) 才指认得了"段里哪一行"; // 行号随对象回到调用方, 于是同刻多行也能各自精确地改 / 删 / 重读 package main import ( "errors" "fmt" "os" "slices" "strings" "time" "TsDB2/DB" tsindex "TsDB2/DB/core/index" ) // Employee 主键表: _id 主键 + Hash 二级索引(Age) + B+ 二级索引(Score) type Employee struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 `vrtx:"index:idx_age,type:hash"` Score int64 `vrtx:"index:idx_score"` Dept string `vrtx:"maxlen:16"` } // Metric 时序表: _ts 主键 (时序索引) type Metric struct { DB.TimeSeriesModel Host string `vrtx:"maxlen:16"` Value int64 } // LateMetric 用于乱序写入 / 自动时间戳实验 type LateMetric struct { DB.TimeSeriesModel Value int64 } // SameTSMetric 同一时间戳下会出多行的时序表。 // // 刻意没有设备 / 测点 / 标签这类天然区分列 —— 这正是"只能靠行号区分"的最坏情况: // 时间戳只说得清"落在哪个分段", 只有行号指认得了"段里哪一行"。 type SameTSMetric struct { DB.TimeSeriesModel Val int64 } // Ticket 主键列由表自己设计: vrtx:"pk" 把基类默认的 _id 换成 No (_id 不再建列), // 头部增删改查照旧可用 —— 父类定义方法, 子类决定主键样式。 type Ticket struct { DB.BaseModel No int64 `vrtx:"pk"` Title string `vrtx:"maxlen:32"` } // notATable 没有内嵌表基类: 它不是本框架的表, UseTable 会直接拒绝。 // // 只有内嵌基类的结构体才有"行主键列 / 一级索引类型 / 头部方法"这三样东西 —— // 缺了基类, ORM 只能靠猜, 猜错的代价是一张插入时不回写主键、按写入值查不到的表。 type notATable struct { Name string `vrtx:"maxlen:32"` } const ( empN = 20000 metN = 20000 tsBase = int64(1_700_000_000_000) // 2023-11-14 22:13:20 UTC (毫秒) tsStep = int64(1000) // 每点间隔 1 秒 ) var passed, failed int func section(t string) { fmt.Printf("\n%s\n【%s】\n", strings.Repeat("=", 66), t) } func ok(name string, cond bool, detail string) { if cond { passed++ fmt.Printf(" [OK] %s\n", name) } else { failed++ fmt.Printf(" [FAIL] %s %s\n", name, detail) } } // ageOf 确定性分布: 偶数行 Age=25 (热点重复键), 奇数行 Age=50..99 func ageOf(i int) int64 { if i%2 == 0 { return 25 } return int64(50 + i%50) } // avgOf 对 f 执行 n 次, 返回单次均摊耗时 (批量均摊, 规避时钟粒度) func avgOf(n int, f func()) time.Duration { if n <= 0 { n = 1 } t := time.Now() for i := 0; i < n; i++ { f() } return time.Since(t) / time.Duration(n) } func main() { path := "orm_full.db" os.Remove(path) os.Remove(path + ".wal") fmt.Println(strings.Repeat("=", 66)) fmt.Println(" TsDB2 ORM 全量特性验证") fmt.Println(" 主键表(_id) / Hash 二级 / B+ 二级 / 时序表(_ts)") fmt.Println(strings.Repeat("=", 66)) db, err := DB.Open(path) if err != nil { panic(err) } db.SetAutoCommitDelay(0) db.SetDirtyThreshold(1 << 30) // 关闭阈值自动刷盘, 手动 Commit db.SetBufferPoolCapacity(8192) emp, err := DB.UseTable[Employee](db, "employee") if err != nil { panic(err) } met, err := DB.UseTable[Metric](db, "metric") if err != nil { panic(err) } late, err := DB.UseTable[LateMetric](db, "late_metric") if err != nil { panic(err) } dup, err := DB.UseTable[SameTSMetric](db, "same_ts_metric") if err != nil { panic(err) } // ============================================================= section("0. 建表与索引清单 (索引类型由 vrtx tag 自动决定)") empCore, _ := db.CoreDB().FindTable("employee") metCore, _ := db.CoreDB().FindTable("metric") for _, n := range []string{"pk_employee", "idx_age", "idx_score"} { if m, found := empCore.IndexMeta().FindIndex(n); found { fmt.Printf(" employee.%-13s IndexType=%d EntryPage=%d\n", n, m.IndexType, m.EntryPage) } else { fmt.Printf(" employee.%-13s 未找到\n", n) } } for _, n := range []string{"pk_metric"} { if m, found := metCore.IndexMeta().FindIndex(n); found { fmt.Printf(" metric.%-15s IndexType=%d EntryPage=%d\n", n, m.IndexType, m.EntryPage) } else { fmt.Printf(" metric.%-15s 未找到\n", n) } } if m, f := empCore.IndexMeta().FindIndex("idx_age"); f { ok("Age 字段使用 Hash 索引 (IndexType=6)", m.IndexType == 6, fmt.Sprintf("实际=%d", m.IndexType)) } if m, f := empCore.IndexMeta().FindIndex("idx_score"); f { ok("Score 字段使用 B+ 树索引 (IndexType=5)", m.IndexType == 5, fmt.Sprintf("实际=%d", m.IndexType)) } if m, f := empCore.IndexMeta().FindIndex("pk_employee"); f { ok("主键 _id 使用 Primary 索引 (IndexType=2)", m.IndexType == 2, fmt.Sprintf("实际=%d", m.IndexType)) } if m, f := metCore.IndexMeta().FindIndex("pk_metric"); f { ok("时序主键 _ts 使用 Time 索引 (IndexType=1)", m.IndexType == 1, fmt.Sprintf("实际=%d", m.IndexType)) } // ============================================================= section("1. 主键表 CRUD (Employee: _id + Hash(Age) + B+(Score))") // ---- 1.1 增 ---- t0 := time.Now() for i := 1; i <= empN; i++ { if err := emp.Insert(&Employee{ Name: fmt.Sprintf("Emp%06d", i), Age: ageOf(i), Score: int64(i % 1000), Dept: fmt.Sprintf("D%d", i%5), }); err != nil { panic(err) } } insDur := time.Since(t0) if err := db.Commit(); err != nil { panic(err) } cnt, _ := emp.Count() ok(fmt.Sprintf("Insert %d 行 (%.2fs, %.0f 行/s), Count=%d", empN, insDur.Seconds(), float64(empN)/insDur.Seconds(), cnt), cnt == empN, fmt.Sprintf("Count=%d", cnt)) // ---- 1.2 查 ---- // (a) 主键精确查询 r, err := emp.Find(12345) ok("主键 Find(12345) 命中且字段正确", err == nil && r != nil && r.ID == 12345 && r.Age == ageOf(12345), fmt.Sprintf("err=%v", err)) // (b) Hash 等值查询 keys := make([]int64, 0, 100) for i := 1; i <= 100; i++ { keys = append(keys, ageOf(i)) } // 说明: Age=25 是热点键(约 1 万行), 每次查询都要逐条回主键取行, // 迭代次数取 100 即可 (单次已远大于时钟粒度, 避免上千万次回查导致测试跑不完) k := 0 dHashEq := avgOf(100, func() { _, _ = emp.Where("Age", "=", keys[k%len(keys)]).Find() k++ }) hot, _ := emp.Where("Age", "=", int64(25)).Count() ok(fmt.Sprintf("Hash 等值查询 Age=25 → %d 行 (期望 %d), 均摊 %v", hot, empN/2, dHashEq), hot == int64(empN/2), fmt.Sprintf("got=%d", hot)) // (c) Hash 范围查询 (正确性 + 代价) expHashRange := int64(0) for i := 1; i <= empN; i++ { if a := ageOf(i); a >= 60 && a <= 70 { expHashRange++ } } hashRangeRows, _ := emp.Where("Age", ">=", int64(60)).Where("Age", "<=", int64(70)).Find() dHashRange := avgOf(20, func() { _, _ = emp.Where("Age", ">=", int64(60)).Where("Age", "<=", int64(70)).Find() }) inRange := true for _, e := range hashRangeRows { if e.Age < 60 || e.Age > 70 { inRange = false break } } ok(fmt.Sprintf("Hash 范围查询 Age∈[60,70] → %d 行 (期望 %d), 均摊 %v (全桶扫描)", len(hashRangeRows), expHashRange, dHashRange), len(hashRangeRows) == int(expHashRange) && inRange, fmt.Sprintf("len=%d", len(hashRangeRows))) // (d) B+ 范围查询 + 有序输出 scoreRows, _ := emp.Where("Score", ">=", int64(200)).Where("Score", "<=", int64(299)).OrderBy("Score", false).Find() dBTRee := avgOf(50, func() { _, _ = emp.Where("Score", ">=", int64(200)).Where("Score", "<=", int64(299)).Find() }) sorted := true for i := 1; i < len(scoreRows); i++ { if scoreRows[i-1].Score > scoreRows[i].Score { sorted = false break } } ok(fmt.Sprintf("B+ 范围查询 Score∈[200,299] → %d 行, 有序=%v, 均摊 %v", len(scoreRows), sorted, dBTRee), len(scoreRows) == 2000 && sorted, fmt.Sprintf("len=%d sorted=%v", len(scoreRows), sorted)) fmt.Printf(" → 延迟对比: Hash 等值 %v | Hash 范围 %v | B+ 范围 %v\n", dHashEq, dHashRange, dBTRee) // (e) 排序 / 分页 / 聚合 / 流式 top5, _ := emp.OrderBy("Score", true).Limit(5).Find() ok("OrderBy(Score desc).Limit(5) 返回 5 行且分数降序", len(top5) == 5 && top5[0].Score >= top5[4].Score, fmt.Sprintf("len=%d", len(top5))) pg2, _ := emp.OrderBy("Score", false).Offset(10).Limit(5).Find() ok("Offset(10).Limit(5) 分页返回 5 行", len(pg2) == 5, fmt.Sprintf("len=%d", len(pg2))) eqCnt, _ := emp.Where("Dept", "=", "D1").Count() ok(fmt.Sprintf("Where(Dept=D1).Count()=%d (期望 %d, 全表扫描无索引)", eqCnt, empN/5), eqCnt == int64(empN/5), fmt.Sprintf("got=%d", eqCnt)) first, err := emp.Where("Score", "=", int64(0)).First() ok("First() 返回单条", err == nil && first != nil, fmt.Sprintf("err=%v", err)) scanN := 0 _ = emp.Query().Limit(100).Scan(func(*Employee) bool { scanN++; return true }) ok("Scan 流式遍历 Limit(100) → 100 行", scanN == 100, fmt.Sprintf("got=%d", scanN)) all, _ := emp.FindAll() ok(fmt.Sprintf("FindAll() → %d 行", len(all)), len(all) == empN, fmt.Sprintf("got=%d", len(all))) // ---- 1.3 改 ---- // (a) 验证 ORM Update 是否整行覆盖 // 用奇数 ID, 避免把某行 Age 清零后干扰下面 Age=25 (偶数 ID) 的迁移计数 before, _ := emp.Find(1001) _ = emp.Update(&Employee{BaseModel: DB.BaseModel{ID: 1001}, Score: 777}) after, _ := emp.Find(1001) wholeRow := after != nil && after.Name == "" && after.Age == 0 && after.Dept == "" fmt.Printf(" [INFO] Update 只传 Score 后: Name=%q Age=%d Score=%d Dept=%q (改前 Name=%q)\n", after.Name, after.Age, after.Score, after.Dept, before.Name) ok("Update 为整行覆盖语义 (未赋值字段被清零)", wholeRow, fmt.Sprintf("after=%+v", after)) // (b) 改 Hash 索引字段 (Age) → 索引条目重建 // 把 ID 1..200 中 Age=25 的行 (偶数 ID) 改成 Age=123 // 123 不在原分布 {25} ∪ [50,99] 内, 便于精确校验索引迁移 const newAge = int64(123) moved := 0 for i := 1; i <= 200; i++ { row, _ := emp.Find(uint64(i)) if row == nil || row.Age != 25 { continue } moved++ if err := emp.Update(&Employee{ BaseModel: DB.BaseModel{ID: uint64(i)}, Name: row.Name, Age: newAge, Score: row.Score, Dept: row.Dept, }); err != nil { panic(err) } } c25, _ := emp.Where("Age", "=", int64(25)).Count() cNew, _ := emp.Where("Age", "=", newAge).Count() ok(fmt.Sprintf("改 Age(25→%d) %d 行后: Age=25 → %d (期望 %d), Age=%d → %d (期望 %d)", newAge, moved, c25, int64(empN/2-moved), newAge, cNew, int64(moved)), c25 == int64(empN/2-moved) && cNew == int64(moved), fmt.Sprintf("25=%d new=%d", c25, cNew)) // (c) 改 B+ 索引字段 (Score) → 索引条目重建 row1, _ := emp.Find(1) oldScore := row1.Score newScore := int64(5000) if err := emp.Update(&Employee{ BaseModel: DB.BaseModel{ID: 1}, Name: row1.Name, Age: row1.Age, Score: newScore, Dept: row1.Dept, }); err != nil { panic(err) } oldCnt, _ := emp.Where("Score", "=", oldScore).Count() newCnt, _ := emp.Where("Score", "=", newScore).Count() ok(fmt.Sprintf("改 Score(%d→%d): 旧值计数=%d, 新值计数=%d", oldScore, newScore, oldCnt, newCnt), newCnt == 1, fmt.Sprintf("old=%d new=%d", oldCnt, newCnt)) // ---- 1.4 删 ---- delBefore, _ := emp.Count() delFail := 0 for i := 1; i <= 200; i++ { if err := emp.Delete(uint64(i)); err != nil { delFail++ } } if err := db.Commit(); err != nil { panic(err) } delAfter, _ := emp.Count() _, gone := emp.Find(1) _, gone2 := emp.Find(200) _, alive := emp.Find(201) ok(fmt.Sprintf("Delete 200 行: Count %d→%d (失败 %d), Find(1)/(200) 失败, Find(201) 成功", delBefore, delAfter, delFail), delAfter == delBefore-200 && delFail == 0 && gone != nil && gone2 != nil && alive == nil, fmt.Sprintf("%v %v %v", gone, gone2, alive)) // 级联: 被删行的 Hash/B+ 索引条目应一并清除 // 之前迁到 Age=123 的 100 行正好是 ID 1..200, 全部删除 → 索引应归零 cNewAfter, _ := emp.Where("Age", "=", int64(123)).Count() s5000, _ := emp.Where("Score", "=", int64(5000)).Count() ok(fmt.Sprintf("删除后 Hash 索引级联清理: Age=123 → %d (期望 0); B+ 索引级联清理: Score=5000 → %d (期望 0)", cNewAfter, s5000), cNewAfter == 0 && s5000 == 0, fmt.Sprintf("age123=%d score5000=%d", cNewAfter, s5000)) // ============================================================= section("1.5 对象头部增删改查 + 表必须内嵌基类") // 内嵌基类之后, 增删改查被提升到对象自己身上 (父类定义方法): // 不用每次都把对象交回 Table, 适合"读出来 → 改几个字段 → 写回去"的写法。 hu := emp.New() // New: 创建 + 绑定 (知道自己属于哪张表) hu.Name, hu.Age, hu.Score, hu.Dept = "header", 33, 777, "HDR" if err := hu.Insert(); err != nil { // 增: 主键为 0 → 引擎发号并回写 hu.ID panic(err) } ok(fmt.Sprintf("头部 Insert: 自增主键回写 ID=%d, 对象自知表名 %q", hu.ID, hu.TableName()), hu.ID != 0 && hu.TableName() == "employee", "绑定信息缺失") // 改: 按对象自己的主键 (hu.ID) 整行覆盖 hu.Score = 778 if err := hu.Update(); err != nil { panic(err) } rowH, errH := emp.Find(hu.ID) ok("头部 Update: 按对象自己的主键整行更新", errH == nil && rowH != nil && rowH.Score == 778, fmt.Sprintf("row=%+v err=%v", rowH, errH)) // 查: Load / Reload 是"填自己" —— 头部方法没有类型参数, 拿不到 T, 也就没法返回 // *T; 要一个新建的、类型安全的对象请用 emp.Find。 loaded := emp.New() if err := loaded.Load(hu.ID); err != nil { panic(err) } ok("头部 Load: 按主键把整行读进对象自己", loaded.Name == "header" && loaded.Score == 778, fmt.Sprintf("%+v", loaded)) // 删: 按对象自己的主键 if err := hu.Delete(); err != nil { panic(err) } if _, goneH := emp.Find(hu.ID); goneH == nil { ok("头部 Delete: 按对象自己的主键删除", false, "删除后仍能查到") } else { ok("头部 Delete: 按对象自己的主键删除", true, "") } // 未绑定的对象调用头部方法 → 明确报错, 而不是崩或者写错表 if err := (&Employee{Name: "unbound"}).Insert(); !errors.Is(err, DB.ErrModelNotBound) { ok("未绑定对象头部 Insert 报 ErrModelNotBound", false, fmt.Sprintf("got %v", err)) } else { ok("未绑定对象头部 Insert 报 ErrModelNotBound", true, "") } // ---- 每张表自己设计主键列: vrtx:"pk" 覆盖基类默认的 _id ---- tkt, errT := DB.UseTable[Ticket](db, "ticket") if errT != nil { panic(errT) } tk := tkt.New() tk.No, tk.Title = 7, "自定义主键" if err := tk.Insert(); err != nil { panic(err) } tk.Title = "改过" if err := tk.Update(); err != nil { // 按 tk.No 定位, 不是 _id panic(err) } tkGot, errTk := tkt.Find(7) cols := tkt.SchemaColumns() ok(fmt.Sprintf("vrtx:\"pk\": 主键列变成 No, 基类默认的 _id 不再建列 (实际列=%v)", cols), errTk == nil && tkGot.No == 7 && tkGot.Title == "改过" && !slices.Contains(cols, "_id"), fmt.Sprintf("%+v err=%v", tkGot, errTk)) // ---- 没有内嵌基类的结构体不是本框架的表: UseTable 直接拒绝 ---- if _, err := DB.UseTable[notATable](db, "not_a_table"); !errors.Is(err, DB.ErrNotModel) { ok("UseTable[未内嵌基类] 报 ErrNotModel", false, fmt.Sprintf("got %v", err)) } else { ok("UseTable[未内嵌基类] 报 ErrNotModel", true, "") } if _, found := db.CoreDB().FindTable("not_a_table"); found { ok("被拒绝的模型不留下表", false, "表被建出来了") } else { ok("被拒绝的模型不留下表 (校验发生在建表之前)", true, "") } if err := db.Commit(); err != nil { panic(err) } // ============================================================= section("2. 时序表 CRUD (Metric: _ts + 时序索引)") // ---- 2.1 增 (按时间递增顺序写入, 时序场景标准写法) ---- t1 := time.Now() for i := 0; i < metN; i++ { if err := met.Insert(&Metric{ TimeSeriesModel: DB.TimeSeriesModel{TS: uint64(tsBase + int64(i)*tsStep)}, Host: fmt.Sprintf("h%d", i%4), Value: int64(i * 7 % 1000), }); err != nil { panic(err) } } metInsDur := time.Since(t1) if err := db.Commit(); err != nil { panic(err) } mc, _ := met.Count() ok(fmt.Sprintf("顺序写入 %d 个时间点 (%.2fs, %.0f 点/s), Count=%d", metN, metInsDur.Seconds(), float64(metN)/metInsDur.Seconds(), mc), mc == metN, fmt.Sprintf("Count=%d", mc)) // ---- 2.2 查 ---- tsMid := int64(tsBase + 12345*tsStep) mr, err := met.Find(uint64(tsMid)) ok("时序主键 Find(ts) 精确命中", err == nil && mr != nil && int64(mr.TS) == tsMid, fmt.Sprintf("err=%v", err)) // 时间范围查询: 用列名 _ts (主键列名) lo := int64(tsBase + 3000*tsStep) hi := int64(tsBase + 3999*tsStep) rangeRows, err := met.Where("_ts", ">=", lo).Where("_ts", "<=", hi).OrderBy("_ts", false).Find() tsSorted, tsInRange := true, true for i := 0; i < len(rangeRows); i++ { if int64(rangeRows[i].TS) < lo || int64(rangeRows[i].TS) > hi { tsInRange = false } if i > 0 && rangeRows[i-1].TS > rangeRows[i].TS { tsSorted = false } } dTSRange := avgOf(50, func() { _, _ = met.Where("_ts", ">=", lo).Where("_ts", "<=", hi).Find() }) ok(fmt.Sprintf("时间范围 [_ts∈3.0s~3.999s] → %d 行 (期望 1000), 有序=%v, 在界内=%v, 均摊 %v", len(rangeRows), tsSorted, tsInRange, dTSRange), err == nil && len(rangeRows) == 1000 && tsSorted && tsInRange, fmt.Sprintf("len=%d err=%v", len(rangeRows), err)) // 反向时间范围 (最近 N 个点) latest, _ := met.OrderBy("_ts", true).Limit(10).Find() desc := true for i := 1; i < len(latest); i++ { if latest[i-1].TS < latest[i].TS { desc = false break } } ok("OrderBy(_ts desc).Limit(10) 取到最新 10 个点且降序", len(latest) == 10 && desc, fmt.Sprintf("len=%d desc=%v", len(latest), desc)) // 时序表上的非主键条件 (Host 无索引 → 全表扫描过滤) hostCnt, _ := met.Where("Host", "=", "h2").Count() ok(fmt.Sprintf("Where(Host=h2).Count()=%d (期望 %d, 无索引全扫描)", hostCnt, metN/4), hostCnt == int64(metN/4), fmt.Sprintf("got=%d", hostCnt)) // ---- 2.3 改 (时序行原地更新) ---- updTS := uint64(tsBase + 5000*tsStep) _ = met.Update(&Metric{ TimeSeriesModel: DB.TimeSeriesModel{TS: updTS}, Host: "hX", Value: 9999, }) mu, err := met.Find(updTS) ok("时序行 Update → Find 校验 Value=9999", err == nil && mu != nil && mu.Value == 9999 && mu.Host == "hX", fmt.Sprintf("err=%v row=%+v", err, mu)) // ---- 2.4 删 ---- delTS := uint64(tsBase + 6000*tsStep) if err := met.Delete(delTS); err != nil { panic(err) } if err := db.Commit(); err != nil { panic(err) } _, errGone := met.Find(delTS) mcAfter, _ := met.Count() ok(fmt.Sprintf("时序行 Delete(ts) 后: Find 失败, Count=%d (期望 %d)", mcAfter, metN-1), errGone != nil && mcAfter == int64(metN-1), fmt.Sprintf("err=%v cnt=%d", errGone, mcAfter)) // ---- 2.5 同一时间戳多行 (时间戳之外的第二个定位维度: 行号) ---- // _ts 只决定"落在哪个分段", 指认不了"段里哪一行"。同一时刻出多行是时序数据的常态 // (同一时刻的多个测点 / 多次事件), 所以引擎给每行另发一个隐藏行号 (pkID)。行号随对象 // 回到调用方, 于是"查出来 → 改几个字段 → 写回去"在同刻多行上也精确到行。 const sameTS = uint64(tsBase + 9000*tsStep) rowA, rowB := dup.New(), dup.New() rowA.TS, rowA.Val = sameTS, 1 rowB.TS, rowB.Val = sameTS, 2 if err := rowA.Insert(); err != nil { panic(err) } if err := rowB.Insert(); err != nil { panic(err) } // Table.Insert(手工对象) 同样可以: 三种写法共用同一条写入路径 if err := dup.Insert(&SameTSMetric{TimeSeriesModel: DB.TimeSeriesModel{TS: sameTS}, Val: 3}); err != nil { panic(err) } if err := db.Commit(); err != nil { panic(err) } sameCnt, _ := dup.Where("_ts", "=", sameTS).Count() ok(fmt.Sprintf("同一时间戳容许多行: 写入 3 行, 同刻 Count=%d (期望 3)", sameCnt), sameCnt == 3, fmt.Sprintf("got=%d", sameCnt)) // 每行都带着自己的行号 → 各自精确改, 不串行 sameRows, _ := dup.Where("_ts", "=", sameTS).Find() for i, r := range sameRows { r.Val = int64(100 + i) if err := r.Update(); err != nil { panic(err) } } if err := db.Commit(); err != nil { panic(err) } afterRows, _ := dup.Where("_ts", "=", sameTS).Find() vals := make([]int64, 0, len(afterRows)) for _, r := range afterRows { vals = append(vals, r.Val) } ok(fmt.Sprintf("同刻 3 行各自 Update: 值=%v (期望 [100 101 102])", vals), len(vals) == 3 && vals[0] == 100 && vals[1] == 101 && vals[2] == 102, fmt.Sprintf("got=%v", vals)) // 只删中间那一行: 同刻的其它行不能被牵连 if err := afterRows[1].Delete(); err != nil { panic(err) } if err := db.Commit(); err != nil { panic(err) } leftCnt, _ := dup.Where("_ts", "=", sameTS).Count() ok(fmt.Sprintf("同刻多行只删指定的那一行: 剩余 Count=%d (期望 2)", leftCnt), leftCnt == 2, fmt.Sprintf("got=%d", leftCnt)) // 剩下的各行仍按行号精确读回 (Value 应为删掉 101 之后的 [100 102]) restRows, _ := dup.Where("_ts", "=", sameTS).Find() reloadOK := len(restRows) == 2 gotVals := make([]int64, 0, len(restRows)) for i, r := range restRows { if err := r.Reload(); err != nil || r.Val != int64(100+i*2) { reloadOK = false } gotVals = append(gotVals, r.Val) } ok(fmt.Sprintf("删后其余各行 Reload 仍读回自己那一行: 值=%v (期望 [100 102])", gotVals), reloadOK, fmt.Sprintf("got=%v", gotVals)) // ============================================================= section("3. 时序写入的顺序约束 (乱序拒绝 / 缺省时间戳)") // ---- 3.1 乱序写入: 时序索引只收非递减写入, 更早的点一律拒绝 ---- for i := 0; i < 1000; i++ { _ = late.Insert(&LateMetric{ TimeSeriesModel: DB.TimeSeriesModel{TS: uint64(tsBase + int64(i)*tsStep)}, Value: int64(i), }) } if err := db.Commit(); err != nil { panic(err) } lc, _ := late.Count() ok(fmt.Sprintf("LateMetric 顺序写入 1000 点, Count=%d", lc), lc == 1000, fmt.Sprintf("cnt=%d", lc)) // 已在 0..999s 区间落盘后, 再补写中间时间点 (乱序 late data): 必须被拒。 // // 时序索引只有时间一个维度 —— 顺序写入时每个分段天然覆盖一段互不重叠的时间区间、 // 段内也天然有序, 范围查询才裁剪得动分段。放乱序进来会让段区间互相重叠、段内失序, // 查询退化成"全链 + 整段逐行过滤", 所以引擎直接拒绝, 并给出可判定的 ErrOutOfOrder。 // (确有乱序数据时, 先按时间排序再写, 或改用普通表 + 二级索引。) lateTS := []uint64{ uint64(tsBase + 500*tsStep + 1), uint64(tsBase + 500*tsStep + 2), uint64(tsBase + 500*tsStep + 3), } lateInsErr, lateOOOErr := 0, 0 for _, t := range lateTS { err := late.Insert(&LateMetric{TimeSeriesModel: DB.TimeSeriesModel{TS: t}, Value: int64(t)}) if err != nil { lateInsErr++ if errors.Is(err, tsindex.ErrOutOfOrder) { lateOOOErr++ } } } _ = db.Commit() lateFound := 0 for _, t := range lateTS { if row, e := late.Find(t); e == nil && row != nil { lateFound++ } } loL := int64(tsBase) hiL := int64(tsBase + 999*tsStep) lateRows, _ := late.Where("_ts", ">=", loL).Where("_ts", "<=", hiL).Find() fmt.Printf(" [INFO] 乱序补写 3 个中间时间点: 被拒=%d/3 (ErrOutOfOrder=%d), 主键 Find 命中=%d/3, 时间范围扫描回读=%d 行 (期望 1000)\n", lateInsErr, lateOOOErr, lateFound, len(lateRows)) ok("乱序补写的中间时间点全部被拒 (3/3)", lateInsErr == 3, fmt.Sprintf("被拒 %d/3", lateInsErr)) ok("拒绝原因可判定为 ErrOutOfOrder", lateOOOErr == 3, fmt.Sprintf("ErrOutOfOrder=%d/3", lateOOOErr)) ok("被拒的乱序点没有落盘 (主键 Find 0/3)", lateFound == 0, fmt.Sprintf("命中 %d/3", lateFound)) ok("时间范围扫描仍是顺序写入的 1000 行", len(lateRows) == 1000, fmt.Sprintf("got=%d", len(lateRows))) // ---- 3.2 缺省时间戳自动分配 ---- // 时序键就是时间戳本身。_ts 缺省时 ORM 补的是"写入这一刻"的纳秒级时间戳 —— 与显式 // 传入的时间戳同量纲, 因此它天然大于此前写入的时间戳, 顺序校验不会拦它。 autoTSBefore := uint64(time.Now().UnixNano()) auto := &LateMetric{Value: -1} // 不设置 TS if err := late.Insert(auto); err != nil { panic(err) } _ = db.Commit() autoTSAfter := uint64(time.Now().UnixNano()) fmt.Printf(" [INFO] 缺省 _ts 插入后, 结构体回写得到的 TS=%d (纳秒时间戳)\n", auto.TS) ok("缺省 _ts 由 ORM 补当前纳秒时间戳 (落在插入前后之间)", auto.TS >= autoTSBefore && auto.TS <= autoTSAfter, fmt.Sprintf("TS=%d 不在 [%d,%d]", auto.TS, autoTSBefore, autoTSAfter)) // ============================================================= section("4. 事务 (ORM 增删改 + 提交/回滚)") txn := db.BeginTransaction() for i := 0; i < 10; i++ { _ = emp.Insert(&Employee{Name: fmt.Sprintf("Txn%03d", i), Age: 33, Score: 333, Dept: "TX"}) } if err := txn.Commit(); err != nil { panic(err) } txnCnt, _ := emp.Where("Dept", "=", "TX").Count() ok(fmt.Sprintf("事务内插入 10 行并提交 → Where(Dept=TX)=%d", txnCnt), txnCnt == 10, fmt.Sprintf("got=%d", txnCnt)) txn2 := db.BeginTransaction() for i := 0; i < 10; i++ { _ = emp.Insert(&Employee{Name: fmt.Sprintf("Roll%03d", i), Age: 44, Score: 444, Dept: "RB"}) } if err := txn2.Rollback(); err != nil { panic(err) } rbCnt, _ := emp.Where("Dept", "=", "RB").Count() ok(fmt.Sprintf("事务内插入后回滚 → Where(Dept=RB)=%d (期望 0)", rbCnt), rbCnt == 0, fmt.Sprintf("got=%d", rbCnt)) // ============================================================= section("5. 关闭重开 (持久化验证)") empBefore, _ := emp.Count() metBefore, _ := met.Count() if err := db.Close(); err != nil { panic(err) } db, err = DB.Open(path) if err != nil { panic(err) } db.SetAutoCommitDelay(0) db.SetDirtyThreshold(1 << 30) emp, _ = DB.UseTable[Employee](db, "employee") met, _ = DB.UseTable[Metric](db, "metric") empAfter, _ := emp.Count() metAfter, _ := met.Count() ok(fmt.Sprintf("重开后 employee Count %d → %d", empBefore, empAfter), empBefore == empAfter, fmt.Sprintf("%d/%d", empBefore, empAfter)) ok(fmt.Sprintf("重开后 metric Count %d → %d", metBefore, metAfter), metBefore == metAfter, fmt.Sprintf("%d/%d", metBefore, metAfter)) rpk, err := emp.Find(201) ok("重开后主键 Find(201) 命中", err == nil && rpk != nil, fmt.Sprintf("err=%v", err)) rt, err := met.Find(uint64(tsBase + 9000*tsStep)) ok("重开后时序 Find(ts) 命中", err == nil && rt != nil, fmt.Sprintf("err=%v", err)) h25, _ := emp.Where("Age", "=", int64(25)).Count() ok(fmt.Sprintf("重开后 Hash 索引可用: Age=25 → %d", h25), h25 > 0, fmt.Sprintf("got=%d", h25)) s300, _ := emp.Where("Score", ">=", int64(300)).Where("Score", "<=", int64(399)).Find() ok(fmt.Sprintf("重开后 B+ 索引可用: Score∈[300,399] → %d 行", len(s300)), len(s300) > 0, "") if err := db.Close(); err != nil { panic(err) } fi, _ := os.Stat(path) if fi != nil { fmt.Printf("\n 数据库文件: %.2f MB\n", float64(fi.Size())/1024/1024) } // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除 // os.Remove(path) // os.Remove(path + ".wal") // ============================================================= section("6. 四种索引形态特性对比") printRow := func(name, typ, struct_, leaf, cx, decl string) { fmt.Printf(" %s\n IndexType = %s\n 底层结构 = %s\n 叶子/条目 = %s\n 查询复杂度 = %s\n ORM 声明 = %s\n", name, typ, struct_, leaf, cx, decl) } printRow("主键 _id", "2 Primary", "B+ 树", "叶子直接存行数据(DataPage)", "等值/范围 O(logN), 天然按 pk 有序", "内嵌 DB.BaseModel") printRow("Hash 二级", "6 Hash", "桶目录 + 溢出链", "叶子存 pkID, 需回主键取行", "等值 O(1); 范围=全桶扫描 O(N)", `vrtx:"index:x,type:hash"`) printRow("B+ 二级", "5 BTree", "B+ 树", "叶子存 pkID, 需回主键取行", "等值/范围/有序 均 O(logN)", `vrtx:"index:x" (默认)`) printRow("时序 _ts", "1 Time", "扁平时间条目链", "[Begin,Last,DataPageID] → 分段 DataPage", "时间范围只加载命中分段; 等值需扫段内行", "内嵌 DB.TimeSeriesModel") fmt.Printf("\n%s\n", strings.Repeat("=", 66)) fmt.Printf(" 总计: 通过 %d, 失败 %d\n", passed, failed) fmt.Println(strings.Repeat("=", 66)) if failed > 0 { os.Exit(1) } }