From be40ebefd78224e25ebd92f429da8c70a655b26c Mon Sep 17 00:00:00 2001 From: Charles Archibong Date: Fri, 15 May 2026 18:52:10 +0100 Subject: [PATCH 1/2] docs: Add PostGIS geography fields documentation Add documentation for geography field types (GeographyPoint, GeographyLineString, GeographyPolygon, GeographyGeometryCollection), spatial filter operators (intersects, dwithin, distance, contains, within), GIST index support, and a guide for upgrading existing projects to use PostGIS. --- docs/06-concepts/02-models/01-models.md | 2 +- .../02-models/04-geography-fields.md | 62 ++++++++++ docs/06-concepts/06-database/04-indexing.md | 20 +++ docs/06-concepts/06-database/06-filter.md | 89 ++++++++++++++ docs/09-upgrading/04-upgrade-to-postgis.md | 116 ++++++++++++++++++ 5 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 docs/06-concepts/02-models/04-geography-fields.md create mode 100644 docs/09-upgrading/04-upgrade-to-postgis.md diff --git a/docs/06-concepts/02-models/01-models.md b/docs/06-concepts/02-models/01-models.md index 67609f43..967b3400 100644 --- a/docs/06-concepts/02-models/01-models.md +++ b/docs/06-concepts/02-models/01-models.md @@ -23,7 +23,7 @@ fields: employees: List ``` -Supported types are [bool](https://api.dart.dev/dart-core/bool-class.html), [int](https://api.dart.dev/dart-core/int-class.html), [double](https://api.dart.dev/dart-core/double-class.html), [String](https://api.dart.dev/dart-core/String-class.html), [Duration](https://api.dart.dev/dart-core/Duration-class.html), [DateTime](https://api.dart.dev/dart-core/DateTime-class.html), [ByteData](https://api.dart.dev/dart-typed_data/ByteData-class.html), [UuidValue](https://pub.dev/documentation/uuid/latest/uuid_value/UuidValue-class.html), [Uri](https://api.dart.dev/dart-core/Uri-class.html), [BigInt](https://api.dart.dev/dart-core/BigInt-class.html), [Vector](./models/vector-fields#vector), [HalfVector](./models/vector-fields#halfvector), [SparseVector](./models/vector-fields#sparsevector), [Bit](./models/vector-fields#bit) and other serializable [classes](#class), [exceptions](#exception) and [enums](#enum). You can also use [List](https://api.dart.dev/dart-core/List-class.html)s, [Map](https://api.dart.dev/dart-core/Map-class.html)s and [Set](https://api.dart.dev/dart-core/Set-class.html)s of the supported types, just make sure to specify the types. All supported types can also be used inside [Record](https://api.dart.dev/dart-core/Record-class.html)s. Null safety is supported. Once your classes are generated, you can use them as parameters or return types to endpoint methods. +Supported types are [bool](https://api.dart.dev/dart-core/bool-class.html), [int](https://api.dart.dev/dart-core/int-class.html), [double](https://api.dart.dev/dart-core/double-class.html), [String](https://api.dart.dev/dart-core/String-class.html), [Duration](https://api.dart.dev/dart-core/Duration-class.html), [DateTime](https://api.dart.dev/dart-core/DateTime-class.html), [ByteData](https://api.dart.dev/dart-typed_data/ByteData-class.html), [UuidValue](https://pub.dev/documentation/uuid/latest/uuid_value/UuidValue-class.html), [Uri](https://api.dart.dev/dart-core/Uri-class.html), [BigInt](https://api.dart.dev/dart-core/BigInt-class.html), [Vector](./models/vector-fields#vector), [HalfVector](./models/vector-fields#halfvector), [SparseVector](./models/vector-fields#sparsevector), [Bit](./models/vector-fields#bit), [GeographyPoint](./models/geography-fields#geographypoint), [GeographyLineString](./models/geography-fields#geographylinestring), [GeographyPolygon](./models/geography-fields#geographypolygon), [GeographyGeometryCollection](./models/geography-fields#geographygeometrycollection) and other serializable [classes](#class), [exceptions](#exception) and [enums](#enum). You can also use [List](https://api.dart.dev/dart-core/List-class.html)s, [Map](https://api.dart.dev/dart-core/Map-class.html)s and [Set](https://api.dart.dev/dart-core/Set-class.html)s of the supported types, just make sure to specify the types. All supported types can also be used inside [Record](https://api.dart.dev/dart-core/Record-class.html)s. Null safety is supported. Once your classes are generated, you can use them as parameters or return types to endpoint methods. ### Required fields diff --git a/docs/06-concepts/02-models/04-geography-fields.md b/docs/06-concepts/02-models/04-geography-fields.md new file mode 100644 index 00000000..47f37f93 --- /dev/null +++ b/docs/06-concepts/02-models/04-geography-fields.md @@ -0,0 +1,62 @@ +# Geography fields + +Geography types are used for storing geospatial data on the surface of the Earth. They are stored as PostGIS geography columns in PostgreSQL using the WGS 84 coordinate system (SRID 4326), which is the standard used by GPS. + +All geography types support spatial filter operations such as proximity search, intersection, containment, and distance-based ordering. See the [Geography operators](../database/filter#geography-operators) section for details. + +To ensure optimal performance with spatial queries, consider creating a GIST index on your geography fields. See the [Geography indexes](../database/indexing#geography-indexes) section for more details. + +:::info +The usage of Geography fields requires the PostGIS PostgreSQL extension to be installed. To set up PostGIS in a new or existing project, see the [Upgrading to PostGIS support](../../upgrading/upgrade-to-postgis) guide. +::: + +## GeographyPoint + +The `GeographyPoint` type stores a single geographic location defined by longitude and latitude. + +```yaml +class: Store +table: store +fields: + name: String + location: GeographyPoint + address: String? +``` + +## GeographyLineString + +The `GeographyLineString` type stores an ordered sequence of points forming a path or route. + +```yaml +class: DeliveryRoute +table: delivery_route +fields: + name: String + path: GeographyLineString + description: String? +``` + +## GeographyPolygon + +The `GeographyPolygon` type stores a closed region defined by an exterior ring and optional interior holes. + +```yaml +class: DeliveryZone +table: delivery_zone +fields: + name: String + boundary: GeographyPolygon + description: String? +``` + +## GeographyGeometryCollection + +The `GeographyGeometryCollection` type stores a collection of mixed geography types — points, lines, and polygons — as a single field. + +```yaml +class: Region +table: region +fields: + name: String + features: GeographyGeometryCollection +``` diff --git a/docs/06-concepts/06-database/04-indexing.md b/docs/06-concepts/06-database/04-indexing.md index 9b2fe594..b0408a07 100644 --- a/docs/06-concepts/06-database/04-indexing.md +++ b/docs/06-concepts/06-database/04-indexing.md @@ -212,3 +212,23 @@ If more than one distance function is going to be frequently used on the same ve ::: For more details on vector indexes and its configuration, refer to the [pgvector extension documentation](https://github.com/pgvector/pgvector/tree/master?tab=readme-ov-file#indexing). + +### Geography indexes + +Geography columns benefit from GIST (Generalized Search Tree) spatial indexes, which significantly improve the performance of spatial queries such as proximity searches, intersection tests, and containment checks. + +```yaml +class: Store +table: store +fields: + name: String + location: GeographyPoint +indexes: + store_location_idx: + fields: location + type: gist +``` + +:::tip +A GIST index on a geography column accelerates all spatial operations (`intersects`, `dwithin`, `distance`, `contains`, `within`). For tables with many rows and frequent spatial queries, adding a GIST index is strongly recommended. +::: diff --git a/docs/06-concepts/06-database/06-filter.md b/docs/06-concepts/06-database/06-filter.md index e9318291..e66f1f47 100644 --- a/docs/06-concepts/06-database/06-filter.md +++ b/docs/06-concepts/06-database/06-filter.md @@ -404,3 +404,92 @@ await User.db.find( ``` In the example we fetch all users that have only "book" orders. + +## Geography operators + +All geography field types (`GeographyPoint`, `GeographyLineString`, `GeographyPolygon`, `GeographyGeometryCollection`) support spatial filter and ordering operations. + +### intersects + +Returns rows where the geography column spatially intersects the given geography value. Wraps `ST_Intersects`. + +```dart +var point = GeographyPoint(longitude: 2.35, latitude: 48.85); + +await Store.db.find( + session, + where: (t) => t.location.intersects(point), +); +``` + +### dwithin + +Returns rows where the geography column is within a given distance (in metres) of the given geography value. Wraps `ST_DWithin`. + +```dart +var point = GeographyPoint(longitude: 2.35, latitude: 48.85); + +await Store.db.find( + session, + where: (t) => t.location.dwithin(point, 1000), +); +``` + +### distance + +Returns the distance in metres between the geography column and the given geography value. Wraps `ST_Distance`. The result can be used in `orderBy` for nearest-first ordering, or compared numerically in `where`. + +```dart +var point = GeographyPoint(longitude: 2.35, latitude: 48.85); + +// Order results nearest-first +await Store.db.find( + session, + orderBy: (t) => t.location.distance(point), +); + +// Filter by distance and order results nearest-first +await Store.db.find( + session, + where: (t) => t.location.distance(point) < 5000, + orderBy: (t) => t.location.distance(point), +); +``` + +### contains + +Returns rows where the geography column fully contains the given geography value. Wraps `ST_Covers`. + +```dart +var point = GeographyPoint(longitude: 2.35, latitude: 48.85); + +await DeliveryZone.db.find( + session, + where: (t) => t.boundary.contains(point), +); +``` + +### within + +Returns rows where the geography column is fully within the given geography value. Wraps `ST_CoveredBy`. + +```dart +var zone = GeographyPolygon( + exteriorRing: [ + GeographyPoint(longitude: 2.30, latitude: 48.82), + GeographyPoint(longitude: 2.40, latitude: 48.82), + GeographyPoint(longitude: 2.40, latitude: 48.90), + GeographyPoint(longitude: 2.30, latitude: 48.90), + GeographyPoint(longitude: 2.30, latitude: 48.82), + ], +); + +await Store.db.find( + session, + where: (t) => t.location.within(zone), +); +``` + +:::tip +For optimal performance with spatial queries, consider creating a GIST index on your geography fields. See the [Geography indexes](indexing#geography-indexes) section for more details. +::: diff --git a/docs/09-upgrading/04-upgrade-to-postgis.md b/docs/09-upgrading/04-upgrade-to-postgis.md new file mode 100644 index 00000000..f7dd2874 --- /dev/null +++ b/docs/09-upgrading/04-upgrade-to-postgis.md @@ -0,0 +1,116 @@ +# Upgrading to PostGIS support + +New Serverpod projects do not include PostGIS by default. To use geography fields in your models, you need a PostgreSQL instance with the PostGIS extension installed. + +:::info +This upgrade is only necessary if you want to use geography fields in your models. If you do not plan to use geography fields, you can skip this upgrade. +::: + +:::warning +If trying to use geography fields without upgrading, you will encounter an error when applying migrations. +::: + +## For Docker-based environments + +1. Update your `docker-compose.yml` to use a PostgreSQL image with PostGIS (e.g., `postgis/postgis:16-3.5`): + +```yaml +services: + postgres: + image: postgis/postgis:16-3.5 # <-- Change from postgres image here + ports: + - '8090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: + POSTGRES_PASSWORD: + volumes: + - _data:/var/lib/postgresql/data + +# Other services... + + postgres_test: + image: postgis/postgis:16-3.5 # <-- Change from postgres image here + ports: + - '9090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: _test + POSTGRES_PASSWORD: + volumes: + - _test_data:/var/lib/postgresql/data +``` + +If your project also uses [vector fields](../concepts/models/vector-fields), you need both pgvector and PostGIS. Create a custom `Dockerfile` instead: + +```dockerfile +FROM pgvector/pgvector:pg16 +RUN apt-get update \ + && apt-get install -y --no-install-recommends postgresql-16-postgis-3 \ + && rm -rf /var/lib/apt/lists/* +``` + +Then reference it in your `docker-compose.yml`: + +```yaml +services: + postgres: + build: + context: . + dockerfile: Dockerfile + ports: + - '8090:5432' + environment: + POSTGRES_USER: postgres + POSTGRES_DB: + POSTGRES_PASSWORD: + volumes: + - _data:/var/lib/postgresql/data +``` + + +2. Recreate your containers to use the new image: + +```bash +docker compose down +docker compose up -d +``` + + +3. Create your first geography field in a model: + +```yaml +class: Store +table: store +fields: + name: String + location: GeographyPoint +``` + + +4. Generate and apply a migration: + +```bash +$ serverpod create-migration +$ dart run bin/main.dart --apply-migrations +``` + +For more details on creating and applying migrations, see the [Migrations](../concepts/database/migrations) section. + +The PostGIS extension will be automatically enabled during the first migration that includes a geography column. + +## For managed PostgreSQL services + +For cloud providers (AWS RDS, Google Cloud SQL, Azure Database, etc.), ensure that the PostGIS extension is available on your PostgreSQL instance. Most major managed services support PostGIS with no additional setup required. If available, the extension will be enabled automatically when applying the migration. + +:::tip +If the cloud provider instructs you to run a `CREATE EXTENSION postgis;` command, you can skip this step as Serverpod will handle it automatically during migration. +::: + +## Troubleshooting + +If you encounter issues with PostGIS: + +- Verify that your PostgreSQL version is 12 or later. +- Check that the PostGIS extension is properly installed on the instance. +- Ensure your database user has the necessary permissions to create extensions. From 6adde980e1646bd10539ee1fe13b1c293458d592 Mon Sep 17 00:00:00 2001 From: Charles Archibong Date: Fri, 15 May 2026 20:05:55 +0100 Subject: [PATCH 2/2] docs: Fix numbering conflict for upgrade-to-postgis guide Rename 04-upgrade-to-postgis.md to 05-upgrade-to-postgis.md to avoid a collision with the existing 04-archive/ directory in the upgrading section. --- .../{04-upgrade-to-postgis.md => 05-upgrade-to-postgis.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/09-upgrading/{04-upgrade-to-postgis.md => 05-upgrade-to-postgis.md} (100%) diff --git a/docs/09-upgrading/04-upgrade-to-postgis.md b/docs/09-upgrading/05-upgrade-to-postgis.md similarity index 100% rename from docs/09-upgrading/04-upgrade-to-postgis.md rename to docs/09-upgrading/05-upgrade-to-postgis.md