Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions graphile/graphile-postgis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,90 @@ const preset = {
- Concrete types for all geometry subtypes: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
- Subtype-specific fields (x/y/z for Points, points for LineStrings, exterior/interiors for Polygons, etc.)
- Geography-aware field naming (longitude/latitude/height instead of x/y/z)
- Cross-table spatial filters via `@spatialRelation` smart tags (see below)
- Graceful degradation when PostGIS is not installed

## Spatial relations via smart tags

`PostgisSpatialRelationsPlugin` lets you declare a cross-table or
self-relation whose join predicate is a PostGIS spatial function. The
plugin emits a first-class relation + filter field on the owning codec's
`Filter` type that compiles to an `EXISTS (…)` subquery using the
declared operator.

### Tag grammar

```sql
COMMENT ON COLUMN <owner_table>.<owner_col> IS
E'@spatialRelation <relation_name> <target_table>.<target_col> <operator> [<param_name>]';
```

- `<relation_name>` — user-chosen name for the generated field (e.g. `county`)
- `<target_table>.<target_col>` — target geometry/geography column; also
accepts `<schema>.<table>.<col>`
- `<operator>` — PG-native `st_*` function name; resolved at schema build
time against `pg_proc`
- `<param_name>` — required only for parametric operators
(currently `st_dwithin`)

### Supported operators (v1)

| Operator | PostGIS function | Kind | Arity |
|---|---|---|---|
| `st_contains` | `ST_Contains` | function | 2 |
| `st_within` | `ST_Within` | function | 2 |
| `st_covers` | `ST_Covers` | function | 2 |
| `st_coveredby` | `ST_CoveredBy` | function | 2 |
| `st_intersects` | `ST_Intersects` | function | 2 |
| `st_equals` | `ST_Equals` | function | 2 |
| `st_bbox_intersects` | `&&` | infix | 2 |
| `st_dwithin` | `ST_DWithin` | function | 3 (parametric) |

### Filter shapes

2-arg operators use the familiar `some` / `every` / `none` shape:

```graphql
telemedicineClinics(
filter: { county: { some: { name: { eq: "California County" } } } }
) { nodes { id name } }
```

`st_dwithin` takes its distance at the relation level (it parametrises
the join, not the joined row):

```graphql
telemedicineClinics(
filter: {
nearbyClinic: {
distance: 5000
some: { specialty: { eq: "pediatrics" } }
}
}
) { nodes { id name } }
```

Distance units follow PostGIS semantics: **meters** for `geography`
columns, **SRID coordinate units** for `geometry` columns.

### Self-relations

When `<owner_table>` equals `<target_table>`, the plugin emits an
automatic self-exclusion predicate so a row is never "related to
itself":

- Single-column PK: `other.id <> self.id`
- Composite PK: `(other.a, other.b) IS DISTINCT FROM (self.a, self.b)`

Self-relations on tables without a primary key are rejected at schema
build time.

### GIST index warning

At schema build time the plugin emits a non-fatal warning when the
target geometry/geography column has no GIST index — spatial predicates
are typically unusable without one.

## License

MIT
6 changes: 6 additions & 0 deletions graphile/graphile-postgis/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('graphile-postgis exports', () => {
expect(postgisExports.PostgisMeasurementFieldsPlugin).toBeDefined();
expect(postgisExports.PostgisTransformationFieldsPlugin).toBeDefined();
expect(postgisExports.PostgisAggregatePlugin).toBeDefined();
expect(postgisExports.PostgisSpatialRelationsPlugin).toBeDefined();
});

it('should export constants', () => {
Expand Down Expand Up @@ -42,6 +43,11 @@ describe('graphile-postgis exports', () => {
'PostgisMeasurementFieldsPlugin',
'PostgisTransformationFieldsPlugin',
'PostgisAggregatePlugin',
'PostgisSpatialRelationsPlugin',
// Spatial-relations helpers
'OPERATOR_REGISTRY',
'parseSpatialRelationTag',
'collectSpatialRelations',
// Constants
'GisSubtype',
'SUBTYPE_STRING_BY_SUBTYPE',
Expand Down
9 changes: 7 additions & 2 deletions graphile/graphile-postgis/__tests__/preset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { PostgisGeometryFieldsPlugin } from '../src/plugins/geometry-fields';
import { PostgisMeasurementFieldsPlugin } from '../src/plugins/measurement-fields';
import { PostgisTransformationFieldsPlugin } from '../src/plugins/transformation-functions';
import { PostgisAggregatePlugin } from '../src/plugins/aggregate-functions';
import { PostgisSpatialRelationsPlugin } from '../src/plugins/spatial-relations';

describe('GraphilePostgisPreset', () => {
it('should include all 8 plugins', () => {
expect(GraphilePostgisPreset.plugins).toHaveLength(8);
it('should include all 9 plugins', () => {
expect(GraphilePostgisPreset.plugins).toHaveLength(9);
});

it('should include PostgisSpatialRelationsPlugin', () => {
expect(GraphilePostgisPreset.plugins).toContain(PostgisSpatialRelationsPlugin);
});

it('should include PostgisCodecPlugin', () => {
Expand Down
Loading
Loading