blob_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. package core
  2. import (
  3. "bytes"
  4. "crypto/rand"
  5. "errors"
  6. "io"
  7. "strings"
  8. "testing"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  11. )
  12. // TestBlobEmptyRoundTrip 验证"空 blob" = (FirstPageID=0, TotalLen=0) 全程不分配页.
  13. func TestBlobEmptyRoundTrip(t *testing.T) {
  14. db := fixOpenDB(t)
  15. _, err := db.CreateTable("empty_blob", []types.FieldInfo{
  16. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  17. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  18. })
  19. if err != nil {
  20. t.Fatalf("CreateTable: %v", err)
  21. }
  22. tx := db.BeginTransaction()
  23. ref, err := tx.BlobCommit(nil, types.CompressNone)
  24. if err != nil {
  25. t.Fatalf("BlobCommit(nil): %v", err)
  26. }
  27. if !ref.Empty() {
  28. t.Errorf("nil io.Reader -> ref %+v, want Empty", ref)
  29. }
  30. ref2, err := tx.BlobCommit(bytes.NewReader(nil), types.CompressNone)
  31. if err != nil {
  32. t.Fatalf("BlobCommit(empty stream): %v", err)
  33. }
  34. if !ref2.Empty() {
  35. t.Errorf("empty stream -> ref %+v, want Empty", ref2)
  36. }
  37. if err := tx.Commit(); err != nil {
  38. t.Fatalf("Commit: %v", err)
  39. }
  40. }
  41. // TestBlobSinglePageRoundTrip 单页 (16364 字节以内) 的写读一致.
  42. func TestBlobSinglePageRoundTrip(t *testing.T) {
  43. db := fixOpenDB(t)
  44. tbl, _ := db.CreateTable("t1", []types.FieldInfo{
  45. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  46. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  47. })
  48. payload := make([]byte, 8000)
  49. for i := range payload {
  50. payload[i] = byte(i % 251)
  51. }
  52. tx := db.BeginTransaction()
  53. ref, err := tx.BlobCommit(bytes.NewReader(payload), types.CompressNone)
  54. if err != nil {
  55. t.Fatalf("BlobCommit: %v", err)
  56. }
  57. if ref.TotalLen != uint32(len(payload)) {
  58. t.Errorf("TotalLen = %d, want %d", ref.TotalLen, len(payload))
  59. }
  60. if err := tx.Commit(); err != nil {
  61. t.Fatalf("Commit: %v", err)
  62. }
  63. got, err := tbl.BlobRead(ref, types.CompressNone)
  64. if err != nil {
  65. t.Fatalf("BlobRead: %v", err)
  66. }
  67. if !bytes.Equal(got, payload) {
  68. t.Errorf("BlobRead bytes mismatch (len got=%d want=%d)", len(got), len(payload))
  69. }
  70. }
  71. // TestBlobMultiPageRoundTrip 跨页 (4 页, ~50KB).
  72. func TestBlobMultiPageRoundTrip(t *testing.T) {
  73. db := fixOpenDB(t)
  74. tbl, _ := db.CreateTable("t2", []types.FieldInfo{
  75. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  76. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  77. })
  78. payload := make([]byte, 50_000)
  79. if _, err := rand.Read(payload); err != nil {
  80. t.Fatalf("rand.Read: %v", err)
  81. }
  82. tx := db.BeginTransaction()
  83. ref, err := tx.BlobCommit(bytes.NewReader(payload), types.CompressNone)
  84. if err != nil {
  85. t.Fatalf("BlobCommit: %v", err)
  86. }
  87. if err := tx.Commit(); err != nil {
  88. t.Fatalf("Commit: %v", err)
  89. }
  90. got, err := tbl.BlobRead(ref, types.CompressNone)
  91. if err != nil {
  92. t.Fatalf("BlobRead: %v", err)
  93. }
  94. if !bytes.Equal(got, payload) {
  95. t.Errorf("mismatch: got %d bytes, want %d", len(got), len(payload))
  96. }
  97. }
  98. // TestBlobSnappyRoundTrip Snappy 压缩全程 (压缩-解压字节完全一致).
  99. func TestBlobSnappyRoundTrip(t *testing.T) {
  100. db := fixOpenDB(t)
  101. tbl, _ := db.CreateTable("t3", []types.FieldInfo{
  102. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  103. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  104. })
  105. // 高度可压缩的 1MB (少量字节重复)
  106. payload := make([]byte, 1024*1024)
  107. for i := range payload {
  108. payload[i] = byte(i % 4)
  109. }
  110. tx := db.BeginTransaction()
  111. ref, err := tx.BlobCommit(bytes.NewReader(payload), types.CompressSnappy)
  112. if err != nil {
  113. t.Fatalf("BlobCommit snappy: %v", err)
  114. }
  115. if ref.TotalLen != uint32(len(payload)) {
  116. t.Errorf("TotalLen = %d, want %d", ref.TotalLen, len(payload))
  117. }
  118. if err := tx.Commit(); err != nil {
  119. t.Fatalf("Commit: %v", err)
  120. }
  121. got, err := tbl.BlobRead(ref, types.CompressSnappy)
  122. if err != nil {
  123. t.Fatalf("BlobRead snappy: %v", err)
  124. }
  125. if !bytes.Equal(got, payload) {
  126. t.Errorf("snappy round-trip mismatch (got %d, want %d)", len(got), len(payload))
  127. }
  128. // 错误压缩方式 -> 解出的字节数与 TotalLen 不一致, 应报 ErrBlobCorrupted
  129. _, err = tbl.BlobRead(ref, types.CompressNone)
  130. if !errors.Is(err, ErrBlobCorrupted) {
  131. t.Errorf("wrong compress -> got err=%v, want ErrBlobCorrupted", err)
  132. }
  133. }
  134. // TestBlobInterruptedThenRollback 流中断 + Rollback -> 链页全部回收 (无副作用).
  135. func TestBlobInterruptedThenRollback(t *testing.T) {
  136. db := fixOpenDB(t)
  137. _, _ = db.CreateTable("t4", []types.FieldInfo{
  138. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  139. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  140. })
  141. // 模拟 100KB 网络流, 中途"网线断"返回 io.ErrUnexpectedEOF
  142. r := &brokenReader{remaining: 100_000, failAt: 50_000}
  143. tx := db.BeginTransaction()
  144. _, err := tx.BlobCommit(r, types.CompressNone)
  145. if err == nil {
  146. t.Fatal("expected error from broken stream, got nil")
  147. }
  148. if err := tx.Rollback(); err != nil {
  149. t.Fatalf("Rollback: %v", err)
  150. }
  151. // Rollback 后库应回到事务前状态: 再 Commit 应能成功
  152. if err := db.Commit(); err != nil {
  153. t.Fatalf("Commit after rollback: %v", err)
  154. }
  155. }
  156. // TestBlobCommitAndFree 写读一致 + Free 后 BlobFree(Empty) 应是 no-op.
  157. func TestBlobCommitAndFree(t *testing.T) {
  158. db := fixOpenDB(t)
  159. tbl, _ := db.CreateTable("t5", []types.FieldInfo{
  160. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  161. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  162. })
  163. payload := []byte("hello blob world")
  164. tx := db.BeginTransaction()
  165. ref, err := tx.BlobCommit(bytes.NewReader(payload), types.CompressNone)
  166. if err != nil {
  167. t.Fatalf("BlobCommit: %v", err)
  168. }
  169. if err := tx.Commit(); err != nil {
  170. t.Fatalf("Commit: %v", err)
  171. }
  172. got, err := tbl.BlobRead(ref, types.CompressNone)
  173. if err != nil || !bytes.Equal(got, payload) {
  174. t.Fatalf("BlobRead: got=%v err=%v", got, err)
  175. }
  176. // Free 整链
  177. if err := tbl.BlobFree(ref); err != nil {
  178. t.Fatalf("BlobFree: %v", err)
  179. }
  180. if err := db.Commit(); err != nil {
  181. t.Fatalf("Commit after free: %v", err)
  182. }
  183. // BlobFree(Empty) 应是 no-op
  184. if err := tbl.BlobFree(BlobRef{}); err != nil {
  185. t.Errorf("BlobFree(empty): %v", err)
  186. }
  187. }
  188. // TestBlobSnappyIncompressibleMultiBlock 回归: 不可压缩数据 + Snappy 时,
  189. // snappy.Reader 内部 io.ReadFull 会以 >64KB 的 buffer 调用 chainReader.Read;
  190. // 旧实现 uint16(len(p)-total) 在 >65535 时截断为 0, 导致跳页丢数据 -> corrupt input.
  191. func TestBlobSnappyIncompressibleMultiBlock(t *testing.T) {
  192. db := fixOpenDB(t)
  193. tbl, _ := db.CreateTable("t6", []types.FieldInfo{
  194. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  195. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  196. })
  197. // 随机数据 = snappy 几乎不压缩, 每块编码后 >65536 字节, 触发大 buffer Read.
  198. for _, size := range []int{65_556, 200_000, 1_024_000} {
  199. payload := make([]byte, size)
  200. if _, err := rand.Read(payload); err != nil {
  201. t.Fatalf("rand.Read: %v", err)
  202. }
  203. tx := db.BeginTransaction()
  204. ref, err := tx.BlobCommit(bytes.NewReader(payload), types.CompressSnappy)
  205. if err != nil {
  206. t.Fatalf("BlobCommit(%d): %v", size, err)
  207. }
  208. if err := tx.Commit(); err != nil {
  209. t.Fatalf("Commit: %v", err)
  210. }
  211. got, err := tbl.BlobRead(ref, types.CompressSnappy)
  212. if err != nil {
  213. t.Fatalf("BlobRead(%d): %v", size, err)
  214. }
  215. if !bytes.Equal(got, payload) {
  216. t.Fatalf("size=%d: round-trip mismatch (got %d bytes)", size, len(got))
  217. }
  218. }
  219. }
  220. // brokenReader N 字节后返错, 模拟流中断.
  221. type brokenReader struct {
  222. remaining int
  223. failAt int
  224. consumed int
  225. }
  226. func (b *brokenReader) Read(p []byte) (int, error) {
  227. if b.consumed >= b.failAt {
  228. return 0, io.ErrUnexpectedEOF
  229. }
  230. left := b.failAt - b.consumed
  231. if left > len(p) {
  232. left = len(p)
  233. }
  234. n := b.remaining
  235. if n > left {
  236. n = left
  237. }
  238. for i := 0; i < n; i++ {
  239. p[i] = byte(i)
  240. }
  241. b.consumed += n
  242. b.remaining -= n
  243. if b.remaining == 0 {
  244. return n, io.EOF
  245. }
  246. return n, nil
  247. }
  248. // TestBlobColumnChainRoundTrip blob 列直接写"内容本身"([]byte / string / io.Reader) 时,
  249. // 引擎自动落链页, 行内只留 8 字节引用; 读回逐字节一致。
  250. //
  251. // 这条钉的是 types/blob.go 里那句设计目标的执行落点 (Table.blobFieldRef): 用户不需要自己
  252. // 开事务、自己调 BlobCommit 就能写 blob 列。同时钉住"Dynamicbyte 的单值上限不管 blob"——
  253. // 超过一整块堆页 (MaxBlobSize) 的内容照样写得进, 因为链能跨页。
  254. //
  255. // 空内容 (长度为 0) 是特例: 不分配任何链页, 引用为空 (0/0), 读回来是"读不到值"。
  256. func TestBlobColumnChainRoundTrip(t *testing.T) {
  257. db := fixOpenDB(t)
  258. tbl, err := db.CreateTable("blobcol", []types.FieldInfo{
  259. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  260. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  261. })
  262. if err != nil {
  263. t.Fatalf("CreateTable: %v", err)
  264. }
  265. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  266. t.Fatalf("CreateIndex: %v", err)
  267. }
  268. // ② 特意超过 Dynamicbyte 的单值上限: 只有链式存储装得下。
  269. big := make([]byte, MaxBlobSize*2+7)
  270. for i := range big {
  271. big[i] = byte(i * 7 % 251)
  272. }
  273. streamed := strings.Repeat("stream-", 3000)
  274. cases := []struct {
  275. pk uint64
  276. val interface{}
  277. want []byte
  278. }{
  279. {1, []byte("hello blob"), []byte("hello blob")},
  280. {2, big, big},
  281. {3, "string 载体", []byte("string 载体")},
  282. {4, strings.NewReader(streamed), []byte(streamed)},
  283. {5, []byte{}, []byte{}},
  284. }
  285. for _, c := range cases {
  286. if _, err := tbl.InsertRow([]types.FieldValue{
  287. {FieldName: []byte("id"), Value: c.pk},
  288. {FieldName: []byte("data"), Value: c.val},
  289. }); err != nil {
  290. t.Fatalf("InsertRow(pk=%d, %T): %v", c.pk, c.val, err)
  291. }
  292. }
  293. for _, c := range cases {
  294. vals, err := tbl.FindByPK(c.pk)
  295. if err != nil {
  296. t.Fatalf("FindByPK(%d): %v", c.pk, err)
  297. }
  298. var ref types.BlobRef
  299. var got []byte
  300. for _, fv := range vals {
  301. if string(fv.FieldName) != "data" {
  302. continue
  303. }
  304. r, ok := fv.Value.(types.BlobRef)
  305. if !ok {
  306. t.Fatalf("pk=%d: blob 列读出的值是 %T, 期望 types.BlobRef (行内只存 8 字节引用)",
  307. c.pk, fv.Value)
  308. }
  309. ref = r
  310. got, err = tbl.BlobRead(r, types.CompressNone)
  311. if err != nil {
  312. t.Fatalf("pk=%d: BlobRead(%+v): %v", c.pk, r, err)
  313. }
  314. }
  315. if !bytes.Equal(got, c.want) {
  316. t.Errorf("pk=%d: 读回 %d 字节, 期望 %d 字节 (内容不一致)", c.pk, len(got), len(c.want))
  317. }
  318. if int(ref.TotalLen) != len(c.want) {
  319. t.Errorf("pk=%d: ref.TotalLen = %d, 期望 %d", c.pk, ref.TotalLen, len(c.want))
  320. }
  321. // "超过堆页上限的内容"必须真的落在多页链上, 否则这条用例没验到跨页。
  322. if len(c.want) > int(MaxBlobSize) && ref.FirstPageID == 0 {
  323. t.Errorf("pk=%d: %d 字节的内容竟然没有链首页", c.pk, len(c.want))
  324. }
  325. }
  326. // 行内宽度必须是引用宽度 (8 字节), 不是"装得下多少内容" —— 这是能跨页的前提。
  327. if got := types.FieldSize(types.FieldType_Blob, 0); got != types.BlobRefSize {
  328. t.Errorf("blob 列行内宽度 = %d, 期望引用宽度 %d", got, types.BlobRefSize)
  329. }
  330. }
  331. // TestBlobColumnNullableRowLayout 带 NULL blob 列的行布局必须自洽。
  332. //
  333. // blob 列的行内是固定 8 字节引用槽位: NULL 只是不拷字节, 槽位照样要推进。布局预算
  334. // (Pass 2) 与布局落地 (Pass 3) 必须用同一个占用公式 —— 预算按 NULL 记 0、落地却推进 8,
  335. // 末尾那句"行布局长度失配"会让"写一行带 NULL blob 列"直接失败。
  336. //
  337. // 尾列 (变长) 用来验对齐: 槽位错位的话它读回来的就不是写下去的那串字节。
  338. func TestBlobColumnNullableRowLayout(t *testing.T) {
  339. db := fixOpenDB(t)
  340. tbl, err := db.CreateTable("blobnull", []types.FieldInfo{
  341. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  342. {FieldType: types.FieldType_Blob, FieldName: []byte("data"), Flag: types.FlagNullable},
  343. {FieldType: types.FieldType_String, FieldName: []byte("tail"), Flag: types.WidthCodeFor(32)},
  344. })
  345. if err != nil {
  346. t.Fatalf("CreateTable: %v", err)
  347. }
  348. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  349. t.Fatalf("CreateIndex: %v", err)
  350. }
  351. rows := []struct {
  352. pk uint64
  353. data interface{}
  354. tail string
  355. }{
  356. {1, nil, "after-null-blob"}, // NULL blob
  357. {2, []byte("has-value"), "after-value"}, // 有值 blob, 与上一行布局共存
  358. {3, []byte{}, "after-empty"}, // 空 blob (零页)
  359. }
  360. for _, r := range rows {
  361. if _, err := tbl.InsertRow([]types.FieldValue{
  362. {FieldName: []byte("id"), Value: r.pk},
  363. {FieldName: []byte("data"), Value: r.data},
  364. {FieldName: []byte("tail"), Value: r.tail},
  365. }); err != nil {
  366. t.Fatalf("InsertRow(pk=%d, data=%T): %v", r.pk, r.data, err)
  367. }
  368. }
  369. for _, r := range rows {
  370. fvs, err := tbl.FindByPK(r.pk)
  371. if err != nil {
  372. t.Fatalf("FindByPK(%d): %v", r.pk, err)
  373. }
  374. var tail string
  375. var hasBlobValue bool
  376. for _, fv := range fvs {
  377. switch string(fv.FieldName) {
  378. case "tail":
  379. b, _ := fv.Value.([]byte)
  380. tail = string(b)
  381. case "data":
  382. hasBlobValue = true
  383. }
  384. }
  385. if tail != r.tail {
  386. t.Errorf("pk=%d: tail = %q, 期望 %q (NULL blob 的 8B 槽位没对齐?)", r.pk, tail, r.tail)
  387. }
  388. // 值为 nil 或空 blob 时, 行内是空引用, 解码侧不产出这一列的值。
  389. if r.data == nil && hasBlobValue {
  390. t.Errorf("pk=%d: NULL blob 列不该解出值", r.pk)
  391. }
  392. }
  393. }
  394. // TestBlobColumnRejectsUnsupportedValue blob 列收到引擎无法理解的值时必须报错点名。
  395. //
  396. // 宁可报错也不要"把 %T 序列化成某个字节串" —— 那会把一个整数静默变成 blob 内容, 用户
  397. // 直到读回来才发现存的不是自己写的东西。
  398. func TestBlobColumnRejectsUnsupportedValue(t *testing.T) {
  399. db := fixOpenDB(t)
  400. tbl, err := db.CreateTable("blobbad", []types.FieldInfo{
  401. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  402. {FieldType: types.FieldType_Blob, FieldName: []byte("data")},
  403. })
  404. if err != nil {
  405. t.Fatalf("CreateTable: %v", err)
  406. }
  407. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  408. t.Fatalf("CreateIndex: %v", err)
  409. }
  410. _, err = tbl.InsertRow([]types.FieldValue{
  411. {FieldName: []byte("id"), Value: uint64(1)},
  412. {FieldName: []byte("data"), Value: int64(42)},
  413. })
  414. if err == nil {
  415. t.Fatal("blob 列写 int64 必须被拒")
  416. }
  417. if !strings.Contains(err.Error(), "data") {
  418. t.Errorf("报错应点出列名: %v", err)
  419. }
  420. }