| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- 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 - 332 = 3764 字节),
- // 超长会在运行期由 newHeader 报错。
- // 改版本时同步更新本文案。
- VersionComment = `TsDB2 (VRTX) 格式版本 v12 — 功能与修复:
- · v12 引擎级修复: Windows LockFileEx 锁偏移从 0 改为 EOF+1 — 原锁偏移 0 被 NTFS 按 4KB 页粒度落锁, 整个头页被锁死, ReadHeader 读到零数据, RecoverWAL 整个空转; 现已避开所有有效页, 跨进程互斥语义不变
- · v12 引擎级修复: writeHeaderLocked 加 bp.UpdatePage(0, buf[:]) — 原 WriteAt(0) 绕过缓冲池, Close 时 bp.Flush 把陈旧 FirstWAL 缓存写回去覆盖 RecoverWAL 刚落盘的干净头页
- · v11 头页字段重排 (v11 库不兼容, 需重建): FreePageEnd 紧邻 FreePageStart, 字段 8 字节对齐零填充; Comment 扩为 3764B 介绍区
- · BufferPool 采样时间戳 LRU: atomic 时间戳替代双向链表 (cache-hit 路径全程 RLock+atomic, 无写锁), 32 采样淘汰干净页 (池子 ≤ 32 精确 LRU, > 32 近似 LRU), 脏页永不淘汰
- · 查询执行器: IN 走索引 / 二级索引批量回表 / ORDER BY+LIMIT 流式 top-k / Select 投影下推
- · 复合索引处理器 = SBT B+ 树, 分量不限 8 字节
- · 正确性: 事务/崩溃恢复的页回收时序 (删空页延迟还链), VACUUM 重建保真 (索引名/类型/NULL), ORM 查询错误传播
- · 常量管理: FileLockByteCount / MaxBufferPoolCapacity / BlobChainMax 等硬编码集中 const.go 管理`
- // 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
- )
|