Skip to content

Commit 7ff1c88

Browse files
rakeshkky0x777
authored andcommitted
add PostGIS operators in boolean expressions (closes hasura#1051) (hasura#1372)
1 parent e375c61 commit 7ff1c88

File tree

16 files changed

+663
-9
lines changed

16 files changed

+663
-9
lines changed

docs/graphql/manual/api-reference/query.rst

+36
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,42 @@ Checking for ``null`` values :
361361

362362
- ``_is_null`` (takes true/false as values)
363363

364+
PostGIS related operators on GEOMETRY columns :
365+
366+
.. list-table::
367+
:header-rows: 1
368+
369+
* - Operator
370+
- PostGIS equivalent
371+
* - ``_st_contains``
372+
- ``ST_Contains``
373+
* - ``_st_crosses``
374+
- ``ST_Crosses``
375+
* - ``_st_equals``
376+
- ``ST_Equals``
377+
* - ``_st_intersects``
378+
- ``ST_Intersects``
379+
* - ``_st_overlaps``
380+
- ``ST_Overlaps``
381+
* - ``_st_touches``
382+
- ``ST_Touches``
383+
* - ``_st_within``
384+
- ``ST_Within``
385+
* - ``_st_d_within``
386+
- ``ST_DWithin``
387+
388+
(For more details on what these operators do, refer to `PostGIS docs <http://postgis.net/workshops/postgis-intro/spatial_relationships.html>`__.)
389+
390+
.. Note::
391+
1. All operators take a json representation of ``geometry/geography`` values. Also see :doc:`here <../queries/query-filters>` for more query examples on these operators
392+
2. Input value for ``_st_d_within`` operator is an object:-
393+
394+
.. parsed-literal::
395+
396+
{
397+
field-name : {_st_d_within: {distance: Float, from: Value} }
398+
}
399+
364400
365401
.. _OrderByExp:
366402

docs/graphql/manual/queries/query-filters.rst

+130
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,136 @@ Fetch a list of authors whose names begin with A or C (``similar`` is case-sensi
473473
}
474474
}
475475

476+
PostGIS topology operators
477+
--------------------------
478+
The ``_st_contains``, ``_st_crosses``, ``_st_equals``, ``_st_intersects``, ``_st_overlaps``,
479+
``_st_touches``, ``_st_within`` and ``_st_d_within`` operators are used to filter ``geometry`` like columns.
480+
For more details on what these operators do, refer to `PostGIS docs <http://postgis.net/workshops/postgis-intro/spatial_relationships.html>`__.
481+
482+
Use ``json`` (`GeoJSON <https://tools.ietf.org/html/rfc7946>`_) representation of ``geometry`` values in ``variables`` as shown in the following examples
483+
484+
485+
Example: _st_within
486+
^^^^^^^^^^^^^^^^^^^^^
487+
Fetch a list of geometry values which are within the given ``polygon`` value
488+
489+
.. graphiql::
490+
:view_only:
491+
:query:
492+
query geom_table($polygon: geometry){
493+
geom_table(where: {geom_col: {_st_within: $polygon}}){
494+
id
495+
geom_col
496+
}
497+
}
498+
:response:
499+
{
500+
"data": {
501+
"geom_table": [
502+
{
503+
"id": 1,
504+
"geom_col": {
505+
"type": "Point",
506+
"coordinates": [
507+
1,
508+
2
509+
]
510+
}
511+
}
512+
]
513+
}
514+
}
515+
516+
Variables for above query:-
517+
518+
.. code-block:: json
519+
520+
{
521+
"polygon": {
522+
"type": "Polygon",
523+
"coordinates": [
524+
[
525+
[
526+
0,
527+
0
528+
],
529+
[
530+
0,
531+
2
532+
],
533+
[
534+
2,
535+
2
536+
],
537+
[
538+
2,
539+
0
540+
],
541+
[
542+
0,
543+
0
544+
]
545+
]
546+
]
547+
}
548+
}
549+
550+
Example: _st_d_within
551+
^^^^^^^^^^^^^^^^^^^^^
552+
Fetch a list of geometry values which are 3 units from given ``point`` value
553+
554+
.. graphiql::
555+
:view_only:
556+
:query:
557+
query geom_table($point: geometry){
558+
geom_table(where: {geom_col: {_st_d_within: {distance: 3, from: $point}}}){
559+
id
560+
geom_col
561+
}
562+
}
563+
:response:
564+
{
565+
"data": {
566+
"geom_table": [
567+
{
568+
"id": 1,
569+
"geom_col": {
570+
"type": "Point",
571+
"coordinates": [
572+
1,
573+
2
574+
]
575+
}
576+
},
577+
{
578+
"id": 2,
579+
"geom_col": {
580+
"type": "Point",
581+
"coordinates": [
582+
3,
583+
0
584+
]
585+
}
586+
}
587+
]
588+
}
589+
}
590+
591+
Variables for above query:-
592+
593+
.. code-block:: json
594+
595+
{
596+
"point": {
597+
"type": "Point",
598+
"coordinates": [
599+
0,
600+
0
601+
]
602+
}
603+
}
604+
605+
476606
Filter or check for null values
477607
-------------------------------
478608
Checking for null values can be achieved using the ``_is_null`` operator.

