main.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/rand"
  5. "fmt"
  6. "io"
  7. "os"
  8. "strings"
  9. "time"
  10. "git.ycl.pub/yl32940/TsDB2/DB"
  11. "git.ycl.pub/yl32940/TsDB2/DB/core"
  12. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  13. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  14. )
  15. const dbPath = "blob_demo.db"
  16. // brokenStream 模拟"前 N 字节正常, 之后 io.ErrUnexpectedEOF"(网络中断 / 文件被截断).
  17. type brokenStream struct {
  18. consumed int
  19. failAt int
  20. }
  21. func (b *brokenStream) Read(p []byte) (int, error) {
  22. if b.consumed >= b.failAt {
  23. return 0, io.ErrUnexpectedEOF
  24. }
  25. n := b.failAt - b.consumed
  26. if n > len(p) {
  27. n = len(p)
  28. }
  29. got, _ := rand.Read(p[:n])
  30. b.consumed += got
  31. return got, nil
  32. }
  33. func must(err error) {
  34. if err != nil {
  35. fmt.Println("ERR:", err)
  36. os.Exit(1)
  37. }
  38. }
  39. // text 把定长字符串列读回的原样字节转成可打印文本 (定长列会补零到列宽).
  40. func text(v interface{}) string {
  41. b, ok := v.([]byte)
  42. if !ok {
  43. return fmt.Sprintf("%v", v)
  44. }
  45. return strings.TrimRight(string(b), "\x00")
  46. }
  47. // fieldValue 从读回的行里按列名取值 (找不到返 nil).
  48. func fieldValue(fvs []types.FieldValue, name string) interface{} {
  49. for _, fv := range fvs {
  50. if string(fv.FieldName) == name {
  51. return fv.Value
  52. }
  53. }
  54. return nil
  55. }
  56. // blobRef 取出一行的 blob 列引用, 并按 compress 把链页内容读回来。
  57. func blobRef(t *core.Table, fvs []types.FieldValue, name string, compress types.FieldCompress) (types.BlobRef, []byte, error) {
  58. ref, ok := fieldValue(fvs, name).(types.BlobRef)
  59. if !ok {
  60. return types.BlobRef{}, nil, fmt.Errorf("列 %s 读出的不是 types.BlobRef (内容被内联进行里了?)", name)
  61. }
  62. got, err := t.BlobRead(ref, compress)
  63. return ref, got, err
  64. }
  65. func main() {
  66. os.Remove(dbPath)
  67. fmt.Println("=== TsDB2 Blob 流式存储 demo ===")
  68. fmt.Println("blob 是【普通的一列】: 与 name/size 同在一行, 内容单独走链页。")
  69. fmt.Println("写入给 []byte / io.Reader / types.Blob 都行, 引擎自动流式落链, 不用手动开事务。")
  70. fmt.Println("行号 (pk) 由引擎自增分配, InsertRow 的返回值就是它 —— 不用自己造 id 列。")
  71. fmt.Println()
  72. db, err := DB.Open(dbPath)
  73. must(err)
  74. db.SetAutoCommitDelay(0) // 关掉防抖自动提交: 每步手动 Commit, 便于对照
  75. // 两列普通列 + 一列 blob。blob 列在行内只占 8 字节引用, 与其它列平级。
  76. tbl, err := db.CoreDB().CreateTable("files", []types.FieldInfo{
  77. {FieldType: types.FieldType_String, FieldName: []byte("name"), Flag: types.WidthCodeFor(32)},
  78. {FieldType: types.FieldType_Int64, FieldName: []byte("size")},
  79. {FieldType: types.FieldType_Blob, FieldName: []byte("data"), Flag: types.FlagNullable},
  80. })
  81. must(err)
  82. // 主键 B+ 树的键始终是行号 pkID (与索引声明在哪个字段上无关, 见 PrimaryHandler),
  83. // 建了它 FindByPK / UpdateRowByPK / DeleteRowByPK 才能按行号定位。
  84. _, err = tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0})
  85. must(err)
  86. // ------------------------------------------------------------------
  87. fmt.Println("[1] 插一行: name + size + data([]byte) —— 多列共存, 行号自增")
  88. payload := bytes.Repeat([]byte("hello blob "), 512) // ~5.5 KB
  89. pk1, err := tbl.InsertRow([]types.FieldValue{
  90. {FieldName: []byte("name"), Value: "readme.txt"},
  91. {FieldName: []byte("size"), Value: int64(len(payload))},
  92. {FieldName: []byte("data"), Value: payload},
  93. })
  94. must(err)
  95. must(db.Commit())
  96. fvs, err := tbl.FindByPK(pk1)
  97. must(err)
  98. ref1, got1, err := blobRef(tbl, fvs, "data", types.CompressNone)
  99. must(err)
  100. fmt.Printf(" 行号 pk=%d; 读回整行: name=%q size=%d data=%d 字节\n",
  101. pk1, text(fieldValue(fvs, "name")), fieldValue(fvs, "size").(int64), len(got1))
  102. fmt.Printf(" 行内只放 8B 引用: ref=%+v; 内容逐字节一致: %v\n", ref1, bytes.Equal(got1, payload))
  103. fmt.Println()
  104. // ------------------------------------------------------------------
  105. fmt.Println("[2] 流式插入: 给 io.Reader (超过 Dynamicbyte 的单块上限, 链能跨页)")
  106. streamLen := int(core.MaxBlobSize)*2 + 7
  107. pk2, err := tbl.InsertRow([]types.FieldValue{
  108. {FieldName: []byte("name"), Value: "random.bin"},
  109. {FieldName: []byte("size"), Value: int64(streamLen)},
  110. // 流在这里被引擎 io.Copy (32KB buf) 消费完, 内存峰值与 blob 大小无关。
  111. {FieldName: []byte("data"), Value: io.LimitReader(rand.Reader, int64(streamLen))},
  112. })
  113. must(err)
  114. must(db.Commit())
  115. fvs, err = tbl.FindByPK(pk2)
  116. must(err)
  117. ref2, got2, err := blobRef(tbl, fvs, "data", types.CompressNone)
  118. must(err)
  119. fmt.Printf(" pk=%d 写入 %d 字节 (单块堆页上限 %d) -> ref.TotalLen=%d 首页=%d, 读回 %d 字节\n",
  120. pk2, streamLen, core.MaxBlobSize, ref2.TotalLen, ref2.FirstPageID, len(got2))
  121. fmt.Println()
  122. // ------------------------------------------------------------------
  123. fmt.Println("[3] 更新同一行的 blob 列: 换内容时旧链自动归还空闲链")
  124. next := []byte("second version, 换了内容")
  125. err = tbl.UpdateRowByPK(pk2, []types.FieldValue{
  126. // 只传 data: 其它列未提供 = 沿用旧行原值 (不会清空)
  127. {FieldName: []byte("data"), Value: next},
  128. })
  129. must(err)
  130. must(db.Commit())
  131. fvs, err = tbl.FindByPK(pk2)
  132. must(err)
  133. ref3, got3, err := blobRef(tbl, fvs, "data", types.CompressNone)
  134. must(err)
  135. fmt.Printf(" 链换了: 更新前 ref=%+v / 更新后 ref=%+v\n", ref2, ref3)
  136. fmt.Printf(" 内容: %q; 其它列仍在: name=%q size=%d\n",
  137. string(got3), text(fieldValue(fvs, "name")), fieldValue(fvs, "size").(int64))
  138. fmt.Println()
  139. // ------------------------------------------------------------------
  140. fmt.Println("[4] 流中断: 100KB 流读到 50KB 断 —— 插入失败 + Rollback, 链页归还")
  141. before := tbl.TotalRows()
  142. tx := db.BeginTransaction()
  143. _, err = tbl.InsertRow([]types.FieldValue{
  144. {FieldName: []byte("name"), Value: "broken.bin"},
  145. {FieldName: []byte("size"), Value: int64(100_000)},
  146. {FieldName: []byte("data"), Value: &brokenStream{failAt: 50_000}},
  147. })
  148. if err == nil {
  149. fmt.Println(" ERR: 期望流错误, 实际成功")
  150. os.Exit(1)
  151. }
  152. fmt.Printf(" 插入失败: %v\n", err)
  153. must(tx.Rollback())
  154. fmt.Printf(" 回滚后行数仍是 %d (失败的行未生效, 已分配的链页随事务归还)\n", tbl.TotalRows())
  155. if tbl.TotalRows() != before {
  156. fmt.Println(" ERR: 行数被改动了")
  157. os.Exit(1)
  158. }
  159. if _, err := tbl.InsertRow([]types.FieldValue{
  160. {FieldName: []byte("name"), Value: "after-rollback.txt"},
  161. {FieldName: []byte("size"), Value: int64(4)},
  162. {FieldName: []byte("data"), Value: []byte("ok")},
  163. }); err != nil {
  164. fmt.Println(" ERR: 回滚后库不可用:", err)
  165. os.Exit(1)
  166. }
  167. must(db.Commit())
  168. fmt.Println(" 回滚后照常插入新行: 成功")
  169. fmt.Println()
  170. // ------------------------------------------------------------------
  171. fmt.Println("[5] 100 MB 随机流 + Snappy: 内存峰值固定 (~128KB, 与 blob 大小无关)")
  172. const MB = 1024 * 1024
  173. t0 := time.Now()
  174. pkBig, err := tbl.InsertRow([]types.FieldValue{
  175. {FieldName: []byte("name"), Value: "big.snappy"},
  176. {FieldName: []byte("size"), Value: int64(100 * MB)},
  177. // 压缩策略是运行时参数 (不落盘), 读侧必须给同一个 compress。
  178. {FieldName: []byte("data"), Value: types.Blob{
  179. Source: io.LimitReader(rand.Reader, 100*MB),
  180. Compress: types.CompressSnappy,
  181. }},
  182. })
  183. must(err)
  184. must(db.Commit())
  185. fvs, err = tbl.FindByPK(pkBig)
  186. must(err)
  187. ref4, got4, err := blobRef(tbl, fvs, "data", types.CompressSnappy)
  188. must(err)
  189. fmt.Printf(" pk=%d 写入耗时 %.1fs; ref=%+v; 读回 %d 字节 (链上 %d 字节)\n",
  190. pkBig, time.Since(t0).Seconds(), ref4, len(got4), ref4.TotalLen)
  191. fmt.Println()
  192. // ------------------------------------------------------------------
  193. fmt.Println("[6] 关库重开再读: 表是从盘上加载的, blob 仍读得回来")
  194. must(db.Close())
  195. db2, err := DB.Open(dbPath)
  196. must(err)
  197. db2.SetAutoCommitDelay(0)
  198. tbl2, ok := db2.CoreDB().FindTable("files")
  199. if !ok {
  200. fmt.Println(" ERR: 重开后找不到表 files")
  201. os.Exit(1)
  202. }
  203. fvs, err = tbl2.FindByPK(pk1)
  204. must(err)
  205. _, got6, err := blobRef(tbl2, fvs, "data", types.CompressNone)
  206. must(err)
  207. fmt.Printf(" pk=%d 内容与写入一致: %v (前 16 字节 %q)\n",
  208. pk1, bytes.Equal(got6, payload), string(got6[:min(16, len(got6))]))
  209. must(db2.Close())
  210. fmt.Println()
  211. fmt.Println("=== 全部场景通过 ===")
  212. }