graphql-engine/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

695 lines
32 KiB
Haskell
Raw Normal View History

module Hasura.Backends.DataConnector.Plan.QueryPlan
( -- Main external interface
mkQueryPlan,
-- Internals reused by other plan modules
translateAnnSimpleSelectToQueryRequest,
translateAnnAggregateSelectToQueryRequest,
translateAnnFields,
reshapeSimpleSelectRows,
reshapeTableAggregateFields,
reshapeAnnFields,
)
where
--------------------------------------------------------------------------------
import Control.Monad.Trans.Writer.CPS qualified as CPS
import Data.Aeson qualified as J
import Data.Aeson.Encoding qualified as JE
import Data.Bifunctor (Bifunctor (bimap))
import Data.Has (Has)
import Data.HashMap.Strict qualified as HashMap
import Data.List.NonEmpty qualified as NE
import Data.Semigroup (Min (..))
import Data.Set qualified as Set
import Data.Text.Extended (toTxt)
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Backends.DataConnector.Adapter.Backend
import Hasura.Backends.DataConnector.Adapter.Types
import Hasura.Backends.DataConnector.Plan.Common
import Hasura.Base.Error
import Hasura.Function.Cache qualified as Function
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp
import Hasura.RQL.IR.OrderBy
import Hasura.RQL.IR.Select
import Hasura.RQL.IR.Value
import Hasura.RQL.Types.BackendType
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.Session
import Language.GraphQL.Draft.Syntax qualified as G
import Witch qualified
--------------------------------------------------------------------------------
data FieldsAndAggregates = FieldsAndAggregates
{ _faaFields :: Maybe (HashMap FieldName API.Field),
_faaAggregates :: Maybe (HashMap FieldName API.Aggregate)
}
deriving stock (Show, Eq)
instance Semigroup FieldsAndAggregates where
left <> right =
FieldsAndAggregates
(_faaFields left <> _faaFields right)
(_faaAggregates left <> _faaAggregates right)
instance Monoid FieldsAndAggregates where
mempty = FieldsAndAggregates Nothing Nothing
--------------------------------------------------------------------------------
-- | Map a 'QueryDB 'DataConnector' term into a 'Plan'
mkQueryPlan ::
forall m r.
(MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) =>
SessionVariables ->
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
m (Plan API.QueryRequest API.QueryResponse)
mkQueryPlan sessionVariables ir = do
queryRequest <- translateQueryDB ir
pure $ Plan queryRequest (reshapeResponseToQueryShape ir)
where
translateQueryDB ::
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
m API.QueryRequest
translateQueryDB =
\case
QDBMultipleRows simpleSelect -> translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect
QDBSingleRow simpleSelect -> translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect
QDBAggregation aggregateSelect -> translateAnnAggregateSelectToQueryRequest sessionVariables aggregateSelect
translateAnnSimpleSelectToQueryRequest ::
forall m r.
(MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) =>
SessionVariables ->
AnnSimpleSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
m API.QueryRequest
translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect =
translateAnnSelectToQueryRequest sessionVariables (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) simpleSelect
translateAnnAggregateSelectToQueryRequest ::
forall m r.
(MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) =>
SessionVariables ->
AnnAggregateSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
m API.QueryRequest
translateAnnAggregateSelectToQueryRequest sessionVariables aggregateSelect =
translateAnnSelectToQueryRequest sessionVariables (translateTableAggregateFields sessionVariables) aggregateSelect
translateAnnSelectToQueryRequest ::
forall m r fieldType.
(MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) =>
SessionVariables ->
(TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) ->
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
m API.QueryRequest
translateAnnSelectToQueryRequest sessionVariables translateFieldsAndAggregates selectG = do
case _asnFrom selectG of
FromIdentifier _ -> throw400 NotSupported "AnnSelectG: FromIdentifier not supported"
FromNativeQuery {} -> throw400 NotSupported "AnnSelectG: FromNativeQuery not supported"
FromStoredProcedure {} -> throw400 NotSupported "AnnSelectG: FromStoredProcedure not supported"
FromTable tableName -> do
(query, TableRelationships tableRelationships) <-
CPS.runWriterT (translateAnnSelect sessionVariables translateFieldsAndAggregates (TableNameKey (Witch.into tableName)) selectG)
let relationships = mkRelationships <$> HashMap.toList tableRelationships
pure
$ API.QRTable
API.TableRequest
{ _trTable = Witch.into tableName,
_trRelationships = Set.fromList relationships,
_trQuery = query,
_trForeach = Nothing
}
FromFunction fn@(FunctionName functionName) argsExp _dListM -> do
args <- mkArgs sessionVariables argsExp fn
(query, TableRelationships tableRelationships) <-
CPS.runWriterT (translateAnnSelect sessionVariables translateFieldsAndAggregates (FunctionNameKey (Witch.into functionName)) selectG)
let relationships = mkRelationships <$> HashMap.toList tableRelationships
pure
$ API.QRFunction
API.FunctionRequest
{ _frFunction = Witch.into functionName,
_frRelationships = Set.fromList relationships,
_frQuery = query,
_frFunctionArguments = args
}
mkRelationships :: (TableRelationshipsKey, (HashMap API.RelationshipName API.Relationship)) -> API.Relationships
mkRelationships (FunctionNameKey functionName, relationships) = API.RFunction (API.FunctionRelationships functionName relationships)
mkRelationships (TableNameKey tableName, relationships) = API.RTable (API.TableRelationships tableName relationships)
mkArgs ::
( MonadError QErr m
) =>
SessionVariables ->
Function.FunctionArgsExpG (ArgumentExp (UnpreparedValue 'DataConnector)) ->
FunctionName ->
m [API.FunctionArgument]
mkArgs sessionVariables (Function.FunctionArgsExp ps ns) functionName = do
unless (null ps) $ throw400 NotSupported $ "Positional arguments not supported in function " <> toTxt functionName
getNamed
where
getNamed = mapM mkArg (HashMap.toList ns)
mkArg (n, input) = (API.NamedArgument n . API.ScalarArgumentValue) <$> getValue input
getValue (AEInput x) = case x of
UVLiteral _ -> throw400 NotSupported "Literal not supported in Data Connector function args."
UVSessionVar _ _ -> throw400 NotSupported "SessionVar not supported in Data Connector function args."
UVParameter _ (ColumnValue t v) -> pure (API.ScalarValue v (coerce (toTxt t)))
UVSession -> pure (API.ScalarValue (J.toJSON sessionVariables) (API.ScalarType "json"))
translateAnnSelect ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
(TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) ->
TableRelationshipsKey ->
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m API.Query
translateAnnSelect sessionVariables translateFieldsAndAggregates entityName selectG = do
FieldsAndAggregates {..} <- translateFieldsAndAggregates entityName (_asnFields selectG)
let whereClauseWithPermissions =
case _saWhere (_asnArgs selectG) of
Just expr -> BoolAnd [expr, _tpFilter (_asnPerm selectG)]
Nothing -> _tpFilter (_asnPerm selectG)
whereClause <- translateBoolExpToExpression sessionVariables entityName whereClauseWithPermissions
orderBy <- traverse (translateOrderBy sessionVariables entityName) (_saOrderBy $ _asnArgs selectG)
pure
API.Query
{ _qFields = mapFieldNameHashMap <$> _faaFields,
_qAggregates = mapFieldNameHashMap <$> _faaAggregates,
_qAggregatesLimit = _saLimit (_asnArgs selectG) <* _faaAggregates, -- Only include the aggregates limit if we actually have aggregrates
_qLimit =
fmap getMin
$ foldMap
(fmap Min)
[ _saLimit (_asnArgs selectG),
_tpLimit (_asnPerm selectG)
],
_qOffset = fmap fromIntegral (_saOffset (_asnArgs selectG)),
_qWhere = whereClause,
_qOrderBy = orderBy
}
translateOrderBy ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector)) ->
CPS.WriterT writerOutput m API.OrderBy
translateOrderBy sessionVariables sourceName orderByItems = do
orderByElementsAndRelations <- for orderByItems \OrderByItemG {..} -> do
let orderDirection = maybe API.Ascending Witch.from obiType
translateOrderByElement sessionVariables sourceName orderDirection [] obiColumn
relations <- lift . mergeOrderByRelations $ snd <$> orderByElementsAndRelations
pure
API.OrderBy
{ _obRelations = relations,
_obElements = fst <$> orderByElementsAndRelations
}
translateOrderByElement ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
API.OrderDirection ->
[API.RelationshipName] ->
AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation)
translateOrderByElement sessionVariables sourceName orderDirection targetReversePath = \case
AOCColumn (ColumnInfo {..}) ->
pure
( API.OrderByElement
{ _obeTargetPath = reverse targetReversePath,
_obeTarget = API.OrderByColumn $ Witch.from ciColumn,
_obeOrderDirection = orderDirection
},
mempty
)
AOCObjectRelation relationshipInfo filterExp orderByElement -> do
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceName relationshipInfo
(translatedOrderByElement, subOrderByRelations) <- translateOrderByElement sessionVariables (TableNameKey _rTargetTable) orderDirection (relationshipName : targetReversePath) orderByElement
targetTableWhereExp <- translateBoolExpToExpression sessionVariables (TableNameKey _rTargetTable) filterExp
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp subOrderByRelations)]
pure (translatedOrderByElement, orderByRelations)
AOCArrayAggregation relationshipInfo filterExp aggregateOrderByElement -> do
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceName relationshipInfo
orderByTarget <- case aggregateOrderByElement of
AAOCount ->
pure API.OrderByStarCountAggregate
AAOOp aggFunctionTxt resultType ColumnInfo {..} -> do
aggFunction <- lift $ translateSingleColumnAggregateFunction aggFunctionTxt
let resultScalarType = Witch.from $ columnTypeToScalarType resultType
pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction (Witch.from ciColumn) resultScalarType
let translatedOrderByElement =
API.OrderByElement
{ _obeTargetPath = reverse (relationshipName : targetReversePath),
_obeTarget = orderByTarget,
_obeOrderDirection = orderDirection
}
targetTableWhereExp <- translateBoolExpToExpression sessionVariables (TableNameKey _rTargetTable) filterExp
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp mempty)]
pure (translatedOrderByElement, orderByRelations)
mergeOrderByRelations ::
forall m f.
(MonadError QErr m, Foldable f) =>
f (HashMap API.RelationshipName API.OrderByRelation) ->
m (HashMap API.RelationshipName API.OrderByRelation)
mergeOrderByRelations orderByRelationsList =
foldM mergeMap mempty orderByRelationsList
where
mergeMap :: HashMap API.RelationshipName API.OrderByRelation -> HashMap API.RelationshipName API.OrderByRelation -> m (HashMap API.RelationshipName API.OrderByRelation)
mergeMap left right = foldM (\targetMap (relName, orderByRel) -> HashMap.alterF (maybe (pure $ Just orderByRel) (fmap Just . mergeOrderByRelation orderByRel)) relName targetMap) left $ HashMap.toList right
mergeOrderByRelation :: API.OrderByRelation -> API.OrderByRelation -> m API.OrderByRelation
mergeOrderByRelation right left =
if API._obrWhere left == API._obrWhere right
then do
mergedSubrelations <- mergeMap (API._obrSubrelations left) (API._obrSubrelations right)
pure $ API.OrderByRelation (API._obrWhere left) mergedSubrelations
else throw500 "mergeOrderByRelations: Differing filter expressions found for the same table"
translateAnnFieldsWithNoAggregates ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
FieldPrefix ->
TableRelationshipsKey ->
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m FieldsAndAggregates
translateAnnFieldsWithNoAggregates sessionVariables fieldNamePrefix sourceName fields =
(\fields' -> FieldsAndAggregates (Just fields') Nothing) <$> translateAnnFields sessionVariables fieldNamePrefix sourceName fields
translateAnnFields ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
FieldPrefix ->
TableRelationshipsKey ->
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m (HashMap FieldName API.Field)
translateAnnFields sessionVariables fieldNamePrefix sourceName fields = do
translatedFields <- traverse (traverse (translateAnnField sessionVariables sourceName)) fields
pure $ HashMap.fromList (mapMaybe (\(fieldName, field) -> (applyPrefix fieldNamePrefix fieldName,) <$> field) translatedFields)
translateAnnField ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
AnnFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m (Maybe API.Field)
translateAnnField sessionVariables sourceTableName = \case
AFNestedObject nestedObj ->
Just
. API.NestedObjField (Witch.from $ _anosColumn nestedObj)
<$> translateNestedObjectSelect sessionVariables sourceTableName nestedObj
Nested array support for Data Connectors Backend and MongoDB ## Description This change adds support for querying into nested arrays in Data Connector agents that support such a concept (currently MongoDB). ### DC API changes - New API type `ColumnType` which allows representing the type of a "column" as either a scalar type, an object reference or an array of `ColumnType`s. This recursive definition allows arbitrary nesting of arrays of types. - The `type` fields in the API types `ColumnInfo` and `ColumnInsertSchema` now take a `ColumnType` instead of a `ScalarType`. - To ensure backwards compatibility, a `ColumnType` representing a scalar serialises and deserialises to the same representation as `ScalarType`. - In queries, the `Field` type now has a new constructor `NestedArrayField`. This contains a nested `Field` along with optional `limit`, `offset`, `where` and `order_by` arguments. (These optional arguments are not yet used by either HGE or the MongoDB agent.) ### MongoDB Haskell agent changes - The `/schema` endpoint will now recognise arrays within the JSON validation schema and generate corresponding arrays in the DC schema. - The `/query` endpoint will now handle `NestedArrayField`s within queries (although it does not yet handle `limit`, `offset`, `where` and `order_by`). ### HGE server changes - The `Backend` type class adds a new type family `XNestedArrays b` to enable nested arrays on a per-backend basis (currently enabled only for the `DataConnector` backend. - Within `RawColumnInfo` the column type is now represented by a new type `RawColumnType b` which mirrors the shape of the DC API `ColumnType`, but uses `XNestedObjects b` and `XNestedArrays b` type families to allow turning nested object and array supports on or off for a particular backend. In the `DataConnector` backend `API.CustomType` is converted into `RawColumnInfo 'DataConnector` while building the schema. - In the next stage of schema building, the `RawColumnInfo` is converted into a `StructuredColumnInfo` which allows us to represent the three different types of columns: scalar, object and array. TODO: the `StructuredColumnInfo` looks very similar to the Logical Model types. The main difference is that it uses the `XNestedObjects` and `XNestedArrays` type families. We should be able to combine these two representations. - The `StructuredColumnInfo` is then placed into a `FIColumn` `FieldInfo`. This involved some refactoring of `FieldInfo` as I had previously split out `FINestedObject` into a separate constructor. However it works out better to represent all "column" fields (i.e. scalar, object and array) using `FIColumn` as this make it easier to implement permission checking correctly. This is the reason the `StructuredColumnInfo` was needed. - Next, the `FieldInfo` are used to generate `FieldParser`s. We add a new constructor to `AnnFieldG` for `AFNestedArray`. An `AFNestedArray` field parser can contain either a simple array selection or an array aggregate. Simple array `FieldParsers` are currently limited to subfield selection. We will add support for limit, offset, where and order_by in a future PR. We also don't yet generate array aggregate `FieldParsers. - The new `AFNestedArray` field is handled by the `QueryPlan` module in the `DataConnector` backend. There we generate an `API.NestedArrayField` from the AFNestedArray. We also handle nested arrays when reshaping the response from the DC agent. ## Limitations - Support for limit, offset, filter (where) and order_by is not yet fully implemented, although it should not be hard to add this - Support for aggregations on nested arrays is not yet fully implemented - Permissions involving nested arrays (and objects) not yet implemented - This should be integrated with Logical Model types, but that will happen in a separate PR PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9149 GitOrigin-RevId: 0e7b71a994fc1d2ca1ef73bfe7b96e95b5328531
2023-05-24 11:00:59 +03:00
AFNestedArray _ (ANASSimple field) ->
fmap mkArrayField <$> translateAnnField sessionVariables sourceTableName field
where
mkArrayField nestedField =
API.NestedArrayField (API.ArrayField nestedField Nothing Nothing Nothing Nothing)
-- TODO(dmoverton): support limit, offset, where and order_by in ArrayField
AFNestedArray _ (ANASAggregate _) ->
pure Nothing -- TODO(dmoverton): support nested array aggregates
AFColumn colField ->
-- TODO: make sure certain fields in colField are not in use, since we don't support them
pure . Just $ API.ColumnField (Witch.from $ _acfColumn colField) (Witch.from . columnTypeToScalarType $ _acfType colField)
AFObjectRelation objRel ->
case _aosTarget (_aarAnnSelect objRel) of
FromTable tableName -> do
let targetTable = Witch.from tableName
let relationshipName = mkRelationshipName $ _aarRelationshipName objRel
fields <- translateAnnFields sessionVariables noPrefix (TableNameKey targetTable) (_aosFields (_aarAnnSelect objRel))
whereClause <- translateBoolExpToExpression sessionVariables (TableNameKey targetTable) (_aosTargetFilter (_aarAnnSelect objRel))
recordTableRelationship
sourceTableName
relationshipName
API.Relationship
{ _rTargetTable = targetTable,
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping objRel)
}
pure
. Just
. API.RelField
$ API.RelationshipField
relationshipName
( API.Query
{ _qFields = Just $ mapFieldNameHashMap fields,
_qAggregates = mempty,
_qWhere = whereClause,
_qAggregatesLimit = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qOrderBy = Nothing
}
)
other -> error $ "translateAnnField: " <> show other
AFArrayRelation (ASSimple arrayRelationSelect) -> do
Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) arrayRelationSelect
AFArrayRelation (ASAggregate arrayRelationSelect) ->
Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateTableAggregateFields sessionVariables) arrayRelationSelect
AFExpression _literal ->
-- We ignore literal text fields (we don't send them to the data connector agent)
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure Nothing
translateArrayRelationSelect ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
(TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) ->
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector)) ->
CPS.WriterT writerOutput m API.Field
translateArrayRelationSelect sessionVariables sourceName translateFieldsAndAggregates arrRel = do
case _asnFrom (_aarAnnSelect arrRel) of
FromIdentifier _ -> lift $ throw400 NotSupported "AnnSelectG: FromIdentifier not supported"
FromNativeQuery {} -> lift $ throw400 NotSupported "AnnSelectG: FromNativeQuery not supported"
FromStoredProcedure {} -> lift $ throw400 NotSupported "AnnSelectG: FromStoredProcedure not supported"
FromFunction {} -> lift $ throw400 NotSupported "translateArrayRelationSelect: FromFunction not currently supported"
FromTable targetTable -> do
query <- translateAnnSelect sessionVariables translateFieldsAndAggregates (TableNameKey (Witch.into targetTable)) (_aarAnnSelect arrRel)
let relationshipName = mkRelationshipName $ _aarRelationshipName arrRel
recordTableRelationship
sourceName
relationshipName
API.Relationship
{ _rTargetTable = Witch.into targetTable,
_rRelationshipType = API.ArrayRelationship,
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping arrRel)
}
pure
. API.RelField
$ API.RelationshipField
relationshipName
query
translateTableAggregateFields ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
TableAggregateFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m FieldsAndAggregates
translateTableAggregateFields sessionVariables sourceName fields = do
mconcat <$> traverse (uncurry (translateTableAggregateField sessionVariables sourceName)) fields
translateTableAggregateField ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
) =>
SessionVariables ->
TableRelationshipsKey ->
FieldName ->
TableAggregateFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m FieldsAndAggregates
translateTableAggregateField sessionVariables sourceName fieldName = \case
TAFAgg aggregateFields -> do
let fieldNamePrefix = prefixWith fieldName
translatedAggregateFields <- lift $ mconcat <$> traverse (uncurry (translateAggregateField fieldNamePrefix)) aggregateFields
pure
$ FieldsAndAggregates
Nothing
(Just translatedAggregateFields)
TAFNodes _ fields ->
translateAnnFieldsWithNoAggregates sessionVariables (prefixWith fieldName) sourceName fields
TAFExp _txt ->
-- We ignore literal text fields (we don't send them to the data connector agent)
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure mempty
translateAggregateField ::
(MonadError QErr m) =>
FieldPrefix ->
FieldName ->
AggregateField 'DataConnector (UnpreparedValue 'DataConnector) ->
m (HashMap FieldName API.Aggregate)
translateAggregateField fieldPrefix fieldName = \case
AFCount countAggregate ->
let aggregate =
case countAggregate of
StarCount -> API.StarCount
ColumnCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = False}
ColumnDistinctCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = True}
in pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) aggregate
AFOp AggregateOp {..} -> do
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
aggFunction <- translateSingleColumnAggregateFunction _aoOp
fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) ->
case columnField of
SFCol column resultType ->
let resultScalarType = Witch.from $ columnTypeToScalarType resultType
in pure . Just $ (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn $ API.SingleColumnAggregate aggFunction (Witch.from column) resultScalarType)
SFExp _txt ->
-- We ignore literal text fields (we don't send them to the data connector agent)
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure Nothing
-- See Hasura.RQL.Types.Backend.supportsAggregateComputedFields
SFComputedField _ _ -> error "Aggregate computed fields aren't currently supported for Data Connectors!"
AFExp _txt ->
-- We ignore literal text fields (we don't send them to the data connector agent)
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure mempty
translateSingleColumnAggregateFunction :: (MonadError QErr m) => Text -> m API.SingleColumnAggregateFunction
translateSingleColumnAggregateFunction functionName =
fmap API.SingleColumnAggregateFunction (G.mkName functionName)
`onNothing` throw500 ("translateSingleColumnAggregateFunction: Invalid aggregate function encountered: " <> functionName)
Support nested object fields in DC API and use this to implement nest… ## Description This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent. ### Data Connector API changes - The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name. - Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field. ### MongoDB agent changes - Add support for querying into nested documents using the new `object` field type. ### HGE changes - The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend. - For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`. - If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source. - During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`. - When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`. - The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent. ## Limitations ### HGE not yet implemented: - Support for nested arrays - Support for nested objects/arrays in mutations - Support for nested objects/arrays in order-by - Support for filters (`where`) in nested objects/arrays - Support for adding custom GraphQL types via track table metadata API - Support for interface and union types - Tests for nested objects ### Mongo agent not yet implemented: - Generate nested object types from validation schema - Support for aggregates - Support for order-by - Configure agent port - Build agent in CI - Agent tests for nested objects and MongoDB agent PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844 GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
2023-04-11 04:29:05 +03:00
translateNestedObjectSelect ::
( Has TableRelationships writerOutput,
Monoid writerOutput,
MonadError QErr m,
MonadReader r m,
Has API.ScalarTypesCapabilities r
Support nested object fields in DC API and use this to implement nest… ## Description This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent. ### Data Connector API changes - The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name. - Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field. ### MongoDB agent changes - Add support for querying into nested documents using the new `object` field type. ### HGE changes - The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend. - For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`. - If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source. - During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`. - When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`. - The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent. ## Limitations ### HGE not yet implemented: - Support for nested arrays - Support for nested objects/arrays in mutations - Support for nested objects/arrays in order-by - Support for filters (`where`) in nested objects/arrays - Support for adding custom GraphQL types via track table metadata API - Support for interface and union types - Tests for nested objects ### Mongo agent not yet implemented: - Generate nested object types from validation schema - Support for aggregates - Support for order-by - Configure agent port - Build agent in CI - Agent tests for nested objects and MongoDB agent PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844 GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
2023-04-11 04:29:05 +03:00
) =>
SessionVariables ->
TableRelationshipsKey ->
Support nested object fields in DC API and use this to implement nest… ## Description This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent. ### Data Connector API changes - The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name. - Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field. ### MongoDB agent changes - Add support for querying into nested documents using the new `object` field type. ### HGE changes - The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend. - For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`. - If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source. - During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`. - When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`. - The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent. ## Limitations ### HGE not yet implemented: - Support for nested arrays - Support for nested objects/arrays in mutations - Support for nested objects/arrays in order-by - Support for filters (`where`) in nested objects/arrays - Support for adding custom GraphQL types via track table metadata API - Support for interface and union types - Tests for nested objects ### Mongo agent not yet implemented: - Generate nested object types from validation schema - Support for aggregates - Support for order-by - Configure agent port - Build agent in CI - Agent tests for nested objects and MongoDB agent PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844 GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
2023-04-11 04:29:05 +03:00
AnnNestedObjectSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m API.Query
translateNestedObjectSelect sessionVariables relationshipKey selectG = do
FieldsAndAggregates {..} <- translateAnnFieldsWithNoAggregates sessionVariables noPrefix relationshipKey $ _anosFields selectG
Support nested object fields in DC API and use this to implement nest… ## Description This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent. ### Data Connector API changes - The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name. - Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field. ### MongoDB agent changes - Add support for querying into nested documents using the new `object` field type. ### HGE changes - The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend. - For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`. - If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source. - During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`. - When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`. - The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent. ## Limitations ### HGE not yet implemented: - Support for nested arrays - Support for nested objects/arrays in mutations - Support for nested objects/arrays in order-by - Support for filters (`where`) in nested objects/arrays - Support for adding custom GraphQL types via track table metadata API - Support for interface and union types - Tests for nested objects ### Mongo agent not yet implemented: - Generate nested object types from validation schema - Support for aggregates - Support for order-by - Configure agent port - Build agent in CI - Agent tests for nested objects and MongoDB agent PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844 GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
2023-04-11 04:29:05 +03:00
pure
API.Query
{ _qFields = mapFieldNameHashMap <$> _faaFields,
_qAggregates = Nothing,
_qAggregatesLimit = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Nothing,
_qOrderBy = Nothing
}
--------------------------------------------------------------------------------
reshapeResponseToQueryShape ::
(MonadError QErr m) =>
QueryDB 'DataConnector Void v ->
API.QueryResponse ->
m J.Encoding
reshapeResponseToQueryShape queryDb response =
case queryDb of
QDBMultipleRows simpleSelect -> reshapeSimpleSelectRows Many (_asnFields simpleSelect) response
QDBSingleRow simpleSelect -> reshapeSimpleSelectRows Single (_asnFields simpleSelect) response
QDBAggregation aggregateSelect -> reshapeTableAggregateFields (_asnFields aggregateSelect) response
reshapeSimpleSelectRows ::
(MonadError QErr m) =>
Cardinality ->
AnnFieldsG 'DataConnector Void v ->
API.QueryResponse ->
m J.Encoding
reshapeSimpleSelectRows cardinality fields API.QueryResponse {..} =
case cardinality of
Single ->
case rows of
[] -> pure $ J.toEncoding J.Null
[singleRow] -> reshapeAnnFields noPrefix fields singleRow
_multipleRows ->
throw500 "Data Connector agent returned multiple rows when only one was expected" -- TODO(dchambers): Add pathing information for error clarity
Many -> do
reshapedRows <- traverse (reshapeAnnFields noPrefix fields) rows
pure $ JE.list id reshapedRows
where
rows = fromMaybe mempty _qrRows
reshapeTableAggregateFields ::
(MonadError QErr m) =>
TableAggregateFieldsG 'DataConnector Void v ->
API.QueryResponse ->
m J.Encoding
reshapeTableAggregateFields tableAggregateFields API.QueryResponse {..} = do
reshapedFields <- forM tableAggregateFields $ \(fieldName@(FieldName fieldNameText), tableAggregateField) -> do
case tableAggregateField of
TAFAgg aggregateFields -> do
reshapedAggregateFields <- reshapeAggregateFields (prefixWith fieldName) aggregateFields responseAggregates
pure $ (fieldNameText, reshapedAggregateFields)
TAFNodes _ annFields -> do
reshapedRows <- traverse (reshapeAnnFields (prefixWith fieldName) annFields) responseRows
pure $ (fieldNameText, JE.list id reshapedRows)
TAFExp txt ->
pure $ (fieldNameText, JE.text txt)
pure $ encodeAssocListAsObject reshapedFields
where
responseRows = fromMaybe mempty _qrRows
responseAggregates = fromMaybe mempty _qrAggregates
reshapeAggregateFields ::
(MonadError QErr m) =>
FieldPrefix ->
AggregateFields 'DataConnector v ->
HashMap API.FieldName J.Value ->
m J.Encoding
reshapeAggregateFields fieldPrefix aggregateFields responseAggregates = do
reshapedFields <- forM aggregateFields $ \(fieldName@(FieldName fieldNameText), aggregateField) ->
case aggregateField of
AFCount _countAggregate -> do
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix fieldName
responseAggregateValue <-
HashMap.lookup fieldNameKey responseAggregates
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName fieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
pure (fieldNameText, J.toEncoding responseAggregateValue)
AFOp AggregateOp {..} -> do
reshapedColumnFields <- forM _aoFields $ \(columnFieldName@(FieldName columnFieldNameText), columnField) ->
case columnField of
SFCol _column _columnType -> do
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
let columnFieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix' columnFieldName
responseAggregateValue <-
HashMap.lookup columnFieldNameKey responseAggregates
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName columnFieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
pure (columnFieldNameText, J.toEncoding responseAggregateValue)
SFExp txt ->
pure (columnFieldNameText, JE.text txt)
-- See Hasura.RQL.Types.Backend.supportsAggregateComputedFields
SFComputedField _ _ -> error "Aggregate computed fields aren't currently supported for Data Connectors!"
pure (fieldNameText, encodeAssocListAsObject reshapedColumnFields)
AFExp txt ->
pure (fieldNameText, JE.text txt)
pure $ encodeAssocListAsObject reshapedFields
reshapeAnnFields ::
(MonadError QErr m) =>
FieldPrefix ->
AnnFieldsG 'DataConnector Void v ->
HashMap API.FieldName API.FieldValue ->
m J.Encoding
reshapeAnnFields fieldNamePrefix fields responseRow = do
reshapedFields <- forM fields $ \(fieldName@(FieldName fieldNameText), field) -> do
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldNamePrefix fieldName
let responseField = fromMaybe API.nullFieldValue $ HashMap.lookup fieldNameKey responseRow
reshapedField <- reshapeField field responseField
pure (fieldNameText, reshapedField)
pure $ encodeAssocListAsObject reshapedFields
reshapeField ::
(MonadError QErr m) =>
AnnFieldG 'DataConnector Void v ->
API.FieldValue ->
m J.Encoding
reshapeField field responseFieldValue =
case field of
AFNestedObject nestedObj ->
handleNull responseFieldValue
$ case API.deserializeAsNestedObjFieldValue responseFieldValue of
Left err -> throw500 $ "Expected object in field returned by Data Connector agent: " <> err -- TODO(dmoverton): Add pathing information for error clarity
Right nestedResponse ->
reshapeAnnFields noPrefix (_anosFields nestedObj) nestedResponse
AFNestedArray _ (ANASSimple arrayField) ->
handleNull responseFieldValue
$ case API.deserializeAsNestedArrayFieldValue responseFieldValue of
Left err -> throw500 $ "Expected array in field returned by Data Connector agent: " <> err -- TODO(dmoverton): Add pathing information for error clarity
Right arrayResponse ->
JE.list id <$> traverse (reshapeField arrayField) arrayResponse
Nested array support for Data Connectors Backend and MongoDB ## Description This change adds support for querying into nested arrays in Data Connector agents that support such a concept (currently MongoDB). ### DC API changes - New API type `ColumnType` which allows representing the type of a "column" as either a scalar type, an object reference or an array of `ColumnType`s. This recursive definition allows arbitrary nesting of arrays of types. - The `type` fields in the API types `ColumnInfo` and `ColumnInsertSchema` now take a `ColumnType` instead of a `ScalarType`. - To ensure backwards compatibility, a `ColumnType` representing a scalar serialises and deserialises to the same representation as `ScalarType`. - In queries, the `Field` type now has a new constructor `NestedArrayField`. This contains a nested `Field` along with optional `limit`, `offset`, `where` and `order_by` arguments. (These optional arguments are not yet used by either HGE or the MongoDB agent.) ### MongoDB Haskell agent changes - The `/schema` endpoint will now recognise arrays within the JSON validation schema and generate corresponding arrays in the DC schema. - The `/query` endpoint will now handle `NestedArrayField`s within queries (although it does not yet handle `limit`, `offset`, `where` and `order_by`). ### HGE server changes - The `Backend` type class adds a new type family `XNestedArrays b` to enable nested arrays on a per-backend basis (currently enabled only for the `DataConnector` backend. - Within `RawColumnInfo` the column type is now represented by a new type `RawColumnType b` which mirrors the shape of the DC API `ColumnType`, but uses `XNestedObjects b` and `XNestedArrays b` type families to allow turning nested object and array supports on or off for a particular backend. In the `DataConnector` backend `API.CustomType` is converted into `RawColumnInfo 'DataConnector` while building the schema. - In the next stage of schema building, the `RawColumnInfo` is converted into a `StructuredColumnInfo` which allows us to represent the three different types of columns: scalar, object and array. TODO: the `StructuredColumnInfo` looks very similar to the Logical Model types. The main difference is that it uses the `XNestedObjects` and `XNestedArrays` type families. We should be able to combine these two representations. - The `StructuredColumnInfo` is then placed into a `FIColumn` `FieldInfo`. This involved some refactoring of `FieldInfo` as I had previously split out `FINestedObject` into a separate constructor. However it works out better to represent all "column" fields (i.e. scalar, object and array) using `FIColumn` as this make it easier to implement permission checking correctly. This is the reason the `StructuredColumnInfo` was needed. - Next, the `FieldInfo` are used to generate `FieldParser`s. We add a new constructor to `AnnFieldG` for `AFNestedArray`. An `AFNestedArray` field parser can contain either a simple array selection or an array aggregate. Simple array `FieldParsers` are currently limited to subfield selection. We will add support for limit, offset, where and order_by in a future PR. We also don't yet generate array aggregate `FieldParsers. - The new `AFNestedArray` field is handled by the `QueryPlan` module in the `DataConnector` backend. There we generate an `API.NestedArrayField` from the AFNestedArray. We also handle nested arrays when reshaping the response from the DC agent. ## Limitations - Support for limit, offset, filter (where) and order_by is not yet fully implemented, although it should not be hard to add this - Support for aggregations on nested arrays is not yet fully implemented - Permissions involving nested arrays (and objects) not yet implemented - This should be integrated with Logical Model types, but that will happen in a separate PR PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9149 GitOrigin-RevId: 0e7b71a994fc1d2ca1ef73bfe7b96e95b5328531
2023-05-24 11:00:59 +03:00
AFNestedArray _ (ANASAggregate _) ->
throw400 NotSupported "Nested array aggregate not supported"
AFColumn _columnField -> do
let columnFieldValue = API.deserializeAsColumnFieldValue responseFieldValue
pure $ J.toEncoding columnFieldValue
AFObjectRelation objectRelationField -> do
case API.deserializeAsRelationshipFieldValue responseFieldValue of
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
Right subqueryResponse ->
let fields = _aosFields $ _aarAnnSelect objectRelationField
in reshapeSimpleSelectRows Single fields subqueryResponse
AFArrayRelation (ASSimple simpleArrayRelationField) ->
reshapeAnnRelationSelect (reshapeSimpleSelectRows Many) simpleArrayRelationField responseFieldValue
AFArrayRelation (ASAggregate aggregateArrayRelationField) ->
reshapeAnnRelationSelect reshapeTableAggregateFields aggregateArrayRelationField responseFieldValue
AFExpression txt -> pure $ JE.text txt
where
handleNull v a =
if API.isNullFieldValue v
then pure JE.null_
else a
reshapeAnnRelationSelect ::
(MonadError QErr m) =>
(Fields (fieldType v) -> API.QueryResponse -> m J.Encoding) ->
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType v) ->
API.FieldValue ->
m J.Encoding
reshapeAnnRelationSelect reshapeFields annRelationSelect fieldValue =
case API.deserializeAsRelationshipFieldValue fieldValue of
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
Right subqueryResponse ->
let annSimpleSelect = _aarAnnSelect annRelationSelect
in reshapeFields (_asnFields annSimpleSelect) subqueryResponse