- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
193 lines
5.1 KiB
Go
193 lines
5.1 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/lib/pq"
|
|
_ "modernc.org/sqlite"
|
|
|
|
"switchboard-core/config"
|
|
)
|
|
|
|
// DB is the application-wide database connection pool.
|
|
var DB *sql.DB
|
|
|
|
// rawDSN holds the Postgres connection string for LISTEN/NOTIFY consumers.
|
|
// Empty when running SQLite.
|
|
var rawDSN string
|
|
|
|
// DSN returns the raw Postgres connection string, or "" for SQLite.
|
|
// Used by the event broadcast layer to open a dedicated listener connection.
|
|
func DSN() string { return rawDSN }
|
|
|
|
// Connect opens a connection pool to the configured database.
|
|
// Driver is selected by DB_DRIVER env var: "postgres" (default) or "sqlite".
|
|
// For SQLite the DATABASE_URL is treated as a file path; ":memory:" is supported.
|
|
func Connect(cfg *config.Config) error {
|
|
driver := strings.ToLower(cfg.DBDriver)
|
|
if driver == "" {
|
|
driver = detectDriver(cfg.DatabaseURL)
|
|
}
|
|
|
|
switch driver {
|
|
case "sqlite", "sqlite3":
|
|
return connectSQLite(cfg.DatabaseURL)
|
|
case "postgres", "postgresql", "":
|
|
return connectPostgres(cfg.DatabaseURL)
|
|
default:
|
|
return fmt.Errorf("unsupported DB_DRIVER: %q (expected postgres or sqlite)", driver)
|
|
}
|
|
}
|
|
|
|
// connectPostgres opens a PostgreSQL connection pool.
|
|
func connectPostgres(dsn string) error {
|
|
if dsn == "" {
|
|
log.Println("⚠ DATABASE_URL not set — running without database")
|
|
return nil
|
|
}
|
|
|
|
// Log the database name for operational clarity (never log credentials).
|
|
dbName = extractDBName(dsn)
|
|
rawDSN = dsn
|
|
log.Printf("Connecting to Postgres database %q ...", dbName)
|
|
|
|
var err error
|
|
DB, err = sql.Open("postgres", dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("database open: %w", err)
|
|
}
|
|
|
|
DB.SetMaxOpenConns(25)
|
|
DB.SetMaxIdleConns(5)
|
|
DB.SetConnMaxLifetime(5 * time.Minute) // recycle connections — prevents stale TCP after PG restart/network blip
|
|
DB.SetConnMaxIdleTime(1 * time.Minute) // close idle connections faster — reduces pool pressure after write bursts
|
|
|
|
if err := DB.Ping(); err != nil {
|
|
return fmt.Errorf("database ping: %w", err)
|
|
}
|
|
|
|
CurrentDialect = DialectPostgres
|
|
log.Printf("✅ Database connected (postgres, db=%s)", dbName)
|
|
return nil
|
|
}
|
|
|
|
// connectSQLite opens a SQLite database with WAL mode enabled.
|
|
func connectSQLite(dsn string) error {
|
|
if dsn == "" {
|
|
dsn = "switchboard.db"
|
|
}
|
|
|
|
// Ensure parent directory exists for file-based DBs.
|
|
if dsn != ":memory:" && !strings.HasPrefix(dsn, "file:") {
|
|
dir := filepath.Dir(dsn)
|
|
if dir != "" && dir != "." {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("create db directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// modernc.org/sqlite uses "sqlite" as the driver name.
|
|
// Append pragmas via query string for file paths.
|
|
// _time_format tells the driver how to convert between Go time.Time and SQLite TEXT timestamps.
|
|
connStr := dsn
|
|
if dsn != ":memory:" && !strings.Contains(dsn, "?") {
|
|
connStr = fmt.Sprintf("file:%s?_pragma=journal_mode%%3Dwal&_pragma=busy_timeout%%3D5000&_pragma=foreign_keys%%3Don", dsn)
|
|
} else if dsn == ":memory:" {
|
|
connStr = "file::memory:?_pragma=foreign_keys%%3Don"
|
|
}
|
|
|
|
var err error
|
|
DB, err = sql.Open("sqlite", connStr)
|
|
if err != nil {
|
|
return fmt.Errorf("database open: %w", err)
|
|
}
|
|
|
|
// SQLite performs best with a single writer.
|
|
DB.SetMaxOpenConns(1)
|
|
DB.SetMaxIdleConns(1)
|
|
|
|
if err := DB.Ping(); err != nil {
|
|
return fmt.Errorf("database ping: %w", err)
|
|
}
|
|
|
|
// Verify WAL mode took effect (some in-memory modes ignore it).
|
|
var mode string
|
|
DB.QueryRow("PRAGMA journal_mode").Scan(&mode)
|
|
dbName = dsn
|
|
log.Printf("✅ Database connected (sqlite, journal_mode=%s, path=%s)", mode, dsn)
|
|
|
|
CurrentDialect = DialectSQLite
|
|
return nil
|
|
}
|
|
|
|
// detectDriver infers the driver from DATABASE_URL format.
|
|
func detectDriver(dsn string) string {
|
|
if dsn == "" {
|
|
return "postgres"
|
|
}
|
|
if strings.HasPrefix(dsn, "postgres") {
|
|
return "postgres"
|
|
}
|
|
if strings.HasSuffix(dsn, ".db") || strings.HasSuffix(dsn, ".sqlite") ||
|
|
dsn == ":memory:" || strings.HasPrefix(dsn, "file:") {
|
|
return "sqlite"
|
|
}
|
|
return "postgres"
|
|
}
|
|
|
|
// extractDBName pulls the database name from a DSN for logging.
|
|
// Handles both URL-style (postgres://user:pass@host/dbname) and
|
|
// keyword-style (host=... dbname=...) formats. Never returns creds.
|
|
func extractDBName(dsn string) string {
|
|
// URL style: postgres://user:pass@host:port/dbname?params
|
|
if strings.Contains(dsn, "://") {
|
|
idx := strings.LastIndex(dsn, "/")
|
|
if idx >= 0 && idx < len(dsn)-1 {
|
|
name := dsn[idx+1:]
|
|
if q := strings.Index(name, "?"); q >= 0 {
|
|
name = name[:q]
|
|
}
|
|
if name != "" {
|
|
return name
|
|
}
|
|
}
|
|
}
|
|
// Keyword style: host=... dbname=xyz ...
|
|
for _, part := range strings.Fields(dsn) {
|
|
if strings.HasPrefix(part, "dbname=") {
|
|
return strings.TrimPrefix(part, "dbname=")
|
|
}
|
|
}
|
|
return "(unknown)"
|
|
}
|
|
|
|
// Close gracefully shuts down the connection pool.
|
|
func Close() {
|
|
if DB != nil {
|
|
DB.Close()
|
|
}
|
|
}
|
|
|
|
// IsConnected returns true if a database connection is available.
|
|
func IsConnected() bool {
|
|
if DB == nil {
|
|
return false
|
|
}
|
|
return DB.Ping() == nil
|
|
}
|
|
|
|
// dbName holds the database name extracted at connect time.
|
|
var dbName string
|
|
|
|
// Name returns the database name (extracted from the DSN at connect time).
|
|
func Name() string {
|
|
return dbName
|
|
}
|