From f7c99689da8073dd16ea72b4937b039cccd44429 Mon Sep 17 00:00:00 2001 From: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com> Date: Thu, 29 Aug 2019 18:37:05 +0530 Subject: [PATCH] support intersect filters on raster columns (close #2613) (#2704) * initial raster support * _st_intersects_geom -> _st_intersects_geom_nband * add tests * update docs * improve docs As requested by @marionschleifer * new type for raster values Suggested by @lexi-lambda * replace `SEUnsafe "NULL"` with SENull --- .../api-reference/graphql-api/query.rst | 34 ++++ docs/graphql/manual/queries/query-filters.rst | 154 ++++++++++++++++++ server/graphql-engine.cabal | 1 + .../src-lib/Hasura/GraphQL/Execute/Query.hs | 4 +- server/src-lib/Hasura/GraphQL/Explain.hs | 2 +- .../src-lib/Hasura/GraphQL/Resolve/BoolExp.hs | 26 ++- .../Hasura/GraphQL/Resolve/Mutation.hs | 2 +- server/src-lib/Hasura/GraphQL/Schema.hs | 14 +- .../src-lib/Hasura/GraphQL/Schema/BoolExp.hs | 65 +++++++- server/src-lib/Hasura/RQL/DDL/EventTrigger.hs | 8 +- server/src-lib/Hasura/RQL/DML/Internal.hs | 2 +- server/src-lib/Hasura/RQL/GBoolExp.hs | 11 ++ server/src-lib/Hasura/RQL/Types/BoolExp.hs | 24 +++ server/src-lib/Hasura/SQL/DML.hs | 2 +- server/src-lib/Hasura/SQL/Types.hs | 6 + server/src-lib/Hasura/SQL/Value.hs | 36 +++- .../query_st_intersects_geom_nband.yaml | 30 ++++ ...uery_st_intersects_geom_nband_no_rows.yaml | 26 +++ .../raster/query_st_intersects_rast.yaml | 21 +++ .../raster/query_st_intersects_rast_fail.yaml | 20 +++ .../query_st_intersects_rast_no_rows.yaml | 17 ++ .../graphql_query/boolexp/raster/setup.yaml | 22 +++ .../boolexp/raster/teardown.yaml | 7 + server/tests-py/test_graphql_queries.py | 22 +++ 24 files changed, 533 insertions(+), 23 deletions(-) create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband_no_rows.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_fail.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_no_rows.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/setup.yaml create mode 100644 server/tests-py/queries/graphql_query/boolexp/raster/teardown.yaml diff --git a/docs/graphql/manual/api-reference/graphql-api/query.rst b/docs/graphql/manual/api-reference/graphql-api/query.rst index 5143da67..0a5c47f6 100644 --- a/docs/graphql/manual/api-reference/graphql-api/query.rst +++ b/docs/graphql/manual/api-reference/graphql-api/query.rst @@ -432,6 +432,40 @@ Operator field-name : {_st_d_within: {distance: Float, from: Value} } } +**Intersect Operators on RASTER columns:** + +- ``_st_intersects_rast`` + +Executes ``boolean ST_Intersects( raster , raster )`` + +.. parsed-literal :: + + { _st_intersects_rast: raster } + + +- ``_st_intersects_nband_geom`` + +Executes ``boolean ST_Intersects( raster , integer nband , geometry geommin )`` + +This accepts ``st_intersects_nband_geom_input`` input object + +.. parsed-literal :: + + { _st_intersects_nband_geom: {nband: Integer! geommin: geometry!} + + + +- ``_st_intersects_geom_nband`` + +Executes ``boolean ST_Intersects( raster , geometry geommin , integer nband = NULL )`` + +This accepts ``st_intersects_geom_nband_input`` input object + +.. parsed-literal :: + + { _st_intersects_geom_nband: {geommin: geometry! nband: Integer } + + .. _CastExp: CastExp diff --git a/docs/graphql/manual/queries/query-filters.rst b/docs/graphql/manual/queries/query-filters.rst index 2a99c737..dfb7ee4c 100644 --- a/docs/graphql/manual/queries/query-filters.rst +++ b/docs/graphql/manual/queries/query-filters.rst @@ -1508,3 +1508,157 @@ Columns of type ``geography`` are more accurate, but they don’t support as man .. code-block:: sql CREATE INDEX cities_location_geography ON cities USING GIST ((location::geography)); + +Intersect operators on RASTER columns +------------------------------------- + +Intersect operators on columns with ``raster`` type are supported. +Refer to `Postgis docs `__ to know more about intersect functions on ``raster`` columns. +Please submit a feature request via `github `__ if you want support for more functions. + +Example: _st_intersects_rast +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Filter the raster values which intersect the input raster value. + +Executes the following SQL function: + +.. code-block:: sql + + boolean ST_Intersects( raster , raster ); + + +.. graphiql:: + :view_only: + :query: + query getIntersectingValues ($rast: raster){ + dummy_rast(where: {rast: {_st_intersects_rast: $rast}}){ + rid + rast + } + } + :response: + { + "data": { + "dummy_rast": [ + { + "rid": 1, + "rast": "01000001009A9999999999E93F9A9999999999E9BF000000000000F0BF000000000000104000000000000000000000000000000000E610000005000500440000010101000101010101010101010101010101010001010100" + }, + { + "rid": 2, + "rast": "0100000100166C8E335B91F13FE2385B00285EF6BF360EE40064EBFFBF8D033900D9FA134000000000000000000000000000000000E610000005000500440000000101010001010101010101010101010101000101010000" + } + ] + } + } + :variables: + { + "rast": "0100000100000000000000004000000000000000C00000000000000000000000000000084000000000000000000000000000000000E610000001000100440001" + } + +Example: _st_intersects_geom_nband +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Filter the raster values which intersect the input geometry value and optional band number. + +Executes the following SQL function: + +.. code-block:: sql + + boolean ST_Intersects( raster , geometry geommin , integer nband=NULL ); + + +.. graphiql:: + :view_only: + :query: + query getIntersectingValues ($point: geometry!){ + dummy_rast(where: {rast: {_st_intersects_geom_nband: {geommin: $point}}}){ + rid + rast + } + } + :response: + { + "data": { + "dummy_rast": [ + { + "rid": 1, + "rast": "01000001009A9999999999E93F9A9999999999E9BF000000000000F0BF000000000000104000000000000000000000000000000000E610000005000500440000010101000101010101010101010101010101010001010100" + }, + { + "rid": 2, + "rast": "0100000100166C8E335B91F13FE2385B00285EF6BF360EE40064EBFFBF8D033900D9FA134000000000000000000000000000000000E610000005000500440000000101010001010101010101010101010101000101010000" + } + ] + } + } + :variables: + { + "point": { + "type": "Point", + "coordinates": [ + 1, + 2 + ], + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::4326" + } + } + } + } + +Example: _st_intersects_nband_geom +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Filter the raster values (with specified band number) which intersect the input geometry value. + +Executes the following SQL function: + +.. code-block:: sql + + boolean ST_Intersects( raster , integer nband , geometry geommin ); + + +.. graphiql:: + :view_only: + :query: + query getIntersectingValues ($point: geometry!){ + dummy_rast(where: {rast: {_st_intersects_nband_geom: {nband: 5 geommin: $point}}}){ + rid + rast + } + } + :response: + { + "data": { + "dummy_rast": [ + { + "rid": 1, + "rast": "01000001009A9999999999E93F9A9999999999E9BF000000000000F0BF000000000000104000000000000000000000000000000000E610000005000500440000010101000101010101010101010101010101010001010100" + }, + { + "rid": 2, + "rast": "0100000100166C8E335B91F13FE2385B00285EF6BF360EE40064EBFFBF8D033900D9FA134000000000000000000000000000000000E610000005000500440000000101010001010101010101010101010101000101010000" + } + ] + } + } + :variables: + { + "point": { + "type": "Point", + "coordinates": [ + 1, + 2 + ], + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::4326" + } + } + } + } diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index ae471383..143c5c3c 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -94,6 +94,7 @@ library -- String related , case-insensitive , string-conversions + , text-conversions -- Http client , wreq diff --git a/server/src-lib/Hasura/GraphQL/Execute/Query.hs b/server/src-lib/Hasura/GraphQL/Execute/Query.hs index b3411e34..646f6e08 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/Query.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/Query.hs @@ -192,13 +192,15 @@ prepareWithPlan = \case _ -> getNextArgNum addPrepArg argNum $ toBinaryValue colVal return $ toPrepParam argNum (pstType colVal) + R.UVSessVar ty sessVar -> do let sessVarVal = S.SEOpApp (S.SQLOp "->>") [S.SEPrep 1, S.SELit $ T.toLower sessVar] return $ flip S.SETyAnn (S.mkTypeAnn ty) $ case ty of - PGTypeScalar colTy -> withGeoVal colTy sessVarVal + PGTypeScalar colTy -> withConstructorFn colTy sessVarVal PGTypeArray _ -> sessVarVal + R.UVSQL sqlExp -> return sqlExp queryRootName :: Text diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index 277b5ea4..3a937025 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -61,7 +61,7 @@ resolveVal userInfo = \case RS.UVSessVar ty sessVar -> do sessVarVal <- S.SELit <$> getSessVarVal userInfo sessVar return $ flip S.SETyAnn (S.mkTypeAnn ty) $ case ty of - PGTypeScalar colTy -> withGeoVal colTy sessVarVal + PGTypeScalar colTy -> withConstructorFn colTy sessVarVal PGTypeArray _ -> sessVarVal RS.UVSQL sqlExp -> return sqlExp diff --git a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs index bff7b794..3930d323 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs @@ -67,7 +67,12 @@ parseOpExps colTy annVal = do "_st_overlaps" -> fmap ASTOverlaps <$> asOpRhs v "_st_touches" -> fmap ASTTouches <$> asOpRhs v "_st_within" -> fmap ASTWithin <$> asOpRhs v - "_st_d_within" -> asObjectM v >>= mapM parseAsSTDWithinObj + "_st_d_within" -> parseAsObjectM v parseAsSTDWithinObj + + -- raster type related operators + "_st_intersects_rast" -> fmap ASTIntersectsRast <$> asOpRhs v + "_st_intersects_nband_geom" -> parseAsObjectM v parseAsSTIntersectsNbandGeomObj + "_st_intersects_geom_nband" -> parseAsObjectM v parseAsSTIntersectsGeomNbandObj _ -> throw500 @@ -79,6 +84,8 @@ parseOpExps colTy annVal = do where asOpRhs = fmap (fmap UVPG) . asPGColumnValueM + parseAsObjectM v f = asObjectM v >>= mapM f + asPGArray rhsTy v = do valsM <- parseMany asPGColumnValue v forM valsM $ \vals -> do @@ -115,6 +122,23 @@ parseOpExps colTy annVal = do return $ ASTDWithinGeom $ DWithinGeomOp dist from _ -> throw500 "expected PGGeometry/PGGeography column for st_d_within" + parseAsSTIntersectsNbandGeomObj obj = do + nbandVal <- onNothing (OMap.lookup "nband" obj) $ + throw500 "expected \"nband\" input field" + nband <- UVPG <$> asPGColumnValue nbandVal + geommin <- parseGeommin obj + return $ ASTIntersectsNbandGeom $ STIntersectsNbandGeommin nband geommin + + parseAsSTIntersectsGeomNbandObj obj = do + nbandMM <- (fmap . fmap) UVPG <$> mapM asPGColumnValueM (OMap.lookup "nband" obj) + geommin <- parseGeommin obj + return $ ASTIntersectsGeomNband $ STIntersectsGeomminNband geommin $ join nbandMM + + parseGeommin obj = do + geomminVal <- onNothing (OMap.lookup "geommin" obj) $ + throw500 "expected \"geommin\" input field" + UVPG <$> asPGColumnValue geomminVal + parseCastExpression :: (MonadError QErr m) => AnnInpVal -> m (Maybe (CastExp UnresolvedVal)) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index 74530513..d0b12f6e 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -60,7 +60,7 @@ convertRowObj val = flip withObject val $ \_ obj -> forM (OMap.toList obj) $ \(k, v) -> do prepExpM <- fmap UVPG <$> asPGColumnValueM v - let prepExp = fromMaybe (UVSQL $ S.SEUnsafe "NULL") prepExpM + let prepExp = fromMaybe (UVSQL S.SENull) prepExpM return (PGCol $ G.unName k, prepExp) type ApplySQLOp = (PGCol, S.SQLExp) -> S.SQLExp diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index 426a1a3d..ca23bb21 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -661,6 +661,7 @@ mkGCtx tyAgg (RootFields queryFields mutationFields) insCtxMap = , TIEnum <$> ordByEnumTyM ] <> scalarTys <> compTys <> defaultTypes <> wiredInGeoInputTypes + <> wiredInRastInputTypes -- for now subscription root is query root in GCtx allTys fldInfos queryRoot mutRootM subRootM ordByEnums (Map.map fst queryFields) (Map.map fst mutationFields) insCtxMap @@ -687,6 +688,17 @@ mkGCtx tyAgg (RootFields queryFields mutationFields) insCtxMap = -- operations even if just one of the two appears in the schema then Set.union (Set.fromList [PGColumnScalar PGGeometry, PGColumnScalar PGGeography]) colTys else colTys - allScalarTypes = (allComparableTypes ^.. folded._PGColumnScalar) <> scalars + + additionalScalars = + Set.fromList + -- raster comparison expression needs geometry input + (guard anyRasterTypes *> pure PGGeometry) + + allScalarTypes = (allComparableTypes ^.. folded._PGColumnScalar) + <> additionalScalars <> scalars wiredInGeoInputTypes = guard anyGeoTypes *> map TIInpObj geoInputTypes + + anyRasterTypes = any (isScalarColumnWhere (== PGRaster)) colTys + wiredInRastInputTypes = guard anyRasterTypes *> + map TIInpObj rasterIntersectsInputTypes diff --git a/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs index 1e15d270..f9ab2a93 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/BoolExp.hs @@ -1,5 +1,6 @@ module Hasura.GraphQL.Schema.BoolExp ( geoInputTypes + , rasterIntersectsInputTypes , mkCompExpInp , mkBoolExpTy @@ -67,16 +68,18 @@ mkCastExpressionInputType sourceType targetTypes = mkCompExpInp :: PGColumnType -> InpObjTyInfo mkCompExpInp colTy = InpObjTyInfo (Just tyDesc) (mkCompExpTy colTy) (fromInpValL $ concat - [ map (mk colGqlType) typedOps + [ map (mk colGqlType) eqOps + , guard (isScalarWhere (/= PGRaster)) *> map (mk colGqlType) compOps , map (mk $ G.toLT $ G.toNT colGqlType) listOps , guard (isScalarWhere isStringType) *> map (mk $ mkScalarTy PGText) stringOps - , guard (isScalarWhere (== PGJSONB)) *> map jsonbOpToInpVal jsonbOps + , guard (isScalarWhere (== PGJSONB)) *> map opToInpVal jsonbOps , guard (isScalarWhere (== PGGeometry)) *> (stDWithinGeoOpInpVal stDWithinGeometryInpTy : map geoOpToInpVal (geoOps ++ geomOps)) , guard (isScalarWhere (== PGGeography)) *> (stDWithinGeoOpInpVal stDWithinGeographyInpTy : map geoOpToInpVal geoOps) , [InpValInfo Nothing "_is_null" Nothing $ G.TypeNamed (G.Nullability True) $ G.NamedType "Boolean"] , castOpInputValues + , guard (isScalarWhere (== PGRaster)) *> map opToInpVal rasterOps ]) TLHasuraType where colGqlType = mkColumnType colTy @@ -87,8 +90,12 @@ mkCompExpInp colTy = isScalarWhere = flip isScalarColumnWhere colTy mk t n = InpValInfo Nothing n Nothing $ G.toGT t - typedOps = - ["_eq", "_neq", "_gt", "_lt", "_gte", "_lte"] + -- colScalarListTy = GA.GTList colGTy + eqOps = + ["_eq", "_neq"] + compOps = + ["_gt", "_lt", "_gte", "_lte"] + listOps = [ "_in", "_nin" ] -- TODO @@ -99,7 +106,8 @@ mkCompExpInp colTy = , "_similar", "_nsimilar" ] - jsonbOpToInpVal (opName, ty, desc) = InpValInfo (Just desc) opName Nothing ty + opToInpVal (opName, ty, desc) = InpValInfo (Just desc) opName Nothing ty + jsonbOps = [ ( "_contains" , G.toGT $ mkScalarTy PGJSONB @@ -168,6 +176,25 @@ mkCompExpInp colTy = ) ] + -- raster related operators + rasterOps = + [ + ( "_st_intersects_rast" + , G.toGT $ mkScalarTy PGRaster + , boolFnMsg <> "ST_Intersects(raster , raster )" + ) + , ( "_st_intersects_nband_geom" + , G.toGT stIntersectsNbandGeomInputTy + , boolFnMsg <> "ST_Intersects(raster , integer nband, geometry geommin)" + ) + , ( "_st_intersects_geom_nband" + , G.toGT stIntersectsGeomNbandInputTy + , boolFnMsg <> "ST_Intersects(raster , geometry geommin, integer nband=NULL)" + ) + ] + + boolFnMsg = "evaluates the following boolean Postgres function; " + geoInputTypes :: [InpObjTyInfo] geoInputTypes = [ stDWithinGeometryInputType @@ -189,6 +216,34 @@ geoInputTypes = Nothing "use_spheroid" (Just $ G.VCBoolean True) $ G.toGT $ mkScalarTy PGBoolean ] +stIntersectsNbandGeomInputTy :: G.NamedType +stIntersectsNbandGeomInputTy = G.NamedType "st_intersects_nband_geom_input" + +stIntersectsGeomNbandInputTy :: G.NamedType +stIntersectsGeomNbandInputTy = G.NamedType "st_intersects_geom_nband_input" + +rasterIntersectsInputTypes :: [InpObjTyInfo] +rasterIntersectsInputTypes = + [ stIntersectsNbandGeomInput + , stIntersectsGeomNbandInput + ] + where + stIntersectsNbandGeomInput = + mkHsraInpTyInfo Nothing stIntersectsNbandGeomInputTy $ fromInpValL + [ InpValInfo Nothing "nband" Nothing $ + G.toGT $ G.toNT $ mkScalarTy PGInteger + , InpValInfo Nothing "geommin" Nothing $ + G.toGT $ G.toNT $ mkScalarTy PGGeometry + ] + + stIntersectsGeomNbandInput = + mkHsraInpTyInfo Nothing stIntersectsGeomNbandInputTy $ fromInpValL + [ InpValInfo Nothing "geommin" Nothing $ + G.toGT $ G.toNT $ mkScalarTy PGGeometry + , InpValInfo Nothing "nband" Nothing $ + G.toGT $ mkScalarTy PGInteger + ] + mkBoolExpName :: QualifiedTable -> G.Name mkBoolExpName tn = qualObjectToName tn <> "_bool_exp" diff --git a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs index 94afe4fd..2bf54edd 100644 --- a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs +++ b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs @@ -80,16 +80,16 @@ getTriggerSql op trn qt allCols strfyNum spec = ] renderOldDataExp op2 scs = case op2 of - INSERT -> S.SEUnsafe "NULL" + INSERT -> S.SENull UPDATE -> getRowExpression OLD scs DELETE -> getRowExpression OLD scs - MANUAL -> S.SEUnsafe "NULL" + MANUAL -> S.SENull renderNewDataExp op2 scs = case op2 of INSERT -> getRowExpression NEW scs UPDATE -> getRowExpression NEW scs - DELETE -> S.SEUnsafe "NULL" - MANUAL -> S.SEUnsafe "NULL" + DELETE -> S.SENull + MANUAL -> S.SENull getRowExpression opVar scs = case scs of SubCStar -> applyRowToJson $ S.SEUnsafe $ opToTxt opVar diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index cd181471..bd44a01f 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -223,7 +223,7 @@ sessVarFromCurrentSetting' :: PGType PGScalarType -> SessVar -> S.SQLExp sessVarFromCurrentSetting' ty sessVar = flip S.SETyAnn (S.mkTypeAnn ty) $ case ty of - PGTypeScalar baseTy -> withGeoVal baseTy sessVarVal + PGTypeScalar baseTy -> withConstructorFn baseTy sessVarVal PGTypeArray _ -> sessVarVal where curSess = S.SEUnsafe "current_setting('hasura.user')::json" diff --git a/server/src-lib/Hasura/RQL/GBoolExp.hs b/server/src-lib/Hasura/RQL/GBoolExp.hs index d438cea7..af78b339 100644 --- a/server/src-lib/Hasura/RQL/GBoolExp.hs +++ b/server/src-lib/Hasura/RQL/GBoolExp.hs @@ -381,6 +381,13 @@ mkColCompExp qual lhsCol = mkCompExp (mkQCol lhsCol) ASTDWithinGeog (DWithinGeogOp r val sph) -> applySQLFn "ST_DWithin" [lhs, val, r, sph] + ASTIntersectsRast val -> + applySTIntersects [lhs, val] + ASTIntersectsNbandGeom (STIntersectsNbandGeommin nband geommin) -> + applySTIntersects [lhs, nband, geommin] + ASTIntersectsGeomNband (STIntersectsGeomminNband geommin mNband)-> + applySTIntersects [lhs, geommin, withSQLNull mNband] + ANISNULL -> S.BENull lhs ANISNOTNULL -> S.BENotNull lhs CEQ rhsCol -> S.BECompare S.SEQ lhs $ mkQCol rhsCol @@ -394,6 +401,10 @@ mkColCompExp qual lhsCol = mkCompExp (mkQCol lhsCol) applySQLFn f exps = S.BEExp $ S.SEFnApp f exps Nothing + applySTIntersects = applySQLFn "ST_Intersects" + + withSQLNull = fromMaybe S.SENull + mkCastsExp casts = sqlAll . flip map (M.toList casts) $ \(targetType, operations) -> let targetAnn = S.mkTypeAnn $ PGTypeScalar targetType diff --git a/server/src-lib/Hasura/RQL/Types/BoolExp.hs b/server/src-lib/Hasura/RQL/Types/BoolExp.hs index 154ceaf0..c46eb430 100644 --- a/server/src-lib/Hasura/RQL/Types/BoolExp.hs +++ b/server/src-lib/Hasura/RQL/Types/BoolExp.hs @@ -9,6 +9,8 @@ module Hasura.RQL.Types.BoolExp , CastExp , OpExpG(..) , opExpDepCol + , STIntersectsNbandGeommin(..) + , STIntersectsGeomminNband(..) , AnnBoolExpFld(..) , AnnBoolExp @@ -123,6 +125,20 @@ data DWithinGeogOp a = } deriving (Show, Eq, Functor, Foldable, Traversable, Data) $(deriveJSON (aesonDrop 6 snakeCase) ''DWithinGeogOp) +data STIntersectsNbandGeommin a = + STIntersectsNbandGeommin + { singNband :: !a + , singGeommin :: !a + } deriving (Show, Eq, Functor, Foldable, Traversable, Data) +$(deriveJSON (aesonDrop 4 snakeCase) ''STIntersectsNbandGeommin) + +data STIntersectsGeomminNband a = + STIntersectsGeomminNband + { signGeommin :: !a + , signNband :: !(Maybe a) + } deriving (Show, Eq, Functor, Foldable, Traversable, Data) +$(deriveJSON (aesonDrop 4 snakeCase) ''STIntersectsGeomminNband) + type CastExp a = M.HashMap PGScalarType [OpExpG a] data OpExpG a @@ -164,6 +180,10 @@ data OpExpG a | ASTTouches !a | ASTWithin !a + | ASTIntersectsRast !a + | ASTIntersectsGeomNband !(STIntersectsGeomminNband a) + | ASTIntersectsNbandGeom !(STIntersectsNbandGeommin a) + | ANISNULL -- IS NULL | ANISNOTNULL -- IS NOT NULL @@ -225,6 +245,10 @@ opExpToJPair f = \case ASTTouches a -> ("_st_touches", f a) ASTWithin a -> ("_st_within", f a) + ASTIntersectsRast a -> ("_st_intersects_rast", f a) + ASTIntersectsNbandGeom a -> ("_st_intersects_nband_geom", toJSON $ f <$> a) + ASTIntersectsGeomNband a -> ("_st_intersects_geom_nband", toJSON $ f <$> a) + ANISNULL -> ("_is_null", toJSON True) ANISNOTNULL -> ("_is_null", toJSON False) diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 4470f524..6e7f9779 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -312,7 +312,7 @@ instance ToSQL SQLExp where toSQL (SEPrep argNumber) = TB.char '$' <> fromString (show argNumber) toSQL SENull = - TB.text "null" + TB.text "NULL" toSQL (SELit tv) = TB.text $ pgFmtLit tv toSQL (SEUnsafe t) = diff --git a/server/src-lib/Hasura/SQL/Types.hs b/server/src-lib/Hasura/SQL/Types.hs index 05253147..38102f39 100644 --- a/server/src-lib/Hasura/SQL/Types.hs +++ b/server/src-lib/Hasura/SQL/Types.hs @@ -267,6 +267,7 @@ data PGScalarType | PGJSONB | PGGeometry | PGGeography + | PGRaster | PGUnknown !T.Text deriving (Show, Eq, Lift, Generic, Data) @@ -293,6 +294,7 @@ instance ToSQL PGScalarType where PGJSONB -> "jsonb" PGGeometry -> "geometry" PGGeography -> "geography" + PGRaster -> "raster" PGUnknown t -> TB.text t instance ToJSON PGScalarType where @@ -351,6 +353,8 @@ txtToPgColTy t = case t of "geometry" -> PGGeometry "geography" -> PGGeography + + "raster" -> PGRaster _ -> PGUnknown t @@ -379,6 +383,8 @@ pgTypeOid PGJSONB = PTI.jsonb -- we are using the ST_GeomFromGeoJSON($i) instead of $i pgTypeOid PGGeometry = PTI.text pgTypeOid PGGeography = PTI.text +-- we are using the ST_RastFromHexWKB($i) instead of $i +pgTypeOid PGRaster = PTI.text pgTypeOid (PGUnknown _) = PTI.auto isIntegerType :: PGScalarType -> Bool diff --git a/server/src-lib/Hasura/SQL/Value.hs b/server/src-lib/Hasura/SQL/Value.hs index d4b4b54c..794f28d1 100644 --- a/server/src-lib/Hasura/SQL/Value.hs +++ b/server/src-lib/Hasura/SQL/Value.hs @@ -1,7 +1,7 @@ module Hasura.SQL.Value ( PGScalarValue(..) , pgColValueToInt - , withGeoVal + , withConstructorFn , parsePGValue , TxtEncodedPGVal @@ -30,13 +30,27 @@ import Hasura.Prelude import qualified Data.Aeson.Text as AE import qualified Data.Aeson.Types as AT +import qualified Data.ByteString as B import qualified Data.Text as T +import qualified Data.Text.Conversions as TC import qualified Data.Text.Encoding as TE import qualified Data.Text.Lazy as TL import qualified Database.PostgreSQL.LibPQ as PQ import qualified PostgreSQL.Binary.Encoding as PE +newtype RasterWKB + = RasterWKB { getRasterWKB :: TC.Base16 B.ByteString } + deriving (Show, Eq) + +instance FromJSON RasterWKB where + parseJSON = \case + String t -> case TC.fromText t of + Just v -> return $ RasterWKB v + Nothing -> fail + "invalid hexadecimal representation of raster well known binary format" + _ -> fail "expecting String for raster" + -- Binary value. Used in prepared sq data PGScalarValue = PGValInteger !Int32 @@ -56,6 +70,7 @@ data PGScalarValue | PGValJSON !Q.JSON | PGValJSONB !Q.JSONB | PGValGeo !GeometryWithCRS + | PGValRaster !RasterWKB | PGValUnknown !T.Text deriving (Show, Eq) @@ -65,15 +80,17 @@ pgColValueToInt (PGValSmallInt i) = Just $ fromIntegral i pgColValueToInt (PGValBigInt i) = Just $ fromIntegral i pgColValueToInt _ = Nothing -withGeoVal :: PGScalarType -> S.SQLExp -> S.SQLExp -withGeoVal ty v +withConstructorFn :: PGScalarType -> S.SQLExp -> S.SQLExp +withConstructorFn ty v | isGeoType ty = S.SEFnApp "ST_GeomFromGeoJSON" [v] Nothing + | ty == PGRaster = S.SEFnApp "ST_RastFromHexWKB" [v] Nothing | otherwise = v parsePGValue :: PGScalarType -> Value -> AT.Parser PGScalarValue parsePGValue ty val = case (ty, val) of (_ , Null) -> pure $ PGNull ty (PGUnknown _, String t) -> pure $ PGValUnknown t + (PGRaster , _) -> parseTyped -- strictly parse raster value (_ , String t) -> parseTyped <|> pure (PGValUnknown t) (_ , _) -> parseTyped where @@ -97,7 +114,9 @@ parsePGValue ty val = case (ty, val) of PGJSONB -> PGValJSONB . Q.JSONB <$> parseJSON val PGGeometry -> PGValGeo <$> parseJSON val PGGeography -> PGValGeo <$> parseJSON val - PGUnknown tyName -> fail $ "A string is expected for type : " ++ T.unpack tyName + PGRaster -> PGValRaster <$> parseJSON val + PGUnknown tyName -> + fail $ "A string is expected for type : " ++ T.unpack tyName data TxtEncodedPGVal = TENull @@ -136,6 +155,7 @@ txtEncodedPGVal colVal = case colVal of AE.encodeToLazyText j PGValGeo o -> TELit $ TL.toStrict $ AE.encodeToLazyText o + PGValRaster r -> TELit $ TC.toText $ getRasterWKB r PGValUnknown t -> TELit t binEncoder :: PGScalarValue -> Q.PrepArg @@ -157,18 +177,20 @@ binEncoder colVal = case colVal of PGValJSON u -> Q.toPrepVal u PGValJSONB u -> Q.toPrepVal u PGValGeo o -> Q.toPrepVal $ TL.toStrict $ AE.encodeToLazyText o + PGValRaster r -> Q.toPrepVal $ TC.toText $ getRasterWKB r PGValUnknown t -> (PTI.auto, Just (TE.encodeUtf8 t, PQ.Text)) txtEncoder :: PGScalarValue -> S.SQLExp txtEncoder colVal = case txtEncodedPGVal colVal of - TENull -> S.SEUnsafe "NULL" + TENull -> S.SENull TELit t -> S.SELit t toPrepParam :: Int -> PGScalarType -> S.SQLExp -toPrepParam i ty = withGeoVal ty $ S.SEPrep i +toPrepParam i ty = withConstructorFn ty $ S.SEPrep i toBinaryValue :: WithScalarType PGScalarValue -> Q.PrepArg toBinaryValue = binEncoder . pstValue toTxtValue :: WithScalarType PGScalarValue -> S.SQLExp -toTxtValue (WithScalarType ty val) = S.withTyAnn ty . withGeoVal ty $ txtEncoder val +toTxtValue (WithScalarType ty val) = + S.withTyAnn ty . withConstructorFn ty $ txtEncoder val diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband.yaml new file mode 100644 index 00000000..680a1c13 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband.yaml @@ -0,0 +1,30 @@ +description: Fetch raster values which intersects the input geometry +url: /v1/graphql +status: 200 +response: + data: + dummy_rast: + - rid: 1 + rast: 01000001009A9999999999E93F9A9999999999E9BF000000000000F0BF000000000000104000000000000000000000000000000000E610000005000500440000010101000101010101010101010101010101010001010100 + - rid: 2 + rast: 0100000100166C8E335B91F13FE2385B00285EF6BF360EE40064EBFFBF8D033900D9FA134000000000000000000000000000000000E610000005000500440000000101010001010101010101010101010101000101010000 + +query: + variables: + point: + type: Point + coordinates: + - 1 + - 2 + crs: + type: name + properties: + name: urn:ogc:def:crs:EPSG::4326 + + query: | + query ($point: geometry!){ + dummy_rast(where: {rast: {_st_intersects_geom_nband: {geommin: $point}}}){ + rid + rast + } + } diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband_no_rows.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband_no_rows.yaml new file mode 100644 index 00000000..1be2a707 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_geom_nband_no_rows.yaml @@ -0,0 +1,26 @@ +description: Fetch raster values which intersects the input geometry +url: /v1/graphql +status: 200 +response: + data: + dummy_rast: [] + +query: + variables: + point: + type: Point + coordinates: + - 4 + - 4 + crs: + type: name + properties: + name: urn:ogc:def:crs:EPSG::4326 + + query: | + query ($point: geometry!){ + dummy_rast(where: {rast: {_st_intersects_geom_nband: {geommin: $point}}}){ + rid + rast + } + } diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast.yaml new file mode 100644 index 00000000..81d00c0a --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast.yaml @@ -0,0 +1,21 @@ +description: Fetch raster values which intersects the input raster +url: /v1/graphql +status: 200 +response: + data: + dummy_rast: + - rid: 1 + rast: 01000001009A9999999999E93F9A9999999999E9BF000000000000F0BF000000000000104000000000000000000000000000000000E610000005000500440000010101000101010101010101010101010101010001010100 + - rid: 2 + rast: 0100000100166C8E335B91F13FE2385B00285EF6BF360EE40064EBFFBF8D033900D9FA134000000000000000000000000000000000E610000005000500440000000101010001010101010101010101010101000101010000 + +query: + variables: + rast: '0100000100000000000000004000000000000000C00000000000000000000000000000084000000000000000000000000000000000E610000001000100440001' + query: | + query ($rast: raster){ + dummy_rast(where: {rast: {_st_intersects_rast: $rast}}){ + rid + rast + } + } diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_fail.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_fail.yaml new file mode 100644 index 00000000..719df183 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_fail.yaml @@ -0,0 +1,20 @@ +description: Fetch raster values which intersects the input raster +url: /v1/graphql +status: 200 +response: + errors: + - extensions: + path: "$.variableValues.rast" + code: parse-failed + message: invalid hexadecimal representation of raster well known binary format + +query: + variables: + rast: 'this is invalid raster value' + query: | + query ($rast: raster){ + dummy_rast(where: {rast: {_st_intersects_rast: $rast}}){ + rid + rast + } + } diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_no_rows.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_no_rows.yaml new file mode 100644 index 00000000..1b008f18 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/query_st_intersects_rast_no_rows.yaml @@ -0,0 +1,17 @@ +description: Fetch raster values which intersects the input raster +url: /v1/graphql +status: 200 +response: + data: + dummy_rast: [] + +query: + variables: + rast: '0100000100000000000000004000000000000000C00000000000002240000000000000264000000000000000000000000000000000E610000001000100440001' + query: | + query ($rast: raster){ + dummy_rast(where: {rast: {_st_intersects_rast: $rast}}){ + rid + rast + } + } diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/setup.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/setup.yaml new file mode 100644 index 00000000..12930ef5 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/setup.yaml @@ -0,0 +1,22 @@ +type: bulk +args: + +#Create required extensions, tables and insert test data +- type: run_sql + args: + sql: | + CREATE EXTENSION IF NOT EXISTS postgis; + CREATE EXTENSION IF NOT EXISTS postgis_topology; + + CREATE TABLE dummy_rast( + rid serial primary key, + rast raster + ); + INSERT INTO dummy_rast (rast) values + (ST_AsRaster(ST_Buffer(ST_GeomFromText('SRID=4326;POINT(1 2)'),2), 5, 5)) + , (ST_AsRaster(ST_Buffer(ST_GeomFromText('SRID=4326;LINESTRING(0 0, 0.5 1, 1 2, 1.5 3)'), 2), 5, 5)) + ; +- type: track_table + args: + name: dummy_rast + schema: public diff --git a/server/tests-py/queries/graphql_query/boolexp/raster/teardown.yaml b/server/tests-py/queries/graphql_query/boolexp/raster/teardown.yaml new file mode 100644 index 00000000..2927cea5 --- /dev/null +++ b/server/tests-py/queries/graphql_query/boolexp/raster/teardown.yaml @@ -0,0 +1,7 @@ +type: bulk +args: +- type: run_sql + args: + sql: | + DROP TABLE dummy_rast; + cascade: true diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 337f7939..5d29ddfc 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -353,6 +353,28 @@ class TestGraphQLQueryBoolExpPostGIS(DefaultTestSelectQueries): def dir(cls): return 'queries/graphql_query/boolexp/postgis' +@pytest.mark.parametrize("transport", ['http', 'websocket']) +class TestGraphQLQueryBoolExpRaster(DefaultTestSelectQueries): + + def test_query_st_intersects_geom_nband(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_st_intersects_geom_nband.yaml', transport) + + def test_query_st_intersects_geom_nband_no_rows(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_st_intersects_geom_nband_no_rows.yaml', transport) + + def test_query_st_intersects_rast(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_st_intersects_rast.yaml', transport) + + def test_query_st_intersects_rast_no_rows(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_st_intersects_rast_no_rows.yaml', transport) + + def test_query_st_intersects_rast_fail(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_st_intersects_rast_fail.yaml', transport) + + @classmethod + def dir(cls): + return 'queries/graphql_query/boolexp/raster' + @pytest.mark.parametrize("transport", ['http', 'websocket']) class TestGraphQLQueryOrderBy(DefaultTestSelectQueries): def test_articles_order_by_without_id(self, hge_ctx, transport):