package index import ( "TsDB2/DB/core/page" ) // ======================== 页 I/O 回调 ======================== // GlobalBack 页 I/O 回调 (传递给索引规则,由外部实现) type GlobalBack struct { SavePage func(pagID uint32, data []byte) error GetNewPage func(pagID uint32) uint32 GetDataPage func(pagID uint32, data *[]byte) bool PoolGet func(size uint32) []byte PoolPut func([]byte) Handle page.HandleDb } // IEntry 索引条目操作接口(操作索引页内的条目字节切片) type IEntry interface { Compare(a, b []byte) int // 比较判断 Serialize(e IEntry) []byte // 序列化 Deserialize(buf []byte) IEntry // 反序列化 EntryLength() uint16 // 固定条目长度 Insert(Entries *[]byte, data []byte) error // 插入索引条目 Search(Entries []byte, key []byte) (uint32, bool) // 查找索引条目 DataEntry() IDateEntry // 获取数据页操作器 } // IDateEntry 数据页实际操作接口(增删改查真实行数据) // // 二级索引(Str/Compose): // - Insert: 在 IndexPage 条目中写入 [key + PK_ID(8B)] // - Search: 二分查找返回 PK_ID 的 8 字节编码(作为 rowData 返回) // - Delete: 标记删除条目 // // 主键索引(B+ 树)/ 时序索引: // - Insert: 在 DataPage 中写入行数据 // - Search: 找到 DataPage 返回行数据 // - Delete: 软删除行数据 type IDateEntry interface { // Insert 插入数据 // count: 当前页中已有的条目数量(由调用方从 IndexHead.Count 读取) // pkID: 主键ID(二级索引存入条目,B+树/时序忽略) // rowData: 序列化的行数据(B+树/时序写入 DataPage,二级索引忽略) Insert(handle page.HandleDb, indexEntries []byte, entryLen int, count int, key []byte, pkID uint64, rowData []byte) error // Search 查找数据 // 返回值: rowData 对于二级索引是 8 字节 PK_ID 编码,对于 B+树/时序是实际行数据 Search(handle page.HandleDb, indexEntries []byte, entryLen int, key []byte) ([]byte, bool) // Update 更新行数据(仅时序索引使用) Update(handle page.HandleDb, indexEntries []byte, entryLen int, key []byte, pkID uint64, rowData []byte) error // Delete 删除数据 Delete(handle page.HandleDb, indexEntries []byte, entryLen int, key []byte) error // CreateDataPage 创建一个新的数据页并写入第一行数据(仅时序索引需要) // // pkID 是这一行的引擎行号, 必须原样写进 storeRow 的 8 字节前缀: // 行存储索引里 DataPage 是按 pkID 增序排列的, 前缀写错 (例如拿首列数据冒充) // 会让 FindRowByPK 的提前结束判断立刻成立 —— 整页的行都再也按行号找不到。 CreateDataPage(handle page.HandleDb, key []byte, pkID uint64, rowData []byte) (uint32, error) } // RowResolver 由上层注入的回表函数: 按 pkID 读取整行数据 (带 8B pkID 前缀)。 // // 二级索引本身不存行数据, 回表时必须"去找一级索引"。把这个动作做成注入函数后, // 二级索引内部即可自行完成回表, 上层 (IndexMetaPage / Table) 完全不需要知道 // 哪个索引是主键、哪个是二级, 也不需要知道索引类型枚举。 type RowResolver func(pkID uint64) ([]byte, bool) // DPRange 数据页的 pkID 区间, 供行存储索引回表时做二分定位缓存。 type DPRange struct { PageID uint32 MinPK uint64 MaxPK uint64 } // IndexHandler 高层索引操作接口 // 每种索引类型实现此接口,封装所有存储细节(B+树路由、扁平链遍历、行存储/回表等) // Table 层只需遍历 indexMeta.metas 并调用 handler 方法,无需关心索引类型与内部实现 type IndexHandler interface { // Insert 向索引插入数据 // entryPage: 当前根页ID指针(B+树分裂时可能修改) // indexKey: 从行数据提取的索引键; pkID: 自增主键; rowData: 序列化行数据 // keyLen: 索引键字节长度 // 返回新的根页ID Insert(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, rowData []byte, keyLen int) (uint32, error) // Search 按索引键查找整行数据 (带 8B pkID 前缀)。 // 行存储索引(主键/时序): 直接返回其存储的行数据; // 二级索引: 内部取出 pkID 后调用 resolve 回表, 由索引自己完成"找一级索引"。 // resolve 为 nil 时二级索引退化为返回 8B BigEndian pkID。 Search(handle page.HandleDb, entryPage uint32, indexKey []byte, keyLen int, resolve RowResolver) ([]byte, bool) // Delete 从索引删除条目 // indexKey: 提取出的索引键; pkID: 该行的自增主键。 // 二级索引允许重复 key, 必须用 (indexKey, pkID) 才能精确定位到某一行的条目; // 主键/时序索引的键本身唯一, 可忽略 pkID。各索引实现自行决定如何使用 pkID。 Delete(handle page.HandleDb, entryPage *uint32, indexKey []byte, pkID uint64, keyLen int) error // Update 更新该行在本索引中的条目 // oldKey: 该行在本索引中的旧键 (nil 表示未知); newKey: 新键; 二者相等表示键未变化。 // 二级索引的条目只有 [key + pkID], 键未变化时是空操作; 键变化时内部按 // (oldKey, pkID) 精确删旧条目再插新条目。行存储索引则就地改写行数据。 Update(handle page.HandleDb, entryPage *uint32, oldKey, newKey, rowData []byte, pkID uint64, keyLen int) error // IsRowStore 该索引是否直接承载行数据 (主键/时序索引为 true, 二级索引为 false)。 // 上层据此判断"行存在哪里", 而不需要认识具体的索引类型枚举。 IsRowStore() bool // ResolveRow 按 pkID 读取整行 (带 8B pkID 前缀); 仅 IsRowStore 的索引有意义。 // cache 由调用方持有并跨次复用, 用于避免重复构建 DataPage 边界索引。 ResolveRow(handle page.HandleDb, entryPage uint32, pkID uint64, cache *[]DPRange) ([]byte, bool) // GetDataPageIDs 返回所有数据页ID (按叶节点顺序, 用于全表扫描) GetDataPageIDs(handle page.HandleDb, entryPage uint32) []uint32 // ReadDataPageRows 读取数据页中所有未删除的行 (带 pkID(8B LE) 前缀) ReadDataPageRows(handle page.HandleDb, dpID uint32) [][]byte // RangeScan 范围扫描 // startKey/endKey: 范围边界 (nil 表示无下界/上界) // startInclusive/endInclusive: 是否包含边界 // reverse: true 时从右向左反向遍历 // callback: 每行回调, 返回 false 停止 // 主键/时序索引: row = [pkID(8B BE) + rowData] // 二级索引: row = 8B BigEndian pkID RangeScan(handle page.HandleDb, entryPage uint32, startKey, endKey []byte, startInclusive, endInclusive bool, reverse bool, callback func(row []byte) bool) error } // BatchRow 批量插入的单行输入 (handler 视角)。 // // IndexKey 为 nil 表示该行不进本索引 (任一索引分量为 NULL), handler 自行跳过。 type BatchRow struct { PKID uint64 IndexKey []byte RowData []byte } // BatchInserter 可选的批量插入能力。 // // 实现它的 IndexHandler 获得批量写入路径 (B+ 树叶驻内存、页落盘按页摊销); // 未实现者由 IndexMetaPage.InsertRows 退化为逐条 Insert, 行为与单行路径完全一致。 // 键长不再单独传: 从首个非 nil IndexKey 推得 (同一索引内键长恒定)。 type BatchInserter interface { InsertBatch(handle page.HandleDb, entryPage *uint32, rows []BatchRow) error } // PageCollector 可选的页枚举能力: 由页布局特殊的索引处理器自行实现, 汇报该索引 // 当前占用的全部页 ID。 // // 为什么需要它: 索引被整体"换根"后要把旧结构还进空闲链表, 而 B+ 树以外的索引 // 没有统一的节点结构可供上层遍历 —— // - B+ 树: 节点格式一致, btree.CollectAllPageIDs 按内部条目下降即可枚举; // - Hash: 元数据页 + 目录页链 + 各桶的溢出链, 三种页三种头; // - Compose: 扁平条目链, 只有 NextPage 可循。 // 因此枚举动作只能交回处理器, 按各自协议走。 // // 实现约定: 任何一步解析不出来就**少收**, 绝不猜 —— 漏收只是页泄漏 (文件胀大), // 多收会把仍然可达的页还进空闲链表, 之后被分配给别的索引, 那是静默的数据损坏。 type PageCollector interface { // CollectPageIDs 返回从 entryPage 出发可枚举到的全部页 ID (含 entryPage 自身)。 CollectPageIDs(handle page.HandleDb, entryPage uint32) []uint32 } // PageChain 一条已经用公共头 PageHdr.NextPage 串好的页链。 // 链内每一页的 next 都在 PageHdr.NextPage 上, 与空闲链同字段, 因此这样一条链可以 // 整体交给 HandleDb.FreeChain(head, tail, count) 一次拼接进空闲链。 type PageChain struct { Head uint32 // 链首页 Tail uint32 // 链尾页 Count uint32 // 链长 (页数) } // ChainCollector 索引可选实现: 以「整链」而不是「逐页」汇报占用的页。 // // 与 PageCollector 的关系: CollectPageIDs 让上层逐页 FreePage, 每页都要改写一次 // 空闲链头并落盘; CollectChains 让上层对每条链只做一次 FreeChain —— 桶页的 next // 既然已经统一到 PageHdr.NextPage, 就没有理由再逐页还。 // // 实现约定与 PageCollector 相同: 解析不出来就少报 (漏报只是页泄漏), 绝不能多报 // (多报会把仍可达的页还进空闲链, 造成静默数据损坏)。 type ChainCollector interface { // CollectChains 返回该索引占用的全部链 (含元数据页这样的单页链)。 CollectChains(handle page.HandleDb, entryPage uint32) []PageChain } type ComposeType struct { IndexEntry map[page.IndexType]IEntry IndexHandlers map[page.IndexType]IndexHandler } func (com *ComposeType) register(indexType page.IndexType, entry IEntry) { com.IndexEntry[indexType] = entry } func (com *ComposeType) RegisterHandler(indexType page.IndexType, handler IndexHandler) { com.IndexHandlers[indexType] = handler } func (com *ComposeType) GetEntryHandler(t page.IndexType) (IEntry, bool) { if entry, ok := com.IndexEntry[t]; ok { return entry, true } return nil, false } func (com *ComposeType) GetHandler(t page.IndexType) (IndexHandler, bool) { if h, ok := com.IndexHandlers[t]; ok { return h, true } return nil, false } var Compose = ComposeType{ IndexEntry: make(map[page.IndexType]IEntry), IndexHandlers: make(map[page.IndexType]IndexHandler), } func init() { Compose.register(page.IndexTypeTime, &IndexTimeEntry{}) Compose.register(page.IndexTypeCompose, &ComposeEntry{}) // 向后兼容:旧数据库中 IndexTypeStr=3 的索引条目用 ComposeEntry 处理 Compose.register(page.IndexTypeStr, &ComposeEntry{}) }