add PostGIS operators in boolean expressions (closes #1051) (#1372)

This commit is contained in:
Rakesh Emmadi 2019-01-17 11:51:38 +05:30 committed by Vamshi Surabhi
parent e375c61e4a
commit 7ff1c8829a
16 changed files with 663 additions and 9 deletions

View File

@ -361,6 +361,42 @@ Checking for ``null`` values :
- ``_is_null`` (takes true/false as values)
PostGIS related operators on GEOMETRY columns :
.. list-table::
:header-rows: 1
* - Operator
- PostGIS equivalent
* - ``_st_contains``
- ``ST_Contains``
* - ``_st_crosses``
- ``ST_Crosses``
* - ``_st_equals``
- ``ST_Equals``
* - ``_st_intersects``
- ``ST_Intersects``
* - ``_st_overlaps``
- ``ST_Overlaps``
* - ``_st_touches``
- ``ST_Touches``
* - ``_st_within``
- ``ST_Within``
* - ``_st_d_within``
- ``ST_DWithin``
(For more details on what these operators do, refer to `PostGIS docs <http://postgis.net/workshops/postgis-intro/spatial_relationships.html>`__.)
.. Note::
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
2. Input value for ``_st_d_within`` operator is an object:-
.. parsed-literal::
{
field-name : {_st_d_within: {distance: Float, from: Value} }
}
.. _OrderByExp:

View File

@ -473,6 +473,136 @@ Fetch a list of authors whose names begin with A or C (``similar`` is case-sensi
}
}
PostGIS topology operators
--------------------------
The ``_st_contains``, ``_st_crosses``, ``_st_equals``, ``_st_intersects``, ``_st_overlaps``,
``_st_touches``, ``_st_within`` and ``_st_d_within`` operators are used to filter ``geometry`` like columns.
For more details on what these operators do, refer to `PostGIS docs <http://postgis.net/workshops/postgis-intro/spatial_relationships.html>`__.
Use ``json`` (`GeoJSON <https://tools.ietf.org/html/rfc7946>`_) representation of ``geometry`` values in ``variables`` as shown in the following examples
Example: _st_within
^^^^^^^^^^^^^^^^^^^^^
Fetch a list of geometry values which are within the given ``polygon`` value
.. graphiql::
:view_only:
:query:
query geom_table($polygon: geometry){
geom_table(where: {geom_col: {_st_within: $polygon}}){
id
geom_col
}
}
:response:
{
"data": {
"geom_table": [
{
"id": 1,
"geom_col": {
"type": "Point",
"coordinates": [
1,
2
]
}
}
]
}
}
Variables for above query:-
.. code-block:: json
{
"polygon": {
"type": "Polygon",
"coordinates": [
[
[
0,
0
],
[
0,
2
],
[
2,
2
],
[
2,
0
],
[
0,
0
]
]
]
}
}
Example: _st_d_within
^^^^^^^^^^^^^^^^^^^^^
Fetch a list of geometry values which are 3 units from given ``point`` value
.. graphiql::
:view_only:
:query:
query geom_table($point: geometry){
geom_table(where: {geom_col: {_st_d_within: {distance: 3, from: $point}}}){
id
geom_col
}
}
:response:
{
"data": {
"geom_table": [
{
"id": 1,
"geom_col": {
"type": "Point",
"coordinates": [
1,
2
]
}
},
{
"id": 2,
"geom_col": {
"type": "Point",
"coordinates": [
3,
0
]
}
}
]
}
}
Variables for above query:-
.. code-block:: json
{
"point": {
"type": "Point",
"coordinates": [
0,
0
]
}
}
Filter or check for null values
-------------------------------
Checking for null values can be achieved using the ``_is_null`` operator.

View File

