const.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package core
  2. import (
  3. "time"
  4. )
  5. var Magic = [4]byte{'V', 'R', 'T', 'X'}
  6. // =============================================================================
  7. // 页面与存储常量
  8. // =============================================================================
  9. const (
  10. // Version 数据库文件格式版本
  11. // v2: schema 改为「精确内联块 + 全库共享 SchemaExtIndex 扩展页」, 不再使用溢出页链
  12. // v3: 空闲页统一符合 PageHdr —— 空闲链指针从页偏移 0 改到页头 NextPage (偏移 12),
  13. // 不再覆盖 Type/RefCount; 旧库空闲链按旧偏移写入, 不再兼容, 需重建
  14. // v4: 页头 RefCount 由 1 字节扩为 2 字节, 空出的保留字节改为 Flags (页状态位),
  15. // 页头仍为 20 字节; WAL 页头统一为 40 字节 (首页/续页同构), 条目区起点随之变化。
  16. // 所有页头布局集中在 DB/core/page 包定义, 并按编译期断言钉死。
  17. // v5: TableNameSize 由 24 扩为 48 —— TableInfo 80 → 104 字节, IndexMeta 60 → 84 字节,
  18. // 表页记录与索引元数据记录整体位移 (analyzer 的硬编码偏移同步更新)。
  19. // 旧库按 v4 解析会读到错位的字段, 因此不兼容, 需重建。
  20. // v6: 索引名与表名解耦, 新增 IndexNameSize = 64 —— IndexMeta 84 → 100 字节 (TableInfo 不变)。
  21. // 索引名定长数组变长会让其后所有字段整体后移, 旧库按 v5 解析会把索引名的高位字节
  22. // 当成 IndexType/FieldCnt, 因此不兼容, 需重建。
  23. // v7: 有符号整数单独占类型值 (FieldType_Int8/16/32/64) 并改用保序编码 —— 旧库把 ORM 的
  24. // int8/16/32/64 列存在 Uint8..Uint64 上, 是补码大端, 对负数不保序 (等值查得到, 但跨 0 的
  25. // 范围扫描 / ORDER BY 会漏行或混入错误行)。改用保序编码后, 同一列在盘上的字节与旧库
  26. // 不同, 索引键也按新编码重建, 因此不兼容, 需重建。
  27. // v8: DataPage 引入槽位目录 (行号稳定), 主键 B+ 树的叶条目由 [key(8)+dpID(4)] 12 字节
  28. // 扩为 [key(8)+dpID(4)+slot(2)] 14 字节 —— 命中叶即可按 (dpID, slot) O(1) 取行,
  29. // 不再在页内按 pkID 扫一遍。叶条目长度变了, 旧库按 12 字节解析会读错 dpID,
  30. // 因此不兼容, 需重建。
  31. // v9: 字段类型枚举整体重排并改用语义名 (1/2/4/8byte → Uint8/16/32/64; 定长字符串的
  32. // 16/32/64/128/256byte 五档合并为单一 String, 行内宽度全交 Flag 低 5 位的宽度码)。
  33. // 类型值编号与 v8 完全不同 (如 Uint8 由 1 变 2, Int8 由 12 变 3, String 吸收 5..9),
  34. // 旧库按 v8 解析会把类型字节当成另一种类型: 行内宽度与解码语义同时错位, 因此不兼容,
  35. // 需重建。
  36. // v10: FieldType_Blob 行内由 6B 堆引用改为 8B BlobRef 直引链页 (流式多页链), 内容上
  37. // 限从单堆页 ~16KB 扩到链页总 ≈4GB。Update/Delete/DropTable 自动释放旧 blob 链;
  38. // 旧库的 blob 列行内 6B 引用会被新版误读成 BlobRef 高 6B → TotalLen 异常 →
  39. // 读/写无法对齐, 因此不兼容, 需重建任何带 Blob 列的表。
  40. // v11: HeaderPage 字段重排 —— FreePageEnd 从 Comment 之后 (偏移 840) 移到 FreePageStart
  41. // 之后 (偏移 60), 全部标量字段按 8 字节对齐、零对齐填充字节; Comment 启用为
  42. // 数据库介绍区 (建库自动写入, DB.Comment/SetComment 读写)。所有字段偏移变化,
  43. // 旧库不兼容, 需重建。
  44. Version = 1
  45. // VersionComment 当前格式版本的【功能介绍与修复点】。建库时若调用方没有
  46. // 提供介绍 (newHeader 传空串), 头页 Comment 区默认写入本文案 —— 拿到一个
  47. // .db 文件的人从头页就能读到这个库是什么版本、有什么能力。
  48. //
  49. // 约束: 必须 ≤ HeaderPage.Comment 的容量 (PageSize4K - 332 = 3764 字节),
  50. // 超长会在运行期由 newHeader 报错。
  51. // 改版本时同步更新本文案。
  52. VersionComment = `TsDB2 (VRTX) 格式版本 v12 — 功能与修复:
  53. · v12 引擎级修复: Windows LockFileEx 锁偏移从 0 改为 EOF+1 — 原锁偏移 0 被 NTFS 按 4KB 页粒度落锁, 整个头页被锁死, ReadHeader 读到零数据, RecoverWAL 整个空转; 现已避开所有有效页, 跨进程互斥语义不变
  54. · v12 引擎级修复: writeHeaderLocked 加 bp.UpdatePage(0, buf[:]) — 原 WriteAt(0) 绕过缓冲池, Close 时 bp.Flush 把陈旧 FirstWAL 缓存写回去覆盖 RecoverWAL 刚落盘的干净头页
  55. · v11 头页字段重排 (v11 库不兼容, 需重建): FreePageEnd 紧邻 FreePageStart, 字段 8 字节对齐零填充; Comment 扩为 3764B 介绍区
  56. · BufferPool 采样时间戳 LRU: atomic 时间戳替代双向链表 (cache-hit 路径全程 RLock+atomic, 无写锁), 32 采样淘汰干净页 (池子 ≤ 32 精确 LRU, > 32 近似 LRU), 脏页永不淘汰
  57. · 查询执行器: IN 走索引 / 二级索引批量回表 / ORDER BY+LIMIT 流式 top-k / Select 投影下推
  58. · 复合索引处理器 = SBT B+ 树, 分量不限 8 字节
  59. · 正确性: 事务/崩溃恢复的页回收时序 (删空页延迟还链), VACUUM 重建保真 (索引名/类型/NULL), ORM 查询错误传播
  60. · 常量管理: FileLockByteCount / MaxBufferPoolCapacity / BlobChainMax 等硬编码集中 const.go 管理`
  61. // PageSize 数据页大小 (16KB) — 索引页、数据页、WAL 页统一使用
  62. PageSize uint32 = 4096 * 4
  63. // PageSize4K 头页大小 (4KB) — HeaderPage、TablePage 元数据等固定页
  64. PageSize4K = 4096
  65. // TableNameSize 表名的最大字节数 (TableInfo.Name 的定长数组长度)。
  66. //
  67. // 48 不是随手取的: 它落在「不产生额外对齐填充」的那一支 (长度 ≡ 0 mod 8)。
  68. // TableInfo 里除 Name 外全是 1/2/4/8 字节的定长字段, Name 取 8 的倍数, 其后所有字段的
  69. // 相对偏移就不受 Name 长度影响, 因此除 Name 本身变长外一字节不多花。实测 (amd64):
  70. //
  71. // TableInfo Name=24 → 80 字节 Name=48 → 104 字节 (104 % 8 == 0, 满足内部 uint64 的 8 字节对齐)
  72. //
  73. // 若取 44 之类 ≡ 4 (mod 8) 的值, TotalRows 会被顶到下一个 8 字节边界, 白多 4 字节填充。
  74. TableNameSize = 48
  75. // IndexNameSize 索引名的最大字节数 (IndexMeta.IndexName 的定长数组长度)。
  76. //
  77. // 与表名分开的原因: 表名要短 —— 它会出现在每一处日志与错误里, 而 48 已是「8 的倍数」这
  78. // 一支里的合适取值; 索引名却往往由「前缀 + 表名 + 列名」拼出来 (pk_<表名>、
  79. // idx_<表名>_<列名>), 和表名挤同一个 48 就会互相侵占。给到 64 之后, 前缀那笔账只从
  80. // 索引名里出, 表名可以稳稳用满 48。
  81. //
  82. // 代价只有 IndexMeta 每条多 16 字节 —— 而它是**目录记录**, 基数 = 表数 + 索引数 (不是行数):
  83. // 即便 100 个索引也才多 1.6KB (≈0.1 个 16KB 页), 完全不影响数据文件大小。
  84. //
  85. // 尺寸仍然对齐: 64 ≡ 0 (mod 8), 且 IndexMeta 里最大字段是 uint32, 因此
  86. //
  87. // IndexMeta IndexName=48 → 84 字节 IndexName=64 → 100 字节 (100 % 4 == 0)
  88. //
  89. // 注意: 以上常量都直接改变盘上布局, 修改时必须同步
  90. // - TableInfo / IndexMeta 的编译期尺寸与偏移断言 (DB/core/Table.go, index_meta_page.go)
  91. // - analyzer 里那套硬编码尺寸/偏移 (analyzer/main.go)
  92. // - ORM 的命名额度预检 (DB/db.go 的 checkNameBudget 及其编译期不变量)
  93. // - 并提升 Version
  94. IndexNameSize = 64
  95. // MaxInlineSchemaSize 表页内联 schema 块的最大字节数
  96. // 建表时按 schema 实际长度精确分配, 仅当超过此上限时才截断并把余下部分
  97. // 放进共享 schema 扩展页 (避免单表吃满整张表页)
  98. MaxInlineSchemaSize = 4096
  99. // MaxFieldSize 单次创建字段的最大数量 (建表那一批次的字段个数上界)。
  100. MaxFieldSize = 256
  101. // MaxFieldsPerTable 一张表的列数上限 (master registry, 含已被 DropField 软删除的列)。
  102. //
  103. // 两条约束取更紧的一条:
  104. // - 硬约束: TableInfo.FieldCount 与 schema 里的字段索引 (SchemaVersion.FieldIndices)
  105. // 都是 1 字节, 列数到 256 就会回绕 / 截断 → 绝对上限 255;
  106. // - 建表政策: 在 255 上再留 1 格余量 → MaxFieldSize-2 = 254。
  107. //
  108. // 建表 (TablePage.Create) 与加列 (Table.AddField) 必须卡同一条: 只在建表卡住而加列
  109. // 不限, 就能一路推到 255 以上, 落盘时把 FieldCount / 字段索引截断成错位 schema ——
  110. // 写进去的时候不报错, 读回来整表错乱。
  111. //
  112. // 导出给库外的调用方 (migrate 要在读源库结构时就提示"源表有多少列、VRTX 只到多少列",
  113. // 而不是等建表时抛一句"字段数量非法"): 这是 VRTX 的能力边界, 不该只藏在 core 里。
  114. MaxFieldsPerTable = MaxFieldSize - 2
  115. // FieldNameSize 列名的最大字节数。
  116. //
  117. // 列名在 schema 里不是定长数组, 而是 [nameLen(1B)][name...]: 长度前缀只有 1 字节,
  118. // 因此上限是 255。超长会让落盘的 nameLen 被截断, 解析侧从错的偏移读后续字段 ——
  119. // 不是"名字被缩短"这么轻, 是整张表错位, 所以建列时必须拒绝
  120. // (见 validateFieldsForSchema)。
  121. FieldNameSize = 255
  122. // MaxIndexFields 一个索引最多允许的分量列数。
  123. //
  124. // 上限取 16: IndexMeta.FieldIDs 是定长 [16]byte, 超出的分量根本无处落盘
  125. // (会被静默丢弃, 于是索引只覆盖了前 16 列却声称覆盖全部)。见 IndexMetaPage.CreateIndex。
  126. //
  127. // 导出给库外的调用方: 迁移侧要能提示"源库这个索引有几列、VRTX 只到多少列"
  128. // (SQLite 的索引列数几乎没有限制, 直接建会撞上 core 的闸门)。
  129. MaxIndexFields = 16
  130. )
  131. // =============================================================================
  132. // BufferPool 配置
  133. // =============================================================================
  134. const (
  135. // DefaultBufferPoolCapacity 默认缓冲池容量 (页数)
  136. // 1000 页 × 16KB = ~16MB (嵌入式/资源受限场景)
  137. // - 最小安全值: 20页 (低于此值可能死锁)
  138. // - 推荐配置: 1000-5000页 (16-78MB)
  139. // - 高性能场景: 10000+页 (156MB+)
  140. DefaultBufferPoolCapacity = 1000
  141. // MaxBufferPoolCapacity 自适应缓冲池容量的硬上限 (页数)
  142. // OpenWithOptions 按文件 1/4 分配容量, 但上限 8000 页 = 128MB,
  143. // 防止巨型库把缓冲池撑爆物理内存。
  144. MaxBufferPoolCapacity = 8000
  145. )
  146. // =============================================================================
  147. // WAL 与自动提交配置
  148. // =============================================================================
  149. const (
  150. // DefaultDirtyThreshold 脏页阈值 (页数)
  151. // BufferPool 中脏页数量超过此值时,自动触发 Commit 刷盘
  152. // 注意: 这是 BufferPool 级别的计数,不是事务级别
  153. // - 500 页 × 16KB = ~8MB (平衡安全性、性能和内存)
  154. // - 断电/崩溃时最多丢失 8MB 数据 (约 10,000 行)
  155. // - 性能接近最优 (比 2000 页仅慢 1.5%)
  156. // - 内存占用合理 (比 2000 页减少 75%)
  157. DefaultDirtyThreshold = 500
  158. // DefaultAutoCommitDelay 默认防抖自动提交延迟
  159. // 每次写操作重置计时器,停止操作后该时间内无新操作则自动 Commit
  160. // - 适用场景: 低频写入、交互式应用
  161. // - 禁用方法: 设置为 0 可完全禁用防抖,仅靠脏页阈值触发
  162. DefaultAutoCommitDelay = time.Second
  163. )
  164. // =============================================================================
  165. // 跨进程文件锁配置 (Windows LockFileEx / Unix flock 共用)
  166. // =============================================================================
  167. const (
  168. // FileLockByteCount 跨进程文件锁锁定的字节数。
  169. // Windows LockFileEx 的锁是【字节范围锁】, 锁文件末尾之后的 1 字节虚拟区域
  170. // [EOF, EOF+1) 即可实现跨进程互斥, 又完全避开所有有效数据页 (历史曾用偏移 0,
  171. // 结果 Windows NTFS 按 4KB 页粒度落锁, 整个头页被锁死, ReadHeader 读到零数据)。
  172. // Unix flock 是整文件锁, 不使用此值。
  173. FileLockByteCount = 1
  174. )
  175. // =============================================================================
  176. // Blob 配置
  177. // =============================================================================
  178. const (
  179. // BlobChainMax 单个 Blob 列的最大内容长度 (字节)。
  180. // 超出此值 Blob 写入会被拒绝, 提示 "blob 内容超过上限"。
  181. // 上限由来: 链长用 uint32 计数, 单页 16KB, 256K 页 ≈ 4GB,
  182. // 对绝大多数 binary 大对象够用; 超大文件 (>4GB) 应改用对象存储。
  183. BlobChainMax = 256 * 1024 // 256KB
  184. )