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 }