deleter.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. package core
  2. import (
  3. "TsDB2/DB/core/index"
  4. "TsDB2/DB/core/index/btree"
  5. "TsDB2/DB/core/index/datapage"
  6. "TsDB2/DB/core/page"
  7. "TsDB2/DB/core/types"
  8. "bytes"
  9. "encoding/binary"
  10. "fmt"
  11. "runtime/debug"
  12. "sort"
  13. "sync"
  14. )
  15. // Deleter 删除操作对象
  16. type Deleter struct {
  17. table *Table
  18. where Condition
  19. }
  20. // Delete 返回删除操作对象
  21. func (table *Table) Delete() *Deleter {
  22. return &Deleter{table: table}
  23. }
  24. // Where 设置删除条件
  25. func (d *Deleter) Where(cond Condition) *Deleter {
  26. d.where = cond
  27. return d
  28. }
  29. // Exec 执行删除
  30. // SQLite 风格优化: 先软删除 DataPage 行 (极快), 再重建索引 (流式, 低内存)
  31. // 避免逐行级联删除 B+ 树的高昂开销
  32. func (d *Deleter) Exec() (int64, error) {
  33. // 找到主键索引
  34. var pkMeta *IndexMeta
  35. for i := range d.table.indexMeta.metas {
  36. m := &d.table.indexMeta.metas[i]
  37. if m.Flag&0x80 != 0 {
  38. continue
  39. }
  40. if m.IndexType == page.IndexTypePrimary || m.IndexType == page.IndexTypeTime {
  41. pkMeta = &d.table.indexMeta.metas[i]
  42. break
  43. }
  44. }
  45. if pkMeta == nil {
  46. return 0, nil
  47. }
  48. // 本流程靠"重建索引"落地, 前提是每种索引都支持从零批量构建。
  49. // 必须在 Phase 1 之前校验: 那里已经把行标记成删除, 事后再报错会留下
  50. // "行没了、索引还是旧的" 的半途状态, 比直接拒绝更糟。
  51. if err := d.checkRebuildSupported(pkMeta); err != nil {
  52. return 0, err
  53. }
  54. // 用 Scan 流式收集匹配行的 pkID (避免 Find 反序列化全行)。
  55. // 必须用 scanWithPKID 拿真实 pkID: 行数据里没有 pkID 字段 (它是行的 8 字节前缀),
  56. // 旧实现退化成"取第一个 uint64 字段值当 pkID", 主键列不是行内第一个 8 字节列时
  57. // 会删掉另一个主键指向的行, 而真正匹配的行一条都没删 —— 静默删错数据。
  58. pkIDs := make(map[uint64]bool)
  59. err := d.table.Query().Where(d.where).scanWithPKID(func(pkID uint64, _ []types.FieldValue) bool {
  60. pkIDs[pkID] = true
  61. return true
  62. })
  63. if err != nil {
  64. return 0, err
  65. }
  66. if len(pkIDs) == 0 {
  67. return 0, nil
  68. }
  69. // Phase 1: 软删除 DataPage 中的行 (只标记 DeletedFlag, 极快!)
  70. pkHandler, _ := index.Compose.GetHandler(pkMeta.IndexType)
  71. dpIDs := pkHandler.GetDataPageIDs(d.table.handle, pkMeta.EntryPage)
  72. var count int64
  73. pkKeyBE := make([]byte, 8) // 复用缓冲区
  74. for _, dpID := range dpIDs {
  75. dp, err := datapage.Load(d.table.handle, dpID)
  76. if err != nil {
  77. continue
  78. }
  79. changed := false
  80. rows := dp.GetAllRows()
  81. for _, storeRow := range rows {
  82. if len(storeRow) < 8 {
  83. continue
  84. }
  85. pkID := binary.BigEndian.Uint64(storeRow[:8])
  86. if pkIDs[pkID] {
  87. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  88. if dp.DeleteByPK(pkKeyBE) {
  89. changed = true
  90. count++
  91. // 行已作废 → 释放它占用的动态字段堆块引用。
  92. // storeRow 是 GetAllRows 的独立拷贝, storeRow[8:] 即该行
  93. // 完整行数据(首字节是 schemaVer, 决定按哪个版本定位引用)。
  94. // 不释放的话堆页 RefCount 只增不减, 永不归零 → 堆页永久泄漏。
  95. d.table.heapReleaseRow(storeRow[8:])
  96. }
  97. }
  98. }
  99. if changed {
  100. dp.Save()
  101. }
  102. }
  103. // 释放 pkIDs 内存 (Phase 2 不需要)
  104. pkIDs = nil
  105. // Phase 2: 流式重建所有索引 (逐个索引从 DataPage 读取, 不缓存全部行)
  106. if err := d.rebuildIndexes(pkMeta, dpIDs); err != nil {
  107. return count, err
  108. }
  109. // Phase 3: 存活行已全部重建到全新的 DataPage, 旧 dpIDs 整个作废
  110. // (pkMeta.EntryPage 已指向新树, 二级索引只存 PKID, 都不再引用旧页)。
  111. // 必须在这里回收, 否则每执行一次批量删除就漏掉整批数据页。
  112. // 走延迟回收: 事务中途直写盘会让"旧页被清空"先于 WAL 落盘, 回滚即丢数据。
  113. // 顺序关键 —— 必须等上面两组读取循环都跑完, 否则会读到已还链的页。
  114. if d.table.handle.DeferFreePage != nil {
  115. for _, dpID := range dpIDs {
  116. d.table.handle.DeferFreePage(dpID)
  117. }
  118. }
  119. // 更新行数
  120. d.table.info.TotalRows -= uint64(count)
  121. d.table.saveTableInfo()
  122. // 失效缓存
  123. d.table.indexMeta.InvalidateRowCache()
  124. return count, nil
  125. }
  126. // rebuildIndexes 批量重建所有索引
  127. // 主键索引: 顺序创建 DataPage + 批量构建 B+ 树 (避免逐行搜索)
  128. // 二级索引: 收集条目 → 排序 → 批量构建 B+ 树
  129. func (d *Deleter) rebuildIndexes(pkMeta *IndexMeta, dpIDs []uint32) error {
  130. fields := d.table.Field
  131. // 行缓冲区按"所有 schema 版本中最宽的行" + 8B pkID 前缀分配。
  132. // 不能用 info.RowSize: AddField/DropField 之后它小于实际行长,
  133. // 沿用会让 GetAllRowsPooled 里的 copy(buf[:rowLen]) 越界 panic。
  134. rowSize := 8 + d.table.maxRowDataSize()
  135. // 内存池: 复用行缓冲区
  136. rowPool := &sync.Pool{
  137. New: func() interface{} {
  138. b := make([]byte, rowSize)
  139. return &b
  140. },
  141. }
  142. // ===== 重建主键索引: 顺序扫描 + 批量构建 =====
  143. var leafEntries [][]byte
  144. var curDP *datapage.DataPage
  145. var curDPID uint32
  146. for _, dpID := range dpIDs {
  147. dp, err := datapage.Load(d.table.handle, dpID)
  148. if err != nil {
  149. continue
  150. }
  151. rows := dp.GetAllRowsPooled(rowPool)
  152. for _, storeRow := range rows {
  153. if len(storeRow) < 8 {
  154. continue
  155. }
  156. rowData := storeRow[8:]
  157. indexKey := extractIndexKey(rowData, pkMeta, fields, d.table.activeVersion, d.table.versions)
  158. if indexKey == nil {
  159. continue
  160. }
  161. // 确保有可用的 DataPage
  162. if curDP == nil {
  163. curDP = datapage.New(d.table.handle)
  164. curDPID = curDP.PageID()
  165. }
  166. // 当前 DataPage 已满则创建新的
  167. if curDP.IsFull(uint32(len(storeRow))) {
  168. curDP = datapage.New(d.table.handle)
  169. curDPID = curDP.PageID()
  170. }
  171. slot := curDP.AppendRowSlot(storeRow)
  172. curDP.Save()
  173. if slot == 0 {
  174. // IsFull 预判与实际容量出现出入时的兜底: 宁可跳过, 也不要写出 slot=0 的坏叶条目
  175. continue
  176. }
  177. // 收集叶条目: [pkID(8B BE) + dpID(4B BE) + slot(2B BE)] —— 带 slot 才能 O(1) 取行
  178. pkID := binary.BigEndian.Uint64(storeRow[:8])
  179. pkKeyBE := make([]byte, 8)
  180. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  181. entry := make([]byte, 14)
  182. copy(entry[:8], pkKeyBE)
  183. binary.BigEndian.PutUint32(entry[8:12], curDPID)
  184. binary.BigEndian.PutUint16(entry[12:14], uint16(slot))
  185. leafEntries = append(leafEntries, entry)
  186. }
  187. // 归还缓冲区到池
  188. for i := range rows {
  189. rowPool.Put(&rows[i])
  190. }
  191. }
  192. // 按 pkID 升序排序后再批量构建。
  193. // BuildFromSorted 假设输入有序, 不能依赖 DataPage 扫描顺序:
  194. // 更新导致的行搬迁会把新 DataPage 排到扫描序列末尾,
  195. // 从而产生 1,2,3,4,6,...,100,5 这样的失序序列, 破坏 B+ 树不变式。
  196. sort.Slice(leafEntries, func(i, j int) bool {
  197. return binary.BigEndian.Uint64(leafEntries[i][:8]) <
  198. binary.BigEndian.Uint64(leafEntries[j][:8])
  199. })
  200. // 批量构建主键 B+ 树 (一次创建所有节点, 避免逐行搜索)
  201. oldPKRoot := pkMeta.EntryPage
  202. pkMeta.EntryPage = btree.BuildFromSorted(leafEntries, d.table.handle)
  203. d.table.indexMeta.saveMeta(pkMeta)
  204. // 新树已建好、元数据已落盘, 旧主键树至此彻底不可达 → 归还其全部页。
  205. // 必须晚于建树: 否则这些页会先被 NewPage 拿去分给新树, 而这里还要按旧树结构遍历。
  206. d.releaseOldTree(pkMeta, oldPKRoot)
  207. leafEntries = nil // 释放内存
  208. debug.FreeOSMemory()
  209. // ===== 重建每个二级索引: 收集 → 排序 → 批量构建 =====
  210. for i := range d.table.indexMeta.metas {
  211. m := &d.table.indexMeta.metas[i]
  212. if m.Flag&0x80 != 0 {
  213. continue
  214. }
  215. if indexStoresRows(m) {
  216. // 行存储索引: 主键索引已在上一步重建; 时序索引在 Exec 开头的
  217. // checkRebuildSupported 里已被拒绝, 走不到这里。
  218. continue
  219. }
  220. // 收集所有二级索引条目
  221. var secEntries [][]byte
  222. for _, dpID := range dpIDs {
  223. dp, err := datapage.Load(d.table.handle, dpID)
  224. if err != nil {
  225. continue
  226. }
  227. rows := dp.GetAllRowsPooled(rowPool)
  228. for _, storeRow := range rows {
  229. if len(storeRow) < 8 {
  230. continue
  231. }
  232. pkID := binary.BigEndian.Uint64(storeRow[:8])
  233. rowData := storeRow[8:]
  234. secKey := extractIndexKey(rowData, m, fields, d.table.activeVersion, d.table.versions)
  235. if secKey == nil {
  236. continue
  237. }
  238. // 构建二级条目: [secKey + pkID(8B BE)]
  239. secEntry := make([]byte, len(secKey)+8)
  240. copy(secEntry[:len(secKey)], secKey)
  241. binary.BigEndian.PutUint64(secEntry[len(secKey):], pkID)
  242. secEntries = append(secEntries, secEntry)
  243. }
  244. for i := range rows {
  245. rowPool.Put(&rows[i])
  246. }
  247. }
  248. // 按复合键 (secKey, pkID) 排序: 二级树的复合分隔键与叶内复合二分都要求
  249. // 全树按复合键全局有序 —— 只按 secKey 排序不够 (同 key 条目的 pkID 顺序
  250. // 无法靠"稳定排序保持物理序"保证, 数据页按主键序而非 pkID 序)。
  251. sort.SliceStable(secEntries, func(i, j int) bool {
  252. ki := secEntries[i][:len(secEntries[i])-8]
  253. kj := secEntries[j][:len(secEntries[j])-8]
  254. if c := compareBytes(ki, kj); c != 0 {
  255. return c < 0
  256. }
  257. pi := binary.BigEndian.Uint64(secEntries[i][len(secEntries[i])-8:])
  258. pj := binary.BigEndian.Uint64(secEntries[j][len(secEntries[j])-8:])
  259. return pi < pj
  260. })
  261. oldSecRoot := m.EntryPage
  262. if err := buildSecondaryFromEntries(d.table.handle, m, secEntries); err != nil {
  263. return err
  264. }
  265. d.table.indexMeta.saveMeta(m)
  266. // 同上: 新结构就位后归还旧结构的全部页。
  267. d.releaseOldTree(m, oldSecRoot)
  268. secEntries = nil
  269. debug.FreeOSMemory()
  270. }
  271. return nil
  272. }
  273. // buildSecondaryFromEntries 用"已按 secKey 排好序"的条目 [secKey + pkID(8B BE)] 构建一个二级索引。
  274. //
  275. // 页布局决定做法, 不能一律批量建树:
  276. // - B+ 树二级索引: 批量建树;
  277. // - 复合 / 哈希二级索引: 页不是 B+ 节点, 必须走各自的 handler.Insert
  278. // (详见 rebuildSecondaryViaHandler 注释)。
  279. //
  280. // 调用方负责 m.EntryPage 的旧根归还 (整表重建走 releaseOldTree); 新建索引回填时
  281. // 旧根本就是 0, 无需归还。
  282. func buildSecondaryFromEntries(handle page.HandleDb, m *IndexMeta, entries [][]byte) error {
  283. if isBTreePageIndex(m) {
  284. m.EntryPage = btree.SBTBuildFromSorted(entries, handle)
  285. return nil
  286. }
  287. return rebuildSecondaryViaHandler(handle, m, entries)
  288. }
  289. // releaseOldTree 归还一棵已被换根、彻底不可达的旧索引结构 (内部节点 + 叶子节点)。
  290. //
  291. // 调用前提:
  292. // - EntryPage 已改指新结构且已 saveMeta;
  293. // - 新结构已构建完成 —— 否则归还的页会被 NewPage 立刻分给新结构, 而这里还要
  294. // 按旧结构遍历收集页 ID;
  295. // - 此后不再有任何代码读这棵旧结构。
  296. //
  297. // 走 table.deferFreePage: 事务中途直写盘会让"页已清零"先于 WAL 落盘, 回滚/崩溃
  298. // 后旧根指针还在, 却指向一张空页。与 Phase 3 回收旧 DataPage 是同一套时序约定。
  299. //
  300. // 遍历方式按页布局分流: B+ 树节点结构统一, CollectAllPageIDs 按内部条目下降即可;
  301. // 哈希 / 复合索引没有共同形状, 走处理器自己实现的 index.PageCollector。
  302. //
  303. // 不回收的后果: 每执行一次批量删除就漏掉整个旧索引结构 (实测 2 万行两级索引
  304. // 约 40 页), 文件只涨不缩。
  305. func (d *Deleter) releaseOldTree(m *IndexMeta, rootPID uint32) {
  306. if m == nil {
  307. return
  308. }
  309. releaseIndexPages(d.table, m.IndexType, rootPID)
  310. }
  311. // releaseIndexPages 归还一棵已彻底不可达的索引结构 (内部节点 + 叶子 / 桶链) 的全部页。
  312. //
  313. // 这是 releaseOldTree 与 DROP INDEX 的共用实现: 两者都换掉/摘掉了根, 旧结构从此没人读,
  314. // 必须把它的页还给空闲链, 否则文件只涨不缩。遍历方式按索引类型的页布局分流:
  315. // - B+ 树 (主键 / 二级 / 旧 Str): CollectAllPageIDs 按内部条目 BFS 下降;
  316. // - 哈希: 桶页 next 已统一到 PageHdr.NextPage, 一条链只登记一次, 提交后一次
  317. // FreeChain 还链 (O(1));
  318. // - 复合 / 时序: 扁平条目链, 走处理器自己的 PageCollector。
  319. //
  320. // 一定要在"新结构就位 (已 saveMeta)"之后调用, 否则归还的页会被 NewPage 立刻分给新结构,
  321. // 而这里还要按旧结构遍历 —— 会取到半截结构。
  322. func releaseIndexPages(table *Table, itype page.IndexType, rootPID uint32) {
  323. if rootPID == 0 {
  324. return
  325. }
  326. if isBTreePageType(itype) {
  327. for _, pid := range btree.CollectAllPageIDs(table.handle, rootPID) {
  328. table.deferFreePage(pid)
  329. }
  330. return
  331. }
  332. h, ok := index.Compose.GetHandler(itype)
  333. if !ok {
  334. return
  335. }
  336. // 整链延迟回收优先: 哈希索引的桶页 next 已统一到 PageHdr.NextPage, 且每条桶链的
  337. // (head, tail, count) 就持久化在目录槽里 —— 一条链只登记一次, 提交后一次 FreeChain
  338. // 还链 (O(1)), 不必逐页查询、逐页改写空闲链。
  339. if cc, ok := h.(index.ChainCollector); ok && table.handle.DeferFreeChain != nil {
  340. for _, ch := range cc.CollectChains(table.handle, rootPID) {
  341. if ch.Head == 0 || ch.Count == 0 {
  342. continue
  343. }
  344. if ch.Head == ch.Tail {
  345. table.deferFreePage(ch.Head) // 单页链 (例如元数据页)
  346. continue
  347. }
  348. table.handle.DeferFreeChain(ch.Head, ch.Tail, ch.Count)
  349. }
  350. return
  351. }
  352. pc, ok := h.(index.PageCollector)
  353. if !ok {
  354. return
  355. }
  356. for _, pid := range pc.CollectPageIDs(table.handle, rootPID) {
  357. table.deferFreePage(pid)
  358. }
  359. }
  360. // isBTreePageIndex 判断该索引的页是不是 B+ 节点页 (page.IndexHead + 内部条目结构)。
  361. //
  362. // 只有 B+ 树页能用 CollectAllPageIDs 安全遍历, 也只有它们能用
  363. // BuildFromSorted / SBTBuildFromSorted 批量构建。其余类型的页布局都不同:
  364. // - HashHandler: 元数据页 + 目录页链 + 桶页链。按 IndexHead 解析会把
  365. // BucketID / NextPage 当成 EntryLen / IsLeaf, 遍历出一串垃圾页 ID, 进而把
  366. // 不属于本索引的页归还掉 —— 那是数据损坏, 不是泄漏;
  367. // - ComposeChainHandler / TimeIndexHandler: 扁平条目链, 没有内部节点。
  368. //
  369. // 注意时序索引 (IndexTypeTime): 它同样是行存储索引, 但页是扁平链而非 B+ 树。
  370. // 因此这里**不能**用 indexStoresRows 来判断 —— 那样会把时序索引当成 B+ 树, 既在
  371. // 回收时漏收页链上的其余页, 又让重建拿 B+ 树页去覆盖它 (见 bulkRebuildSupported)。
  372. func isBTreePageIndex(m *IndexMeta) bool {
  373. if m == nil {
  374. return false
  375. }
  376. return isBTreePageType(m.IndexType)
  377. }
  378. // isBTreePageType 仅按类型判断页布局是否为 B+ 节点页 (与 isBTreePageIndex 同源)。
  379. func isBTreePageType(t page.IndexType) bool {
  380. switch t {
  381. case page.IndexTypePrimary, page.IndexTypeBTree:
  382. return true
  383. case page.IndexTypeStr:
  384. // IndexTypeStr 是 IndexTypeBTree 的旧值, 页格式相同。
  385. return true
  386. default:
  387. return false
  388. }
  389. }
  390. // bulkRebuildSupported 校验一个索引能否参与批量删除的"重建"流程。
  391. //
  392. // 批量删除统一采用「存活行写进全新 DataPage + 索引从零重建」, 因此要求每种索引
  393. // 都具备从零批量构建的能力。页布局决定做法:
  394. // - 主键 B+ 树 / B+ 树二级索引: BuildFromSorted 批量建树
  395. // - 复合 / 哈希二级索引: 清空根后走各自 handler.Insert 逐条重建
  396. //
  397. // 时序索引 (IndexTypeTime) 是"扁平链 + TimeDataPage": 行本身就住在按时间分段的
  398. // TimeDataPage 里, 重建必须重新分段, 与本流程的模型完全不同 —— 明确拒绝。
  399. // 旧实现没有这个校验, 会默默用 btree.BuildFromSorted 把时序索引整体改写成 B+ 树
  400. // (元数据仍写着 IndexTypeTime, 页却成了 B+ 节点), 之后 Search / RangeScan 全部错乱。
  401. func bulkRebuildSupported(m *IndexMeta) error {
  402. if m == nil {
  403. return nil
  404. }
  405. name := string(bytes.TrimRight(m.IndexName[:], "\x00"))
  406. if indexStoresRows(m) && m.IndexType != page.IndexTypePrimary {
  407. return fmt.Errorf(
  408. "批量删除不支持索引 %q (行存储索引类型 %d): 时序索引的行数据按时间分段存放, "+
  409. "批量重建需要重新分段, 请改用逐行删除", name, m.IndexType)
  410. }
  411. switch m.IndexType {
  412. case page.IndexTypePrimary, page.IndexTypeBTree, page.IndexTypeStr,
  413. page.IndexTypeCompose, page.IndexTypeHash:
  414. return nil
  415. default:
  416. return fmt.Errorf("批量删除不支持索引 %q 的类型 %d", name, m.IndexType)
  417. }
  418. }
  419. // checkRebuildSupported 在动任何数据之前确认全部活跃索引都能被重建。
  420. func (d *Deleter) checkRebuildSupported(pkMeta *IndexMeta) error {
  421. if err := bulkRebuildSupported(pkMeta); err != nil {
  422. return err
  423. }
  424. for i := range d.table.indexMeta.metas {
  425. m := &d.table.indexMeta.metas[i]
  426. if m.Flag&0x80 != 0 {
  427. continue
  428. }
  429. if err := bulkRebuildSupported(m); err != nil {
  430. return err
  431. }
  432. }
  433. return nil
  434. }
  435. // rebuildSecondaryViaHandler 用索引自己的 Insert 从零重建一个非 B+ 树二级索引
  436. // (复合索引的扁平页链 / 哈希索引的目录+桶链)。
  437. //
  438. // 为什么不能沿用 SBTBuildFromSorted: 它产出的页是 B+ 节点布局, 而这两种索引的页
  439. // 布局完全不同 —— 元数据仍写着 IndexTypeCompose / IndexTypeHash, 页却成了 B+ 树。
  440. // 之后 readHashMeta 在 page.IndexHeadSize 处的魔数校验直接失败
  441. // (errHashFormatIncompatible: 等值查找恒为 not found, 插入直接报错), 而复合索引
  442. // 的扁平链遍历会把 B+ 内部节点当条目读, 逐步把整条链写乱。这不只是"格式不统一",
  443. // 等于把索引写坏。
  444. //
  445. // 走 handler.Insert 的好处是正确性由正常写入路径背书: 哈希的目录扩容 / 桶分裂、
  446. // 复合链的页追加与链接全部自动走通。代价是这条路径为 O(n log n), 但只影响这两种
  447. // 索引 —— B+ 树二级索引仍是批量建树, 性能没有回退。
  448. //
  449. // entries 已按 key 排序: 复合链页内用二分查找, 必须有序; 哈希不要求有序, 不受影响。
  450. func rebuildSecondaryViaHandler(handle page.HandleDb, m *IndexMeta, entries [][]byte) error {
  451. name := string(bytes.TrimRight(m.IndexName[:], "\x00"))
  452. h, ok := index.Compose.GetHandler(m.IndexType)
  453. if !ok {
  454. return fmt.Errorf("重建索引 %q: 类型 %d 没有注册处理器", name, m.IndexType)
  455. }
  456. // 从空根开始重建; 旧结构由 releaseOldTree 按 PageCollector 单独归还。
  457. m.EntryPage = 0
  458. for _, e := range entries {
  459. if len(e) < 8 {
  460. continue
  461. }
  462. keyLen := len(e) - 8
  463. root, err := h.Insert(handle, &m.EntryPage, e[:keyLen],
  464. binary.BigEndian.Uint64(e[keyLen:]), nil, keyLen)
  465. if err != nil {
  466. return fmt.Errorf("重建索引 %q: %w", name, err)
  467. }
  468. m.EntryPage = root
  469. }
  470. return nil
  471. }
  472. // compareBytes 按字典序比较两个字节切片
  473. func compareBytes(a, b []byte) int {
  474. ml := len(a)
  475. if len(b) < ml {
  476. ml = len(b)
  477. }
  478. for i := 0; i < ml; i++ {
  479. if a[i] < b[i] {
  480. return -1
  481. }
  482. if a[i] > b[i] {
  483. return 1
  484. }
  485. }
  486. if len(a) < len(b) {
  487. return -1
  488. }
  489. if len(a) > len(b) {
  490. return 1
  491. }
  492. return 0
  493. }