f3471ae139
- config 增 DB_DRIVER(mysql 默认 | sqlite);DSN 对 sqlite 为文件路径
- db.OpenDriver 按驱动分派:sqlite 用 modernc(纯 Go 免 CGO)+ WAL/
busy_timeout/foreign_keys/_txlock=immediate;mysql 路径不变
- store.Open 分派;mysql 保留 UTC/collation 断言,sqlite 跳过
- 迁移拆 migrations/{mysql,sqlite}/ 双套,embed 双 FS,migrate 按驱动选源
与 golang-migrate 驱动;修复 m.Close() 误关调用方 *sql.DB 的坑
- cmd/migrate 串入 DB_DRIVER;集成测试 MigrateUp 签名更新
- 新增 SQLite 时间往返 smoke 测试与端到端迁移测试(免 docker)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
2.5 KiB
Go
81 lines
2.5 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
// mysql.ParseDSN is used to structurally rewrite the DSN for UTC.
|
|
"github.com/go-sql-driver/mysql"
|
|
|
|
"github.com/wangjia/pangolin/server/internal/config"
|
|
"github.com/wangjia/pangolin/server/internal/db"
|
|
)
|
|
|
|
// Open is the canonical database entry point for the Pangolin server. It
|
|
// dispatches on cfg.Driver:
|
|
//
|
|
// - sqlite: opens the file/in-memory DB (UTC is native; no session assertion).
|
|
// - mysql (default): structurally overrides the DSN for UTC (ParseTime, Loc,
|
|
// Collation, time_zone), opens the pool, and asserts
|
|
// SELECT @@session.time_zone = "+00:00" (startup-fatal if the server ignores
|
|
// our UTC override — e.g. SQL mode forces a different TZ).
|
|
//
|
|
// The actual connection/pool/ping lives in internal/db; this layer adds only the
|
|
// MySQL-specific UTC rigor.
|
|
func Open(cfg *config.Config) (*sql.DB, error) {
|
|
if db.Normalize(cfg.Driver) == "sqlite" {
|
|
return db.OpenDriver("sqlite", cfg.DSN)
|
|
}
|
|
|
|
dsn, err := buildDSN(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store.Open: %w", err)
|
|
}
|
|
|
|
database, err := db.OpenDriver("mysql", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store.Open: %w", err)
|
|
}
|
|
|
|
// Hard assertion: the session time_zone must be "+00:00".
|
|
// This catches MySQL servers that ignore client-supplied time_zone params.
|
|
var tz string
|
|
if err := database.QueryRow("SELECT @@session.time_zone").Scan(&tz); err != nil {
|
|
_ = database.Close()
|
|
return nil, fmt.Errorf("store.Open: query @@session.time_zone: %w", err)
|
|
}
|
|
if tz != "+00:00" {
|
|
_ = database.Close()
|
|
return nil, fmt.Errorf(
|
|
"store.Open: session time_zone=%q, want +00:00; UTC DSN override failed",
|
|
tz,
|
|
)
|
|
}
|
|
|
|
return database, nil
|
|
}
|
|
|
|
// buildDSN parses cfg.DSN and structurally overrides UTC and utf8mb4
|
|
// parameters regardless of what the caller supplied.
|
|
// This is a pure function and is tested independently.
|
|
func buildDSN(cfg *config.Config) (string, error) {
|
|
dsnCfg, err := mysql.ParseDSN(cfg.DSN)
|
|
if err != nil {
|
|
return "", fmt.Errorf("buildDSN: parse DSN: %w", err)
|
|
}
|
|
|
|
// Force UTC — overwrite any caller-supplied values (structural, not string).
|
|
dsnCfg.ParseTime = true
|
|
dsnCfg.Loc = time.UTC
|
|
// Collation implicitly sets charset to utf8mb4 via SET NAMES.
|
|
dsnCfg.Collation = "utf8mb4_unicode_ci"
|
|
if dsnCfg.Params == nil {
|
|
dsnCfg.Params = make(map[string]string)
|
|
}
|
|
// The time_zone connection parameter sends SET time_zone = '+00:00' on connect.
|
|
dsnCfg.Params["time_zone"] = "'+00:00'"
|
|
|
|
return dsnCfg.FormatDSN(), nil
|
|
}
|