Files
plumber/internal/store/postgres.go
T

108 lines
2.5 KiB
Go

package store
import (
"database/sql"
"fmt"
"net/url"
"strconv"
"strings"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
// rebind converts ? placeholders to Postgres $1, $2, ... form.
func rebind(query string) string {
n := 0
var b strings.Builder
for i := 0; i < len(query); i++ {
if query[i] == '?' {
n++
b.WriteByte('$')
b.WriteString(strconv.Itoa(n))
continue
}
b.WriteByte(query[i])
}
return b.String()
}
// q rebinds SQL placeholders for Postgres.
func (s *Store) q(query string) string {
return rebind(query)
}
// applySchema runs semicolon-separated DDL statements, skipping PRAGMA lines.
func applySchema(db *sql.DB, schema string) error {
for _, stmt := range strings.Split(schema, ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
upper := strings.ToUpper(stmt)
if strings.HasPrefix(upper, "PRAGMA") {
continue
}
if _, err := db.Exec(stmt); err != nil {
return fmt.Errorf("%w: %s", err, stmt)
}
}
return nil
}
// postgresDSN normalizes DATABASE_URL for pgx (sslmode default, strip unsupported params).
func postgresDSN(raw string) (string, error) {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("DATABASE_URL: %w", err)
}
switch u.Scheme {
case "postgres", "postgresql":
default:
return "", fmt.Errorf("DATABASE_URL must be a postgres URL")
}
q := u.Query()
if strings.EqualFold(q.Get("sslrootcert"), "system") {
q.Del("sslrootcert")
}
q.Del("sslnegotiation")
if q.Get("sslmode") == "" {
q.Set("sslmode", "verify-full")
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// OpenPostgres connects to Postgres, applies schema/migrations, and starts session cleanup.
func OpenPostgres(databaseURL, schema string) (*Store, error) {
dsn, err := postgresDSN(databaseURL)
if err != nil {
return nil, err
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(5)
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("postgres ping: %w", err)
}
if err := applySchema(db, schema); err != nil {
_ = db.Close()
return nil, fmt.Errorf("apply schema: %w", err)
}
if err := applySessionsSchema(db); err != nil {
_ = db.Close()
return nil, fmt.Errorf("apply sessions schema: %w", err)
}
if err := migrateUserProfileColumns(db); err != nil {
_ = db.Close()
return nil, fmt.Errorf("migrate profile columns: %w", err)
}
st := &Store{db: db}
st.initSessionStore(5 * time.Minute)
return st, nil
}