main.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. // TsDB2 ORM 全量特性验证
  2. //
  3. // 覆盖 4 种索引形态, 全部通过 ORM 完成 增 / 删 / 改 / 查:
  4. //
  5. // 1. 主键表 BaseModel(_id) → 一级索引 IndexTypePrimary (B+ 树, 叶子存 DataPage)
  6. // 2. Hash 二级 Age type:hash → IndexTypeHash (等值 O(1), 范围 O(N) 全桶扫描)
  7. // 3. B+ 二级 Score (默认) → IndexTypeBTree (等值 + 范围 + 有序)
  8. // 4. 时序表 TimeSeriesModel(_ts) → IndexTypeTime (扁平时间索引链 + 分段 DataPage)
  9. //
  10. // 另外验证三条现在必须成立的约定:
  11. //
  12. // 5. 对象头部增删改查: 内嵌基类后, 记录自己带 Insert/Update/Delete/Save/Load/Reload
  13. // 6. 表必须内嵌基类: 没有基类的结构体不是本框架的表, UseTable 报 ErrNotModel;
  14. // 主键列可由每张表用 vrtx:"pk" 覆盖基类默认的 _id / _ts
  15. // 7. 同一时间戳多行: _ts 只决定落在哪个分段, 行号 (pkID) 才指认得了"段里哪一行";
  16. // 行号随对象回到调用方, 于是同刻多行也能各自精确地改 / 删 / 重读
  17. package main
  18. import (
  19. "errors"
  20. "fmt"
  21. "os"
  22. "slices"
  23. "strings"
  24. "time"
  25. "git.ycl.pub/yl32940/TsDB2/DB"
  26. tsindex "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  27. )
  28. // Employee 主键表: _id 主键 + Hash 二级索引(Age) + B+ 二级索引(Score)
  29. type Employee struct {
  30. DB.BaseModel
  31. Name string `vrtx:"maxlen:32"`
  32. Age int64 `vrtx:"index:idx_age,type:hash"`
  33. Score int64 `vrtx:"index:idx_score"`
  34. Dept string `vrtx:"maxlen:16"`
  35. }
  36. // Metric 时序表: _ts 主键 (时序索引)
  37. type Metric struct {
  38. DB.TimeSeriesModel
  39. Host string `vrtx:"maxlen:16"`
  40. Value int64
  41. }
  42. // LateMetric 用于乱序写入 / 自动时间戳实验
  43. type LateMetric struct {
  44. DB.TimeSeriesModel
  45. Value int64
  46. }
  47. // SameTSMetric 同一时间戳下会出多行的时序表。
  48. //
  49. // 刻意没有设备 / 测点 / 标签这类天然区分列 —— 这正是"只能靠行号区分"的最坏情况:
  50. // 时间戳只说得清"落在哪个分段", 只有行号指认得了"段里哪一行"。
  51. type SameTSMetric struct {
  52. DB.TimeSeriesModel
  53. Val int64
  54. }
  55. // Ticket 主键列由表自己设计: vrtx:"pk" 把基类默认的 _id 换成 No (_id 不再建列),
  56. // 头部增删改查照旧可用 —— 父类定义方法, 子类决定主键样式。
  57. type Ticket struct {
  58. DB.BaseModel
  59. No int64 `vrtx:"pk"`
  60. Title string `vrtx:"maxlen:32"`
  61. }
  62. // notATable 没有内嵌表基类: 它不是本框架的表, UseTable 会直接拒绝。
  63. //
  64. // 只有内嵌基类的结构体才有"行主键列 / 一级索引类型 / 头部方法"这三样东西 ——
  65. // 缺了基类, ORM 只能靠猜, 猜错的代价是一张插入时不回写主键、按写入值查不到的表。
  66. type notATable struct {
  67. Name string `vrtx:"maxlen:32"`
  68. }
  69. const (
  70. empN = 20000
  71. metN = 20000
  72. tsBase = int64(1_700_000_000_000) // 2023-11-14 22:13:20 UTC (毫秒)
  73. tsStep = int64(1000) // 每点间隔 1 秒
  74. )
  75. var passed, failed int
  76. func section(t string) {
  77. fmt.Printf("\n%s\n【%s】\n", strings.Repeat("=", 66), t)
  78. }
  79. func ok(name string, cond bool, detail string) {
  80. if cond {
  81. passed++
  82. fmt.Printf(" [OK] %s\n", name)
  83. } else {
  84. failed++
  85. fmt.Printf(" [FAIL] %s %s\n", name, detail)
  86. }
  87. }
  88. // ageOf 确定性分布: 偶数行 Age=25 (热点重复键), 奇数行 Age=50..99
  89. func ageOf(i int) int64 {
  90. if i%2 == 0 {
  91. return 25
  92. }
  93. return int64(50 + i%50)
  94. }
  95. // avgOf 对 f 执行 n 次, 返回单次均摊耗时 (批量均摊, 规避时钟粒度)
  96. func avgOf(n int, f func()) time.Duration {
  97. if n <= 0 {
  98. n = 1
  99. }
  100. t := time.Now()
  101. for i := 0; i < n; i++ {
  102. f()
  103. }
  104. return time.Since(t) / time.Duration(n)
  105. }
  106. func main() {
  107. path := "orm_full.db"
  108. os.Remove(path)
  109. os.Remove(path + ".wal")
  110. fmt.Println(strings.Repeat("=", 66))
  111. fmt.Println(" TsDB2 ORM 全量特性验证")
  112. fmt.Println(" 主键表(_id) / Hash 二级 / B+ 二级 / 时序表(_ts)")
  113. fmt.Println(strings.Repeat("=", 66))
  114. db, err := DB.Open(path)
  115. if err != nil {
  116. panic(err)
  117. }
  118. db.SetAutoCommitDelay(0)
  119. db.SetDirtyThreshold(1 << 30) // 关闭阈值自动刷盘, 手动 Commit
  120. db.SetBufferPoolCapacity(8192)
  121. emp, err := DB.UseTable[Employee](db, "employee")
  122. if err != nil {
  123. panic(err)
  124. }
  125. met, err := DB.UseTable[Metric](db, "metric")
  126. if err != nil {
  127. panic(err)
  128. }
  129. late, err := DB.UseTable[LateMetric](db, "late_metric")
  130. if err != nil {
  131. panic(err)
  132. }
  133. dup, err := DB.UseTable[SameTSMetric](db, "same_ts_metric")
  134. if err != nil {
  135. panic(err)
  136. }
  137. // =============================================================
  138. section("0. 建表与索引清单 (索引类型由 vrtx tag 自动决定)")
  139. empCore, _ := db.CoreDB().FindTable("employee")
  140. metCore, _ := db.CoreDB().FindTable("metric")
  141. for _, n := range []string{"pk_employee", "idx_age", "idx_score"} {
  142. if m, found := empCore.IndexMeta().FindIndex(n); found {
  143. fmt.Printf(" employee.%-13s IndexType=%d EntryPage=%d\n", n, m.IndexType, m.EntryPage)
  144. } else {
  145. fmt.Printf(" employee.%-13s 未找到\n", n)
  146. }
  147. }
  148. for _, n := range []string{"pk_metric"} {
  149. if m, found := metCore.IndexMeta().FindIndex(n); found {
  150. fmt.Printf(" metric.%-15s IndexType=%d EntryPage=%d\n", n, m.IndexType, m.EntryPage)
  151. } else {
  152. fmt.Printf(" metric.%-15s 未找到\n", n)
  153. }
  154. }
  155. if m, f := empCore.IndexMeta().FindIndex("idx_age"); f {
  156. ok("Age 字段使用 Hash 索引 (IndexType=6)", m.IndexType == 6, fmt.Sprintf("实际=%d", m.IndexType))
  157. }
  158. if m, f := empCore.IndexMeta().FindIndex("idx_score"); f {
  159. ok("Score 字段使用 B+ 树索引 (IndexType=5)", m.IndexType == 5, fmt.Sprintf("实际=%d", m.IndexType))
  160. }
  161. if m, f := empCore.IndexMeta().FindIndex("pk_employee"); f {
  162. ok("主键 _id 使用 Primary 索引 (IndexType=2)", m.IndexType == 2, fmt.Sprintf("实际=%d", m.IndexType))
  163. }
  164. if m, f := metCore.IndexMeta().FindIndex("pk_metric"); f {
  165. ok("时序主键 _ts 使用 Time 索引 (IndexType=1)", m.IndexType == 1, fmt.Sprintf("实际=%d", m.IndexType))
  166. }
  167. // =============================================================
  168. section("1. 主键表 CRUD (Employee: _id + Hash(Age) + B+(Score))")
  169. // ---- 1.1 增 ----
  170. t0 := time.Now()
  171. for i := 1; i <= empN; i++ {
  172. if err := emp.Insert(&Employee{
  173. Name: fmt.Sprintf("Emp%06d", i),
  174. Age: ageOf(i),
  175. Score: int64(i % 1000),
  176. Dept: fmt.Sprintf("D%d", i%5),
  177. }); err != nil {
  178. panic(err)
  179. }
  180. }
  181. insDur := time.Since(t0)
  182. if err := db.Commit(); err != nil {
  183. panic(err)
  184. }
  185. cnt, _ := emp.Count()
  186. ok(fmt.Sprintf("Insert %d 行 (%.2fs, %.0f 行/s), Count=%d", empN, insDur.Seconds(), float64(empN)/insDur.Seconds(), cnt),
  187. cnt == empN, fmt.Sprintf("Count=%d", cnt))
  188. // ---- 1.2 查 ----
  189. // (a) 主键精确查询
  190. r, err := emp.Find(12345)
  191. ok("主键 Find(12345) 命中且字段正确",
  192. err == nil && r != nil && r.ID == 12345 && r.Age == ageOf(12345),
  193. fmt.Sprintf("err=%v", err))
  194. // (b) Hash 等值查询
  195. keys := make([]int64, 0, 100)
  196. for i := 1; i <= 100; i++ {
  197. keys = append(keys, ageOf(i))
  198. }
  199. // 说明: Age=25 是热点键(约 1 万行), 每次查询都要逐条回主键取行,
  200. // 迭代次数取 100 即可 (单次已远大于时钟粒度, 避免上千万次回查导致测试跑不完)
  201. k := 0
  202. dHashEq := avgOf(100, func() {
  203. _, _ = emp.Where("Age", "=", keys[k%len(keys)]).Find()
  204. k++
  205. })
  206. hot, _ := emp.Where("Age", "=", int64(25)).Count()
  207. ok(fmt.Sprintf("Hash 等值查询 Age=25 → %d 行 (期望 %d), 均摊 %v", hot, empN/2, dHashEq),
  208. hot == int64(empN/2), fmt.Sprintf("got=%d", hot))
  209. // (c) Hash 范围查询 (正确性 + 代价)
  210. expHashRange := int64(0)
  211. for i := 1; i <= empN; i++ {
  212. if a := ageOf(i); a >= 60 && a <= 70 {
  213. expHashRange++
  214. }
  215. }
  216. hashRangeRows, _ := emp.Where("Age", ">=", int64(60)).Where("Age", "<=", int64(70)).Find()
  217. dHashRange := avgOf(20, func() {
  218. _, _ = emp.Where("Age", ">=", int64(60)).Where("Age", "<=", int64(70)).Find()
  219. })
  220. inRange := true
  221. for _, e := range hashRangeRows {
  222. if e.Age < 60 || e.Age > 70 {
  223. inRange = false
  224. break
  225. }
  226. }
  227. ok(fmt.Sprintf("Hash 范围查询 Age∈[60,70] → %d 行 (期望 %d), 均摊 %v (全桶扫描)",
  228. len(hashRangeRows), expHashRange, dHashRange),
  229. len(hashRangeRows) == int(expHashRange) && inRange, fmt.Sprintf("len=%d", len(hashRangeRows)))
  230. // (d) B+ 范围查询 + 有序输出
  231. scoreRows, _ := emp.Where("Score", ">=", int64(200)).Where("Score", "<=", int64(299)).OrderBy("Score", false).Find()
  232. dBTRee := avgOf(50, func() {
  233. _, _ = emp.Where("Score", ">=", int64(200)).Where("Score", "<=", int64(299)).Find()
  234. })
  235. sorted := true
  236. for i := 1; i < len(scoreRows); i++ {
  237. if scoreRows[i-1].Score > scoreRows[i].Score {
  238. sorted = false
  239. break
  240. }
  241. }
  242. ok(fmt.Sprintf("B+ 范围查询 Score∈[200,299] → %d 行, 有序=%v, 均摊 %v", len(scoreRows), sorted, dBTRee),
  243. len(scoreRows) == 2000 && sorted, fmt.Sprintf("len=%d sorted=%v", len(scoreRows), sorted))
  244. fmt.Printf(" → 延迟对比: Hash 等值 %v | Hash 范围 %v | B+ 范围 %v\n", dHashEq, dHashRange, dBTRee)
  245. // (e) 排序 / 分页 / 聚合 / 流式
  246. top5, _ := emp.OrderBy("Score", true).Limit(5).Find()
  247. ok("OrderBy(Score desc).Limit(5) 返回 5 行且分数降序",
  248. len(top5) == 5 && top5[0].Score >= top5[4].Score, fmt.Sprintf("len=%d", len(top5)))
  249. pg2, _ := emp.OrderBy("Score", false).Offset(10).Limit(5).Find()
  250. ok("Offset(10).Limit(5) 分页返回 5 行", len(pg2) == 5, fmt.Sprintf("len=%d", len(pg2)))
  251. eqCnt, _ := emp.Where("Dept", "=", "D1").Count()
  252. ok(fmt.Sprintf("Where(Dept=D1).Count()=%d (期望 %d, 全表扫描无索引)", eqCnt, empN/5),
  253. eqCnt == int64(empN/5), fmt.Sprintf("got=%d", eqCnt))
  254. first, err := emp.Where("Score", "=", int64(0)).First()
  255. ok("First() 返回单条", err == nil && first != nil, fmt.Sprintf("err=%v", err))
  256. scanN := 0
  257. _ = emp.Query().Limit(100).Scan(func(*Employee) bool { scanN++; return true })
  258. ok("Scan 流式遍历 Limit(100) → 100 行", scanN == 100, fmt.Sprintf("got=%d", scanN))
  259. all, _ := emp.FindAll()
  260. ok(fmt.Sprintf("FindAll() → %d 行", len(all)), len(all) == empN, fmt.Sprintf("got=%d", len(all)))
  261. // ---- 1.3 改 ----
  262. // (a) 验证 ORM Update 是否整行覆盖
  263. // 用奇数 ID, 避免把某行 Age 清零后干扰下面 Age=25 (偶数 ID) 的迁移计数
  264. before, _ := emp.Find(1001)
  265. _ = emp.Update(&Employee{BaseModel: DB.BaseModel{ID: 1001}, Score: 777})
  266. after, _ := emp.Find(1001)
  267. wholeRow := after != nil && after.Name == "" && after.Age == 0 && after.Dept == ""
  268. fmt.Printf(" [INFO] Update 只传 Score 后: Name=%q Age=%d Score=%d Dept=%q (改前 Name=%q)\n",
  269. after.Name, after.Age, after.Score, after.Dept, before.Name)
  270. ok("Update 为整行覆盖语义 (未赋值字段被清零)", wholeRow, fmt.Sprintf("after=%+v", after))
  271. // (b) 改 Hash 索引字段 (Age) → 索引条目重建
  272. // 把 ID 1..200 中 Age=25 的行 (偶数 ID) 改成 Age=123
  273. // 123 不在原分布 {25} ∪ [50,99] 内, 便于精确校验索引迁移
  274. const newAge = int64(123)
  275. moved := 0
  276. for i := 1; i <= 200; i++ {
  277. row, _ := emp.Find(uint64(i))
  278. if row == nil || row.Age != 25 {
  279. continue
  280. }
  281. moved++
  282. if err := emp.Update(&Employee{
  283. BaseModel: DB.BaseModel{ID: uint64(i)},
  284. Name: row.Name, Age: newAge, Score: row.Score, Dept: row.Dept,
  285. }); err != nil {
  286. panic(err)
  287. }
  288. }
  289. c25, _ := emp.Where("Age", "=", int64(25)).Count()
  290. cNew, _ := emp.Where("Age", "=", newAge).Count()
  291. ok(fmt.Sprintf("改 Age(25→%d) %d 行后: Age=25 → %d (期望 %d), Age=%d → %d (期望 %d)",
  292. newAge, moved, c25, int64(empN/2-moved), newAge, cNew, int64(moved)),
  293. c25 == int64(empN/2-moved) && cNew == int64(moved), fmt.Sprintf("25=%d new=%d", c25, cNew))
  294. // (c) 改 B+ 索引字段 (Score) → 索引条目重建
  295. row1, _ := emp.Find(1)
  296. oldScore := row1.Score
  297. newScore := int64(5000)
  298. if err := emp.Update(&Employee{
  299. BaseModel: DB.BaseModel{ID: 1},
  300. Name: row1.Name, Age: row1.Age, Score: newScore, Dept: row1.Dept,
  301. }); err != nil {
  302. panic(err)
  303. }
  304. oldCnt, _ := emp.Where("Score", "=", oldScore).Count()
  305. newCnt, _ := emp.Where("Score", "=", newScore).Count()
  306. ok(fmt.Sprintf("改 Score(%d→%d): 旧值计数=%d, 新值计数=%d", oldScore, newScore, oldCnt, newCnt),
  307. newCnt == 1, fmt.Sprintf("old=%d new=%d", oldCnt, newCnt))
  308. // ---- 1.4 删 ----
  309. delBefore, _ := emp.Count()
  310. delFail := 0
  311. for i := 1; i <= 200; i++ {
  312. if err := emp.Delete(uint64(i)); err != nil {
  313. delFail++
  314. }
  315. }
  316. if err := db.Commit(); err != nil {
  317. panic(err)
  318. }
  319. delAfter, _ := emp.Count()
  320. _, gone := emp.Find(1)
  321. _, gone2 := emp.Find(200)
  322. _, alive := emp.Find(201)
  323. ok(fmt.Sprintf("Delete 200 行: Count %d→%d (失败 %d), Find(1)/(200) 失败, Find(201) 成功",
  324. delBefore, delAfter, delFail),
  325. delAfter == delBefore-200 && delFail == 0 && gone != nil && gone2 != nil && alive == nil,
  326. fmt.Sprintf("%v %v %v", gone, gone2, alive))
  327. // 级联: 被删行的 Hash/B+ 索引条目应一并清除
  328. // 之前迁到 Age=123 的 100 行正好是 ID 1..200, 全部删除 → 索引应归零
  329. cNewAfter, _ := emp.Where("Age", "=", int64(123)).Count()
  330. s5000, _ := emp.Where("Score", "=", int64(5000)).Count()
  331. ok(fmt.Sprintf("删除后 Hash 索引级联清理: Age=123 → %d (期望 0); B+ 索引级联清理: Score=5000 → %d (期望 0)",
  332. cNewAfter, s5000), cNewAfter == 0 && s5000 == 0, fmt.Sprintf("age123=%d score5000=%d", cNewAfter, s5000))
  333. // =============================================================
  334. section("1.5 对象头部增删改查 + 表必须内嵌基类")
  335. // 内嵌基类之后, 增删改查被提升到对象自己身上 (父类定义方法):
  336. // 不用每次都把对象交回 Table, 适合"读出来 → 改几个字段 → 写回去"的写法。
  337. hu := emp.New() // New: 创建 + 绑定 (知道自己属于哪张表)
  338. hu.Name, hu.Age, hu.Score, hu.Dept = "header", 33, 777, "HDR"
  339. if err := hu.Insert(); err != nil { // 增: 主键为 0 → 引擎发号并回写 hu.ID
  340. panic(err)
  341. }
  342. ok(fmt.Sprintf("头部 Insert: 自增主键回写 ID=%d, 对象自知表名 %q", hu.ID, hu.TableName()),
  343. hu.ID != 0 && hu.TableName() == "employee", "绑定信息缺失")
  344. // 改: 按对象自己的主键 (hu.ID) 整行覆盖
  345. hu.Score = 778
  346. if err := hu.Update(); err != nil {
  347. panic(err)
  348. }
  349. rowH, errH := emp.Find(hu.ID)
  350. ok("头部 Update: 按对象自己的主键整行更新",
  351. errH == nil && rowH != nil && rowH.Score == 778, fmt.Sprintf("row=%+v err=%v", rowH, errH))
  352. // 查: Load / Reload 是"填自己" —— 头部方法没有类型参数, 拿不到 T, 也就没法返回
  353. // *T; 要一个新建的、类型安全的对象请用 emp.Find。
  354. loaded := emp.New()
  355. if err := loaded.Load(hu.ID); err != nil {
  356. panic(err)
  357. }
  358. ok("头部 Load: 按主键把整行读进对象自己",
  359. loaded.Name == "header" && loaded.Score == 778, fmt.Sprintf("%+v", loaded))
  360. // 删: 按对象自己的主键
  361. if err := hu.Delete(); err != nil {
  362. panic(err)
  363. }
  364. if _, goneH := emp.Find(hu.ID); goneH == nil {
  365. ok("头部 Delete: 按对象自己的主键删除", false, "删除后仍能查到")
  366. } else {
  367. ok("头部 Delete: 按对象自己的主键删除", true, "")
  368. }
  369. // 未绑定的对象调用头部方法 → 明确报错, 而不是崩或者写错表
  370. if err := (&Employee{Name: "unbound"}).Insert(); !errors.Is(err, DB.ErrModelNotBound) {
  371. ok("未绑定对象头部 Insert 报 ErrModelNotBound", false, fmt.Sprintf("got %v", err))
  372. } else {
  373. ok("未绑定对象头部 Insert 报 ErrModelNotBound", true, "")
  374. }
  375. // ---- 每张表自己设计主键列: vrtx:"pk" 覆盖基类默认的 _id ----
  376. tkt, errT := DB.UseTable[Ticket](db, "ticket")
  377. if errT != nil {
  378. panic(errT)
  379. }
  380. tk := tkt.New()
  381. tk.No, tk.Title = 7, "自定义主键"
  382. if err := tk.Insert(); err != nil {
  383. panic(err)
  384. }
  385. tk.Title = "改过"
  386. if err := tk.Update(); err != nil { // 按 tk.No 定位, 不是 _id
  387. panic(err)
  388. }
  389. tkGot, errTk := tkt.Find(7)
  390. cols := tkt.SchemaColumns()
  391. ok(fmt.Sprintf("vrtx:\"pk\": 主键列变成 No, 基类默认的 _id 不再建列 (实际列=%v)", cols),
  392. errTk == nil && tkGot.No == 7 && tkGot.Title == "改过" && !slices.Contains(cols, "_id"),
  393. fmt.Sprintf("%+v err=%v", tkGot, errTk))
  394. // ---- 没有内嵌基类的结构体不是本框架的表: UseTable 直接拒绝 ----
  395. if _, err := DB.UseTable[notATable](db, "not_a_table"); !errors.Is(err, DB.ErrNotModel) {
  396. ok("UseTable[未内嵌基类] 报 ErrNotModel", false, fmt.Sprintf("got %v", err))
  397. } else {
  398. ok("UseTable[未内嵌基类] 报 ErrNotModel", true, "")
  399. }
  400. if _, found := db.CoreDB().FindTable("not_a_table"); found {
  401. ok("被拒绝的模型不留下表", false, "表被建出来了")
  402. } else {
  403. ok("被拒绝的模型不留下表 (校验发生在建表之前)", true, "")
  404. }
  405. if err := db.Commit(); err != nil {
  406. panic(err)
  407. }
  408. // =============================================================
  409. section("2. 时序表 CRUD (Metric: _ts + 时序索引)")
  410. // ---- 2.1 增 (按时间递增顺序写入, 时序场景标准写法) ----
  411. t1 := time.Now()
  412. for i := 0; i < metN; i++ {
  413. if err := met.Insert(&Metric{
  414. TimeSeriesModel: DB.TimeSeriesModel{TS: uint64(tsBase + int64(i)*tsStep)},
  415. Host: fmt.Sprintf("h%d", i%4),
  416. Value: int64(i * 7 % 1000),
  417. }); err != nil {
  418. panic(err)
  419. }
  420. }
  421. metInsDur := time.Since(t1)
  422. if err := db.Commit(); err != nil {
  423. panic(err)
  424. }
  425. mc, _ := met.Count()
  426. ok(fmt.Sprintf("顺序写入 %d 个时间点 (%.2fs, %.0f 点/s), Count=%d",
  427. metN, metInsDur.Seconds(), float64(metN)/metInsDur.Seconds(), mc),
  428. mc == metN, fmt.Sprintf("Count=%d", mc))
  429. // ---- 2.2 查 ----
  430. tsMid := int64(tsBase + 12345*tsStep)
  431. mr, err := met.Find(uint64(tsMid))
  432. ok("时序主键 Find(ts) 精确命中",
  433. err == nil && mr != nil && int64(mr.TS) == tsMid, fmt.Sprintf("err=%v", err))
  434. // 时间范围查询: 用列名 _ts (主键列名)
  435. lo := int64(tsBase + 3000*tsStep)
  436. hi := int64(tsBase + 3999*tsStep)
  437. rangeRows, err := met.Where("_ts", ">=", lo).Where("_ts", "<=", hi).OrderBy("_ts", false).Find()
  438. tsSorted, tsInRange := true, true
  439. for i := 0; i < len(rangeRows); i++ {
  440. if int64(rangeRows[i].TS) < lo || int64(rangeRows[i].TS) > hi {
  441. tsInRange = false
  442. }
  443. if i > 0 && rangeRows[i-1].TS > rangeRows[i].TS {
  444. tsSorted = false
  445. }
  446. }
  447. dTSRange := avgOf(50, func() {
  448. _, _ = met.Where("_ts", ">=", lo).Where("_ts", "<=", hi).Find()
  449. })
  450. ok(fmt.Sprintf("时间范围 [_ts∈3.0s~3.999s] → %d 行 (期望 1000), 有序=%v, 在界内=%v, 均摊 %v",
  451. len(rangeRows), tsSorted, tsInRange, dTSRange),
  452. err == nil && len(rangeRows) == 1000 && tsSorted && tsInRange,
  453. fmt.Sprintf("len=%d err=%v", len(rangeRows), err))
  454. // 反向时间范围 (最近 N 个点)
  455. latest, _ := met.OrderBy("_ts", true).Limit(10).Find()
  456. desc := true
  457. for i := 1; i < len(latest); i++ {
  458. if latest[i-1].TS < latest[i].TS {
  459. desc = false
  460. break
  461. }
  462. }
  463. ok("OrderBy(_ts desc).Limit(10) 取到最新 10 个点且降序",
  464. len(latest) == 10 && desc, fmt.Sprintf("len=%d desc=%v", len(latest), desc))
  465. // 时序表上的非主键条件 (Host 无索引 → 全表扫描过滤)
  466. hostCnt, _ := met.Where("Host", "=", "h2").Count()
  467. ok(fmt.Sprintf("Where(Host=h2).Count()=%d (期望 %d, 无索引全扫描)", hostCnt, metN/4),
  468. hostCnt == int64(metN/4), fmt.Sprintf("got=%d", hostCnt))
  469. // ---- 2.3 改 (时序行原地更新) ----
  470. updTS := uint64(tsBase + 5000*tsStep)
  471. _ = met.Update(&Metric{
  472. TimeSeriesModel: DB.TimeSeriesModel{TS: updTS},
  473. Host: "hX", Value: 9999,
  474. })
  475. mu, err := met.Find(updTS)
  476. ok("时序行 Update → Find 校验 Value=9999",
  477. err == nil && mu != nil && mu.Value == 9999 && mu.Host == "hX",
  478. fmt.Sprintf("err=%v row=%+v", err, mu))
  479. // ---- 2.4 删 ----
  480. delTS := uint64(tsBase + 6000*tsStep)
  481. if err := met.Delete(delTS); err != nil {
  482. panic(err)
  483. }
  484. if err := db.Commit(); err != nil {
  485. panic(err)
  486. }
  487. _, errGone := met.Find(delTS)
  488. mcAfter, _ := met.Count()
  489. ok(fmt.Sprintf("时序行 Delete(ts) 后: Find 失败, Count=%d (期望 %d)", mcAfter, metN-1),
  490. errGone != nil && mcAfter == int64(metN-1), fmt.Sprintf("err=%v cnt=%d", errGone, mcAfter))
  491. // ---- 2.5 同一时间戳多行 (时间戳之外的第二个定位维度: 行号) ----
  492. // _ts 只决定"落在哪个分段", 指认不了"段里哪一行"。同一时刻出多行是时序数据的常态
  493. // (同一时刻的多个测点 / 多次事件), 所以引擎给每行另发一个隐藏行号 (pkID)。行号随对象
  494. // 回到调用方, 于是"查出来 → 改几个字段 → 写回去"在同刻多行上也精确到行。
  495. const sameTS = uint64(tsBase + 9000*tsStep)
  496. rowA, rowB := dup.New(), dup.New()
  497. rowA.TS, rowA.Val = sameTS, 1
  498. rowB.TS, rowB.Val = sameTS, 2
  499. if err := rowA.Insert(); err != nil {
  500. panic(err)
  501. }
  502. if err := rowB.Insert(); err != nil {
  503. panic(err)
  504. }
  505. // Table.Insert(手工对象) 同样可以: 三种写法共用同一条写入路径
  506. if err := dup.Insert(&SameTSMetric{TimeSeriesModel: DB.TimeSeriesModel{TS: sameTS}, Val: 3}); err != nil {
  507. panic(err)
  508. }
  509. if err := db.Commit(); err != nil {
  510. panic(err)
  511. }
  512. sameCnt, _ := dup.Where("_ts", "=", sameTS).Count()
  513. ok(fmt.Sprintf("同一时间戳容许多行: 写入 3 行, 同刻 Count=%d (期望 3)", sameCnt),
  514. sameCnt == 3, fmt.Sprintf("got=%d", sameCnt))
  515. // 每行都带着自己的行号 → 各自精确改, 不串行
  516. sameRows, _ := dup.Where("_ts", "=", sameTS).Find()
  517. for i, r := range sameRows {
  518. r.Val = int64(100 + i)
  519. if err := r.Update(); err != nil {
  520. panic(err)
  521. }
  522. }
  523. if err := db.Commit(); err != nil {
  524. panic(err)
  525. }
  526. afterRows, _ := dup.Where("_ts", "=", sameTS).Find()
  527. vals := make([]int64, 0, len(afterRows))
  528. for _, r := range afterRows {
  529. vals = append(vals, r.Val)
  530. }
  531. ok(fmt.Sprintf("同刻 3 行各自 Update: 值=%v (期望 [100 101 102])", vals),
  532. len(vals) == 3 && vals[0] == 100 && vals[1] == 101 && vals[2] == 102,
  533. fmt.Sprintf("got=%v", vals))
  534. // 只删中间那一行: 同刻的其它行不能被牵连
  535. if err := afterRows[1].Delete(); err != nil {
  536. panic(err)
  537. }
  538. if err := db.Commit(); err != nil {
  539. panic(err)
  540. }
  541. leftCnt, _ := dup.Where("_ts", "=", sameTS).Count()
  542. ok(fmt.Sprintf("同刻多行只删指定的那一行: 剩余 Count=%d (期望 2)", leftCnt),
  543. leftCnt == 2, fmt.Sprintf("got=%d", leftCnt))
  544. // 剩下的各行仍按行号精确读回 (Value 应为删掉 101 之后的 [100 102])
  545. restRows, _ := dup.Where("_ts", "=", sameTS).Find()
  546. reloadOK := len(restRows) == 2
  547. gotVals := make([]int64, 0, len(restRows))
  548. for i, r := range restRows {
  549. if err := r.Reload(); err != nil || r.Val != int64(100+i*2) {
  550. reloadOK = false
  551. }
  552. gotVals = append(gotVals, r.Val)
  553. }
  554. ok(fmt.Sprintf("删后其余各行 Reload 仍读回自己那一行: 值=%v (期望 [100 102])", gotVals),
  555. reloadOK, fmt.Sprintf("got=%v", gotVals))
  556. // =============================================================
  557. section("3. 时序写入的顺序约束 (乱序拒绝 / 缺省时间戳)")
  558. // ---- 3.1 乱序写入: 时序索引只收非递减写入, 更早的点一律拒绝 ----
  559. for i := 0; i < 1000; i++ {
  560. _ = late.Insert(&LateMetric{
  561. TimeSeriesModel: DB.TimeSeriesModel{TS: uint64(tsBase + int64(i)*tsStep)},
  562. Value: int64(i),
  563. })
  564. }
  565. if err := db.Commit(); err != nil {
  566. panic(err)
  567. }
  568. lc, _ := late.Count()
  569. ok(fmt.Sprintf("LateMetric 顺序写入 1000 点, Count=%d", lc), lc == 1000, fmt.Sprintf("cnt=%d", lc))
  570. // 已在 0..999s 区间落盘后, 再补写中间时间点 (乱序 late data): 必须被拒。
  571. //
  572. // 时序索引只有时间一个维度 —— 顺序写入时每个分段天然覆盖一段互不重叠的时间区间、
  573. // 段内也天然有序, 范围查询才裁剪得动分段。放乱序进来会让段区间互相重叠、段内失序,
  574. // 查询退化成"全链 + 整段逐行过滤", 所以引擎直接拒绝, 并给出可判定的 ErrOutOfOrder。
  575. // (确有乱序数据时, 先按时间排序再写, 或改用普通表 + 二级索引。)
  576. lateTS := []uint64{
  577. uint64(tsBase + 500*tsStep + 1),
  578. uint64(tsBase + 500*tsStep + 2),
  579. uint64(tsBase + 500*tsStep + 3),
  580. }
  581. lateInsErr, lateOOOErr := 0, 0
  582. for _, t := range lateTS {
  583. err := late.Insert(&LateMetric{TimeSeriesModel: DB.TimeSeriesModel{TS: t}, Value: int64(t)})
  584. if err != nil {
  585. lateInsErr++
  586. if errors.Is(err, tsindex.ErrOutOfOrder) {
  587. lateOOOErr++
  588. }
  589. }
  590. }
  591. _ = db.Commit()
  592. lateFound := 0
  593. for _, t := range lateTS {
  594. if row, e := late.Find(t); e == nil && row != nil {
  595. lateFound++
  596. }
  597. }
  598. loL := int64(tsBase)
  599. hiL := int64(tsBase + 999*tsStep)
  600. lateRows, _ := late.Where("_ts", ">=", loL).Where("_ts", "<=", hiL).Find()
  601. fmt.Printf(" [INFO] 乱序补写 3 个中间时间点: 被拒=%d/3 (ErrOutOfOrder=%d), 主键 Find 命中=%d/3, 时间范围扫描回读=%d 行 (期望 1000)\n",
  602. lateInsErr, lateOOOErr, lateFound, len(lateRows))
  603. ok("乱序补写的中间时间点全部被拒 (3/3)", lateInsErr == 3, fmt.Sprintf("被拒 %d/3", lateInsErr))
  604. ok("拒绝原因可判定为 ErrOutOfOrder", lateOOOErr == 3, fmt.Sprintf("ErrOutOfOrder=%d/3", lateOOOErr))
  605. ok("被拒的乱序点没有落盘 (主键 Find 0/3)", lateFound == 0, fmt.Sprintf("命中 %d/3", lateFound))
  606. ok("时间范围扫描仍是顺序写入的 1000 行", len(lateRows) == 1000, fmt.Sprintf("got=%d", len(lateRows)))
  607. // ---- 3.2 缺省时间戳自动分配 ----
  608. // 时序键就是时间戳本身。_ts 缺省时 ORM 补的是"写入这一刻"的纳秒级时间戳 —— 与显式
  609. // 传入的时间戳同量纲, 因此它天然大于此前写入的时间戳, 顺序校验不会拦它。
  610. autoTSBefore := uint64(time.Now().UnixNano())
  611. auto := &LateMetric{Value: -1} // 不设置 TS
  612. if err := late.Insert(auto); err != nil {
  613. panic(err)
  614. }
  615. _ = db.Commit()
  616. autoTSAfter := uint64(time.Now().UnixNano())
  617. fmt.Printf(" [INFO] 缺省 _ts 插入后, 结构体回写得到的 TS=%d (纳秒时间戳)\n", auto.TS)
  618. ok("缺省 _ts 由 ORM 补当前纳秒时间戳 (落在插入前后之间)",
  619. auto.TS >= autoTSBefore && auto.TS <= autoTSAfter,
  620. fmt.Sprintf("TS=%d 不在 [%d,%d]", auto.TS, autoTSBefore, autoTSAfter))
  621. // =============================================================
  622. section("4. 事务 (ORM 增删改 + 提交/回滚)")
  623. txn := db.BeginTransaction()
  624. for i := 0; i < 10; i++ {
  625. _ = emp.Insert(&Employee{Name: fmt.Sprintf("Txn%03d", i), Age: 33, Score: 333, Dept: "TX"})
  626. }
  627. if err := txn.Commit(); err != nil {
  628. panic(err)
  629. }
  630. txnCnt, _ := emp.Where("Dept", "=", "TX").Count()
  631. ok(fmt.Sprintf("事务内插入 10 行并提交 → Where(Dept=TX)=%d", txnCnt), txnCnt == 10, fmt.Sprintf("got=%d", txnCnt))
  632. txn2 := db.BeginTransaction()
  633. for i := 0; i < 10; i++ {
  634. _ = emp.Insert(&Employee{Name: fmt.Sprintf("Roll%03d", i), Age: 44, Score: 444, Dept: "RB"})
  635. }
  636. if err := txn2.Rollback(); err != nil {
  637. panic(err)
  638. }
  639. rbCnt, _ := emp.Where("Dept", "=", "RB").Count()
  640. ok(fmt.Sprintf("事务内插入后回滚 → Where(Dept=RB)=%d (期望 0)", rbCnt), rbCnt == 0, fmt.Sprintf("got=%d", rbCnt))
  641. // =============================================================
  642. section("5. 关闭重开 (持久化验证)")
  643. empBefore, _ := emp.Count()
  644. metBefore, _ := met.Count()
  645. if err := db.Close(); err != nil {
  646. panic(err)
  647. }
  648. db, err = DB.Open(path)
  649. if err != nil {
  650. panic(err)
  651. }
  652. db.SetAutoCommitDelay(0)
  653. db.SetDirtyThreshold(1 << 30)
  654. emp, _ = DB.UseTable[Employee](db, "employee")
  655. met, _ = DB.UseTable[Metric](db, "metric")
  656. empAfter, _ := emp.Count()
  657. metAfter, _ := met.Count()
  658. ok(fmt.Sprintf("重开后 employee Count %d → %d", empBefore, empAfter), empBefore == empAfter,
  659. fmt.Sprintf("%d/%d", empBefore, empAfter))
  660. ok(fmt.Sprintf("重开后 metric Count %d → %d", metBefore, metAfter), metBefore == metAfter,
  661. fmt.Sprintf("%d/%d", metBefore, metAfter))
  662. rpk, err := emp.Find(201)
  663. ok("重开后主键 Find(201) 命中", err == nil && rpk != nil, fmt.Sprintf("err=%v", err))
  664. rt, err := met.Find(uint64(tsBase + 9000*tsStep))
  665. ok("重开后时序 Find(ts) 命中", err == nil && rt != nil, fmt.Sprintf("err=%v", err))
  666. h25, _ := emp.Where("Age", "=", int64(25)).Count()
  667. ok(fmt.Sprintf("重开后 Hash 索引可用: Age=25 → %d", h25), h25 > 0, fmt.Sprintf("got=%d", h25))
  668. s300, _ := emp.Where("Score", ">=", int64(300)).Where("Score", "<=", int64(399)).Find()
  669. ok(fmt.Sprintf("重开后 B+ 索引可用: Score∈[300,399] → %d 行", len(s300)), len(s300) > 0, "")
  670. if err := db.Close(); err != nil {
  671. panic(err)
  672. }
  673. fi, _ := os.Stat(path)
  674. if fi != nil {
  675. fmt.Printf("\n 数据库文件: %.2f MB\n", float64(fi.Size())/1024/1024)
  676. }
  677. // [KEEP-DB] 保留数据库文件供 Analyzer 分析, 屏蔽运行结束后的清理删除
  678. // os.Remove(path)
  679. // os.Remove(path + ".wal")
  680. // =============================================================
  681. section("6. 四种索引形态特性对比")
  682. printRow := func(name, typ, struct_, leaf, cx, decl string) {
  683. fmt.Printf(" %s\n IndexType = %s\n 底层结构 = %s\n 叶子/条目 = %s\n 查询复杂度 = %s\n ORM 声明 = %s\n",
  684. name, typ, struct_, leaf, cx, decl)
  685. }
  686. printRow("主键 _id", "2 Primary", "B+ 树", "叶子直接存行数据(DataPage)",
  687. "等值/范围 O(logN), 天然按 pk 有序", "内嵌 DB.BaseModel")
  688. printRow("Hash 二级", "6 Hash", "桶目录 + 溢出链", "叶子存 pkID, 需回主键取行",
  689. "等值 O(1); 范围=全桶扫描 O(N)", `vrtx:"index:x,type:hash"`)
  690. printRow("B+ 二级", "5 BTree", "B+ 树", "叶子存 pkID, 需回主键取行",
  691. "等值/范围/有序 均 O(logN)", `vrtx:"index:x" (默认)`)
  692. printRow("时序 _ts", "1 Time", "扁平时间条目链", "[Begin,Last,DataPageID] → 分段 DataPage",
  693. "时间范围只加载命中分段; 等值需扫段内行", "内嵌 DB.TimeSeriesModel")
  694. fmt.Printf("\n%s\n", strings.Repeat("=", 66))
  695. fmt.Printf(" 总计: 通过 %d, 失败 %d\n", passed, failed)
  696. fmt.Println(strings.Repeat("=", 66))
  697. if failed > 0 {
  698. os.Exit(1)
  699. }
  700. }