main_test.go 21 KB

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