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 }