main_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "math/big"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "testing"
  10. "TsDB2/DB/core"
  11. "TsDB2/DB/core/page"
  12. "TsDB2/DB/core/types"
  13. )
  14. // ============================ 列型表同步守卫 ============================
  15. //
  16. // analyzer 在自己的模块里手写偏移直接读盘 (见 layout_assert.go 的说明)。布局偏移有
  17. // 编译期断言兜着, 但"列类型 → 宽度 / 名字 / 解码分支"这三张表以前是纯手写的, 没有
  18. // 任何东西拦着它漂移 —— 而它已经漂过一次: core 新增 7 个列型 (bool/float32/decimal/
  19. // uuid/year/int128/blob) 时 analyzer 没跟, 后果不是"显示成 ?", 而是:
  20. //
  21. // - fSz 返回 0 → 行解码 (decodeRows 里的 off += sz) 偏移不推进, 新类型字段【之后
  22. // 的所有列】都从同一个错位偏移解码, 整行静默错位;
  23. // - indexcheck 因 size <= 0 直接放弃索引键校验;
  24. // - decodeFieldValue 把新类型当定长字符串, 打印出二进制噪声。
  25. //
  26. // 现在 fSz/fName 改成投影到 core 的 typeRegistry (漏同步在结构上不可能), 但
  27. // decodeFieldValue 仍是一份手写 switch —— 这两个测试就是它的守卫。
  28. //
  29. // 判据一律取自 core 的 types.AllTypes(), 而不是在这里再抄一份期望名单: 抄一份就又
  30. // 造出一个会漂移的副本, 而这回的教训恰恰就是"手写副本没跟着新类型走"。
  31. // TestDecodeCoversEveryRegisteredType 要求 core 注册的每一个列型都被 analyzer 认得出:
  32. // 有宽度、有名字、能解码。
  33. func TestDecodeCoversEveryRegisteredType(t *testing.T) {
  34. for _, ft := range types.AllTypes() {
  35. t.Run(types.TypeNameOf(ft), func(t *testing.T) {
  36. sz := fSz(byte(ft))
  37. if want := int(types.GetSize(ft)); sz != want {
  38. t.Fatalf("fSz(%d) = %d, core 注册的是 %d", ft, sz, want)
  39. }
  40. if sz <= 0 {
  41. t.Fatalf("fSz(%d) = 0: 行解码靠它推进偏移, 0 会让整行字段错位", ft)
  42. }
  43. if n := fName(byte(ft)); n == "?" {
  44. t.Fatalf("fName(%d) = ?: 该类型在 core 已注册, 名字不该是未知", ft)
  45. }
  46. // 全零缓冲对每个列型都必须能解出结果 (而不是落到未同步分支)。
  47. // 这里用零值而不是真值, 是为了把"这个类型有没有被处理"和"解出来的值对不对"
  48. // 分开: 后者由 TestDecodeNewFieldTypes 用 core 编码的真值来钉。
  49. buf := make([]byte, sz)
  50. if got := decodeFieldValue(buf, byte(ft)); got == unknownFieldValue {
  51. t.Fatalf("decodeFieldValue 未处理类型 %d (%s): 落到了未同步分支",
  52. ft, types.TypeNameOf(ft))
  53. }
  54. })
  55. }
  56. }
  57. // TestDecodeNewFieldTypes 新增的列型必须按盘上语义正确解码。
  58. //
  59. // 字节一律由 core 的 types.EncodeValue 生成 —— 测的是"两份独立实现在同一份盘格式下
  60. // 读出来一不一致", 而不是"自己编码再自己解码"的自证 (那种测试实现错了一起错也全绿)。
  61. func TestDecodeNewFieldTypes(t *testing.T) {
  62. minInt128, ok := new(big.Int).SetString("-170141183460469231731687303715884105728", 10)
  63. if !ok {
  64. t.Fatal("minInt128 字面量解析失败")
  65. }
  66. cases := []struct {
  67. name string
  68. ft types.FieldType
  69. param byte
  70. val any
  71. want string
  72. }{
  73. {"bool true", types.FieldType_Bool, 0, true, "true"},
  74. {"bool false", types.FieldType_Bool, 0, false, "false"},
  75. {"float32 正", types.FieldType_Float32, 0, float32(0.5), "0.5"},
  76. {"float32 负", types.FieldType_Float32, 0, float32(-2.25), "-2.25"},
  77. {"float64 正", types.FieldType_Float64, 0, 0.5, "0.5"},
  78. {"float64 负", types.FieldType_Float64, 0, -2.25, "-2.25"},
  79. {"decimal 两位", types.FieldType_Decimal, 2, "1.50", "1.50"},
  80. {"decimal 负不足一位", types.FieldType_Decimal, 2, "-0.05", "-0.05"},
  81. {"decimal 补零到位", types.FieldType_Decimal, 2, "0", "0.00"},
  82. {
  83. "uuid", types.FieldType_UUID, 0,
  84. "550e8400-e29b-41d4-a716-446655440000",
  85. "550e8400-e29b-41d4-a716-446655440000",
  86. },
  87. {"year", types.FieldType_Year, 0, 2026, "2026"},
  88. {"int128 下界", types.FieldType_Int128, 0, minInt128,
  89. "-170141183460469231731687303715884105728"},
  90. {"int128 零", types.FieldType_Int128, 0, new(big.Int), "0"},
  91. }
  92. for _, c := range cases {
  93. t.Run(c.name, func(t *testing.T) {
  94. buf := make([]byte, types.GetSize(c.ft))
  95. if err := types.EncodeValue(c.ft, c.param, c.val, buf); err != nil {
  96. t.Fatalf("core EncodeValue 失败: %v", err)
  97. }
  98. if got := decodeFieldValue(buf, byte(c.ft)); got != c.want {
  99. t.Fatalf("解码 = %q, 期望 %q (盘上字节 %x)", got, c.want, buf)
  100. }
  101. })
  102. }
  103. }
  104. // TestDecodeDynamicRef 动态列 (dynamic / blob) 的行内 6 字节是堆引用, 不是内容。
  105. //
  106. // 这两类不走 EncodeValue (内容在堆页, 由 core 解引用后拿到), 所以单独钉。
  107. // 以前它们落到 default 分支, 把页号和偏移当字符串打印出来 —— 真正想看的
  108. // "内容在哪一页" 反而看不见。
  109. func TestDecodeDynamicRef(t *testing.T) {
  110. for _, ft := range []types.FieldType{types.FieldType_Dynamicbyte, types.FieldType_Blob} {
  111. t.Run(types.TypeNameOf(ft), func(t *testing.T) {
  112. // 行内引用布局: [PageID uint32 大端][Offset uint16 大端]
  113. if got, want := decodeFieldValue([]byte{0, 0, 0, 5, 0, 12}, byte(ft)),
  114. "(heap page=5 off=12)"; got != want {
  115. t.Fatalf("解码 = %q, 期望 %q", got, want)
  116. }
  117. // PageID == 0 表示空值
  118. if got := decodeFieldValue(make([]byte, 6), byte(ft)); got != "(empty)" {
  119. t.Fatalf("空引用 = %q, 期望 (empty)", got)
  120. }
  121. })
  122. }
  123. }
  124. // TestUnknownFieldTypeIsExplicit 未注册的列型必须显式报未知。
  125. //
  126. // 这是上面两个守卫的前提: 如果未知类型也和"正常解码"长得一样 (以前是当定长字符串
  127. // 打印), 那"漏同步"就永远测不出来 —— 输出看着像正常内容, 实际是错的。
  128. func TestUnknownFieldTypeIsExplicit(t *testing.T) {
  129. if got := decodeFieldValue([]byte{1, 2, 3, 4}, 0x7E); got != unknownFieldValue {
  130. t.Fatalf("未注册类型解码 = %q, 期望 %q", got, unknownFieldValue)
  131. }
  132. // fSz/fName 对未注册类型同样要给出"未知"而不是假装知道
  133. if got := fSz(0x7E); got != 0 {
  134. t.Fatalf("fSz(未注册) = %d, 期望 0", got)
  135. }
  136. if got := fName(0x7E); got != "?" {
  137. t.Fatalf("fName(未注册) = %q, 期望 ?", got)
  138. }
  139. }
  140. // TestFieldLabelShowsRealWidth 类型徽章必须显示【真实行内宽度】。
  141. //
  142. // 字符串家族的类型值只到 256byte 一档, 更宽的列靠 Flag 低 5 位的宽度码声明 —— 于是
  143. // "类型档 256byte" 的列可以是 1024 字节宽。徽章上只写类型名, 看起来就是 256 字节, 与
  144. // 盘上差 4 倍; 而"字符串列有多大"恰恰是看 schema 时最想知道的一件事。所以宽字符串列
  145. // 显示成 string(<真实宽度>B)。这里钉住四条边界:
  146. // - 宽度码列按真实宽度显示 (与 Size 列同源, 不打架);
  147. // - 宽度码为 0 的列保持类型名 (v7 及以前落盘的 schema 低 5 位全为 0, 显示逐字不变);
  148. // - decimal 仍显示 scale;
  149. // - 其余类型不被改写。
  150. func TestFieldLabelShowsRealWidth(t *testing.T) {
  151. wide := types.WidthCodeFor(1024) // 声明 1024 字节 → 向上取到的宽度码 (>0)
  152. wideSz := int(types.FieldSize(types.FieldType_256byte, wide))
  153. if wide == 0 || wideSz < 1024 {
  154. t.Fatalf("宽度码 %d / 宽度 %d: core 的宽度表变了, 本测试的前提不成立", wide, wideSz)
  155. }
  156. cases := []struct {
  157. name string
  158. typ byte
  159. param byte
  160. want string
  161. }{
  162. {"宽字符串列按真实宽度显示", byte(types.FieldType_256byte), wide, fmt.Sprintf("string(%dB)", wideSz)},
  163. {"宽度码 0 保持类型名", byte(types.FieldType_256byte), 0, "256byte"},
  164. {"最窄档同理", byte(types.FieldType_32byte), 0, "32byte"},
  165. {"decimal 显示 scale", byte(types.FieldType_Decimal), 2, "decimal(2)"},
  166. {"decimal 无 scale 不加括号", byte(types.FieldType_Decimal), 0, "decimal"},
  167. {"普通类型不被改写", byte(types.FieldType_Float64), 0, types.TypeNameOf(types.FieldType_Float64)},
  168. {"动态列不被改写", byte(types.FieldType_Dynamicbyte), 0, types.TypeNameOf(types.FieldType_Dynamicbyte)},
  169. {"未注册类型", 0x7E, 0, unknownFieldValue},
  170. }
  171. for _, c := range cases {
  172. sz := int(types.FieldSize(types.FieldType(c.typ), c.param))
  173. if got := fLabel(c.typ, c.param, sz); got != c.want {
  174. t.Errorf("%s: fLabel = %q, 期望 %q", c.name, got, c.want)
  175. }
  176. }
  177. // 提示里必须说破"盘上只存宽度码、这是取档后的宽度": 声明值本身没有保留,
  178. // 不说破就会有人拿这个数当声明值对账 (声明 1000 会显示成档位 1024)。
  179. if tip := fTip(byte(types.FieldType_256byte), wide, wideSz); !strings.Contains(tip, "宽度码") {
  180. t.Errorf("宽度码列的 tip = %q, 未说明宽度码来源", tip)
  181. }
  182. // 序列化后必须带着 label / tip: 前端只认 JSON 的 key, 少一个就退回"只显示类型档名",
  183. // 而"字符串大小"恰好挂在 label 上。这个契约跨了语言边界, 编译器不会替我们看着。
  184. b, err := json.Marshal(MFInfo{Name: "Wide", Label: "string(1024B)", Tip: "行内宽度 1024 字节"})
  185. if err != nil {
  186. t.Fatalf("json.Marshal(MFInfo): %v", err)
  187. }
  188. for _, k := range []string{`"label":"string(1024B)"`, `"tip":"行内宽度 1024 字节"`} {
  189. if !strings.Contains(string(b), k) {
  190. t.Errorf("MFInfo 序列化 = %s, 缺 %s —— 前端拿不到就不会显示", b, k)
  191. }
  192. }
  193. }
  194. // ============================ schema 格式同步守卫 ============================
  195. // TestParseSchemaAgainstCore analyzer 的 schema 解析必须与 core 写盘的字节逐字节一致。
  196. //
  197. // 为什么不在这里手写一份"期望字节/格式": 那测的是"我抄的格式对不对", 抄错了测试也跟着
  198. // 一起错 —— 这次就是这么栽的: 手写的 stride 按 [typ][nameLen][name] 走位, 漏掉了紧跟
  199. // typ 之后的 param 字节, 而没有任何东西拦着它。
  200. //
  201. // 所以这里改成让 core **真的建一张含全部列型的表**, 再用 analyzer 自己的读取链路
  202. // (useDB → rHdr → pTabs) 解析同一个文件, 要求两侧对同一份盘上字节的解释一致。
  203. //
  204. // 回归目标: 缺 param 字节时 masterFields 会退化成"1 个无名字段", versions 变乱数,
  205. // 本测试的第一条断言 (字段个数) 就会失败。
  206. func TestParseSchemaAgainstCore(t *testing.T) {
  207. path := "zz_analyzer_schema_guard.db"
  208. os.Remove(path)
  209. os.Remove(path + ".wal")
  210. t.Cleanup(func() {
  211. // 先释放 analyzer 读取链路 (useDB) 持有的文件句柄: Windows 上被打开的
  212. // 文件无法删除, 不关句柄清理就会失败 —— 失败被 _ = 吞掉后, 仓库里就会
  213. // 永久残留一个 zz_*.db。
  214. fileMu.Lock()
  215. if dbf != nil {
  216. dbf.Close()
  217. dbf = nil
  218. }
  219. fileMu.Unlock()
  220. retiredMu.Lock()
  221. old := retired
  222. retired = nil
  223. retiredMu.Unlock()
  224. for _, f := range old {
  225. if f != nil {
  226. f.Close()
  227. }
  228. }
  229. if t.Failed() {
  230. return // 失败时保留现场, 便于排查
  231. }
  232. _ = os.Remove(path)
  233. _ = os.Remove(path + ".wal")
  234. })
  235. const tableName = "guardtbl"
  236. fields := []types.FieldInfo{
  237. {FieldType: types.FieldType_8byte, FieldName: []byte("_id")},
  238. {FieldType: types.FieldType_32byte, FieldName: []byte("Name")},
  239. {FieldType: types.FieldType_Bool, FieldName: []byte("Flag")},
  240. {FieldType: types.FieldType_Float32, FieldName: []byte("Ratio")},
  241. {FieldType: types.FieldType_Float64, FieldName: []byte("Wide")},
  242. {FieldType: types.FieldType_Decimal, Flag: 2, FieldName: []byte("Amount")},
  243. {FieldType: types.FieldType_UUID, FieldName: []byte("UID")},
  244. {FieldType: types.FieldType_Year, FieldName: []byte("Yr")},
  245. {FieldType: types.FieldType_Int128, FieldName: []byte("Big")},
  246. {FieldType: types.FieldType_Blob, FieldName: []byte("Body")},
  247. {FieldType: types.FieldType_256byte, FieldName: []byte("Note")},
  248. // 用宽度码声明出来的宽字符串列: 它的行内宽度 (1024) 不等于类型默认档 (256)。
  249. // 少了这一列, "Sz 忘了带列级参数" 这类漏同步在这个测试里是看不见的。
  250. {FieldType: types.FieldType_256byte, Flag: types.WidthCodeFor(1024), FieldName: []byte("Wide")},
  251. }
  252. db, err := core.Open(path)
  253. if err != nil {
  254. t.Fatalf("core.Open: %v", err)
  255. }
  256. if _, err := db.CreateTable(tableName, fields); err != nil {
  257. t.Fatalf("core.CreateTable: %v", err)
  258. }
  259. if err := db.Close(); err != nil {
  260. t.Fatalf("core.Close: %v", err)
  261. }
  262. // 走 analyzer 自己的链路: 切库 → 读 HeaderPage → 遍历表页链
  263. if err := useDB(path); err != nil {
  264. t.Fatalf("useDB: %v", err)
  265. }
  266. invalidateAudit()
  267. mu.Lock()
  268. rHdr()
  269. tabs = pTabs()
  270. got := append([]TInfo(nil), tabs...)
  271. mu.Unlock()
  272. if len(got) != 1 {
  273. t.Fatalf("解析出 %d 张表, 期望 1 张", len(got))
  274. }
  275. tb := got[0]
  276. if tb.Name != tableName {
  277. t.Fatalf("表名 = %q, 期望 %q", tb.Name, tableName)
  278. }
  279. if len(tb.MasterFields) != len(fields) {
  280. t.Fatalf("masterFields = %d 个, 期望 %d 个 —— 少读说明字段 stride 与 core 不一致",
  281. len(tb.MasterFields), len(fields))
  282. }
  283. for i, want := range fields {
  284. g := tb.MasterFields[i]
  285. if g.Name != string(want.FieldName) {
  286. t.Errorf("字段 %d 名 = %q, 期望 %q", i, g.Name, want.FieldName)
  287. }
  288. if g.Type != byte(want.FieldType) {
  289. t.Errorf("字段 %d 类型 = %d, 期望 %d", i, g.Type, want.FieldType)
  290. }
  291. if g.Flag != want.Flag {
  292. t.Errorf("字段 %d flag = %d, 期望 %d (状态位与 decimal scale 都在这里)",
  293. i, g.Flag, want.Flag)
  294. }
  295. if g.Param != want.Param() {
  296. t.Errorf("字段 %d 列级参数 = %d, 期望 %d (状态位与列级参数共用一个字节, 取值串位了)",
  297. i, g.Param, want.Param())
  298. }
  299. // 宽度要按【列级参数】算: 字符串列的宽度码 (maxlen 声明) 决定真实行宽,
  300. // 只看类型会一律算成 256, 而盘上那列可能是 1024 —— 行解码 off += sz 全错位。
  301. if wantSz := int(types.FieldSize(want.FieldType, want.Param())); g.Sz != wantSz {
  302. t.Errorf("字段 %d 宽度 = %d, 期望 %d (类型 %s, 列级参数 %d)",
  303. i, g.Sz, wantSz, types.TypeNameOf(want.FieldType), want.Param())
  304. }
  305. // label / tip 是列表里唯一直接显示"这一列多大"的地方 (徽章 + 悬浮说明),
  306. // 空掉就等于前端退回只显示类型档名 —— 而宽度码列的档名 (256byte) 与真实
  307. // 宽度 (1024) 能差 4 倍。所以这里连同 Sz 一起钉住, 保证两者同源。
  308. if g.Label == "" || g.Tip == "" {
  309. t.Errorf("字段 %d (%s) 的 label/tip 为空, 列表里没有类型/宽度可显示", i, g.Name)
  310. }
  311. if !strings.Contains(g.Tip, strconv.Itoa(g.Sz)) {
  312. t.Errorf("字段 %d (%s) 的 tip = %q 未提到行内宽度 %d", i, g.Name, g.Tip, g.Sz)
  313. }
  314. }
  315. // 宽度码声明出来的那列, 徽章上必须能看到真实宽度 (1024), 而不是类型档名 (256byte) ——
  316. // 这正是"字符串大小显示在列表里"要落地的那个位置: 类型那一格就得看得见。
  317. wide := tb.MasterFields[len(fields)-1]
  318. if wide.Name != "Wide" || !strings.Contains(wide.Label, "1024") {
  319. t.Errorf("宽字符串列 %s 的 label = %q, 期望带出真实行内宽度 1024", wide.Name, wide.Label)
  320. }
  321. // 版本链在 masterFields 之后, 是错位的第一个受害者 —— 单独钉住
  322. if len(tb.Versions) != 1 {
  323. t.Fatalf("versions = %d 个, 期望 1 个", len(tb.Versions))
  324. }
  325. if tb.Versions[0].Ver != 1 {
  326. t.Errorf("主版本号 = %d, 期望 1 —— 版本号是乱数说明 masterFields 之后的字节错位了",
  327. tb.Versions[0].Ver)
  328. }
  329. if n := len(tb.Versions[0].Indices); n != len(fields) {
  330. t.Errorf("主版本索引 = %d 个, 期望 %d 个", n, len(fields))
  331. } else {
  332. for i, fi := range tb.Versions[0].Indices {
  333. if fi != i { // 建表后未动过 schema, 本版字段就是 masterFields 的 0..n-1
  334. t.Errorf("版本字段下标[%d] = %d, 期望 %d", i, fi, i)
  335. }
  336. }
  337. }
  338. // VInfo.Indices 必须序列化成【数字数组】: 它是 []byte 时 JSON 会变成 base64 字符串
  339. // ("AAECAwQ="), 前端 v.indices[i] 拿到的是第 i 个字符, masterFields['A'] 恒为
  340. // undefined —— "这一版每列多宽"整块静默消失, 而页面只是少一行悬浮说明, 不报错。
  341. vJSON, err := json.Marshal(tb.Versions[0])
  342. if err != nil {
  343. t.Fatalf("json.Marshal(VInfo): %v", err)
  344. }
  345. if !strings.Contains(string(vJSON), `"indices":[0,1`) {
  346. t.Errorf("VInfo 序列化 = %s, 期望 \"indices\" 是数字数组", vJSON)
  347. }
  348. }
  349. // TestParseFlagsAgainstCore 列的可空/唯一声明与索引唯一位的读取。
  350. //
  351. // 这三件事各自只藏在盘上的一个位里 (列 flag 的 bit6 / bit5, 索引 flag 的 bit0)。
  352. // 读错了不会报错, 只会把"可空"画成"非空"、把唯一约束画成普通索引 —— 一张表的
  353. // 约束在分析结果里就此消失, 而它恰恰是看 schema 时最想知道的东西。所以这里不重述
  354. // 位布局, 而是用 core 造一张**真库**, 要求 analyzer 的看法与建表时的声明一致。
  355. func TestParseFlagsAgainstCore(t *testing.T) {
  356. path := "zz_analyzer_flags_guard.db"
  357. os.Remove(path)
  358. os.Remove(path + ".wal")
  359. t.Cleanup(func() {
  360. fileMu.Lock()
  361. if dbf != nil {
  362. dbf.Close()
  363. dbf = nil
  364. }
  365. fileMu.Unlock()
  366. retiredMu.Lock()
  367. old := retired
  368. retired = nil
  369. retiredMu.Unlock()
  370. for _, f := range old {
  371. if f != nil {
  372. f.Close()
  373. }
  374. }
  375. if t.Failed() {
  376. return // 失败时保留现场
  377. }
  378. _ = os.Remove(path)
  379. _ = os.Remove(path + ".wal")
  380. })
  381. const tableName = "flagtbl"
  382. fields := []types.FieldInfo{
  383. {FieldType: types.FieldType_8byte, FieldName: []byte("_id")},
  384. {FieldType: types.FieldType_8byte, FieldName: []byte("Note"), Flag: types.FlagNullable | types.FlagUnique},
  385. {FieldType: types.FieldType_8byte, FieldName: []byte("Plain")},
  386. {FieldType: types.FieldType_Decimal, FieldName: []byte("Amount"), Flag: 3},
  387. }
  388. db, err := core.Open(path)
  389. if err != nil {
  390. t.Fatalf("core.Open: %v", err)
  391. }
  392. tbl, err := db.CreateTable(tableName, fields)
  393. if err != nil {
  394. t.Fatalf("core.CreateTable: %v", err)
  395. }
  396. // Note 上挂唯一索引 (列声明与索引约束都到位), Plain 上挂普通索引作对照,
  397. // 主键索引不带唯一位 (行存储索引的 pkID 自成唯一, 标记是二级索引的事)。
  398. for _, spec := range []struct {
  399. name string
  400. typ page.IndexType
  401. col byte
  402. unique bool
  403. }{
  404. {"pk_" + tableName, page.IndexTypePrimary, 0, false},
  405. {"idx_note", page.IndexTypeBTree, 1, true},
  406. {"idx_plain", page.IndexTypeBTree, 2, false},
  407. } {
  408. if _, err := tbl.CreateIndex(spec.name, spec.typ, []byte{spec.col}, spec.unique); err != nil {
  409. t.Fatalf("CreateIndex %s: %v", spec.name, err)
  410. }
  411. }
  412. // 插一行: 索引要有条目才有 EntryPage, 空索引在盘上没有数据页, analyzer 会把
  413. // 它当成"没有索引"(见索引解析里对 ep == 0 的过滤)。要验证的是索引上的唯一位,
  414. // 就得让索引真的建起来。Amount (decimal) 不给值, 非空列会落零值。
  415. if _, err := tbl.InsertRow([]types.FieldValue{
  416. {FieldName: []byte("_id"), Value: uint64(1)},
  417. {FieldName: []byte("Note"), Value: uint64(7)},
  418. {FieldName: []byte("Plain"), Value: uint64(9)},
  419. }); err != nil {
  420. t.Fatalf("InsertRow: %v", err)
  421. }
  422. if err := db.Commit(); err != nil {
  423. t.Fatalf("core.Commit: %v", err)
  424. }
  425. if err := db.Close(); err != nil {
  426. t.Fatalf("core.Close: %v", err)
  427. }
  428. // 走 analyzer 自己的链路
  429. if err := useDB(path); err != nil {
  430. t.Fatalf("useDB: %v", err)
  431. }
  432. invalidateAudit()
  433. mu.Lock()
  434. rHdr()
  435. tabs = pTabs()
  436. got := append([]TInfo(nil), tabs...)
  437. mu.Unlock()
  438. if len(got) != 1 {
  439. t.Fatalf("解析出 %d 张表, 期望 1 张", len(got))
  440. }
  441. tb := got[0]
  442. if len(tb.MasterFields) != len(fields) {
  443. t.Fatalf("masterFields = %d 个, 期望 %d 个", len(tb.MasterFields), len(fields))
  444. }
  445. wantNullable := map[string]bool{"_id": false, "Note": true, "Plain": false, "Amount": false}
  446. wantUnique := map[string]bool{"_id": false, "Note": true, "Plain": false, "Amount": false}
  447. for _, f := range tb.MasterFields {
  448. if f.Nullable != wantNullable[f.Name] {
  449. t.Errorf("列 %s 的可空 = %v, 期望 %v (flag=0x%02x)",
  450. f.Name, f.Nullable, wantNullable[f.Name], f.Flag)
  451. }
  452. if f.Unique != wantUnique[f.Name] {
  453. t.Errorf("列 %s 的唯一声明 = %v, 期望 %v (flag=0x%02x)",
  454. f.Name, f.Unique, wantUnique[f.Name], f.Flag)
  455. }
  456. if f.Name == "Amount" && f.Param != 3 {
  457. t.Errorf("decimal 列的 scale = %d, 期望 3 —— scale 与状态位共用一个字节, 取值串位了",
  458. f.Param)
  459. }
  460. }
  461. if len(tb.Idxs) != 3 {
  462. t.Fatalf("索引 = %d 个, 期望 3 个: %v", len(tb.Idxs), tb.Idxs)
  463. }
  464. uniq := map[string]bool{}
  465. for _, idx := range tb.Idxs {
  466. uniq[idx.Name] = idx.Uniq
  467. }
  468. if !uniq["idx_note"] {
  469. t.Errorf("唯一索引 idx_note 没被认出来 (idx flag 的 bit0): %v", tb.Idxs)
  470. }
  471. if uniq["idx_plain"] {
  472. t.Errorf("普通索引 idx_plain 被错认成唯一: %v", tb.Idxs)
  473. }
  474. if uniq["pk_"+tableName] {
  475. t.Errorf("行存储主键索引不该带唯一位: %v", tb.Idxs)
  476. }
  477. }