@ -161,6 +161,17 @@ mkCompExpTy :: PGColType -> G.NamedType
mkCompExpTy =
G.NamedType . mkCompExpName
{-
input st_d_within_input {
distance: Float!
from: geometry!
}
-}
stDWithinInpTy :: G.NamedType
stDWithinInpTy = G.NamedType "st_d_within_input"
--- | make compare expression input type
mkCompExpInp :: PGColType -> InpObjTyInfo
mkCompExpInp colTy =
@ -169,6 +180,7 @@ mkCompExpInp colTy =
, map (mk $ G.toLT colScalarTy) listOps
, bool [] (map (mk $ mkScalarTy PGText) stringOps) isStringTy
, bool [] (map jsonbOpToInpVal jsonbOps) isJsonbTy
, bool [] (stDWithinOpInpVal : map geomOpToInpVal geomOps) isGeometryTy
, [InpValInfo Nothing "_is_null" $ G.TypeNamed (G.Nullability True) $ G.NamedType "Boolean"]
]) HasuraType
where
@ -195,6 +207,7 @@ mkCompExpInp colTy =
[ "_like", "_nlike", "_ilike", "_nilike"
, "_similar", "_nsimilar"
]
isJsonbTy = case colTy of
PGJSONB -> True
_ -> False
@ -222,6 +235,43 @@ mkCompExpInp colTy =
)
]
-- Geometry related ops
stDWithinOpInpVal =
InpValInfo (Just stDWithinDesc) "_st_d_within" $ G.toGT stDWithinInpTy
stDWithinDesc =
"is the column within a distance from a geometry value"
isGeometryTy = case colTy of
PGGeometry -> True
_ -> False
geomOpToInpVal (op, desc) =
InpValInfo (Just desc) op $ G.toGT $ mkScalarTy PGGeometry
geomOps =
[
( "_st_contains"
, "does the column contain the given geometry value"
)
, ( "_st_crosses"
, "does the column crosses the given geometry value"
)
, ( "_st_equals"
, "is the column equal to given geometry value. Directionality is ignored"
)
, ( "_st_intersects"
, "does the column spatially intersect the given geometry value"
)
, ( "_st_overlaps"
, "does the column 'spatially overlap' (intersect but not completely contain) the given geometry value"
)
, ( "_st_touches"
, "does the column have atleast one point in common with the given geometry value"
)
, ( "_st_within"
, "is the column contained in the given geometry value"
)
]
ordByTy :: G.NamedType
ordByTy = G.NamedType "order_by"
@ -263,8 +313,6 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
let queryRoot = mkHsraObjTyInfo (Just "query root")
(G.NamedType "query_root") $
mapFromL _fiName (schemaFld:typeFld:qFlds)
colTys = Set.toList $ Set.fromList $ map pgiType $
lefts $ Map.elems fldInfos
scalarTys = map (TIScalar . mkHsraScalarTyInfo) colTys
compTys = map (TIInpObj . mkCompExpInp) colTys
ordByEnumTyM = bool (Just ordByEnumTy) Nothing $ null qFlds
@ -273,12 +321,15 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
, TIObj <$> mutRootM
, TIObj <$> subRootM
, TIEnum <$> ordByEnumTyM
, TIInpObj <$> stDWithinInpM
] <>
scalarTys <> compTys <> defaultTypes
-- for now subscription root is query root
in GCtx allTys fldInfos ordByEnums queryRoot mutRootM subRootM
(Map.map fst flds) insCtxMap
where
colTys = Set.toList $ Set.fromList $ map pgiType $
lefts $ Map.elems fldInfos
mkMutRoot =
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") .
mapFromL _fiName
@ -298,5 +349,12 @@ mkGCtx (TyAgg tyInfos fldInfos ordByEnums) (RootFlds flds) insCtxMap =
$ G.toGT $ G.toNT $ G.NamedType "String"
]
stDWithinInpM = bool Nothing (Just stDWithinInp) (PGGeometry `elem` colTys)
stDWithinInp =
mkHsraInpTyInfo Nothing stDWithinInpTy $ fromInpValL
[ InpValInfo Nothing "from" $ G.toGT $ G.toNT $ mkScalarTy PGGeometry
, InpValInfo Nothing "distance" $ G.toNT $ G.toNT $ mkScalarTy PGFloat
]
emptyGCtx :: GCtx
emptyGCtx = mkGCtx mempty mempty mempty

View File

@ -27,7 +27,6 @@ import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
import qualified Hasura.GraphQL.Validate as GV
import qualified Hasura.GraphQL.Validate.Types as VT
import qualified Hasura.RQL.DML.Select as RS
import qualified Hasura.SQL.DML as S
data GQLExplain
= GQLExplain
@ -89,8 +88,8 @@ explainField userInfo gCtx fld =
return $ FieldPlan fName (Just selectSQL) $ Just planLines
where
fName = _fName fld
txtConverter (ty, val) =
return $ S.annotateExp (txtEncoder val) ty
txtConverter = return . uncurry toTxtValue
opCtxMap = _gOpCtxMap gCtx
fldMap = _gFields gCtx
orderByCtx = _gOrdByCtx gCtx

View File

