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 }