Spatial Columns (PostGIS)

CrateStack can declare PostGIS geography / 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:
2. Enable the matching Cargo feature. This is what makes the supporting code exist in your build:
Declaring the extension without the feature is a 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

Both 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:
The type modifier is rendered without a space after the comma, matching how PostGIS itself reports the type — so a later introspection diff of the same column compares equal instead of reporting a phantom change.

Querying

The generated FieldRef 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:
For farthest-first, use .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 a Vec<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! rejects extension 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.