@ -55,6 +55,16 @@ parseOpExps annVal = do
"_has_keys_any" -> fmap AHasKeysAny <$> parseMany asPGColText v
"_has_keys_all" -> fmap AHasKeysAll <$> parseMany asPGColText v
-- geometry type related operators
"_st_contains" -> fmap ASTContains <$> asPGColValM v
"_st_crosses" -> fmap ASTCrosses <$> asPGColValM v
"_st_equals" -> fmap ASTEquals <$> asPGColValM v
"_st_intersects" -> fmap ASTIntersects <$> asPGColValM v
"_st_overlaps" -> fmap ASTOverlaps <$> asPGColValM v
"_st_touches" -> fmap ASTTouches <$> asPGColValM v
"_st_within" -> fmap ASTWithin <$> asPGColValM v
"_st_d_within" -> asObjectM v >>= mapM parseAsSTDWithinObj
_ ->
throw500
$ "unexpected operator found in opexp of "
@ -70,6 +80,14 @@ parseOpExps annVal = do
AGScalar _ _ -> throw500 "boolean value is expected"
_ -> tyMismatch "pgvalue" v
parseAsSTDWithinObj obj = do
distanceVal <- onNothing (OMap.lookup "distance" obj) $
throw500 "expected \"distance\" input field in st_d_within_input ty"
distSQL <- uncurry toTxtValue <$> asPGColVal distanceVal
fromVal <- onNothing (OMap.lookup "from" obj) $
throw500 "expected \"from\" input field in st_d_within_input ty"
ASTDWithin distSQL <$> asPGColVal fromVal
parseAsEqOp
:: (MonadError QErr m)
=> AnnGValue -> m [OpExp]

View File

@ -276,6 +276,16 @@ mkColCompExp qual lhsCol = \case
AHasKey val -> S.BECompare S.SHasKey lhs val
AHasKeysAny keys -> S.BECompare S.SHasKeysAny lhs $ toTextArray keys
AHasKeysAll keys -> S.BECompare S.SHasKeysAll lhs $ toTextArray keys
ASTContains val -> mkGeomOpBe "ST_Contains" val
ASTCrosses val -> mkGeomOpBe "ST_Crosses" val
ASTDWithin r val -> applySQLFn "ST_DWithin" [lhs, val, r]
ASTEquals val -> mkGeomOpBe "ST_Equals" val
ASTIntersects val -> mkGeomOpBe "ST_Intersects" val
ASTOverlaps val -> mkGeomOpBe "ST_Overlaps" val
ASTTouches val -> mkGeomOpBe "ST_Touches" val
ASTWithin val -> mkGeomOpBe "ST_Within" val
ANISNULL -> S.BENull lhs
ANISNOTNULL -> S.BENotNull lhs
CEQ rhsCol -> S.BECompare S.SEQ lhs $ mkQCol rhsCol
@ -291,6 +301,10 @@ mkColCompExp qual lhsCol = \case
toTextArray arr =
S.SETyAnn (S.SEArray $ map (txtEncoder . PGValText) arr) S.textArrType
mkGeomOpBe fn v = applySQLFn fn [lhs, v]
applySQLFn f exps = S.BEExp $ S.SEFnApp f exps Nothing
handleEmptyIn [] = S.BELit False
handleEmptyIn vals = S.BEIN lhs vals

View File

@ -117,6 +117,15 @@ data OpExpG a
| AHasKeysAny [Text]
| AHasKeysAll [Text]
| ASTContains !a
| ASTCrosses !a
| ASTDWithin !S.SQLExp !a
| ASTEquals !a
| ASTIntersects !a
| ASTOverlaps !a
| ASTTouches !a
| ASTWithin !a
| ANISNULL -- IS NULL
| ANISNOTNULL -- IS NOT NULL
@ -157,6 +166,15 @@ opExpToJPair f = \case
AHasKeysAny a -> ("_has_keys_any", toJSON a)
AHasKeysAll a -> ("_has_keys_all", toJSON a)
ASTContains a -> ("_st_contains", f a)
ASTCrosses a -> ("_st_crosses", f a)
ASTDWithin _ a -> ("_st_d_within", f a)
ASTEquals a -> ("_st_equals", f a)
ASTIntersects a -> ("_st_intersects", f a)
ASTOverlaps a -> ("_st_overlaps", f a)
ASTTouches a -> ("_st_touches", f a)
ASTWithin a -> ("_st_within", f a)
ANISNULL -> ("_is_null", toJSON True)
ANISNOTNULL -> ("_is_null", toJSON False)

View File

