package commons import ( "context" "fmt" "net/url" "os" "strings" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // DB wraps pgxpool.Pool with helper methods type DB struct { *pgxpool.Pool } // OpenDatabase connects to PostgreSQL using environment variables or connection string func OpenDatabase(connString string) (*DB, error) { fmt.Printf("Connecting to database...\n") // If connection string not provided, try environment variables if connString == "" { connString = os.Getenv("DATABASE_URL") } if connString == "" { // Build from individual env vars host := GetEnvOrDefault("DB_HOST", "infra-postgres") port := GetEnvOrDefault("DB_PORT", "5432") user := GetEnvOrDefault("DB_USER", "dba_1440_news") dbname := GetEnvOrDefault("DB_NAME", "db_1440_news") // Support Docker secrets (password file) or direct password password := os.Getenv("DB_PASSWORD") if password == "" { if passwordFile := os.Getenv("DB_PASSWORD_FILE"); passwordFile != "" { data, err := os.ReadFile(passwordFile) if err != nil { return nil, fmt.Errorf("failed to read password file: %v", err) } password = strings.TrimSpace(string(data)) } } connString = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, url.QueryEscape(password), host, port, dbname) } config, err := pgxpool.ParseConfig(connString) if err != nil { return nil, fmt.Errorf("failed to parse connection string: %v", err) } // Connection pool settings config.MaxConns = 10 config.MinConns = 0 // Don't pre-create connections to avoid schema race conditions config.MaxConnLifetime = 5 * time.Minute config.MaxConnIdleTime = 1 * time.Minute ctx := context.Background() pool, err := pgxpool.NewWithConfig(ctx, config) if err != nil { return nil, fmt.Errorf("failed to connect to database: %v", err) } // Verify connection if err := pool.Ping(ctx); err != nil { pool.Close() return nil, fmt.Errorf("failed to ping database: %v", err) } fmt.Println(" Connected to PostgreSQL") return &DB{pool}, nil } // GetEnvOrDefault returns environment variable value or default func GetEnvOrDefault(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal } // QueryRow wraps pool.QueryRow for compatibility func (db *DB) QueryRow(query string, args ...interface{}) pgx.Row { return db.Pool.QueryRow(context.Background(), query, args...) } // Query wraps pool.Query for compatibility func (db *DB) Query(query string, args ...interface{}) (pgx.Rows, error) { return db.Pool.Query(context.Background(), query, args...) } // Exec wraps pool.Exec for compatibility func (db *DB) Exec(query string, args ...interface{}) (int64, error) { result, err := db.Pool.Exec(context.Background(), query, args...) if err != nil { return 0, err } return result.RowsAffected(), nil } // Begin starts a transaction func (db *DB) Begin() (pgx.Tx, error) { return db.Pool.Begin(context.Background()) } // Close closes the connection pool func (db *DB) Close() error { db.Pool.Close() return nil } // NullableString returns nil for empty strings, otherwise the string pointer func NullableString(s string) *string { if s == "" { return nil } return &s } // NullableTime returns nil for zero times, otherwise the time pointer func NullableTime(t time.Time) *time.Time { if t.IsZero() { return nil } return &t } // StringValue returns empty string for nil, otherwise the dereferenced value func StringValue(s *string) string { if s == nil { return "" } return *s } // TimeValue returns zero time for nil, otherwise the dereferenced value func TimeValue(t *time.Time) time.Time { if t == nil { return time.Time{} } return *t } // ToSearchQuery converts a user query to PostgreSQL tsquery format func ToSearchQuery(query string) string { // Simple conversion: split on spaces and join with & words := strings.Fields(query) if len(words) == 0 { return "" } return strings.Join(words, " & ") }