roundtrip_test.go 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244
  1. package migrate
  2. import (
  3. "TsDB2/DB/core"
  4. "TsDB2/DB/core/page"
  5. "TsDB2/DB/core/types"
  6. "TsDB2/DB/core/util"
  7. "bytes"
  8. "database/sql"
  9. "errors"
  10. "fmt"
  11. "math"
  12. "path/filepath"
  13. "strconv"
  14. "strings"
  15. "testing"
  16. _ "modernc.org/sqlite"
  17. )
  18. // quiet 一份不往日志里写东西的选项, 免得测试输出被进度刷屏。
  19. func quiet() *Options {
  20. o := DefaultOptions()
  21. o.Log = func(string, ...any) {}
  22. return o
  23. }
  24. // ============================ 样本库 ============================
  25. type sampleRow struct {
  26. pk uint64
  27. vals map[string]any
  28. }
  29. // buildVRTXUsers 用 core 原生接口建一张覆盖各宽度档位的表:
  30. // 8/1/2/4/32 字节 + 浮点 + 变长, 外加一个 B+ 树索引与一个 Hash 索引。
  31. func buildVRTXUsers(t *testing.T, path string) {
  32. t.Helper()
  33. db, err := core.Open(path)
  34. if err != nil {
  35. t.Fatalf("建样本 VRTX 库: %v", err)
  36. }
  37. defer db.Close()
  38. fields := []types.FieldInfo{
  39. {FieldType: types.FieldType_8byte, FieldName: []byte("_id")},
  40. {FieldType: types.FieldType_32byte, FieldName: []byte("Name")},
  41. {FieldType: types.FieldType_2byte, FieldName: []byte("Age")},
  42. {FieldType: types.FieldType_Float64, FieldName: []byte("Score")},
  43. {FieldType: types.FieldType_Dynamicbyte, FieldName: []byte("Blob")},
  44. {FieldType: types.FieldType_1byte, FieldName: []byte("Flag")},
  45. {FieldType: types.FieldType_4byte, FieldName: []byte("Count")},
  46. {FieldType: types.FieldType_8byte, FieldName: []byte("Big")},
  47. }
  48. tbl, err := db.CreateTable("Users", fields)
  49. if err != nil {
  50. t.Fatalf("建表: %v", err)
  51. }
  52. for _, spec := range []struct {
  53. name string
  54. typ page.IndexType
  55. col byte
  56. }{
  57. {"pk_Users", page.IndexTypePrimary, 0},
  58. {"idx_Users_Name", page.IndexTypeHash, 1},
  59. {"idx_Users_Age", page.IndexTypeBTree, 2},
  60. } {
  61. if _, err := tbl.CreateIndex(spec.name, spec.typ, []byte{spec.col}); err != nil {
  62. t.Fatalf("建索引 %s: %v", spec.name, err)
  63. }
  64. }
  65. big := bytes.Repeat([]byte{0xAB}, 300)
  66. rows := []sampleRow{
  67. {1, map[string]any{
  68. "_id": uint64(1), "Name": []byte("Alice"), "Age": int16(30), "Score": 1.5,
  69. "Blob": []byte("hello"), "Flag": uint8(7), "Count": int32(100), "Big": uint64(42),
  70. }},
  71. {2, map[string]any{
  72. // 8byte 的负位模式与 255 的有符号/无符号边界
  73. "_id": uint64(2), "Name": []byte("Bob"), "Age": int16(-5), "Score": -2.25,
  74. "Blob": []byte{0, 1, 2, 255}, "Flag": uint8(255), "Count": int32(-1),
  75. "Big": uint64(1) << 63,
  76. }},
  77. {3, map[string]any{
  78. // 空字符串 / 空变长列 / 零值
  79. "_id": uint64(3), "Name": []byte(""), "Age": int16(32767), "Score": float64(0),
  80. "Blob": []byte{}, "Flag": uint8(0), "Count": int32(math.MaxInt32), "Big": uint64(0),
  81. }},
  82. {4, map[string]any{
  83. // 定长串打满 + 多字节 UTF-8 + 变长二进制 + uint64 全 1
  84. "_id": uint64(4), "Name": []byte(strings.Repeat("x", 32)), "Age": int16(-32768),
  85. "Score": 1e-9, "Blob": big, "Flag": uint8(1), "Count": int32(math.MinInt32),
  86. "Big": uint64(math.MaxUint64),
  87. }},
  88. {5, map[string]any{
  89. "_id": uint64(5), "Name": []byte("王小明"), "Age": int16(1),
  90. "Score": 3.141592653589793, "Blob": []byte("中文内容"), "Flag": uint8(128),
  91. "Count": int32(0), "Big": uint64(7),
  92. }},
  93. }
  94. writeVRTXRows(t, tbl, fields, rows)
  95. }
  96. // buildVRTXMetrics 建一张时序表: 行存储索引是 Time 类型, 键是 _ts。
  97. func buildVRTXMetrics(t *testing.T, path string) {
  98. t.Helper()
  99. db, err := core.Open(path)
  100. if err != nil {
  101. t.Fatalf("建时序样本库: %v", err)
  102. }
  103. defer db.Close()
  104. fields := []types.FieldInfo{
  105. {FieldType: types.FieldType_8byte, FieldName: []byte("_ts")},
  106. {FieldType: types.FieldType_Float64, FieldName: []byte("Value")},
  107. {FieldType: types.FieldType_16byte, FieldName: []byte("Device")},
  108. }
  109. tbl, err := db.CreateTable("Metrics", fields)
  110. if err != nil {
  111. t.Fatalf("建表: %v", err)
  112. }
  113. if _, err := tbl.CreateIndex("pk_Metrics", page.IndexTypeTime, []byte{0}); err != nil {
  114. t.Fatalf("建时序索引: %v", err)
  115. }
  116. // 时间必须递增: 时序索引按时间段分段, 乱序插入会被拒。
  117. writeVRTXRows(t, tbl, fields, []sampleRow{
  118. {1, map[string]any{"_ts": uint64(1000), "Value": 1.5, "Device": []byte("s1")}},
  119. {2, map[string]any{"_ts": uint64(2000), "Value": 2.5, "Device": []byte("s2")}},
  120. {3, map[string]any{"_ts": uint64(3000), "Value": 3.5, "Device": []byte("s1")}},
  121. })
  122. }
  123. func writeVRTXRows(t *testing.T, tbl *core.Table, fields []types.FieldInfo, rows []sampleRow) {
  124. t.Helper()
  125. for _, r := range rows {
  126. vals := make([]types.FieldValue, len(fields))
  127. for i, f := range fields {
  128. vals[i] = types.FieldValue{FieldName: f.FieldName, Value: r.vals[util.BytesToString(f.FieldName)]}
  129. }
  130. if _, err := tbl.InsertRowWithID(r.pk, vals); err != nil {
  131. t.Fatalf("插入 pk=%d: %v", r.pk, err)
  132. }
  133. }
  134. }
  135. // ============================ 读回真实文件 ============================
  136. // vrtxSnapshot 直接把 VRTX 文件读成 (列结构, 索引, 数据) 三份快照 —— 不走 migrate
  137. // 自己的读端, 免得"读写两端用同一段代码"把不对称的错悄悄抹平。
  138. type vrtxSnapshot struct {
  139. columns []string // "Name:32byte"
  140. indexes map[string]string
  141. rows map[uint64]map[string]any
  142. }
  143. func snapshotVRTX(t *testing.T, path, table string) vrtxSnapshot {
  144. t.Helper()
  145. db, err := core.Open(path)
  146. if err != nil {
  147. t.Fatalf("打开 %s: %v", path, err)
  148. }
  149. defer db.Close()
  150. tbl, ok := db.FindTable(table)
  151. if !ok {
  152. t.Fatalf("%s 里没有表 %s", path, table)
  153. }
  154. snap := vrtxSnapshot{indexes: map[string]string{}, rows: map[uint64]map[string]any{}}
  155. fields := tbl.GetFields()
  156. for _, f := range fields {
  157. if util.IsHighBitSet(byte(f.FieldType)) {
  158. continue
  159. }
  160. // NULL / 唯一声明也进快照: 它们和类型一样是结构的一部分, 往返丢了就是
  161. // 静默改结构 (列从"可空"变"非空"、约束从"唯一"变"普通")。
  162. snap.columns = append(snap.columns, fmt.Sprintf("%s:%s%s",
  163. util.BytesToString(f.FieldName), TypeName(f.FieldType), flagMarks(f.Flag)))
  164. }
  165. for _, m := range tbl.IndexMeta().GetIndexes() {
  166. var cols []string
  167. for _, fid := range m.Fields() {
  168. cols = append(cols, util.BytesToString(fields[fid].FieldName))
  169. }
  170. mark := ""
  171. if m.Unique() {
  172. mark = " unique"
  173. }
  174. snap.indexes[m.Name()] = fmt.Sprintf("%s(%s)%s",
  175. IndexTypeName(m.IndexType), strings.Join(cols, ","), mark)
  176. }
  177. if err := tbl.Query().ScanWithPKID(func(pkID uint64, vals []types.FieldValue) bool {
  178. m := make(map[string]any, len(vals))
  179. for _, fv := range vals {
  180. m[util.BytesToString(fv.FieldName)] = fv.Value
  181. }
  182. snap.rows[pkID] = m
  183. return true
  184. }); err != nil {
  185. t.Fatalf("扫描 %s.%s: %v", path, table, err)
  186. }
  187. return snap
  188. }
  189. func norm(v any) string {
  190. if b, ok := v.([]byte); ok {
  191. return fmt.Sprintf("bytes:%x", b)
  192. }
  193. if f, ok := v.(float64); ok {
  194. return "f64:" + strconv.FormatFloat(f, 'x', -1, 64)
  195. }
  196. return fmt.Sprintf("%T:%v", v, v)
  197. }
  198. // ============================ 用例 ============================
  199. // TestVRTXSQLiteRoundTrip VRTX → SQLite → VRTX: 结构、索引类型、主键与每一行的
  200. // 每个字段都必须原样回来。
  201. func TestVRTXSQLiteRoundTrip(t *testing.T) {
  202. dir := t.TempDir()
  203. srcPath := filepath.Join(dir, "src.db")
  204. midPath := filepath.Join(dir, "mid.sqlite")
  205. backPath := filepath.Join(dir, "back.db")
  206. buildVRTXUsers(t, srcPath)
  207. // 先取一份源快照, 后面拿它对照
  208. want := snapshotVRTX(t, srcPath, "Users")
  209. // VRTX → SQLite
  210. v1, err := OpenVRTX(srcPath, quiet())
  211. if err != nil {
  212. t.Fatal(err)
  213. }
  214. s1, err := OpenSQLite(midPath, quiet())
  215. if err != nil {
  216. t.Fatal(err)
  217. }
  218. rep, err := Run(v1, s1, quiet())
  219. if err != nil {
  220. t.Fatalf("VRTX→SQLite: %v", err)
  221. }
  222. if rep.Rows != 5 {
  223. t.Fatalf("VRTX→SQLite 应迁 5 行, 实际 %d", rep.Rows)
  224. }
  225. _ = v1.Close()
  226. _ = s1.Close()
  227. // SQLite → VRTX
  228. s2, err := OpenSQLite(midPath, quiet())
  229. if err != nil {
  230. t.Fatal(err)
  231. }
  232. v2, err := OpenVRTX(backPath, quiet())
  233. if err != nil {
  234. t.Fatal(err)
  235. }
  236. if _, err := Run(s2, v2, quiet()); err != nil {
  237. t.Fatalf("SQLite→VRTX: %v", err)
  238. }
  239. _ = s2.Close()
  240. _ = v2.Close()
  241. got := snapshotVRTX(t, backPath, "Users")
  242. if strings.Join(got.columns, "|") != strings.Join(want.columns, "|") {
  243. t.Errorf("列结构不一致\n 源: %v\n 回: %v", want.columns, got.columns)
  244. }
  245. for name, def := range want.indexes {
  246. if got.indexes[name] != def {
  247. t.Errorf("索引 %s 不一致: 源 %q, 回 %q", name, def, got.indexes[name])
  248. }
  249. }
  250. if len(got.indexes) != len(want.indexes) {
  251. t.Errorf("索引数量不一致: 源 %v, 回 %v", want.indexes, got.indexes)
  252. }
  253. if len(got.rows) != len(want.rows) {
  254. t.Fatalf("行数不一致: 源 %d, 回 %d", len(want.rows), len(got.rows))
  255. }
  256. for pk, wantRow := range want.rows {
  257. gotRow, ok := got.rows[pk]
  258. if !ok {
  259. t.Errorf("pk=%d 的行丢了", pk)
  260. continue
  261. }
  262. for col, wantVal := range wantRow {
  263. if norm(gotRow[col]) != norm(wantVal) {
  264. t.Errorf("pk=%d 列 %s 不一致: 源 %s, 回 %s",
  265. pk, col, norm(wantVal), norm(gotRow[col]))
  266. }
  267. }
  268. }
  269. }
  270. // TestVRTXTimeSeriesRoundTrip 时序表: 行存储索引是 Time 类型, 键是 _ts 而不是行号。
  271. // 这种表的行号由索引内部维护 (且首个分段的 pkID 并不可靠), 所以往返时按 _ts 对照;
  272. // 要求是 Time 索引仍建在 _ts 上, 且每个时间点的数据一字不差。
  273. func TestVRTXTimeSeriesRoundTrip(t *testing.T) {
  274. dir := t.TempDir()
  275. srcPath := filepath.Join(dir, "src.db")
  276. midPath := filepath.Join(dir, "mid.sqlite")
  277. backPath := filepath.Join(dir, "back.db")
  278. buildVRTXMetrics(t, srcPath)
  279. want := snapshotVRTX(t, srcPath, "Metrics")
  280. v1, err := OpenVRTX(srcPath, quiet())
  281. if err != nil {
  282. t.Fatal(err)
  283. }
  284. s1, err := OpenSQLite(midPath, quiet())
  285. if err != nil {
  286. t.Fatal(err)
  287. }
  288. if _, err := Run(v1, s1, quiet()); err != nil {
  289. t.Fatalf("VRTX→SQLite: %v", err)
  290. }
  291. _ = v1.Close()
  292. _ = s1.Close()
  293. // 中间库不该冒出 _id 之类的替代主键: 时序表在 SQL 侧本来就没有整数主键
  294. mid, err := OpenSQLite(midPath, quiet())
  295. if err != nil {
  296. t.Fatal(err)
  297. }
  298. midSchema, err := mid.Schema("Metrics")
  299. if err != nil {
  300. t.Fatal(err)
  301. }
  302. if midSchema.PKColumn != "" {
  303. t.Errorf("时序表导出到 SQL 后不该有主键列, 实际 %q", midSchema.PKColumn)
  304. }
  305. _ = mid.Close()
  306. s2, err := OpenSQLite(midPath, quiet())
  307. if err != nil {
  308. t.Fatal(err)
  309. }
  310. v2, err := OpenVRTX(backPath, quiet())
  311. if err != nil {
  312. t.Fatal(err)
  313. }
  314. if _, err := Run(s2, v2, quiet()); err != nil {
  315. t.Fatalf("SQLite→VRTX: %v", err)
  316. }
  317. _ = s2.Close()
  318. _ = v2.Close()
  319. got := snapshotVRTX(t, backPath, "Metrics")
  320. if got.indexes["pk_Metrics"] != "time(_ts)" {
  321. t.Errorf("时序索引没回来: %v", got.indexes)
  322. }
  323. if len(got.rows) != len(want.rows) {
  324. t.Fatalf("行数不一致: 源 %d, 回 %d", len(want.rows), len(got.rows))
  325. }
  326. // 行号不可比, 按 _ts 配对
  327. byTS := func(rows map[uint64]map[string]any) map[string]map[string]any {
  328. out := make(map[string]map[string]any, len(rows))
  329. for _, r := range rows {
  330. out[norm(r["_ts"])] = r
  331. }
  332. return out
  333. }
  334. wantTS, gotTS := byTS(want.rows), byTS(got.rows)
  335. for ts, wantRow := range wantTS {
  336. gotRow, ok := gotTS[ts]
  337. if !ok {
  338. t.Errorf("_ts=%s 的点丢了", ts)
  339. continue
  340. }
  341. for _, col := range []string{"_ts", "Value", "Device"} {
  342. if norm(gotRow[col]) != norm(wantRow[col]) {
  343. t.Errorf("_ts=%s 列 %s 不一致: 源 %s, 回 %s",
  344. ts, col, norm(wantRow[col]), norm(gotRow[col]))
  345. }
  346. }
  347. }
  348. }
  349. // TestSQLiteOriginToVRTX SQLite 里手工建的表 (无 __vrtx_meta) → VRTX → SQLite:
  350. // 类型按 SQL 声明就近取档, 主键与二级索引都要保住。
  351. func TestSQLiteOriginToVRTX(t *testing.T) {
  352. dir := t.TempDir()
  353. srcPath := filepath.Join(dir, "src.sqlite")
  354. vrtxPath := filepath.Join(dir, "mid.db")
  355. backPath := filepath.Join(dir, "back.sqlite")
  356. createSQLiteSource(t, srcPath)
  357. want := readSQLiteRows(t, srcPath)
  358. s1, err := OpenSQLite(srcPath, quiet())
  359. if err != nil {
  360. t.Fatal(err)
  361. }
  362. v1, err := OpenVRTX(vrtxPath, quiet())
  363. if err != nil {
  364. t.Fatal(err)
  365. }
  366. if _, err := Run(s1, v1, quiet()); err != nil {
  367. t.Fatalf("SQLite→VRTX: %v", err)
  368. }
  369. _ = s1.Close()
  370. _ = v1.Close()
  371. // VRTX 侧应该拿到 8 字节主键 + 两个索引 (primary + btree)
  372. vSnap := snapshotVRTX(t, vrtxPath, "Items")
  373. if vSnap.indexes["pk_Items"] != "primary(id)" {
  374. t.Errorf("VRTX 主键索引不对: %v", vSnap.indexes)
  375. }
  376. if vSnap.indexes["idx_Items_name"] != "btree(name)" {
  377. t.Errorf("VRTX 二级索引不对: %v", vSnap.indexes)
  378. }
  379. v2, err := OpenVRTX(vrtxPath, quiet())
  380. if err != nil {
  381. t.Fatal(err)
  382. }
  383. s2, err := OpenSQLite(backPath, quiet())
  384. if err != nil {
  385. t.Fatal(err)
  386. }
  387. if _, err := Run(v2, s2, quiet()); err != nil {
  388. t.Fatalf("VRTX→SQLite: %v", err)
  389. }
  390. _ = v2.Close()
  391. _ = s2.Close()
  392. got := readSQLiteRows(t, backPath)
  393. if len(got) != len(want) {
  394. t.Fatalf("行数不一致: 源 %d, 回 %d", len(want), len(got))
  395. }
  396. for id, wantRow := range want {
  397. gotRow, ok := got[id]
  398. if !ok {
  399. t.Errorf("id=%s 的行丢了", id)
  400. continue
  401. }
  402. if norm(gotRow["name"]) != norm(wantRow["name"]) ||
  403. norm(gotRow["qty"]) != norm(wantRow["qty"]) ||
  404. norm(gotRow["price"]) != norm(wantRow["price"]) ||
  405. norm(gotRow["memo"]) != norm(wantRow["memo"]) {
  406. t.Errorf("id=%s 不一致: 源 %v, 回 %v", id, wantRow, gotRow)
  407. }
  408. }
  409. }
  410. // TestSQLiteTimeColumnsToVRTX 源库的时间列迁到 VRTX。
  411. //
  412. // 三条硬要求, 每一条都对应一个真实踩过的坑:
  413. //
  414. // 1. 落 8byte —— VRTX 没有日期类型, 时间就是 8 字节整数 (时序键 _ts 同款),
  415. // 只有这样时间列才能比较大小、做范围查询;
  416. // 2. 建在时间列上的索引必须真的建出来。时间列以前落在 dynamic 上, 引擎会拒
  417. // "动态列不能作为索引键", 整张表跟着废掉 (resource_online.idx_ctime 那个报错);
  418. // 3. 值要是时间戳本身, 且按 UTC 解释 —— 同一份数据在不同时区的机器上必须迁出
  419. // 同一个数。
  420. func TestSQLiteTimeColumnsToVRTX(t *testing.T) {
  421. dir := t.TempDir()
  422. srcPath := filepath.Join(dir, "src.sqlite")
  423. midPath := filepath.Join(dir, "mid.db")
  424. db, err := sql.Open("sqlite", srcPath)
  425. if err != nil {
  426. t.Fatal(err)
  427. }
  428. for _, s := range []string{
  429. `CREATE TABLE Events (
  430. id INTEGER PRIMARY KEY,
  431. ctime DATETIME NOT NULL,
  432. day DATE NOT NULL
  433. )`,
  434. `CREATE INDEX idx_Events_ctime ON Events(ctime)`,
  435. `INSERT INTO Events (id, ctime, day) VALUES (1, '2024-03-05 12:34:56', '2024-03-05')`,
  436. } {
  437. if _, err := db.Exec(s); err != nil {
  438. t.Fatalf("执行 %s: %v", s, err)
  439. }
  440. }
  441. _ = db.Close()
  442. s1, err := OpenSQLite(srcPath, quiet())
  443. if err != nil {
  444. t.Fatal(err)
  445. }
  446. v1, err := OpenVRTX(midPath, quiet())
  447. if err != nil {
  448. t.Fatal(err)
  449. }
  450. rep, err := Run(s1, v1, quiet())
  451. if err != nil {
  452. t.Fatalf("SQLite→VRTX: %v", err)
  453. }
  454. if rep.Rows != 1 {
  455. t.Fatalf("应迁 1 行, 实际 %d", rep.Rows)
  456. }
  457. _ = s1.Close()
  458. _ = v1.Close()
  459. snap := snapshotVRTX(t, midPath, "Events")
  460. // 1. 时间列是 8byte, 不是 dynamic
  461. cols := strings.Join(snap.columns, ",")
  462. for _, want := range []string{"ctime:8byte", "day:8byte"} {
  463. if !strings.Contains(cols, want) {
  464. t.Errorf("时间列应当落成 %s, 实际列结构 %v", want, snap.columns)
  465. }
  466. }
  467. // 2. 索引建出来了 (以前这里整张表都废掉)
  468. if got := snap.indexes["idx_Events_ctime"]; got != "btree(ctime)" {
  469. t.Errorf("建在时间列上的索引没建出来, 实际 %v", snap.indexes)
  470. }
  471. // 3. 值是 UTC 时间戳本身。数字写死是为了不让实现自证自己:
  472. // 2024-03-05 12:34:56 UTC = 1709642096 秒; 2024-03-05 = 1709596800 秒。
  473. for col, wantNS := range map[string]uint64{
  474. "ctime": 1709642096000000000,
  475. "day": 1709596800000000000,
  476. } {
  477. got := snap.rows[1][col]
  478. if s := fmt.Sprintf("%v", got); s != strconv.FormatUint(wantNS, 10) {
  479. t.Errorf("%s 的 Unix 纳秒应为 %d, 实际 %v (%T)", col, wantNS, got, got)
  480. }
  481. }
  482. }
  483. // TestSkippedIndexDoesNotKillTable 目标端建不出来的索引应当只跳它自己。
  484. //
  485. // dynamic 列 (长文本) 不能作为索引键是引擎的硬限制, 但源库里这是条合法索引。
  486. // 迁一张"数据都在, 只是少个索引"的表, 比让整张表陪葬合理 —— 索引可以事后补,
  487. // 数据没迁才是真的丢。跳过的索引要写进报告, 不能静默。
  488. func TestSkippedIndexDoesNotKillTable(t *testing.T) {
  489. dir := t.TempDir()
  490. srcPath := filepath.Join(dir, "src.sqlite")
  491. midPath := filepath.Join(dir, "mid.db")
  492. db, err := sql.Open("sqlite", srcPath)
  493. if err != nil {
  494. t.Fatal(err)
  495. }
  496. for _, s := range []string{
  497. `CREATE TABLE Blobs (
  498. id INTEGER PRIMARY KEY,
  499. body BLOB NOT NULL
  500. )`,
  501. `CREATE INDEX idx_Blobs_body ON Blobs(body)`,
  502. `INSERT INTO Blobs (id, body) VALUES (1, 'hello')`,
  503. } {
  504. if _, err := db.Exec(s); err != nil {
  505. t.Fatalf("执行 %s: %v", s, err)
  506. }
  507. }
  508. _ = db.Close()
  509. s1, err := OpenSQLite(srcPath, quiet())
  510. if err != nil {
  511. t.Fatal(err)
  512. }
  513. v1, err := OpenVRTX(midPath, quiet())
  514. if err != nil {
  515. t.Fatal(err)
  516. }
  517. rep, err := Run(s1, v1, quiet())
  518. if err != nil {
  519. t.Fatalf("跳个索引不该让迁移失败: %v", err)
  520. }
  521. _ = s1.Close()
  522. _ = v1.Close()
  523. if rep.Rows != 1 {
  524. t.Fatalf("表的数据应当照迁, 实际 %d 行", rep.Rows)
  525. }
  526. if len(rep.Tables) != 1 || !strings.Contains(rep.Tables[0].Note, "跳过索引") {
  527. t.Fatalf("跳过的索引必须写进报告, 实际: %+v", rep.Tables)
  528. }
  529. snap := snapshotVRTX(t, midPath, "Blobs")
  530. if _, ok := snap.indexes["idx_Blobs_body"]; ok {
  531. t.Errorf("dynamic 列的索引不该建出来: %v", snap.indexes)
  532. }
  533. if len(snap.rows) != 1 {
  534. t.Errorf("数据应当照常迁过去, 实际 %d 行", len(snap.rows))
  535. }
  536. }
  537. // TestRunRejectsSelfMigration 源和目标指向同一个库必须拦住 —— 否则边读边写会把
  538. // 表毁掉。
  539. func TestRunRejectsSelfMigration(t *testing.T) {
  540. dir := t.TempDir()
  541. path := filepath.Join(dir, "same.db")
  542. buildVRTXUsers(t, path)
  543. a, err := OpenVRTX(path, quiet())
  544. if err != nil {
  545. t.Fatal(err)
  546. }
  547. defer a.Close()
  548. b, err := OpenVRTX(path, quiet())
  549. if err != nil {
  550. t.Fatal(err)
  551. }
  552. defer b.Close()
  553. if _, err := Run(a, b, quiet()); err == nil {
  554. t.Fatal("同一个库的自我迁移应当报错")
  555. }
  556. }
  557. // TestRunLimit 只搬前 N 行。
  558. func TestRunLimit(t *testing.T) {
  559. dir := t.TempDir()
  560. srcPath := filepath.Join(dir, "src.db")
  561. midPath := filepath.Join(dir, "mid.sqlite")
  562. buildVRTXUsers(t, srcPath)
  563. o := quiet()
  564. o.Limit = 2
  565. v1, err := OpenVRTX(srcPath, quiet())
  566. if err != nil {
  567. t.Fatal(err)
  568. }
  569. defer v1.Close()
  570. s1, err := OpenSQLite(midPath, quiet())
  571. if err != nil {
  572. t.Fatal(err)
  573. }
  574. defer s1.Close()
  575. rep, err := Run(v1, s1, o)
  576. if err != nil {
  577. t.Fatalf("限额迁移失败: %v", err)
  578. }
  579. if rep.Rows != 2 {
  580. t.Fatalf("应只搬 2 行, 实际 %d", rep.Rows)
  581. }
  582. }
  583. // createSQLiteSource 用原生 SQL 建一个外部风格的库 (没有 __vrtx_meta)。
  584. func createSQLiteSource(t *testing.T, path string) {
  585. t.Helper()
  586. db, err := sql.Open("sqlite", path)
  587. if err != nil {
  588. t.Fatal(err)
  589. }
  590. defer db.Close()
  591. stmts := []string{
  592. `CREATE TABLE Items (
  593. id INTEGER PRIMARY KEY,
  594. name VARCHAR(32) NOT NULL,
  595. qty INTEGER NOT NULL,
  596. price REAL NOT NULL,
  597. memo BLOB NOT NULL
  598. )`,
  599. `CREATE INDEX idx_Items_name ON Items(name)`,
  600. }
  601. for _, s := range stmts {
  602. if _, err := db.Exec(s); err != nil {
  603. t.Fatalf("执行 %s: %v", s, err)
  604. }
  605. }
  606. rows := []struct {
  607. id int64
  608. name string
  609. qty int64
  610. price float64
  611. memo []byte
  612. }{
  613. {1, "apple", 10, 1.25, []byte("fresh")},
  614. {2, "banana", -3, 0.5, []byte{0x00, 0xFF, 0x10}},
  615. {3, "中文名", 0, 9.99, []byte("")},
  616. }
  617. for _, r := range rows {
  618. if _, err := db.Exec(`INSERT INTO Items (id, name, qty, price, memo) VALUES (?, ?, ?, ?, ?)`,
  619. r.id, r.name, r.qty, r.price, r.memo); err != nil {
  620. t.Fatalf("插入 %v: %v", r, err)
  621. }
  622. }
  623. }
  624. func readSQLiteRows(t *testing.T, path string) map[string]map[string]any {
  625. t.Helper()
  626. db, err := sql.Open("sqlite", path)
  627. if err != nil {
  628. t.Fatal(err)
  629. }
  630. defer db.Close()
  631. rows, err := db.Query(`SELECT id, name, qty, price, memo FROM Items`)
  632. if err != nil {
  633. t.Fatalf("查询 %s: %v", path, err)
  634. }
  635. defer rows.Close()
  636. out := map[string]map[string]any{}
  637. for rows.Next() {
  638. var (
  639. id int64
  640. name string
  641. qty int64
  642. price float64
  643. memo []byte
  644. )
  645. if err := rows.Scan(&id, &name, &qty, &price, &memo); err != nil {
  646. t.Fatal(err)
  647. }
  648. out[strconv.FormatInt(id, 10)] = map[string]any{
  649. "name": name, "qty": qty, "price": price, "memo": memo,
  650. }
  651. }
  652. if err := rows.Err(); err != nil {
  653. t.Fatal(err)
  654. }
  655. return out
  656. }
  657. // TestMySQLDatabasesUnreachable 主机不通时列库要立刻报错 —— 连接界面上是个按钮,
  658. // 绝不能让它挂着不动, 更不能悄悄返回空列表 (会被当成"这台机器上没有库")。
  659. func TestMySQLDatabasesUnreachable(t *testing.T) {
  660. // 1 号端口基本不会有人监听: 连接立即被拒, 不依赖任何真实 MySQL
  661. dsn := "root:@tcp(127.0.0.1:1)/?timeout=2s"
  662. if _, err := MySQLDatabases(dsn); err == nil {
  663. t.Fatal("连不上的主机应当报错, 而不是返回空列表")
  664. }
  665. }
  666. // TestRunContinueOnError 验的是"一张表出问题不能连坐整趟":
  667. // 没有主键的表会被 prepareForVRTX 拒掉, 但同批里能迁的表必须照常迁完 ——
  668. // 失败原因记进报告, 最后 Run 用聚合错误点名, 而不是静默吞掉。
  669. func TestRunContinueOnError(t *testing.T) {
  670. dir := t.TempDir()
  671. srcPath := filepath.Join(dir, "src.sqlite")
  672. db, err := sql.Open("sqlite", srcPath)
  673. if err != nil {
  674. t.Fatal(err)
  675. }
  676. stmts := []string{
  677. // 没有主键: VRTX 侧建不出来, 这张表注定失败
  678. `CREATE TABLE OldUserDetail (id INTEGER, detail TEXT)`,
  679. `INSERT INTO OldUserDetail (id, detail) VALUES (1, 'x')`,
  680. // 正常表: 不管上面那张怎么炸, 它都该迁完
  681. `CREATE TABLE GoodTable (id INTEGER PRIMARY KEY, name VARCHAR(32))`,
  682. `INSERT INTO GoodTable (id, name) VALUES (1, 'a'), (2, 'b')`,
  683. }
  684. for _, s := range stmts {
  685. if _, err := db.Exec(s); err != nil {
  686. t.Fatalf("执行 %s: %v", s, err)
  687. }
  688. }
  689. if err := db.Close(); err != nil {
  690. t.Fatal(err)
  691. }
  692. // 继续档: 坏表跳过, 好表照迁
  693. o := quiet()
  694. o.ContinueOnError = true
  695. src, err := OpenSQLite(srcPath, quiet())
  696. if err != nil {
  697. t.Fatal(err)
  698. }
  699. defer src.Close()
  700. dst, err := OpenVRTX(filepath.Join(dir, "keep.db"), quiet())
  701. if err != nil {
  702. t.Fatal(err)
  703. }
  704. defer dst.Close()
  705. rep, runErr := Run(src, dst, o)
  706. if runErr == nil {
  707. t.Fatalf("有表没能迁完, 应当返回汇总错误; 报告:\n%s", rep)
  708. }
  709. if got := rep.Failed(); got != 1 {
  710. t.Fatalf("应当只有 1 张表失败, 实际 %d; 报告:\n%s", got, rep)
  711. }
  712. if !strings.Contains(runErr.Error(), "OldUserDetail") {
  713. t.Fatalf("汇总错误应点名失败的表: %v", runErr)
  714. }
  715. for _, tr := range rep.Tables {
  716. if tr.Table == "OldUserDetail" && tr.Error == "" {
  717. t.Fatalf("失败原因没记进报告: %+v", tr)
  718. }
  719. }
  720. var (
  721. goodRows uint64
  722. seen bool
  723. )
  724. for _, tr := range rep.Tables {
  725. if tr.Table == "GoodTable" {
  726. seen, goodRows = true, tr.Rows
  727. }
  728. }
  729. if !seen {
  730. t.Fatalf("坏表把后面的表一起带走了, GoodTable 根本没过一遍; 报告:\n%s", rep)
  731. }
  732. if goodRows != 2 {
  733. t.Fatalf("GoodTable 应当迁 2 行, 实际 %d", goodRows)
  734. }
  735. if n := rep.Failed(); n != 1 {
  736. t.Fatalf("报告里的失败表数应为 1, 实际 %d", n)
  737. }
  738. // 对照: 默认档遇错即停, 不该有"带了失败的报告"这种东西返回
  739. strict := quiet()
  740. src2, err := OpenSQLite(srcPath, quiet())
  741. if err != nil {
  742. t.Fatal(err)
  743. }
  744. defer src2.Close()
  745. dst2, err := OpenVRTX(filepath.Join(dir, "strict.db"), quiet())
  746. if err != nil {
  747. t.Fatal(err)
  748. }
  749. defer dst2.Close()
  750. if _, err := Run(src2, dst2, strict); err == nil {
  751. t.Fatal("默认档应当在第一张失败的表上就中止并报错")
  752. }
  753. }
  754. // ============================ NULL / 唯一约束 ============================
  755. // flagMarks 列上的 NULL / 唯一 / decimal scale 声明, 拼成快照里的后缀。
  756. func flagMarks(flag byte) string {
  757. var s string
  758. if flag&types.FlagNullable != 0 {
  759. s += " NULL"
  760. }
  761. if flag&types.FlagUnique != 0 {
  762. s += " UNIQUE"
  763. }
  764. if sc := flag & types.FlagScaleMask; sc != 0 {
  765. s += fmt.Sprintf(" scale=%d", sc)
  766. }
  767. return s
  768. }
  769. // buildVRTXNullUnique 建一张把 NULL 与唯一约束都压满的样本表:
  770. //
  771. // _id 8byte 行号 (主键列, 非空)
  772. // Name 32byte 非空 + 唯一索引 (列上也带声明位)
  773. // Note 8byte 可空 + 唯一索引
  774. // Age 2byte 可空, 无索引
  775. //
  776. // Note 特意让两行为 NULL: 唯一索引对 NULL 是免除的 (任一分量为 NULL 的行不进索引),
  777. // 非空唯一列测不出这条 —— 也正是"NULL 不能被写成 0"最容易出问题的地方。
  778. func buildVRTXNullUnique(t *testing.T, path string) {
  779. t.Helper()
  780. db, err := core.Open(path)
  781. if err != nil {
  782. t.Fatalf("建样本 VRTX 库: %v", err)
  783. }
  784. defer db.Close()
  785. fields := []types.FieldInfo{
  786. {FieldType: types.FieldType_8byte, FieldName: []byte("_id")},
  787. {FieldType: types.FieldType_32byte, FieldName: []byte("Name"), Flag: types.FlagUnique},
  788. {FieldType: types.FieldType_8byte, FieldName: []byte("Note"), Flag: types.FlagNullable | types.FlagUnique},
  789. {FieldType: types.FieldType_2byte, FieldName: []byte("Age"), Flag: types.FlagNullable},
  790. }
  791. tbl, err := db.CreateTable("People", fields)
  792. if err != nil {
  793. t.Fatalf("建表: %v", err)
  794. }
  795. for _, spec := range []struct {
  796. name string
  797. typ page.IndexType
  798. col byte
  799. unique bool
  800. }{
  801. {"pk_People", page.IndexTypePrimary, 0, false},
  802. {"idx_People_Name", page.IndexTypeBTree, 1, true},
  803. {"idx_People_Note", page.IndexTypeBTree, 2, true},
  804. } {
  805. if _, err := tbl.CreateIndex(spec.name, spec.typ, []byte{spec.col}, spec.unique); err != nil {
  806. t.Fatalf("建索引 %s: %v", spec.name, err)
  807. }
  808. }
  809. writeVRTXRows(t, tbl, fields, []sampleRow{
  810. {pk: 1, vals: map[string]any{"_id": uint64(1), "Name": []byte("Alice"), "Note": uint64(100), "Age": int16(30)}},
  811. // Note / Age 写 nil: 落盘就是 NULL 位图上的一位
  812. {pk: 2, vals: map[string]any{"_id": uint64(2), "Name": []byte("Bob"), "Note": nil, "Age": nil}},
  813. {pk: 3, vals: map[string]any{"_id": uint64(3), "Name": []byte("Cara"), "Note": nil, "Age": int16(7)}},
  814. })
  815. }
  816. // checkSQLiteShape 直接查中间库的 PRAGMA, 确认可空性与唯一性真的落在了 DDL 上
  817. // (而不是只记在 __vrtx_meta 里, SQL 侧其实什么都没表达)。
  818. func checkSQLiteShape(t *testing.T, path, table string, nullable, unique map[string]bool) {
  819. t.Helper()
  820. db, err := sql.Open("sqlite", path)
  821. if err != nil {
  822. t.Fatalf("打开中间库: %v", err)
  823. }
  824. defer db.Close()
  825. rows, err := db.Query(`PRAGMA table_info("` + table + `")`)
  826. if err != nil {
  827. t.Fatalf("table_info: %v", err)
  828. }
  829. gotNullable := map[string]bool{}
  830. for rows.Next() {
  831. var cid, notNull, pk int
  832. var name, decl string
  833. var dflt any
  834. if err := rows.Scan(&cid, &name, &decl, &notNull, &dflt, &pk); err != nil {
  835. t.Fatalf("扫描 table_info: %v", err)
  836. }
  837. gotNullable[name] = notNull == 0
  838. }
  839. rows.Close()
  840. for col, want := range nullable {
  841. if gotNullable[col] != want {
  842. t.Errorf("中间库列 %s 的可空性不对: 实际 %v, 期望 %v", col, gotNullable[col], want)
  843. }
  844. }
  845. idxRows, err := db.Query(`PRAGMA index_list("` + table + `")`)
  846. if err != nil {
  847. t.Fatalf("index_list: %v", err)
  848. }
  849. gotUnique := map[string]bool{}
  850. for idxRows.Next() {
  851. var seq, uniq, partial int
  852. var name, origin string
  853. if err := idxRows.Scan(&seq, &name, &uniq, &origin, &partial); err != nil {
  854. t.Fatalf("扫描 index_list: %v", err)
  855. }
  856. if origin == "c" { // 显式 CREATE INDEX 建的才是我们要比对的
  857. gotUnique[name] = uniq != 0
  858. }
  859. }
  860. idxRows.Close()
  861. for name, want := range unique {
  862. if gotUnique[name] != want {
  863. t.Errorf("中间库索引 %s 的唯一性不对: 实际 %v, 期望 %v", name, gotUnique[name], want)
  864. }
  865. }
  866. }
  867. // TestVRTXSQLiteNullUniqueRoundTrip 可空列 + 唯一索引的 VRTX → SQLite → VRTX。
  868. //
  869. // 这两个属性在两端各只有一半的表达: NULL 在 VRTX 是行内位图的位、在 SQL 是列可空
  870. // 声明加上行里的 NULL; 唯一在 VRTX 是索引上的标志位、在 SQL 是 UNIQUE INDEX。
  871. // 少带任何一半, 往返都会静默变形 —— NULL 被写成一个 0, 或者约束悄悄退化成普通索引。
  872. func TestVRTXSQLiteNullUniqueRoundTrip(t *testing.T) {
  873. dir := t.TempDir()
  874. srcPath := filepath.Join(dir, "src.db")
  875. midPath := filepath.Join(dir, "mid.sqlite")
  876. backPath := filepath.Join(dir, "back.db")
  877. buildVRTXNullUnique(t, srcPath)
  878. want := snapshotVRTX(t, srcPath, "People")
  879. v1, err := OpenVRTX(srcPath, quiet())
  880. if err != nil {
  881. t.Fatal(err)
  882. }
  883. s1, err := OpenSQLite(midPath, quiet())
  884. if err != nil {
  885. t.Fatal(err)
  886. }
  887. if _, err := Run(v1, s1, quiet()); err != nil {
  888. t.Fatalf("VRTX→SQLite: %v", err)
  889. }
  890. _ = v1.Close()
  891. _ = s1.Close()
  892. // 中间库的结构: 可空列写 NULL (不是硬写 NOT NULL), 索引写 UNIQUE INDEX
  893. checkSQLiteShape(t, midPath, "People",
  894. map[string]bool{"_id": false, "Name": false, "Note": true, "Age": true},
  895. map[string]bool{"idx_People_Name": true, "idx_People_Note": true})
  896. s2, err := OpenSQLite(midPath, quiet())
  897. if err != nil {
  898. t.Fatal(err)
  899. }
  900. v2, err := OpenVRTX(backPath, quiet())
  901. if err != nil {
  902. t.Fatal(err)
  903. }
  904. if _, err := Run(s2, v2, quiet()); err != nil {
  905. t.Fatalf("SQLite→VRTX: %v", err)
  906. }
  907. _ = s2.Close()
  908. _ = v2.Close()
  909. got := snapshotVRTX(t, backPath, "People")
  910. if strings.Join(got.columns, "|") != strings.Join(want.columns, "|") {
  911. t.Errorf("列结构 (含 NULL / 唯一声明) 不一致\n 源: %v\n 回: %v", want.columns, got.columns)
  912. }
  913. if len(got.indexes) != len(want.indexes) {
  914. t.Errorf("索引数量不一致\n 源: %v\n 回: %v", want.indexes, got.indexes)
  915. }
  916. for name, def := range want.indexes {
  917. if got.indexes[name] != def {
  918. t.Errorf("索引 %s 不一致\n 源: %q\n 回: %q", name, def, got.indexes[name])
  919. }
  920. }
  921. if len(got.rows) != len(want.rows) {
  922. t.Fatalf("行数不一致: 源 %d, 回 %d", len(want.rows), len(got.rows))
  923. }
  924. // 逐列比对, 并且把"源里有值/回里没值"也算不一致 —— NULL 往返丢了不会体现在
  925. // norm() 上, 只会体现为"这一列在 map 里消失了"。
  926. for pk, wantRow := range want.rows {
  927. gotRow, ok := got.rows[pk]
  928. if !ok {
  929. t.Errorf("pk=%d 的行丢了", pk)
  930. continue
  931. }
  932. for _, col := range []string{"Name", "Note", "Age"} {
  933. wantVal, wantHas := wantRow[col]
  934. gotVal, gotHas := gotRow[col]
  935. if wantHas != gotHas {
  936. t.Errorf("pk=%d 列 %s 的有无不一致: 源 %v (有值=%v), 回 %v (有值=%v) —— NULL 被写成零值了",
  937. pk, col, wantVal, wantHas, gotVal, gotHas)
  938. continue
  939. }
  940. if wantHas && norm(gotVal) != norm(wantVal) {
  941. t.Errorf("pk=%d 列 %s 不一致: 源 %s, 回 %s", pk, col, norm(wantVal), norm(gotVal))
  942. }
  943. }
  944. }
  945. // 约束回到 VRTX 后必须真的生效, 不是画在 schema 上的装饰
  946. back, err := core.Open(backPath)
  947. if err != nil {
  948. t.Fatal(err)
  949. }
  950. defer back.Close()
  951. btbl, ok := back.FindTable("People")
  952. if !ok {
  953. t.Fatal("回来的库里没有 People 表")
  954. }
  955. if _, err := btbl.InsertRow([]types.FieldValue{
  956. {FieldName: []byte("Name"), Value: []byte("Alice")},
  957. }); !errors.Is(err, core.ErrDuplicatedKey) {
  958. t.Errorf("重复 Name 应当报 ErrDuplicatedKey, 实际 %v", err)
  959. }
  960. if _, err := btbl.InsertRow([]types.FieldValue{
  961. {FieldName: []byte("Name"), Value: []byte("Dan")},
  962. {FieldName: []byte("Note"), Value: uint64(100)},
  963. }); !errors.Is(err, core.ErrDuplicatedKey) {
  964. t.Errorf("重复 Note 应当报 ErrDuplicatedKey, 实际 %v", err)
  965. }
  966. // NULL 不算重复: 再加两行 Note=NULL 都该进得去
  967. for _, nm := range []string{"Eve", "Fay"} {
  968. if _, err := btbl.InsertRow([]types.FieldValue{
  969. {FieldName: []byte("Name"), Value: []byte(nm)},
  970. {FieldName: []byte("Note"), Value: nil},
  971. }); err != nil {
  972. t.Errorf("Note=NULL 的 %s 应当能插入 (唯一索引对 NULL 免除), 实际 %v", nm, err)
  973. }
  974. }
  975. }
  976. // TestSQLiteUniqueConstraintToVRTX 外部库用【列级 UNIQUE 约束】时, 约束也要跟着走。
  977. //
  978. // 这种写法在 SQLite 里没有显式索引, 只生成一条 sqlite_autoindex_* (origin='u')。
  979. // 原实现只收 origin='c' 并跳过它, 注释理由是"VRTX 没有唯一约束的概念" —— 现在有了,
  980. // 跳过就等于把用户的约束悄悄删掉。名字也不能照搬 (sqlite_ 前缀是保留的), 要改。
  981. func TestSQLiteUniqueConstraintToVRTX(t *testing.T) {
  982. dir := t.TempDir()
  983. srcPath := filepath.Join(dir, "ext.sqlite")
  984. dstPath := filepath.Join(dir, "ext.db")
  985. db, err := sql.Open("sqlite", srcPath)
  986. if err != nil {
  987. t.Fatal(err)
  988. }
  989. // code 用 INTEGER: TEXT 落到 VRTX 是动态列 (堆存), 动态列不能当索引键
  990. // (引擎会拒, migrate 记进 SkippedIndexes), 那样测的就不是唯一约束的迁移了。
  991. for _, q := range []string{
  992. `CREATE TABLE t (id INTEGER PRIMARY KEY, code INTEGER UNIQUE, note TEXT)`,
  993. `INSERT INTO t (id, code, note) VALUES (1, 100, 'x')`,
  994. `INSERT INTO t (id, code, note) VALUES (2, 200, NULL)`,
  995. } {
  996. if _, err := db.Exec(q); err != nil {
  997. t.Fatalf("%s: %v", q, err)
  998. }
  999. }
  1000. _ = db.Close()
  1001. src, err := OpenSQLite(srcPath, quiet())
  1002. if err != nil {
  1003. t.Fatal(err)
  1004. }
  1005. sc, err := src.Schema("t")
  1006. if err != nil {
  1007. t.Fatalf("读外部库结构: %v", err)
  1008. }
  1009. var uniq *IndexSchema
  1010. for i := range sc.Indexes {
  1011. if sc.Indexes[i].Unique {
  1012. uniq = &sc.Indexes[i]
  1013. }
  1014. }
  1015. if uniq == nil {
  1016. t.Fatalf("code 的唯一约束没读出来, 索引: %v", sc.Indexes)
  1017. }
  1018. if len(uniq.Columns) != 1 || uniq.Columns[0] != "code" {
  1019. t.Fatalf("唯一索引的分量不对: %v", uniq.Columns)
  1020. }
  1021. if strings.HasPrefix(uniq.Name, "sqlite_") {
  1022. t.Errorf("唯一索引名不该照搬 SQLite 的保留名: %s", uniq.Name)
  1023. }
  1024. dst, err := OpenVRTX(dstPath, quiet())
  1025. if err != nil {
  1026. t.Fatal(err)
  1027. }
  1028. if _, err := Run(src, dst, quiet()); err != nil {
  1029. t.Fatalf("SQLite→VRTX: %v", err)
  1030. }
  1031. _ = src.Close()
  1032. _ = dst.Close()
  1033. back, err := core.Open(dstPath)
  1034. if err != nil {
  1035. t.Fatal(err)
  1036. }
  1037. defer back.Close()
  1038. tbl, ok := back.FindTable("t")
  1039. if !ok {
  1040. t.Fatal("VRTX 里没有 t 表")
  1041. }
  1042. // 1) 唯一性落到索引上, 且列上补出了"唯一列"的声明位
  1043. var uniqueIdx core.IndexMeta
  1044. hasUnique := false
  1045. for _, m := range tbl.IndexMeta().GetIndexes() {
  1046. if m.Unique() {
  1047. uniqueIdx, hasUnique = m, true
  1048. }
  1049. }
  1050. if !hasUnique {
  1051. t.Fatalf("唯一约束没迁过来: 库里一条唯一索引都没有")
  1052. }
  1053. codeFid := -1
  1054. for i, f := range tbl.GetFields() {
  1055. if util.BytesToString(f.FieldName) == "code" {
  1056. codeFid = i
  1057. }
  1058. }
  1059. if codeFid < 0 {
  1060. t.Fatal("找不到 code 列")
  1061. }
  1062. if !tbl.GetFields()[codeFid].IsUnique() {
  1063. t.Errorf("code 列应当带上唯一声明位")
  1064. }
  1065. if got := uniqueIdx.Fields(); len(got) != 1 || got[0] != byte(codeFid) {
  1066. t.Errorf("唯一索引的分量应当是 code(%d), 实际 %v", codeFid, got)
  1067. }
  1068. // 2) 约束真的会拦: 重复 code 被拒
  1069. if _, err := tbl.InsertRow([]types.FieldValue{
  1070. {FieldName: []byte("id"), Value: uint64(3)},
  1071. {FieldName: []byte("code"), Value: uint64(100)},
  1072. }); !errors.Is(err, core.ErrDuplicatedKey) {
  1073. t.Errorf("重复 code 应当报 ErrDuplicatedKey, 实际 %v", err)
  1074. }
  1075. // 3) NULL 也保住了 (note 第 2 行是 NULL)
  1076. row, err := tbl.FindByPK(2)
  1077. if err != nil {
  1078. t.Fatalf("读回 pk=2: %v", err)
  1079. }
  1080. for _, fv := range row {
  1081. if util.BytesToString(fv.FieldName) == "note" {
  1082. t.Errorf("note 本该是 NULL (不出现), 实际 %v", fv.Value)
  1083. }
  1084. }
  1085. }
  1086. // TestCappedColumnCapacityReported 源库声明了超过单值上限的列必须写进迁移报告。
  1087. //
  1088. // 与 SkippedIndexes 是同一条原则: 静默把容量改小等于悄悄改结构 —— 源库写的是
  1089. // varchar(60000), 到 VRTX 端只能是条装不下 60000 字节的动态列, 不报出来用户永远不会知道。
  1090. //
  1091. // 但它与"丢索引"一样【不该让整张表陪葬】: 超限只说明这一列的上限比声明的小, 大部分数据照样
  1092. // 迁得动, 真有大值会在写入时报出列名并拒 —— 为了一列的容量放弃整张表的数据, 代价不成比例。
  1093. func TestCappedColumnCapacityReported(t *testing.T) {
  1094. dir := t.TempDir()
  1095. srcPath := filepath.Join(dir, "src.sqlite")
  1096. dstPath := filepath.Join(dir, "dst.db")
  1097. raw, err := sql.Open("sqlite", srcPath)
  1098. if err != nil {
  1099. t.Fatal(err)
  1100. }
  1101. // SQLite 不校验 varchar 的长度, 所以"声明了引擎兑现不了的容量"这种库在现实中真会出现。
  1102. stmts := []string{
  1103. `CREATE TABLE Wide (
  1104. id INTEGER PRIMARY KEY,
  1105. big VARCHAR(60000),
  1106. ok VARCHAR(200)
  1107. )`,
  1108. `INSERT INTO Wide (id, big, ok) VALUES (1, 'small', 'fine')`,
  1109. }
  1110. for _, s := range stmts {
  1111. if _, err := raw.Exec(s); err != nil {
  1112. t.Fatalf("建样本 SQLite 库: %v", err)
  1113. }
  1114. }
  1115. if err := raw.Close(); err != nil {
  1116. t.Fatal(err)
  1117. }
  1118. src, err := OpenSQLite(srcPath, quiet())
  1119. if err != nil {
  1120. t.Fatal(err)
  1121. }
  1122. defer src.Close()
  1123. dst, err := OpenVRTX(dstPath, quiet())
  1124. if err != nil {
  1125. t.Fatal(err)
  1126. }
  1127. defer dst.Close()
  1128. rep, err := Run(src, dst, quiet())
  1129. if err != nil {
  1130. t.Fatalf("迁移不该因为容量封顶而失败: %v", err)
  1131. }
  1132. if len(rep.Tables) != 1 {
  1133. t.Fatalf("应当只有 1 张表的报告, 实际 %d", len(rep.Tables))
  1134. }
  1135. note := rep.Tables[0].Note
  1136. // 报告要点名到列, 并给出两个数: 用户写下的 60000 与引擎能给的上限。
  1137. for _, want := range []string{"容量封顶", "big", "60000", strconv.Itoa(types.MaxStringSize)} {
  1138. if !strings.Contains(note, want) {
  1139. t.Errorf("报告里缺少 %q: %q", want, note)
  1140. }
  1141. }
  1142. // 没超限的列不该被连坐 (ok 列只有 200 字节, 在定长档里)。
  1143. if strings.Contains(note, "ok:") {
  1144. t.Errorf("未超限的列不该出现在容量报告里: %q", note)
  1145. }
  1146. // 数据照迁: 容量封顶只是提示, 不是拒绝。
  1147. if rep.Rows != 1 {
  1148. t.Errorf("应照常搬走 1 行, 实际 %d", rep.Rows)
  1149. }
  1150. }