package geo import ( "context" "database/sql" "errors" "net" "time" _ "github.com/jackc/pgx/v5/stdlib" ) const defaultLookupQuery = ` SELECT ip::text, country, region, city, latitude, longitude FROM geoip.lookup_city($1); ` type postgresResolver struct { db *sql.DB lookupQuery string } func newPostgresResolver(databaseURL, lookupQuery string) (Resolver, error) { if databaseURL == "" { return nil, errors.New("database url is required for postgres backend") } db, err := sql.Open("pgx", databaseURL) if err != nil { return nil, err } db.SetMaxOpenConns(10) db.SetMaxIdleConns(2) db.SetConnMaxIdleTime(5 * time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { _ = db.Close() return nil, err } if lookupQuery == "" { lookupQuery = defaultLookupQuery } return &postgresResolver{ db: db, lookupQuery: lookupQuery, }, nil } func (r *postgresResolver) Close() error { return r.db.Close() } func (r *postgresResolver) Lookup(ipStr string) (Location, error) { ip := net.ParseIP(ipStr) if ip == nil { return Location{}, ErrInvalidIP } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() row := r.db.QueryRowContext(ctx, r.lookupQuery, ip.String()) var ( resolvedIP string country, region sql.NullString city sql.NullString latitude, longitude sql.NullFloat64 ) if err := row.Scan(&resolvedIP, &country, ®ion, &city, &latitude, &longitude); err != nil { if errors.Is(err, sql.ErrNoRows) { return Location{}, ErrNotFound } return Location{}, err } return Location{ IP: resolvedIP, Country: country.String, Region: region.String, City: city.String, Address: buildAddress(city.String, region.String, country.String), Latitude: latitude.Float64, Longitude: longitude.Float64, }, nil }