server/src-lib/Hasura/GraphQL/Context.hs

+60-2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,17 @@ mkCompExpTy :: PGColType -> G.NamedType
161161
mkCompExpTy =
162162
G.NamedType . mkCompExpName
163163

164+
{-
165+
input st_d_within_input {
166+
distance: Float!
167+
from: geometry!
168+
}
169+
-}
170+
171+
stDWithinInpTy :: G.NamedType
172+
stDWithinInpTy = G.NamedType "st_d_within_input"
173+
174+
164175
--- | make compare expression input type
165176
mkCompExpInp :: PGColType -> InpObjTyInfo
166177
mkCompExpInp colTy =
@@ -169,6 +180,7 @@ mkCompExpInp colTy =
169180
, map (mk $ G.toLT colScalarTy) listOps
170181
, bool [] (map (mk $ mkScalarTy PGText) stringOps) isStringTy
171182
, bool [] (map jsonbOpToInpVal jsonbOps) isJsonbTy
183+
, bool [] (stDWithinOpInpVal : map geomOpToInpVal geomOps) isGeometryTy
172184
, [InpValInfo Nothing "_is_null" $ G.TypeNamed (G.Nullability True) $ G.NamedType "Boolean"]
173185
]) HasuraType
174186
where
@@ -195,6 +207,7 @@ mkCompExpInp colTy =
195207
[ "_like", "_nlike", "_ilike", "_nilike"
196208
, "_similar", "_nsimilar"
197209
]
210+
198211
isJsonbTy = case colTy of
199212
PGJSONB -> True
200213
_ -> False
@@ -222,6 +235,43 @@ mkCompExpInp colTy =
222235
)
223236
]
224237

238+
-- Geometry related ops
239+
stDWithinOpInpVal =
240+
InpValInfo (Just stDWithinDesc) "_st_d_within" $ G.toGT stDWithinInpTy
241+
stDWithinDesc =
242+
"is the column within a distance from a geometry value"
243+
244+
isGeometryTy = case colTy of
245+
PGGeometry -> True
246+
_ -> False
247+
248+
geomOpToInpVal (op, desc) =
249+
InpValInfo (Just desc) op $ G.toGT $ mkScalarTy PGGeometry
250+
geomOps =
251+
[
252+
( "_st_contains"
253+
, "does the column contain the given geometry value"
254+
)
255+
, ( "_st_crosses"
256+
, "does the column crosses the given geometry value"
257+
)
258+
, ( "_st_equals"
259+
, "is the column equal to given geometry value. Directionality is ignored"
260+
)
261+
, ( "_st_intersects"
262+
, "does the column spatially intersect the given geometry value"
263+
)
264+
, ( "_st_overlaps"
265+
, "does the column 'spatially overlap' (intersect but not completely contain) the given geometry value"
266+
)
267+
, ( "_st_touches"
268+
, "does the column have atleast one point in common with the given geometry value"
269+
)
270+
, ( "_st_within"
271+
, "is the column contained in the given geometry value"
272+
)
273+
]
274+
225275
ordByTy :: G.NamedType
226276
ordByTy = G.NamedType "order_by"
227277

@@ -263,8 +313,6 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
263313
let queryRoot = mkHsraObjTyInfo (Just "query root")
264314
(G.NamedType "query_root") $
265315
mapFromL _fiName (schemaFld:typeFld:qFlds)
266-
colTys = Set.toList $ Set.fromList $ map pgiType $
267-
lefts $ Map.elems fldInfos
268316
scalarTys = map (TIScalar . mkHsraScalarTyInfo) colTys
269317
compTys = map (TIInpObj . mkCompExpInp) colTys
270318
ordByEnumTyM = bool (Just ordByEnumTy) Nothing $ null qFlds
@@ -273,12 +321,15 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
273321
, TIObj <$> mutRootM
274322
, TIObj <$> subRootM
275323
, TIEnum <$> ordByEnumTyM
324+
, TIInpObj <$> stDWithinInpM
276325
] <>
277326
scalarTys <> compTys <> defaultTypes
278327
-- for now subscription root is query root
279328
in GCtx allTys fldInfos ordByEnums queryRoot mutRootM subRootM
280329
(Map.map fst flds) insCtxMap
281330
where
331+
colTys = Set.toList $ Set.fromList $ map pgiType $
332+
lefts $ Map.elems fldInfos
282333
mkMutRoot =
283334
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") .
284335
mapFromL _fiName
@@ -298,5 +349,12 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
298349
$ G.toGT $ G.toNT $ G.NamedType "String"
299350
]
300351