@ -460,6 +460,7 @@ data BoolExp
| BENotNull !SQLExp
| BEExists !Select
| BEIN !SQLExp ![SQLExp]
| BEExp !SQLExp
deriving (Show, Eq)
-- removes extraneous 'AND true's
@ -507,6 +508,8 @@ instance ToSQL BoolExp where
-- special case to handle lhs IN (exp1, exp2)
toSQL (BEIN vl exps) =
paren (toSQL vl) <-> toSQL SIN <-> paren (", " <+> exps)
-- Any SQL expression which evaluates to bool value
toSQL (BEExp e) = paren $ toSQL e
data BinOp = AndOp
| OrOp

View File

@ -120,6 +120,7 @@ uBoolExp = restoringIdens . \case
S.BENotNull e -> S.BENotNull <$> uSqlExp e
S.BEExists sel -> S.BEExists <$> uSelect sel
S.BEIN left exps -> S.BEIN <$> uSqlExp left <*> mapM uSqlExp exps
S.BEExp e -> S.BEExp <$> uSqlExp e
uOrderBy :: S.OrderByExp -> Uniq S.OrderByExp
uOrderBy (S.OrderByExp ordByItems) =

View File

@ -188,11 +188,29 @@ iresToEither (ISuccess a) = return a
pgValFromJVal :: (FromJSON a) => Value -> Either String a
pgValFromJVal = iresToEither . ifromJSON
applyGeomFromGeoJson :: S.SQLExp -> S.SQLExp
applyGeomFromGeoJson v =
S.SEFnApp "ST_GeomFromGeoJSON" [v] Nothing
isGeoTy :: PGColType -> Bool
isGeoTy = \case
PGGeometry -> True
PGGeography -> True
_ -> False
toPrepParam :: Int -> PGColType -> S.SQLExp
toPrepParam i pct =
if pct == PGGeometry || pct == PGGeography
then S.SEFnApp "ST_GeomFromGeoJSON" [S.SEPrep i] Nothing
else S.SEPrep i
toPrepParam i =
bool prepVal (applyGeomFromGeoJson prepVal) . isGeoTy
where
prepVal = S.SEPrep i
toTxtValue :: PGColType -> PGColValue -> S.SQLExp
toTxtValue ty val =
S.annotateExp txtVal ty
where
txtVal = withGeoVal $ txtEncoder val
withGeoVal v =
bool v (applyGeomFromGeoJson v) $ isGeoTy ty
pgColValueToInt :: PGColValue -> Maybe Int
pgColValueToInt (PGValInteger i) = Just $ fromIntegral i

View File

@ -0,0 +1,99 @@
description: Query data from geom_table using postgis ops in bool exp and line value as input argument
url: /v1alpha1/graphql
status: 200
response:
data:
st_crosses: []
st_touches:
- id: 1
type: point
geom_col:
type: Point
coordinates:
- 1
- 2
- id: 2
type: linestring
geom_col:
type: LineString
coordinates:
- - 0
- 0
- - 0.5
- 1
- - 1
- 2
- - 1.5
- 3
st_disjoint:
- id: 3
type: linestring
geom_col:
type: LineString
coordinates:
- - 1
- 0
- - 0.5
- 0.5
- - 0
- 1
- id: 4
type: polygon
geom_col:
type: Polygon
coordinates:
- - - 0
- 0
- - 0
- 1
- - 1
- 1
- - 1
- 0
- - 0
- 0
- id: 5
type: polygon
geom_col:
type: Polygon
coordinates:
- - - 2
- 0
- - 2
- 1
- - 3
- 1
- - 3
- 0
- - 2
- 0
query:
variables:
line:
type: LineString
coordinates:
- - -1
- 0
- - 0
- 1.5
- - 1
- 2
query: |
query geom_table($line:geometry){
st_crosses: geom_table(where: {geom_col: {_st_crosses: $line}}){
id
type
geom_col
}
st_touches: geom_table(where: {geom_col: {_st_touches: $line}}){
id
type
geom_col
}
st_disjoint: geom_table(where: {_not: {geom_col: {_st_intersects: $line}}}){
id
type
geom_col
}
}

View File

