db.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. package DB
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB/core"
  4. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  5. "fmt"
  6. "reflect"
  7. "strings"
  8. "time"
  9. )
  10. // DB ORM 层数据库入口
  11. type DB struct {
  12. coreDB *core.DB
  13. }
  14. // Open 打开或创建数据库
  15. func Open(path string) (*DB, error) {
  16. cdb, err := core.Open(path)
  17. if err != nil {
  18. return nil, fmt.Errorf("vrtx: open %s: %w", path, err)
  19. }
  20. return &DB{coreDB: cdb}, nil
  21. }
  22. // Close 关闭数据库
  23. func (db *DB) Close() error {
  24. return db.coreDB.Close()
  25. }
  26. // Commit 显式提交 — 通过 WAL 保证原子性
  27. func (db *DB) Commit() error {
  28. return db.coreDB.Commit()
  29. }
  30. // SetAutoCommitDelay 设置防抖自动提交延迟
  31. // delay=0 表示禁用自动提交 (仅手动 Commit)
  32. // 默认值为 1 秒: 每次写操作重置计时器, 停止操作 1 秒后自动 Commit
  33. func (db *DB) SetAutoCommitDelay(d time.Duration) {
  34. db.coreDB.SetAutoCommitDelay(d)
  35. }
  36. // SetDirtyThreshold 设置脏页阈值(页数),超过此值自动触发 Commit
  37. func (db *DB) SetDirtyThreshold(threshold int) {
  38. db.coreDB.SetDirtyThreshold(threshold)
  39. }
  40. // SetCommitPreset 设置提交策略预设
  41. // preset: "batch" | "online" | "safe" | "ultra-safe"
  42. // - batch: 离线批量导入 (500页, 1s超时) - 高吞吐,可接受少量数据丢失风险
  43. // - online: 在线业务默认 (100页, 500ms超时) - 平衡性能和安全性
  44. // - safe: 金融/医疗关键数据 (20页, 100ms超时) - 最高安全性,性能较低
  45. // - ultra-safe: 极端安全场景 (40页, 50ms超时) - 极致安全,性能很低(慢5-8倍)
  46. //
  47. // 注意: SetCommitPreset 之后**不要再调用 SetAutoCommitDelay(0)** —— delay=0 会
  48. // 完全禁用防抖, 让阈值自动 commit 也无效, 退化成"每行 fsync"。如需调整防抖时长,
  49. // 用 SetAutoCommitDelay(time.Millisecond*200) 这种非零值。
  50. func (db *DB) SetCommitPreset(preset string) {
  51. switch preset {
  52. case "batch":
  53. // 离线批量导入:高吞吐
  54. db.SetDirtyThreshold(500) // 8MB
  55. db.SetAutoCommitDelay(time.Second) // 1s
  56. case "online":
  57. // 在线业务默认:平衡
  58. db.SetDirtyThreshold(100) // 1.6MB
  59. db.SetAutoCommitDelay(500 * time.Millisecond) // 500ms
  60. case "safe":
  61. // 关键数据:最高安全
  62. db.SetDirtyThreshold(20) // 320KB
  63. db.SetAutoCommitDelay(100 * time.Millisecond) // 100ms
  64. case "ultra-safe":
  65. // 极端安全:每40页 Commit,断电最多丢失 ~800行
  66. db.SetDirtyThreshold(40) // 640KB
  67. db.SetAutoCommitDelay(50 * time.Millisecond) // 50ms
  68. default:
  69. // 未知预设,使用默认值
  70. db.SetDirtyThreshold(500)
  71. db.SetAutoCommitDelay(time.Second)
  72. }
  73. }
  74. // LockWrites 锁定写入 —— 锁的是"页落盘"(WritePage), 不是写入动作本身。
  75. //
  76. // 锁定期 Insert/Update/Delete 照常改内存中的页并标记脏, 只有真正要写磁盘时才会阻塞在锁上;
  77. // 没有任何"写入被拒绝"的错误码。Commit/Flush 期间引擎内部会调用它, 目的是拿一个不再变化的
  78. // 磁盘视图。适用场景: 批量导入前、维护操作期间、备份时 (期望这几步之间磁盘内容保持不变)。
  79. func (db *DB) LockWrites() {
  80. db.coreDB.LockWrites()
  81. }
  82. // UnlockWrites 解锁写入 (见 LockWrites 的语义说明)
  83. func (db *DB) UnlockWrites() {
  84. db.coreDB.UnlockWrites()
  85. }
  86. // SetBufferPoolCapacity 设置 BufferPool 容量(页数)
  87. func (db *DB) SetBufferPoolCapacity(capacity int) {
  88. db.coreDB.SetBufferPoolCapacity(capacity)
  89. }
  90. // BeginTransaction 开启新事务
  91. // 返回 Transaction 对象,用于 Commit/Rollback
  92. //
  93. // 使用示例:
  94. //
  95. // txn := db.BeginTransaction()
  96. // defer func() {
  97. // if err := recover(); err != nil {
  98. // txn.Rollback() // 异常时回滚
  99. // panic(err)
  100. // }
  101. // }()
  102. //
  103. // tbl.Insert(&Record{...})
  104. // tbl.Update(...)
  105. //
  106. // if err := txn.Commit(); err != nil {
  107. // return err // 提交失败
  108. // }
  109. func (db *DB) BeginTransaction() *core.Transaction {
  110. return db.coreDB.BeginTransaction()
  111. }
  112. // DebugDBState 打印数据库调试信息 (调试用)
  113. func (db *DB) DebugDBState() {
  114. db.coreDB.DebugDBState()
  115. }
  116. // DebugTableState 打印表调试信息 (调试用)
  117. func (db *DB) DebugTableState(tableName string) {
  118. db.coreDB.DebugTableState(tableName)
  119. }
  120. // checkNameBudget 在建表 / 建索引之前, 把自动命名额外吃掉的字节预先算清楚。
  121. //
  122. // 为什么必须先算:
  123. // - 表名和索引名各有各的定长上限: 表名 TableNameSize(48)、索引名 IndexNameSize(64);
  124. // 而自动命名要在索引名里再加前缀 —— pk_<表名>、idx_<表名>_<列名> —— 索引名会额外多吃几个字节;
  125. // - 不预检的话, 表会先建成功, 失败发生在紧接着的 CreateIndex 上: 库里留下半成品的表,
  126. // 而错误信息只有"索引名过长", 完全看不出根因其实是表名 / 列名太长。
  127. //
  128. // 报错里把「是哪个名字、多少字节、上限多少」一次讲清楚。
  129. func checkNameBudget(tableName string, indexes []IndexDef) error {
  130. if len(tableName) > core.TableNameSize {
  131. return fmt.Errorf("%w: 表名 %q 有 %d 字节, 上限 %d",
  132. core.ErrTableNameTooLong, tableName, len(tableName), core.TableNameSize)
  133. }
  134. // 主键索引名 pk_<表名> 必须塞进 IndexNameSize。
  135. // 当前取值下 64-48-3 = 13 > 0, 所以这条不会被触发 (文件下方的编译期不变量保证了它);
  136. // 留着是为了将来收紧 IndexNameSize 时, 拦截点仍然落在建表之前,
  137. // 而不是回退成"表建好了、紧跟着建 pk 索引才失败"的半成品状态。
  138. if name := pkIndexPrefix + tableName; len(name) > core.IndexNameSize {
  139. return fmt.Errorf("%w: 自动主键索引名 %q 有 %d 字节 (表名 %q 的 %d 字节 + 前缀 %q), 上限 %d",
  140. core.ErrIndexNameTooLong, name, len(name), tableName, len(tableName), pkIndexPrefix, core.IndexNameSize)
  141. }
  142. for _, idx := range indexes {
  143. if name := idx.indexName(tableName); len(name) > core.IndexNameSize {
  144. return fmt.Errorf("%w: 自动二级索引名 %q 有 %d 字节 (上限 %d) —— 用 `vrtx:\"index:<名字>\"` 显式命名, 或缩短表名 / 首列名",
  145. core.ErrIndexNameTooLong, name, len(name), core.IndexNameSize)
  146. }
  147. }
  148. return nil
  149. }
  150. // 编译期不变量: 表名合法 (<= TableNameSize) 时, 自动主键索引名 pk_<表名> 必须仍然合法,
  151. // 即 IndexNameSize >= TableNameSize + len(pkIndexPrefix)。差值为负时 uint 转换直接编译失败,
  152. // 把这层依赖钉在编译期, 而不是靠注释和自觉。
  153. const _ = uint(core.IndexNameSize - core.TableNameSize - len(pkIndexPrefix))
  154. // UseTable 获取或创建泛型表
  155. // 如果表不存在,会自动创建表和索引
  156. // 如果表已存在,会自动同步 schema:struct 多的字段自动添加,少的字段自动删除
  157. //
  158. // T 必须内嵌 DB.BaseModel / DB.PrimaryModel (主键表) 或 DB.TimeSeriesModel (时序表):
  159. // 内嵌基类既是"这是一张本框架的表"的标记, 也是对象头部增删改查的来源, 所以没有内嵌
  160. // 基类的 T 会被直接拒绝 (ErrNotModel), 不会留下一张半成品表。主键列可由每张表用
  161. // `vrtx:"pk"` 覆盖基类的默认键列 (_id / _ts)。
  162. //
  163. // 用法1: 自动推导表名(推荐)
  164. //
  165. // tbl, _ := DB.UseTable[Employee](db) // 表名自动为 "employee"
  166. //
  167. // 用法2: 手动指定表名
  168. //
  169. // tbl, _ := DB.UseTable[Employee](db, "emp_table") // 自定义表名
  170. func UseTable[T any](db *DB, name ...string) (*Table[T], error) {
  171. var zero T
  172. model, err := ParseModel(&zero)
  173. if err != nil {
  174. return nil, fmt.Errorf("vrtx: parse model: %w", err)
  175. }
  176. // 自动推导表名:从类型名转为小写(如 Employee -> employee)
  177. tableName := typeNameToTableName(zero)
  178. if len(name) > 0 && name[0] != "" {
  179. tableName = name[0] // 用户指定的表名优先
  180. }
  181. // 建表前先把「自动命名要吃掉多少字节」算清楚。pk_ / idx_ 前缀都占用
  182. // core.TableNameSize 的额度, 不提前拦就会出现"表建好了、紧跟着建 pk 索引才失败"
  183. // 的半成品状态, 而且报错只会喊"索引名过长", 把根因 (表名太长) 指偏。
  184. // 表已存在的分支也要查: 新加一个自动命名的二级索引会撞同一个坑。
  185. if err := checkNameBudget(tableName, model.Indexes); err != nil {
  186. return nil, err
  187. }
  188. // 尝试查找已有表
  189. coreTbl, ok := db.coreDB.FindTable(tableName)
  190. if !ok {
  191. // 表不存在,创建新表
  192. coreTbl, err = db.coreDB.CreateTable(tableName, model.CoreFields)
  193. if err != nil {
  194. return nil, fmt.Errorf("vrtx: create table %s: %w", tableName, err)
  195. }
  196. // 创建一级索引
  197. idxName := pkIndexPrefix + tableName
  198. _, err = coreTbl.CreateIndex(idxName, model.GetIndexType(), []byte{model.CorePKField})
  199. if err != nil {
  200. return nil, fmt.Errorf("vrtx: create index for %s: %w", tableName, err)
  201. }
  202. // 自动创建标签声明的二级索引 (同名字段已合并为复合索引)
  203. for _, idx := range model.Indexes {
  204. name := idx.indexName(tableName)
  205. if _, err := coreTbl.CreateIndex(name, idx.IndexType, idx.coreFieldIDs(), idx.Unique); err != nil {
  206. return nil, fmt.Errorf("vrtx: create index %s: %w", name, err)
  207. }
  208. }
  209. } else {
  210. // 表已存在,自动同步 schema
  211. if err := syncSchema(coreTbl, model); err != nil {
  212. return nil, err
  213. }
  214. // 检查并补充缺失的二级索引
  215. for _, idx := range model.Indexes {
  216. name := idx.indexName(tableName)
  217. m, found := coreTbl.IndexMeta().FindIndex(name)
  218. if !found {
  219. if _, err := coreTbl.CreateIndex(name, idx.IndexType, idx.coreFieldIDs(), idx.Unique); err != nil {
  220. return nil, fmt.Errorf("vrtx: create index %s: %w", name, err)
  221. }
  222. continue
  223. }
  224. // 已存在: 校验分量列表与模型一致。
  225. // 不一致说明索引是按旧结构建的 (例如后来才把同名字段改成复合索引),
  226. // 静默沿用会一直用一个"少一列"的索引 —— 查询结果对但索引形同虚设,
  227. // 这里直接报错, 让人决定是重建还是改名。
  228. if !sameIndexFields(m, idx.coreFieldIDs()) {
  229. return nil, fmt.Errorf("vrtx: 索引 %s 已存在但分量列表不同 (已有 %d 列, 模型期望 %d 列), 请先删除该索引",
  230. name, m.FieldCnt, len(idx.coreFieldIDs()))
  231. }
  232. // 模型声明了 unique 而库里那个索引不是唯一的 → 约束根本不会生效。
  233. // 静默沿用等于"模型说唯一、写入却允许重复", 是最危险的一类不一致
  234. // (只在数据脏了以后才发现), 直接报错要求重建。
  235. if idx.Unique && !m.Unique() {
  236. return nil, fmt.Errorf("vrtx: 索引 %s 已存在但不是唯一索引 (模型声明了 unique), 请先删除该索引后重建", name)
  237. }
  238. }
  239. }
  240. tbl := &Table[T]{
  241. db: db,
  242. coreTable: coreTbl,
  243. model: model,
  244. name: tableName,
  245. }
  246. tbl.refreshActiveColumns()
  247. return tbl, nil
  248. }
  249. // sameIndexFields 比较已有索引的分量列表与模型期望是否一致 (顺序敏感)
  250. func sameIndexFields(m *core.IndexMeta, want []byte) bool {
  251. n := int(m.FieldCnt)
  252. if n != len(want) {
  253. return false
  254. }
  255. for i := 0; i < n; i++ {
  256. if m.FieldIDs[i] != want[i] {
  257. return false
  258. }
  259. }
  260. return true
  261. }
  262. // isPKColumn 判断列名是否是主键列。
  263. // 主键列不参与 schema 同步: 它在建表时按模型建好, 一旦被 DropField 删掉,
  264. // 行里的主键值就再也读不回来 —— 引擎的 pkID 只存在于行的 8 字节前缀里,
  265. // 结构体侧的 pk 字段没有任何别的途径反查。
  266. // 主键列既可能是内嵌基类带来的 _id / _ts, 也可能是用户 vrtx:"pk" 指定的列。
  267. func isPKColumn(model *ModelInfo, name string) bool {
  268. if name == pkColumnName || name == tsColumnName {
  269. return true
  270. }
  271. return model.PKInStruct && name == model.PKField
  272. }
  273. // syncSchema 将 DB schema 同步为与 struct 一致
  274. // struct 多 → AddField;struct 少 → DropField
  275. //
  276. // 除了增删列, 还会校验【已存在列的列级参数】: decimal 的 scale 与字符串列的宽度码都必须
  277. // 与模型一致。两者都是"行布局参数":
  278. // - scale 不一致 → "行内字节序 == 数值序"不再成立 (见 types.FieldType_Decimal), 排序静默出错;
  279. // - 宽度不一致 → 模型以为的容量与库里的实际容量不同 (闸门只认库里的档位), 声明 maxlen:16
  280. // 的列实际能写满 1024 字节, 或反之合法值被拒。
  281. //
  282. // 对不上时直接报错, 而不是让新写入的行用另一套参数 —— 那会让同一列里两种布局混排。
  283. func syncSchema(coreTbl *core.Table, model *ModelInfo) error {
  284. // 构建 struct 字段名集合 (不含主键)
  285. structCols := make(map[string]FieldDef, len(model.Fields))
  286. for _, fd := range model.Fields {
  287. if isPKColumn(model, fd.ColName) {
  288. continue
  289. }
  290. structCols[fd.ColName] = fd
  291. }
  292. // 获取 DB 当前活跃列 (排除主键列)
  293. allDBCols := coreTbl.GetActiveFieldNames()
  294. dbCols := make([]string, 0, len(allDBCols))
  295. dbColSet := make(map[string]bool, len(allDBCols))
  296. for _, name := range allDBCols {
  297. if isPKColumn(model, name) {
  298. continue
  299. }
  300. dbCols = append(dbCols, name)
  301. dbColSet[name] = true
  302. }
  303. // 1. struct 有但 DB 没有 → 添加列 (主键列除外: 它由建表时确定, AddField 造不出主键)
  304. for _, fd := range model.Fields {
  305. if isPKColumn(model, fd.ColName) {
  306. continue
  307. }
  308. if !dbColSet[fd.ColName] {
  309. if err := coreTbl.AddField(types.FieldInfo{
  310. FieldType: fd.FieldType,
  311. FieldName: []byte(fd.ColName),
  312. Flag: fd.Flag,
  313. }); err != nil {
  314. return fmt.Errorf("vrtx: add field %s: %w", fd.ColName, err)
  315. }
  316. continue
  317. }
  318. // 已存在的列: 校验列级参数 (decimal 的 scale / 字符串列的宽度码) 是否与模型一致。
  319. dbFT, _ := coreTbl.GetActiveFieldType(fd.ColName)
  320. dbFlag, _ := coreTbl.GetActiveFieldFlag(fd.ColName)
  321. dbParam := dbFlag & types.FlagScaleMask
  322. if fd.FieldType == types.FieldType_Decimal || dbFT == types.FieldType_Decimal {
  323. if fd.FieldType == types.FieldType_Decimal && dbFT == types.FieldType_Decimal &&
  324. dbParam != fd.Param() {
  325. return fmt.Errorf(
  326. "vrtx: 列 %s 的 decimal scale 与库中不一致 (库中 %d, 模型 %d); "+
  327. "同一列只能有一个 scale, 请对齐模型或迁移/重建该列",
  328. fd.ColName, dbParam, fd.Param())
  329. }
  330. continue
  331. }
  332. // 字符串列 (含动态列): 行内宽度必须与模型声明一致。
  333. //
  334. // 宽度决定"这一列占行内哪几字节", 库里的档位就是真正的闸门 (见 types.FieldSize):
  335. // 库里是 1024 字节档而模型声明 16 字节档时, 模型以为"最多 16 字节"的列其实能写满
  336. // 1024; 反过来库里的档位更窄, 模型认为合法的值会被引擎拒。两种都不会自己暴露,
  337. // 所以在这里一次性说清库中与模型各自的宽度和宽度码。
  338. if types.IsStringType(fd.FieldType) || types.IsStringType(dbFT) ||
  339. types.IsDynamic(fd.FieldType) || types.IsDynamic(dbFT) {
  340. dbW := types.FieldSize(dbFT, dbParam)
  341. modelW := fd.Width()
  342. if dbW != modelW {
  343. return fmt.Errorf(
  344. "vrtx: 列 %s 的行内宽度与库中不一致 (库中 %s: %d 字节/宽度码 %d, "+
  345. "模型 %s: %d 字节/宽度码 %d); 定宽随 schema 落盘, 对不上说明模型与库"+
  346. "是两次不同的声明 — 请对齐 maxlen/type, 或重建该列",
  347. fd.ColName, types.TypeNameOf(dbFT), dbW, dbParam,
  348. types.TypeNameOf(fd.FieldType), modelW, fd.Param())
  349. }
  350. }
  351. }
  352. // 2. DB 有但 struct 没有 → 删除列 (dbCols 已排除主键列)
  353. for _, name := range dbCols {
  354. if _, ok := structCols[name]; !ok {
  355. // 错误必须上抛: 静默继续会留下"库里有、struct 没有"的列,
  356. // 后续 UseTable 反复尝试删除, 库与模型的不一致也无从排查。
  357. // (主键列等删不掉的场景由 DropField 守卫转为明确错误。)
  358. if err := coreTbl.DropField(name); err != nil {
  359. return fmt.Errorf("同步 schema 删除列 %q: %w", name, err)
  360. }
  361. }
  362. }
  363. return nil
  364. }
  365. // DropTable 删除表
  366. func (db *DB) DropTable(name string) error {
  367. return db.coreDB.DropTable(name)
  368. }
  369. // CoreDB 返回底层 core.DB (用于高级操作如创建二级索引)
  370. func (db *DB) CoreDB() *core.DB {
  371. return db.coreDB
  372. }
  373. // VACUUM 整库重写, 让数据库文件真正缩小。
  374. //
  375. // 与自动 compact (40% 失效字节触发) 的区别:
  376. // - 自动 compact: 页内墓碑回收, 文件大小几乎不变
  377. // - VACUUM: 整库重写, 文件真正缩小, 归还所有空闲页给 OS
  378. //
  379. // 任何阶段失败 → 删除临时文件, 旧库保持不变。
  380. // 调用后 db 句柄会被关闭并重新打开, 调用者继续使用即可。
  381. func (db *DB) VACUUM(opts ...core.VACUUMOptions) (*core.VACUUMResult, error) {
  382. return db.coreDB.VACUUM(opts...)
  383. }
  384. // SetAutoVACUUM 开启自动 VACUUM, 周期触发。
  385. //
  386. // interval: 两次 VACUUM 的最小间隔 (例如 1*time.Hour)
  387. // options: 每次 VACUUM 使用的选项
  388. //
  389. // 关闭: SetAutoVACUUM(0)
  390. func (db *DB) SetAutoVACUUM(interval time.Duration, opts ...core.VACUUMOptions) {
  391. db.coreDB.SetAutoVACUUM(interval, opts...)
  392. }
  393. // LastVACUUM 返回上次自动 VACUUM 的时间
  394. func (db *DB) LastVACUUM() time.Time {
  395. return db.coreDB.LastVACUUM()
  396. }
  397. // typeNameToTableName 从类型名推导表名(Employee -> employee)
  398. func typeNameToTableName(v any) string {
  399. t := reflect.TypeOf(v)
  400. name := t.Name()
  401. if name == "" {
  402. return "table"
  403. }
  404. // 首字母小写:Employee -> employee, OrderItem -> orderItem
  405. return strings.ToLower(name[:1]) + name[1:]
  406. }