postgis

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 15 Imported by: 0

README

gorm-postgis

PostGIS support for gorm, built on twpayne/go-geom.

  • Geometry[T] / Geography[T] column types (EWKB read/write, SRID preserved, GeoJSON in JSON)
  • Aliases: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, Any
  • Query clauses: Intersects, Within, Contains, Covers, BBoxIntersects, DWithin, DWithinMeters, Distance, DistanceMeters
  • Expressions for Select/Where (for ORDER BY use db.Clauses(clause.OrderBy{Expression: expr})): Buffer, Transform, AsGeoJSON, Area, MakeEnvelope (and Envelope, a Go-side rectangle)
  • Migrator: CREATE EXTENSION postgis, typed columns (geometry(Point,4326)), drift detection
type Station struct {
	ID       uint
	Location postgis.Point   `gorm:"index:,type:gist"`            // geometry(Point,4326)
	Extent   postgis.Polygon `gorm:"srid:2154;index:,type:gist"`  // geometry(Polygon,2154)
	Zone     postgis.Geography[*geom.Polygon]                     // geography(Polygon,4326)
	Track    postgis.Any     `gorm:"geomtype:LineStringZ"`        // override the deduced type
}

db, _ := gorm.Open(postgis.Open(dsn), &gorm.Config{})
db.AutoMigrate(&Station{})

p := geom.NewPointFlat(geom.XY, []float64{166.44, -22.27}).SetSRID(4326)
db.Create(&Station{Location: postgis.Of(p)})

var near []Station
db.Where(postgis.DWithinMeters("location", p, 500)).Find(&near)
db.Clauses(clause.OrderBy{Expression: postgis.DistanceMeters("location", p)}).Find(&near)

// Server-side GeoJSON, area in m² (Buffer/Area need a lon/lat SRID such as 4326)
db.Model(&Station{}).Select("id, ? AS geojson, ? AS area_m2",
	postgis.AsGeoJSON("location"), postgis.Area("zone")).Scan(&rows)

Tags

Tag Effect
srid:<n> SRID of the column (default 4326). An invalid value panics.
geomtype:<T> PostGIS type, overriding the one deduced from the Go type (PointZ, PointM, PointZM, ...).
type:<sql> Standard gorm tag; used verbatim as the column type.
index:,type:gist Native gorm GIST index.

Geometries: NULL, empty, Multi*, dimensions

Covered by integration tests:

  • NULL: a zero Geometry[T]/Geography[T] is stored as NULL; a NULL column scans as Valid() == false.
  • Empty: POLYGON EMPTY round-trips as an empty polygon (Empty() == true); POINT EMPTY is read as an empty point (writing one is not covered).
  • Multi*: MultiPolygon with holes and several parts, and Geography[*geom.MultiPolygon], round-trip intact.
  • Dimensions: Z and M are supported with a matching column type (geomtype:PointZ, geomtype:PointM). PostGIS rejects a 2D value inserted into a Z column (and vice versa). Build the value with the right layout (geom.XYZ, geom.XYM, geom.XYZM).
  • Rasters are out of scope.

Indexes

Use gorm's native tag for a plain GIST index: gorm:"index:,type:gist".

DWithinMeters/DistanceMeters cast the column to geography, so a plain GIST index on a geometry column is not used by them. Index the expression instead (verified with EXPLAIN in the integration tests):

Loc postgis.Point `gorm:"index:idx_sites_loc_geog,type:gist,expression:(loc::geography)"`

(or store a geography column).

Column drift

