Files
pangolin/server/internal/store/mysql.go
T
2026-06-13 18:52:30 +08:00

86 lines
2.4 KiB
Go

package store
import (
"database/sql"
"fmt"
"time"
// Register the mysql driver with database/sql.
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
"github.com/wangjia/pangolin/server/internal/config"
)
// Open is the single canonical MySQL entry point for the Pangolin server.
//
// It:
// 1. Parses the DSN from cfg.DSN.
// 2. Structurally overrides UTC parameters (ParseTime, Loc, Collation,
// time_zone session variable) — no string manipulation.
// 3. Configures the connection pool.
// 4. Pings the server.
// 5. Asserts SELECT @@session.time_zone = "+00:00" (startup fatal if the
// server ignores our DSN override — e.g. SQL mode forces a different TZ).
func Open(cfg *config.Config) (*sql.DB, error) {
dsn, err := buildDSN(cfg)
if err != nil {
return nil, fmt.Errorf("store.Open: %w", err)
}
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("store.Open: %w", err)
}
// Connection pool.
db.SetMaxOpenConns(30)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("store.Open: ping: %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 := db.QueryRow("SELECT @@session.time_zone").Scan(&tz); err != nil {
_ = db.Close()
return nil, fmt.Errorf("store.Open: query @@session.time_zone: %w", err)
}
if tz != "+00:00" {
_ = db.Close()
return nil, fmt.Errorf(
"store.Open: session time_zone=%q, want +00:00; UTC DSN override failed",
tz,
)
}
return db, 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
}