primary_handler.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. package index
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
  4. "git.ycl.pub/yl32940/TsDB2/DB/core/index/datapage"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  6. "encoding/binary"
  7. )
  8. // PrimaryHandler — 主键 B+ 树索引 (IndexTypePrimary)
  9. // 存储: storeRow = [pkID(8B LE) + rowData]
  10. type PrimaryHandler struct{}
  11. func init() {
  12. Compose.RegisterHandler(page.IndexTypePrimary, &PrimaryHandler{})
  13. // 主键 B+ 树分裂参数 —— 写死在本文件, 不随索引元数据传递。
  14. // 主键多为自增/递增: 顺序追加时右叶只保留 1 条 (SequentialRight), 避免每次
  15. // 分裂都把整页对半搬运; 非顺序插入仍按 SplitPercent 的比例分裂。
  16. btree.PKSplitCfg = btree.SplitConfig{SplitPercent: 50, SequentialRight: true}
  17. }
  18. func (h *PrimaryHandler) Insert(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, rowData []byte, keyLen int) (uint32, error) {
  19. // 一次分配即得 [pkID(8B BE) + rowData]: 前 8 字节同时作为 B+ 树 key (只读)
  20. storeRowData := make([]byte, 8+len(rowData))
  21. binary.BigEndian.PutUint64(storeRowData[:8], pkID)
  22. copy(storeRowData[8:], rowData)
  23. // 主键 B+ 树始终以 pkID(8B BE) 为 key, 与 Search/Delete/Update 保持一致
  24. return h.insertPrebuilt(handle, entryPage, storeRowData[:8], storeRowData)
  25. }
  26. // insertPrebuilt 写入已构建好的 storeRowData (storeRowData = [pkID(8B BE) + rowData])
  27. func (h *PrimaryHandler) insertPrebuilt(handle page.HandleDb, entryPage *uint32, indexKey []byte, storeRowData []byte) (uint32, error) {
  28. rootPID := *entryPage
  29. rowSize := uint32(len(storeRowData)) + 4 // 4B len prefix + row data
  30. // appendInto 把行写进 dpID 这一页, 并把 (key -> dpID, slot) 挂进 B+ 树叶。
  31. // 返回 (B+ 树新根, 是否写入成功, 错误) —— "未写入"只表示这一页装不下, 由调用方决定换页。
  32. //
  33. // 用 InsertRowSorted 而不是 AppendRow: 主键由调用方显式给出时
  34. // (自定义主键 / BaseModel.ID) 插入顺序与 pkID 顺序无关, 而
  35. // FindRowByPK 依赖"页内行按 pkID 升序"提前终止 —— 追加小号会让
  36. // 行在页里却查不到 (详见 datapage.InsertRowSorted 的说明)。
  37. //
  38. // 返回的 slot 会一并写进叶条目, 之后查找/范围扫描就能 O(1) 取行。
  39. appendInto := func(dpID uint32) (uint32, bool, error) {
  40. dp, err := datapage.Load(handle, dpID)
  41. if err != nil {
  42. return 0, false, nil
  43. }
  44. slot, ok := dp.InsertRowSortedSlot(storeRowData)
  45. if !ok {
  46. dp.Release()
  47. return 0, false, nil
  48. }
  49. dp.Save()
  50. dp.Release() // Save 已把整页交给缓冲池, 此后不再访问 dp
  51. newRoot, _, _, err := btree.BTreePKInsert(rootPID, indexKey, dpID, uint16(slot), handle)
  52. if err != nil {
  53. return 0, false, err
  54. }
  55. return newRoot, true, nil
  56. }
  57. // commit 记录新的树根 (B+ 树未改根时 newRoot == rootPID, 无需回写)
  58. commit := func(newRoot uint32) uint32 {
  59. if newRoot != rootPID {
  60. *entryPage = newRoot
  61. }
  62. return newRoot
  63. }
  64. // 1) 尝试复用 DataPage: 只在路由目标叶子内查找
  65. if rootPID != 0 {
  66. if dpID, found := btree.FindLastLeafDP(handle, rootPID, rowSize, indexKey); found {
  67. if newRoot, ok, err := appendInto(dpID); err != nil {
  68. return 0, err
  69. } else if ok {
  70. return commit(newRoot), nil
  71. }
  72. }
  73. // 2) 末页判满: 开新页之前先按配置就地整理一次 (墓碑占比达到 CompactDeadRatio 才动手,
  74. // 配置 <=0 即关闭)。整理会把墓碑占的空间还给活行, 常常就够放下这一行;
  75. // CompactForInsert 会先判断"整理后确实装得下", 判断不成立就放弃, 不白搬一趟。
  76. //
  77. // 为什么值得做: 被删掉大半的页若一直占着位置, 写入就只能不断开新页 (每页只放
  78. // 少量行), 全表扫描要读的页数随之膨胀 —— 这正是"删多了以后表越写越散"的来路。
  79. if dpID, ok := btree.LastLeafDPID(handle, rootPID, indexKey); ok {
  80. if dp, lerr := datapage.Load(handle, dpID); lerr == nil && dp.CompactForInsert(rowSize) {
  81. if newRoot, ok, err := appendInto(dpID); err != nil {
  82. return 0, err
  83. } else if ok {
  84. return commit(newRoot), nil
  85. }
  86. }
  87. }
  88. }
  89. // 3) 整理后仍装不下 (或本页没有值得回收的墓碑, 或树为空): 创建新 DataPage
  90. dp := datapage.New(handle)
  91. slot := dp.AppendRowSlot(storeRowData)
  92. if slot == 0 {
  93. // 空页都装不下 → 行本身超长 (调用方应先经 CheckRowSize / 溢出页处理)
  94. return 0, page.ErrRowTooLarge
  95. }
  96. dp.Save()
  97. newRoot, _, _, err := btree.BTreePKInsert(rootPID, indexKey, dp.PageID(), uint16(slot), handle)
  98. if err != nil {
  99. return 0, err
  100. }
  101. *entryPage = newRoot
  102. return newRoot, nil
  103. }
  104. func (h *PrimaryHandler) Search(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int, resolve RowResolver) ([]byte, bool) {
  105. // 主键索引自己就是行数据所在, 不需要回表 (resolve 仅对二级索引有意义)
  106. if entryPage == 0 || len(indexKey) < 8 {
  107. return nil, false
  108. }
  109. // 主键索引的键就是 pkID (见 Insert: 叶键 = storeRow[:8]), 所以这里必须按 pkID 定位。
  110. // 旧实现是 dp.FindRowByIndexKey(indexKey, 9) —— 即"拿 key 去和第 1 个字段的值比",
  111. // 只有当第 1 列的值恒等于 pkID (ORM 的 _id/_ts) 时才凑巧成立; 主键索引声明在别的
  112. // 列上时, SearchByIndex(pk_.., pkKey(id)) 会查不到行 (数据在, 但 pkID 不可达),
  113. // 而且它按偏移 9 匹配的是"页内第一行满足者", 大表上还会路由到别的页而漏行。
  114. // 统一按 pkID 走, 与 SearchByPK/DeleteByPK/Update/ResolveRow 同一套语义。
  115. return h.SearchByPK(handle, entryPage, binary.BigEndian.Uint64(indexKey[:8]))
  116. }
  117. // IsRowStore 主键索引直接承载行数据
  118. func (h *PrimaryHandler) IsRowStore() bool { return true }
  119. // ResolveRow 按 pkID 读取整行。
  120. //
  121. // 叶条目现在直接给出 (dpID, slot), 一次树下降 + ReadRowBySlotPK 就是 O(1); 而旧的
  122. // 「页区间二分 cache + 页内全扫」在 slot 之后反而更慢 (它仍要逐行扫页), 所以这里统一
  123. // 交给 SearchByPK。cache 参数仅为兼容接口保留 —— 不再需要它来避免重复扫页。
  124. func (h *PrimaryHandler) ResolveRow(handle page.HandleDb, entryPage uint32, pkID uint64, cache *[]DPRange) ([]byte, bool) {
  125. _ = cache
  126. return h.SearchByPK(handle, entryPage, pkID)
  127. }
  128. // resolveChunk 批量回表的单批行数: 每批"游标走查 + 物化"完成后才触发上层回调,
  129. // 回调里对行存储索引的 Update/Delete 不会踩坏正在推进的游标。
  130. const resolveChunk = 512
  131. // ResolveMany 批量回表: 见 index.BatchRowResolver 接口契约。
  132. //
  133. // 分批执行: 每批"游标走查 + 物化"完成后才调 sink —— 回调里对行存储索引的
  134. // Update/Delete 不会踩坏正在推进的叶链游标 (踩坏场景: 删除触发叶合并, NextPage
  135. // 被改写, 游标读到回收页; demo/stress_test 的 Scan+Delete 实测踩过)。
  136. // 行快照跨 sink 调用有效 (实现内部缓冲)。
  137. func (h *PrimaryHandler) ResolveMany(handle page.HandleDb, entryPage uint32,
  138. sortedPKIDs []uint64, sink func(pkID uint64, storeRow []byte) bool) {
  139. if entryPage == 0 || len(sortedPKIDs) == 0 {
  140. return
  141. }
  142. for start := 0; start < len(sortedPKIDs); start += resolveChunk {
  143. end := start + resolveChunk
  144. if end > len(sortedPKIDs) {
  145. end = len(sortedPKIDs)
  146. }
  147. chunk := sortedPKIDs[start:end]
  148. keys := make([][]byte, len(chunk))
  149. for i, pkID := range chunk {
  150. k := make([]byte, 8)
  151. binary.BigEndian.PutUint64(k, pkID)
  152. keys[i] = k
  153. }
  154. // 本批物化: 游标走查期间【没有】用户回调在跑
  155. type hit struct {
  156. pkID uint64
  157. row []byte
  158. }
  159. var batch []hit
  160. var lastDP *datapage.DataPage
  161. lastDPID := uint32(0)
  162. btree.BTreePKBatchSearch(entryPage, keys, handle,
  163. func(keyIdx int, dpID uint32, slot uint16) {
  164. if dpID != lastDPID {
  165. dp, err := datapage.Load(handle, dpID)
  166. if err != nil {
  167. lastDPID = dpID
  168. lastDP = nil
  169. return
  170. }
  171. lastDP, lastDPID = dp, dpID
  172. }
  173. if lastDP == nil {
  174. return
  175. }
  176. if row, ok := lastDP.ReadRowBySlotPK(uint32(slot), keys[keyIdx]); ok {
  177. batch = append(batch, hit{pkID: chunk[keyIdx], row: row})
  178. }
  179. })
  180. // 走查完成, 现在才触发上层回调
  181. for _, h := range batch {
  182. if !sink(h.pkID, h.row) {
  183. return
  184. }
  185. }
  186. }
  187. }
  188. func (h *PrimaryHandler) SearchByPK(handle page.HandleDb, entryPage uint32, pkID uint64) ([]byte, bool) {
  189. rootPID := entryPage
  190. if rootPID == 0 {
  191. return nil, false
  192. }
  193. pkKeyBE := make([]byte, 8)
  194. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  195. // 叶条目带 slot: 命中叶即 O(1) 取行, 无需在页内按 pkID 扫一遍
  196. dpID, slot, st := btree.BTreePKSearchEx(rootPID, pkKeyBE, handle)
  197. if st == btree.PKMiss {
  198. // 正常下降到达叶子且键不在: 该主键不存在, 直接返回。
  199. // 旧实现在这里无条件全叶扫描 —— 查不存在的键是 O(叶数), 插入查重
  200. // (rowStoreHasPK) 逐行触发它, 显式主键的批量导入退化成 O(N²)。
  201. return nil, false
  202. }
  203. if st == btree.PKHit {
  204. dp, err := datapage.Load(handle, dpID)
  205. if err == nil {
  206. if rowData, ok := dp.ReadRowBySlotPK(uint32(slot), pkKeyBE); ok {
  207. return rowData, true
  208. }
  209. }
  210. // 叶条目命中但槽位取不到行 (slot 失配): 数据页可能被换页/重建过, 仍走兜底确认
  211. }
  212. // 路由失败 (树结构异常): 扫描所有叶子
  213. return btree.ScanAllLeavesForPK(rootPID, handle, pkKeyBE)
  214. }
  215. func (h *PrimaryHandler) Delete(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, keyLen int) error {
  216. rootPID := *entryPage
  217. if rootPID == 0 {
  218. return page.ErrNotFound
  219. }
  220. dpID, slot, st := btree.BTreePKSearchEx(rootPID, indexKey, handle)
  221. if st == btree.PKMiss {
  222. return page.ErrNotFound // 键不存在: 树里没有条目可删, 也无行可删
  223. }
  224. if st == btree.PKHit {
  225. dp, err := datapage.Load(handle, dpID)
  226. if err == nil {
  227. deleted := false
  228. if _, ok := dp.ReadRowBySlotPK(uint32(slot), indexKey); ok {
  229. // 槽位命中 → O(1) 删行
  230. deleted = dp.DeleteBySlot(uint32(slot))
  231. } else if dp.DeleteByIndexKey(indexKey, 8) {
  232. deleted = true
  233. } else if dp.DeleteByPK(indexKey) {
  234. deleted = true
  235. }
  236. if deleted {
  237. // 摘除本行在 B+ 树叶里的条目 (pk -> dpID)。必须「每次删除都做」,
  238. // 不能只在 DataPage 变空时做: 旧实现只在 AllDeleted() 时删除
  239. // 「触发变空的那一个键」, 同一页里先被删掉的行会留下陈旧叶条目。
  240. // 该 DataPage 随后被回收复用 (例如改作 WAL 页) 时, 这些条目就指向了
  241. // 非数据页, 空闲链上也挂着仍被索引引用的页
  242. // (analyzer 报 not-data-page / freeLive, 属数据损坏前兆)。
  243. newRoot, _, derr := btree.BTreePKDelete(rootPID, indexKey, handle)
  244. if derr != nil && derr != page.ErrNotFound {
  245. return derr
  246. }
  247. if newRoot != rootPID {
  248. *entryPage = newRoot
  249. }
  250. if dp.AllDeleted() {
  251. // 延迟还链: 页仍被 B+ 树叶条目引用 (回滚/崩溃重放后树会
  252. // 复活并继续指向它), 立即还链会让它被复用后读到别人的页
  253. // (数据损坏)。等 Commit 成功 (或 Close) 后由 pendingFree 统一还。
  254. handle.DeferFreeDataPage(dpID)
  255. } else {
  256. dp.Save()
  257. }
  258. return nil
  259. }
  260. }
  261. }
  262. // 路由失败 (树结构异常), 扫描所有叶子
  263. newRoot, err := btree.ScanAndDelete(rootPID, handle, indexKey, indexKey)
  264. if err == nil && newRoot != rootPID {
  265. *entryPage = newRoot // 兜底删除也可能换根 (树删空/降高), 必须回写
  266. }
  267. return err
  268. }
  269. func (h *PrimaryHandler) DeleteByPK(handle page.HandleDb, entryPage *uint32, pkID uint64) error {
  270. rootPID := *entryPage
  271. if rootPID == 0 {
  272. return page.ErrNotFound
  273. }
  274. pkKeyBE := make([]byte, 8)
  275. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  276. dpID, slot, st := btree.BTreePKSearchEx(rootPID, pkKeyBE, handle)
  277. if st == btree.PKMiss {
  278. return page.ErrNotFound // 键不存在
  279. }
  280. if st == btree.PKHit {
  281. dp, err := datapage.Load(handle, dpID)
  282. if err == nil {
  283. deleted := false
  284. if _, ok := dp.ReadRowBySlotPK(uint32(slot), pkKeyBE); ok {
  285. deleted = dp.DeleteBySlot(uint32(slot))
  286. } else {
  287. deleted = dp.DeleteByPK(pkKeyBE)
  288. }
  289. if deleted {
  290. // 同 Delete: 每次成功删除都要摘掉本行的叶条目, 否则 DataPage 回收复用后
  291. // 会留下指向非数据页的陈旧条目 (详见 Delete 处的说明)。
  292. newRoot, _, derr := btree.BTreePKDelete(rootPID, pkKeyBE, handle)
  293. if derr != nil && derr != page.ErrNotFound {
  294. return derr
  295. }
  296. if newRoot != rootPID {
  297. *entryPage = newRoot
  298. }
  299. if dp.AllDeleted() {
  300. // 延迟还链: 页仍被 B+ 树叶条目引用 (回滚/崩溃重放后树会
  301. // 复活并继续指向它), 立即还链会让它被复用后读到别人的页
  302. // (数据损坏)。等 Commit 成功 (或 Close) 后由 pendingFree 统一还。
  303. handle.DeferFreeDataPage(dpID)
  304. } else {
  305. dp.Save()
  306. }
  307. return nil
  308. }
  309. }
  310. }
  311. // 路由失败 (树结构异常), 扫描所有叶子
  312. newRoot, err := btree.ScanAndDeleteByPK(rootPID, handle, pkKeyBE)
  313. if err == nil && newRoot != rootPID {
  314. *entryPage = newRoot // 兜底删除也可能换根, 必须回写
  315. }
  316. return err
  317. }
  318. // Update 更新主键索引中 pkID 对应的行。
  319. //
  320. // schema 演进 (AddField) 后, 旧行的 schemaVer 与行长都会变长, "原地覆盖"不再可行,
  321. // 因此采用三级策略, 保证更新要么真正生效, 要么返回明确错误:
  322. //
  323. // 1. 原地更新 — 新行长 <= 旧行长 (最常见, 零成本)
  324. // 2. 页内重建 — 页内腾挪并顺带回收墓碑, 支持行变长而无需换页
  325. // 3. 换页 — 页内确实装不下时, 把行搬到新 DataPage 并改写 B+ 树叶条目
  326. func (h *PrimaryHandler) Update(handle page.HandleDb, entryPage *uint32, oldKey, newKey, rowData []byte, pkID uint64, keyLen int) error {
  327. rootPID := *entryPage
  328. if rootPID == 0 {
  329. return page.ErrNotFound
  330. }
  331. pkKeyBE := make([]byte, 8)
  332. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  333. // 一次分配构造 [pkID(8B BE) + rowData], 取代 pkBuf+append 的二次分配
  334. storeRowData := make([]byte, 8+len(rowData))
  335. copy(storeRowData, pkKeyBE)
  336. copy(storeRowData[8:], rowData)
  337. // 1/2 级: B+ 树路由定位 DataPage (slot 直接给出该行的槽位号)
  338. dpID, slot, st := btree.BTreePKSearchEx(rootPID, pkKeyBE, handle)
  339. if st == btree.PKMiss {
  340. return page.ErrNotFound // 键不存在, 无行可更新
  341. }
  342. if st == btree.PKHit {
  343. if dp, err := datapage.Load(handle, dpID); err == nil {
  344. if err := dp.UpdateBySlot(uint32(slot), storeRowData); err == nil {
  345. return nil
  346. }
  347. if err := dp.RebuildWithSlot(uint32(slot), storeRowData); err == nil {
  348. return nil
  349. }
  350. // 页内装不下 → 直接换页, 不再做无意义的全树扫描
  351. return h.relocateRow(handle, entryPage, pkKeyBE, storeRowData, dpID)
  352. }
  353. }
  354. // 路由失败: 回退扫描所有叶子 (内部同样含 1/2 级兜底)
  355. if err := btree.ScanAndUpdate(rootPID, handle, pkKeyBE, storeRowData); err == nil {
  356. return nil
  357. }
  358. // 3 级: 目标行位于"已满且需变长"的页上, 定位后换页
  359. if oldDPID, ok := btree.FindDataPageIDByPK(rootPID, handle, pkKeyBE); ok {
  360. return h.relocateRow(handle, entryPage, pkKeyBE, storeRowData, oldDPID)
  361. }
  362. return page.ErrNotFound
  363. }
  364. // relocateRow 把 pkID 对应的行搬到新的 DataPage 承载。
  365. //
  366. // 步骤 (无事务, 顺序上优先保证"索引始终可达该行"):
  367. // 1. 新建 DataPage 并写入新行 (此时尚未被索引引用)
  368. // 2. 摘除该键的旧叶条目, 再插入指向新页的条目
  369. // 3. 旧页软删该行; 若旧页已无活行则直接回收
  370. func (h *PrimaryHandler) relocateRow(handle page.HandleDb, entryPage *uint32, pkKeyBE, storeRowData []byte, oldDPID uint32) error {
  371. newDP := datapage.New(handle)
  372. newSlot := newDP.AppendRowSlot(storeRowData)
  373. if newSlot == 0 {
  374. return page.ErrRowTooLarge
  375. }
  376. newDP.Save()
  377. newDPID := newDP.PageID()
  378. // BTreePKInsert 遇到已存在的键会判为重复而不更新指向, 故必须先摘除旧条目
  379. newRoot, _, err := btree.BTreePKDelete(*entryPage, pkKeyBE, handle)
  380. if err != nil {
  381. if handle.FreePage != nil {
  382. handle.FreePage(newDPID)
  383. }
  384. return err
  385. }
  386. newRoot2, _, _, err := btree.BTreePKInsert(newRoot, pkKeyBE, newDPID, uint16(newSlot), handle)
  387. if err != nil {
  388. *entryPage = newRoot
  389. if handle.FreePage != nil {
  390. handle.FreePage(newDPID)
  391. }
  392. return err
  393. }
  394. *entryPage = newRoot2
  395. // 旧页收尾: 只软删这一行, 其它行仍由各自的叶条目引用
  396. if oldDP, lerr := datapage.Load(handle, oldDPID); lerr == nil {
  397. if oldDP.DeleteByPK(pkKeyBE) {
  398. if oldDP.AllDeleted() {
  399. // 延迟还链, 理由同 Delete 路径
  400. handle.DeferFreeDataPage(oldDPID)
  401. } else {
  402. oldDP.Save()
  403. }
  404. }
  405. }
  406. return nil
  407. }
  408. func (h *PrimaryHandler) GetDataPageIDs(handle page.HandleDb, entryPage uint32) []uint32 {
  409. return btree.GetDataPageIDs(handle, entryPage)
  410. }
  411. func (h *PrimaryHandler) ReadDataPageRows(handle page.HandleDb, dpID uint32) [][]byte {
  412. dp, err := datapage.Load(handle, dpID)
  413. if err != nil {
  414. return nil
  415. }
  416. return dp.GetAllRows()
  417. }
  418. // ReadDataPageLiveCount 返回数据页中未删除的行数 (不物化行)。
  419. // 无条件 COUNT(*) 的快速路径; 通过接口断言暴露, 不强加给所有 IndexHandler。
  420. func (h *PrimaryHandler) ReadDataPageLiveCount(handle page.HandleDb, dpID uint32) int {
  421. dp, err := datapage.Load(handle, dpID)
  422. if err != nil {
  423. return 0
  424. }
  425. return dp.CountLiveRows()
  426. }
  427. func (h *PrimaryHandler) RangeScan(handle page.HandleDb, entryPage uint32,
  428. startKey, endKey []byte, startInclusive, endInclusive bool, reverse bool,
  429. callback func(row []byte) bool) error {
  430. return btree.BTreePKRangeScan(entryPage, handle, startKey, endKey, startInclusive, endInclusive, reverse, callback)
  431. }
  432. // InsertBatch 批量写入行存储 (主键 B+ 树 + 数据页)。
  433. //
  434. // 两段式:
  435. // 1) 数据页驻留灌行 —— 路由到首行归属的叶末数据页, 加载一次后连续写入,
  436. // 装满才落盘换新页 (逐条路径每行 Load+Save 整页, 16KB×2 memcpy/行;
  437. // 批量路径摊到 ~每页一次)。行内仍走 InsertRowSortedSlot 保持"页内按
  438. // pkID 升序"不变式。
  439. // 2) 主键树批量挂条目 —— 收集 (pkID → 数据页, 槽位) 后交给 BTreePKBulkInsert。
  440. //
  441. // rows 必须按 PKID 升序 (Table.InsertRowsWithIDs 保证; 自增批量天然满足)。
  442. // 页内整理 (CompactForInsert) 不做: 批量灌行不依赖旧页腾挪, 装不下直接开新页。
  443. func (h *PrimaryHandler) InsertBatch(handle page.HandleDb, entryPage *uint32, rows []BatchRow) error {
  444. if len(rows) == 0 {
  445. return nil
  446. }
  447. // 数据页: 路由到首行归属叶的末数据页, 驻留复用
  448. rootPID := *entryPage
  449. var dp *datapage.DataPage
  450. if rootPID != 0 {
  451. firstKey := make([]byte, 8)
  452. binary.BigEndian.PutUint64(firstKey, rows[0].PKID)
  453. if dpID, ok := btree.LastLeafDPID(handle, rootPID, firstKey); ok {
  454. if d, err := datapage.Load(handle, dpID); err == nil {
  455. dp = d
  456. }
  457. }
  458. }
  459. items := make([]btree.PKBulkItem, len(rows))
  460. for i := range rows {
  461. r := &rows[i]
  462. // [pkID(8B BE) + rowData]: 前 8 字节同时作为 B+ 树 key (与 Insert 一致)
  463. storeRow := make([]byte, 8+len(r.RowData))
  464. binary.BigEndian.PutUint64(storeRow[:8], r.PKID)
  465. copy(storeRow[8:], r.RowData)
  466. if dp != nil {
  467. if slot, ok := dp.InsertRowSortedSlot(storeRow); ok {
  468. items[i] = btree.PKBulkItem{DPID: dp.PageID(), Slot: uint16(slot)}
  469. binary.BigEndian.PutUint64(items[i].Key[:], r.PKID)
  470. continue
  471. }
  472. dp.Save()
  473. dp.Release()
  474. dp = nil
  475. }
  476. dp = datapage.New(handle)
  477. slot := dp.AppendRowSlot(storeRow)
  478. if slot == 0 {
  479. dp.Save()
  480. dp.Release()
  481. return page.ErrRowTooLarge
  482. }
  483. items[i] = btree.PKBulkItem{DPID: dp.PageID(), Slot: uint16(slot)}
  484. binary.BigEndian.PutUint64(items[i].Key[:], r.PKID)
  485. }
  486. if dp != nil {
  487. dp.Save()
  488. dp.Release()
  489. }
  490. newRoot, err := btree.BTreePKBulkInsert(rootPID, items, handle)
  491. if err != nil {
  492. return err
  493. }
  494. if newRoot != *entryPage {
  495. *entryPage = newRoot
  496. }
  497. return nil
  498. }