| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- package main
- import (
- "encoding/json"
- "fmt"
- "math/big"
- "os"
- "strconv"
- "strings"
- "testing"
- "git.ycl.pub/yl32940/TsDB2/DB/core"
- "git.ycl.pub/yl32940/TsDB2/DB/core/page"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- )
- // ============================ 列型表同步守卫 ============================
- //
- // analyzer 在自己的模块里手写偏移直接读盘 (见 layout_assert.go 的说明)。布局偏移有
- // 编译期断言兜着, 但"列类型 → 宽度 / 名字 / 解码分支"这三张表以前是纯手写的, 没有
- // 任何东西拦着它漂移 —— 而它已经漂过一次: core 新增 7 个列型 (bool/float32/decimal/
- // uuid/year/int128/blob) 时 analyzer 没跟, 后果不是"显示成 ?", 而是:
- //
- // - fSz 返回 0 → 行解码 (decodeRows 里的 off += sz) 偏移不推进, 新类型字段【之后
- // 的所有列】都从同一个错位偏移解码, 整行静默错位;
- // - indexcheck 因 size <= 0 直接放弃索引键校验;
- // - decodeFieldValue 把新类型当定长字符串, 打印出二进制噪声。
- //
- // 现在 fSz/fName 改成投影到 core 的 typeRegistry (漏同步在结构上不可能), 但
- // decodeFieldValue 仍是一份手写 switch —— 这两个测试就是它的守卫。
- //
- // 判据一律取自 core 的 types.AllTypes(), 而不是在这里再抄一份期望名单: 抄一份就又
- // 造出一个会漂移的副本, 而这回的教训恰恰就是"手写副本没跟着新类型走"。
- // TestDecodeCoversEveryRegisteredType 要求 core 注册的每一个列型都被 analyzer 认得出:
- // 有宽度、有名字、能解码。
- func TestDecodeCoversEveryRegisteredType(t *testing.T) {
- for _, ft := range types.AllTypes() {
- t.Run(types.TypeNameOf(ft), func(t *testing.T) {
- sz := fSz(byte(ft))
- if want := int(types.GetSize(ft)); sz != want {
- t.Fatalf("fSz(%d) = %d, core 注册的是 %d", ft, sz, want)
- }
- if sz <= 0 {
- t.Fatalf("fSz(%d) = 0: 行解码靠它推进偏移, 0 会让整行字段错位", ft)
- }
- if n := fName(byte(ft)); n == "?" {
- t.Fatalf("fName(%d) = ?: 该类型在 core 已注册, 名字不该是未知", ft)
- }
- // 全零缓冲对每个列型都必须能解出结果 (而不是落到未同步分支)。
- // 这里用零值而不是真值, 是为了把"这个类型有没有被处理"和"解出来的值对不对"
- // 分开: 后者由 TestDecodeNewFieldTypes 用 core 编码的真值来钉。
- buf := make([]byte, sz)
- if got := decodeFieldValue(buf, byte(ft)); got == unknownFieldValue {
- t.Fatalf("decodeFieldValue 未处理类型 %d (%s): 落到了未同步分支",
- ft, types.TypeNameOf(ft))
- }
- })
- }
- }
- // TestDecodeNewFieldTypes 新增的列型必须按盘上语义正确解码。
- //
- // 字节一律由 core 的 types.EncodeValue 生成 —— 测的是"两份独立实现在同一份盘格式下
- // 读出来一不一致", 而不是"自己编码再自己解码"的自证 (那种测试实现错了一起错也全绿)。
- func TestDecodeNewFieldTypes(t *testing.T) {
- minInt128, ok := new(big.Int).SetString("-170141183460469231731687303715884105728", 10)
- if !ok {
- t.Fatal("minInt128 字面量解析失败")
- }
- cases := []struct {
- name string
- ft types.FieldType
- param byte
- val any
- want string
- }{
- {"bool true", types.FieldType_Bool, 0, true, "true"},
- {"bool false", types.FieldType_Bool, 0, false, "false"},
- {"float32 正", types.FieldType_Float32, 0, float32(0.5), "0.5"},
- {"float32 负", types.FieldType_Float32, 0, float32(-2.25), "-2.25"},
- {"float64 正", types.FieldType_Float64, 0, 0.5, "0.5"},
- {"float64 负", types.FieldType_Float64, 0, -2.25, "-2.25"},
- {"decimal 两位", types.FieldType_Decimal, 2, "1.50", "1.50"},
- {"decimal 负不足一位", types.FieldType_Decimal, 2, "-0.05", "-0.05"},
- {"decimal 补零到位", types.FieldType_Decimal, 2, "0", "0.00"},
- {
- "uuid", types.FieldType_UUID, 0,
- "550e8400-e29b-41d4-a716-446655440000",
- "550e8400-e29b-41d4-a716-446655440000",
- },
- {"year", types.FieldType_Year, 0, 2026, "2026"},
- {"int128 下界", types.FieldType_Int128, 0, minInt128,
- "-170141183460469231731687303715884105728"},
- {"int128 零", types.FieldType_Int128, 0, new(big.Int), "0"},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- buf := make([]byte, types.GetSize(c.ft))
- if err := types.EncodeValue(c.ft, c.param, c.val, buf); err != nil {
- t.Fatalf("core EncodeValue 失败: %v", err)
- }
- if got := decodeFieldValue(buf, byte(c.ft)); got != c.want {
- t.Fatalf("解码 = %q, 期望 %q (盘上字节 %x)", got, c.want, buf)
- }
- })
- }
- }
- // TestDecodeDynamicRef 动态列的行内 6 字节是堆引用, 不是内容。
- //
- // 动态列不走 EncodeValue (内容在堆页, 由 core 解引用后拿到), 所以单独钉。
- // 以前它落到 default 分支, 把页号和偏移当字符串打印出来 —— 真正想看的
- // "内容在哪一页" 反而看不见。
- func TestDecodeDynamicRef(t *testing.T) {
- // 行内引用布局: [PageID uint32 大端][Offset uint16 大端]
- if got, want := decodeFieldValue([]byte{0, 0, 0, 5, 0, 12}, byte(types.FieldType_Dynamicbyte)),
- "(heap page=5 off=12)"; got != want {
- t.Fatalf("解码 = %q, 期望 %q", got, want)
- }
- // PageID == 0 表示空值
- if got := decodeFieldValue(make([]byte, 6), byte(types.FieldType_Dynamicbyte)); got != "(empty)" {
- t.Fatalf("空引用 = %q, 期望 (empty)", got)
- }
- }
- // TestDecodeBlobRef blob 列 (v10) 的行内 8 字节是链引用, 不是内容。
- //
- // 与动态列长得像但读法不同 —— 布局是 [TotalLen uint32 大端][FirstPageID uint32 大端],
- // 拿动态列那套 (PageID 在前、只有 6 字节) 去读会把长度当页号、还会越界读。
- func TestDecodeBlobRef(t *testing.T) {
- // buf[0:4] = TotalLen = 5, buf[4:8] = FirstPageID = 12
- if got, want := decodeFieldValue([]byte{0, 0, 0, 5, 0, 0, 0, 12}, byte(types.FieldType_Blob)),
- "(blob page=12 len=5)"; got != want {
- t.Fatalf("解码 = %q, 期望 %q", got, want)
- }
- // (TotalLen, FirstPageID) == (0, 0) 表示空 blob
- if got := decodeFieldValue(make([]byte, 8), byte(types.FieldType_Blob)); got != "(empty)" {
- t.Fatalf("空引用 = %q, 期望 (empty)", got)
- }
- }
- // TestUnknownFieldTypeIsExplicit 未注册的列型必须显式报未知。
- //
- // 这是上面两个守卫的前提: 如果未知类型也和"正常解码"长得一样 (以前是当定长字符串
- // 打印), 那"漏同步"就永远测不出来 —— 输出看着像正常内容, 实际是错的。
- func TestUnknownFieldTypeIsExplicit(t *testing.T) {
- if got := decodeFieldValue([]byte{1, 2, 3, 4}, 0x7E); got != unknownFieldValue {
- t.Fatalf("未注册类型解码 = %q, 期望 %q", got, unknownFieldValue)
- }
- // fSz/fName 对未注册类型同样要给出"未知"而不是假装知道
- if got := fSz(0x7E); got != 0 {
- t.Fatalf("fSz(未注册) = %d, 期望 0", got)
- }
- if got := fName(0x7E); got != "?" {
- t.Fatalf("fName(未注册) = %q, 期望 ?", got)
- }
- }
- // TestFieldLabelShowsRealWidth 类型徽章必须显示【真实行内宽度】。
- //
- // 定长字符串列的行内宽度完全由 Flag 低 5 位的宽度码决定 (类型值只有一个 String) —— 于是
- // 同一类型的列可以是 1024 字节宽。徽章上只写类型名, 看起来就是默认的 256 字节, 与
- // 盘上差 4 倍; 而"字符串列有多大"恰恰是看 schema 时最想知道的一件事。所以宽字符串列
- // 显示成 string(<真实宽度>B)。这里钉住四条边界:
- // - 宽度码列按真实宽度显示 (与 Size 列同源, 不打架);
- // - 宽度码为 0 的列保持类型名 (v7 及以前落盘的 schema 低 5 位全为 0, 显示逐字不变);
- // - decimal 仍显示 scale;
- // - 其余类型不被改写。
- func TestFieldLabelShowsRealWidth(t *testing.T) {
- wide := types.WidthCodeFor(1024) // 声明 1024 字节 → 向上取到的宽度码 (>0)
- wideSz := int(types.FieldSize(types.FieldType_String, wide))
- if wide == 0 || wideSz < 1024 {
- t.Fatalf("宽度码 %d / 宽度 %d: core 的宽度表变了, 本测试的前提不成立", wide, wideSz)
- }
- cases := []struct {
- name string
- typ byte
- param byte
- want string
- }{
- {"宽字符串列按真实宽度显示", byte(types.FieldType_String), wide, fmt.Sprintf("string(%dB)", wideSz)},
- {"宽度码 0 保持类型名", byte(types.FieldType_String), 0, types.TypeNameOf(types.FieldType_String)},
- {"decimal 显示 scale", byte(types.FieldType_Decimal), 2, "decimal(2)"},
- {"decimal 无 scale 不加括号", byte(types.FieldType_Decimal), 0, "decimal"},
- {"普通类型不被改写", byte(types.FieldType_Float64), 0, types.TypeNameOf(types.FieldType_Float64)},
- {"动态列不被改写", byte(types.FieldType_Dynamicbyte), 0, types.TypeNameOf(types.FieldType_Dynamicbyte)},
- {"未注册类型", 0x7E, 0, unknownFieldValue},
- }
- for _, c := range cases {
- sz := int(types.FieldSize(types.FieldType(c.typ), c.param))
- if got := fLabel(c.typ, c.param, sz); got != c.want {
- t.Errorf("%s: fLabel = %q, 期望 %q", c.name, got, c.want)
- }
- }
- // 提示里必须说破"盘上只存宽度码、这是取档后的宽度": 声明值本身没有保留,
- // 不说破就会有人拿这个数当声明值对账 (声明 1000 会显示成档位 1024)。
- if tip := fTip(byte(types.FieldType_String), wide, wideSz); !strings.Contains(tip, "宽度码") {
- t.Errorf("宽度码列的 tip = %q, 未说明宽度码来源", tip)
- }
- // 序列化后必须带着 label / tip: 前端只认 JSON 的 key, 少一个就退回"只显示类型档名",
- // 而"字符串大小"恰好挂在 label 上。这个契约跨了语言边界, 编译器不会替我们看着。
- b, err := json.Marshal(MFInfo{Name: "Wide", Label: "string(1024B)", Tip: "行内宽度 1024 字节"})
- if err != nil {
- t.Fatalf("json.Marshal(MFInfo): %v", err)
- }
- for _, k := range []string{`"label":"string(1024B)"`, `"tip":"行内宽度 1024 字节"`} {
- if !strings.Contains(string(b), k) {
- t.Errorf("MFInfo 序列化 = %s, 缺 %s —— 前端拿不到就不会显示", b, k)
- }
- }
- }
- // ============================ schema 格式同步守卫 ============================
- // TestParseSchemaAgainstCore analyzer 的 schema 解析必须与 core 写盘的字节逐字节一致。
- //
- // 为什么不在这里手写一份"期望字节/格式": 那测的是"我抄的格式对不对", 抄错了测试也跟着
- // 一起错 —— 这次就是这么栽的: 手写的 stride 按 [typ][nameLen][name] 走位, 漏掉了紧跟
- // typ 之后的 param 字节, 而没有任何东西拦着它。
- //
- // 所以这里改成让 core **真的建一张含全部列型的表**, 再用 analyzer 自己的读取链路
- // (useDB → rHdr → pTabs) 解析同一个文件, 要求两侧对同一份盘上字节的解释一致。
- //
- // 回归目标: 缺 param 字节时 masterFields 会退化成"1 个无名字段", versions 变乱数,
- // 本测试的第一条断言 (字段个数) 就会失败。
- func TestParseSchemaAgainstCore(t *testing.T) {
- path := "zz_analyzer_schema_guard.db"
- os.Remove(path)
- os.Remove(path + ".wal")
- t.Cleanup(func() {
- // 先释放 analyzer 读取链路 (useDB) 持有的文件句柄: Windows 上被打开的
- // 文件无法删除, 不关句柄清理就会失败 —— 失败被 _ = 吞掉后, 仓库里就会
- // 永久残留一个 zz_*.db。
- fileMu.Lock()
- if dbf != nil {
- dbf.Close()
- dbf = nil
- }
- fileMu.Unlock()
- retiredMu.Lock()
- old := retired
- retired = nil
- retiredMu.Unlock()
- for _, f := range old {
- if f != nil {
- f.Close()
- }
- }
- if t.Failed() {
- return // 失败时保留现场, 便于排查
- }
- _ = os.Remove(path)
- _ = os.Remove(path + ".wal")
- })
- const tableName = "guardtbl"
- fields := []types.FieldInfo{
- {FieldType: types.FieldType_Uint64, FieldName: []byte("_id")},
- {FieldType: types.FieldType_String, FieldName: []byte("Name")},
- {FieldType: types.FieldType_Bool, FieldName: []byte("Flag")},
- {FieldType: types.FieldType_Float32, FieldName: []byte("Ratio")},
- {FieldType: types.FieldType_Float64, FieldName: []byte("Wide")},
- {FieldType: types.FieldType_Decimal, Flag: 2, FieldName: []byte("Amount")},
- {FieldType: types.FieldType_UUID, FieldName: []byte("UID")},
- {FieldType: types.FieldType_Year, FieldName: []byte("Yr")},
- {FieldType: types.FieldType_Int128, FieldName: []byte("Big")},
- {FieldType: types.FieldType_Blob, FieldName: []byte("Body")},
- {FieldType: types.FieldType_String, FieldName: []byte("Note")},
- // 用宽度码声明出来的宽字符串列: 它的行内宽度 (1024) 不等于类型默认档 (256)。
- // 少了这一列, "Sz 忘了带列级参数" 这类漏同步在这个测试里是看不见的。
- {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(1024), FieldName: []byte("Wide")},
- }
- db, err := core.Open(path)
- if err != nil {
- t.Fatalf("core.Open: %v", err)
- }
- if _, err := db.CreateTable(tableName, fields); err != nil {
- t.Fatalf("core.CreateTable: %v", err)
- }
- if err := db.Close(); err != nil {
- t.Fatalf("core.Close: %v", err)
- }
- // 走 analyzer 自己的链路: 切库 → 读 HeaderPage → 遍历表页链
- if err := useDB(path); err != nil {
- t.Fatalf("useDB: %v", err)
- }
- invalidateAudit()
- mu.Lock()
- rHdr()
- tabs = pTabs()
- got := append([]TInfo(nil), tabs...)
- mu.Unlock()
- if len(got) != 1 {
- t.Fatalf("解析出 %d 张表, 期望 1 张", len(got))
- }
- tb := got[0]
- if tb.Name != tableName {
- t.Fatalf("表名 = %q, 期望 %q", tb.Name, tableName)
- }
- if len(tb.MasterFields) != len(fields) {
- t.Fatalf("masterFields = %d 个, 期望 %d 个 —— 少读说明字段 stride 与 core 不一致",
- len(tb.MasterFields), len(fields))
- }
- for i, want := range fields {
- g := tb.MasterFields[i]
- if g.Name != string(want.FieldName) {
- t.Errorf("字段 %d 名 = %q, 期望 %q", i, g.Name, want.FieldName)
- }
- if g.Type != byte(want.FieldType) {
- t.Errorf("字段 %d 类型 = %d, 期望 %d", i, g.Type, want.FieldType)
- }
- if g.Flag != want.Flag {
- t.Errorf("字段 %d flag = %d, 期望 %d (状态位与 decimal scale 都在这里)",
- i, g.Flag, want.Flag)
- }
- if g.Param != want.Param() {
- t.Errorf("字段 %d 列级参数 = %d, 期望 %d (状态位与列级参数共用一个字节, 取值串位了)",
- i, g.Param, want.Param())
- }
- // 宽度要按【列级参数】算: 字符串列的宽度码 (maxlen 声明) 决定真实行宽,
- // 只看类型会一律算成 256, 而盘上那列可能是 1024 —— 行解码 off += sz 全错位。
- if wantSz := int(types.FieldSize(want.FieldType, want.Param())); g.Sz != wantSz {
- t.Errorf("字段 %d 宽度 = %d, 期望 %d (类型 %s, 列级参数 %d)",
- i, g.Sz, wantSz, types.TypeNameOf(want.FieldType), want.Param())
- }
- // label / tip 是列表里唯一直接显示"这一列多大"的地方 (徽章 + 悬浮说明),
- // 空掉就等于前端退回只显示类型档名 —— 而宽度码列的档名 (string) 与真实
- // 宽度 (1024) 能差 4 倍。所以这里连同 Sz 一起钉住, 保证两者同源。
- if g.Label == "" || g.Tip == "" {
- t.Errorf("字段 %d (%s) 的 label/tip 为空, 列表里没有类型/宽度可显示", i, g.Name)
- }
- if !strings.Contains(g.Tip, strconv.Itoa(g.Sz)) {
- t.Errorf("字段 %d (%s) 的 tip = %q 未提到行内宽度 %d", i, g.Name, g.Tip, g.Sz)
- }
- }
- // 宽度码声明出来的那列, 徽章上必须能看到真实宽度 (1024), 而不是类型档名 (string) ——
- // 这正是"字符串大小显示在列表里"要落地的那个位置: 类型那一格就得看得见。
- wide := tb.MasterFields[len(fields)-1]
- if wide.Name != "Wide" || !strings.Contains(wide.Label, "1024") {
- t.Errorf("宽字符串列 %s 的 label = %q, 期望带出真实行内宽度 1024", wide.Name, wide.Label)
- }
- // 版本链在 masterFields 之后, 是错位的第一个受害者 —— 单独钉住
- if len(tb.Versions) != 1 {
- t.Fatalf("versions = %d 个, 期望 1 个", len(tb.Versions))
- }
- if tb.Versions[0].Ver != 1 {
- t.Errorf("主版本号 = %d, 期望 1 —— 版本号是乱数说明 masterFields 之后的字节错位了",
- tb.Versions[0].Ver)
- }
- if n := len(tb.Versions[0].Indices); n != len(fields) {
- t.Errorf("主版本索引 = %d 个, 期望 %d 个", n, len(fields))
- } else {
- for i, fi := range tb.Versions[0].Indices {
- if fi != i { // 建表后未动过 schema, 本版字段就是 masterFields 的 0..n-1
- t.Errorf("版本字段下标[%d] = %d, 期望 %d", i, fi, i)
- }
- }
- }
- // VInfo.Indices 必须序列化成【数字数组】: 它是 []byte 时 JSON 会变成 base64 字符串
- // ("AAECAwQ="), 前端 v.indices[i] 拿到的是第 i 个字符, masterFields['A'] 恒为
- // undefined —— "这一版每列多宽"整块静默消失, 而页面只是少一行悬浮说明, 不报错。
- vJSON, err := json.Marshal(tb.Versions[0])
- if err != nil {
- t.Fatalf("json.Marshal(VInfo): %v", err)
- }
- if !strings.Contains(string(vJSON), `"indices":[0,1`) {
- t.Errorf("VInfo 序列化 = %s, 期望 \"indices\" 是数字数组", vJSON)
- }
- }
- // TestParseFlagsAgainstCore 列的可空/唯一声明与索引唯一位的读取。
- //
- // 这三件事各自只藏在盘上的一个位里 (列 flag 的 bit6 / bit5, 索引 flag 的 bit0)。
- // 读错了不会报错, 只会把"可空"画成"非空"、把唯一约束画成普通索引 —— 一张表的
- // 约束在分析结果里就此消失, 而它恰恰是看 schema 时最想知道的东西。所以这里不重述
- // 位布局, 而是用 core 造一张**真库**, 要求 analyzer 的看法与建表时的声明一致。
- func TestParseFlagsAgainstCore(t *testing.T) {
- path := "zz_analyzer_flags_guard.db"
- os.Remove(path)
- os.Remove(path + ".wal")
- t.Cleanup(func() {
- fileMu.Lock()
- if dbf != nil {
- dbf.Close()
- dbf = nil
- }
- fileMu.Unlock()
- retiredMu.Lock()
- old := retired
- retired = nil
- retiredMu.Unlock()
- for _, f := range old {
- if f != nil {
- f.Close()
- }
- }
- if t.Failed() {
- return // 失败时保留现场
- }
- _ = os.Remove(path)
- _ = os.Remove(path + ".wal")
- })
- const tableName = "flagtbl"
- fields := []types.FieldInfo{
- {FieldType: types.FieldType_Uint64, FieldName: []byte("_id")},
- {FieldType: types.FieldType_Uint64, FieldName: []byte("Note"), Flag: types.FlagNullable | types.FlagUnique},
- {FieldType: types.FieldType_Uint64, FieldName: []byte("Plain")},
- {FieldType: types.FieldType_Decimal, FieldName: []byte("Amount"), Flag: 3},
- }
- db, err := core.Open(path)
- if err != nil {
- t.Fatalf("core.Open: %v", err)
- }
- tbl, err := db.CreateTable(tableName, fields)
- if err != nil {
- t.Fatalf("core.CreateTable: %v", err)
- }
- // Note 上挂唯一索引 (列声明与索引约束都到位), Plain 上挂普通索引作对照,
- // 主键索引不带唯一位 (行存储索引的 pkID 自成唯一, 标记是二级索引的事)。
- for _, spec := range []struct {
- name string
- typ page.IndexType
- col byte
- unique bool
- }{
- {"pk_" + tableName, page.IndexTypePrimary, 0, false},
- {"idx_note", page.IndexTypeBTree, 1, true},
- {"idx_plain", page.IndexTypeBTree, 2, false},
- } {
- if _, err := tbl.CreateIndex(spec.name, spec.typ, []byte{spec.col}, spec.unique); err != nil {
- t.Fatalf("CreateIndex %s: %v", spec.name, err)
- }
- }
- // 插一行: 索引要有条目才有 EntryPage, 空索引在盘上没有数据页, analyzer 会把
- // 它当成"没有索引"(见索引解析里对 ep == 0 的过滤)。要验证的是索引上的唯一位,
- // 就得让索引真的建起来。Amount (decimal) 不给值, 非空列会落零值。
- if _, err := tbl.InsertRow([]types.FieldValue{
- {FieldName: []byte("_id"), Value: uint64(1)},
- {FieldName: []byte("Note"), Value: uint64(7)},
- {FieldName: []byte("Plain"), Value: uint64(9)},
- }); err != nil {
- t.Fatalf("InsertRow: %v", err)
- }
- if err := db.Commit(); err != nil {
- t.Fatalf("core.Commit: %v", err)
- }
- if err := db.Close(); err != nil {
- t.Fatalf("core.Close: %v", err)
- }
- // 走 analyzer 自己的链路
- if err := useDB(path); err != nil {
- t.Fatalf("useDB: %v", err)
- }
- invalidateAudit()
- mu.Lock()
- rHdr()
- tabs = pTabs()
- got := append([]TInfo(nil), tabs...)
- mu.Unlock()
- if len(got) != 1 {
- t.Fatalf("解析出 %d 张表, 期望 1 张", len(got))
- }
- tb := got[0]
- if len(tb.MasterFields) != len(fields) {
- t.Fatalf("masterFields = %d 个, 期望 %d 个", len(tb.MasterFields), len(fields))
- }
- wantNullable := map[string]bool{"_id": false, "Note": true, "Plain": false, "Amount": false}
- wantUnique := map[string]bool{"_id": false, "Note": true, "Plain": false, "Amount": false}
- for _, f := range tb.MasterFields {
- if f.Nullable != wantNullable[f.Name] {
- t.Errorf("列 %s 的可空 = %v, 期望 %v (flag=0x%02x)",
- f.Name, f.Nullable, wantNullable[f.Name], f.Flag)
- }
- if f.Unique != wantUnique[f.Name] {
- t.Errorf("列 %s 的唯一声明 = %v, 期望 %v (flag=0x%02x)",
- f.Name, f.Unique, wantUnique[f.Name], f.Flag)
- }
- if f.Name == "Amount" && f.Param != 3 {
- t.Errorf("decimal 列的 scale = %d, 期望 3 —— scale 与状态位共用一个字节, 取值串位了",
- f.Param)
- }
- }
- if len(tb.Idxs) != 3 {
- t.Fatalf("索引 = %d 个, 期望 3 个: %v", len(tb.Idxs), tb.Idxs)
- }
- uniq := map[string]bool{}
- for _, idx := range tb.Idxs {
- uniq[idx.Name] = idx.Uniq
- }
- if !uniq["idx_note"] {
- t.Errorf("唯一索引 idx_note 没被认出来 (idx flag 的 bit0): %v", tb.Idxs)
- }
- if uniq["idx_plain"] {
- t.Errorf("普通索引 idx_plain 被错认成唯一: %v", tb.Idxs)
- }
- if uniq["pk_"+tableName] {
- t.Errorf("行存储主键索引不该带唯一位: %v", tb.Idxs)
- }
- }
|