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") )