package core import ( "time" ) var Magic = [4]byte{'V', 'R', 'T', 'X'} // ============================================================================= // 页面与存储常量 // ============================================================================= const ( // Version 数据库文件格式版本 // v2: schema 改为「精确内联块 + 全库共享 SchemaExtIndex 扩展页」, 不再使用溢出页链 // v3: 空闲页统一符合 PageHdr —— 空闲链指针从页偏移 0 改到页头 NextPage (偏移 12), // 不再覆盖 Type/RefCount; 旧库空闲链按旧偏移写入, 不再兼容, 需重建 // v4: 页头 RefCount 由 1 字节扩为 2 字节, 空出的保留字节改为 Flags (页状态位), // 页头仍为 20 字节; WAL 页头统一为 40 字节 (首页/续页同构), 条目区起点随之变化。 // 所有页头布局集中在 DB/core/page 包定义, 并按编译期断言钉死。 // v5: TableNameSize 由 24 扩为 48 —— TableInfo 80 → 104 字节, IndexMeta 60 → 84 字节, // 表页记录与索引元数据记录整体位移 (analyzer 的硬编码偏移同步更新)。 // 旧库按 v4 解析会读到错位的字段, 因此不兼容, 需重建。 // v6: 索引名与表名解耦, 新增 IndexNameSize = 64 —— IndexMeta 84 → 100 字节 (TableInfo 不变)。 // 索引名定长数组变长会让其后所有字段整体后移, 旧库按 v5 解析会把索引名的高位字节 // 当成 IndexType/FieldCnt, 因此不兼容, 需重建。 // v7: 有符号整数单独占类型值 (FieldType_Int8/16/32/64) 并改用保序编码 —— 旧库把 ORM 的 // int8/16/32/64 列存在 Uint8..Uint64 上, 是补码大端, 对负数不保序 (等值查得到, 但跨 0 的 // 范围扫描 / ORDER BY 会漏行或混入错误行)。改用保序编码后, 同一列在盘上的字节与旧库 // 不同, 索引键也按新编码重建, 因此不兼容, 需重建。 // v8: DataPage 引入槽位目录 (行号稳定), 主键 B+ 树的叶条目由 [key(8)+dpID(4)] 12 字节 // 扩为 [key(8)+dpID(4)+slot(2)] 14 字节 —— 命中叶即可按 (dpID, slot) O(1) 取行, // 不再在页内按 pkID 扫一遍。叶条目长度变了, 旧库按 12 字节解析会读错 dpID, // 因此不兼容, 需重建。 // v9: 字段类型枚举整体重排并改用语义名 (1/2/4/8byte → Uint8/16/32/64; 定长字符串的 // 16/32/64/128/256byte 五档合并为单一 String, 行内宽度全交 Flag 低 5 位的宽度码)。 // 类型值编号与 v8 完全不同 (如 Uint8 由 1 变 2, Int8 由 12 变 3, String 吸收 5..9), // 旧库按 v8 解析会把类型字节当成另一种类型: 行内宽度与解码语义同时错位, 因此不兼容, // 需重建。 // v10: FieldType_Blob 行内由 6B 堆引用改为 8B BlobRef 直引链页 (流式多页链), 内容上 // 限从单堆页 ~16KB 扩到链页总 ≈4GB。Update/Delete/DropTable 自动释放旧 blob 链; // 旧库的 blob 列行内 6B 引用会被新版误读成 BlobRef 高 6B → TotalLen 异常 → // 读/写无法对齐, 因此不兼容, 需重建任何带 Blob 列的表。 // v11: HeaderPage 字段重排 —— FreePageEnd 从 Comment 之后 (偏移 840) 移到 FreePageStart // 之后 (偏移 60), 全部标量字段按 8 字节对齐、零对齐填充字节; Comment 启用为 // 数据库介绍区 (建库自动写入, DB.Comment/SetComment 读写)。所有字段偏移变化, // 旧库不兼容, 需重建。 Version = 1 // VersionComment 当前格式版本的【功能介绍与修复点】。建库时若调用方没有 // 提供介绍 (newHeader 传空串), 头页 Comment 区默认写入本文案 —— 拿到一个 // .db 文件的人从头页就能读到这个库是什么版本、有什么能力。 // // 约束: 必须 ≤ HeaderPage.Comment 的容量 (PageSize4K-844 = 3252 字节), // 超长会在编译期由 header_page.go 的断言挡住? 不会 — 是 newHeader 运行期报错。 // 改版本时同步更新本文案。 VersionComment = `TsDB2 (VRTX) 格式版本 v11 — 功能与修复: · HeaderPage 重排: FreePageEnd 紧邻 FreePageStart, 字段 8 字节对齐零填充; Comment 扩为 3252B 介绍区 · 查询执行器: IN 走索引 / 二级索引批量回表 / ORDER BY+LIMIT 流式 top-k / Select 投影下推 · BufferPool 真 LRU + 读缓存; WAL 提交链优化; 无唯一索引表 Update 跳过预检序列化 · 复合索引处理器 = SBT B+ 树, 分量不限 8 字节 · 正确性: 事务/崩溃恢复的页回收时序 (删空页延迟还链), VACUUM 重建保真 (索引名/类型/NULL), ORM 查询错误传播 · 结构: RowStoreHandler 能力接口, table_*.go / query_*.go / btree 生命周期拆分` // PageSize 数据页大小 (16KB) — 索引页、数据页、WAL 页统一使用 PageSize uint32 = 4096 * 4 // PageSize4K 头页大小 (4KB) — HeaderPage、TablePage 元数据等固定页 PageSize4K = 4096 // TableNameSize 表名的最大字节数 (TableInfo.Name 的定长数组长度)。 // // 48 不是随手取的: 它落在「不产生额外对齐填充」的那一支 (长度 ≡ 0 mod 8)。 // TableInfo 里除 Name 外全是 1/2/4/8 字节的定长字段, Name 取 8 的倍数, 其后所有字段的 // 相对偏移就不受 Name 长度影响, 因此除 Name 本身变长外一字节不多花。实测 (amd64): // // TableInfo Name=24 → 80 字节 Name=48 → 104 字节 (104 % 8 == 0, 满足内部 uint64 的 8 字节对齐) // // 若取 44 之类 ≡ 4 (mod 8) 的值, TotalRows 会被顶到下一个 8 字节边界, 白多 4 字节填充。 TableNameSize = 48 // IndexNameSize 索引名的最大字节数 (IndexMeta.IndexName 的定长数组长度)。 // // 与表名分开的原因: 表名要短 —— 它会出现在每一处日志与错误里, 而 48 已是「8 的倍数」这 // 一支里的合适取值; 索引名却往往由「前缀 + 表名 + 列名」拼出来 (pk_<表名>、 // idx_<表名>_<列名>), 和表名挤同一个 48 就会互相侵占。给到 64 之后, 前缀那笔账只从 // 索引名里出, 表名可以稳稳用满 48。 // // 代价只有 IndexMeta 每条多 16 字节 —— 而它是**目录记录**, 基数 = 表数 + 索引数 (不是行数): // 即便 100 个索引也才多 1.6KB (≈0.1 个 16KB 页), 完全不影响数据文件大小。 // // 尺寸仍然对齐: 64 ≡ 0 (mod 8), 且 IndexMeta 里最大字段是 uint32, 因此 // // IndexMeta IndexName=48 → 84 字节 IndexName=64 → 100 字节 (100 % 4 == 0) // // 注意: 以上常量都直接改变盘上布局, 修改时必须同步 // - TableInfo / IndexMeta 的编译期尺寸与偏移断言 (DB/core/Table.go, index_meta_page.go) // - analyzer 里那套硬编码尺寸/偏移 (analyzer/main.go) // - ORM 的命名额度预检 (DB/db.go 的 checkNameBudget 及其编译期不变量) // - 并提升 Version IndexNameSize = 64 // MaxInlineSchemaSize 表页内联 schema 块的最大字节数 // 建表时按 schema 实际长度精确分配, 仅当超过此上限时才截断并把余下部分 // 放进共享 schema 扩展页 (避免单表吃满整张表页) MaxInlineSchemaSize = 4096 // MaxFieldSize 单次创建字段的最大数量 (建表那一批次的字段个数上界)。 MaxFieldSize = 256 // MaxFieldsPerTable 一张表的列数上限 (master registry, 含已被 DropField 软删除的列)。 // // 两条约束取更紧的一条: // - 硬约束: TableInfo.FieldCount 与 schema 里的字段索引 (SchemaVersion.FieldIndices) // 都是 1 字节, 列数到 256 就会回绕 / 截断 → 绝对上限 255; // - 建表政策: 在 255 上再留 1 格余量 → MaxFieldSize-2 = 254。 // // 建表 (TablePage.Create) 与加列 (Table.AddField) 必须卡同一条: 只在建表卡住而加列 // 不限, 就能一路推到 255 以上, 落盘时把 FieldCount / 字段索引截断成错位 schema —— // 写进去的时候不报错, 读回来整表错乱。 // // 导出给库外的调用方 (migrate 要在读源库结构时就提示"源表有多少列、VRTX 只到多少列", // 而不是等建表时抛一句"字段数量非法"): 这是 VRTX 的能力边界, 不该只藏在 core 里。 MaxFieldsPerTable = MaxFieldSize - 2 // FieldNameSize 列名的最大字节数。 // // 列名在 schema 里不是定长数组, 而是 [nameLen(1B)][name...]: 长度前缀只有 1 字节, // 因此上限是 255。超长会让落盘的 nameLen 被截断, 解析侧从错的偏移读后续字段 —— // 不是"名字被缩短"这么轻, 是整张表错位, 所以建列时必须拒绝 // (见 validateFieldsForSchema)。 FieldNameSize = 255 // MaxIndexFields 一个索引最多允许的分量列数。 // // 上限取 16: IndexMeta.FieldIDs 是定长 [16]byte, 超出的分量根本无处落盘 // (会被静默丢弃, 于是索引只覆盖了前 16 列却声称覆盖全部)。见 IndexMetaPage.CreateIndex。 // // 导出给库外的调用方: 迁移侧要能提示"源库这个索引有几列、VRTX 只到多少列" // (SQLite 的索引列数几乎没有限制, 直接建会撞上 core 的闸门)。 MaxIndexFields = 16 ) // ============================================================================= // BufferPool 配置 // ============================================================================= const ( // DefaultBufferPoolCapacity 默认缓冲池容量 (页数) // 1000 页 × 16KB = ~16MB (嵌入式/资源受限场景) // - 最小安全值: 20页 (低于此值可能死锁) // - 推荐配置: 1000-5000页 (16-78MB) // - 高性能场景: 10000+页 (156MB+) DefaultBufferPoolCapacity = 1000 // MaxBufferPoolCapacity 自适应缓冲池容量的硬上限 (页数) // OpenWithOptions 按文件 1/4 分配容量, 但上限 8000 页 = 128MB, // 防止巨型库把缓冲池撑爆物理内存。 MaxBufferPoolCapacity = 8000 ) // ============================================================================= // WAL 与自动提交配置 // ============================================================================= const ( // DefaultDirtyThreshold 脏页阈值 (页数) // BufferPool 中脏页数量超过此值时,自动触发 Commit 刷盘 // 注意: 这是 BufferPool 级别的计数,不是事务级别 // - 500 页 × 16KB = ~8MB (平衡安全性、性能和内存) // - 断电/崩溃时最多丢失 8MB 数据 (约 10,000 行) // - 性能接近最优 (比 2000 页仅慢 1.5%) // - 内存占用合理 (比 2000 页减少 75%) DefaultDirtyThreshold = 500 // DefaultAutoCommitDelay 默认防抖自动提交延迟 // 每次写操作重置计时器,停止操作后该时间内无新操作则自动 Commit // - 适用场景: 低频写入、交互式应用 // - 禁用方法: 设置为 0 可完全禁用防抖,仅靠脏页阈值触发 DefaultAutoCommitDelay = time.Second ) // ============================================================================= // 跨进程文件锁配置 (Windows LockFileEx / Unix flock 共用) // ============================================================================= const ( // FileLockByteCount 跨进程文件锁锁定的字节数。 // Windows LockFileEx 的锁是【字节范围锁】, 锁文件末尾之后的 1 字节虚拟区域 // [EOF, EOF+1) 即可实现跨进程互斥, 又完全避开所有有效数据页 (历史曾用偏移 0, // 结果 Windows NTFS 按 4KB 页粒度落锁, 整个头页被锁死, ReadHeader 读到零数据)。 // Unix flock 是整文件锁, 不使用此值。 FileLockByteCount = 1 ) // ============================================================================= // Blob 配置 // ============================================================================= const ( // BlobChainMax 单个 Blob 列的最大内容长度 (字节)。 // 超出此值 Blob 写入会被拒绝, 提示 "blob 内容超过上限"。 // 上限由来: 链长用 uint32 计数, 单页 16KB, 256K 页 ≈ 4GB, // 对绝大多数 binary 大对象够用; 超大文件 (>4GB) 应改用对象存储。 BlobChainMax = 256 * 1024 // 256KB )