| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- package page
- import (
- "errors"
- "unsafe"
- )
- type PageType byte
- const (
- TableIndex PageType = 1 // 表页
- DataIndex PageType = 2 // 数据页
- HeapIndex PageType = 3 // 堆页
- IndexMetaIndex PageType = 4 // 索引元数据页(全局)
- IndexDataIndex PageType = 5 // 索引数据页(B+树节点)
- DataOverflowPage PageType = 6 // 数据页溢出页 (已废弃: schema 续块改用 SchemaExtIndex)
- WALIndex PageType = 7 // WAL 日志页
- SchemaExtIndex PageType = 8 // 表结构扩展页(全库共享, 存各表 schema 续块)
- BlobIndex PageType = 9 // Blob 链页 (v10 新增: 一页 = 一个 blob chunk, 链由 PageHdr.NextPage 串)
- )
- // 操作类的回调函数
- type HandleDb struct {
- NewPage func() uint32
- // FreePage 立即回收页: 直写盘 + 把页挂上空闲链表。
- // 只适合"收尾类"路径 (DropTable 的整表回收), 那条路径依赖空闲链立刻变化。
- FreePage func(pageID uint32)
- // DeferFreePage 延迟回收页: 只登记, 等 Commit(提交成功) / Close 后再真正还链。
- //
- // 为什么需要它: 在"先 WAL 后回写"的提交顺序下, 修改中途直写盘会造出
- // "页已清零、WAL 尚未提交"的中间态 —— 崩溃或 Rollback 之后, 原页在磁盘上
- // 已经变成空闲链表节点(内容全空), 而堆链/索引仍然指向它 → 数据丢失。
- // 事务中途也不能回退 header.FreePageStart/FreePageCount (直写盘绕过 BufferPool)。
- DeferFreePage func(pageID uint32)
- SavePage func(pageID uint32, buf []byte) error
- ReadPage func(pageID uint32, buf []byte) error
- // FreeChain 把一条已经用 PageHdr.NextPage 串好的整链 (head…tail) 一次性挂到
- // 空闲链表尾, O(1) 完成 —— 调用方已持有链首/链尾/链长, 不需要逐页遍历。
- //
- // 前提: 链内每页的 next 都写在 PageHdr.NextPage (与空闲链同一字段), 且
- // tail.NextPage == 0。用于 DropTable 的堆链、WAL 提交/恢复后的整条 WAL 链,
- // 以及哈希索引的桶目录链/桶溢出链 (桶页 next 已统一到 PageHdr.NextPage)。
- FreeChain func(head, tail, count uint32) error
- // DeferFreeChain 延迟整链回收: 时序约定与 DeferFreePage 完全一致 —— 只登记,
- // 等 Commit(提交成功) / Close 之后才真正还链, 避免事务中途直写盘造出
- // "页已清零、WAL 尚未提交"的中间态。
- //
- // 与 DeferFreePage 的区别: 一条链只登记一次 (回收方已持有 head/tail/count),
- // 提交后一次 FreeChain 还链, 不需要逐页登记、逐页改写空闲链。
- // nil 时调用方应退回逐页 DeferFreePage。
- DeferFreeChain func(head, tail, count uint32)
- // ValidPage 校验页号是否落在「已分配区间」内, 返回 false 的页号一律不得还链或被引用。
- // 供给方的标准实现: 2 <= pageID && pageID < header.NextFreePage
- // (0 是头页, 1 是建库预留的表页, 都不可回收; >= NextFreePage 的页从未分配过)。
- //
- // 为什么必须有: 页枚举路径 (btree.CollectAllPageIDs / btree.GetDataPageIDs / 链遍历)
- // 只按偏移解析页头, 不看页类型 —— 一旦索引元数据的 EntryPage 指向一张被回收复用过的页
- // (DataPage / 堆页 / WAL 页), 行数据会被当成 B+ 条目解析: IndexHead.Count 落在行键
- // Min/Max 上就是个大整数, btPID 于是从任意位置截出 4 字节, 吐出「246 万」这种垃圾页号。
- // 这种页号一旦交给 FreePage:
- // 1) 空闲链头指向文件里不存在的位置 → 下一次 NewPage 拿到它, 所有写入跑到几十 GB 偏移;
- // 2) 每次还链 = 读+写整页 + 写 4KB 头页, 上千个垃圾页号能让 DROP TABLE 假死到分钟级。
- // nil 表示不做校验 (仅老 demo 手搓的 HandleDb)。
- ValidPage func(pageID uint32) bool
- }
- // ======================== 索引共享类型 ========================
- // IndexPageSize 索引页大小 (字节)
- const IndexPageSize = 16384
- // IndexType 索引类型
- type IndexType byte
- const (
- IndexTypeTime IndexType = 1 // 时序索引
- IndexTypePrimary IndexType = 2 // 主键索引
- IndexTypeStr IndexType = 3 // 已废弃,合并到 IndexTypeBTree (保留仅为向后兼容旧数据库文件)
- IndexTypeCompose IndexType = 4 // 复合索引
- IndexTypeBTree IndexType = 5 // B+树二级索引 (叶子存 PKID)
- IndexTypeHash IndexType = 6 // Hash 桶式二级索引 (等值查找 O(1))
- )
- // IndexHead B+ 树节点头 / 索引页头 (从页缓冲区 unsafe 解析)
- //
- // 注意字段之间有编译器填充: Hdr(20B) 之后 IndexType 占 1 字节, 而 EntryLen 是
- // uint16 需 2 字节对齐, 于是偏移 21 处垫 1 字节 —— 结构体实际 32 字节, 不是各
- // 字段简单相加的 29。尾部 _ [3]byte 是显式填充, 让整体对齐到 4 字节。
- type IndexHead struct {
- Hdr PageHdr // off 0 20 bytes
- IndexType IndexType // off 20 1 byte
- EntryLen uint16 // off 22 2 bytes (off 21 是编译器填充)
- Count uint32 // off 24 4 bytes
- IsLeaf byte // off 28 1 byte
- _ [3]byte // off 29 显式填充
- }
- // IndexHeadSize IndexHead 结构体字节大小
- const IndexHeadSize = int(unsafe.Sizeof(IndexHead{}))
- // BTreeFillFactor B+ 树分裂填充因子 (百分比 50~100)
- var BTreeFillFactor = 100
- // 索引共享错误
- var (
- ErrNotFound = errors.New("key not found")
- ErrRowTooLarge = errors.New("row too large, need heap overflow")
- ErrDuplicateKey = errors.New("duplicate key")
- )
|