// Package db 提供 visionA-backend 的 PostgreSQL 連線池(pgxpool)與 migration runner。 // // DB 接入塊 0(DB 基礎建設):本套件只負責「連線池建池 + 啟動 ping + graceful shutdown + // migration」這些跨 domain 共用的基礎設施;不含任何 store / repository 的 Postgres 實作 // (那是塊 1–3,會新增各 domain 的 postgres_repository.go,實作既有 interface)。 // // 對齊 docs/autoflow/04-architecture/database.md §5、§5.5。 package db import ( "fmt" "net/url" "strconv" "visiona-backend/internal/config" ) // BuildDSN 從 DatabaseConfig 組出 pgxpool 用的 PostgreSQL DSN。 // // 格式(對齊 database.md §5.5.1): // // postgres://{User}:{Password}@{Host}:{Port}/{DBName}?sslmode={SSLMode}&pool_max_conns={MaxConns}&pool_min_conns={MinConns} // // User / Password 走 url.UserPassword 做 percent-encoding,避免密碼含特殊字元(@ : / ?)破壞 DSN。 // // 安全:回傳值含明文密碼,呼叫端**不可**將其寫入 log。需要在 log 標識連線目標時, // 改用 SafeTarget()(只含 host:port/dbname、不含密碼)。 func BuildDSN(c config.DatabaseConfig) string { u := baseURL(c) q := baseQuery(c) // pgxpool 專屬參數:只有 pgxpool 認得,golang-migrate 的標準 database/sql 連線會被 PG // server 端拒絕(FATAL: unrecognized configuration parameter "pool_max_conns"), // 故這些參數**只**加在 BuildDSN(pgxpool 路徑),不加在 baseDSN(migrate 路徑)。 if c.MaxConns > 0 { q.Set("pool_max_conns", strconv.Itoa(c.MaxConns)) } if c.MinConns > 0 { q.Set("pool_min_conns", strconv.Itoa(c.MinConns)) } u.RawQuery = q.Encode() return u.String() } // baseDSN 組「只含標準 libpq 連線參數」的 PostgreSQL DSN(host/port/user/password/dbname/sslmode), // 不含任何 pgxpool 專屬參數(pool_max_conns / pool_min_conns)。 // // 供 golang-migrate 的 pgx5 driver 使用:它走標準 database/sql 單連線,PG server 端不認得 // pool_* 參數會直接 FATAL,故 migrate 路徑必須走這個「乾淨」DSN。 func baseDSN(c config.DatabaseConfig) string { u := baseURL(c) u.RawQuery = baseQuery(c).Encode() return u.String() } // baseURL 組 DSN 的 scheme / 帳密 / host / path 部分(不含 query)。 func baseURL(c config.DatabaseConfig) url.URL { return url.URL{ Scheme: "postgres", User: url.UserPassword(c.User, c.Password), Host: net_JoinHostPort(c.Host, c.Port), Path: "/" + c.DBName, } } // baseQuery 組標準 libpq 連線參數(目前只有 sslmode)。pgxpool 與 migrate 共用這組基礎參數。 func baseQuery(c config.DatabaseConfig) url.Values { q := url.Values{} sslMode := c.SSLMode if sslMode == "" { sslMode = "require" } q.Set("sslmode", sslMode) return q } // SafeTarget 回傳「可安全寫入 log」的連線目標字串(host:port/dbname),不含使用者名稱或密碼。 func SafeTarget(c config.DatabaseConfig) string { return fmt.Sprintf("%s/%s", net_JoinHostPort(c.Host, c.Port), c.DBName) } // net_JoinHostPort 是 net.JoinHostPort 的小包裝(避免在多檔 import net 只為一個呼叫)。 func net_JoinHostPort(host string, port int) string { if port == 0 { port = 5432 } return host + ":" + strconv.Itoa(port) }