@@ -40,8 +40,90 @@ const preset = {
4040- Concrete types for all geometry subtypes: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
4141- Subtype-specific fields (x/y/z for Points, points for LineStrings, exterior/interiors for Polygons, etc.)
4242- Geography-aware field naming (longitude/latitude/height instead of x/y/z)
43+ - Cross-table spatial filters via ` @spatialRelation ` smart tags (see below)
4344- Graceful degradation when PostGIS is not installed
4445
46+ ## Spatial relations via smart tags
47+
48+ ` PostgisSpatialRelationsPlugin ` lets you declare a cross-table or
49+ self-relation whose join predicate is a PostGIS spatial function. The
50+ plugin emits a first-class relation + filter field on the owning codec's
51+ ` Filter ` type that compiles to an ` EXISTS (…) ` subquery using the
52+ declared operator.
53+
54+ ### Tag grammar
55+
56+ ``` sql
57+ COMMENT ON COLUMN <owner_table>.<owner_col> IS
58+ E' @spatialRelation <relation_name> <target_table>.<target_col> <operator> [<param_name>]' ;
59+ ```
60+
61+ - ` <relation_name> ` — user-chosen name for the generated field (e.g. ` county ` )
62+ - ` <target_table>.<target_col> ` — target geometry/geography column; also
63+ accepts ` <schema>.<table>.<col> `
64+ - ` <operator> ` — PG-native ` st_* ` function name; resolved at schema build
65+ time against ` pg_proc `
66+ - ` <param_name> ` — required only for parametric operators
67+ (currently ` st_dwithin ` )
68+
69+ ### Supported operators (v1)
70+
71+ | Operator | PostGIS function | Kind | Arity |
72+ | ---| ---| ---| ---|
73+ | ` st_contains ` | ` ST_Contains ` | function | 2 |
74+ | ` st_within ` | ` ST_Within ` | function | 2 |
75+ | ` st_covers ` | ` ST_Covers ` | function | 2 |
76+ | ` st_coveredby ` | ` ST_CoveredBy ` | function | 2 |
77+ | ` st_intersects ` | ` ST_Intersects ` | function | 2 |
78+ | ` st_equals ` | ` ST_Equals ` | function | 2 |
79+ | ` st_bbox_intersects ` | ` && ` | infix | 2 |
80+ | ` st_dwithin ` | ` ST_DWithin ` | function | 3 (parametric) |
81+
82+ ### Filter shapes
83+
84+ 2-arg operators use the familiar ` some ` / ` every ` / ` none ` shape:
85+
86+ ``` graphql
87+ telemedicineClinics (
88+ filter : { county : { some : { name : { eq : "California County" } } } }
89+ ) { nodes { id name } }
90+ ```
91+
92+ ` st_dwithin ` takes its distance at the relation level (it parametrises
93+ the join, not the joined row):
94+
95+ ``` graphql
96+ telemedicineClinics (
97+ filter : {
98+ nearbyClinic : {
99+ distance : 5000
100+ some : { specialty : { eq : "pediatrics" } }
101+ }
102+ }
103+ ) { nodes { id name } }
104+ ```
105+
106+ Distance units follow PostGIS semantics: ** meters** for ` geography `
107+ columns, ** SRID coordinate units** for ` geometry ` columns.
108+
109+ ### Self-relations
110+
111+ When ` <owner_table> ` equals ` <target_table> ` , the plugin emits an
112+ automatic self-exclusion predicate so a row is never "related to
113+ itself":
114+
115+ - Single-column PK: ` other.id <> self.id `
116+ - Composite PK: ` (other.a, other.b) IS DISTINCT FROM (self.a, self.b) `
117+
118+ Self-relations on tables without a primary key are rejected at schema
119+ build time.
120+
121+ ### GIST index warning
122+
123+ At schema build time the plugin emits a non-fatal warning when the
124+ target geometry/geography column has no GIST index — spatial predicates
125+ are typically unusable without one.
126+
45127## License
46128
47129MIT
0 commit comments