| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773 |
- // 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"
- "git.ycl.pub/yl32940/TsDB2/DB"
- tsindex "git.ycl.pub/yl32940/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)
- }
- }
|