|
|
@@ -8,6 +8,7 @@ import (
|
|
|
"git.ycl.pub/yl32940/TsDB2/DB/core/types"
|
|
|
"git.ycl.pub/yl32940/TsDB2/DB/core/util"
|
|
|
"database/sql"
|
|
|
+ "fmt"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"testing"
|
|
|
@@ -265,32 +266,37 @@ func TestTextWidthScanDisabled(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TestTextWidthCode 宽度规则本身: 大于等于 maxLen 的最小 2 次幂, 下限 32, 上限 2048,
|
|
|
-// 超过 2048 不可定宽。档位必须精确命中 (WidthCodeWidth 就是 2 次幂本身, 不是 224 之类)。
|
|
|
+// TestTextWidthCode 宽度规则本身: 大于等于 maxLen 的最小 2 次幂, 下限 32, 上限 2048;
|
|
|
+// 2048 < maxLen ≤ MaxStringSize 保持动态列; maxLen > MaxStringSize 落 Blob 链式列。
|
|
|
+// 档位必须精确命中 (WidthCodeWidth 就是 2 次幂本身, 不是 224 之类)。
|
|
|
func TestTextWidthCode(t *testing.T) {
|
|
|
cases := []struct {
|
|
|
maxLen int64
|
|
|
wantWidth int
|
|
|
- wantOK bool
|
|
|
+ wantClass textWidthClass
|
|
|
}{
|
|
|
- {0, 32, true}, // 全 NULL / 空表
|
|
|
- {1, 32, true}, // 下限 32: 任何非空值至少 32 档
|
|
|
- {16, 32, true}, // 16 字节也抬到 32 (下限)
|
|
|
- {32, 32, true},
|
|
|
- {33, 64, true},
|
|
|
- {100, 128, true},
|
|
|
- {2048, 2048, true},
|
|
|
- {2049, 0, false}, // 超上限: 保持动态列
|
|
|
- {5000, 0, false},
|
|
|
+ {0, 32, widthToFixed}, // 全 NULL / 空表
|
|
|
+ {1, 32, widthToFixed}, // 下限 32: 任何非空值至少 32 档
|
|
|
+ {16, 32, widthToFixed}, // 16 字节也抬到 32 (下限)
|
|
|
+ {32, 32, widthToFixed},
|
|
|
+ {33, 64, widthToFixed},
|
|
|
+ {100, 128, widthToFixed},
|
|
|
+ {2048, 2048, widthToFixed},
|
|
|
+ {2049, 0, widthToDynamic}, // 超定宽上限: 保持动态列
|
|
|
+ {5000, 0, widthToDynamic},
|
|
|
+ {16360, 0, widthToDynamic}, // 正好顶到单值上限: 动态列仍装得下
|
|
|
+ {16361, 0, widthToBlob}, // 超单值上限: 落 Blob 链式列
|
|
|
+ {60000, 0, widthToBlob},
|
|
|
+ {1 << 30, 0, widthToBlob},
|
|
|
}
|
|
|
for _, tc := range cases {
|
|
|
- code, width, ok := textWidthCode(tc.maxLen)
|
|
|
- if ok != tc.wantOK || width != tc.wantWidth {
|
|
|
- t.Errorf("textWidthCode(%d) = (码 %d, 宽 %d, ok %v), 期望 (宽 %d, ok %v)",
|
|
|
- tc.maxLen, code, width, ok, tc.wantWidth, tc.wantOK)
|
|
|
+ code, width, class := textWidthCode(tc.maxLen)
|
|
|
+ if class != tc.wantClass || width != tc.wantWidth {
|
|
|
+ t.Errorf("textWidthCode(%d) = (码 %d, 宽 %d, 态 %d), 期望 (宽 %d, 态 %d)",
|
|
|
+ tc.maxLen, code, width, class, tc.wantWidth, tc.wantClass)
|
|
|
continue
|
|
|
}
|
|
|
- if ok && int(types.WidthCodeWidth(code)) != width {
|
|
|
+ if class == widthToFixed && int(types.WidthCodeWidth(code)) != width {
|
|
|
t.Errorf("textWidthCode(%d): 宽度码 %d 对应 %d 字节, 与档位 %d 不符 (没有精确命中)",
|
|
|
tc.maxLen, code, types.WidthCodeWidth(code), width)
|
|
|
}
|
|
|
@@ -356,3 +362,161 @@ func TestTextWidthsOneQueryPerTable(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+// TestTextWidthOversizeFallsToBlob 采样超过动态列单值上限 (types.MaxStringSize, 16360)
|
|
|
+// 的 TEXT 列不再"保持动态列、写入时被引擎按单块上限拒掉整表", 而是落成 Blob 链式列:
|
|
|
+// 60KB 的值逐字节迁进 VRTX (经引擎 BlobRead 对照), 再逐字节迁回 SQLite —— 复刻
|
|
|
+// DockerManager 实测的失败场景 (sys_logs.param 59566 字节)。
|
|
|
+func TestTextWidthOversizeFallsToBlob(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ srcPath := filepath.Join(dir, "src.sqlite")
|
|
|
+ midPath := filepath.Join(dir, "mid.db")
|
|
|
+ backPath := filepath.Join(dir, "back.sqlite")
|
|
|
+
|
|
|
+ big := strings.Repeat("0123456789abcdef", 3750) // 60000 字节, 超 16360 上限
|
|
|
+
|
|
|
+ db, err := sql.Open("sqlite", srcPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ for _, s := range []string{
|
|
|
+ `CREATE TABLE sys_logs (
|
|
|
+ id INTEGER PRIMARY KEY,
|
|
|
+ action VARCHAR(32),
|
|
|
+ param TEXT
|
|
|
+ )`,
|
|
|
+ `INSERT INTO sys_logs (id, action, param) VALUES (1, 'deploy', ?)`,
|
|
|
+ } {
|
|
|
+ if _, err := db.Exec(s, big); err != nil {
|
|
|
+ t.Fatalf("执行 %s: %v", s, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 同一列再补一行短值 + 一行 NULL: 落 Blob 后小值与 NULL 的读写也必须正常。
|
|
|
+ if _, err := db.Exec(`INSERT INTO sys_logs (id, action, param) VALUES (2, 'ping', 'ok')`); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if _, err := db.Exec(`INSERT INTO sys_logs (id, action, param) VALUES (3, 'noop', NULL)`); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ _ = db.Close()
|
|
|
+
|
|
|
+ src, err := OpenSQLite(srcPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ mid, err := OpenVRTX(midPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ rep, err := Run(src, mid, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("SQLite→VRTX (60KB TEXT 应自动落 Blob 迁过去): %v", err)
|
|
|
+ }
|
|
|
+ _ = src.Close()
|
|
|
+ _ = mid.Close()
|
|
|
+
|
|
|
+ // 列类型: 必须是 Blob 链式列, 而不是保持动态列等写入时失败。
|
|
|
+ f := vrtxColumn(t, midPath, "sys_logs", "param")
|
|
|
+ if f.FieldType != types.FieldType_Blob {
|
|
|
+ t.Fatalf("采样超单值上限的列 param 应落 FieldType_Blob, 实际 %v", f.FieldType)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 内容逐字节对照 (不经 migrate 读端, 直接开引擎文件读链页, 免得"两端同一段代码"
|
|
|
+ // 把不对称的错抹平): 快照 norm 用既有 bytes:%x 形态。
|
|
|
+ vdb, err := core.Open(midPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ vtbl, ok := vdb.FindTable("sys_logs")
|
|
|
+ if !ok {
|
|
|
+ t.Fatal("mid.db 里没有 sys_logs")
|
|
|
+ }
|
|
|
+ got := map[uint64]string{}
|
|
|
+ err = vtbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
|
|
|
+ // NULL / 空 blob 的行内引用读侧一律"读不到" (位图置位 / 空引用跳过),
|
|
|
+ // 缺该列即 NULL; 先按 NULL 预置, 见到值再覆盖。
|
|
|
+ got[pkID] = "NULL"
|
|
|
+ for _, fv := range vals {
|
|
|
+ if util.BytesToString(fv.FieldName) != "param" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ ref, ok := fv.Value.(types.BlobRef)
|
|
|
+ if !ok {
|
|
|
+ t.Fatalf("param 的行内值应是 BlobRef, 实际 %T", fv.Value)
|
|
|
+ }
|
|
|
+ data, err := vtbl.BlobRead(ref, types.CompressNone)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("读回 param 的 blob 链: %v", err)
|
|
|
+ }
|
|
|
+ got[pkID] = fmt.Sprintf("bytes:%x", data)
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ _ = vdb.Close()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ wantBig := fmt.Sprintf("bytes:%x", []byte(big))
|
|
|
+ if got[1] != wantBig {
|
|
|
+ t.Errorf("60KB 值迁过去后内容变了: 长度 got=%d want=%d", len(got[1]), len(wantBig))
|
|
|
+ }
|
|
|
+ if got[2] != fmt.Sprintf("bytes:%x", []byte("ok")) {
|
|
|
+ t.Errorf("短值行 param = %q", got[2])
|
|
|
+ }
|
|
|
+ if got[3] != "NULL" {
|
|
|
+ t.Errorf("NULL 行 param = %q, 想要 NULL", got[3])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 报告: 落 Blob 必须可见。
|
|
|
+ if len(rep.Tables) != 1 {
|
|
|
+ t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
|
|
|
+ }
|
|
|
+ note := rep.Tables[0].Note
|
|
|
+ for _, w := range []string{"落 Blob 链式列", "60000 字节", "param"} {
|
|
|
+ if !strings.Contains(note, w) {
|
|
|
+ t.Errorf("报告 Note 里缺少 %q: %q", w, note)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 反向再迁一轮 (VRTX → SQLite): Blob 列经 migrate 读端解引用、按 BLOB 列建表,
|
|
|
+ // 内容必须逐字节回来。
|
|
|
+ vmid, err := OpenVRTX(midPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ back, err := OpenSQLite(backPath, quiet())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if _, err := Run(vmid, back, quiet()); err != nil {
|
|
|
+ t.Fatalf("VRTX→SQLite: %v", err)
|
|
|
+ }
|
|
|
+ _ = vmid.Close()
|
|
|
+ _ = back.Close()
|
|
|
+
|
|
|
+ bdb, err := sql.Open("sqlite", backPath)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ defer bdb.Close()
|
|
|
+ var backBig, backShort []byte
|
|
|
+ var backNull sql.NullString
|
|
|
+ if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 1`).Scan(&backBig); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if fmt.Sprintf("bytes:%x", backBig) != wantBig {
|
|
|
+ t.Errorf("迁回 SQLite 的 60KB 值逐字节不一致 (got %d 字节)", len(backBig))
|
|
|
+ }
|
|
|
+ if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 2`).Scan(&backShort); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if string(backShort) != "ok" {
|
|
|
+ t.Errorf("短值行迁回 = %q", backShort)
|
|
|
+ }
|
|
|
+ if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 3`).Scan(&backNull); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if backNull.Valid {
|
|
|
+ t.Errorf("NULL 行迁回应是 NULL, 实际 %q", backNull.String)
|
|
|
+ }
|
|
|
+}
|