textwidth_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. package migrate
  2. // TEXT 列采样定宽的用例: 真实 SQLite 源 → VRTX, 验证采样结果落成了哪一档、
  3. // 索引有没有跟着保住, 以及关掉开关后的旧行为。风格与 roundtrip_test.go 一致。
  4. import (
  5. "git.ycl.pub/yl32940/TsDB2/DB/core"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/util"
  8. "database/sql"
  9. "fmt"
  10. "path/filepath"
  11. "strings"
  12. "testing"
  13. _ "modernc.org/sqlite"
  14. )
  15. // vrtxColumn 直接打开 VRTX 文件读一列的引擎定义 —— 与 migrate 的读端无关,
  16. // 宽度码按 Flag 低 5 位读 (与 vrtxFields 落盘的方式一致)。
  17. func vrtxColumn(t *testing.T, path, table, col string) types.FieldInfo {
  18. t.Helper()
  19. db, err := core.Open(path)
  20. if err != nil {
  21. t.Fatalf("打开 %s: %v", path, err)
  22. }
  23. defer db.Close()
  24. tbl, ok := db.FindTable(table)
  25. if !ok {
  26. t.Fatalf("%s 里没有表 %s", path, table)
  27. }
  28. for _, f := range tbl.GetFields() {
  29. if util.BytesToString(f.FieldName) == col {
  30. return f
  31. }
  32. }
  33. t.Fatalf("表 %s 里没有列 %s", table, col)
  34. return types.FieldInfo{}
  35. }
  36. // colWidth 列的行内宽度 (字节): 宽度码在 Flag 低 5 位。
  37. func colWidth(f types.FieldInfo) int {
  38. return int(types.FieldSize(f.FieldType, f.Flag&types.FlagScaleMask))
  39. }
  40. // buildSQLiteTexts 建一张带 TEXT 列的源表:
  41. //
  42. // body TEXT 最大 100 字节 (ASCII), 上面有索引 —— 采到 128 档后索引必须建出来
  43. // memo TEXT 最大 5 字节, 含 NULL —— 采到 32 档
  44. // tag TEXT 20 个汉字 = 60 字节 —— 钉住"采样数的是字节不是字符" (SQLite 的
  45. // length() 对文本数字符, 不 CAST 成 BLOB 会采成 20 → 32 档, 写入即超宽)
  46. func buildSQLiteTexts(t *testing.T, path string) {
  47. t.Helper()
  48. db, err := sql.Open("sqlite", path)
  49. if err != nil {
  50. t.Fatal(err)
  51. }
  52. defer db.Close()
  53. for _, s := range []string{
  54. `CREATE TABLE Docs (
  55. id INTEGER PRIMARY KEY,
  56. body TEXT,
  57. memo TEXT,
  58. tag TEXT
  59. )`,
  60. `CREATE INDEX idx_Docs_body ON Docs(body)`,
  61. } {
  62. if _, err := db.Exec(s); err != nil {
  63. t.Fatalf("执行 %s: %v", s, err)
  64. }
  65. }
  66. rows := []struct {
  67. id int
  68. body string
  69. memo any
  70. tag string
  71. }{
  72. {1, strings.Repeat("x", 100), "short", strings.Repeat("汉", 20)}, // 60 字节
  73. {2, "hi", nil, ""},
  74. }
  75. for _, r := range rows {
  76. if _, err := db.Exec(`INSERT INTO Docs (id, body, memo, tag) VALUES (?, ?, ?, ?)`,
  77. r.id, r.body, r.memo, r.tag); err != nil {
  78. t.Fatalf("插入 %v: %v", r, err)
  79. }
  80. }
  81. }
  82. // TestTextWidthScanToVRTX TEXT 列按源端采样落成定长档: 100 字节 → 128 档、短列 → 32 档、
  83. // 多字节文本按【字节】采样 (60 字节 → 64 档); 源表建在 TEXT 列上的索引因此不再被跳过。
  84. func TestTextWidthScanToVRTX(t *testing.T) {
  85. dir := t.TempDir()
  86. srcPath := filepath.Join(dir, "src.sqlite")
  87. dstPath := filepath.Join(dir, "dst.db")
  88. buildSQLiteTexts(t, srcPath)
  89. src, err := OpenSQLite(srcPath, quiet())
  90. if err != nil {
  91. t.Fatal(err)
  92. }
  93. dst, err := OpenVRTX(dstPath, quiet())
  94. if err != nil {
  95. t.Fatal(err)
  96. }
  97. rep, err := Run(src, dst, quiet())
  98. if err != nil {
  99. t.Fatalf("SQLite→VRTX: %v", err)
  100. }
  101. _ = src.Close()
  102. _ = dst.Close()
  103. if rep.Rows != 2 {
  104. t.Fatalf("应迁 2 行, 实际 %d", rep.Rows)
  105. }
  106. // 三列都应是定长 string, 档位精确等于 2 次幂 (不是向上漂到 224 之类):
  107. // 采样 100 → 128, 采样 5 → 32, 采样 60 (字节!) → 64。
  108. for col, wantWidth := range map[string]int{"body": 128, "memo": 32, "tag": 64} {
  109. f := vrtxColumn(t, dstPath, "Docs", col)
  110. if f.FieldType != types.FieldType_String {
  111. t.Fatalf("列 %s 应是 FieldType_String, 实际 %v (定宽档位没生效)", col, f.FieldType)
  112. }
  113. if got := colWidth(f); got != wantWidth {
  114. t.Errorf("列 %s 的定宽应为 %d 字节, 实际 %d (Flag 低 5 位 = 宽度码 %d)",
  115. col, wantWidth, got, f.Flag&types.FlagScaleMask)
  116. }
  117. if code := f.Flag & types.FlagScaleMask; code != types.WidthCodeFor(wantWidth) {
  118. t.Errorf("列 %s 的宽度码 %d 应是 WidthCodeFor(%d) = %d",
  119. col, code, wantWidth, types.WidthCodeFor(wantWidth))
  120. }
  121. }
  122. // 建在 TEXT 列上的索引: 定宽后必须真的建出来 (旧行为里它因"动态列不能当索引键"被跳过)。
  123. snap := snapshotVRTX(t, dstPath, "Docs")
  124. if got := snap.indexes["idx_Docs_body"]; got != "btree(body)" {
  125. t.Errorf("TEXT 列上的索引应随采样定宽建出来, 实际 %v", snap.indexes)
  126. }
  127. // 报告: 采样结果要写进 Note (定了哪些档), 且不该再有"跳过索引"。
  128. if len(rep.Tables) != 1 {
  129. t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
  130. }
  131. note := rep.Tables[0].Note
  132. for _, want := range []string{"采样定宽", "body 128 字节", "memo 32 字节", "tag 64 字节"} {
  133. if !strings.Contains(note, want) {
  134. t.Errorf("报告 Note 里缺少 %q: %q", want, note)
  135. }
  136. }
  137. if strings.Contains(note, "跳过索引") {
  138. t.Errorf("定宽后不该再跳过索引: %q", note)
  139. }
  140. }
  141. // TestTextWidthScanNullAndOversize 两个边界: 全 NULL 的 TEXT 列采到 0, 落最窄的 32 档;
  142. // 采样超过 2048 字节的列保持动态列 (并写进报告)。
  143. func TestTextWidthScanNullAndOversize(t *testing.T) {
  144. dir := t.TempDir()
  145. srcPath := filepath.Join(dir, "src.sqlite")
  146. dstPath := filepath.Join(dir, "dst.db")
  147. db, err := sql.Open("sqlite", srcPath)
  148. if err != nil {
  149. t.Fatal(err)
  150. }
  151. for _, s := range []string{
  152. `CREATE TABLE Notes (
  153. id INTEGER PRIMARY KEY,
  154. empty TEXT,
  155. big TEXT
  156. )`,
  157. `INSERT INTO Notes (id, empty, big) VALUES (1, NULL, ?)`,
  158. } {
  159. if _, err := db.Exec(s, strings.Repeat("y", 5000)); err != nil {
  160. t.Fatalf("执行 %s: %v", s, err)
  161. }
  162. }
  163. _ = db.Close()
  164. src, err := OpenSQLite(srcPath, quiet())
  165. if err != nil {
  166. t.Fatal(err)
  167. }
  168. dst, err := OpenVRTX(dstPath, quiet())
  169. if err != nil {
  170. t.Fatal(err)
  171. }
  172. rep, err := Run(src, dst, quiet())
  173. if err != nil {
  174. t.Fatalf("SQLite→VRTX: %v", err)
  175. }
  176. _ = src.Close()
  177. _ = dst.Close()
  178. // 全 NULL → 0 → 32 档。
  179. empty := vrtxColumn(t, dstPath, "Notes", "empty")
  180. if empty.FieldType != types.FieldType_String || colWidth(empty) != 32 {
  181. t.Errorf("全 NULL 的 TEXT 列应落 32 档, 实际 %v 宽 %d", empty.FieldType, colWidth(empty))
  182. }
  183. // 5000 字节 > 2048: 保持动态列。
  184. big := vrtxColumn(t, dstPath, "Notes", "big")
  185. if big.FieldType != types.FieldType_Dynamicbyte {
  186. t.Errorf("采样超 2048 字节的列应保持动态列, 实际 %v", big.FieldType)
  187. }
  188. if len(rep.Tables) != 1 {
  189. t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
  190. }
  191. note := rep.Tables[0].Note
  192. for _, want := range []string{"采样定宽", "empty 32 字节", "保持动态", "big"} {
  193. if !strings.Contains(note, want) {
  194. t.Errorf("报告 Note 里缺少 %q: %q", want, note)
  195. }
  196. }
  197. // 数据照常迁过去 (5000 字节的值装得进动态列)。
  198. snap := snapshotVRTX(t, dstPath, "Notes")
  199. if len(snap.rows) != 1 {
  200. t.Errorf("应迁 1 行, 实际 %d", len(snap.rows))
  201. }
  202. }
  203. // TestTextWidthScanDisabled TextWidthScan=false 退回旧行为: TEXT 家族全落动态列,
  204. // 上面的索引照旧被跳过并写进报告。
  205. func TestTextWidthScanDisabled(t *testing.T) {
  206. dir := t.TempDir()
  207. srcPath := filepath.Join(dir, "src.sqlite")
  208. dstPath := filepath.Join(dir, "dst.db")
  209. buildSQLiteTexts(t, srcPath)
  210. src, err := OpenSQLite(srcPath, quiet())
  211. if err != nil {
  212. t.Fatal(err)
  213. }
  214. dst, err := OpenVRTX(dstPath, quiet())
  215. if err != nil {
  216. t.Fatal(err)
  217. }
  218. o := quiet()
  219. o.TextWidthScan = false
  220. rep, err := Run(src, dst, o)
  221. if err != nil {
  222. t.Fatalf("SQLite→VRTX: %v", err)
  223. }
  224. _ = src.Close()
  225. _ = dst.Close()
  226. for _, col := range []string{"body", "memo", "tag"} {
  227. f := vrtxColumn(t, dstPath, "Docs", col)
  228. if f.FieldType != types.FieldType_Dynamicbyte {
  229. t.Errorf("关掉采样后列 %s 应落动态列 (旧行为), 实际 %v", col, f.FieldType)
  230. }
  231. }
  232. snap := snapshotVRTX(t, dstPath, "Docs")
  233. if _, ok := snap.indexes["idx_Docs_body"]; ok {
  234. t.Errorf("动态列上的索引不该建出来: %v", snap.indexes)
  235. }
  236. if len(rep.Tables) != 1 {
  237. t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
  238. }
  239. note := rep.Tables[0].Note
  240. if !strings.Contains(note, "跳过索引") || !strings.Contains(note, "idx_Docs_body") {
  241. t.Errorf("动态列的索引跳过必须写进报告: %q", note)
  242. }
  243. if strings.Contains(note, "采样定宽") {
  244. t.Errorf("关掉采样后不该再有采样定宽的记录: %q", note)
  245. }
  246. }
  247. // TestTextWidthCode 宽度规则本身: 大于等于 maxLen 的最小 2 次幂, 下限 32, 上限 2048;
  248. // 2048 < maxLen ≤ MaxStringSize 保持动态列; maxLen > MaxStringSize 落 Blob 链式列。
  249. // 档位必须精确命中 (WidthCodeWidth 就是 2 次幂本身, 不是 224 之类)。
  250. func TestTextWidthCode(t *testing.T) {
  251. cases := []struct {
  252. maxLen int64
  253. wantWidth int
  254. wantClass textWidthClass
  255. }{
  256. {0, 32, widthToFixed}, // 全 NULL / 空表
  257. {1, 32, widthToFixed}, // 下限 32: 任何非空值至少 32 档
  258. {16, 32, widthToFixed}, // 16 字节也抬到 32 (下限)
  259. {32, 32, widthToFixed},
  260. {33, 64, widthToFixed},
  261. {100, 128, widthToFixed},
  262. {2048, 2048, widthToFixed},
  263. {2049, 0, widthToDynamic}, // 超定宽上限: 保持动态列
  264. {5000, 0, widthToDynamic},
  265. {16360, 0, widthToDynamic}, // 正好顶到单值上限: 动态列仍装得下
  266. {16361, 0, widthToBlob}, // 超单值上限: 落 Blob 链式列
  267. {60000, 0, widthToBlob},
  268. {1 << 30, 0, widthToBlob},
  269. }
  270. for _, tc := range cases {
  271. code, width, class := textWidthCode(tc.maxLen)
  272. if class != tc.wantClass || width != tc.wantWidth {
  273. t.Errorf("textWidthCode(%d) = (码 %d, 宽 %d, 态 %d), 期望 (宽 %d, 态 %d)",
  274. tc.maxLen, code, width, class, tc.wantWidth, tc.wantClass)
  275. continue
  276. }
  277. if class == widthToFixed && int(types.WidthCodeWidth(code)) != width {
  278. t.Errorf("textWidthCode(%d): 宽度码 %d 对应 %d 字节, 与档位 %d 不符 (没有精确命中)",
  279. tc.maxLen, code, types.WidthCodeWidth(code), width)
  280. }
  281. }
  282. }
  283. // TestIsTextFamilyDecl 只有 TEXT 家族参与采样: JSON/BLOB/BINARY 这类同样落动态列的
  284. // 类型必须被排除, 带长度的 CHAR/VARCHAR 走声明容量取档, 也不参与。
  285. func TestIsTextFamilyDecl(t *testing.T) {
  286. for _, decl := range []string{
  287. "TEXT", "text", " Text ", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", "CLOB", "NCLOB",
  288. "TEXT(65535)", // MySQL information_schema 会给 TEXT 报一个类型自身上界
  289. } {
  290. if !isTextFamilyDecl(decl) {
  291. t.Errorf("%q 应判定为 TEXT 家族", decl)
  292. }
  293. }
  294. for _, decl := range []string{
  295. "", "JSON", "JSONB", "BLOB", "TINYBLOB", "LONGBLOB", "BINARY", "VARBINARY",
  296. "VARCHAR(32)", "CHAR(8)", "NVARCHAR(50)", "DATETIME", "DECIMAL(18,2)",
  297. } {
  298. if isTextFamilyDecl(decl) {
  299. t.Errorf("%q 不应判定为 TEXT 家族", decl)
  300. }
  301. }
  302. }
  303. // TestTextWidthsOneQueryPerTable 同表多列一次查询取回 (TextWidthSampler 的约定),
  304. // 且全 NULL 列采到 0 —— 直接钉 SQL.TextWidths, 不经过 Run。
  305. func TestTextWidthsOneQueryPerTable(t *testing.T) {
  306. dir := t.TempDir()
  307. srcPath := filepath.Join(dir, "src.sqlite")
  308. db, err := sql.Open("sqlite", srcPath)
  309. if err != nil {
  310. t.Fatal(err)
  311. }
  312. for _, s := range []string{
  313. `CREATE TABLE T (id INTEGER PRIMARY KEY, a TEXT, b TEXT)`,
  314. `INSERT INTO T (id, a, b) VALUES (1, '12345', NULL)`,
  315. } {
  316. if _, err := db.Exec(s); err != nil {
  317. t.Fatalf("执行 %s: %v", s, err)
  318. }
  319. }
  320. _ = db.Close()
  321. sqlSrc, err := OpenSQLite(srcPath, quiet())
  322. if err != nil {
  323. t.Fatal(err)
  324. }
  325. defer sqlSrc.Close()
  326. var src Source = sqlSrc // 走 Source 接口形态, 钉住运行期可选能力断言
  327. sampler, ok := src.(TextWidthSampler)
  328. if !ok {
  329. t.Fatal("*SQL 应实现 TextWidthSampler")
  330. }
  331. got, err := sampler.TextWidths("T", []string{"a", "b"})
  332. if err != nil {
  333. t.Fatalf("采样: %v", err)
  334. }
  335. if got["a"] != 5 || got["b"] != 0 {
  336. t.Errorf("采样结果应为 a=5, b=0 (全 NULL), 实际 %v", got)
  337. }
  338. }
  339. // TestTextWidthOversizeFallsToBlob 采样超过动态列单值上限 (types.MaxStringSize, 16360)
  340. // 的 TEXT 列不再"保持动态列、写入时被引擎按单块上限拒掉整表", 而是落成 Blob 链式列:
  341. // 60KB 的值逐字节迁进 VRTX (经引擎 BlobRead 对照), 再逐字节迁回 SQLite —— 复刻
  342. // DockerManager 实测的失败场景 (sys_logs.param 59566 字节)。
  343. func TestTextWidthOversizeFallsToBlob(t *testing.T) {
  344. dir := t.TempDir()
  345. srcPath := filepath.Join(dir, "src.sqlite")
  346. midPath := filepath.Join(dir, "mid.db")
  347. backPath := filepath.Join(dir, "back.sqlite")
  348. big := strings.Repeat("0123456789abcdef", 3750) // 60000 字节, 超 16360 上限
  349. db, err := sql.Open("sqlite", srcPath)
  350. if err != nil {
  351. t.Fatal(err)
  352. }
  353. for _, s := range []string{
  354. `CREATE TABLE sys_logs (
  355. id INTEGER PRIMARY KEY,
  356. action VARCHAR(32),
  357. param TEXT
  358. )`,
  359. `INSERT INTO sys_logs (id, action, param) VALUES (1, 'deploy', ?)`,
  360. } {
  361. if _, err := db.Exec(s, big); err != nil {
  362. t.Fatalf("执行 %s: %v", s, err)
  363. }
  364. }
  365. // 同一列再补一行短值 + 一行 NULL: 落 Blob 后小值与 NULL 的读写也必须正常。
  366. if _, err := db.Exec(`INSERT INTO sys_logs (id, action, param) VALUES (2, 'ping', 'ok')`); err != nil {
  367. t.Fatal(err)
  368. }
  369. if _, err := db.Exec(`INSERT INTO sys_logs (id, action, param) VALUES (3, 'noop', NULL)`); err != nil {
  370. t.Fatal(err)
  371. }
  372. _ = db.Close()
  373. src, err := OpenSQLite(srcPath, quiet())
  374. if err != nil {
  375. t.Fatal(err)
  376. }
  377. mid, err := OpenVRTX(midPath, quiet())
  378. if err != nil {
  379. t.Fatal(err)
  380. }
  381. rep, err := Run(src, mid, quiet())
  382. if err != nil {
  383. t.Fatalf("SQLite→VRTX (60KB TEXT 应自动落 Blob 迁过去): %v", err)
  384. }
  385. _ = src.Close()
  386. _ = mid.Close()
  387. // 列类型: 必须是 Blob 链式列, 而不是保持动态列等写入时失败。
  388. f := vrtxColumn(t, midPath, "sys_logs", "param")
  389. if f.FieldType != types.FieldType_Blob {
  390. t.Fatalf("采样超单值上限的列 param 应落 FieldType_Blob, 实际 %v", f.FieldType)
  391. }
  392. // 内容逐字节对照 (不经 migrate 读端, 直接开引擎文件读链页, 免得"两端同一段代码"
  393. // 把不对称的错抹平): 快照 norm 用既有 bytes:%x 形态。
  394. vdb, err := core.Open(midPath)
  395. if err != nil {
  396. t.Fatal(err)
  397. }
  398. vtbl, ok := vdb.FindTable("sys_logs")
  399. if !ok {
  400. t.Fatal("mid.db 里没有 sys_logs")
  401. }
  402. got := map[uint64]string{}
  403. err = vtbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
  404. // NULL / 空 blob 的行内引用读侧一律"读不到" (位图置位 / 空引用跳过),
  405. // 缺该列即 NULL; 先按 NULL 预置, 见到值再覆盖。
  406. got[pkID] = "NULL"
  407. for _, fv := range vals {
  408. if util.BytesToString(fv.FieldName) != "param" {
  409. continue
  410. }
  411. ref, ok := fv.Value.(types.BlobRef)
  412. if !ok {
  413. t.Fatalf("param 的行内值应是 BlobRef, 实际 %T", fv.Value)
  414. }
  415. data, err := vtbl.BlobRead(ref, types.CompressNone)
  416. if err != nil {
  417. t.Fatalf("读回 param 的 blob 链: %v", err)
  418. }
  419. got[pkID] = fmt.Sprintf("bytes:%x", data)
  420. }
  421. return true
  422. })
  423. _ = vdb.Close()
  424. if err != nil {
  425. t.Fatal(err)
  426. }
  427. wantBig := fmt.Sprintf("bytes:%x", []byte(big))
  428. if got[1] != wantBig {
  429. t.Errorf("60KB 值迁过去后内容变了: 长度 got=%d want=%d", len(got[1]), len(wantBig))
  430. }
  431. if got[2] != fmt.Sprintf("bytes:%x", []byte("ok")) {
  432. t.Errorf("短值行 param = %q", got[2])
  433. }
  434. if got[3] != "NULL" {
  435. t.Errorf("NULL 行 param = %q, 想要 NULL", got[3])
  436. }
  437. // 报告: 落 Blob 必须可见。
  438. if len(rep.Tables) != 1 {
  439. t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
  440. }
  441. note := rep.Tables[0].Note
  442. for _, w := range []string{"落 Blob 链式列", "60000 字节", "param"} {
  443. if !strings.Contains(note, w) {
  444. t.Errorf("报告 Note 里缺少 %q: %q", w, note)
  445. }
  446. }
  447. // 反向再迁一轮 (VRTX → SQLite): Blob 列经 migrate 读端解引用、按 BLOB 列建表,
  448. // 内容必须逐字节回来。
  449. vmid, err := OpenVRTX(midPath, quiet())
  450. if err != nil {
  451. t.Fatal(err)
  452. }
  453. back, err := OpenSQLite(backPath, quiet())
  454. if err != nil {
  455. t.Fatal(err)
  456. }
  457. if _, err := Run(vmid, back, quiet()); err != nil {
  458. t.Fatalf("VRTX→SQLite: %v", err)
  459. }
  460. _ = vmid.Close()
  461. _ = back.Close()
  462. bdb, err := sql.Open("sqlite", backPath)
  463. if err != nil {
  464. t.Fatal(err)
  465. }
  466. defer bdb.Close()
  467. var backBig, backShort []byte
  468. var backNull sql.NullString
  469. if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 1`).Scan(&backBig); err != nil {
  470. t.Fatal(err)
  471. }
  472. if fmt.Sprintf("bytes:%x", backBig) != wantBig {
  473. t.Errorf("迁回 SQLite 的 60KB 值逐字节不一致 (got %d 字节)", len(backBig))
  474. }
  475. if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 2`).Scan(&backShort); err != nil {
  476. t.Fatal(err)
  477. }
  478. if string(backShort) != "ok" {
  479. t.Errorf("短值行迁回 = %q", backShort)
  480. }
  481. if err := bdb.QueryRow(`SELECT param FROM sys_logs WHERE id = 3`).Scan(&backNull); err != nil {
  482. t.Fatal(err)
  483. }
  484. if backNull.Valid {
  485. t.Errorf("NULL 行迁回应是 NULL, 实际 %q", backNull.String)
  486. }
  487. }
  488. // TestCharDeclOversizeUpgrade 声明长度的字符列"实际数据超档"的采样升档 —— 复刻
  489. // DockerManager 实测: SQLite 的 sys_ssl.cert 声明 VARCHAR(2048), 但 SQLite 不强制
  490. // 声明长度, 实际值 4803 字节; 旧行为按声明建 2048 定长档, 写入即报"值 4803 字节
  491. // 超过定长列容量 2048"。现在这类列与 TEXT 列合一句采样, 按 textWidthCode 三态升档:
  492. // cert (声明 2048 / 实际 4803) → 动态列; payload (声明 8192 / 实际 9000) → 动态列;
  493. // bulky (声明 8192 / 实际 20000, 超动态列单值上限) → Blob 链式列; code (声明 100 /
  494. // 实际 100) → 定长 128 档不动。数据逐字节往返, 报告写升档 note。
  495. func TestCharDeclOversizeUpgrade(t *testing.T) {
  496. dir := t.TempDir()
  497. srcPath := filepath.Join(dir, "src.sqlite")
  498. midPath := filepath.Join(dir, "mid.db")
  499. backPath := filepath.Join(dir, "back.sqlite")
  500. cert := strings.Repeat("c", 4803)
  501. payload := strings.Repeat("m", 9000)
  502. bulky := strings.Repeat("h", 20000)
  503. db, err := sql.Open("sqlite", srcPath)
  504. if err != nil {
  505. t.Fatal(err)
  506. }
  507. if _, err := db.Exec(`CREATE TABLE sys_ssl (
  508. id INTEGER PRIMARY KEY,
  509. cert VARCHAR(2048),
  510. payload VARCHAR(8192),
  511. bulky VARCHAR(8192),
  512. code VARCHAR(100)
  513. )`); err != nil {
  514. t.Fatal(err)
  515. }
  516. if _, err := db.Exec(`INSERT INTO sys_ssl (id, cert, payload, bulky, code) VALUES (1, ?, ?, ?, ?)`,
  517. cert, payload, bulky, strings.Repeat("k", 100)); err != nil {
  518. t.Fatal(err)
  519. }
  520. // 第二行短值 + NULL: 升档后小值与 NULL 的读写也必须正常。
  521. if _, err := db.Exec(`INSERT INTO sys_ssl (id, cert, payload, bulky, code) VALUES (2, 'ok', NULL, NULL, 'go')`); err != nil {
  522. t.Fatal(err)
  523. }
  524. _ = db.Close()
  525. src, err := OpenSQLite(srcPath, quiet())
  526. if err != nil {
  527. t.Fatal(err)
  528. }
  529. mid, err := OpenVRTX(midPath, quiet())
  530. if err != nil {
  531. t.Fatal(err)
  532. }
  533. rep, err := Run(src, mid, quiet())
  534. if err != nil {
  535. t.Fatalf("SQLite→VRTX (超档字符列应自动升档迁过去): %v", err)
  536. }
  537. _ = src.Close()
  538. _ = mid.Close()
  539. if rep.Rows != 2 {
  540. t.Fatalf("应迁 2 行, 实际 %d", rep.Rows)
  541. }
  542. // 列类型: cert / payload 升 (保持) 动态列, bulky 超单值上限落 Blob, code 定长不动。
  543. if f := vrtxColumn(t, midPath, "sys_ssl", "cert"); f.FieldType != types.FieldType_Dynamicbyte {
  544. t.Errorf("列 cert (声明 2048 / 实际 4803) 应升档动态列, 实际 %v", f.FieldType)
  545. }
  546. if f := vrtxColumn(t, midPath, "sys_ssl", "payload"); f.FieldType != types.FieldType_Dynamicbyte {
  547. t.Errorf("列 payload (声明 8192 / 实际 9000) 应为动态列, 实际 %v", f.FieldType)
  548. }
  549. if f := vrtxColumn(t, midPath, "sys_ssl", "bulky"); f.FieldType != types.FieldType_Blob {
  550. t.Errorf("列 bulky (声明 8192 / 实际 20000, 超单值上限) 应落 Blob 链式列, 实际 %v", f.FieldType)
  551. }
  552. if f := vrtxColumn(t, midPath, "sys_ssl", "code"); f.FieldType != types.FieldType_String || colWidth(f) != 128 {
  553. t.Errorf("列 code (声明 100 / 实际 100) 应保持定长 128 档, 实际 %v 宽 %d", f.FieldType, colWidth(f))
  554. }
  555. // 值逐字节对照 (不经 migrate 读端, 直接开引擎文件读, 免得"两端同一段代码"把
  556. // 不对称的错抹平): 定长/动态列读回 []byte, Blob 列是 BlobRef 要走 BlobRead;
  557. // NULL 列读侧缺项 (位图置位跳过), 缺列即 NULL。
  558. vdb, err := core.Open(midPath)
  559. if err != nil {
  560. t.Fatal(err)
  561. }
  562. vtbl, ok := vdb.FindTable("sys_ssl")
  563. if !ok {
  564. t.Fatal("mid.db 里没有 sys_ssl")
  565. }
  566. got := map[uint64]map[string]string{}
  567. err = vtbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
  568. m := map[string]string{}
  569. for _, fv := range vals {
  570. name := util.BytesToString(fv.FieldName)
  571. switch name {
  572. case "cert", "payload", "bulky", "code":
  573. default:
  574. continue // id (行号列) 等其它列不在逐字节对照范围
  575. }
  576. if ref, isBlob := fv.Value.(types.BlobRef); isBlob {
  577. data, err := vtbl.BlobRead(ref, types.CompressNone)
  578. if err != nil {
  579. t.Fatalf("读回 %s 的 blob 链: %v", name, err)
  580. }
  581. m[name] = fmt.Sprintf("bytes:%x", data)
  582. continue
  583. }
  584. b, ok := fv.Value.([]byte)
  585. if !ok {
  586. t.Fatalf("列 %s 的行内值应是 []byte, 实际 %T", name, fv.Value)
  587. }
  588. m[name] = fmt.Sprintf("bytes:%x", b)
  589. }
  590. got[pkID] = m
  591. return true
  592. })
  593. _ = vdb.Close()
  594. if err != nil {
  595. t.Fatal(err)
  596. }
  597. wantRow1 := map[string]string{
  598. "cert": fmt.Sprintf("bytes:%x", []byte(cert)),
  599. "payload": fmt.Sprintf("bytes:%x", []byte(payload)),
  600. "bulky": fmt.Sprintf("bytes:%x", []byte(bulky)),
  601. "code": fmt.Sprintf("bytes:%x", []byte(strings.Repeat("k", 100))),
  602. }
  603. for col, want := range wantRow1 {
  604. if got[1][col] != want {
  605. t.Errorf("行 1 列 %s 逐字节不一致 (got %d 字节)", col, len(got[1][col]))
  606. }
  607. }
  608. if got[2]["cert"] != fmt.Sprintf("bytes:%x", []byte("ok")) ||
  609. got[2]["code"] != fmt.Sprintf("bytes:%x", []byte("go")) {
  610. t.Errorf("行 2 短值行 = %v", got[2])
  611. }
  612. if _, ok := got[2]["payload"]; ok {
  613. t.Errorf("行 2 的 payload 应是 NULL, 实际有值")
  614. }
  615. if _, ok := got[2]["bulky"]; ok {
  616. t.Errorf("行 2 的 bulky 应是 NULL, 实际有值")
  617. }
  618. // 报告: 升档必须可见, 且只有 cert / bulky 升档 (payload 没超单值上限,
  619. // code 没超档宽, 都不该出现在升档名单里)。
  620. if len(rep.Tables) != 1 {
  621. t.Fatalf("应有 1 张表的报告, 实际 %d", len(rep.Tables))
  622. }
  623. note := rep.Tables[0].Note
  624. for _, want := range []string{
  625. "升档",
  626. "列 cert 声明 2048 字节但实际最大 4803 字节, 升档 动态列",
  627. "列 bulky 声明 8192 字节但实际最大 20000 字节, 升档 Blob 链式列",
  628. } {
  629. if !strings.Contains(note, want) {
  630. t.Errorf("报告 Note 里缺少 %q: %q", want, note)
  631. }
  632. }
  633. if strings.Contains(note, "payload 声明") {
  634. t.Errorf("payload (9000 ≤ 单值上限) 不该升档: %q", note)
  635. }
  636. if strings.Contains(note, "code 声明") {
  637. t.Errorf("code (数据没超档) 不该升档: %q", note)
  638. }
  639. // 反向再迁一轮 (VRTX → SQLite): 升档后的列经 migrate 读端解出来建表,
  640. // 内容必须逐字节回来。
  641. vmid, err := OpenVRTX(midPath, quiet())
  642. if err != nil {
  643. t.Fatal(err)
  644. }
  645. back, err := OpenSQLite(backPath, quiet())
  646. if err != nil {
  647. t.Fatal(err)
  648. }
  649. if _, err := Run(vmid, back, quiet()); err != nil {
  650. t.Fatalf("VRTX→SQLite: %v", err)
  651. }
  652. _ = vmid.Close()
  653. _ = back.Close()
  654. bdb, err := sql.Open("sqlite", backPath)
  655. if err != nil {
  656. t.Fatal(err)
  657. }
  658. defer bdb.Close()
  659. var backCert, backPayload, backBulky, backCode []byte
  660. if err := bdb.QueryRow(`SELECT cert, payload, bulky, code FROM sys_ssl WHERE id = 1`).
  661. Scan(&backCert, &backPayload, &backBulky, &backCode); err != nil {
  662. t.Fatal(err)
  663. }
  664. if fmt.Sprintf("bytes:%x", backCert) != wantRow1["cert"] {
  665. t.Errorf("迁回 SQLite 的 cert 逐字节不一致 (got %d 字节)", len(backCert))
  666. }
  667. if fmt.Sprintf("bytes:%x", backPayload) != wantRow1["payload"] {
  668. t.Errorf("迁回 SQLite 的 payload 逐字节不一致 (got %d 字节)", len(backPayload))
  669. }
  670. if fmt.Sprintf("bytes:%x", backBulky) != wantRow1["bulky"] {
  671. t.Errorf("迁回 SQLite 的 bulky 逐字节不一致 (got %d 字节)", len(backBulky))
  672. }
  673. if fmt.Sprintf("bytes:%x", backCode) != wantRow1["code"] {
  674. t.Errorf("迁回 SQLite 的 code 逐字节不一致 (got %d 字节)", len(backCode))
  675. }
  676. var backShort string
  677. var backNull1, backNull2 sql.NullString
  678. if err := bdb.QueryRow(`SELECT cert, payload, bulky FROM sys_ssl WHERE id = 2`).
  679. Scan(&backShort, &backNull1, &backNull2); err != nil {
  680. t.Fatal(err)
  681. }
  682. if backShort != "ok" {
  683. t.Errorf("短值行迁回 cert = %q", backShort)
  684. }
  685. if backNull1.Valid || backNull2.Valid {
  686. t.Errorf("NULL 行迁回应是 NULL, 实际 %q / %q", backNull1.String, backNull2.String)
  687. }
  688. }
  689. // TestDeclaredCharWidth 只有带长度的 CHAR/VARCHAR 家族声明返回声明字节数 —— 这是
  690. // 字符列采样升档那一路的入场券; TEXT 家族 / 无长度声明 / 非字符类型都返回 0,
  691. // 不采样。名单与 inferSQLType 的字符列分支同源。
  692. func TestDeclaredCharWidth(t *testing.T) {
  693. for decl, want := range map[string]int{
  694. "VARCHAR(2048)": 2048, // DockerManager 实测的 sys_ssl.cert
  695. "varchar(100)": 100,
  696. "CHAR(8)": 8,
  697. "CHARACTER(16)": 16,
  698. "NCHAR(4)": 4,
  699. "NVARCHAR(50)": 50,
  700. "VARCHAR2(32)": 32,
  701. " VARCHAR(64) ": 64,
  702. "VARCHAR(8192)": 8192, // 声明超 2048 落成动态列, 仍要采样 (超单值上限落 Blob)
  703. } {
  704. if got := declaredCharWidth(decl); got != want {
  705. t.Errorf("declaredCharWidth(%q) = %d, 期望 %d", decl, got, want)
  706. }
  707. }
  708. for _, decl := range []string{
  709. "", "TEXT", "TINYTEXT", "CLOB", "BLOB", "JSON", "BINARY", "VARBINARY",
  710. "VARCHAR", "CHAR", "NCHAR", "DECIMAL(18,2)", "DATETIME", "INT",
  711. } {
  712. if got := declaredCharWidth(decl); got != 0 {
  713. t.Errorf("declaredCharWidth(%q) = %d, 期望 0 (不参与字符列采样)", decl, got)
  714. }
  715. }
  716. }