@ -0,0 +1,55 @@
description: Query data from geom_table using postgis ops in bool exp and point value as input argument
url: /v1alpha1/graphql
status: 200
response:
data:
st_contains:
- id: 1
type: point
geom_col:
type: Point
coordinates:
- 1
- 2
- id: 2
type: linestring
geom_col:
type: LineString
coordinates:
- - 0
- 0
- - 0.5
- 1
- - 1
- 2
- - 1.5
- 3
st_equals:
- id: 1
type: point
geom_col:
type: Point
coordinates:
- 1
- 2
query:
variables:
point:
type: Point
coordinates:
- 1
- 2
query: |
query geom_table($point: geometry){
st_contains: geom_table(where: {geom_col: {_st_contains: $point}}){
id
type
geom_col
}
st_equals: geom_table(where: {geom_col: {_st_equals: $point}}){
id
type
geom_col
}
}

View File

@ -0,0 +1,146 @@
description: Query data from geom_table using postgis ops in bool exp and polygon value as input argument
url: /v1alpha1/graphql
status: 200
response:
data:
st_d_within_2:
- id: 1
geom_col:
type: Point
coordinates:
- 1
- 2
- id: 2
geom_col:
type: LineString
coordinates:
- - 0
- 0
- - 0.5
- 1
- - 1
- 2
- - 1.5
- 3
- id: 3
geom_col:
type: LineString
coordinates:
- - 1
- 0
- - 0.5
- 0.5
- - 0
- 1
- id: 4
geom_col:
type: Polygon
coordinates:
- - - 0
- 0
- - 0
- 1
- - 1
- 1
- - 1
- 0
- - 0
- 0
- id: 5
geom_col:
type: Polygon
coordinates:
- - - 2
- 0
- - 2
- 1
- - 3
- 1
- - 3
- 0
- - 2
- 0
st_d_within_3:
- id: 1
geom_col:
type: Point
coordinates:
- 1
- 2
- id: 2
geom_col:
type: LineString
coordinates:
- - 0
- 0
- - 0.5
- 1
- - 1
- 2
- - 1.5
- 3
- id: 3
geom_col:
type: LineString
coordinates:
- - 1
- 0
- - 0.5
- 0.5
- - 0
- 1
- id: 4
geom_col:
type: Polygon
coordinates:
- - - 0
- 0
- - 0
- 1
- - 1
- 1
- - 1
- 0
- - 0
- 0
- id: 5
geom_col:
type: Polygon
coordinates:
- - - 2
- 0
- - 2
- 1
- - 3
- 1
- - 3
- 0
- - 2
- 0
query:
variables:
polygon:
type: Polygon
coordinates:
- - - 2
- 0
- - 2
- 1
- - 3
- 1
- - 3
- 0
- - 2
- 0
query: |
query geom_table($polygon: geometry){
st_d_within_2: geom_table(where: {geom_col: {_st_d_within: {distance: 2 from: $polygon}}}){
id
geom_col
}
st_d_within_3: geom_table(where: {geom_col: {_st_d_within: {distance: 3 from: $polygon}}}){
id
geom_col
}
}

View File

@ -0,0 +1,39 @@
type: bulk
args:
- type: run_sql
args:
sql: |
CREATE EXTENSION IF NOT EXISTS postgis;
- type: run_sql
args:
sql: |
CREATE EXTENSION IF NOT EXISTS postgis_topology;
#Create table
- type: run_sql
args:
sql: |
CREATE TABLE geom_table(
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
geom_col geometry NOT NULL
);
- type: track_table
args:
name: geom_table
schema: public
#Insert data
- type: run_sql
args:
sql: |
INSERT INTO geom_table (type, geom_col)
VALUES
('point', ST_GeomFromText('POINT(1 2)')),
('linestring', ST_GeomFromText('LINESTRING(0 0, 0.5 1, 1 2, 1.5 3)')),
('linestring', ST_GeomFromText('LINESTRING(1 0, 0.5 0.5, 0 1)')),
('polygon', ST_GeomFromText('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')),
('polygon', ST_GeomFromText('POLYGON((2 0, 2 1, 3 1, 3 0, 2 0))'))
;

View File

@ -0,0 +1,6 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DROP TABLE geom_table;

View File

@ -263,6 +263,20 @@ class TestGraphQLQueryBoolExpJsonB(DefaultTestSelectQueries):
def dir(cls):
return 'queries/graphql_query/boolexp/jsonb'
class TestGraphQLQueryBoolExpPostGIS(DefaultTestSelectQueries):
def test_query_using_point(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/query_using_point.yaml')
def test_query_using_line(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/query_using_line.yaml')
def test_query_using_polygon(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/query_using_polygon.yaml')
@classmethod
def dir(cls):
return 'queries/graphql_query/boolexp/postgis'
class TestGraphQLQueryOrderBy(DefaultTestSelectQueries):
def test_articles_order_by_without_id(self, hge_ctx):