package postgres import ( "context" "database/sql" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" ) type LocationRepository struct { db *sql.DB } func NewLocationRepository(db *sql.DB) *LocationRepository { return &LocationRepository{db: db} } func (r *LocationRepository) ListCountries(ctx context.Context) ([]*entity.Country, error) { query := `SELECT id, name, iso2, iso3, phonecode, currency, emoji, emoji_u, created_at, updated_at FROM countries ORDER BY name ASC` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var countries []*entity.Country for rows.Next() { c := &entity.Country{} // Scan columns. Be careful with NULL handling if fields are nullable, but schema says NOT NULL mostly. // Check schema again. phonecode, etc can be null. // For simplicity, we scan into sql.NullString or just string (and risk error if NULL). // Migration 021 says: // name VARCHAR(100) NOT NULL // iso2 CHAR(2) // phonecode VARCHAR(255) // ... // So nullable fields need handling. var iso2, iso3, phonecode, currency, emoji, emojiU sql.NullString if err := rows.Scan(&c.ID, &c.Name, &iso2, &iso3, &phonecode, ¤cy, &emoji, &emojiU, &c.CreatedAt, &c.UpdatedAt); err != nil { return nil, err } c.ISO2 = iso2.String c.ISO3 = iso3.String c.PhoneCode = phonecode.String c.Currency = currency.String c.Emoji = emoji.String c.EmojiU = emojiU.String countries = append(countries, c) } return countries, nil } func (r *LocationRepository) ListStates(ctx context.Context, countryID int64) ([]*entity.State, error) { query := `SELECT id, name, country_id, country_code, iso2, type, latitude, longitude FROM states WHERE country_id = $1 ORDER BY name ASC` rows, err := r.db.QueryContext(ctx, query, countryID) if err != nil { return nil, err } defer rows.Close() var states []*entity.State for rows.Next() { s := &entity.State{} var iso2, typeStr sql.NullString var lat, lng sql.NullFloat64 if err := rows.Scan(&s.ID, &s.Name, &s.CountryID, &s.CountryCode, &iso2, &typeStr, &lat, &lng); err != nil { return nil, err } s.ISO2 = iso2.String s.Type = typeStr.String s.Latitude = lat.Float64 s.Longitude = lng.Float64 states = append(states, s) } return states, nil } func (r *LocationRepository) ListCities(ctx context.Context, stateID int64) ([]*entity.City, error) { query := `SELECT id, name, state_id, country_id, latitude, longitude FROM cities WHERE state_id = $1 ORDER BY name ASC` rows, err := r.db.QueryContext(ctx, query, stateID) if err != nil { return nil, err } defer rows.Close() var cities []*entity.City for rows.Next() { c := &entity.City{} // schema: latitude NOT NULL, longitude NOT NULL. if err := rows.Scan(&c.ID, &c.Name, &c.StateID, &c.CountryID, &c.Latitude, &c.Longitude); err != nil { return nil, err } cities = append(cities, c) } return cities, nil } func (r *LocationRepository) Search(ctx context.Context, query string, countryID int64) ([]*entity.LocationSearchResult, error) { // Search Cities and States // Simple LIKE query q := "%" + query + "%" sqlQuery := ` SELECT id, name, 'state' as type, country_id, NULL as state_id, '' as region_name FROM states WHERE country_id = $1 AND name ILIKE $2 UNION ALL SELECT c.id, c.name, 'city' as type, c.country_id, c.state_id, s.name as region_name FROM cities c JOIN states s ON c.state_id = s.id WHERE c.country_id = $1 AND c.name ILIKE $2 LIMIT 20 ` rows, err := r.db.QueryContext(ctx, sqlQuery, countryID, q) if err != nil { return nil, err } defer rows.Close() var results []*entity.LocationSearchResult for rows.Next() { res := &entity.LocationSearchResult{} var stateID sql.NullInt64 var regionName sql.NullString if err := rows.Scan(&res.ID, &res.Name, &res.Type, &res.CountryID, &stateID, ®ionName); err != nil { return nil, err } if stateID.Valid { id := stateID.Int64 res.StateID = &id } res.RegionName = regionName.String results = append(results, res) } return results, nil }