dialector.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. // Package gormvrtx 把 TsDB2 (vrtxsql database/sql 驱动) 接入 GORM:
  2. // CRUD 走 GORM 生成 SQL → vrtx 驱动执行; schema 自省 (AutoMigrate) 不经过 SQL,
  3. // 由 Migrator 直接调引擎 API。
  4. //
  5. // 引擎是嵌入式单写者存储, 与 mysql/pgx 这类网络库有两个本质差别, 本包据此裁剪:
  6. // - 无版本协商、无嵌套事务 (SavePoint/RollbackTo 不声明, 让 GORM 默认行为生效);
  7. // - 行存储即主键索引, 表必须有整型主键列承载。
  8. package gormvrtx
  9. import (
  10. "context"
  11. "database/sql"
  12. "fmt"
  13. "reflect"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "gorm.io/gorm"
  19. "gorm.io/gorm/callbacks"
  20. "gorm.io/gorm/clause"
  21. "gorm.io/gorm/schema"
  22. "git.ycl.pub/yl32940/TsDB2/DB/core"
  23. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  24. vrtxsql "git.ycl.pub/yl32940/TsDB2/DB/driver"
  25. )
  26. // DriverName 是 vrtxsql 注册的 database/sql 驱动名。
  27. const DriverName = "vrtx"
  28. // Config Dialector 配置。
  29. type Config struct {
  30. DriverName string // database/sql 驱动名, 默认 "vrtx"
  31. DSN string // 数据库文件路径 (可带 ?mode=ro / ?cache=private)
  32. }
  33. // Dialector 实现 gorm.Dialector。
  34. type Dialector struct {
  35. *Config
  36. db *core.DB // Initialize 时经 SharedDB 取得的引擎实例, Migrator 直接用
  37. sqlDB *sql.DB // 连接池本体 (供调用方 Close 时正确释放)
  38. }
  39. var _ gorm.Dialector = (*Dialector)(nil)
  40. // Open 以默认配置打开: DriverName="vrtx", DSN=文件路径。
  41. func Open(dsn string) gorm.Dialector {
  42. return &Dialector{Config: &Config{DriverName: DriverName, DSN: dsn}}
  43. }
  44. // New 按配置构造 Dialector; DriverName 为空时用 "vrtx"。
  45. func New(config Config) gorm.Dialector {
  46. if config.DriverName == "" {
  47. config.DriverName = DriverName
  48. }
  49. return &Dialector{Config: &config}
  50. }
  51. // Name 方言名。
  52. func (d *Dialector) Name() string { return "vrtx" }
  53. // Initialize 打开驱动连接池与引擎实例。不做任何 SELECT VERSION() 探测 ——
  54. // 引擎无版本概念, 打开时的 CRC/WAL 恢复即完整性校验。
  55. //
  56. // 两个引擎相关的适配点:
  57. // - SetMaxOpenConns(16): 引擎已是一写多读模型, 允许多个读者并发。
  58. // 写锁由 core.accMu.Lock() 排他保证 (Begin/Commit/DDL 拿写锁), 读锁
  59. // 由 core.accMu.RLock() 允许多读者并行 —— 连接池容量要 ≥ driver 层
  60. // shared.sem (16), 否则并发读在 sql.DB 层就被串死了。
  61. // - ConnPool 包一层 rewritePool: 内联 LIMIT/OFFSET 整数参数 (驱动解析器支持限定名)
  62. // (旧解析器只认裸列名/字面量 LIMIT, 该包装让 GORM 的 SQL 形态稳定可用)。
  63. func (d *Dialector) Initialize(db *gorm.DB) error {
  64. if d.DriverName == "" {
  65. d.DriverName = DriverName
  66. }
  67. sqlDB, err := sql.Open(d.DriverName, d.DSN)
  68. if err != nil {
  69. return err
  70. }
  71. sqlDB.SetMaxOpenConns(16)
  72. eng, err := vrtxsql.SharedDB(d.DSN)
  73. if err != nil {
  74. _ = sqlDB.Close()
  75. return fmt.Errorf("vrtx: 打开引擎 %s: %w", d.DSN, err)
  76. }
  77. d.db = eng
  78. d.sqlDB = sqlDB
  79. db.ConnPool = rewritePool{DB: sqlDB}
  80. // GORM v1.25 的 Open 不注册默认回调 —— 由 Dialector 负责 (与 mysql/sqlite 驱动一致)。
  81. callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{})
  82. // vrtx 的 SQL INSERT 语义要求显式提供整型主键 (引擎行存储定位键), 不像
  83. // AUTO_INCREMENT 那样省略列由库自增回填; 而 gorm 只有 tag 显式 autoIncrement
  84. // 才认为主键有库侧默认值 (LastInsertId 回填路径只对这类字段生效)。
  85. // 折衷: 建 create 前回调, 用引擎游标预分配主键写进对象 —— INSERT 带上该值走
  86. // 显式主键路径 (预留号免查重), 行为对齐"自增主键 + 回填"的直觉。
  87. db.Callback().Create().Before("gorm:create").Register("vrtx:assign_pk", d.assignPKCallback)
  88. return nil
  89. }
  90. // assignPKCallback create 前回调: 零值整型主键 → 引擎 AllocPKID 预分配并写回对象。
  91. func (d *Dialector) assignPKCallback(tx *gorm.DB) {
  92. stmt := tx.Statement
  93. if stmt == nil || stmt.Schema == nil || stmt.DryRun {
  94. return
  95. }
  96. pk := stmt.Schema.PrioritizedPrimaryField
  97. if pk == nil {
  98. return
  99. }
  100. kind := pk.IndirectFieldType.Kind()
  101. isUint := kind == reflect.Uint || kind == reflect.Uint8 || kind == reflect.Uint16 ||
  102. kind == reflect.Uint32 || kind == reflect.Uint64
  103. isInt := kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 ||
  104. kind == reflect.Int32 || kind == reflect.Int64
  105. if !isUint && !isInt {
  106. return
  107. }
  108. // 列被 Select/Omit 排除时跳过 (INSERT 不会带它, 预分配反而写错对象)。
  109. selectColumns, restricted := stmt.SelectAndOmitColumns(true, false)
  110. if v, present := selectColumns[pk.DBName]; present {
  111. if !v {
  112. return
  113. }
  114. } else if restricted {
  115. return
  116. }
  117. tbl, ok := d.db.FindTable(stmt.Table)
  118. if !ok {
  119. return // 表不存在: 让 INSERT 自己报错
  120. }
  121. // ⭐ AllocPKID 必须在【逐行闭包内】调用: 批量创建 (切片) 时每行都要独立
  122. // 主键号 — 在循环外只取一次, 切片所有行会共用同一个号, 第二行起被引擎
  123. // 按重复主键拒掉 (实测: 监控批量保存 Create(&metrics) 3 行共用一号全灭)。
  124. // 号只增不减, 逐行分配产生的跳号无害。
  125. assign := func(rv reflect.Value) {
  126. _, isZero := pk.ValueOf(stmt.Context, rv)
  127. if !isZero {
  128. return
  129. }
  130. id := tbl.AllocPKID()
  131. if isUint {
  132. tx.AddError(pk.Set(stmt.Context, rv, id))
  133. } else {
  134. tx.AddError(pk.Set(stmt.Context, rv, int64(id)))
  135. }
  136. }
  137. rv := stmt.ReflectValue
  138. for rv.Kind() == reflect.Ptr {
  139. if rv.IsNil() {
  140. return
  141. }
  142. rv = rv.Elem()
  143. }
  144. switch rv.Kind() {
  145. case reflect.Struct:
  146. assign(rv)
  147. case reflect.Slice, reflect.Array:
  148. for i := 0; i < rv.Len(); i++ {
  149. assign(rv.Index(i))
  150. }
  151. }
  152. }
  153. // Migrator 返回自定义 Migrator: schema 自省直接调引擎 API (不经 SQL)。
  154. func (d *Dialector) Migrator(db *gorm.DB) gorm.Migrator {
  155. return Migrator{db: db, eng: d.db, dsn: d.DSN, dialector: d}
  156. }
  157. // explicitTypeMapping 小型 SQL 类型映射: 把 gorm:"type:..." 标签里用户显式声明的
  158. // SQL 类型归一化成引擎侧类型名 (前缀 = 空白或 '(' 之前的部分, 故 "bigint unsigned"
  159. // 归 BIGINT、"varchar(500)" 归 VARCHAR 且宽度取括号值)。独立实现, 不依赖 DB/driver
  160. // 的 SQL 文本解析。第三个返回值为 false 表示不认识 (decimal/json/...), 调用方落回
  161. // Go 类型的默认映射并注释说明。
  162. func explicitTypeMapping(tag string) (string, int, bool) {
  163. t := strings.ToLower(strings.TrimSpace(tag))
  164. if t == "" {
  165. return "", 0, false
  166. }
  167. name := t
  168. if i := strings.IndexAny(name, "( "); i >= 0 {
  169. name = name[:i]
  170. }
  171. size := 0
  172. if i := strings.IndexByte(t, '('); i >= 0 {
  173. if j := strings.IndexByte(t[i:], ')'); j > 0 {
  174. size, _ = strconv.Atoi(strings.TrimSpace(t[i+1 : i+j]))
  175. }
  176. }
  177. switch name {
  178. case "bigint":
  179. return "BIGINT", 0, true
  180. case "int":
  181. return "INT", 0, true
  182. case "varchar", "char":
  183. return "VARCHAR", size, true
  184. case "text":
  185. return "TEXT", 0, true
  186. case "double":
  187. return "DOUBLE", 0, true
  188. case "bool", "boolean":
  189. return "BOOLEAN", 0, true
  190. case "datetime":
  191. return "DATETIME", 0, true
  192. }
  193. return "", 0, false
  194. }
  195. // DataTypeOf GORM 字段 → 引擎理解的 SQL 类型名 (供 FullDataTypeOf / 高级用法;
  196. // 建表本身走引擎 API, 不经 SQL)。
  197. //
  198. // 显式 gorm:"type:..." 标签优先 (报告 #10 的逃生门: string 字段默认无 size 会落成
  199. // Blob 链式列, 带索引时引擎拒绝建索引 —— 用 type:varchar(500) 精确声明即可绕过
  200. // 默认映射): 按 explicitTypeMapping 解析, 认识 → 按其返回; 不认识 → 保持默认映射
  201. // 并附注释说明。
  202. // 默认映射: string 带尺寸 → VARCHAR(size), 超过引擎行内定长上限 2048 时截断声明并
  203. // 注释说明; 无尺寸 → BLOB (链式存储)。
  204. //
  205. // 无尺寸 string 落 BLOB 而不是旧行为的 TEXT (动态列): 动态列单值上限 16360 字节
  206. // (types.MaxStringSize), 运行时写入更大的文本会撞墙; Blob 列行内 8B 引用 + 链页,
  207. // 单值上限约 4GB。读回时驱动把链内容物化成 []byte, GORM 照常扫进 string。
  208. // 权衡: 该列失去 LIKE 与索引能力 (行内只有引用, 按值查不到) —— 需要可搜索的大文本
  209. // 请给 size:1..2048 走定长档, 或把检索字段拆出来单独建列。
  210. func (d *Dialector) DataTypeOf(field *schema.Field) string {
  211. if tag, ok := field.TagSettings["TYPE"]; ok {
  212. if name, size, ok := explicitTypeMapping(tag); ok {
  213. switch name {
  214. case "TEXT":
  215. // 显式 type:text 与无 size 默认同一归宿: Blob 链式列 (权衡见上)。
  216. return "BLOB /* 不定长大文本: 链式存储, 单值上限 ~4GB; 失去 LIKE/索引能力, 需要可搜索文本请用 size:1..2048 */"
  217. case "VARCHAR":
  218. switch {
  219. case size <= 0:
  220. return "VARCHAR /* 声明无宽度, 建列按动态列处理 */"
  221. case size > types.MaxInlineColumnWidth:
  222. return fmt.Sprintf("VARCHAR(2048) /* 声明 %d 超引擎行内上限 2048, 截断 */", size)
  223. default:
  224. return fmt.Sprintf("VARCHAR(%d)", size)
  225. }
  226. default:
  227. return name
  228. }
  229. }
  230. return fmt.Sprintf("%s /* 未知 type:%s, 按 Go 类型默认映射 */", d.defaultDataTypeOf(field), tag)
  231. }
  232. return d.defaultDataTypeOf(field)
  233. }
  234. // defaultDataTypeOf 按 Go 类型给默认 SQL 类型名 (DataTypeOf 的回落分支)。
  235. //
  236. // 无尺寸 string 落 BLOB (链式存储): 动态列单值上限 16360 字节, 不定长大文本运行时会
  237. // 撞墙; Blob 列行内 8B 引用 + 链页 (~4GB)。代价是失去 LIKE/索引能力 —— 需要可搜索
  238. // 文本时给 size:1..2048 走定长档。读回经驱动物化成 []byte, GORM 可扫进 string。
  239. func (d *Dialector) defaultDataTypeOf(field *schema.Field) string {
  240. switch field.DataType {
  241. case schema.Uint:
  242. return "BIGINT UNSIGNED"
  243. case schema.Int:
  244. return "BIGINT"
  245. case schema.Float:
  246. return "DOUBLE"
  247. case schema.String:
  248. if field.Size > 0 {
  249. if field.Size > types.MaxInlineColumnWidth {
  250. return fmt.Sprintf("VARCHAR(2048) /* 声明 %d 超引擎行内上限 2048, 截断 */", field.Size)
  251. }
  252. return fmt.Sprintf("VARCHAR(%d)", field.Size)
  253. }
  254. return "BLOB"
  255. case schema.Bool:
  256. return "BOOLEAN"
  257. case schema.Time:
  258. return "DATETIME" // 引擎约定: 行内 8B Unix 毫秒
  259. case schema.Bytes:
  260. return "BLOB"
  261. default:
  262. // struct / JSON 等: 序列化为文本存动态列
  263. return "TEXT"
  264. }
  265. }
  266. // DefaultValueOf 引擎不支持列级 DEFAULT (未提供的列按 NULL/零值裁决), 给出占位表达式。
  267. func (d *Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
  268. return clause.Expr{SQL: "DEFAULT"}
  269. }
  270. // BindVarTo 占位符: 与 mysql 一致, 一律 "?" (驱动只认 ? 占位符)。
  271. func (d *Dialector) BindVarTo(w clause.Writer, stmt *gorm.Statement, v interface{}) {
  272. w.WriteByte('?')
  273. }
  274. // QuoteTo 反引号包裹; 含 "." 时按段分别引用: t.col → `t`.`col`。
  275. func (d *Dialector) QuoteTo(w clause.Writer, s string) {
  276. for i, part := range strings.Split(s, ".") {
  277. if i > 0 {
  278. w.WriteByte('.')
  279. }
  280. w.WriteByte('`')
  281. w.WriteString(strings.ReplaceAll(part, "`", "``"))
  282. w.WriteByte('`')
  283. }
  284. }
  285. // Explain 把 "?" 按序替换为字面值 (字符串单引号包裹并转义, 数字直写,
  286. // bool → 1/0, nil → NULL)。仅用于 SQL 日志展示。
  287. func (d *Dialector) Explain(sql string, vars ...interface{}) string {
  288. var sb strings.Builder
  289. idx := 0
  290. for i := 0; i < len(sql); i++ {
  291. c := sql[i]
  292. if c != '?' || idx >= len(vars) {
  293. sb.WriteByte(c)
  294. continue
  295. }
  296. sb.WriteString(literalOf(vars[idx]))
  297. idx++
  298. }
  299. return sb.String()
  300. }
  301. // literalOf 把一个参数值格式化为 SQL 字面量。
  302. func literalOf(v interface{}) string {
  303. switch x := v.(type) {
  304. case nil:
  305. return "NULL"
  306. case string:
  307. return "'" + strings.ReplaceAll(x, "'", "''") + "'"
  308. case []byte:
  309. return "'" + strings.ReplaceAll(string(x), "'", "''") + "'"
  310. case bool:
  311. if x {
  312. return "1"
  313. }
  314. return "0"
  315. case time.Time:
  316. return "'" + x.Format("2006-01-02 15:04:05.999") + "'"
  317. }
  318. rv := reflect.ValueOf(v)
  319. switch rv.Kind() {
  320. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  321. return strconv.FormatInt(rv.Int(), 10)
  322. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
  323. return strconv.FormatUint(rv.Uint(), 10)
  324. case reflect.Float32, reflect.Float64:
  325. return strconv.FormatFloat(rv.Float(), 'g', -1, 64)
  326. }
  327. return "'" + strings.ReplaceAll(fmt.Sprint(v), "'", "''") + "'"
  328. }
  329. // =============================================================================
  330. // rewritePool: SQL 进驱动前的轻量改写 (内联 LIMIT/OFFSET 整数参数)
  331. // =============================================================================
  332. // 注: 早期驱动解析器不支持表限定列名, 这里曾剥掉 "`t`." 前缀; 驱动现已原生
  333. // 支持限定名 (单表剥限定 + JOIN 按别名归属), 剥前缀反而会制造歧义 (JOIN 下
  334. // 两表同名列), 故只保留 LIMIT/OFFSET 参数内联。
  335. // limitClauseRe 匹配 LIMIT/OFFSET 占位符形态 (GORM 把 Limit(1) 编成 "LIMIT ?",
  336. // 而 vrtx 解析器只接受字面量整数, 需要按参数内联)。
  337. var limitClauseRe = regexp.MustCompile(`(?i)\b(LIMIT|OFFSET)[ \t\r\n]+\?`)
  338. // rewriteSQL 进驱动前的统一改写: 内联 LIMIT/OFFSET 整数参数 (限定名保持原样)。
  339. func rewriteSQL(query string, args []interface{}) (string, []interface{}) {
  340. if !limitClauseRe.MatchString(query) {
  341. return query, args
  342. }
  343. // 找出 LIMIT/OFFSET 子句里 '?' 是整条 SQL 的第几个 '?' (即参数下标),
  344. // 把它替换成字面量并把对应参数从 args 剔除 (后面的参数依次前移)。
  345. mark := map[int]string{} // 第k个 '?' → 字面量
  346. for _, loc := range limitClauseRe.FindAllStringSubmatchIndex(query, -1) {
  347. qpos := strings.IndexByte(query[loc[0]:loc[1]], '?')
  348. if qpos < 0 {
  349. continue
  350. }
  351. abs := loc[0] + qpos
  352. k := 0
  353. for i := 0; i < abs; i++ {
  354. if query[i] == '?' {
  355. k++
  356. }
  357. }
  358. if k >= len(args) {
  359. continue
  360. }
  361. mark[k] = intLiteral(args[k])
  362. }
  363. if len(mark) == 0 {
  364. return query, args
  365. }
  366. var sb strings.Builder
  367. argIdx := 0
  368. for i := 0; i < len(query); i++ {
  369. c := query[i]
  370. if c != '?' {
  371. sb.WriteByte(c)
  372. continue
  373. }
  374. if lit, ok := mark[argIdx]; ok {
  375. sb.WriteString(lit)
  376. } else {
  377. sb.WriteByte('?')
  378. }
  379. argIdx++
  380. }
  381. kept := make([]interface{}, 0, len(args)-len(mark))
  382. for i, a := range args {
  383. if _, ok := mark[i]; !ok {
  384. kept = append(kept, a)
  385. }
  386. }
  387. return sb.String(), kept
  388. }
  389. // intLiteral 把 LIMIT/OFFSET 参数格式化为整数字面量; 不认识的类型原样占位返回 "0"。
  390. func intLiteral(v interface{}) string {
  391. switch x := v.(type) {
  392. case int:
  393. return strconv.Itoa(x)
  394. case int8:
  395. return strconv.FormatInt(int64(x), 10)
  396. case int16:
  397. return strconv.FormatInt(int64(x), 10)
  398. case int32:
  399. return strconv.FormatInt(int64(x), 10)
  400. case int64:
  401. return strconv.FormatInt(x, 10)
  402. case uint:
  403. return strconv.FormatUint(uint64(x), 10)
  404. case uint8:
  405. return strconv.FormatUint(uint64(x), 10)
  406. case uint16:
  407. return strconv.FormatUint(uint64(x), 10)
  408. case uint32:
  409. return strconv.FormatUint(uint64(x), 10)
  410. case uint64:
  411. return strconv.FormatUint(x, 10)
  412. case float64:
  413. return strconv.FormatInt(int64(x), 10)
  414. case string:
  415. return x
  416. }
  417. return "0"
  418. }
  419. // rewritePool 包装 *sql.DB, SQL 进驱动前统一过 rewriteSQL (剥 “ `t`. “ 限定前缀、
  420. // 内联 LIMIT/OFFSET)。嵌入 *sql.DB 顺带获得 BeginTx (TxBeginner) 等全部池能力。
  421. type rewritePool struct {
  422. *sql.DB
  423. }
  424. func (p rewritePool) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
  425. q, _ := rewriteSQL(query, nil)
  426. return p.DB.PrepareContext(ctx, q)
  427. }
  428. func (p rewritePool) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
  429. q, a := rewriteSQL(query, args)
  430. return p.DB.ExecContext(ctx, q, a...)
  431. }
  432. func (p rewritePool) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
  433. q, a := rewriteSQL(query, args)
  434. return p.DB.QueryContext(ctx, q, a...)
  435. }
  436. func (p rewritePool) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
  437. q, a := rewriteSQL(query, args)
  438. return p.DB.QueryRowContext(ctx, q, a...)
  439. }