PostGIS geometry
First-class spatial columns on Postgres — geometry / geography with a subtype and SRID, GiST indexes, GeoJSON serialization, and ST_* spatial predicates in the typed QuerySet.
umbral ships spatial columns as a first-class field type on Postgres, behind the opt-in postgis cargo feature. A geometry field carries its subtype and SRID, round-trips as GeoJSON over REST, decodes to geo_types in Rust, and gets a GiST index and ST_* spatial predicates — the same declare-your-data-and-get-everything loop as the rest of the ORM.
Spatial types are Postgres-only. A geometry field on a SQLite backend fails the boot system check with a clear message, exactly like ArrayField and Decimal. The geo dependency stack (geo-types, geozero) compiles only when you enable the feature, so a non-geo app pays nothing.
Enable the feature
# Cargo.tomlumbral = { version = "0.0.11", features = ["postgis"] }Declare a spatial field
The Rust type is always umbral::orm::gis::Geometry; the subtype and SRID come from the attribute, because one newtype can't encode them.
use umbral::prelude::*; #[derive(Debug, Clone, sqlx::FromRow, serde::Serialize, serde::Deserialize, Model)]pub struct Facility { pub id: i64, pub name: String, // A WGS84 point, GiST-indexed for spatial queries. #[umbral(geometry = "point", srid = 4326, index)] pub location: umbral::orm::gis::Geometry,}geometry = "..."selects a planar column;geography = "..."selects a spheroidal one (distances in metres).- The subtype is one of
point,linestring,polygon,multipoint,multilinestring,multipolygon,geometrycollection, orgeometryfor the unconstrained base type (accepts any subtype — the right choice when your data mixesPolygonandMultiPolygon, like most admin boundaries). sriddefaults to4326(WGS84 lon/lat).#[umbral(index)]on a spatial column emits a GiST index (a B-tree does nothing for spatial predicates), and the migration auto-emitsCREATE EXTENSION IF NOT EXISTS postgisahead of the first spatial table.
makemigrations renders location geometry(Point,4326), then migrate applies it.
Read and write GeoJSON
The Geometry value serialises as a GeoJSON geometry object, so REST clients and Leaflet/Mapbox consume it directly, and writes accept the same shape (or a WKT/EWKT string):
{ "name": "Kiriari Dispensary", "location": { "type": "Point", "coordinates": [37.47605, -0.3994] } }In Rust, the column decodes straight into geo_types:
let f = Facility::objects().get(facility::ID.eq(1)).await?;if let geo_types::Geometry::Point(p) = f.location.0 { println!("lon={} lat={}", p.x(), p.y());}Spatial queries
The generated column constant exposes the common spatial vocabulary. Operands are WKT/EWKT strings (SRID=4326;POINT(36.8 -1.3)):
// Facilities within 5 km of a point. `dwithin_meters` casts to geography so// the distance is metres even on a planar `geometry(…, 4326)` column (plain// `dwithin` measures in the column's own units — degrees for geometry(4326)).let near = Facility::objects() .filter(facility::LOCATION.dwithin_meters("SRID=4326;POINT(36.8172 -1.2864)", 5000.0)) .fetch_pg(&pool) .await?; // Facilities whose point falls inside a county polygon.let inside = Facility::objects() .filter(facility::LOCATION.intersects(county_ewkt)) .count() .await?;Available predicates: dwithin(other, distance) (column units), dwithin_meters(other, meters) (spheroidal metres via a ::geography cast), intersects(other), contains(other), within(other), and bbox_overlaps(other) (the index-accelerated && bounding-box pre-filter), plus is_null / is_not_null and ordering.
Spatial filters over REST
umbral-rest exposes a spatial filter family on geometry columns, so a web client filters without writing SQL:
GET /facilities/?location__dwithin=36.8172,-1.2864,5000 # within 5 km (metres) of a pointGET /facilities/?location__bbox=36.6,-1.5,37.1,-1.1 # bounding-box overlap (minx,miny,maxx,maxy)dwithin casts to geography so the distance is always metres; bbox compiles to the GiST-accelerated && operator. A geometry column accepts only these spatial lookups (and isnull); a scalar lookup on it returns a clear 400.
Design & scope
See the design note docs/decisions/2026-08-08-postgis-and-content-types.md for the full rationale — how spatial types reuse the existing SqlType / backend-gating / migration seams rather than bolting on a parallel system. Deferred for v1: raster, 3D/4D coordinates, topology, in-ORM ST_Transform, distance-as-annotation, and the interactive admin map picker.