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 }