package citydb import ( "context" "database/sql" "fmt" "strings" "github.com/doug-martin/goqu/v9" _ "modernc.org/sqlite" "prayertimes/pkg/prayer" ) type Provider struct { db *goqu.Database own bool } func New(db *goqu.Database) Provider { return Provider{db: db, own: false} } func NewWithConn(conn *sql.DB) Provider { return Provider{db: goqu.New("sqlite3", conn), own: false} } func Open(path string) (Provider, error) { conn, err := sql.Open("sqlite", path) if err != nil { return Provider{}, fmt.Errorf("failed to open cities database: %w", err) } if err := conn.Ping(); err != nil { return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err) } return Provider{db: goqu.New("sqlite3", conn), own: true}, nil } func (p Provider) Close() error { if !p.own { return nil } if err := p.db.Db.(*sql.DB).Close(); err != nil { return fmt.Errorf("failed to close cities database: %w", err) } return nil } type locationRow struct { ID int `db:"geoname_id"` Name string `db:"name"` ASCIIName string `db:"ascii_name"` AlternateNames string `db:"alternate_names"` CountryCode string `db:"country_code"` Timezone string `db:"timezone"` Latitude float64 `db:"latitude"` Longitude float64 `db:"longitude"` DistanceSq float64 `db:"distance_sq"` } func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) { query = strings.TrimSpace(query) if query == "" { return []prayer.Location{}, nil } pattern := "%" + query + "%" q := p.db. From("cities"). Select( goqu.I("geoname_id"), goqu.I("name"), goqu.I("ascii_name"), goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"), goqu.I("country_code"), goqu.I("timezone"), goqu.I("latitude"), goqu.I("longitude"), goqu.V(0).As("distance_sq"), ). Where( goqu.Or( goqu.L("name LIKE ? COLLATE NOCASE", pattern), goqu.L("ascii_name LIKE ? COLLATE NOCASE", pattern), goqu.L("alternate_names LIKE ? COLLATE NOCASE", pattern), ), ). Order(goqu.I("population").Desc(), goqu.I("name").Asc()). Limit(50) var rows []locationRow if err := q.ScanStructsContext(ctx, &rows); err != nil { return nil, fmt.Errorf("failed to query locations from cities database: %w", err) } locations := make([]prayer.Location, 0, len(rows)) for _, row := range rows { locations = append(locations, prayer.Location{ ID: row.ID, Name: row.Name, ASCIIName: row.ASCIIName, AlternateNames: row.AlternateNames, CountryCode: row.CountryCode, Timezone: row.Timezone, Latitude: row.Latitude, Longitude: row.Longitude, }) } return locations, nil } func (p Provider) SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error) { q := p.db. From("cities"). Select( goqu.I("geoname_id"), goqu.I("name"), goqu.I("ascii_name"), goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"), goqu.I("country_code"), goqu.I("timezone"), goqu.I("latitude"), goqu.I("longitude"), goqu.L( "((latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?))", coords.Latitude, coords.Latitude, coords.Longitude, coords.Longitude, ).As("distance_sq"), ). Order( goqu.I("distance_sq").Asc(), goqu.I("population").Desc(), ). Limit(50) var rows []locationRow if err := q.ScanStructsContext(ctx, &rows); err != nil { return nil, fmt.Errorf("failed to query locations by coordinates from cities database: %w", err) } locations := make([]prayer.Location, 0, len(rows)) for _, row := range rows { locations = append(locations, prayer.Location{ ID: row.ID, Name: row.Name, ASCIIName: row.ASCIIName, AlternateNames: row.AlternateNames, CountryCode: row.CountryCode, Timezone: row.Timezone, Latitude: row.Latitude, Longitude: row.Longitude, }) } return locations, nil }