Skip to content

fix+test(postgis): ST_GeomFromGeoJSON wrap for spatial filters + regression suite (#724)#992

Merged
pyramation merged 7 commits intomainfrom
feat/postgis-spatial-fix-and-tests
Apr 17, 2026
Merged

fix+test(postgis): ST_GeomFromGeoJSON wrap for spatial filters + regression suite (#724)#992
pyramation merged 7 commits intomainfrom
feat/postgis-spatial-fix-and-tests

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

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 default graphile-connection-filter pipeline, which binds GeoJSON objects as raw text cast to ::geometry / ::geography. PostgreSQL's geometry_in / geography_in parsers do not accept GeoJSON text — they require an explicit ST_GeomFromGeoJSON(...) wrap. Every spatial filter with a GeoJSON input therefore failed at runtime with parse error - invalid geometry. The within-distance-operator.ts sibling 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-postgis and graphql/orm-test were not in the test matrix. This PR adds them.

What's in the diff

  1. Fixgraphile/graphile-postgis/src/plugins/connection-filter-operators.ts
    • Each operator registration now overrides resolveSqlValue: () => sql.null to disable the default raw-text bind.
    • The operator's resolve(...) wraps the input via <schema>.st_geomfromgeojson($1::text), appending ::<schema>.geography when the registration is on a geography-based type.
  2. Unit testsgraphile/graphile-postgis/__tests__/connection-filter-operators.test.ts
    • Rewrites existing SQL-shape tests to assert the new contract: compiled SQL must contain the schema-qualified st_geomfromgeojson($1::text) wrap, and geography registrations must additionally append the ::geography cast.
  3. Structural guardgraphile/graphile-postgis/__tests__/connection-filter-integration.test.ts
    • New test asserting every registered spec overrides value binding (resolveSqlValue / resolveInput / resolveInputCodec). This catches future regressions where a new operator forgets to wrap GeoJSON.
  4. End-to-end regression suitegraphql/orm-test/__tests__/postgis-spatial.test.ts + __fixtures__/seed/postgis-spatial-seed.sql
    • 66 live-PG tests across 10 column types (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, PointZ on geometry; Point, Polygon on geography) exercising all spatial operators through the generated ORM. Seed follows the exact pattern of mega-seed.sql and loads via seed.sqlfile([...]).
  5. CI matrix.github/workflows/run-tests.yaml
    • Adds graphile/graphile-postgis and graphql/orm-test so the wrapping contract and end-to-end behavior are validated on every PR.

Review & Testing Checklist for Human

  • Confirm the geography cast logic is correctspec.baseType === 'geography' appends ::<schema>.geography after the ST_GeomFromGeoJSON(...) wrap. Verify this produces the geography overload of each ST_* function rather than the geometry one (especially for operators registered on both codecs: intersects, covers, coveredBy, exactlyEquals, bboxIntersects2D).
  • Spot-check test-data corrections in 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].
    • bboxOverlapsOrLeftOf excludes LA (east of polygon's right edge) — removed LA from expected.
    • SF_NY_MULTILINESTRING restructured to include SF and NY as explicit vertices, because geography uses geodesic math and a constant-latitude segment is a great-circle arc that dips south of its endpoints' shared latitude.
    • Two loc3D test cases were miscased as loc3d.
  • Confirm the withinDistance xit scope-out is correct. Two withinDistance cases are skipped with a FIXME. Introspection of the generated schema shows WithinDistanceInput and the withinDistance field are both missing from GeometryInterfaceFilter even though within-distance-operator.ts registers 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.
  • Confirm the fix doesn't regress any caller that was passing a non-GeoJSON shape (e.g. a raw WKT string) through the default pipeline. The default bind is now disabled for all 26 operators.

Test Plan

  • Local: pnpm test in graphile/graphile-postgis (218/218) and graphql/orm-test (66/66 + 2 skipped withinDistance).
  • CI: the two newly-added matrix jobs (graphile/graphile-postgis, graphql/orm-test) must be green on this PR head.

Notes

Link to Devin session: https://app.devin.ai/sessions/c5eeee65a3c546c4ac6753bb05fa03e0
Requested by: @pyramation

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.
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpgpm@​1.4.27510010098100

View full report

@pyramation pyramation merged commit 1281a53 into main Apr 17, 2026
51 checks passed
@pyramation pyramation deleted the feat/postgis-spatial-fix-and-tests branch April 17, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant