turash/bugulma/backend/internal/geospatial/geo_helper.go

74 lines
2.7 KiB
Go

package geospatial
import (
"fmt"
"gorm.io/gorm"
)
// GeoHelper centralizes common, parameterized PostGIS fragments and checks.
// Purpose: avoid repeated SQL fragments and parameter ordering bugs like 42P18
type GeoHelper struct {
db *gorm.DB
}
func NewGeoHelper(db *gorm.DB) *GeoHelper {
return &GeoHelper{db: db}
}
// PointExpr returns a parameterized point expression suitable for use in
// Raw SQL with placeholder parameters for longitude and latitude (in that order).
// Example: ST_SetSRID(ST_MakePoint(?::double precision, ?::double precision), 4326)
func (g *GeoHelper) PointExpr() string {
return "ST_SetSRID(ST_MakePoint(?::double precision, ?::double precision), 4326)"
}
// DWithinExpr returns a parameterized ST_DWithin expression using the provided
// geometry column name and the helper's PointExpr. The final parameter expected
// by the expression is the radius (in meters if used with geography).
func (g *GeoHelper) DWithinExpr(geomCol string) string {
return fmt.Sprintf("ST_DWithin(%s::geography, %s::geography, ? * 1000)", geomCol, g.PointExpr())
}
// OrderByDistanceExpr returns an ORDER BY fragment that orders by distance
// between the geometry column and a parameterized point.
func (g *GeoHelper) OrderByDistanceExpr(geomCol string) string {
return fmt.Sprintf("%s <-> %s", geomCol, g.PointExpr())
}
// PointArgs returns ordered args for the point placeholders: longitude, latitude
func (g *GeoHelper) PointArgs(lng, lat float64) []interface{} {
return []interface{}{lng, lat}
}
// PointRadiusArgs returns args in order for queries that need (lng, lat, radius)
// - common for ST_DWithin where we use point twice (distance and order by) set includeOrderBy
// If includeOrderBy is true, it returns [lng, lat, radius, lng, lat]
// otherwise [lng, lat, radius]
func (g *GeoHelper) PointRadiusArgs(lng, lat, radiusKm float64, includeOrderBy bool) []interface{} {
if includeOrderBy {
return []interface{}{lng, lat, radiusKm, lng, lat}
}
return []interface{}{lng, lat, radiusKm}
}
// PostGISAvailable checks if PostGIS extension exists on the connected DB.
// Returns true if extension is present; errors are returned for DB issues.
func (g *GeoHelper) PostGISAvailable() (bool, error) {
var exists bool
if err := g.db.Raw("SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'postgis')").Scan(&exists).Error; err != nil {
return false, err
}
return exists, nil
}
// ColumnExists checks information_schema for a table/column existence
func (g *GeoHelper) ColumnExists(table, column string) (bool, error) {
var exists bool
q := `SELECT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ?)`
if err := g.db.Raw(q, table, column).Scan(&exists).Error; err != nil {
return false, err
}
return exists, nil
}