352+
stDWithinInpM = bool Nothing (Just stDWithinInp) (PGGeometry `elem` colTys)
353+
stDWithinInp =
354+
mkHsraInpTyInfo Nothing stDWithinInpTy $ fromInpValL
355+
[ InpValInfo Nothing "from" $ G.toGT $ G.toNT $ mkScalarTy PGGeometry
356+
, InpValInfo Nothing "distance" $ G.toNT $ G.toNT $ mkScalarTy PGFloat
357+
]
358+
301359
emptyGCtx :: GCtx
302360
emptyGCtx = mkGCtx mempty mempty mempty

server/src-lib/Hasura/GraphQL/Explain.hs

+2-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
2727
import qualified Hasura.GraphQL.Validate as GV
2828
import qualified Hasura.GraphQL.Validate.Types as VT
2929
import qualified Hasura.RQL.DML.Select as RS
30-
import qualified Hasura.SQL.DML as S
3130

3231
data GQLExplain
3332
= GQLExplain
@@ -89,8 +88,8 @@ explainField userInfo gCtx fld =
8988
return $ FieldPlan fName (Just selectSQL) $ Just planLines
9089
where
9190
fName = _fName fld
92-
txtConverter (ty, val) =
93-
return $ S.annotateExp (txtEncoder val) ty
91+
txtConverter = return . uncurry toTxtValue
92+
9493
opCtxMap = _gOpCtxMap gCtx
9594
fldMap = _gFields gCtx
9695
orderByCtx = _gOrdByCtx gCtx

server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs

+18
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ parseOpExps annVal = do
5555
"_has_keys_any" -> fmap AHasKeysAny <$> parseMany asPGColText v
5656
"_has_keys_all" -> fmap AHasKeysAll <$> parseMany asPGColText v
5757

58+
-- geometry type related operators
59+
"_st_contains" -> fmap ASTContains <$> asPGColValM v
60+
"_st_crosses" -> fmap ASTCrosses <$> asPGColValM v
61+
"_st_equals" -> fmap ASTEquals <$> asPGColValM v
62+
"_st_intersects" -> fmap ASTIntersects <$> asPGColValM v
63+
"_st_overlaps" -> fmap ASTOverlaps <$> asPGColValM v
64+
"_st_touches" -> fmap ASTTouches <$> asPGColValM v
65+
"_st_within" -> fmap ASTWithin <$> asPGColValM v
66+
"_st_d_within" -> asObjectM v >>= mapM parseAsSTDWithinObj
67+
5868
_ ->
5969
throw500
6070
$ "unexpected operator found in opexp of "
@@ -70,6 +80,14 @@ parseOpExps annVal = do
7080
AGScalar _ _ -> throw500 "boolean value is expected"
7181
_ -> tyMismatch "pgvalue" v
7282

83+
parseAsSTDWithinObj obj = do
84+
distanceVal <- onNothing (OMap.lookup "distance" obj) $
85+
throw500 "expected \"distance\" input field in st_d_within_input ty"
86+
distSQL <- uncurry toTxtValue <$> asPGColVal distanceVal
87+
fromVal <- onNothing (OMap.lookup "from" obj) $
88+
throw500 "expected \"from\" input field in st_d_within_input ty"
89+
ASTDWithin distSQL <$> asPGColVal fromVal
90+
7391
parseAsEqOp
7492
:: (MonadError QErr m)
7593
=> AnnGValue -> m [OpExp]

server/src-lib/Hasura/RQL/GBoolExp.hs

+14
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,16 @@ mkColCompExp qual lhsCol = \case
276276
AHasKey val -> S.BECompare S.SHasKey lhs val
277277
AHasKeysAny keys -> S.BECompare S.SHasKeysAny lhs $ toTextArray keys
278278
AHasKeysAll keys -> S.BECompare S.SHasKeysAll lhs $ toTextArray keys
279+
280+
ASTContains val -> mkGeomOpBe "ST_Contains" val
281+
ASTCrosses val -> mkGeomOpBe "ST_Crosses" val
282+
ASTDWithin r val -> applySQLFn "ST_DWithin" [lhs, val, r]
283+
ASTEquals val -> mkGeomOpBe "ST_Equals" val
284+
ASTIntersects val -> mkGeomOpBe "ST_Intersects" val
285+
ASTOverlaps val -> mkGeomOpBe "ST_Overlaps" val
286+
ASTTouches val -> mkGeomOpBe "ST_Touches" val
287+
ASTWithin val -> mkGeomOpBe "ST_Within" val
288+
279289
ANISNULL -> S.BENull lhs
280290
ANISNOTNULL -> S.BENotNull lhs
281291
CEQ rhsCol -> S.BECompare S.SEQ lhs $ mkQCol rhsCol
@@ -291,6 +301,10 @@ mkColCompExp qual lhsCol = \case
291301
toTextArray arr =
292302
S.SETyAnn (S.SEArray $ map (txtEncoder . PGValText) arr) S.textArrType
293303

304+
mkGeomOpBe fn v = applySQLFn fn [lhs, v]
305+
306+
applySQLFn f exps = S.BEExp $ S.SEFnApp f exps Nothing
307+
294308
handleEmptyIn [] = S.BELit False
295309
handleEmptyIn vals = S.BEIN lhs vals
296310

0 commit comments

Comments
 (0)