这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions docs/graphql/manual/api-reference/graphql-api/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,18 @@ E.g.

author {
id # scalar integer field

name # scalar text field

address(path: "$.city") # scalar JSON field -> property
address(path: "city") # scalar JSON field -> property; '$.' prefix is optional
contacts(path: "[0]") # scalar JSON field -> array_item
contacts(path: "[0].phone") # scalar JSON field -> array_item_property
contacts(path: "[0].phone") # scalar JSON field -> array_item_property

article { # nested object
title
}

article_aggregate { # aggregate nested object
aggregate {
count
Expand Down Expand Up @@ -371,6 +371,10 @@ Operator

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

**Type casting:**

- ``_cast`` (takes a CastExp_ as a value)

**JSONB operators:**

.. list-table::
Expand Down Expand Up @@ -428,6 +432,19 @@ Operator
field-name : {_st_d_within: {distance: Float, from: Value} }
}

.. _CastExp:

CastExp
#######

.. parsed-literal ::

{type-name: {Operator_: Value}}

.. note::

Currently, only casting between ``geometry`` and ``geography`` types is allowed.

.. _OrderByExp:

OrderByExp
Expand Down
112 changes: 111 additions & 1 deletion docs/graphql/manual/queries/query-filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ Fetches a list of articles rated 1, 3 or 5:

Example: String or Text
^^^^^^^^^^^^^^^^^^^^^^^
Fetch a list of those authors whose names are NOT part of a list:
Fetch a list of those authors whose names are NOT part of a list:

.. graphiql::
:view_only:
Expand Down Expand Up @@ -1396,5 +1396,115 @@ Fetch all authors which have at least one article written by them:
}
}

Cast a field to a different type before filtering (_cast)
---------------------------------------------------------

The ``_cast`` operator can be used to cast a field to a different type, which allows type-specific
operators to be used on fields that otherwise would not support them. Currently, only casting
between PostGIS ``geometry`` and ``geography`` types is supported.

Casting using ``_cast`` corresponds directly to
`SQL type casts <https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS>`__.

Example: cast ``geometry`` to ``geography``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Filtering using ``_st_d_within`` over large distances can be inaccurate for location data stored in
``geometry`` columns. For accurate queries, cast the field to ``geography`` before comparing:

.. graphiql::
:view_only:
:query:
query cities_near($point: geography!, $distance: Float!) {
cities(
where: {location: {
_cast: {geography: {
_st_d_within: {from: $point, distance: $distance}
}}
}}
) {
name
}
}
:response:
{
"data": {
"cities": [
{
"name": "London"
},
{
"name": "Paris"
}
]
}
}
:variables:
{
"point": {
"type": "Point",
"coordinates": [1, 50]
},
"distance": 1000000
}

Example: cast ``geography`` to ``geometry``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Columns of type ``geography`` are more accurate, but they don’t support as many operations as
``geometry``. Cast to ``geometry`` to use those operations in a filter:

.. graphiql::
:view_only:
:query:
query cities_inside($polygon: geometry) {
cities(
where: {location: {
_cast: {geometry: {
_st_within: $polygon
}}
}}
) {
name
}
}
:response:
{
"data": {
"cities": [
{
"name": "New York"
}
]
}
}
:variables:
{
"polygon": {
"type": "Polygon",
"crs": {
"type": "name",
"properties": { "name": "EPSG:4326" }
},
"coordinates": [
[
[-75, 40],
[-74, 40],
[-74, 41],
[-75, 41],
[-75, 40]
]
]
}
}

.. note::

For performant queries that filter on casted fields, create an
`expression index <https://www.postgresql.org/docs/current/indexes-expressional.html>`__
on the casted column. For example, if you frequently perform queries on a field ``location`` of
type ``geometry`` casted to type ``geography``, you should create an index like the following:

.. code-block:: sql

CREATE INDEX cities_location_geography ON cities USING GIST ((location::geography));
72 changes: 56 additions & 16 deletions server/src-lib/Hasura/GraphQL/Context.hs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ mkCompExpTy :: PGColType -> G.NamedType
mkCompExpTy =
G.NamedType . mkCompExpName

mkCastExpName :: PGColType -> G.Name
mkCastExpName pgColTy = G.Name $ T.pack (show pgColTy) <> "_cast_exp"

