const.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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 列存在 1/2/4/8byte 上, 是补码大端, 对负数不保序 (等值查得到, 但跨 0 的
  25. // 范围扫描 / ORDER BY 会漏行或混入错误行)。改用保序编码后, 同一列在盘上的字节与旧库
  26. // 不同, 索引键也按新编码重建, 因此不兼容, 需重建。
  27. Version = 7
  28. // PageSize 数据页大小 (16KB) — 索引页、数据页、WAL 页统一使用
  29. PageSize uint32 = 4096 * 4
  30. // PageSize4K 头页大小 (4KB) — HeaderPage、TablePage 元数据等固定页
  31. PageSize4K = 4096
  32. // TableNameSize 表名的最大字节数 (TableInfo.Name 的定长数组长度)。
  33. //
  34. // 48 不是随手取的: 它落在「不产生额外对齐填充」的那一支 (长度 ≡ 0 mod 8)。
  35. // TableInfo 里除 Name 外全是 1/2/4/8 字节的定长字段, Name 取 8 的倍数, 其后所有字段的
  36. // 相对偏移就不受 Name 长度影响, 因此除 Name 本身变长外一字节不多花。实测 (amd64):
  37. //
  38. // TableInfo Name=24 → 80 字节 Name=48 → 104 字节 (104 % 8 == 0, 满足内部 uint64 的 8 字节对齐)
  39. //
  40. // 若取 44 之类 ≡ 4 (mod 8) 的值, TotalRows 会被顶到下一个 8 字节边界, 白多 4 字节填充。
  41. TableNameSize = 48
  42. // IndexNameSize 索引名的最大字节数 (IndexMeta.IndexName 的定长数组长度)。
  43. //
  44. // 与表名分开的原因: 表名要短 —— 它会出现在每一处日志与错误里, 而 48 已是「8 的倍数」这
  45. // 一支里的合适取值; 索引名却往往由「前缀 + 表名 + 列名」拼出来 (pk_<表名>、
  46. // idx_<表名>_<列名>), 和表名挤同一个 48 就会互相侵占。给到 64 之后, 前缀那笔账只从
  47. // 索引名里出, 表名可以稳稳用满 48。
  48. //
  49. // 代价只有 IndexMeta 每条多 16 字节 —— 而它是**目录记录**, 基数 = 表数 + 索引数 (不是行数):
  50. // 即便 100 个索引也才多 1.6KB (≈0.1 个 16KB 页), 完全不影响数据文件大小。
  51. //
  52. // 尺寸仍然对齐: 64 ≡ 0 (mod 8), 且 IndexMeta 里最大字段是 uint32, 因此
  53. //
  54. // IndexMeta IndexName=48 → 84 字节 IndexName=64 → 100 字节 (100 % 4 == 0)
  55. //
  56. // 注意: 以上常量都直接改变盘上布局, 修改时必须同步
  57. // - TableInfo / IndexMeta 的编译期尺寸与偏移断言 (DB/core/Table.go, IndexMetaPage.go)
  58. // - analyzer 里那套硬编码尺寸/偏移 (analyzer/main.go)
  59. // - ORM 的命名额度预检 (DB/db.go 的 checkNameBudget 及其编译期不变量)
  60. // - 并提升 Version
  61. IndexNameSize = 64
  62. // MaxInlineSchemaSize 表页内联 schema 块的最大字节数
  63. // 建表时按 schema 实际长度精确分配, 仅当超过此上限时才截断并把余下部分
  64. // 放进共享 schema 扩展页 (避免单表吃满整张表页)
  65. MaxInlineSchemaSize = 4096
  66. // MaxFieldSize 单次创建字段的最大数量 (建表那一批次的字段个数上界)。
  67. MaxFieldSize = 256
  68. // MaxFieldsPerTable 一张表的列数上限 (master registry, 含已被 DropField 软删除的列)。
  69. //
  70. // 两条约束取更紧的一条:
  71. // - 硬约束: TableInfo.FieldCount 与 schema 里的字段索引 (SchemaVersion.FieldIndices)
  72. // 都是 1 字节, 列数到 256 就会回绕 / 截断 → 绝对上限 255;
  73. // - 建表政策: 在 255 上再留 1 格余量 → MaxFieldSize-2 = 254。
  74. //
  75. // 建表 (TablePage.Create) 与加列 (Table.AddField) 必须卡同一条: 只在建表卡住而加列
  76. // 不限, 就能一路推到 255 以上, 落盘时把 FieldCount / 字段索引截断成错位 schema ——
  77. // 写进去的时候不报错, 读回来整表错乱。
  78. //
  79. // 导出给库外的调用方 (migrate 要在读源库结构时就提示"源表有多少列、VRTX 只到多少列",
  80. // 而不是等建表时抛一句"字段数量非法"): 这是 VRTX 的能力边界, 不该只藏在 core 里。
  81. MaxFieldsPerTable = MaxFieldSize - 2
  82. // FieldNameSize 列名的最大字节数。
  83. //
  84. // 列名在 schema 里不是定长数组, 而是 [nameLen(1B)][name...]: 长度前缀只有 1 字节,
  85. // 因此上限是 255。超长会让落盘的 nameLen 被截断, 解析侧从错的偏移读后续字段 ——
  86. // 不是"名字被缩短"这么轻, 是整张表错位, 所以建列时必须拒绝
  87. // (见 validateFieldsForSchema)。
  88. FieldNameSize = 255
  89. // MaxIndexFields 一个索引最多允许的分量列数。
  90. //
  91. // 上限取 16: IndexMeta.FieldIDs 是定长 [16]byte, 超出的分量根本无处落盘
  92. // (会被静默丢弃, 于是索引只覆盖了前 16 列却声称覆盖全部)。见 IndexMetaPage.CreateIndex。
  93. //
  94. // 导出给库外的调用方: 迁移侧要能提示"源库这个索引有几列、VRTX 只到多少列"
  95. // (SQLite 的索引列数几乎没有限制, 直接建会撞上 core 的闸门)。
  96. MaxIndexFields = 16
  97. )
  98. // =============================================================================
  99. // BufferPool 配置
  100. // =============================================================================
  101. const (
  102. // DefaultBufferPoolCapacity 默认缓冲池容量 (页数)
  103. // 1000 页 × 16KB = ~16MB (嵌入式/资源受限场景)
  104. // - 最小安全值: 20页 (低于此值可能死锁)
  105. // - 推荐配置: 1000-5000页 (16-78MB)
  106. // - 高性能场景: 10000+页 (156MB+)
  107. DefaultBufferPoolCapacity = 1000
  108. )
  109. // =============================================================================
  110. // WAL 与自动提交配置
  111. // =============================================================================
  112. const (
  113. // DefaultDirtyThreshold 脏页阈值 (页数)
  114. // BufferPool 中脏页数量超过此值时,自动触发 Commit 刷盘
  115. // 注意: 这是 BufferPool 级别的计数,不是事务级别
  116. // - 500 页 × 16KB = ~8MB (平衡安全性、性能和内存)
  117. // - 断电/崩溃时最多丢失 8MB 数据 (约 10,000 行)
  118. // - 性能接近最优 (比 2000 页仅慢 1.5%)
  119. // - 内存占用合理 (比 2000 页减少 75%)
  120. DefaultDirtyThreshold = 500
  121. // DefaultAutoCommitDelay 默认防抖自动提交延迟
  122. // 每次写操作重置计时器,停止操作后该时间内无新操作则自动 Commit
  123. // - 适用场景: 低频写入、交互式应用
  124. // - 禁用方法: 设置为 0 可完全禁用防抖,仅靠脏页阈值触发
  125. DefaultAutoCommitDelay = time.Second
  126. )