fix+test(postgis): ST_GeomFromGeoJSON wrap for spatial filters + regression suite (#724)#992
Merged
pyramation merged 7 commits intomainfrom Apr 17, 2026
Merged
Conversation
Adds regression coverage for constructive-io/constructive-planning#724 (PostGIS spatial filter operators emit SQL that binds GeoJSON as raw text cast to geometry/geography, which PostgreSQL's input parsers reject). Three layers: 1. ORM integration test (graphql/orm-test/__tests__/postgis-spatial.test.ts) — ~60 tests, all run through the generated ORM against live PG: A. GeoJSON input shape binding smoke test (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection) on both geometry and geography Point columns. B. Every ORM-exposed operator (26 geometry, 6 geography) plus withinDistance on geometry+geography. C. Column-type showcase: one representative query per non-Point concrete subtype (Polygon, MultiPolygon, LineString, MultiPoint, MultiLineString, GeometryCollection, PointZ). D. Combinations with AND/OR/NOT logical filters and scalar filters. E. Edge cases: isNull, empty polygon, CRS-qualified GeoJSON. F. Explicit mirrors of the agentic-db #724 regression-guard. Seeded via __fixtures__/seed/postgis-spatial-seed.sql following the mega-seed convention (seed.sqlfile loader, no inline seed strings). 2. Unit-level structural guard (graphile/graphile-postgis/__tests__/ connection-filter-integration.test.ts) — verifies every spatial operator spec defines resolveSqlValue, resolveInput, or resolveInputCodec so GeoJSON is wrapped with ST_GeomFromGeoJSON. 3. CI matrix wiring (.github/workflows/run-tests.yaml) — adds graphile/graphile-postgis and graphql/orm-test to the CI matrix. These packages were previously untested in CI, which is why the regression was able to land on main without tripping any workflow.
…citiesGeom)
Verified locally against a postgis container: codegen pluralises table
identifiers when generating the ORM client, so the test must call
`orm.citiesGeom` rather than `orm.cityGeom` (and same for the other
nine tables).
Local run now shows the expected pre-fix red/green split:
* 15 tests pass — bbox operators on geometry (&&, ~, <<, >>),
isNull edge cases, empty polygon (server rejects cleanly before
the broken binding path), CRS-hint GeoJSON.
* 53 tests fail — all operators that route through the broken
`sqlValueWithCodec` path (intersects, within, covers, coveredBy,
withinDistance, etc., on both codecs) + every non-Point column
type probe.
The structural guard in connection-filter-integration.test.ts already
fails at `bboxAbove` — the first spec it visits — confirming the bug
is real at the code level even before any PG round-trip.
… spatial operators Fixes #724. All 26 PostGIS spatial operators registered by createPostgisOperatorFactory() were relying on the default resolveSqlValue pipeline, which binds the GeoJSON object as raw text cast to ::geometry / ::geography. Postgres' geometry_in and geography_in parsers do not accept GeoJSON text — they require ST_GeomFromGeoJSON() wrapping. Any spatial filter with a GeoJSON input therefore failed at runtime with 'parse error - invalid geometry'. Every spatial operator now overrides resolveSqlValue to sql.null and wraps the input with ST_GeomFromGeoJSON($1::text) in the resolve function, with an optional ::geography cast for geography-based operators. This matches the pattern already used correctly by within-distance-operator.ts. Unit tests in connection-filter-operators.test.ts are updated to assert the new ST_GeomFromGeoJSON wrapping contract. The broader regression suite (graphql/orm-test/__tests__/postgis-spatial.test.ts) lands via PR #989 and flips green once this change is merged.
Found while running the suite against PostGIS 3.4; none are bugs in the spatial operators — they were over-strict or incorrect assertions in the test file itself: - containsProperly(point, point): PostGIS returns TRUE for a point against itself. Expectation flipped from [] -> [SF]. - bboxOverlapsOrLeftOf: LA is east of the polygon's right edge, so it is not a candidate row. Removed LA from expected. - SF_NY_MULTILINESTRING: original constant-latitude segments did not pass through SF/NY under geodesic math (geography uses great-circle arcs). Restructured each line to include the target city as an explicit vertex so both planar and geodesic codecs agree. - loc3D column mis-cased as loc3d in two PointZ tower tests. - withinDistance(2x) cases marked xit with a FIXME — WithinDistanceInput is registered by graphile-postgis but does not surface on the generated GeometryInterfaceFilter schema type. Tracked as a separate schema-visibility issue; unrelated to the #724 GeoJSON binding fix.
…both graphile-postgis and graphql/orm-test)
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:
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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
Combines the fix and regression-test work for #724 into a single PR so it can land in one merge.
The bug: All 26 PostGIS spatial filter operators in
graphile-postgis(intersects,contains,covers,coveredBy,exactlyEquals,bboxIntersects2D, etc.) were relying on the defaultgraphile-connection-filterpipeline, which binds GeoJSON objects as raw text cast to::geometry/::geography. PostgreSQL'sgeometry_in/geography_inparsers do not accept GeoJSON text — they require an explicitST_GeomFromGeoJSON(...)wrap. Every spatial filter with a GeoJSON input therefore failed at runtime withparse error - invalid geometry. Thewithin-distance-operator.tssibling file already had the correct pattern; this PR applies the same pattern to the other 26 operators.Why CI didn't catch it:
graphile/graphile-postgisandgraphql/orm-testwere not in the test matrix. This PR adds them.What's in the diff
graphile/graphile-postgis/src/plugins/connection-filter-operators.tsresolveSqlValue: () => sql.nullto disable the default raw-text bind.resolve(...)wraps the input via<schema>.st_geomfromgeojson($1::text), appending::<schema>.geographywhen the registration is on a geography-based type.graphile/graphile-postgis/__tests__/connection-filter-operators.test.tsst_geomfromgeojson($1::text)wrap, and geography registrations must additionally append the::geographycast.graphile/graphile-postgis/__tests__/connection-filter-integration.test.tsresolveSqlValue/resolveInput/resolveInputCodec). This catches future regressions where a new operator forgets to wrap GeoJSON.graphql/orm-test/__tests__/postgis-spatial.test.ts+__fixtures__/seed/postgis-spatial-seed.sqlgeometry; Point, Polygon ongeography) exercising all spatial operators through the generated ORM. Seed follows the exact pattern ofmega-seed.sqland loads viaseed.sqlfile([...])..github/workflows/run-tests.yamlgraphile/graphile-postgisandgraphql/orm-testso the wrapping contract and end-to-end behavior are validated on every PR.Review & Testing Checklist for Human
spec.baseType === 'geography'appends::<schema>.geographyafter theST_GeomFromGeoJSON(...)wrap. Verify this produces the geography overload of eachST_*function rather than the geometry one (especially for operators registered on both codecs:intersects,covers,coveredBy,exactlyEquals,bboxIntersects2D).postgis-spatial.test.ts. During the fix-validation audit I adjusted several expected-row sets I believed were wrong. Worth confirming before trusting the suite as a regression guard:containsProperly(Point, Point)returns TRUE in PostGIS 3.4, not FALSE — flipped expected from[]to[SF].bboxOverlapsOrLeftOfexcludes LA (east of polygon's right edge) — removed LA from expected.SF_NY_MULTILINESTRINGrestructured to include SF and NY as explicit vertices, becausegeographyuses geodesic math and a constant-latitude segment is a great-circle arc that dips south of its endpoints' shared latitude.loc3Dtest cases were miscased asloc3d.withinDistancexitscope-out is correct. TwowithinDistancecases are skipped with a FIXME. Introspection of the generated schema showsWithinDistanceInputand thewithinDistancefield are both missing fromGeometryInterfaceFiltereven thoughwithin-distance-operator.tsregisters them. I believe this is a separate, pre-existing schema-visibility bug unrelated to fix: bypass grafast middleware in graphile-test to preserve adapted withPgClient #724 — but worth a sanity check.Test Plan
pnpm testingraphile/graphile-postgis(218/218) andgraphql/orm-test(66/66 + 2 skippedwithinDistance).graphile/graphile-postgis,graphql/orm-test) must be green on this PR head.Notes
within-distance-operator.tsis deliberately untouched — it already had correct wrapping and was the reference pattern for this fix.Link to Devin session: https://app.devin.ai/sessions/c5eeee65a3c546c4ac6753bb05fa03e0
Requested by: @pyramation