Spatial Columns (PostGIS)
CrateStack can declare PostGISgeography / geometry columns directly
in .cstack, emit the matching DDL and CREATE EXTENSION, and query
them through typed builders — so a spatial column is an ordinary part of
the schema rather than something bolted on with a hand-written migration.
Enabling it
Two things are required, and both — this is the most common mistake. 1. Declare the extension in the schema. This unlocks the syntax:compile_error!, not a
silent no-op — the message names the feature and the facade to enable it
on. The feature pulls in no third-party crate: PostGIS’s wire format is
EWKB, which is just bytes.
The feature also gates the query surface (ST_Covers / ST_DWithin /
ST_Distance and their FieldRef accessors), so enable it even on a
service that only queries spatial columns another service writes.
Declaring a column
Geography (spheroidal — distances in metres on the WGS-84
spheroid) and Geometry (planar) are available. Three argument forms
are accepted:
An SRID without a subtype (
Geography(4326)) is rejected, because
PostGIS’s type modifier is positional.
Subtypes
Subtype names are validated against PostGIS’s own vocabulary, so a typo is a schema error rather than a runtime SQL failure:Point, LineString, Polygon, MultiPoint, MultiLineString,
MultiPolygon, GeometryCollection, CircularString, CompoundCurve,
CurvePolygon, MultiCurve, MultiSurface, PolyhedralSurface,
Triangle, Tin, and Geometry (the “any subtype” modifier).
Each accepts a Z, M, or ZM suffix for 3D and measured geometries —
PointZ, PointZM, MultiPolygonZM. Casing is free (POINT, point
and Point all work) and is normalised into the migration snapshot, so
re-casing a subtype is not treated as a column change.
The index attribute takes a bare access method name —
using: gist,
not using: "gist". A quoted value is rejected.Generated DDL
cratestack migrate diff emits the extension once, before any DDL that
references it:
Querying
The generatedFieldRef accessor carries the column name, so it is
checked at compile time:
point(lng, lat) follows PostGIS’s ST_MakePoint(x, y) convention —
longitude first. Nothing can detect a swap for you; the filter will
simply match the wrong side of the world.
order_by_distance_to is the ordering half of the pair whose filtering
half is dwithin_geography. Use them together for “closest N within X
metres” rather than re-computing distance in application code after the
radius filter returns:
.distance_to_point(p).desc(). A NULL
geography compares as a NULL distance and sorts last under the
framework’s default NULLS LAST.
The Rust type
A spatial field is aVec<u8> holding EWKB, PostGIS’s binary
format — the same Rust type a Bytes field gets, so it serialises as
base64 on the REST/RPC surface and maps to Uint8List (Dart) and
Uint8Array (TypeScript) in generated clients.
CrateStack does not parse EWKB for you. Produce and consume it with
PostGIS’s own functions (ST_GeogFromText, ST_AsEWKB) or a geometry
crate of your choosing. Writing a literal is usually easiest in SQL:
Not supported
- The embedded backend. PostGIS is Postgres-only, and
include_embedded_schema!rejectsextension postgis { }unconditionally — no Cargo feature makes it valid, because the rusqlite backend ships no SpatiaLite. - Procedure arguments and return types. Spatial types are model/mixin/type/auth fields only in this release.
- Trigger generation. Deriving a geography from ordinary lat/lng columns is application policy, so it belongs in a migration you own.