// Phase 2b — Postgres pool + schema migration. // Voir ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2. package main import ( "context" "database/sql" _ "embed" "fmt" "net/url" "time" _ "github.com/lib/pq" ) //go:embed migrations/001_init.sql var initSQL string // OpenDB opens a Postgres pool from a DSN, pings, and runs the embedded // migration (idempotent CREATE TABLE / CREATE INDEX IF NOT EXISTS). // Caller is responsible for closing the returned *sql.DB. func OpenDB(ctx context.Context, dsn string) (*sql.DB, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, fmt.Errorf("sql.Open: %w", err) } db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(30 * time.Minute) pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := db.PingContext(pingCtx); err != nil { _ = db.Close() return nil, fmt.Errorf("postgres ping: %w", err) } migCtx, cancelMig := context.WithTimeout(ctx, 10*time.Second) defer cancelMig() if _, err := db.ExecContext(migCtx, initSQL); err != nil { _ = db.Close() return nil, fmt.Errorf("apply migration: %w", err) } return db, nil } // RedactDSN strips the password from a Postgres URL for safe logging. func RedactDSN(dsn string) string { u, err := url.Parse(dsn) if err != nil { return "(invalid dsn)" } if u.User != nil { username := u.User.Username() u.User = url.User(username) } return u.String() }