This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/database/database.go
2026-03-15 23:43:36 +00:00

193 lines
5.2 KiB
Go

package database
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/lib/pq"
_ "modernc.org/sqlite"
"git.gobha.me/xcaliber/chat-switchboard/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
}