IndexMetaPage.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. package core
  2. import (
  3. "TsDB2/DB/core/index"
  4. "TsDB2/DB/core/page"
  5. "TsDB2/DB/core/types"
  6. "TsDB2/DB/core/util"
  7. "bytes"
  8. "encoding/binary"
  9. "fmt"
  10. "sort"
  11. "unsafe"
  12. )
  13. // 🔥 最终对接版:只存索引定义 + 指向你的 IndexPage
  14. type IndexMeta struct {
  15. IndexName [IndexNameSize]byte
  16. IndexType page.IndexType
  17. FieldCnt byte // 列的数量
  18. FieldIDs [16]byte // 列
  19. EntryPage uint32 // ✅ 你的 IndexPage 首页ID
  20. PrevMetaID uint32 // 上一个
  21. NextMetaID uint32 // 下一个
  22. Flag byte // 标志位 (见 IndexFlag* 常量)
  23. }
  24. // ===== IndexMeta.Flag 位布局 =====
  25. //
  26. // bit7 | bit0
  27. // 已退位(软删除) | 唯一约束
  28. //
  29. // 位 7 的语义由 util.IsHighBitSet 统一判定 (Deleted); 位 0 是本轮新增的唯一约束标记。
  30. const (
  31. // IndexFlagUnique 该索引是唯一索引: 同一索引键只允许存在一行。
  32. //
  33. // 【NULL 免除】复合唯一索引只要有一个分量是 NULL, extractIndexKey 就返回 nil,
  34. // 该行根本不进这个索引 —— 索引里没有条目, 唯一性自然无从违反。因此
  35. // "唯一但可空"的列可以存任意多行 NULL, 与 SQL 的 UNIQUE 语义一致。
  36. IndexFlagUnique byte = 1 << 0
  37. )
  38. // 编译期断言: 钉死 IndexMeta 的盘上大小与字段偏移。
  39. // analyzer/main.go 里那套硬编码偏移 (META_SZ / im[...]) 必须与之一致,
  40. // 因此改布局时这里会先编译失败, 而不是让分析工具静默读错。
  41. //
  42. // 布局 (IndexNameSize=64) = 100 字节:
  43. //
  44. // IndexName[64] + IndexType(1) + FieldCnt(1) + FieldIDs[16]
  45. // + pad(2) + EntryPage(4) + PrevMetaID(4) + NextMetaID(4) + Flag(1) + pad(3)
  46. var (
  47. _ [unsafe.Sizeof(IndexMeta{}) - 100]struct{}
  48. _ [100 - unsafe.Sizeof(IndexMeta{})]struct{}
  49. _ [64]byte = [unsafe.Offsetof(IndexMeta{}.IndexType)]byte{}
  50. _ [65]byte = [unsafe.Offsetof(IndexMeta{}.FieldCnt)]byte{}
  51. _ [66]byte = [unsafe.Offsetof(IndexMeta{}.FieldIDs)]byte{}
  52. _ [84]byte = [unsafe.Offsetof(IndexMeta{}.EntryPage)]byte{}
  53. _ [96]byte = [unsafe.Offsetof(IndexMeta{}.Flag)]byte{}
  54. )
  55. type IndexMetaPage struct {
  56. hdr page.PageHdr
  57. metas []IndexMeta
  58. handle page.HandleDb
  59. hdrSize uint32
  60. metaSize uint32
  61. lastPageID uint32
  62. rowCache []index.DPRange // 行存储索引回表定位缓存 (由处理器复用, 变更时失效)
  63. // active 活跃索引缓存 (元信息 + 已查表的处理器), 惰性构建。
  64. // 热路径 (InsertRow/DeleteByKey/UpdateByKey/ResolveByPK) 每行都要遍历索引,
  65. // 缓存后省去每行每索引的 Flag 判断与 handler 查表; 索引增删/重载时失效。
  66. active []resolvedIndex
  67. activeResolved bool
  68. rowIdx int // active 中行存储索引的下标, -1 表示无
  69. }
  70. // resolvedIndex 已解析的活跃索引: 元信息 + 已查表的处理器
  71. type resolvedIndex struct {
  72. meta *IndexMeta
  73. handler index.IndexHandler
  74. }
  75. // 初始化
  76. func (im *IndexMetaPage) Init(pageID uint32) {
  77. im.hdrSize = uint32(unsafe.Sizeof(page.PageHdr{}))
  78. im.metaSize = uint32(unsafe.Sizeof(IndexMeta{}))
  79. im.hdr.PageID = pageID
  80. im.hdr.Type = page.IndexMetaIndex
  81. im.hdr.NextPage = 0
  82. im.lastPageID = pageID
  83. im.metas = im.loadMetas(pageID)
  84. im.invalidateIndexCache()
  85. }
  86. // InitEmpty 把本页初始化成"空的索引元数据页", 专供「刚 NewPage 出来的新页」使用。
  87. //
  88. // 为什么不能直接用 Init —— 调用顺序错了就是 DROP TABLE 假死 52s 的根源:
  89. // - Init 是"打开一张已有页": 它用 loadMetas 从盘上回读本页内容来重建 metas。
  90. // 而新页是从空闲链摘出来的, 回收链 (WAL 回收 / Drop 回收) 都只做 O(1) 拼接、
  91. // 不擦除页内容, 盘上还留着它上一世的东西 —— 典型是 WAL 页 (Type=7、
  92. // UseSize=16384、NextPage 还指向旧 WAL 链)。此时 Init 会把整页旧内容当作
  93. // IndexMeta 数组解析, 还顺着旧链多解析好几页: 实测建 G 表时解出 91 条垃圾元数据
  94. // (IndexType=66/71/76…, EntryPage=16777216)。随后即便用 InitHeader 覆盖成干净页,
  95. // 内存里的垃圾 meta 也不会消失, 最终被 DropTable 拿去枚举页 → FreePage 落到从未
  96. // 分配过的页号上 → 写到几十 GB 偏移, DROP 假死。
  97. // - InitEmpty 不读盘: 先写一张只含页头的干净页覆盖旧内容, 再把内存状态置空。
  98. func (im *IndexMetaPage) InitEmpty(pageID uint32) {
  99. im.hdrSize = uint32(unsafe.Sizeof(page.PageHdr{}))
  100. im.metaSize = uint32(unsafe.Sizeof(IndexMeta{}))
  101. im.hdr.Type = page.IndexMetaIndex
  102. im.hdr.PageID = pageID
  103. im.hdr.NextPage = 0
  104. im.hdr.UseSize = im.hdrSize
  105. im.lastPageID = pageID
  106. im.InitHeader(pageID) // 干净页覆盖回收页里的旧内容
  107. im.metas = nil
  108. im.invalidateIndexCache()
  109. }
  110. // loadMetas 解析索引元数据链 (Init / Reload 共用的读取部分)
  111. func (im *IndexMetaPage) loadMetas(pageID uint32) []IndexMeta {
  112. metas := make([]IndexMeta, 0)
  113. for pageID != 0 {
  114. im.lastPageID = pageID
  115. buf := make([]byte, PageSize)
  116. if err := im.handle.ReadPage(pageID, buf); err != nil {
  117. break
  118. }
  119. hdr := page.HdrOf(buf)
  120. // 页类型 / 自述页号校验: 读索引元数据链绝不能"盘上有多少就解析多少"。
  121. // 新页可能刚从空闲链摘出来, 盘上留着上一世的内容 (典型: WAL 页 Type=7、
  122. // UseSize=16384、NextPage 指向旧 WAL 链)。不校验就会把整页旧内容按 IndexMeta
  123. // 数组解析, 还顺着旧链一路解析下去 —— 实测建表时解出 91 条垃圾元数据
  124. // (EntryPage=16777216), 最终让 DropTable 去 FreePage 从未分配过的页号:
  125. // 写到几十 GB 偏移、DROP 假死 52s、稀疏文件暴涨到 40GB。
  126. if hdr.Type != page.IndexMetaIndex || hdr.PageID != pageID {
  127. break
  128. }
  129. offset := im.hdrSize
  130. for offset+im.metaSize <= hdr.UseSize {
  131. m := (*IndexMeta)(unsafe.Pointer(&buf[offset]))
  132. if m.Flag&0x80 == 0 {
  133. metas = append(metas, *m)
  134. }
  135. offset += im.metaSize
  136. }
  137. pageID = hdr.NextPage
  138. }
  139. return metas
  140. }
  141. // Reload 从磁盘重新读回索引元数据, 原地覆盖 im.metas —— 供事务 Rollback 使用。
  142. //
  143. // metas 是 Init 时从页里**拷贝**到 Go 堆的独立副本, 丢弃页缓存并不会刷新它。
  144. // 不重载的话 EntryPage 仍停留在事务中的值 (指向事务里新建的 B+ 树根, 删空时还会被
  145. // 写成 0), 而那个根页已随缓存一起被丢弃 → 回滚后所有索引查找都走空。
  146. //
  147. // firstPageID 必须由调用方从"刚从磁盘读回的 TableInfo.FirstIndexID"传入:
  148. // im.hdr.PageID 同样是内存副本, 事务内首次建索引时可能已被改写。
  149. func (im *IndexMetaPage) Reload(firstPageID uint32) {
  150. im.hdr.PageID = firstPageID
  151. im.hdr.NextPage = 0
  152. im.lastPageID = firstPageID
  153. im.rowCache = nil // 回表定位缓存指向事务中的 DataPage, 必须失效
  154. metas := im.loadMetas(firstPageID)
  155. if len(metas) == len(im.metas) {
  156. copy(im.metas, metas) // 长度一致时原地覆盖, 外部持有的 &metas[i] 继续有效
  157. } else {
  158. im.metas = metas
  159. }
  160. im.invalidateIndexCache() // 元信息已变, 缓存里的 &metas[i] 与 handler 必须重算
  161. }
  162. // PageID 返回索引元数据页 ID (调试用)
  163. func (im *IndexMetaPage) PageID() uint32 { return im.hdr.PageID }
  164. // HdrSize 返回页头大小 (调试用)
  165. func (im *IndexMetaPage) HdrSize() uint32 { return im.hdrSize }
  166. // 创建索引(纯元数据,不碰IndexPage)
  167. //
  168. // unique 是变参 (只取第一个值): 这样 `CreateIndex(name, typ, fields)` 的既有调用
  169. // 一处都不用改, 需要唯一约束的调用方写成 `CreateIndex(name, typ, fields, true)` 即可。
  170. func (im *IndexMetaPage) CreateIndex(indexName string, itype page.IndexType, fieldIDs []byte, unique ...bool) (*IndexMeta, error) {
  171. uniqueIdx := len(unique) > 0 && unique[0]
  172. // 名字超长必须显式报错。下面用 copy 写入定长数组, 超长会被静默截断: 存进盘里的
  173. // 名字与调用方用来查询的名字不一致, 索引建了却永远查不到 —— 必须拦在建的这一步。
  174. // (自动命名 pk_<表名> / idx_<表名>_<列名> 会额外多出几个字节, 所以名字接近上限时
  175. // 这里就会报错, 而不是默默建出一个查不到的索引。
  176. // 上限取 IndexNameSize 而非 TableNameSize —— 索引名与表名是两套独立额度。)
  177. if len(indexName) > IndexNameSize {
  178. return nil, fmt.Errorf("%w: 索引名 %q 有 %d 字节, 上限 %d",
  179. ErrIndexNameTooLong, indexName, len(indexName), IndexNameSize)
  180. }
  181. // 分量个数闸门。先按 int 判再转 byte: 直接 `byte(len(fieldIDs))` 会在超过 255 时回绕
  182. // (例如 257 个份量截成 1), 于是越界的调用反而"通过"了, 之后按错误的分量数建条目。
  183. if len(fieldIDs) == 0 || len(fieldIDs) > MaxIndexFields {
  184. return nil, fmt.Errorf("%w (本次 %d 个分量, 索引 %q)",
  185. ErrIndexFieldCount, len(fieldIDs), indexName)
  186. }
  187. fc := byte(len(fieldIDs))
  188. // 复合键要求各分量互不相同。重复列会让 extractIndexKey 永远取不齐分量
  189. // (某一分量无人填充), 索引静默变成空索引 —— 写入成功却查不到, 因此直接拒绝。
  190. var seen [256]bool
  191. for _, fid := range fieldIDs {
  192. if seen[fid] {
  193. return nil, ErrIndexFieldDuplicated
  194. }
  195. seen[fid] = true
  196. }
  197. // 行存储索引 (主键 B+ 树 / 时序) 的键同时就是"行在哪里"的定位键,
  198. // 复合键会让回表语义失效 (ResolveByPK 用 pkID, 与键是两回事), 明确拒绝。
  199. if fc > 1 {
  200. if h, ok := index.Compose.GetHandler(itype); ok && h.IsRowStore() {
  201. return nil, ErrIndexCompositeRowStore
  202. }
  203. }
  204. // 行存储索引不支持唯一约束, 明确拒绝而不是静默忽略:
  205. // - 主键索引本就唯一 (InsertRowWithID 用 ErrDuplicatePK 兜着), 唯一标记是废话;
  206. // - 时序索引的多行同刻是【刻意允许】的 (同一时刻的多个测点), 给它加唯一约束
  207. // 等于把时序表写死 —— 而"忽略这个标记"会让调用方以为约束生效了,
  208. // 之后插入同刻第二行成功时才发觉不对, 那时数据已经写进去了。
  209. if uniqueIdx {
  210. if h, ok := index.Compose.GetHandler(itype); ok && h.IsRowStore() {
  211. return nil, fmt.Errorf("%w (索引 %q)", ErrUniqueRowStore, indexName)
  212. }
  213. }
  214. // 一张表只能有一个行存储索引。行数据 ([pkID + rowData]) 只可能落在其中一个里, 而
  215. // resolveIndexes 取的是"第一个 IsRowStore 的索引"; 再建一个不会立刻出错, 只会得到一个
  216. // "元数据说自己是行存储、却一行都没有"的索引 —— 之后所有按行存储解读它的路径
  217. // (回表、删除、重建) 都会错乱, 而且错得没有来路。所以拦在建的这一步。
  218. //
  219. // 主键 (B+ 树) 与时序都属于行存储, 因此"建了主键表再补时序索引"同样被拒 —— 这不是
  220. // 额外限制, 而是引擎的真实约束: 一份行数据不可能同时按 pkID 和按时间分段存放。
  221. if h, ok := index.Compose.GetHandler(itype); ok && h.IsRowStore() {
  222. for i := range im.metas {
  223. m := &im.metas[i]
  224. if m.Flag&0x80 != 0 { // 已退位的元数据不占位
  225. continue
  226. }
  227. if indexStoresRows(m) {
  228. return nil, fmt.Errorf("%w: 表上已有行存储索引 %q (类型 %d), 不能再建 %q (类型 %d)",
  229. ErrRowStoreExists, util.BytesToString(m.IndexName[:]), m.IndexType, indexName, itype)
  230. }
  231. }
  232. }
  233. for _, m := range im.metas {
  234. if util.BytesToString(m.IndexName[:]) == indexName && m.Flag&0x80 == 0 {
  235. return nil, ErrIndexExists
  236. }
  237. }
  238. var m IndexMeta
  239. copy(m.IndexName[:], indexName)
  240. m.IndexType = itype
  241. m.FieldCnt = fc
  242. copy(m.FieldIDs[:fc], fieldIDs)
  243. m.Flag = 0
  244. m.SetUnique(uniqueIdx) // bit7 是软删除 (Deleted), 新索引一律不带; 唯一位按参数置
  245. m.EntryPage = 0
  246. var buf []byte
  247. var hdr *page.PageHdr
  248. if im.hdr.PageID == 0 {
  249. pid := im.handle.NewPage()
  250. buf, hdr = im.InitHeader(pid)
  251. im.hdr.PageID = pid
  252. im.lastPageID = pid
  253. } else {
  254. buf = make([]byte, PageSize)
  255. im.handle.ReadPage(im.lastPageID, buf)
  256. hdr = page.HdrOf(buf)
  257. }
  258. offset := hdr.UseSize
  259. if offset+im.metaSize > PageSize {
  260. newID := im.handle.NewPage()
  261. newBuf, newHdr := im.InitHeader(newID)
  262. hdr.NextPage = newID
  263. im.handle.SavePage(im.lastPageID, buf) // 保存下当前页 不然就被新开的页替换了
  264. im.lastPageID = newID
  265. buf = newBuf
  266. hdr = newHdr
  267. offset = im.hdrSize
  268. }
  269. dst := (*IndexMeta)(unsafe.Pointer(&buf[offset]))
  270. *dst = m
  271. hdr.UseSize = offset + im.metaSize
  272. im.handle.SavePage(im.lastPageID, buf)
  273. im.metas = append(im.metas, m)
  274. im.invalidateIndexCache() // metas 扩容后旧缓存里的 &metas[i] 会失效
  275. // 必须返回 im.metas 里的那一份, 而不是局部变量 m 的地址:
  276. // 调用方 (Table.CreateIndex → backfillIndex) 会在建完树后回写 EntryPage。
  277. // 若返回 &m, 回写只落在局部副本 + 磁盘页上, 而查询走的是 im.metas ——
  278. // 于是"回填了存量行却查不到", 直到关库重开 (metas 从盘上重载) 才恢复正常。
  279. // 回填过程中不会 append 新 meta, 故该指针在本次调用内保持有效。
  280. return &im.metas[len(im.metas)-1], nil
  281. }
  282. // 查找单个索引
  283. func (im *IndexMetaPage) FindIndex(idxName string) (*IndexMeta, bool) {
  284. for i := range im.metas {
  285. m := &im.metas[i]
  286. if util.BytesToString(m.IndexName[:]) == idxName && m.Flag&0x80 == 0 {
  287. return m, true
  288. }
  289. }
  290. return nil, false
  291. }
  292. // 删除索引
  293. func (im *IndexMetaPage) DropIndex(idxName string) error {
  294. cur := im.hdr.PageID
  295. for cur != 0 {
  296. buf := make([]byte, PageSize)
  297. im.handle.ReadPage(cur, buf)
  298. hdr := page.HdrOf(buf)
  299. // 同 loadMetas: 页类型/自述页号对不上就不是索引元数据页, 不能在这上面按名字改 Flag
  300. if hdr.Type != page.IndexMetaIndex || hdr.PageID != cur {
  301. break
  302. }
  303. offset := im.hdrSize
  304. for offset+im.metaSize <= hdr.UseSize {
  305. m := (*IndexMeta)(unsafe.Pointer(&buf[offset]))
  306. if util.BytesToString(m.IndexName[:]) == idxName && m.Flag&0x80 == 0 {
  307. m.Flag |= 0x80
  308. im.handle.SavePage(cur, buf)
  309. for i := range im.metas {
  310. if util.BytesToString(im.metas[i].IndexName[:]) == idxName {
  311. im.metas = append(im.metas[:i], im.metas[i+1:]...)
  312. im.invalidateIndexCache() // metas 已删除元素, 旧缓存 (含下标) 失效
  313. break
  314. }
  315. }
  316. return nil
  317. }
  318. offset += im.metaSize
  319. }
  320. cur = hdr.NextPage
  321. }
  322. return ErrIndexNotFound
  323. }
  324. func (im *IndexMetaPage) InitHeader(pageID uint32) ([]byte, *page.PageHdr) {
  325. buf := make([]byte, PageSize)
  326. hdr := page.HdrOf(buf)
  327. hdr.Type = page.IndexMetaIndex
  328. hdr.PageID = pageID
  329. hdr.UseSize = uint32(unsafe.Sizeof(*hdr))
  330. im.handle.SavePage(pageID, buf)
  331. return buf, hdr
  332. }
  333. // ======================== 高层索引操作 ========================
  334. // rowStoreMeta 返回承载行数据的一级索引 (主键/时序)。
  335. // 本层不认识索引类型枚举: 谁是"行存储"由处理器自己通过 IsRowStore 回答。
  336. func (im *IndexMetaPage) rowStoreMeta() (*IndexMeta, index.IndexHandler, bool) {
  337. im.resolveIndexes()
  338. if im.rowIdx < 0 {
  339. return nil, nil, false
  340. }
  341. ri := im.active[im.rowIdx]
  342. return ri.meta, ri.handler, true
  343. }
  344. // Name 索引名 (去掉定长补零)。
  345. func (m *IndexMeta) Name() string {
  346. if m == nil {
  347. return ""
  348. }
  349. return util.BytesToString(m.IndexName[:])
  350. }
  351. // Fields 索引分量在表 master registry 中的字段下标 (按建索引时给的顺序)。
  352. // 返回拷贝: 调用方不该拿到会随元数据增删而失效的内存。
  353. func (m *IndexMeta) Fields() []byte {
  354. if m == nil || m.FieldCnt == 0 {
  355. return nil
  356. }
  357. n := int(m.FieldCnt)
  358. if n > len(m.FieldIDs) {
  359. n = len(m.FieldIDs)
  360. }
  361. out := make([]byte, n)
  362. copy(out, m.FieldIDs[:n])
  363. return out
  364. }
  365. // Deleted 索引是否已被软删除 (Flag 最高位)。
  366. func (m *IndexMeta) Deleted() bool {
  367. return m != nil && util.IsHighBitSet(m.Flag)
  368. }
  369. // Unique 该索引是否为唯一索引 (同一索引键只允许一行; NULL 分量不进索引, 不参与约束)。
  370. //
  371. // 这是唯一约束的【权威标记】: 列上的 types.FlagUnique 只是声明, 约束由索引在
  372. // 写入路径上执行 (插入/更新前先按键探一次), 所以判定必须以索引元信息为准。
  373. func (m *IndexMeta) Unique() bool {
  374. return m != nil && m.Flag&IndexFlagUnique != 0
  375. }
  376. // SetUnique 就地设置/取消唯一标记 (仅改内存元信息, 落盘由 saveMeta 负责)。
  377. func (m *IndexMeta) SetUnique(v bool) {
  378. if m == nil {
  379. return
  380. }
  381. if v {
  382. m.Flag |= IndexFlagUnique
  383. } else {
  384. m.Flag &^= IndexFlagUnique
  385. }
  386. }
  387. // GetIndexes 返回本表全部可用索引的元数据拷贝 (按创建顺序, 已删除的跳过)。
  388. //
  389. // 元数据是迁移/导出类工具的入口: 在此之前 core 包外没有任何办法枚举一张表的索引
  390. // (metas 未导出, 只能逐名 FindIndex —— 那要求调用方事先知道所有索引名)。
  391. // 返回拷贝而不是内部指针: metas 会随 CreateIndex/DropIndex 重新分配,
  392. // 外部持有 &metas[i] 只会在下一次建/删索引时变成野指针。
  393. func (im *IndexMetaPage) GetIndexes() []IndexMeta {
  394. im.resolveIndexes()
  395. out := make([]IndexMeta, 0, len(im.active))
  396. for _, ri := range im.active {
  397. out = append(out, *ri.meta)
  398. }
  399. return out
  400. }
  401. // GetRowStoreIndex 返回承载行数据的一级索引 (VRTX 的主键索引 / 时序索引) 的元数据拷贝。
  402. // 第二个返回值为 false 表示这张表没有行存储索引 —— 此时行数据无处安放,
  403. // 调用方应当报错而不是"照常建表".
  404. func (im *IndexMetaPage) GetRowStoreIndex() (IndexMeta, bool) {
  405. m, _, ok := im.rowStoreMeta()
  406. if !ok {
  407. return IndexMeta{}, false
  408. }
  409. return *m, true
  410. }
  411. // resolveIndexes 惰性构建并返回活跃索引缓存 (跳过已删除/无处理器的索引)。
  412. //
  413. // 缓存的 &metas[i] 指针在 metas 发生增删或整体替换后会失效, 因此 CreateIndex /
  414. // DropIndex / Reload / Init 都必须调用 invalidateIndexCache。
  415. func (im *IndexMetaPage) resolveIndexes() []resolvedIndex {
  416. if im.activeResolved {
  417. return im.active
  418. }
  419. im.active = im.active[:0]
  420. im.rowIdx = -1
  421. for i := range im.metas {
  422. m := &im.metas[i]
  423. if m.Flag&0x80 != 0 {
  424. continue
  425. }
  426. h, ok := index.Compose.GetHandler(m.IndexType)
  427. if !ok {
  428. continue
  429. }
  430. if im.rowIdx < 0 && h.IsRowStore() {
  431. im.rowIdx = len(im.active)
  432. }
  433. im.active = append(im.active, resolvedIndex{meta: m, handler: h})
  434. }
  435. im.activeResolved = true
  436. return im.active
  437. }
  438. // invalidateIndexCache 使活跃索引缓存失效 (元信息增删/重载后必须调用)。
  439. func (im *IndexMetaPage) invalidateIndexCache() {
  440. im.activeResolved = false
  441. im.active = im.active[:0]
  442. im.rowIdx = -1
  443. }
  444. // indexStoresRows 询问索引处理器: 该索引是否承载行数据。
  445. // 调用方据此判断"行存在哪里", 不需要认识具体索引类型 (主键/时序 由处理器自报)。
  446. func indexStoresRows(m *IndexMeta) bool {
  447. if m == nil {
  448. return false
  449. }
  450. h, ok := index.Compose.GetHandler(m.IndexType)
  451. return ok && h.IsRowStore()
  452. }
  453. // resolver 提供给索引处理器的回表函数: 二级索引内部据此"去找一级索引"取整行,
  454. // 上层无需参与, 也无需知道哪个索引才是主键。
  455. func (im *IndexMetaPage) resolver() index.RowResolver {
  456. return func(pkID uint64) ([]byte, bool) { return im.ResolveByPK(pkID) }
  457. }
  458. // findIndexMetaByName 按索引名查找活跃索引的元信息 (跳过已删除)。
  459. func (im *IndexMetaPage) findIndexMetaByName(name string) *IndexMeta {
  460. for i := range im.metas {
  461. if util.BytesToString(im.metas[i].IndexName[:]) == name && im.metas[i].Flag&0x80 == 0 {
  462. return &im.metas[i]
  463. }
  464. }
  465. return nil
  466. }
  467. func (im *IndexMetaPage) saveMeta(m *IndexMeta) {
  468. cur := im.hdr.PageID
  469. for cur != 0 {
  470. buf := make([]byte, PageSize)
  471. im.handle.ReadPage(cur, buf)
  472. hdr := page.HdrOf(buf)
  473. // 同 loadMetas: 页类型/自述页号对不上就不是索引元数据页, 不能在这上面按名字覆盖元数据
  474. if hdr.Type != page.IndexMetaIndex || hdr.PageID != cur {
  475. break
  476. }
  477. offset := im.hdrSize
  478. for offset+im.metaSize <= hdr.UseSize {
  479. dst := (*IndexMeta)(unsafe.Pointer(&buf[offset]))
  480. if util.BytesToString(dst.IndexName[:]) == util.BytesToString(m.IndexName[:]) {
  481. *dst = *m
  482. im.handle.SavePage(cur, buf)
  483. return
  484. }
  485. offset += im.metaSize
  486. }
  487. cur = hdr.NextPage
  488. }
  489. }
  490. // extractIndexKey 从行数据中提取索引键 (版本感知)
  491. // rowData 格式 (v9): [schemaVer 1B][列0 W0 B][列结束偏移表 2N][列1..N-1 内容][NULL 位图]
  492. //
  493. // 复合索引的键 = 按 FieldIDs 顺序, 把各分量字段的原始字节【补齐到档位宽度】后定长拼接。
  494. // - 顺序取 FieldIDs 而非行内物理顺序 —— 查询侧据此才能做"列前缀"范围推导:
  495. // (a, b) 上的索引既能服务 a=?, 也能服务 a=? AND b>?;
  496. // - 每个分量长度 = FieldSize(字段类型, 列级参数), 与查询侧编键完全一致, 因此拼接键是
  497. // 定长的, 字节序比较等价于"先比 a 再比 b"的字典序。宽度必须带列级参数 (字符串列的
  498. // 宽度码): 按类型默认档切键会把宽字符串列切成半截, 后面那个分量整体读错位;
  499. // - v9 起行内字符串列按【实际长度】存储, 取出的字节要 PadToWidth 补零到档位宽度 ——
  500. // 索引键必须保持定长, 二级索引布局 [key][PKID8] 与 idxKeyLenOf 才不会变形;
  501. // - 任一分量在该行版本里取不到 (列被 Drop / schema 版本不匹配) → 返回 nil,
  502. // 该索引跳过这一行, 与单列索引"取不到键就跳过"的既有语义一致。
  503. func extractIndexKey(rowData []byte, meta *IndexMeta, fields []types.FieldInfo, activeVer byte, versions []SchemaVersion) []byte {
  504. if meta == nil || meta.FieldCnt == 0 || len(rowData) < 1 {
  505. return nil
  506. }
  507. n := int(meta.FieldCnt)
  508. if n > len(meta.FieldIDs) {
  509. return nil
  510. }
  511. // 从 rowData 读取该行的 schema 版本
  512. ver := rowData[0]
  513. var sv *SchemaVersion
  514. for i := range versions {
  515. if versions[i].Version == ver {
  516. sv = &versions[i]
  517. break
  518. }
  519. }
  520. if sv == nil {
  521. return nil
  522. }
  523. rowN := len(sv.FieldIndices)
  524. if rowN == 0 {
  525. return nil
  526. }
  527. // 列位置由行内偏移表给出 —— v9 字符串列变长, 旧实现"从偏移 1 起累加档位宽度"必然错位。
  528. col0Width := 0
  529. if idx := int(sv.FieldIndices[0]); idx >= 0 && idx < len(fields) {
  530. col0Width = int(fields[idx].Width())
  531. }
  532. if col0Width <= 0 {
  533. return nil
  534. }
  535. ends, ok := types.RowParseOffsets(rowData, rowN, col0Width)
  536. if !ok {
  537. return nil
  538. }
  539. // seg[k] 指向拼键用的定长字节切片 (补齐后的副本或原位切片), 全部取齐后再一次性拼键。
  540. var (
  541. seg [16][]byte
  542. need = n
  543. )
  544. for i, idx := range sv.FieldIndices {
  545. fi := int(idx)
  546. if fi >= len(fields) {
  547. return nil
  548. }
  549. size := int(fields[fi].Width())
  550. if size == 0 { // 未知类型/非法列参数: 切不出定长分量, 拼出的键会错位
  551. return nil
  552. }
  553. for k := 0; k < n; k++ {
  554. if int(meta.FieldIDs[k]) != fi {
  555. continue
  556. }
  557. // 分量是 NULL → 该行不进本索引 (键里放不下"缺失"这一档, 上层按 nil 键跳过)。
  558. if types.RowIsNull(rowData, ends, rowN, i) {
  559. return nil
  560. }
  561. start := int(types.RowFieldStart(ends, i))
  562. end := int(types.RowFieldEnd(ends, i, col0Width))
  563. if end < start || end > len(rowData) {
  564. return nil
  565. }
  566. // 行内存的是实际长度, 索引键要的是档位定长: 补零对齐, 与查询侧 EncodeValue 同源。
  567. seg[k] = types.PadToWidth(rowData[start:end], uint16(size))
  568. need--
  569. break
  570. }
  571. if need == 0 {
  572. break // 分量已取齐, 后面的字段与本索引无关
  573. }
  574. }
  575. if need != 0 {
  576. return nil // 有分量不在该版本中 (如该列已被 Drop), 无法构成完整键
  577. }
  578. total := 0
  579. for k := 0; k < n; k++ {
  580. total += len(seg[k])
  581. }
  582. key := make([]byte, 0, total)
  583. for k := 0; k < n; k++ {
  584. key = append(key, seg[k]...)
  585. }
  586. _ = activeVer
  587. return key
  588. }
  589. // InsertRow 插入行到所有索引
  590. func (im *IndexMetaPage) InsertRow(fields []types.FieldInfo, pkID uint64, rowData []byte, activeVer byte, versions []SchemaVersion) error {
  591. im.rowCache = nil // 插入可能创建新 DataPage, 失效缓存
  592. indexes := im.resolveIndexes()
  593. // 唯一约束预检必须先于任何写入: 逐个索引边查边写的话, 撞车时前面几个索引
  594. // (含承载行数据的行存储索引) 已经写进去了, 只剩一个错误返回 —— 库里多出一行
  595. // 半成品, 而且它的行还在、二级条目不全, 之后按任何二级索引都查不到它。
  596. if err := im.checkUniqueForKey(indexes, rowData, pkID, fields, activeVer, versions); err != nil {
  597. return err
  598. }
  599. for _, ri := range indexes {
  600. m := ri.meta
  601. indexKey := extractIndexKey(rowData, m, fields, activeVer, versions)
  602. if indexKey == nil {
  603. continue
  604. }
  605. entryPage := m.EntryPage
  606. _, err := ri.handler.Insert(im.handle, &entryPage, indexKey, pkID, rowData, len(indexKey))
  607. if err != nil {
  608. return err
  609. }
  610. if entryPage != m.EntryPage {
  611. m.EntryPage = entryPage
  612. im.saveMeta(m)
  613. }
  614. }
  615. return nil
  616. }
  617. // InsertRows 批量插入: pkIDs[i] 与 rowDatas[i] 一一对应。
  618. //
  619. // 与单行 InsertRow 的语义差异只在"摊销", 不在"语义":
  620. // - 唯一约束仍是"先全部预检、再动任何写入"—— 每行对既有树的查重照做,
  621. // 另补"批内查重"(单行路径里批内冲突会在第 i+1 行的树查重中被发现, 批量
  622. // 路径一次性排序比出来, 语义等价且更便宜)。
  623. // - 实现了 BatchInserter 的索引 (主键 B+ 树 / 二级 B+ 树) 走批量路径;
  624. // 其余 (时序 / hash / 复合链) 退化为逐条 Insert, 行为与单行路径逐字节一致。
  625. //
  626. // 失败语义与单行路径相同: 中途报错时前面已写入的索引不回滚 (调用方靠事务回滚)。
  627. func (im *IndexMetaPage) InsertRows(fields []types.FieldInfo, pkIDs []uint64, rowDatas [][]byte, activeVer byte, versions []SchemaVersion) error {
  628. if len(rowDatas) == 0 {
  629. return nil
  630. }
  631. im.rowCache = nil // 批量插入必然新建 DataPage, 一次性失效缓存
  632. indexes := im.resolveIndexes()
  633. // 唯一约束预检: 全部查完才动第一笔写入 (与单行路径同一铁律)
  634. hasUnique := false
  635. for _, ri := range indexes {
  636. if ri.meta.Unique() {
  637. hasUnique = true
  638. break
  639. }
  640. }
  641. if hasUnique {
  642. for i := range rowDatas {
  643. if err := im.checkUniqueForKey(indexes, rowDatas[i], pkIDs[i], fields, activeVer, versions); err != nil {
  644. return err
  645. }
  646. }
  647. // 批内查重: 同一唯一索引里两行同键
  648. for _, ri := range indexes {
  649. if !ri.meta.Unique() {
  650. continue
  651. }
  652. keys := make([][]byte, 0, len(rowDatas))
  653. for i := range rowDatas {
  654. if k := extractIndexKey(rowDatas[i], ri.meta, fields, activeVer, versions); k != nil {
  655. keys = append(keys, k)
  656. }
  657. }
  658. sort.Slice(keys, func(a, b int) bool { return bytes.Compare(keys[a], keys[b]) < 0 })
  659. for j := 1; j < len(keys); j++ {
  660. if bytes.Equal(keys[j-1], keys[j]) {
  661. return fmt.Errorf("%w: 索引 %q, 批内重复键",
  662. ErrDuplicatedKey, util.BytesToString(ri.meta.IndexName[:]))
  663. }
  664. }
  665. }
  666. }
  667. for _, ri := range indexes {
  668. m := ri.meta
  669. entryPage := m.EntryPage
  670. var err error
  671. if bi, ok := ri.handler.(index.BatchInserter); ok {
  672. rows := make([]index.BatchRow, len(rowDatas))
  673. for i := range rowDatas {
  674. rows[i] = index.BatchRow{
  675. PKID: pkIDs[i],
  676. IndexKey: extractIndexKey(rowDatas[i], m, fields, activeVer, versions),
  677. RowData: rowDatas[i],
  678. }
  679. }
  680. err = bi.InsertBatch(im.handle, &entryPage, rows)
  681. } else {
  682. for i := range rowDatas {
  683. indexKey := extractIndexKey(rowDatas[i], m, fields, activeVer, versions)
  684. if indexKey == nil {
  685. continue
  686. }
  687. if _, err = ri.handler.Insert(im.handle, &entryPage, indexKey, pkIDs[i], rowDatas[i], len(indexKey)); err != nil {
  688. break
  689. }
  690. }
  691. }
  692. if err != nil {
  693. return err
  694. }
  695. if entryPage != m.EntryPage {
  696. m.EntryPage = entryPage
  697. im.saveMeta(m)
  698. }
  699. }
  700. return nil
  701. }
  702. // CheckUniqueForKey 校验行数据 rowData 是否会撞上某个唯一索引里【别的行】已有的键。
  703. // 冲突返回 ErrDuplicatedKey; 无冲突 (含"该行压根不进这些索引") 返回 nil。
  704. //
  705. // 抽成独立方法是因为它有两个必须分开的调用时机:
  706. // - InsertRow / UpdateByKey 内部各调一次 —— 保证任何入口在写入前都拦得住;
  707. // - Table 的更新路径还要在 serializeRow 之前【再】调一次: serializeRow 会就地
  708. // 覆写旧行的动态堆块, 冲突必须在它动手之前发现 (见 serializeRowForCheck)。
  709. //
  710. // 因此这里只读不写、可重复调用。
  711. func (im *IndexMetaPage) CheckUniqueForKey(rowData []byte, pkID uint64, fields []types.FieldInfo, activeVer byte, versions []SchemaVersion) error {
  712. return im.checkUniqueForKey(im.resolveIndexes(), rowData, pkID, fields, activeVer, versions)
  713. }
  714. // checkUniqueForKey 是 CheckUniqueForKey 的内核, 收【已解析】的索引集。
  715. // InsertRow / UpdateByKey 手里本来就有一份 resolveIndexes 的结果, 再解析一遍等于
  716. // 每次写入都把索引元信息链多走一趟 —— 插入/更新是热路径, 这趟省掉。
  717. func (im *IndexMetaPage) checkUniqueForKey(indexes []resolvedIndex, rowData []byte, pkID uint64, fields []types.FieldInfo, activeVer byte, versions []SchemaVersion) error {
  718. for _, ri := range indexes {
  719. if !ri.meta.Unique() {
  720. continue
  721. }
  722. indexKey := extractIndexKey(rowData, ri.meta, fields, activeVer, versions)
  723. if indexKey == nil {
  724. continue // 任一分量为 NULL → 该行不进本索引, 唯一性对 NULL 不设限
  725. }
  726. if im.uniqueConflict(ri, indexKey, pkID) {
  727. return fmt.Errorf("%w: 索引 %q, pkID=%d",
  728. ErrDuplicatedKey, util.BytesToString(ri.meta.IndexName[:]), pkID)
  729. }
  730. }
  731. return nil
  732. }
  733. // uniqueConflict 判断唯一索引里是否已有"别的行"占着这个键。
  734. //
  735. // 用 resolve=nil 直接拿条目里的 8B pkID: 唯一性只关心"是不是同一行", 不需要回表取整行
  736. // (回表会顺着行存储索引再查一次, 每次插入都多一遍 O(页数) 的定位)。返回的 pkID 与
  737. // 当前行相同说明命中的是本行自己的条目 (更新场景), 不算冲突。
  738. func (im *IndexMetaPage) uniqueConflict(ri resolvedIndex, indexKey []byte, pkID uint64) bool {
  739. got, found := ri.handler.Search(im.handle, ri.meta.EntryPage, indexKey, len(indexKey), nil)
  740. if !found || len(got) < 8 {
  741. return false
  742. }
  743. return binary.BigEndian.Uint64(got[:8]) != pkID
  744. }
  745. // searchAt 在已定位的索引元信息上查一行 (统一契约: 返回整行, 带 8B pkID 前缀)。
  746. //
  747. // 本层不判断索引类型: 二级索引所缺的行数据由处理器内部用回表函数补齐。
  748. // 独立抽出是为了让"先定位、再复用"的调用方 (Table.SearchByIndex/DeleteRow/UpdateRow)
  749. // 只做一次树查找, 不必重复「findIndexMetaByName + handler.Search」。
  750. func (im *IndexMetaPage) searchAt(meta *IndexMeta, indexKey []byte) ([]byte, bool) {
  751. if meta == nil {
  752. return nil, false
  753. }
  754. handler, ok := index.Compose.GetHandler(meta.IndexType)
  755. if !ok {
  756. return nil, false
  757. }
  758. return handler.Search(im.handle, meta.EntryPage, indexKey, len(indexKey), im.resolver())
  759. }
  760. // SearchByIndex 通过索引搜索 (统一契约: 返回整行, 带 8B pkID 前缀)
  761. func (im *IndexMetaPage) SearchByIndex(idxName string, indexKey []byte) ([]byte, bool) {
  762. return im.searchAt(im.findIndexMetaByName(idxName), indexKey)
  763. }
  764. // ResolveByPK 按 pkID 查找整行数据 (带 8B pkID 前缀)。
  765. //
  766. // 本层不知道行数据"存在哪种索引里": 由声明 IsRowStore 的处理器自己实现读取
  767. // (主键 B+ 树走 DataPage 二分缓存, 时序索引走条目链)。
  768. func (im *IndexMetaPage) ResolveByPK(pkID uint64) ([]byte, bool) {
  769. m, h, ok := im.rowStoreMeta()
  770. if !ok {
  771. return nil, false
  772. }
  773. return h.ResolveRow(im.handle, m.EntryPage, pkID, &im.rowCache)
  774. }
  775. // InvalidateRowCache 使回表定位缓存失效 (外部直接改动行存储索引后调用)
  776. func (im *IndexMetaPage) InvalidateRowCache() { im.rowCache = nil }
  777. // rowStoreHasPK 判断行存储里是否已存在该 pkID —— 不建立/复用回表定位缓存。
  778. //
  779. // 插入查重专用: ResolveByPK 会按需构建 im.rowCache (O(页数)), 而任何一次插入
  780. // 都会让它失效; 若每行插入都走 ResolveByPK, 逐行插入就退化成 O(N²)
  781. // (千万级写入从分钟级变成不可完成)。这里传 cache=nil 走处理器自己的定位路径。
  782. //
  783. // 语义与 ResolveByPK 一致 (仅看行是否存在, 且过滤墓碑页), 只是不做缓存。
  784. func (im *IndexMetaPage) rowStoreHasPK(pkID uint64) bool {
  785. m, h, ok := im.rowStoreMeta()
  786. if !ok || m == nil {
  787. return false
  788. }
  789. _, found := h.ResolveRow(im.handle, m.EntryPage, pkID, nil)
  790. return found
  791. }
  792. // DeleteByKey 通过索引删除一行 —— 统一入口, 不区分索引类型。
  793. //
  794. // storeRow 由调用方 SearchByIndex 预先定位后传入 (头 8 字节为 pkID), 本方法不再
  795. // 重复树查找 —— 否则删除路径会对同一行做两次 handler.Search。
  796. //
  797. // 步骤:
  798. // 1. 复用调用方已定位的整行 (二级索引所缺的行数据已由处理器内部回表补齐)。
  799. // 2. 遍历全部索引, 让每个处理器各自删掉该行在本索引中的条目,
  800. // pkID 一并传入以便二级索引在重复 key 下精确定位。
  801. // 主键 <-> 二级索引的级联因此天然成立, 无需在本层判断索引类型。
  802. func (im *IndexMetaPage) DeleteByKey(idxName string, indexKey []byte, storeRow []byte, fields []types.FieldInfo, activeVer byte, versions []SchemaVersion) error {
  803. im.rowCache = nil // 删除可能释放 DataPage, 失效缓存
  804. meta := im.findIndexMetaByName(idxName)
  805. if meta == nil || len(storeRow) < 8 {
  806. return page.ErrNotFound
  807. }
  808. pkID := binary.BigEndian.Uint64(storeRow[:8])
  809. rowData := storeRow[8:]
  810. // 每个索引各自删除该行在本索引中的条目
  811. var firstErr error
  812. for _, ri := range im.resolveIndexes() {
  813. m := ri.meta
  814. key := extractIndexKey(rowData, m, fields, activeVer, versions)
  815. if len(key) == 0 {
  816. if m != meta {
  817. continue // 取不到行数据, 无法推导该索引键, 跳过
  818. }
  819. key = indexKey // 目标索引: 调用方给的键就是它在索引里的键
  820. }
  821. entryPage := m.EntryPage
  822. if err := ri.handler.Delete(im.handle, &entryPage, key, pkID, len(key)); err != nil {
  823. if err != page.ErrNotFound && firstErr == nil {
  824. firstErr = err
  825. }
  826. }
  827. if entryPage != m.EntryPage {
  828. m.EntryPage = entryPage
  829. im.saveMeta(m)
  830. }
  831. }
  832. return firstErr
  833. }
  834. // UpdateByKey 通过索引更新一行 —— 统一入口, 不区分索引类型。
  835. //
  836. // oldStoreRow 由调用方 SearchByIndex 预先定位后传入 (头 8 字节为 pkID), 本方法不再
  837. // 重复回表 —— 否则更新路径会先 ResolveByPK, 失败还会再 SearchByIndex。
  838. //
  839. // 每个索引各自处理自己条目的更新:
  840. // - 行存储索引(主键/时序): 就地改写行数据;
  841. // - 二级索引: 只有 [key + pkID], 键未变则无需动作, 键变了则按 (旧键, pkID) 删旧插新。
  842. //
  843. // 本层只负责"把新行数据 + 新旧键交给处理器", 不判断索引类型。
  844. func (im *IndexMetaPage) UpdateByKey(indexKey []byte, newRowData []byte, pkID uint64, oldStoreRow []byte, fields []types.FieldInfo, activeVer byte, versions []SchemaVersion) error {
  845. // 取旧行数据: 二级索引需要用旧行提取"旧键", 才能精确删掉旧条目
  846. var oldRowData []byte
  847. if len(oldStoreRow) > 8 {
  848. oldRowData = oldStoreRow[8:]
  849. }
  850. indexes := im.resolveIndexes()
  851. // 唯一约束预检: 新键若已被"别的行"占着, 必须在任何写入之前拒绝。
  852. // 放在循环里逐个索引边查边更新是不行的 —— 撞车时前面几个索引已经改好了,
  853. // 旧键已删、新键已插, 而函数返回错误, 调用方以为没更新, 数据却变了一半。
  854. // (Table 的更新路径还会在 serializeRow 之前先调一次, 以免旧行的动态堆块被
  855. // 试改 —— 这里这次是给"直接调 UpdateByKey 的调用方"兜底。)
  856. if err := im.checkUniqueForKey(indexes, newRowData, pkID, fields, activeVer, versions); err != nil {
  857. return err
  858. }
  859. // 逐个索引更新本行的条目
  860. var firstErr error
  861. for _, ri := range indexes {
  862. m := ri.meta
  863. // 定位键: 能用旧行提取就用旧键; 提取不到时退回调用方给的查找键
  864. // (主键/时序索引的查找键即其在索引中的键, 与既有行为一致)
  865. oldKey := indexKey
  866. oldKeyFromRow := false
  867. if oldRowData != nil {
  868. if k := extractIndexKey(oldRowData, m, fields, activeVer, versions); len(k) > 0 {
  869. oldKey, oldKeyFromRow = k, true
  870. }
  871. }
  872. newKey := extractIndexKey(newRowData, m, fields, activeVer, versions)
  873. entryPage := m.EntryPage
  874. var err error
  875. switch {
  876. case len(newKey) > 0:
  877. err = ri.handler.Update(im.handle, &entryPage, oldKey, newKey, newRowData, pkID, len(newKey))
  878. case oldRowData == nil:
  879. // 行存储索引 (主键/时序): 键取不到也必须改写行数据, 用旧键原地更新
  880. err = ri.handler.Update(im.handle, &entryPage, oldKey, oldKey, newRowData, pkID, len(oldKey))
  881. case oldKeyFromRow:
  882. // 二级索引且旧行有键、新行取不到键: 该列已变为 NULL (或其版本不再有此列),
  883. // 这一行已退出本索引 —— 必须删掉旧条目。留下旧条目会让该索引继续命中一个
  884. // 现在为 NULL 的行, 直接破坏"NULL 不匹配任何比较"的三值语义。
  885. err = ri.handler.Delete(im.handle, &entryPage, oldKey, pkID, len(oldKey))
  886. default:
  887. // 二级索引且新旧都取不到键: 该行本来就不在本索引里, 无需动作。
  888. }
  889. if err != nil && err != page.ErrNotFound {
  890. // 过去这里丢弃了返回值, 导致 schema 演进后更新旧行"静默失败":
  891. // 对外返回 nil 但数据实际没变。必须向上抛出。
  892. firstErr = fmt.Errorf("更新索引 %q (pkID=%d) 失败: %w",
  893. util.BytesToString(m.IndexName[:]), pkID, err)
  894. }
  895. if entryPage != m.EntryPage {
  896. m.EntryPage = entryPage
  897. im.saveMeta(m)
  898. }
  899. if firstErr != nil {
  900. break
  901. }
  902. }
  903. // 回表定位缓存 (rowCache) 只在"行可能换页"时才失效。
  904. // PrimaryHandler.Update 的三级策略里, 只有行变长且页内装不下才会换页;
  905. // 行长不变 / 变短时行仍留在原 DataPage, 缓存依旧有效。
  906. // 若在这里无条件失效, 逐行更新会把 O(页数) 的缓存全量重建摊到每一行上,
  907. // 退化成 O(N²) —— 50 万行的批量更新会卡死。
  908. if oldRowData == nil || len(newRowData) > len(oldRowData) {
  909. im.rowCache = nil
  910. }
  911. return firstErr
  912. }