Before migrating, AutoMigrate compares each existing geometry/geography column with format_type() from PostgreSQL (so gorm's own AlterColumn can never apply a drifted type silently). Columns with a type: tag are compared too, and -:migration fields are skipped:

Policy Behaviour
postgis.DriftError (default) the migration fails with a descriptive error
postgis.DriftAlter an SRID-only change on a geometry column runs ALTER COLUMN ... TYPE ... USING ST_SetSRID(col, srid) (relabels, does not reproject); any other change is still an error
postgis.DriftIgnore differences are left alone silently
gorm.Open(postgis.Open(dsn, postgis.WithDrift(postgis.DriftAlter)), &gorm.Config{})

Production migrations (no AutoMigrate)

AutoMigrate is convenient for development. Schemas managed with versioned SQL migrations can write the DDL by hand; this is what the library generates:

-- once per database (needs the privilege to create extensions)
CREATE EXTENSION IF NOT EXISTS postgis;

-- column types
--   postgis.Point                    -> geometry(Point,4326)
--   postgis.Polygon  srid:2154       -> geometry(Polygon,2154)
--   postgis.MultiPolygon             -> geometry(MultiPolygon,4326)
--   postgis.Any                      -> geometry(Geometry,4326)
--   postgis.Geography[*geom.Point]   -> geography(Point,4326)
--   geomtype:PointZ                  -> geometry(PointZ,4326)
CREATE TABLE sites (
    id     bigserial PRIMARY KEY,
    loc    geometry(Point,4326),
    extent geometry(Polygon,2154)
);

-- `index:,type:gist`
CREATE INDEX idx_sites_loc ON sites USING gist (loc);
-- `index:idx_sites_loc_geog,type:gist,expression:(loc::geography)`
CREATE INDEX idx_sites_loc_geog ON sites USING gist ((loc::geography));

The column type always follows <kind>(<geomtype>,<srid>); geomtype defaults to the Go type's name (Geometry for postgis.Any). To check an existing database against your models without migrating anything, call postgis.CheckDrift(db, &Model{}, ...) (read-only; tables that do not exist yet are skipped).

Notes

  • Geometries are always sent as bound EWKB parameters (ST_GeomFromEWKB(?)), never interpolated.
  • DWithin/Distance use SRID units (degrees for 4326); the *Meters variants cast to geography.
  • GeoJSON decoded through json.Unmarshal gets SRID 4326 (RFC 7946) so it can be inserted into a typed column; geometries built by hand need SetSRID.
  • Wrap/Open/New need the gorm postgres dialector (pgx).

Tests

go test ./...                       # unit, no database
go test -tags integration ./...     # PostGIS in a container (Docker); image override: POSTGIS_IMAGE

Documentation

Overview

Package postgis adds PostGIS support to gorm: geometry/geography column types backed by go-geom, spatial query clauses, and migration helpers.

Index

Constants

View Source
const DefaultSRID = 4326

DefaultSRID is the SRID used in column definitions when no `srid` tag is set.

Variables

This section is empty.

Functions

func Area

func Area(col string) clause.Expression

Area returns the area of col in square meters (geography). The column must use a lon/lat SRID (e.g. 4326); a projected SRID such as 2154 is rejected by PostGIS.

func AsGeoJSON

func AsGeoJSON(col string) clause.Expression

AsGeoJSON renders col as a GeoJSON string server-side, so an API can stream it without decoding the geometry in Go. Scan it into a string or json.RawMessage.

func BBoxIntersects

func BBoxIntersects(col string, g geom.T) clause.Expression

BBoxIntersects: col && g (bounding boxes overlap, index-only).

func Buffer

func Buffer(col string, meters float64) clause.Expression

Buffer returns col buffered by the given number of meters, computed on geography (a geometry buffer would be in degrees for SRID 4326) and cast back to geometry. The column must use a lon/lat SRID (e.g. 4326).

func CheckDrift

func CheckDrift(db *gorm.DB, models ...any) error

CheckDrift compares the geometry/geography columns of the given models with the database, read-only, and returns the first mismatch as an error. Columns of tables that do not exist yet are skipped.

func Contains

func Contains(col string, g geom.T) clause.Expression

Contains: ST_Contains(col, g) — col contains g.

func Covers

func Covers(col string, g geom.T) clause.Expression

Covers: ST_Covers(col, g) — no point of g lies outside col (unlike Contains, boundary points count).

func DWithin

func DWithin(col string, g geom.T, distance float64) clause.Expression

DWithin: ST_DWithin(col, g, distance) with distance in the units of the SRID (degrees for 4326). For meters, use DWithinMeters.

func DWithinMeters

func DWithinMeters(col string, g geom.T, meters float64) clause.Expression

DWithinMeters: ST_DWithin on geography, distance in meters. Works on geometry (cast to geography, so SRID must be geographic) and geography columns. On a geometry column the cast defeats a plain GIST index: index the expression (`(loc::geography)`) or store a geography column.

func Distance

func Distance(col string, g geom.T) clause.Expression

Distance: ST_Distance(col, g), in SRID units. Usable in ORDER BY via db.Clauses(clause.OrderBy{Expression: postgis.Distance(...)}).

func DistanceMeters

func DistanceMeters(col string, g geom.T) clause.Expression

DistanceMeters: ST_Distance on geography, in meters.

func EnsureExtension

func EnsureExtension(db *gorm.DB) error

EnsureExtension runs CREATE EXTENSION IF NOT EXISTS postgis.

func Envelope

func Envelope(minX, minY, maxX, maxY float64, srid int) *geom.Polygon

Envelope returns the rectangle (minX,minY)-(maxX,maxY) as a closed polygon with the given SRID.

func Intersects

func Intersects(col string, g geom.T) clause.Expression

Intersects: ST_Intersects(col, g).

func MakeEnvelope

func MakeEnvelope(minX, minY, maxX, maxY float64, srid int) clause.Expression

MakeEnvelope is the server-side rectangle ST_MakeEnvelope(minX, minY, maxX, maxY, srid). Prefer Envelope when the rectangle is passed as a geometry argument to another clause.

func New

func New(config postgres.Config, opts ...Option) gorm.Dialector

New is the PostGIS counterpart of postgres.New.

func Open

func Open(dsn string, opts ...Option) gorm.Dialector

Open is the PostGIS counterpart of postgres.Open.

func Transform

func Transform(col string, srid int) clause.Expression

Transform reprojects a geometry column to the given SRID (e.g. a metric projection for measurements). It does not accept geography columns.

func Within

func Within(col string, g geom.T) clause.Expression

Within: ST_Within(col, g) — col lies within g.

func Wrap

func Wrap(d gorm.Dialector, opts ...Option) gorm.Dialector

Wrap upgrades a dialector returned by postgres.Open/postgres.New. It panics on any other dialector, as the migrator relies on the postgres one.

Types

type Any

type Any = Geometry[geom.T]

Convenience aliases for the common geometry types.

type Dialector

type Dialector struct {
	*postgres.Dialector
	// contains filtered or unexported fields
}

Dialector is the gorm postgres dialector with a PostGIS-aware migrator. Everything else (error translation, ...) is promoted from the embedded dialector.

func (*Dialector) Migrator

func (d *Dialector) Migrator(db *gorm.DB) gorm.Migrator

Migrator implements gorm.Dialector.

type Drift

type Drift int

Drift tells AutoMigrate what to do when an existing geometry/geography column does not have the type declared by the model (geometry type, dimension, SRID).

const (
	// DriftError fails the migration with a descriptive error (default).
	DriftError Drift = iota
	// DriftAlter runs ALTER COLUMN ... TYPE ... USING ST_SetSRID(col, srid) when only the SRID
	// differs on a geometry column (this relabels coordinates, it does not reproject them);
	// any other difference is still an error.
	DriftAlter
	// DriftIgnore keeps the previous behaviour: differences are left alone silently.
	DriftIgnore
)

type Geography

type Geography[T geom.T] struct {
	G T
}

Geography is a PostGIS `geography` column holding a T (WGS84 lon/lat, distances in meters). A nil G is stored as NULL.

func (Geography[T]) GormDBDataType

func (Geography[T]) GormDBDataType(_ *gorm.DB, field *schema.Field) string

GormDBDataType implements migrator.GormDataTypeInterface.

func (Geography[T]) GormDataType

func (Geography[T]) GormDataType() string

GormDataType implements schema.GormDataTypeInterface.

func (Geography[T]) GormValue

func (g Geography[T]) GormValue(_ context.Context, db *gorm.DB) clause.Expr

GormValue casts the EWKB to geography on the server.

func (Geography[T]) MarshalJSON

func (g Geography[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the geography as GeoJSON (null when NULL).

func (*Geography[T]) Scan

func (g *Geography[T]) Scan(src any) error

Scan implements sql.Scanner.

func (*Geography[T]) UnmarshalJSON

func (g *Geography[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes GeoJSON into the geography.

func (Geography[T]) Valid

func (g Geography[T]) Valid() bool

Valid reports whether g holds a geometry (i.e. is not NULL).

func (Geography[T]) Value

func (g Geography[T]) Value() (driver.Value, error)

Value implements driver.Valuer (EWKB bytes, SRID included).

type Geometry

type Geometry[T geom.T] struct {
	G T
}

Geometry is a PostGIS `geometry` column holding a T. A nil G is stored as NULL.

Column definition is taken from T and from the optional field tags `srid:4326` and `geomtype:PointZ`, e.g. `gorm:"srid:2154;index:,type:gist"`.

func Of

func Of[T geom.T](g T) Geometry[T]

Of wraps g.

func (Geometry[T]) GormDBDataType

func (Geometry[T]) GormDBDataType(_ *gorm.DB, field *schema.Field) string

GormDBDataType implements migrator.GormDataTypeInterface.

func (Geometry[T]) GormDataType

func (Geometry[T]) GormDataType() string

GormDataType implements schema.GormDataTypeInterface.

func (Geometry[T]) GormValue

func (g Geometry[T]) GormValue(_ context.Context, db *gorm.DB) clause.Expr

GormValue makes gorm send the value through ST_GeomFromEWKB, so the server always receives a typed geometry rather than an untyped bytea.

func (Geometry[T]) MarshalJSON

func (g Geometry[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the geometry as GeoJSON (null when NULL).

func (*Geometry[T]) Scan

func (g *Geometry[T]) Scan(src any) error

Scan implements sql.Scanner.

func (*Geometry[T]) UnmarshalJSON

func (g *Geometry[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes GeoJSON into the geometry.

func (Geometry[T]) Valid

func (g Geometry[T]) Valid() bool

Valid reports whether g holds a geometry (i.e. is not NULL).

func (Geometry[T]) Value

func (g Geometry[T]) Value() (driver.Value, error)

Value implements driver.Valuer (EWKB bytes, SRID included).

type LineString

type LineString = Geometry[*geom.LineString]

Convenience aliases for the common geometry types.

type MultiLineString

type MultiLineString = Geometry[*geom.MultiLineString]

Convenience aliases for the common geometry types.

type MultiPoint

type MultiPoint = Geometry[*geom.MultiPoint]

Convenience aliases for the common geometry types.

type MultiPolygon

type MultiPolygon = Geometry[*geom.MultiPolygon]

Convenience aliases for the common geometry types.

type Option

type Option func(*Dialector)

Option configures the dialector.

func WithDrift

func WithDrift(p Drift) Option

WithDrift sets the drift policy (default DriftError).

type Point

type Point = Geometry[*geom.Point]

Convenience aliases for the common geometry types.

type Polygon

type Polygon = Geometry[*geom.Polygon]

Convenience aliases for the common geometry types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL