mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
8a77386fcf
### Description This PR adds the required IR for DB to DB joins, based on @paf31 and @0x777 's `feature/db-to-db` branch. To do so, it also refactors the IR to introduce a new type parameter, `r`, which is used to recursively constructs the `v` parameter of remote QueryDBs. When collecting remote joins, we replace `r` with `Const Void`, indicating at the type level that there cannot be any leftover remote join. Furthermore, this PR refactors IR.Select for readability, moves some code from IR.Root to IR.Select to avoid having to deal with circular dependencies, and makes it compile by adding `error` in all new cases in the execution pipeline. The diff doesn't make it clear, but most of Select.hs is actually unchanged. Declarations have just been reordered by topic, in the following order: - type declarations - instance declarations - type aliases - constructor functions - traverse functions https://github.com/hasura/graphql-engine-mono/pull/1580 Co-authored-by: Phil Freeman <630306+paf31@users.noreply.github.com> GitOrigin-RevId: bbdcb4119cec8bb3fc32f1294f91b8dea0728721
310 lines
12 KiB
Haskell
310 lines
12 KiB
Haskell
module Hasura.GraphQL.Execute.RemoteJoin.Collect
|
|
( RemoteJoins
|
|
, RemoteJoinMap
|
|
, FieldPath(..)
|
|
, appendPath
|
|
, getRemoteJoins
|
|
, getRemoteJoinsSelect
|
|
, getRemoteJoinsMutationDB
|
|
, getRemoteJoinsActionQuery
|
|
, getRemoteJoinsActionMutation
|
|
) where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.HashMap.Strict as Map
|
|
import qualified Data.HashSet as HS
|
|
import qualified Data.List.NonEmpty as NE
|
|
|
|
import Control.Lens
|
|
|
|
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
|
import Hasura.RQL.IR
|
|
import Hasura.RQL.IR.Returning
|
|
import Hasura.RQL.Types
|
|
|
|
{- Note: [Remote Joins Architecture]
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Unparsed Incoming GraphQL +------------------------------+
|
|
--------------------------> | Parsing of the GraphQL query |-----+
|
|
+------------------------------+ |
|
|
| DB Query and remote joins (if any)
|
|
|
|
|
V
|
|
+----------------------------------+ SQL query response +----------------------------+
|
|
| Traverse the DB response to | <------------------- | Execution of the DB query |
|
|
| get the values of the arguments | +----------------------------+
|
|
| of the remote field |
|
|
+----------------------------------+
|
|
|
|
|
| Remote field arguments
|
|
V
|
|
+--------------------------+ Remote schema response +----------------------------------------+
|
|
| Query the remote schema | ------------------------> | Replace the remote join fields in |
|
|
| with the remote field | | the SQL query response (JSON) with |
|
|
| arguments to the remote | | the response obtained from the remote |
|
|
| field configured in the | | schema at appropriate places. |
|
|
| remote join. | +----------------------------------------+
|
|
+--------------------------+
|
|
-}
|
|
|
|
-- | Collects remote joins from the AST and also adds the necessary join fields
|
|
getRemoteJoins
|
|
:: Backend b
|
|
=> QueryDB b r u
|
|
-> (QueryDB b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoins = \case
|
|
QDBMultipleRows s -> first QDBMultipleRows $ getRemoteJoinsSelect s
|
|
QDBSingleRow s -> first QDBSingleRow $ getRemoteJoinsSelect s
|
|
QDBAggregation s -> first QDBAggregation $ getRemoteJoinsAggregateSelect s
|
|
QDBConnection s -> first QDBConnection $ getRemoteJoinsConnectionSelect s
|
|
|
|
-- | Traverse through 'AnnSimpleSel' and collect remote join fields (if any).
|
|
getRemoteJoinsSelect
|
|
:: Backend b
|
|
=> AnnSimpleSelG b r u
|
|
-> (AnnSimpleSelG b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsSelect =
|
|
second mapToNonEmpty . flip runState mempty . transformSelect mempty
|
|
|
|
-- | Traverse through @'AnnAggregateSelect' and collect remote join fields (if any).
|
|
getRemoteJoinsAggregateSelect
|
|
:: Backend b
|
|
=> AnnAggregateSelectG b r u
|
|
-> (AnnAggregateSelectG b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsAggregateSelect =
|
|
second mapToNonEmpty . flip runState mempty . transformAggregateSelect mempty
|
|
|
|
-- | Traverse through @'ConnectionSelect' and collect remote join fields (if any).
|
|
getRemoteJoinsConnectionSelect
|
|
:: Backend b
|
|
=> ConnectionSelect b r u
|
|
-> (ConnectionSelect b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsConnectionSelect =
|
|
second mapToNonEmpty . flip runState mempty . transformConnectionSelect mempty
|
|
|
|
-- | Traverse through 'MutationOutput' and collect remote join fields (if any)
|
|
getRemoteJoinsMutationOutput
|
|
:: Backend b
|
|
=> MutationOutputG b r u
|
|
-> (MutationOutputG b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsMutationOutput =
|
|
second mapToNonEmpty . flip runState mempty . transformMutationOutput mempty
|
|
where
|
|
transformMutationOutput path = \case
|
|
MOutMultirowFields mutationFields ->
|
|
MOutMultirowFields <$> transfromMutationFields mutationFields
|
|
MOutSinglerowObject annFields ->
|
|
MOutSinglerowObject <$> transformAnnFields path annFields
|
|
where
|
|
transfromMutationFields fields =
|
|
forM fields $ \(fieldName, field') -> do
|
|
let fieldPath = appendPath fieldName path
|
|
(fieldName,) <$> case field' of
|
|
MCount -> pure MCount
|
|
MExp t -> pure $ MExp t
|
|
MRet annFields -> MRet <$> transformAnnFields fieldPath annFields
|
|
|
|
|
|
-- local helpers
|
|
|
|
getRemoteJoinsAnnFields
|
|
:: Backend b
|
|
=> AnnFieldsG b r u
|
|
-> (AnnFieldsG b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsAnnFields =
|
|
second mapToNonEmpty . flip runState mempty . transformAnnFields mempty
|
|
|
|
getRemoteJoinsMutationDB
|
|
:: Backend b
|
|
=> MutationDB b r u
|
|
-> (MutationDB b (Const Void) u, Maybe RemoteJoins)
|
|
getRemoteJoinsMutationDB = \case
|
|
MDBInsert insert ->
|
|
first MDBInsert $ getRemoteJoinsInsert insert
|
|
MDBUpdate update ->
|
|
first MDBUpdate $ getRemoteJoinsUpdate update
|
|
MDBDelete delete ->
|
|
first MDBDelete $ getRemoteJoinsDelete delete
|
|
MDBFunction aggSelect select ->
|
|
first (MDBFunction aggSelect) $ getRemoteJoinsSelect select
|
|
where
|
|
getRemoteJoinsInsert insert =
|
|
let (output', remoteJoins) = getRemoteJoinsMutationOutput $ _aiOutput insert
|
|
in (insert{ _aiOutput = output'}, remoteJoins)
|
|
|
|
getRemoteJoinsUpdate update =
|
|
let (output', remoteJoins) = getRemoteJoinsMutationOutput $ uqp1Output update
|
|
in (update{ uqp1Output = output'}, remoteJoins)
|
|
|
|
getRemoteJoinsDelete delete =
|
|
let (output', remoteJoins) = getRemoteJoinsMutationOutput $ dqp1Output delete
|
|
in (delete{ dqp1Output = output'}, remoteJoins)
|
|
|
|
getRemoteJoinsSyncAction
|
|
:: (Backend b)
|
|
=> AnnActionExecution b r v
|
|
-> (AnnActionExecution b (Const Void) v, Maybe RemoteJoins)
|
|
getRemoteJoinsSyncAction actionExecution =
|
|
let (fields', remoteJoins) = getRemoteJoinsAnnFields $ _aaeFields actionExecution
|
|
in (actionExecution { _aaeFields = fields' }, remoteJoins)
|
|
|
|
getRemoteJoinsActionQuery
|
|
:: (Backend b)
|
|
=> ActionQuery b r v
|
|
-> (ActionQuery b (Const Void) v, Maybe RemoteJoins)
|
|
getRemoteJoinsActionQuery = \case
|
|
AQQuery sync ->
|
|
first AQQuery $ getRemoteJoinsSyncAction sync
|
|
AQAsync async ->
|
|
first AQAsync $ getRemoteJoinsAsyncQuery async
|
|
where
|
|
getRemoteJoinsAsyncQuery async =
|
|
let (fields', remoteJoins) =
|
|
second mapToNonEmpty . flip runState mempty . transformAsyncFields mempty $
|
|
_aaaqFields async
|
|
in (async { _aaaqFields = fields' }, remoteJoins)
|
|
|
|
transformAsyncFields path fields =
|
|
forM fields $ \(fieldName, field) -> do
|
|
let fieldPath = appendPath fieldName path
|
|
(fieldName,) <$> case field of
|
|
AsyncTypename t -> pure $ AsyncTypename t
|
|
AsyncOutput outputFields ->
|
|
AsyncOutput <$> transformAnnFields fieldPath outputFields
|
|
AsyncId -> pure AsyncId
|
|
AsyncCreatedAt -> pure AsyncCreatedAt
|
|
AsyncErrors -> pure AsyncErrors
|
|
|
|
getRemoteJoinsActionMutation
|
|
:: (Backend b)
|
|
=> ActionMutation b r v
|
|
-> (ActionMutation b (Const Void) v, Maybe RemoteJoins)
|
|
getRemoteJoinsActionMutation = \case
|
|
AMSync sync ->
|
|
first AMSync $ getRemoteJoinsSyncAction sync
|
|
AMAsync async -> (AMAsync async, Nothing)
|
|
|
|
|
|
transformSelect
|
|
:: Backend b
|
|
=> FieldPath
|
|
-> AnnSimpleSelG b r u
|
|
-> State RemoteJoinMap (AnnSimpleSelG b (Const Void) u)
|
|
transformSelect path sel = do
|
|
let fields = _asnFields sel
|
|
-- Transform selects in array, object and computed fields
|
|
transformedFields <- transformAnnFields path fields
|
|
pure sel{_asnFields = transformedFields}
|
|
|
|
transformAggregateSelect
|
|
:: Backend b
|
|
=> FieldPath
|
|
-> AnnAggregateSelectG b r u
|
|
-> State RemoteJoinMap (AnnAggregateSelectG b (Const Void) u)
|
|
transformAggregateSelect path sel = do
|
|
let aggFields = _asnFields sel
|
|
transformedFields <- forM aggFields $ \(fieldName, aggField) ->
|
|
(fieldName,) <$> case aggField of
|
|
TAFAgg agg -> pure $ TAFAgg agg
|
|
TAFNodes x annFields -> TAFNodes x <$> transformAnnFields (appendPath fieldName path) annFields
|
|
TAFExp t -> pure $ TAFExp t
|
|
pure sel{_asnFields = transformedFields}
|
|
|
|
transformConnectionSelect
|
|
:: Backend b
|
|
=> FieldPath
|
|
-> ConnectionSelect b r u
|
|
-> State RemoteJoinMap (ConnectionSelect b (Const Void) u)
|
|
transformConnectionSelect path ConnectionSelect{..} = do
|
|
let connectionFields = _asnFields _csSelect
|
|
transformedFields <- forM connectionFields $ \(fieldName, field) ->
|
|
(fieldName,) <$> case field of
|
|
ConnectionTypename t -> pure $ ConnectionTypename t
|
|
ConnectionPageInfo p -> pure $ ConnectionPageInfo p
|
|
ConnectionEdges edges -> ConnectionEdges <$> transformEdges (appendPath fieldName path) edges
|
|
let select = _csSelect{_asnFields = transformedFields}
|
|
pure $ ConnectionSelect _csXRelay _csPrimaryKeyColumns _csSplit _csSlice select
|
|
where
|
|
transformEdges edgePath edgeFields =
|
|
forM edgeFields $ \(fieldName, edgeField) ->
|
|
(fieldName,) <$> case edgeField of
|
|
EdgeTypename t -> pure $ EdgeTypename t
|
|
EdgeCursor -> pure EdgeCursor
|
|
EdgeNode annFields ->
|
|
EdgeNode <$> transformAnnFields (appendPath fieldName edgePath) annFields
|
|
|
|
transformObjectSelect
|
|
:: Backend b
|
|
=> FieldPath
|
|
-> AnnObjectSelectG b r u
|
|
-> State RemoteJoinMap (AnnObjectSelectG b (Const Void) u)
|
|
transformObjectSelect path sel = do
|
|
let fields = _aosFields sel
|
|
transformedFields <- transformAnnFields path fields
|
|
pure sel{_aosFields = transformedFields}
|
|
|
|
transformAnnFields
|
|
:: forall b r u
|
|
. Backend b
|
|
=> FieldPath
|
|
-> AnnFieldsG b r u
|
|
-> State RemoteJoinMap (AnnFieldsG b (Const Void) u)
|
|
transformAnnFields path fields = do
|
|
|
|
-- TODO: Check for correctness. I think this entire function seems to be
|
|
-- assuming that the column names will appear as is in the response from the
|
|
-- server, which is incorrect as they can be aliased. Similarly, the phantom
|
|
-- columns are being added without checking for overlap with aliases
|
|
|
|
let pgColumnFields = HS.fromList $ map (pgiColumn . _acfInfo . snd) $
|
|
getFields _AFColumn fields
|
|
remoteSelects = getFields (_AFRemote) fields
|
|
remoteJoins = remoteSelects <&> \(fieldName, remoteSelect) ->
|
|
let RemoteSelect argsMap selSet hasuraColumns remoteFields rsi = remoteSelect
|
|
hasuraColumnFields = HS.map (fromCol @b . pgiColumn) hasuraColumns
|
|
phantomColumns = HS.filter ((`notElem` pgColumnFields) . pgiColumn) hasuraColumns
|
|
in (phantomColumns, RemoteJoin fieldName argsMap selSet hasuraColumnFields remoteFields rsi $
|
|
map (fromCol @b . pgiColumn) $ toList phantomColumns)
|
|
|
|
transformedFields <- forM fields $ \(fieldName, field') -> do
|
|
let fieldPath = appendPath fieldName path
|
|
(fieldName,) <$> case field' of
|
|
AFNodeId x qt pkeys -> pure $ AFNodeId x qt pkeys
|
|
AFColumn c -> pure $ AFColumn c
|
|
AFObjectRelation annRel ->
|
|
AFObjectRelation <$> transformAnnRelation (transformObjectSelect fieldPath) annRel
|
|
AFArrayRelation (ASSimple annRel) ->
|
|
AFArrayRelation . ASSimple <$> transformAnnRelation (transformSelect fieldPath) annRel
|
|
AFArrayRelation (ASAggregate aggRel) ->
|
|
AFArrayRelation . ASAggregate <$> transformAnnRelation (transformAggregateSelect fieldPath) aggRel
|
|
AFArrayRelation (ASConnection annRel) ->
|
|
AFArrayRelation . ASConnection <$> transformAnnRelation (transformConnectionSelect fieldPath) annRel
|
|
AFComputedField x computedField ->
|
|
AFComputedField x <$> case computedField of
|
|
CFSScalar cfss cbe -> pure $ CFSScalar cfss cbe
|
|
CFSTable jas annSel -> CFSTable jas <$> transformSelect fieldPath annSel
|
|
AFRemote rs -> pure $ AFRemote rs
|
|
AFExpression t -> pure $ AFExpression t
|
|
-- TODO: implement this
|
|
AFDBRemote _ -> error "FIXME"
|
|
|
|
case NE.nonEmpty remoteJoins of
|
|
Nothing -> pure transformedFields
|
|
Just nonEmptyRemoteJoins -> do
|
|
let phantomColumns = map (\ci -> (fromCol @b $ pgiColumn ci, AFColumn $ AnnColumnField ci False Nothing Nothing)) $ toList $ HS.unions $ map fst $ remoteJoins
|
|
modify (Map.insert path $ fmap snd nonEmptyRemoteJoins)
|
|
pure $ transformedFields <> phantomColumns
|
|
|
|
where
|
|
getFields f = mapMaybe (sequence . second (^? f))
|
|
|
|
transformAnnRelation f (AnnRelationSelectG name maps select) = do
|
|
transformedSelect <- f select
|
|
pure $ AnnRelationSelectG name maps transformedSelect
|
|
|
|
|
|
mapToNonEmpty :: RemoteJoinMap -> Maybe RemoteJoins
|
|
mapToNonEmpty = NE.nonEmpty . Map.toList
|