table_page.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. package core
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "unsafe"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/util"
  11. )
  12. type TablePage struct {
  13. hdr page.PageHdr
  14. db *DB // 反指所属 DB, 用于建表时给 Table 注入
  15. tables []*Table
  16. // schemaExt 全库共享的 schema 扩展页分配器 (链头记在表页第 1 页 PageHdr 之后的 4 字节)
  17. schemaExt *SchemaExtAllocator
  18. handle page.HandleDb
  19. hdrSize uint32 // hdr 的大小
  20. tableInfoSize uint32 // 表头大小
  21. lastPageID uint32 // 最后一页
  22. }
  23. // bindDB 给自己与【已经从盘上加载出来的表】注入 db 指针。
  24. //
  25. // 为什么需要回填: 表的加载发生在 Open 的中途 (tablePage.Init), 那时 DB 结构体还没构造
  26. // 出来, 加载出来的 *Table.db 只能是 nil。而 BlobRead / BlobFree / 事务外的 blob 落链
  27. // (blobCommit) 都要靠它拿 file / 分配页 —— 不回填的话, 库关掉再打开之后读 blob 会直接
  28. // 报 "table is nil" (新表看不出来, 因为它们走 Create, 那时 db 已经在了)。
  29. func (t *TablePage) bindDB(db *DB) {
  30. t.db = db
  31. for _, tbl := range t.tables {
  32. if tbl.db == nil {
  33. tbl.db = db
  34. }
  35. }
  36. }
  37. // NewTablePage 创建表页管理器
  38. func NewTablePage(handle page.HandleDb) *TablePage {
  39. return &TablePage{
  40. handle: handle,
  41. hdrSize: uint32(unsafe.Sizeof(page.PageHdr{})),
  42. tableInfoSize: uint32(unsafe.Sizeof(TableInfo{})),
  43. }
  44. }
  45. // ensureSchemaExt 惰性创建共享扩展页分配器
  46. func (t *TablePage) ensureSchemaExt() *SchemaExtAllocator {
  47. if t.schemaExt == nil {
  48. t.schemaExt = newSchemaExtAllocator(t.handle, 0,
  49. func() []*Table { return t.tables },
  50. t.saveSchemaExtHead)
  51. }
  52. return t.schemaExt
  53. }
  54. // saveSchemaExtHead 把共享扩展页链头写回表页第 1 页 PageHdr 之后的 4 字节
  55. func (t *TablePage) saveSchemaExtHead(pageID uint32) error {
  56. if t.hdr.PageID == 0 {
  57. return nil
  58. }
  59. buf := make([]byte, PageSize)
  60. if err := t.handle.ReadPage(t.hdr.PageID, buf); err != nil {
  61. return err
  62. }
  63. binary.BigEndian.PutUint32(buf[t.hdrSize:t.hdrSize+schemaExtHeadSize], pageID)
  64. return t.handle.SavePage(t.hdr.PageID, buf)
  65. }
  66. // Tables 获取所有表
  67. func (t *TablePage) Tables() []*Table {
  68. return t.tables
  69. }
  70. // LastPageID 获取最后页ID
  71. func (t *TablePage) LastPageID() uint32 {
  72. return t.lastPageID
  73. }
  74. // Init 初始化表页:从磁盘读取所有表 + 所有字段(支持跨页存储)
  75. func (t *TablePage) Init(pageID uint32) {
  76. t.tables = make([]*Table, 0)
  77. t.hdrSize = uint32(unsafe.Sizeof(page.PageHdr{}))
  78. t.tableInfoSize = uint32(unsafe.Sizeof(TableInfo{}))
  79. t.hdr.PageID = pageID
  80. t.hdr.Type = page.TableIndex
  81. t.hdr.NextPage = 0
  82. t.lastPageID = 0
  83. if pageID == 0 {
  84. return
  85. }
  86. // 表页第 1 页 PageHdr 之后的 4 字节 = 共享 schema 扩展页链头
  87. first := make([]byte, PageSize)
  88. extPID := uint32(0)
  89. if err := t.handle.ReadPage(pageID, first); err == nil {
  90. extPID = binary.BigEndian.Uint32(first[t.hdrSize : t.hdrSize+schemaExtHeadSize])
  91. }
  92. t.ensureSchemaExt()
  93. t.schemaExt.firstPID = extPID
  94. for pageID != 0 {
  95. t.lastPageID = pageID
  96. pageBuf := make([]byte, PageSize)
  97. if err := t.handle.ReadPage(pageID, pageBuf); err != nil {
  98. break
  99. }
  100. pageHdr := page.HdrOf(pageBuf)
  101. // 页类型 / 自述页号校验: 表页链同样可能走到"尚未初始化的新页" (或被回收复用的页),
  102. // 对不上就说明这不是表页, 必须立刻停止解析 —— 否则页里的旧内容会被当成 TableInfo,
  103. // 解析出的垃圾 FirstIndexID/FirstData 会一路传到页回收路径上。
  104. if pageHdr.Type != page.TableIndex || pageHdr.PageID != pageID {
  105. break
  106. }
  107. offset := t.hdrSize + schemaExtHeadSize
  108. for offset+t.tableInfoSize <= pageHdr.UseSize {
  109. info := (*TableInfo)(unsafe.Pointer(&pageBuf[offset]))
  110. if util.IsHighBitSet(info.Flag) {
  111. offset += t.tableInfoSize + uint32(info.FieldAllocateSize)
  112. continue
  113. }
  114. activeVer, masterFields, versions := readSchema(
  115. t.handle, pageID, uint16(offset+t.tableInfoSize), info.FieldAllocateSize,
  116. info.ExtPage, info.ExtOff)
  117. indexMate := &IndexMetaPage{
  118. handle: t.handle,
  119. }
  120. indexMate.Init(info.FirstIndexID)
  121. t.tables = append(t.tables, &Table{
  122. info: info,
  123. Field: masterFields,
  124. db: t.db, // 见 bindDB: 这里通常还是 nil, Open 结束前会回填
  125. handle: t.handle,
  126. indexMeta: indexMate,
  127. tablePageID: pageID,
  128. tableStartOffset: offset,
  129. schemaExt: t.schemaExt,
  130. activeVersion: activeVer,
  131. versions: versions,
  132. })
  133. offset += t.tableInfoSize + uint32(info.FieldAllocateSize)
  134. }
  135. pageID = pageHdr.NextPage
  136. }
  137. }
  138. // ===========================================================================
  139. // 表页槽位管理
  140. // ===========================================================================
  141. //
  142. // 表页链上的记录布局 (所有页统一):
  143. //
  144. // [PageHdr 20B][SchemaExtPageID 4B(仅第 1 页用)][TableInfo 104B + 内联 schema 块]...
  145. //
  146. // 每条记录占 tableInfoSize + FieldAllocateSize 字节, 长度由记录自身描述。于是
  147. // Drop 只要把 Flag 置高位, 这条记录就变成"可复用空洞", 遍历方按长度自然跳过它。
  148. //
  149. // Create 的落点优先级:
  150. // 1. 尾页尾部追加 最省事, 也不打乱页内记录顺序
  151. // 2. 复用某个空洞 (first-fit) 空洞够大时按需求切分, 余量补一条墓碑记录
  152. // 3. 压缩整条表页链后重试 碎片太多时把空洞合并回尾部, 顺带回收空页
  153. // 4. 追加新表页 真放不下才让链变长
  154. //
  155. // 有了这一层, "批量建表 → 删表 → 再建表" 的循环不会让表页链无限膨胀。
  156. // tableSlot 一段可写入表记录的槽位
  157. type tableSlot struct {
  158. pageID uint32 // 所在页
  159. offset uint32 // TableInfo 起始偏移
  160. capacity uint32 // 槽位容量 (>= 需求); 复用空洞时可能大于需求
  161. bump bool // 位于尾页尾部 → 需要推进 UseSize
  162. }
  163. // allocSlot 为 required 字节的表记录挑选落点
  164. func (t *TablePage) allocSlot(required uint32) (tableSlot, error) {
  165. // 全库第一张表: 先把链头页建出来
  166. if t.hdr.PageID == 0 {
  167. pid := t.handle.NewPage()
  168. t.InitHeader(pid)
  169. t.hdr.PageID = pid
  170. t.lastPageID = pid
  171. }
  172. // 1) 尾页尾部直接追加
  173. if s, ok, err := t.tailSlot(required); err != nil {
  174. return tableSlot{}, err
  175. } else if ok {
  176. return s, nil
  177. }
  178. // 2) 复用 Drop 留下的空洞
  179. if s, ok, err := t.findFreeSlot(required); err != nil {
  180. return tableSlot{}, err
  181. } else if ok {
  182. return s, nil
  183. }
  184. // 3) 单块空洞都装不下 → 压缩整条链, 把碎片合并回尾部再试
  185. if t.hasHoles() {
  186. if err := t.Compact(); err != nil {
  187. return tableSlot{}, err
  188. }
  189. if s, ok, err := t.tailSlot(required); err != nil {
  190. return tableSlot{}, err
  191. } else if ok {
  192. return s, nil
  193. }
  194. }
  195. // 4) 真放不下: 追加新表页
  196. newPID, err := t.appendPage()
  197. if err != nil {
  198. return tableSlot{}, err
  199. }
  200. return tableSlot{
  201. pageID: newPID,
  202. offset: t.hdrSize + schemaExtHeadSize,
  203. capacity: required,
  204. bump: true,
  205. }, nil
  206. }
  207. // tailSlot 尾页尾部是否还剩 required 字节
  208. func (t *TablePage) tailSlot(required uint32) (tableSlot, bool, error) {
  209. if t.lastPageID == 0 {
  210. return tableSlot{}, false, nil
  211. }
  212. buf := make([]byte, PageSize)
  213. if err := t.handle.ReadPage(t.lastPageID, buf); err != nil {
  214. return tableSlot{}, false, err
  215. }
  216. off := page.HdrOf(buf).UseSize
  217. if off < t.hdrSize+schemaExtHeadSize {
  218. off = t.hdrSize + schemaExtHeadSize
  219. }
  220. if off+required > PageSize {
  221. return tableSlot{}, false, nil
  222. }
  223. return tableSlot{pageID: t.lastPageID, offset: off, capacity: required, bump: true}, true, nil
  224. }
  225. // findFreeSlot 在整条表页链上找第一个容量足够的空洞 (first-fit)
  226. func (t *TablePage) findFreeSlot(required uint32) (tableSlot, bool, error) {
  227. for pid := t.hdr.PageID; pid != 0; {
  228. buf := make([]byte, PageSize)
  229. if err := t.handle.ReadPage(pid, buf); err != nil {
  230. return tableSlot{}, false, err
  231. }
  232. hdr := page.HdrOf(buf)
  233. for off := t.hdrSize + schemaExtHeadSize; off+t.tableInfoSize <= hdr.UseSize; {
  234. info := (*TableInfo)(unsafe.Pointer(&buf[off]))
  235. size := t.tableInfoSize + uint32(info.FieldAllocateSize)
  236. if off+size > hdr.UseSize {
  237. break
  238. }
  239. if util.IsHighBitSet(info.Flag) && size >= required {
  240. return tableSlot{pageID: pid, offset: off, capacity: size}, true, nil
  241. }
  242. off += size
  243. }
  244. pid = hdr.NextPage
  245. }
  246. return tableSlot{}, false, nil
  247. }
  248. // hasHoles 表页链上是否还存在 Drop 留下的空洞
  249. func (t *TablePage) hasHoles() bool {
  250. for pid := t.hdr.PageID; pid != 0; {
  251. buf := make([]byte, PageSize)
  252. if err := t.handle.ReadPage(pid, buf); err != nil {
  253. return false
  254. }
  255. hdr := page.HdrOf(buf)
  256. for off := t.hdrSize + schemaExtHeadSize; off+t.tableInfoSize <= hdr.UseSize; {
  257. info := (*TableInfo)(unsafe.Pointer(&buf[off]))
  258. if util.IsHighBitSet(info.Flag) {
  259. return true
  260. }
  261. off += t.tableInfoSize + uint32(info.FieldAllocateSize)
  262. }
  263. pid = hdr.NextPage
  264. }
  265. return false
  266. }
  267. // appendPage 在链尾追加一张空表页, 并挂到前驱的 NextPage 上
  268. func (t *TablePage) appendPage() (uint32, error) {
  269. pid := t.handle.NewPage()
  270. t.InitHeader(pid)
  271. if t.lastPageID != 0 {
  272. buf := make([]byte, PageSize)
  273. if err := t.handle.ReadPage(t.lastPageID, buf); err != nil {
  274. return 0, err
  275. }
  276. page.HdrOf(buf).NextPage = pid
  277. if err := t.handle.SavePage(t.lastPageID, buf); err != nil {
  278. return 0, err
  279. }
  280. }
  281. t.lastPageID = pid
  282. return pid, nil
  283. }
  284. // checkFieldCount 校验列数能否落盘 (建表与加列共用同一条闸门)。
  285. //
  286. // 列数上限见 MaxFieldsPerTable: FieldCount 与字段索引都是 1 字节, 超了会回绕 / 截断。
  287. // 错误里带上"本次多少列、上限多少列", 只说"字段数量非法"用户不知道要砍到几列。
  288. func checkFieldCount(n int) error {
  289. if n <= 0 {
  290. return fmt.Errorf("%w: 一张表至少要有 1 列", ErrInvalidFieldCount)
  291. }
  292. if n > MaxFieldsPerTable {
  293. return fmt.Errorf("%w: 一张表最多 %d 列, 本次 %d 列 "+
  294. "(列数与字段索引都是 1 字节, 超过会截断成错位 schema)",
  295. ErrInvalidFieldCount, MaxFieldsPerTable, n)
  296. }
  297. return nil
  298. }
  299. // ===========================================================================
  300. // ✅ Create 完整正确(字段数量全是 byte,255上限)
  301. // ===========================================================================
  302. func (t *TablePage) Create(name string, fields []types.FieldInfo) (*Table, error) {
  303. if len(name) > TableNameSize {
  304. return nil, fmt.Errorf("%w: 表名 %q 有 %d 字节, 上限 %d",
  305. ErrTableNameTooLong, name, len(name), TableNameSize)
  306. }
  307. for _, tb := range t.tables {
  308. if util.BytesToString(tb.info.Name[:]) == name && !util.IsHighBitSet(tb.info.Flag) {
  309. return nil, ErrTableExists
  310. }
  311. }
  312. if err := checkFieldCount(len(fields)); err != nil {
  313. return nil, err
  314. }
  315. // 建表即校验列级参数与行宽。
  316. //
  317. // 行宽是 schema 属性而不是行数据属性: 一张建出来就超上限的表, 之后每一行都会写入
  318. // 失败, 而且是在插数据时才暴露。必须在建表这一步拒绝, 并把"哪几列占了多少"报出来。
  319. // 校验放在 core 而不是只留在 ORM/migrate: schema 也能从 SQL 或低层 API 直接建。
  320. if err := validateFieldsForSchema(fields); err != nil {
  321. return nil, err
  322. }
  323. // 过完上面两道闸门后 len(fields) ≤ MaxFieldsPerTable(254), 转 byte 不会回绕。
  324. fieldCount := byte(len(fields))
  325. // 内联块按 schema 实际序列化长度精确分配 (不再 ×2 预分配, 也不加 256B 下限)。
  326. // 超出 MaxInlineSchemaSize 的部分截断, 由 writeSchema 写进共享扩展页。
  327. masterIndices := make([]byte, fieldCount)
  328. for i := range masterIndices {
  329. masterIndices[i] = byte(i)
  330. }
  331. initialVersions := []SchemaVersion{{Version: 1, FieldIndices: masterIndices}}
  332. probe := &Table{Field: fields, activeVersion: 1, versions: initialVersions}
  333. allocateSize := len(probe.serializeSchema())
  334. if allocateSize > MaxInlineSchemaSize {
  335. allocateSize = MaxInlineSchemaSize
  336. }
  337. if allocateSize <= 0 || uint32(allocateSize) > PageSize {
  338. return nil, ErrFieldAllocateTooLarge
  339. }
  340. // 这个表总共需要多少空间
  341. required := t.tableInfoSize + uint32(allocateSize)
  342. // 挑选槽位: 尾页尾部 → 复用 Drop 空洞 → 压缩后重试 → 新页
  343. slot, err := t.allocSlot(required)
  344. if err != nil {
  345. return nil, err
  346. }
  347. pageBuf := make([]byte, PageSize)
  348. if err := t.handle.ReadPage(slot.pageID, pageBuf); err != nil {
  349. return nil, err
  350. }
  351. pageHdr := page.HdrOf(pageBuf)
  352. offset := slot.offset
  353. info := (*TableInfo)(unsafe.Pointer(&pageBuf[offset]))
  354. *info = TableInfo{} // 复用空洞时必须整体清零, 否则会残留被删表的字段
  355. copy(info.Name[:], name)
  356. info.Flag = 0
  357. info.FieldCount = fieldCount
  358. info.ActiveFieldCount = fieldCount // 初始时实际字段数等于总字段数
  359. info.RowSize = types.CalcRowSize(fields)
  360. info.FieldAllocateSize = uint16(allocateSize)
  361. info.TotalRows = 0
  362. info.ExtPage = 0
  363. info.ExtOff = 0
  364. // 创建数据页
  365. dataPID := t.handle.NewPage()
  366. info.FirstData = dataPID
  367. info.LastData = dataPID
  368. dataBuf := make([]byte, PageSize)
  369. dHdr := page.HdrOf(dataBuf)
  370. dHdr.Type = page.DataIndex
  371. dHdr.PageID = dataPID
  372. dHdr.UseSize = t.hdrSize
  373. t.handle.SavePage(dataPID, dataBuf)
  374. // 创建堆页
  375. heapPID := t.handle.NewPage()
  376. info.FirstHeap = heapPID
  377. info.LastHeap = heapPID
  378. info.HeapPageCount = 1 // 建表即开一张堆页, 堆链长度从 1 起算
  379. heapBuf := make([]byte, PageSize)
  380. hHdr := page.HdrOf(heapBuf)
  381. hHdr.Type = page.HeapIndex
  382. hHdr.PageID = heapPID
  383. hHdr.UseSize = t.hdrSize
  384. t.handle.SavePage(heapPID, heapBuf)
  385. indexMate := &IndexMetaPage{
  386. handle: t.handle,
  387. }
  388. // 创建索引元数据。
  389. // 注意这里是 InitEmpty 而不是 Init: indexPID 刚从空闲链摘出来, 盘上还留着它上一世
  390. // 的内容 (常见是 WAL 页残留)。Init 会从盘上回读本页内容来重建 metas, 于是把整页旧
  391. // 内容当成 IndexMeta 数组解析出几十上百条垃圾元数据 (实测 91 条, EntryPage=16777216);
  392. // 之后即便再用 InitHeader 把页写干净, 也清不掉内存里那批垃圾 meta —— 最终由
  393. // DropTable 拿着垃圾 EntryPage 去枚举页, FreePage 落到从未分配过的页号上,
  394. // 写到几十 GB 偏移 (DROP 假死 52s、稀疏文件暴涨到 40GB)。
  395. indexPID := t.handle.NewPage()
  396. indexMate.InitEmpty(indexPID)
  397. info.FirstIndexID = indexPID
  398. info.LastIndexID = indexPID
  399. // 槽位大于需求(复用空洞): 余量按 TableInfo 拆成墓碑记录留给下次建表;
  400. // 连一个 TableInfo 都放不下时并入本表内联块 —— 由 FieldAllocateSize 描述的容量
  401. // 把遍历步进撑到槽位末尾 (writeSchema 会清零整块, 不会留下脏字节)。
  402. if rem := slot.capacity - required; rem >= t.tableInfoSize {
  403. free := (*TableInfo)(unsafe.Pointer(&pageBuf[offset+required]))
  404. *free = TableInfo{}
  405. free.Flag = 0x80
  406. free.FieldAllocateSize = uint16(rem - t.tableInfoSize)
  407. } else if rem > 0 {
  408. info.FieldAllocateSize = uint16(slot.capacity - t.tableInfoSize)
  409. }
  410. if slot.bump {
  411. pageHdr.UseSize = offset + slot.capacity
  412. }
  413. if err := t.handle.SavePage(slot.pageID, pageBuf); err != nil {
  414. return nil, err
  415. }
  416. // schema 定义统一由 writeSchema 落盘 (格式: [activeVer][masterCount][master fields][verCount][ver defs...])
  417. // 初始 schema 一般能完整放进内联块; 超过 MaxInlineSchemaSize 时余下部分自动进共享扩展页
  418. newTable := Table{
  419. info: info,
  420. Field: fields,
  421. db: t.db, // 注入, BlobRead/BlobFree 需要
  422. handle: t.handle,
  423. indexMeta: indexMate,
  424. tablePageID: slot.pageID,
  425. tableStartOffset: offset,
  426. schemaExt: t.ensureSchemaExt(),
  427. activeVersion: 1,
  428. versions: initialVersions,
  429. }
  430. if err := newTable.writeSchema(); err != nil {
  431. return nil, err
  432. }
  433. t.tables = append(t.tables, &newTable)
  434. return &newTable, nil
  435. }
  436. // ===========================================================================
  437. // ✅ Compact / Stats —— 表页链压缩与布局统计
  438. // ===========================================================================
  439. // Compact 压缩整条表页链: 丢弃 Drop 留下的空洞, 把活表记录按原顺序紧排到链首的
  440. // 若干页, 多余页回收。
  441. //
  442. // 安全性: 记录以"自描述长度"顺序排列, 页内偏移不对外暴露 —— 外部一律通过
  443. // Table{tablePageID, tableStartOffset} 定位, 所以搬移记录后只要同步更新内存里的
  444. // Table 引用即可, 不会破坏任何索引或 schema。
  445. func (t *TablePage) Compact() error {
  446. if t.hdr.PageID == 0 {
  447. return nil
  448. }
  449. // 1. 顺序收集链上所有页 ID 与所有活记录的原始字节
  450. var pageIDs []uint32
  451. var live [][]byte
  452. for pid := t.hdr.PageID; pid != 0; {
  453. buf := make([]byte, PageSize)
  454. if err := t.handle.ReadPage(pid, buf); err != nil {
  455. return err
  456. }
  457. hdr := page.HdrOf(buf)
  458. pageIDs = append(pageIDs, pid)
  459. for off := t.hdrSize + schemaExtHeadSize; off+t.tableInfoSize <= hdr.UseSize; {
  460. info := (*TableInfo)(unsafe.Pointer(&buf[off]))
  461. size := t.tableInfoSize + uint32(info.FieldAllocateSize)
  462. if off+size > hdr.UseSize {
  463. break
  464. }
  465. if !util.IsHighBitSet(info.Flag) {
  466. data := make([]byte, size)
  467. copy(data, buf[off:off+size])
  468. live = append(live, data)
  469. }
  470. off += size
  471. }
  472. pid = hdr.NextPage
  473. }
  474. if len(pageIDs) == 0 {
  475. return nil
  476. }
  477. // 2. 紧排。活记录总字节 <= 原占用, 所以一定放得进现有的页数
  478. first := t.hdrSize + schemaExtHeadSize
  479. next := 0
  480. keep := 0
  481. for pageIdx := 0; pageIdx < len(pageIDs); pageIdx++ {
  482. if pageIdx > 0 && next >= len(live) {
  483. break // 后面的页已经没有活记录, 直接回收
  484. }
  485. pid := pageIDs[pageIdx]
  486. buf := make([]byte, PageSize)
  487. if err := t.handle.ReadPage(pid, buf); err != nil {
  488. return err
  489. }
  490. hdr := page.HdrOf(buf)
  491. // 只清记录区: [hdrSize, hdrSize+schemaExtHeadSize) 保留共享扩展页链头
  492. for i := first; i < PageSize; i++ {
  493. buf[i] = 0
  494. }
  495. off := first
  496. for next < len(live) && off+uint32(len(live[next])) <= PageSize {
  497. copy(buf[off:], live[next])
  498. off += uint32(len(live[next]))
  499. next++
  500. }
  501. hdr.UseSize = off
  502. // 3. 同步内存 Table 的落点 (活表名唯一)
  503. for o := first; o+t.tableInfoSize <= hdr.UseSize; {
  504. info := (*TableInfo)(unsafe.Pointer(&buf[o]))
  505. if tb := t.findTableByName(util.BytesToString(info.Name[:])); tb != nil {
  506. tb.info = info
  507. tb.tablePageID = pid
  508. tb.tableStartOffset = o
  509. }
  510. o += t.tableInfoSize + uint32(info.FieldAllocateSize)
  511. }
  512. if err := t.handle.SavePage(pid, buf); err != nil {
  513. return err
  514. }
  515. keep++
  516. }
  517. // 4. 重连链尾, 回收多余页
  518. for i := 0; i < keep; i++ {
  519. buf := make([]byte, PageSize)
  520. if err := t.handle.ReadPage(pageIDs[i], buf); err != nil {
  521. return err
  522. }
  523. nx := uint32(0)
  524. if i+1 < keep {
  525. nx = pageIDs[i+1]
  526. }
  527. page.HdrOf(buf).NextPage = nx
  528. if err := t.handle.SavePage(pageIDs[i], buf); err != nil {
  529. return err
  530. }
  531. }
  532. t.lastPageID = pageIDs[keep-1]
  533. if t.handle.FreePage != nil {
  534. for _, pid := range pageIDs[keep:] {
  535. t.handle.FreePage(pid)
  536. }
  537. }
  538. return nil
  539. }
  540. // Stats 表页链布局统计: 页数 / 活表数 / 空洞数 / 可回收字节数 (诊断与测试用)
  541. func (t *TablePage) Stats() (pages, live, dead int, reclaimable uint32) {
  542. for pid := t.hdr.PageID; pid != 0; {
  543. buf := make([]byte, PageSize)
  544. if err := t.handle.ReadPage(pid, buf); err != nil {
  545. break
  546. }
  547. hdr := page.HdrOf(buf)
  548. pages++
  549. for off := t.hdrSize + schemaExtHeadSize; off+t.tableInfoSize <= hdr.UseSize; {
  550. info := (*TableInfo)(unsafe.Pointer(&buf[off]))
  551. size := t.tableInfoSize + uint32(info.FieldAllocateSize)
  552. if util.IsHighBitSet(info.Flag) {
  553. dead++
  554. reclaimable += size
  555. } else {
  556. live++
  557. }
  558. off += size
  559. }
  560. pid = hdr.NextPage
  561. }
  562. return
  563. }
  564. // findTableByName 按表名找内存中的 Table (活表名唯一)
  565. func (t *TablePage) findTableByName(name string) *Table {
  566. for _, tb := range t.tables {
  567. if tb != nil && util.BytesToString(tb.info.Name[:]) == name {
  568. return tb
  569. }
  570. }
  571. return nil
  572. }
  573. // ===========================================================================
  574. // ✅ RenameTable 重命名表 (只改元数据字符串, 不涉及数据页/索引页迁移)
  575. // ===========================================================================
  576. func (t *TablePage) RenameTable(oldName, newName string) error {
  577. if len(newName) > TableNameSize {
  578. return fmt.Errorf("%w: 新表名 %q 有 %d 字节, 上限 %d",
  579. ErrTableNameTooLong, newName, len(newName), TableNameSize)
  580. }
  581. // 1. 内存里新表名不能已存在
  582. for _, tb := range t.tables {
  583. if util.BytesToString(tb.info.Name[:]) == newName && !util.IsHighBitSet(tb.info.Flag) {
  584. return ErrTableExists
  585. }
  586. }
  587. tb := t.findTableByName(oldName)
  588. if tb == nil || util.IsHighBitSet(tb.info.Flag) {
  589. return ErrTableNotFound
  590. }
  591. // 2. 改内存中的 TableInfo.Name
  592. copy(tb.info.Name[:], newName)
  593. // 清零剩余字节 (旧名可能更长, 尾巴残留会让 BytesToString 读到垃圾)
  594. for i := len(newName); i < TableNameSize; i++ {
  595. tb.info.Name[i] = 0
  596. }
  597. // 3. 持久化盘上 TableInfo: 遍历表页链找到这条 TableInfo, 原地改 Name 字段
  598. for pageID := t.hdr.PageID; pageID != 0; {
  599. buf := make([]byte, PageSize)
  600. if err := t.handle.ReadPage(pageID, buf); err != nil {
  601. return err
  602. }
  603. hdr := page.HdrOf(buf)
  604. offset := t.hdrSize + schemaExtHeadSize
  605. for offset+t.tableInfoSize <= hdr.UseSize {
  606. info := (*TableInfo)(unsafe.Pointer(&buf[offset]))
  607. if util.BytesToString(info.Name[:]) == oldName && !util.IsHighBitSet(info.Flag) {
  608. copy(info.Name[:], newName)
  609. for i := len(newName); i < TableNameSize; i++ {
  610. info.Name[i] = 0
  611. }
  612. if err := t.handle.SavePage(pageID, buf); err != nil {
  613. return err
  614. }
  615. return nil
  616. }
  617. size := t.tableInfoSize + uint32(info.FieldAllocateSize)
  618. offset += size
  619. }
  620. pageID = hdr.NextPage
  621. }
  622. return fmt.Errorf("rename: 磁盘上找不到表 %q (内存有但盘上无, 可能未 Commit)", oldName)
  623. }
  624. // ===========================================================================
  625. // ✅ Drop 删除表并回收所有页
  626. // ===========================================================================
  627. func (t *TablePage) Drop(name string) error {
  628. pageID := t.hdr.PageID
  629. for pageID != 0 {
  630. buf := make([]byte, PageSize)
  631. if err := t.handle.ReadPage(pageID, buf); err != nil {
  632. return err
  633. }
  634. hdr := page.HdrOf(buf)
  635. offset := t.hdrSize + schemaExtHeadSize
  636. for offset+t.tableInfoSize <= hdr.UseSize {
  637. info := (*TableInfo)(unsafe.Pointer(&buf[offset]))
  638. if util.BytesToString(info.Name[:]) == name && !util.IsHighBitSet(info.Flag) {
  639. // 找到该表在内存中的 Table 对象,获取其索引元数据
  640. var tableEntry *Table
  641. for _, tb := range t.tables {
  642. if util.BytesToString(tb.info.Name[:]) == name && !util.IsHighBitSet(tb.info.Flag) {
  643. tableEntry = tb
  644. break
  645. }
  646. }
  647. // 回收该表所有页 (用该表自己的 indexMeta.metas,不遍历共享的 IndexMeta 页链)
  648. t.recycleTablePages(info, tableEntry)
  649. // 软删除标记
  650. info.Flag = util.SetHighBit(info.Flag)
  651. t.handle.SavePage(pageID, buf)
  652. for i := range t.tables {
  653. if util.BytesToString(t.tables[i].info.Name[:]) == name {
  654. t.tables = append(t.tables[:i], t.tables[i+1:]...)
  655. break
  656. }
  657. }
  658. return nil
  659. }
  660. offset += t.tableInfoSize + uint32(info.FieldAllocateSize)
  661. }
  662. pageID = hdr.NextPage
  663. }
  664. return ErrTableNotFound
  665. }
  666. // recycleTablePages 回收表的所有页 (B+树页、DataPage页、Heap页)
  667. // 注意: IndexMeta 页是所有表共享的,不能释放!
  668. func (t *TablePage) recycleTablePages(info *TableInfo, tableEntry *Table) {
  669. if t.handle.FreePage == nil {
  670. return
  671. }
  672. var pagesToFree []uint32
  673. // 1. 用该表自己的 indexMeta.metas 收集索引页和 DataPage
  674. // 不遍历共享的 IndexMeta 页链,避免误释放其他表的索引页
  675. //
  676. // 必须按页布局分流, 与 Deleter.releaseOldTree 同一套约定:
  677. // B+ 树节点结构统一, 按内部条目下降即可枚举全部节点页; 哈希 / 复合 / 时序
  678. // 的页根本不是 B+ 节点 (元数据页+目录页+桶页链, 或扁平条目链), 拿 CollectAllPageIDs
  679. // 去遍历不会报错, 只会「少收」—— 实测哈希索引 20 页里只回收了元数据页 1 页,
  680. // 目录页与 18 个桶页全部泄漏; 复合索引 3 页只回收 1 页。文件只涨不缩。
  681. if tableEntry != nil && tableEntry.indexMeta != nil {
  682. for i := range tableEntry.indexMeta.metas {
  683. m := &tableEntry.indexMeta.metas[i]
  684. if m.Flag&0x80 != 0 || m.EntryPage == 0 {
  685. continue
  686. }
  687. if isBTreePageIndex(m) {
  688. // 收集 B+ 树所有节点页 (内部节点+叶子节点)
  689. btPages := btree.CollectAllPageIDs(t.handle, m.EntryPage)
  690. pagesToFree = append(pagesToFree, btPages...)
  691. // 只有承载行数据的索引 (主键/时序) 叶子节点存 DataPage ID
  692. // 二级索引叶子存的是 PKID(8B),不是 DataPage ID!
  693. if indexStoresRows(m) {
  694. dpIDs := btree.GetDataPageIDs(t.handle, m.EntryPage)
  695. pagesToFree = append(pagesToFree, dpIDs...)
  696. }
  697. continue
  698. }
  699. h, ok := index.Compose.GetHandler(m.IndexType)
  700. if !ok {
  701. continue
  702. }
  703. // 优先整链回收: 哈希索引的桶页 next 早已统一到 PageHdr.NextPage, 且每条
  704. // 桶链的 (head, tail, count) 就持久化在目录槽里 —— 每条链一次 FreeChain,
  705. // O(1) 拼进空闲链, 完全不必读链内任何一页。
  706. if cc, ok := h.(index.ChainCollector); ok && t.handle.FreeChain != nil {
  707. for _, ch := range cc.CollectChains(t.handle, m.EntryPage) {
  708. if ch.Head == 0 || ch.Count == 0 {
  709. continue
  710. }
  711. if ch.Head == ch.Tail {
  712. t.handle.FreePage(ch.Head) // 单页链 (例如元数据页)
  713. continue
  714. }
  715. _ = t.handle.FreeChain(ch.Head, ch.Tail, ch.Count)
  716. }
  717. continue
  718. }
  719. if pc, ok := h.(index.PageCollector); ok {
  720. pagesToFree = append(pagesToFree, pc.CollectPageIDs(t.handle, m.EntryPage)...)
  721. continue
  722. }
  723. // 兜底: 该类型没有页枚举能力时, 至少把数据页收干净 ——
  724. // 数据页是行真正的落点, 漏掉它等于漏掉整张表的数据。
  725. // 承载行数据 ⇔ 实现 RowStoreHandler, 直接断言 (等价于 indexStoresRows, 省一次查表)。
  726. if rs, ok := h.(index.RowStoreHandler); ok {
  727. pagesToFree = append(pagesToFree, rs.GetDataPageIDs(t.handle, m.EntryPage)...)
  728. }
  729. }
  730. }
  731. // 1.5) 表自身的数据页链与索引元数据页链。
  732. // CreateTable 每建一张表都会固定分配三类页: Data(FirstData/LastData)、
  733. // Heap(FirstHeap/LastHeap)、IndexMeta(FirstIndexID/LastIndexID)。
  734. // 上面只收了"索引自己的树页/桶页", 这里还必须把 Data 链与 IndexMeta 链整条收掉,
  735. // 否则 DROP 之后这两条链的页既不被任何表引用、也不在空闲链上 —— 成为永久孤儿页
  736. // (analyzer 报的 Data / IdxMeta 孤儿页正是它们)。
  737. //
  738. // IndexMeta 链是「本表独有」的: 每张表的 TableInfo 各自持有 FirstIndexID..LastIndexID,
  739. // 建表时各自 NewPage, 并不共享。旧注释"IndexMeta 页是全库共享的不能释放"不成立。
  740. // 链内 next 用 PageHdr.NextPage (与 loadMetas 遍历一致), 逐页收集后统一去重释放。
  741. for _, head := range []uint32{info.FirstData, info.FirstIndexID} {
  742. // 成环保护: 链一旦自环 (某页被复用后又把 next 指回自己), 这个循环永远转不完,
  743. // DROP TABLE 会直接假死。宁可少收几页, 也不能挂在一条坏链上。
  744. walked := make(map[uint32]bool)
  745. for pid := head; pid != 0; {
  746. if walked[pid] {
  747. break
  748. }
  749. walked[pid] = true
  750. buf := make([]byte, PageSize)
  751. if err := t.handle.ReadPage(pid, buf); err != nil {
  752. break
  753. }
  754. next := page.HdrOf(buf).NextPage
  755. pagesToFree = append(pagesToFree, pid)
  756. pid = next
  757. }
  758. }
  759. // 2. 堆链 (溢出页): 首尾已知 (FirstHeap/LastHeap), 链内 next 与空闲链同用
  760. // PageHdr.NextPage —— 整条一次性拼进空闲链, 不遍历、不逐页写, O(1)。
  761. //
  762. // 为什么以前必须逐页走: 那时的还链入口只有 FreePage(单页), 所以要先把
  763. // 每个页号读出来。现在 FreeChain 只要 (链首, 链尾, 链长): 前两个 TableInfo
  764. // 里就有, 链长由 heapAllocBlock/heapDropPage 一直维护在 HeapPageCount。
  765. //
  766. // 退化: 缺 FreeChain 能力 (老 demo 手搓的 HandleDb) 或链长未知时, 退回逐页遍历。
  767. heapSpliced := false
  768. if info.FirstHeap != 0 && info.LastHeap != 0 && info.HeapPageCount > 0 && t.handle.FreeChain != nil {
  769. if err := t.handle.FreeChain(info.FirstHeap, info.LastHeap, info.HeapPageCount); err == nil {
  770. heapSpliced = true
  771. }
  772. }
  773. if !heapSpliced {
  774. walked := make(map[uint32]bool)
  775. heapID := info.FirstHeap
  776. for heapID != 0 {
  777. // 同 1.5 的成环保护: 坏链不能把 DROP 拖成死循环。
  778. if walked[heapID] {
  779. break
  780. }
  781. walked[heapID] = true
  782. buf := make([]byte, PageSize)
  783. if err := t.handle.ReadPage(heapID, buf); err != nil {
  784. break
  785. }
  786. hdr := page.HdrOf(buf)
  787. nextHeap := hdr.NextPage
  788. pagesToFree = append(pagesToFree, heapID)
  789. heapID = nextHeap
  790. }
  791. }
  792. // 3. schema 续块不再单独回收: 它位于全库共享的扩展页里, 表被删除后
  793. // 那一段自然成为垃圾, 由 SchemaExtAllocator 在满页压缩重排时统一回收。
  794. // 2.5) v10: 释放表中所有行持有的 blob 链. 链页不在 pagesToFree 集合 (它们不是
  795. // "表自身的"页, 是行字节携带的外部页). 路径: 遍历 data 页每行, 抽出非空 BlobRef
  796. // 调 tableEntry.BlobFree 归还空闲链.
  797. //
  798. // 为什么放在堆链回收之后: 链页与堆页分属两类存储空间, 顺序本身不重要; 放后面
  799. // 是为了和"先把本页相关的全部页回收, 再扫内容"的原则一致 (堆链回收完后, 本表
  800. // 相关的其它页也就剩 data 页与 blob 链, 而 data 页马上就要被 free, 行内容仍可读).
  801. //
  802. // 错误处理: 单行扫描失败 / 单链回收失败继续处理后续, 不让一条坏链把整批回收中断
  803. // (与 releaseRowBlobRefs 一致的"放任不中断"语义).
  804. if tableEntry != nil && tableEntry.db != nil && tableEntry.Field != nil {
  805. hasBlob := false
  806. for _, f := range tableEntry.Field {
  807. if types.IsBlob(f.FieldType) {
  808. hasBlob = true
  809. break
  810. }
  811. }
  812. if hasBlob {
  813. dataWalked := make(map[uint32]bool)
  814. pid := info.FirstData
  815. for pid != 0 {
  816. if dataWalked[pid] {
  817. break
  818. }
  819. dataWalked[pid] = true
  820. buf := make([]byte, PageSize)
  821. if err := t.handle.ReadPage(pid, buf); err != nil {
  822. break
  823. }
  824. releaseBlobRefsInDataPage(tableEntry, buf)
  825. next := page.HdrOf(buf).NextPage
  826. pid = next
  827. }
  828. }
  829. }
  830. // 4. 统一释放所有页。去重并跳过 0: 多个枚举器拼在一起后, 同一个页 ID 出现两次
  831. // 会让空闲链形成自环 (NewPage 之后再也走不出去), 而 0 是保留页号, 直接归还
  832. // 会把空闲链头指向非法页。
  833. seen := make(map[uint32]bool, len(pagesToFree))
  834. for _, pid := range pagesToFree {
  835. if pid == 0 || seen[pid] {
  836. continue
  837. }
  838. // 合法性兜底 (最后一道闸): 页枚举 (CollectAllPageIDs / GetDataPageIDs / 链遍历)
  839. // 只按偏移解析页头, 不看页类型。meta.EntryPage 一旦指向被复用过的页, 行数据会被
  840. // 当成 B+ 条目解析, 截出「从未分配过」的垃圾页号 (实测 2463488)。
  841. // 这种页号绝不能还链: 空闲链头被指向文件里不存在的位置, 下一次 NewPage 拿到它,
  842. // 所有写入都跑到几十 GB 偏移处 (DROP TABLE 因此假死 52s、稀疏文件涨到 40GB)。
  843. // 上游已在 btLoadNode / GetDataPageIDs 里按页类型校验, 这里只处理漏网之鱼。
  844. if t.handle.ValidPage != nil && !t.handle.ValidPage(pid) {
  845. continue
  846. }
  847. seen[pid] = true
  848. t.handle.FreePage(pid)
  849. }
  850. if heapSpliced {
  851. // 堆链已整条交还空闲链, 表记录里的堆指针一并清空, 别留下悬垂引用。
  852. // info 指向 Drop 的 buf 内部, 随后的 SavePage 会把这三项一起落盘。
  853. info.FirstHeap = 0
  854. info.LastHeap = 0
  855. info.HeapPageCount = 0
  856. }
  857. // Data 链 / IndexMeta 链已回收, 同样清空表记录里的指针, 避免留下指向空闲页的悬垂引用。
  858. // (表随后被标记删除, 这四项只影响这条墓碑记录的持久化内容。)
  859. info.FirstData = 0
  860. info.LastData = 0
  861. info.FirstIndexID = 0
  862. info.LastIndexID = 0
  863. }
  864. func (t *TablePage) Find(name string) (*Table, bool) {
  865. for i := range t.tables {
  866. tb := t.tables[i]
  867. if util.BytesToString(tb.info.Name[:]) == name && !util.IsHighBitSet(tb.info.Flag) {
  868. return tb, true
  869. }
  870. }
  871. return nil, false
  872. }
  873. // ReloadInfos 从磁盘重新读回各表的 TableInfo, 原地覆盖 table.info 指向的内存结构。
  874. //
  875. // 供事务 Rollback 使用。事务回滚只是把被改的页从 BufferPool 里丢掉, 而 Table.info 是
  876. // 独立于页缓存的内存副本(不随缓存回收而消失), 不重载的话 TotalRows / FirstData 等字段
  877. // 仍停留在事务中的值 —— 例如回滚后 TotalRows 偏大, 下一次 InsertRow 会按错误的行号继续,
  878. // 并把错误值随 saveTableInfo 写回页里, 造成真正的数据不一致。
  879. //
  880. // only 为 nil 表示全部重载; 否则只重载 tablePageID 落在其中的表。
  881. // 原地赋值(而非替换指针), 保证外部持有的 *Table / *TableInfo 引用继续有效。
  882. func (t *TablePage) ReloadInfos(only map[uint32]struct{}) {
  883. for _, tb := range t.tables {
  884. if tb == nil || tb.info == nil {
  885. continue
  886. }
  887. if only != nil {
  888. if _, ok := only[tb.tablePageID]; !ok {
  889. continue
  890. }
  891. }
  892. buf := make([]byte, PageSize)
  893. if err := t.handle.ReadPage(tb.tablePageID, buf); err != nil {
  894. continue
  895. }
  896. end := tb.tableStartOffset + t.tableInfoSize
  897. if end > uint32(len(buf)) {
  898. continue
  899. }
  900. disk := (*TableInfo)(unsafe.Pointer(&buf[tb.tableStartOffset]))
  901. *tb.info = *disk
  902. // 索引元数据 (IndexMetaPage.metas) 同样是独立于页缓存的内存副本, 必须一起重载:
  903. // 否则 EntryPage 仍指向事务中新建的 B+ 树根 (或被删空时写成的 0),
  904. // 而该根页已随缓存丢弃 → 回滚后 SearchByIndex / 范围扫描全部走空。
  905. if tb.indexMeta != nil {
  906. tb.indexMeta.Reload(tb.info.FirstIndexID)
  907. }
  908. }
  909. }
  910. // InitHeader 初始化头部信息
  911. // 注意: UseSize 额外让出 schemaExtHeadSize 字节 —— 所有表页都在 PageHdr 之后统一预留
  912. // 4 字节(仅第 1 页用来存共享扩展页链头), 这样表偏移遍历逻辑无需区分第几页。
  913. func (t *TablePage) InitHeader(pageID uint32) ([]byte, *page.PageHdr) {
  914. newBuf := make([]byte, PageSize)
  915. newHdr := page.HdrOf(newBuf)
  916. newHdr.Type = page.TableIndex
  917. newHdr.PageID = pageID
  918. newHdr.UseSize = uint32(unsafe.Sizeof(*newHdr)) + schemaExtHeadSize
  919. t.handle.SavePage(pageID, newBuf)
  920. return newBuf, newHdr
  921. }
  922. // releaseBlobRefsInDataPage 扫描一张数据页, 把每行的 blob 链归还空闲链表.
  923. //
  924. // DropTable 调用: 数据页马上要被 FreePage 收回, 行内字节虽然还在缓冲里, 但任何后续
  925. // 操作都不该再读它. 现在释放行持有的 blob 链是"唯一安全的时机"—— 错过这一秒, 链页
  926. // 就永远泄露.
  927. //
  928. // 行级格式 (DataPage 内):
  929. //
  930. // [lenRow 4B BE][pkID 8B BE][schemaVer 1B][row bytes lenRow-8B]
  931. //
  932. // 我们只关心 row bytes 段; 跳过前 12B 头部, 把后 lenRow-8 字节喂给 tableEntry 解析.
  933. //
  934. // 实现细节:
  935. // - DataPageHdr 含 deadBytes / liveCount 但拿不到逐 slot 信息, 这里走"扫描每一行"
  936. // 而不是按 slot 目录 — slot 目录在页尾但 len 字段并不直接列到行起, 走扫描更稳;
  937. // - 表 schemaVer 必须已知, 否则 deserializeRow 返 nil — 行损坏或 schema 已变更时
  938. // 静默跳过即可 (blob 释放失败比行解析失败更糟).
  939. func releaseBlobRefsInDataPage(tableEntry *Table, buf []byte) {
  940. if tableEntry == nil {
  941. return
  942. }
  943. // DataPage 头部: PageHdr (固定 20B) + DataPageHdr (固定 16B) — 见 page 包.
  944. const dataHdrSize = 20 + 16
  945. useSize := uint32(buf[8])<<24 | uint32(buf[9])<<16 | uint32(buf[10])<<8 | uint32(buf[11])
  946. if useSize < uint32(dataHdrSize) || int(useSize) > len(buf) {
  947. return
  948. }
  949. off := uint32(dataHdrSize)
  950. for off+12 <= useSize {
  951. rowLen := uint32(binary.BigEndian.Uint32(buf[off : off+4]))
  952. if rowLen < 9 || off+4+rowLen > useSize {
  953. break // 剩余字节不足或行长非法, 终止扫描 (防御性)
  954. }
  955. // 行格式: [pkID 8B][schemaVer 1B][行字节 rowLen-8B]
  956. rowStart := off + 4 + 8
  957. rowBytes := buf[rowStart : off+4+rowLen]
  958. _ = tableEntry.releaseRowBlobRefs(rowBytes)
  959. off += 4 + rowLen
  960. }
  961. }