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-02-28 01:40:31 +00:00

142 lines
3.6 KiB
Go

package database
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"strings"
_ "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
// 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
}
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)
if err := DB.Ping(); err != nil {
return fmt.Errorf("database ping: %w", err)
}
CurrentDialect = DialectPostgres
log.Println("✅ Database connected (postgres)")
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&_time_format=2006-01-02T15%%3A04%%3A05Z", dsn)
} else if dsn == ":memory:" {
connStr = "file::memory:?_pragma=foreign_keys%%3Don&_time_format=2006-01-02T15%%3A04%%3A05Z"
}
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)
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"
}
// 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
}