feat(graphile-postgis): PostgisSpatialRelationsPlugin — cross-table spatial filters via @spatialRelation smart tags#993
Merged
pyramation merged 4 commits intomainfrom Apr 17, 2026
Conversation
…patial filters via @spatialRelation smart tags
Adds PostgisSpatialRelationsPlugin: a smart-tag-driven plugin that
synthesises cross-table and self-relation filter fields whose join
predicate is a PostGIS spatial function (ST_Contains, ST_DWithin, …).
Tag grammar:
COMMENT ON COLUMN owner.col IS
E'@spatialRelation <name> <table>.<col> <operator> [<param>]';
Supported operators (v1, PG-native snake_case):
2-arg: st_contains, st_within, st_covers, st_coveredby,
st_intersects, st_equals, st_bbox_intersects
3-arg: st_dwithin (parametric distance, meters for geography,
SRID units for geometry)
Filter shapes:
2-arg: { county: { some: { name: { eq: 'California County' } } } }
dwithin: { nearbyClinic: { distance: 5000, some: {...} } }
Features:
- Emits EXISTS (…) subqueries with ST_<op>(target, owner) join
predicate, reusing connection-filter's some/every/none plumbing.
- Auto self-join exclusion for self-relations (single- or composite-PK).
- GIST-index warning at schema build time when the target column
has no GIST index.
- pg_proc validation: operators must resolve against the PostGIS
schema at build time.
- Works with non-public PostGIS schemas (uses sql.identifier, not
literals).
Tests:
- 31 unit tests: tag parsing, registry, collection, validation
errors (graphile-postgis/__tests__/spatial-relations.test.ts).
- E2E tests via ORM with counties + telemedicine_clinics fixture
(orm-test/__tests__/postgis-spatial-relations.test.ts).
- Seed adds counties + telemedicine_clinics tables with four
@spatialRelation tags exercising cross-table and self-relation
paths.
Refs: constructive-io/constructive-planning#728
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
… some/every/none collision Setting isPgConnectionFilterMany: true on our per-relation spatial filter type caused ConnectionFilterBackwardRelationsPlugin to auto-register some/every/none fields with FK-join semantics — colliding with (and semantically different from) the ones we register. Our plugin fully owns those fields for spatial-relation filter types, so we leave that flag off.
…and order Tag grammar reads as '<owner_col> <op> <target_col>' so the PostGIS call must be ST_<op>(owner, target). Symmetric operators hid the bug; st_within/st_contains/st_covers/st_coveredby inverted the match set. Also corrects test filter operator name to equalTo (graphile idiom) instead of eq.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
PostgisSpatialRelationsPlugin— a smart-tag-driven plugin that synthesises first-class relation + filter fields whose join predicate is a PostGIS spatial function. Enables cross-table and self-relation spatial filtering through the generated ORM without hand-written PG functions.Closes the v1 scope of constructive-io/constructive-planning#728.
Tag grammar (PG-native snake_case):
Filter shapes:
Supported operators (v1):
st_contains,st_within,st_covers,st_coveredby,st_intersects,st_equals,st_bbox_intersects(2-arg) +st_dwithin(3-arg, parametric).Implementation highlights:
EXISTS (…)subqueries withST_<op>(owner, target)as the join predicate, reusing connection-filter'ssome/every/noneplumbing. Operand order matches the tag's reading direction ("owner<op>target"), which is required for directional operators (st_within,st_contains,st_covers,st_coveredby).other.id <> self.id; composite PK →IS DISTINCT FROMtuple). Self-relations on tables without a PK are rejected at init.pg_procat schema build.sql.identifier, never literals).distance) is read in the parent relation'sapplyso it is available beforesome/every/nonechildren run — does not rely on input-field iteration order.Files:
graphile/graphile-postgis/src/plugins/spatial-relations.ts(new plugin, ~870 lines)graphile/graphile-postgis/src/preset.ts,src/index.ts(wire + export)graphile/graphile-postgis/__tests__/spatial-relations.test.ts(31 unit tests: registry, tag parsing, collection, validation errors, plugin metadata)graphile/graphile-postgis/__tests__/{index,preset}.test.ts(update export/plugin-count assertions)graphql/orm-test/__fixtures__/seed/postgis-spatial-seed.sql(addscounties+telemedicine_clinicsfixture with 4@spatialRelationtags)graphql/orm-test/__tests__/postgis-spatial-relations.test.ts(new E2E file: 17 tests — schema shape + live-PG filter semantics)Updates since last revision
buildSpatialJoinFragmentemittedST_<op>(target, owner), which silently "worked" for symmetric ops (st_intersects,st_dwithin,st_equals,&&) but inverted the match set for the directional ones. Now emitsST_<op>(owner, target)to match the tag grammar's reading direction. Caught by the E2E suite:st_within/st_coveredbyreturned 0 rows;st_intersects(same fixture) returned the expected 3 — the discrepancy made the root cause obvious.isPgConnectionFilterMany: truefrom the per-relation spatial filter type registration. That flag causedConnectionFilterBackwardRelationsPluginto auto-registersome/every/nonefields with FK-join semantics, colliding with the ones this plugin registers. Intent documented inline where the flag is intentionally omitted.{ equalTo: "…" }(graphile-connection-filter idiom) rather than{ eq: "…" }.orm.telemedicineClinic(singular — what the codegen emits) rather thanorm.telemedicineClinics.Review & Testing Checklist for Human
ST_<op>(owner, target)against the tag grammar's reading. Owner is the side the tag sits on (telemedicine_clinics.location); target is the side named after the operator (counties.geom). Taglocation st_within counties.geom⇒ST_Within(location, counties.geom)⇒ "is the point inside the polygon?". If the intended semantic for any op should read the other way, say so before this lands.finds clinics near SF cardiology clinics within 10 SRID units. The assertion thatSF_CARDIOis not in the result set depends on self-exclusion excludingSF_CARDIOfrom its own radius match when the inner filter narrows to cardiology. Please sanity-check this reasoning.@spatialRelation <name> <table>.<col> <op> [<param>]and the PG-native snake_case operator names.Notes
graphile/graphile-postgis(250+ unit tests) andgraphql/orm-test(17 new live-PG E2E tests, all passing).graphile/graphile-postgisfully green;graphql/orm-test -- postgis-spatial-relations→ 17/17 passing against postgres-plus:18 + postgis.spatial_relationnode (Phase 2 in feat(codegen): add docs generation for ORM, React Query, and multi-target support #728) can emit these tags without any plugin change.st_overlaps/st_touches/st_crosses, DE-9IMst_relate.Link to Devin session: https://app.devin.ai/sessions/c5eeee65a3c546c4ac6753bb05fa03e0
Requested by: @pyramation