mkCastExpTy :: PGColType -> G.NamedType
mkCastExpTy = G.NamedType . mkCastExpName

-- TODO(shahidhk) this should ideally be st_d_within_geometry
{-
input st_d_within_input {
Expand Down Expand Up @@ -211,6 +217,7 @@ mkCompExpInp colTy =
, bool [] (stDWithinGeoOpInpVal stDWithinGeographyInpTy :
map geoOpToInpVal geoOps) isGeographyType
, [InpValInfo Nothing "_is_null" Nothing $ G.TypeNamed (G.Nullability True) $ G.NamedType "Boolean"]
, maybeToList castOpInputValue
]) TLHasuraType
where
tyDesc = mconcat
Expand Down Expand Up @@ -264,6 +271,11 @@ mkCompExpInp colTy =
)
]

castOpInputValue =
-- currently, only geometry/geography types support casting
guard (isGeoType colTy) $>
InpValInfo Nothing "_cast" Nothing (G.toGT $ mkCastExpTy colTy)

stDWithinGeoOpInpVal ty =
InpValInfo (Just stDWithinGeoDesc) "_st_d_within" Nothing $ G.toGT ty
stDWithinGeoDesc =
Expand Down Expand Up @@ -316,6 +328,24 @@ mkCompExpInp colTy =
)
]

-- | Makes an input type declaration for the @_cast@ field of a comparison expression.
-- (Currently only used for casting between geometry and geography types.)
mkCastExpressionInputType :: PGColType -> [PGColType] -> InpObjTyInfo
mkCastExpressionInputType sourceType targetTypes =
mkHsraInpTyInfo (Just description) (mkCastExpTy sourceType) (fromInpValL targetFields)
where
description = mconcat
[ "Expression to compare the result of casting a column of type "
, G.Description (T.pack $ show sourceType)
, ". Multiple cast targets are combined with logical 'AND'."
]
targetFields = map targetField targetTypes
targetField targetType = InpValInfo
Nothing
(G.Name . T.pack $ show targetType)
Nothing
(G.toGT $ mkCompExpTy targetType)

ordByTy :: G.NamedType
ordByTy = G.NamedType "order_by"

Expand Down Expand Up @@ -357,25 +387,22 @@ mkGCtx tyAgg (RootFlds flds) insCtxMap =
let queryRoot = mkHsraObjTyInfo (Just "query root")
(G.NamedType "query_root") Set.empty $
mapFromL _fiName (schemaFld:typeFld:qFlds)
scalarTys = map (TIScalar . mkHsraScalarTyInfo) (colTys <> toList scalars)
compTys = map (TIInpObj . mkCompExpInp) colTys
scalarTys = map (TIScalar . mkHsraScalarTyInfo) (Set.toList allScalarTypes)
compTys = map (TIInpObj . mkCompExpInp) (Set.toList allComparableTypes)
ordByEnumTyM = bool (Just ordByEnumTy) Nothing $ null qFlds
allTys = Map.union tyInfos $ mkTyInfoMap $
catMaybes [ Just $ TIObj queryRoot
, TIObj <$> mutRootM
, TIObj <$> subRootM
, TIEnum <$> ordByEnumTyM
, TIInpObj <$> stDWithinGeometryInpM
, TIInpObj <$> stDWithinGeographyInpM
] <>
scalarTys <> compTys <> defaultTypes
scalarTys <> compTys <> defaultTypes <> wiredInGeoInputTypes
-- for now subscription root is query root
in GCtx allTys fldInfos ordByEnums queryRoot mutRootM subRootM
(Map.map fst flds) insCtxMap
where
TyAgg tyInfos fldInfos scalars ordByEnums = tyAgg
colTys = Set.toList $ Set.fromList $ map pgiType $
lefts $ Map.elems fldInfos
colTys = Set.fromList $ map pgiType $ lefts $ Map.elems fldInfos
mkMutRoot =
mkHsraObjTyInfo (Just "mutation root") (G.NamedType "mutation_root") Set.empty .
mapFromL _fiName
Expand All @@ -395,26 +422,39 @@ mkGCtx tyAgg (RootFlds flds) insCtxMap =
$ G.toGT $ G.toNT $ G.NamedType "String"
]

