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 列存在 1/2/4/8byte 上, 是补码大端, 对负数不保序 (等值查得到, 但跨 0 的 // 范围扫描 / ORDER BY 会漏行或混入错误行)。改用保序编码后, 同一列在盘上的字节与旧库 // 不同, 索引键也按新编码重建, 因此不兼容, 需重建。 Version = 7 // 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, IndexMetaPage.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 ) // ============================================================================= // 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 )