main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. package main
  2. // NULL 位图 (types.NullBitmapLen) 的两条主线:
  3. //
  4. // 1. 可空列的写入 / 读取语义, 以及 NULL 参与比较时的三值逻辑 —— NULL 不匹配任何
  5. // 谓词 (不等于 0, 也不算小于), 也不进二级索引;
  6. // 2. schema 演进 (AddField 跨 8 位边界) 后, 各版本旧行仍按自身位图长度正确解析。
  7. //
  8. // 位图放在行尾是硬约束 (时序索引按固定偏移读首列), 因此这里对"字段值"逐列比对,
  9. // 而不是只看行数: 位图位置 / 长度算错一格, 最先坏掉的就是中间列的值。
  10. //
  11. // 运行: cd demo/verify_null_bitmap && go run main.go
  12. import (
  13. "fmt"
  14. "os"
  15. "git.ycl.pub/yl32940/TsDB2/DB/core"
  16. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  17. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  18. )
  19. const dbPath = "null_bitmap_test.db"
  20. var passed, failed int
  21. // check 记一笔断言结果。
  22. func check(name string, ok bool, detail string) {
  23. if ok {
  24. passed++
  25. fmt.Printf(" ✅ %s\n", name)
  26. return
  27. }
  28. failed++
  29. fmt.Printf(" ❌ %s — %s\n", name, detail)
  30. }
  31. // checkErr 断言 err == nil; 返回是否通过。
  32. func checkErr(name string, err error) bool {
  33. if err != nil {
  34. check(name, false, err.Error())
  35. return false
  36. }
  37. check(name, true, "")
  38. return true
  39. }
  40. // fatal 建表 / 建索引失败属于环境问题, 直接中断。
  41. func fatal(what string, err error) {
  42. if err != nil {
  43. fmt.Printf(" 💥 %s: %v\n", what, err)
  44. os.Exit(1)
  45. }
  46. }
  47. // idxErr 把 CreateIndex 的 (meta, err) 收敛成 err。
  48. func idxErr(_ *core.IndexMeta, err error) error { return err }
  49. // nv 构造一个 FieldValue; value 传 nil 即"显式写 NULL"。
  50. func nv(name string, value interface{}) types.FieldValue {
  51. return types.FieldValue{FieldName: []byte(name), Value: value}
  52. }
  53. // nvLookup 在结果行里找字段, 返回 (值, 是否存在)。
  54. func nvLookup(row []types.FieldValue, name string) (interface{}, bool) {
  55. for _, fv := range row {
  56. if string(fv.FieldName) == name {
  57. return fv.Value, true
  58. }
  59. }
  60. return nil, false
  61. }
  62. // checkUint 断言结果行里 field = want。
  63. func checkUint(name string, row []types.FieldValue, field string, want uint64) {
  64. v, ok := nvLookup(row, field)
  65. if !ok {
  66. check(name, false, fmt.Sprintf("字段 %s 缺失 (want %d)", field, want))
  67. return
  68. }
  69. got, ok := v.(uint64)
  70. check(name, ok && got == want, fmt.Sprintf("字段 %s = %v (%T), want %d", field, v, v, want))
  71. }
  72. // checkAbsent 断言字段没有出现在结果里 (NULL 的既定表示: 不 append)。
  73. func checkAbsent(name string, row []types.FieldValue, field string) {
  74. v, ok := nvLookup(row, field)
  75. check(name, !ok, fmt.Sprintf("字段 %s 应为 NULL 而不出现, 实得 %v", field, v))
  76. }
  77. // checkAllUint 逐列比对一组期望值。
  78. func checkAllUint(prefix string, row []types.FieldValue, want map[string]uint64) {
  79. for name, w := range want {
  80. checkUint(fmt.Sprintf("%s.%s = %d", prefix, name, w), row, name, w)
  81. }
  82. }
  83. // checkCount 断言某列等值命中 want 行。
  84. func checkCount(name string, tbl *core.Table, field string, v uint64, want int64) {
  85. n, err := tbl.Query().Where(core.Eq(field, v, types.FieldType(0))).Count()
  86. check(name, err == nil && n == want,
  87. fmt.Sprintf("%s=%d 命中 %d 行 (err=%v), want %d", field, v, n, err, want))
  88. }
  89. func main() {
  90. fmt.Println("==============================================")
  91. fmt.Println(" NULL 位图测试 (可空列语义 / 三值逻辑 / 演进)")
  92. fmt.Println("==============================================")
  93. os.Remove(dbPath)
  94. os.Remove(dbPath + ".wal")
  95. db, err := core.Open(dbPath)
  96. if err != nil {
  97. panic(err)
  98. }
  99. db.SetAutoCommitDelay(0) // 0 = 关闭自动提交, 只在显式 Commit 时落盘
  100. fmt.Println("\n---- 1. 可空列: 写 NULL → 不参与比较 → 不进索引 ----")
  101. testNullableColumn(db)
  102. fmt.Println("\n---- 2. schema 演进: 8 列 → 9 列 → 17 列, 位图按行版本算 ----")
  103. testSchemaEvolutionAcross8(db)
  104. _ = db.Close()
  105. fmt.Println()
  106. fmt.Println("==============================================")
  107. fmt.Printf("测试结果: %d 通过, %d 失败\n", passed, failed)
  108. if failed == 0 {
  109. fmt.Println("🎉 全部通过!")
  110. os.Remove(dbPath)
  111. os.Remove(dbPath + ".wal")
  112. } else {
  113. fmt.Printf("⚠️ 有 %d 项失败 (库文件保留在 %s 便于排查)\n", failed, dbPath)
  114. os.Exit(1)
  115. }
  116. }
  117. func testNullableColumn(db *core.DB) {
  118. tbl, err := db.CreateTable("NULLABLE", []types.FieldInfo{
  119. {FieldType: types.FieldType_Uint64, FieldName: []byte("ID")},
  120. {FieldType: types.FieldType_Uint64, FieldName: []byte("Tag")},
  121. {FieldType: types.FieldType_Uint64, FieldName: []byte("Note"), Flag: types.FlagNullable},
  122. })
  123. fatal("建表 NULLABLE", err)
  124. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_nullable", page.IndexTypePrimary, []byte{0})))
  125. // Note 上建二级索引: 用来验证"该列为 NULL 的行不进这个索引"。
  126. fatal("建二级索引 idx_nullable_note", idxErr(tbl.CreateIndex("idx_nullable_note", page.IndexTypeBTree, []byte{2})))
  127. pk1, err := tbl.InsertRow([]types.FieldValue{nv("ID", uint64(1)), nv("Tag", uint64(10)), nv("Note", nil)})
  128. fatal("插入 NULL 行", err)
  129. pk2, err := tbl.InsertRow([]types.FieldValue{nv("ID", uint64(2)), nv("Tag", uint64(20)), nv("Note", uint64(30))})
  130. fatal("插入持值行", err)
  131. // ===== 读取: NULL 字段不出现, 同行的其它字段完好 =====
  132. row1, err := tbl.FindByPK(pk1)
  133. if checkErr("读回 NULL 行", err) {
  134. checkUint("NULL 行的 Tag 完好", row1, "Tag", 10)
  135. checkAbsent("NULL 行的 Note 不出现", row1, "Note")
  136. }
  137. row2, err := tbl.FindByPK(pk2)
  138. if checkErr("读回持值行", err) {
  139. checkUint("持值行的 Note = 30", row2, "Note", 30)
  140. }
  141. // ===== 比较: NULL 不匹配任何谓词 (SQL 三值逻辑) =====
  142. // 这三条同时覆盖"索引路径" (Eq 走 Note 索引) 与"全表过滤":
  143. // - 若把 NULL 当零字节比: Eq(Note,0) 会命中 NULL 行;
  144. // - 若索引把 NULL 行的键当成 8 个零字节: Eq(Note,0) 同样会命中。
  145. checkCount("Count(Note=0) = 0 (NULL 不等于 0)", tbl, "Note", 0, 0)
  146. checkCount("Count(Note=30) = 1", tbl, "Note", 30, 1)
  147. n, err := tbl.Query().Where(core.Lt("Note", uint64(100), types.FieldType(0))).Count()
  148. check("Count(Note<100) = 1 (NULL 不算小于)", err == nil && n == 1,
  149. fmt.Sprintf("命中 %d 行 (err=%v), want 1", n, err))
  150. // ===== 更新: 有值 → NULL, 再读回 =====
  151. checkErr("把 row2 的 Note 改成 NULL", tbl.UpdateRowByPK(pk2, []types.FieldValue{nv("Note", nil)}))
  152. row2, err = tbl.FindByPK(pk2)
  153. if checkErr("改 NULL 后读回 row2", err) {
  154. checkUint("未提供的列沿用旧值 (Tag = 20)", row2, "Tag", 20)
  155. checkAbsent("row2.Note 现在是 NULL", row2, "Note")
  156. }
  157. checkCount("改成 NULL 后陈旧索引条目已摘掉", tbl, "Note", 30, 0)
  158. }
  159. // testSchemaEvolutionAcross8 8 列(1B 位图) → 9 列(2B) → 17 列(3B),
  160. // 每个版本的旧行永远按自己的位图长度解析; 高位下标 (8 / 16) 的 NULL 也必须落在正确的字节。
  161. func testSchemaEvolutionAcross8(db *core.DB) {
  162. // v1: ID + A..G (G 可空) = 8 列 → 位图 1 字节
  163. fields := []types.FieldInfo{{FieldType: types.FieldType_Uint64, FieldName: []byte("ID")}}
  164. for c := 'A'; c <= 'F'; c++ {
  165. fields = append(fields, types.FieldInfo{FieldType: types.FieldType_Uint64, FieldName: []byte(string(c))})
  166. }
  167. fields = append(fields, types.FieldInfo{
  168. FieldType: types.FieldType_Uint64, FieldName: []byte("G"), Flag: types.FlagNullable,
  169. })
  170. if len(fields) != 8 {
  171. fatal("v1 应为 8 列", fmt.Errorf("实得 %d 列", len(fields)))
  172. }
  173. tbl, err := db.CreateTable("EVOLVE", fields)
  174. fatal("建表 EVOLVE", err)
  175. fatal("建主键索引", idxErr(tbl.CreateIndex("pk_evolve", page.IndexTypePrimary, []byte{0})))
  176. // v1 行: G = NULL (位下标 7, 落在第 1 个字节的最高位)
  177. pk1, err := tbl.InsertRow([]types.FieldValue{
  178. nv("ID", uint64(1)), nv("A", uint64(11)), nv("B", uint64(12)), nv("C", uint64(13)),
  179. nv("D", uint64(14)), nv("E", uint64(15)), nv("F", uint64(16)), nv("G", nil),
  180. })
  181. fatal("插入 v1 行 (G=NULL)", err)
  182. // v2: 加 H (可空) → 9 列 → 位图 2 字节
  183. fatal("加列 H", tbl.AddField(types.FieldInfo{
  184. FieldType: types.FieldType_Uint64, FieldName: []byte("H"), Flag: types.FlagNullable,
  185. }))
  186. pk2, err := tbl.InsertRow([]types.FieldValue{
  187. nv("ID", uint64(2)), nv("A", uint64(21)), nv("B", uint64(22)), nv("C", uint64(23)),
  188. nv("D", uint64(24)), nv("E", uint64(25)), nv("F", uint64(26)), nv("G", uint64(27)), nv("H", nil),
  189. })
  190. fatal("插入 v2 行 (H=NULL)", err)
  191. pk3, err := tbl.InsertRow([]types.FieldValue{
  192. nv("ID", uint64(3)), nv("A", uint64(31)), nv("B", uint64(32)), nv("C", uint64(33)),
  193. nv("D", uint64(34)), nv("E", uint64(35)), nv("F", uint64(36)), nv("G", uint64(37)), nv("H", uint64(38)),
  194. })
  195. fatal("插入 v2 行 (H=val)", err)
  196. // v3: 再加 I..P (P 可空) → 17 列 → 位图 3 字节
  197. for _, name := range []string{"I", "J", "K", "L", "M", "N", "O"} {
  198. fatal("加列 "+name, tbl.AddField(types.FieldInfo{FieldType: types.FieldType_Uint64, FieldName: []byte(name)}))
  199. }
  200. fatal("加列 P", tbl.AddField(types.FieldInfo{
  201. FieldType: types.FieldType_Uint64, FieldName: []byte("P"), Flag: types.FlagNullable,
  202. }))
  203. v3Vals := []types.FieldValue{
  204. nv("ID", uint64(4)), nv("A", uint64(41)), nv("B", uint64(42)), nv("C", uint64(43)),
  205. nv("D", uint64(44)), nv("E", uint64(45)), nv("F", uint64(46)), nv("G", uint64(47)),
  206. nv("H", uint64(48)), nv("I", uint64(49)), nv("J", uint64(50)), nv("K", uint64(51)),
  207. nv("L", uint64(52)), nv("M", uint64(53)), nv("N", uint64(54)), nv("O", uint64(55)), nv("P", nil),
  208. }
  209. pk4, err := tbl.InsertRow(v3Vals)
  210. fatal("插入 v3 行 (P=NULL)", err)
  211. v3Vals2 := make([]types.FieldValue, len(v3Vals))
  212. copy(v3Vals2, v3Vals)
  213. for i := range v3Vals2 {
  214. if string(v3Vals2[i].FieldName) == "P" {
  215. v3Vals2[i] = nv("P", uint64(58))
  216. }
  217. }
  218. pk5, err := tbl.InsertRow(v3Vals2)
  219. fatal("插入 v3 行 (P=val)", err)
  220. // ===== 逐版本读回, 逐列比对值 (位图长度 / 位置算错都会在这里暴露) =====
  221. row1, err := tbl.FindByPK(pk1)
  222. if checkErr("读回 v1 行", err) {
  223. checkAllUint("v1", row1, map[string]uint64{
  224. "ID": 1, "A": 11, "B": 12, "C": 13, "D": 14, "E": 15, "F": 16,
  225. })
  226. checkAbsent("v1.G 是位下标 7 的 NULL", row1, "G") // 位下标 7
  227. checkAbsent("v1 还没有 H 列", row1, "H")
  228. }
  229. row2, err := tbl.FindByPK(pk2)
  230. if checkErr("读回 v2 行 (H=NULL)", err) {
  231. checkUint("v2.G = 27", row2, "G", 27)
  232. checkAbsent("v2.H 是位下标 8 的 NULL (第 2 个字节 bit0)", row2, "H")
  233. }
  234. row3, err := tbl.FindByPK(pk3)
  235. if checkErr("读回 v2 行 (H=val)", err) {
  236. checkUint("v2.G = 37", row3, "G", 37)
  237. checkUint("v2.H = 38", row3, "H", 38)
  238. }
  239. row4, err := tbl.FindByPK(pk4)
  240. if checkErr("读回 v3 行 (P=NULL)", err) {
  241. checkAllUint("v3", row4, map[string]uint64{
  242. "G": 47, "H": 48, "I": 49, "J": 50, "K": 51, "L": 52, "M": 53, "N": 54, "O": 55,
  243. })
  244. checkAbsent("v3.P 是位下标 16 的 NULL (第 3 个字节 bit0)", row4, "P")
  245. }
  246. row5, err := tbl.FindByPK(pk5)
  247. if checkErr("读回 v3 行 (P=val)", err) {
  248. checkUint("v3.O = 55", row5, "O", 55)
  249. checkUint("v3.P = 58", row5, "P", 58)
  250. }
  251. // 全表扫一遍: 版本混杂时行数不能丢, 也不能因某版本解析出错而少行。
  252. rows, err := tbl.Query().Find()
  253. check("全表 5 行 (版本混杂也不丢行)", err == nil && len(rows) == 5,
  254. fmt.Sprintf("行数 = %d (err=%v), want 5", len(rows), err))
  255. // 位图长度必须按行版本算: 这里显式核对三个版本各自的字节数。
  256. for _, tc := range []struct{ n, bitmapBytes int }{{8, 1}, {9, 2}, {17, 3}} {
  257. got := types.NullBitmapLen(tc.n)
  258. check(fmt.Sprintf("NullBitmapLen(%d) = %d 字节", tc.n, tc.bitmapBytes), got == tc.bitmapBytes,
  259. fmt.Sprintf("实得 %d", got))
  260. }
  261. }