@@ -236,34 +236,49 @@ describe('PostGIS operator factory (createPostgisOperatorFactory)', () => {
236236
237237 describe ( 'SQL generation' , ( ) => {
238238 describe ( 'function-based operators' , ( ) => {
239- it ( 'generates schema-qualified SQL for public schema' , ( ) => {
239+ // The input-binding contract (regression guard for #724): every
240+ // operator must wrap the GeoJSON input with ST_GeomFromGeoJSON(...)
241+ // — PostgreSQL's geometry_in / geography_in parsers reject raw
242+ // GeoJSON text. The compiled SQL must therefore always contain the
243+ // schema-qualified function call binding the JSON-encoded input as
244+ // `::text` before any further casting.
245+
246+ it ( 'wraps input with ST_GeomFromGeoJSON (public schema)' , ( ) => {
240247 const { registered } = runFactory ( { schemaName : 'public' } ) ;
241248 const containsOp = registered . find ( r => r . operatorName === 'contains' ) ;
242249 expect ( containsOp ) . toBeDefined ( ) ;
243250
244251 const i = sql . identifier ( 'col' ) ;
245252 const v = sql . identifier ( 'val' ) ;
246- const result = containsOp ! . resolve ( i , v , null , null , {
253+ const input = { type : 'Point' , coordinates : [ - 122.4194 , 37.7749 ] } ;
254+ const result = containsOp ! . resolve ( i , v , input , null , {
247255 fieldName : null ,
248256 operatorName : 'contains'
249257 } ) ;
250258 const compiled = sql . compile ( result ) ;
251- expect ( compiled . text ) . toBe ( '"public"."st_contains"("col", "val")' ) ;
259+ expect ( compiled . text ) . toBe (
260+ '"public"."st_contains"("col", "public"."st_geomfromgeojson"($1::text))'
261+ ) ;
262+ expect ( compiled . values ) . toEqual ( [ JSON . stringify ( input ) ] ) ;
252263 } ) ;
253264
254- it ( 'generates schema-qualified SQL for non-public schema' , ( ) => {
265+ it ( 'wraps input with ST_GeomFromGeoJSON ( non-public schema) ' , ( ) => {
255266 const { registered } = runFactory ( { schemaName : 'postgis' } ) ;
256267 const containsOp = registered . find ( r => r . operatorName === 'contains' ) ;
257268 expect ( containsOp ) . toBeDefined ( ) ;
258269
259270 const i = sql . identifier ( 'col' ) ;
260271 const v = sql . identifier ( 'val' ) ;
261- const result = containsOp ! . resolve ( i , v , null , null , {
272+ const input = { type : 'Point' , coordinates : [ - 122.4194 , 37.7749 ] } ;
273+ const result = containsOp ! . resolve ( i , v , input , null , {
262274 fieldName : null ,
263275 operatorName : 'contains'
264276 } ) ;
265277 const compiled = sql . compile ( result ) ;
266- expect ( compiled . text ) . toBe ( '"postgis"."st_contains"("col", "val")' ) ;
278+ expect ( compiled . text ) . toBe (
279+ '"postgis"."st_contains"("col", "postgis"."st_geomfromgeojson"($1::text))'
280+ ) ;
281+ expect ( compiled . values ) . toEqual ( [ JSON . stringify ( input ) ] ) ;
267282 } ) ;
268283
269284 it ( 'lowercases function names in SQL' , ( ) => {
@@ -273,89 +288,86 @@ describe('PostGIS operator factory (createPostgisOperatorFactory)', () => {
273288
274289 const i = sql . identifier ( 'a' ) ;
275290 const v = sql . identifier ( 'b' ) ;
276- const result = op3d ! . resolve ( i , v , null , null , {
291+ const input = { type : 'Point' , coordinates : [ - 122.4194 , 37.7749 , 254 ] } ;
292+ const result = op3d ! . resolve ( i , v , input , null , {
277293 fieldName : null ,
278294 operatorName : 'intersects3D'
279295 } ) ;
280296 const compiled = sql . compile ( result ) ;
281- expect ( compiled . text ) . toBe ( '"public"."st_3dintersects"("a", "b")' ) ;
297+ expect ( compiled . text ) . toBe (
298+ '"public"."st_3dintersects"("a", "public"."st_geomfromgeojson"($1::text))'
299+ ) ;
282300 } ) ;
283- } ) ;
284301
285- describe ( 'SQL operator-based operators' , ( ) => {
286- it ( 'generates correct SQL for = operator' , ( ) => {
302+ it ( 'casts to geography when the operator is registered on a geography type' , ( ) => {
287303 const { registered } = runFactory ( ) ;
288- const exactOp = registered . find ( r => r . operatorName === 'exactlyEquals' ) ;
289- expect ( exactOp ) . toBeDefined ( ) ;
304+ // `intersects` is registered for both geometry and geography. We
305+ // want the geography variant to append `::geography` after the
306+ // ST_GeomFromGeoJSON wrap so PostGIS picks the geography overload.
307+ const geogIntersects = registered . find (
308+ r => r . operatorName === 'intersects' && r . typeName === 'GeographyPoint'
309+ ) ;
310+ expect ( geogIntersects ) . toBeDefined ( ) ;
290311
291312 const i = sql . identifier ( 'col' ) ;
292313 const v = sql . identifier ( 'val' ) ;
293- const result = exactOp ! . resolve ( i , v , null , null , {
314+ const input = { type : 'Point' , coordinates : [ - 122.4194 , 37.7749 ] } ;
315+ const result = geogIntersects ! . resolve ( i , v , input , null , {
294316 fieldName : null ,
295- operatorName : 'exactlyEquals '
317+ operatorName : 'intersects '
296318 } ) ;
297319 const compiled = sql . compile ( result ) ;
298- expect ( compiled . text ) . toBe ( '"col" = "val"' ) ;
320+ expect ( compiled . text ) . toBe (
321+ '"public"."st_intersects"("col", "public"."st_geomfromgeojson"($1::text)::"public"."geography")'
322+ ) ;
299323 } ) ;
324+ } ) ;
300325
301- it ( 'generates correct SQL for && operator' , ( ) => {
326+ describe ( 'SQL operator-based operators' , ( ) => {
327+ const runOp = ( operatorName : string ) => {
302328 const { registered } = runFactory ( ) ;
303- const bboxOp = registered . find ( r => r . operatorName === 'bboxIntersects2D' ) ;
304- expect ( bboxOp ) . toBeDefined ( ) ;
329+ const op = registered . find ( r => r . operatorName === operatorName ) ;
330+ expect ( op ) . toBeDefined ( ) ;
305331
306- const i = sql . identifier ( 'col' ) ;
307- const v = sql . identifier ( 'val' ) ;
308- const result = bboxOp ! . resolve ( i , v , null , null , {
309- fieldName : null ,
310- operatorName : 'bboxIntersects2D'
311- } ) ;
312- const compiled = sql . compile ( result ) ;
313- expect ( compiled . text ) . toBe ( '"col" && "val"' ) ;
332+ const input = { type : 'Point' , coordinates : [ - 122.4194 , 37.7749 ] } ;
333+ const result = op ! . resolve (
334+ sql . identifier ( 'col' ) ,
335+ sql . identifier ( 'val' ) ,
336+ input ,
337+ null ,
338+ { fieldName : null , operatorName }
339+ ) ;
340+ return sql . compile ( result ) ;
341+ } ;
342+
343+ it ( 'generates correct SQL for = operator' , ( ) => {
344+ expect ( runOp ( 'exactlyEquals' ) . text ) . toBe (
345+ '"col" = "public"."st_geomfromgeojson"($1::text)'
346+ ) ;
314347 } ) ;
315348
316- it ( 'generates correct SQL for ~ operator' , ( ) => {
317- const { registered } = runFactory ( ) ;
318- const bboxContainsOp = registered . find ( r => r . operatorName === 'bboxContains' ) ;
319- expect ( bboxContainsOp ) . toBeDefined ( ) ;
349+ it ( 'generates correct SQL for && operator' , ( ) => {
350+ expect ( runOp ( 'bboxIntersects2D' ) . text ) . toBe (
351+ '"col" && "public"."st_geomfromgeojson"($1::text)'
352+ ) ;
353+ } ) ;
320354
321- const i = sql . identifier ( 'col' ) ;
322- const v = sql . identifier ( 'val' ) ;
323- const result = bboxContainsOp ! . resolve ( i , v , null , null , {
324- fieldName : null ,
325- operatorName : 'bboxContains'
326- } ) ;
327- const compiled = sql . compile ( result ) ;
328- expect ( compiled . text ) . toBe ( '"col" ~ "val"' ) ;
355+ it ( 'generates correct SQL for ~ operator' , ( ) => {
356+ expect ( runOp ( 'bboxContains' ) . text ) . toBe (
357+ '"col" ~ "public"."st_geomfromgeojson"($1::text)'
358+ ) ;
329359 } ) ;
330360
331361 it ( 'generates correct SQL for ~= operator' , ( ) => {
332- const { registered } = runFactory ( ) ;
333- const bboxEqOp = registered . find ( r => r . operatorName === 'bboxEquals' ) ;
334- expect ( bboxEqOp ) . toBeDefined ( ) ;
335-
336- const i = sql . identifier ( 'col' ) ;
337- const v = sql . identifier ( 'val' ) ;
338- const result = bboxEqOp ! . resolve ( i , v , null , null , {
339- fieldName : null ,
340- operatorName : 'bboxEquals'
341- } ) ;
342- const compiled = sql . compile ( result ) ;
343- expect ( compiled . text ) . toBe ( '"col" ~= "val"' ) ;
362+ expect ( runOp ( 'bboxEquals' ) . text ) . toBe (
363+ '"col" ~= "public"."st_geomfromgeojson"($1::text)'
364+ ) ;
344365 } ) ;
345366
346367 it ( 'generates correct SQL for &&& operator' , ( ) => {
347- const { registered } = runFactory ( ) ;
348- const ndOp = registered . find ( r => r . operatorName === 'bboxIntersectsND' ) ;
349- expect ( ndOp ) . toBeDefined ( ) ;
350-
351- const i = sql . identifier ( 'col' ) ;
352- const v = sql . identifier ( 'val' ) ;
353- const result = ndOp ! . resolve ( i , v , null , null , {
354- fieldName : null ,
355- operatorName : 'bboxIntersectsND'
356- } ) ;
357- const compiled = sql . compile ( result ) ;
358- expect ( compiled . text ) . toBe ( '"col" &&& "val"' ) ;
368+ expect ( runOp ( 'bboxIntersectsND' ) . text ) . toBe (
369+ '"col" &&& "public"."st_geomfromgeojson"($1::text)'
370+ ) ;
359371 } ) ;
360372 } ) ;
361373 } ) ;
0 commit comments