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 // 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) 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 }