-- _st_d_within has to stay with geometry type
stDWithinGeometryInpM =
bool Nothing (Just stDWithinGeomInp) (PGGeometry `elem` colTys)
-- _st_d_within_geography is created for geography type
stDWithinGeographyInpM =
bool Nothing (Just stDWithinGeogInp) (PGGeography `elem` colTys)

stDWithinGeomInp =
anyGeoTypes = any isGeoType colTys
allComparableTypes =
if anyGeoTypes
-- due to casting, we need to generate both geometry and geography
-- operations even if just one of the two appears in the schema
then Set.union (Set.fromList [PGGeometry, PGGeography]) colTys
else colTys
allScalarTypes = allComparableTypes <> scalars

wiredInGeoInputTypes =
guard anyGeoTypes *> map TIInpObj
[ stDWithinGeometryInputType
, stDWithinGeographyInputType
, castGeometryInputType
, castGeographyInputType
]

stDWithinGeometryInputType =
mkHsraInpTyInfo Nothing stDWithinGeometryInpTy $ fromInpValL
[ InpValInfo Nothing "from" Nothing $ G.toGT $ G.toNT $ mkScalarTy PGGeometry
, InpValInfo Nothing "distance" Nothing $ G.toNT $ mkScalarTy PGFloat
]
stDWithinGeogInp =
stDWithinGeographyInputType =
mkHsraInpTyInfo Nothing stDWithinGeographyInpTy $ fromInpValL
[ InpValInfo Nothing "from" Nothing $ G.toGT $ G.toNT $ mkScalarTy PGGeography
, InpValInfo Nothing "distance" Nothing $ G.toNT $ mkScalarTy PGFloat
, InpValInfo
Nothing "use_spheroid" (Just $ G.VCBoolean True) $ G.toGT $ mkScalarTy PGBoolean
]

castGeometryInputType = mkCastExpressionInputType PGGeometry [PGGeography]
castGeographyInputType = mkCastExpressionInputType PGGeography [PGGeometry]

emptyGCtx :: GCtx
emptyGCtx = mkGCtx mempty mempty mempty

Expand Down
14 changes: 14 additions & 0 deletions server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Hasura.GraphQL.Resolve.BoolExp
import Data.Has
import Hasura.Prelude

import qualified Data.HashMap.Strict as Map
import qualified Data.HashMap.Strict.InsOrd as OMap
import qualified Language.GraphQL.Draft.Syntax as G

Expand All @@ -26,6 +27,8 @@ parseOpExps colTy annVal = do
opExpsM <- flip withObjectM annVal $ \nt objM -> forM objM $ \obj ->
forM (OMap.toList obj) $ \(k, v) ->
case k of
"_cast" -> fmap ACast <$> parseCastExpression v

"_eq" -> fmap (AEQ True) <$> asOpRhs v
"_ne" -> fmap (ANE True) <$> asOpRhs v
"_neq" -> fmap (ANE True) <$> asOpRhs v
Expand Down Expand Up @@ -107,6 +110,17 @@ parseOpExps colTy annVal = do
return $ ASTDWithinGeom $ DWithinGeomOp dist from
_ -> throw500 "expected PGGeometry/PGGeography column for st_d_within"

parseCastExpression
:: (MonadError QErr m)
=> AnnInpVal -> m (Maybe (CastExp UnresolvedVal))
parseCastExpression =
withObjectM $ \_ objM -> forM objM $ \obj -> do
targetExps <- forM (OMap.toList obj) $ \(targetTypeName, castedComparisonExpressionInput) -> do
let targetType = txtToPgColTy $ G.unName targetTypeName
castedComparisonExpressions <- parseOpExps targetType castedComparisonExpressionInput
return (targetType, castedComparisonExpressions)
return $ Map.fromList targetExps

parseColExp
:: ( MonadError QErr m
, MonadReader r m
Expand Down
1 change: 1 addition & 0 deletions server/src-lib/Hasura/Prelude.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Data.Bool as M (bool)
import Data.Either as M (lefts, partitionEithers,
rights)
import Data.Foldable as M (foldrM, toList)
import Data.Functor as M (($>), (<&>))
import Data.Hashable as M (Hashable)
import Data.List as M (find, foldl', group,
intercalate, intersect,
Expand Down
Loading