server/postgres: Support scalar computed fields in remote joins

https://github.com/hasura/graphql-engine-mono/pull/1692

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
GitOrigin-RevId: fcef85910899859f7421cad554c022f8023965ea
This commit is contained in:
Rakesh Emmadi 2021-07-12 21:33:36 +05:30 committed by hasura-bot
parent effb221d97
commit a375f8c105
24 changed files with 523 additions and 140 deletions

View File

@ -3,6 +3,7 @@
## Next release
(Add entries below in the order of server, console, cli, docs, others)
- server: support scalar computed fields in remote joins (close #7101)
- server: Support computed fields in query filter (`where` argument) (close #7100)
- server: add a `$.detail.operation.request_mode` field to `http-log` which takes the values `"single"` or `"batched"` to log whether a GraphQL request was executed on its own or as part of a batch
- server: add `query` field to `http-log` and `websocket-log` in non-error cases

View File

@ -72,8 +72,8 @@ Args syntax
- Object with table name and schema
* - hasura_fields
- true
- [:ref:`PGColumn <PGColumn>`]
- Column(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
- [:ref:`PGColumn <PGColumn>` | :ref:`ComputedFieldName <ComputedFieldName>`]
- Column/Computed field(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
* - remote_schema
- true
- :ref:`RemoteSchemaName <RemoteSchemaName>`
@ -192,4 +192,4 @@ Args syntax
* - name
- true
- :ref:`RemoteRelationshipName`
- Name of the remote relationship
- Name of the remote relationship

View File

@ -74,8 +74,8 @@ Args syntax
- Object with table name and schema
* - hasura_fields
- true
- [:ref:`PGColumn <PGColumn>`]
- Column(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
- [:ref:`PGColumn <PGColumn>` | :ref:`ComputedFieldName <ComputedFieldName>`]
- Column/Computed field(s) in the table that is used for joining with remote schema field. All join keys in ``remote_field`` must appear here.
* - remote_schema
- true
- :ref:`RemoteSchemaName <RemoteSchemaName>`
@ -194,4 +194,4 @@ Args syntax
* - name
- true
- :ref:`RemoteRelationshipName`
- Name of the remote relationship
- Name of the remote relationship

View File

@ -57,7 +57,7 @@ Computed fields whose associated SQL function returns a
Let's say we have the following schema:
.. code-block:: plpgsql
authors(id integer, first_name text, last_name text)
:ref:`Define an SQL function <create_sql_functions>` called ``author_full_name``:
@ -109,9 +109,9 @@ The return table must be tracked to define such a computed field.
Let's say we have the following schema:
.. code-block:: plpgsql
authors(id integer, first_name text, last_name text)
articles(id integer, title text, content text, author_id integer)
Now we can define a :ref:`table relationship <table_relationships>` on the ``authors``
@ -345,3 +345,77 @@ The value of generated columns is also computed from other columns of a table. P
come with their own limitations. Hasura's computed fields are defined via an SQL function, which allows users
to define any complex business logic in a function. Generated columns will go together with computed fields where
Hasura treats generated columns as normal Postgres columns.
Computed fields in Remote relationships
---------------------------------------
Using computed fields in :doc:`Remote relationships <remote-relationships/index>` allows transformation of data
from table columns before joining with data from remote sources. For instance, suppose we want to extract certain
field from a ``json`` column and join it with a field in a remote schema by argument value. We would define a computed
field which returns a scalar type of the field value in the ``json`` column and use it to join the graphql field of
the remote schema. Consider the following Postgres schema.
.. thumbnail:: /img/graphql/core/databases/postgres/schema/computed-fields-remote-relationship.png
.. code-block:: plpgsql
CREATE TABLE "user" (id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, address json NOT NULL);
-- SQL function returns city of a "user" using "->>" json operator
CREATE FUNCTION get_city(table_row "user")
RETURNS TEXT AS $$
SELECT table_row.address ->> 'city'
$$ LANGUAGE sql STABLE;
Now, let's track the table and add computed field ``user_city`` using the SQL function ``get_city``. Consider the
following remote schema.
.. code-block:: graphql
type Query {
get_coordinates(city: String): Coordinates
}
type Coordinates{
lat: Float
long: Float
}
:ref:`Define a remote relationship<create_remote_relationship>` with name ``user_location`` from ``user_city``
scalar computed field to ``get_coordinates`` remote schema field. We can query users with the pincode of their residing place.
.. graphiql::
:view_only:
:query:
query {
user {
id
name
user_city
user_location
}
}
:response:
{
"data": {
"authors": [
{
"id": 1,
"name": "Alice",
"user_city": "Frisco",
"user_location": {
"lat": 33.155373,
"long": -96.818733
}
}
]
}
}
.. note::
Only ``Scalar computed fields`` are allowed to join fields from remote sources
.. admonition:: Supported from
This feature is available in ``v2.0.1`` and above

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -901,7 +901,7 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
processArrayRelation (mkSourcePrefixes arrRelSourcePrefix) fieldName arrRelAlias arrSel
pure $ S.mkQIdenExp arrRelSourcePrefix fieldName
AFComputedField _ (CFSScalar scalar caseBoolExpMaybe) -> do
AFComputedField _ _ (CFSScalar scalar caseBoolExpMaybe) -> do
computedFieldSQLExp <- fromScalarComputedField scalar
-- The computed field is conditionally outputed depending
-- on the presence of `caseBoolExpMaybe` and the value it
@ -915,7 +915,7 @@ processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
$ _accColCaseBoolExpField <$> caseBoolExp
in pure $ S.SECond boolExp computedFieldSQLExp S.SENull
AFComputedField _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
AFComputedField _ _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
let computedFieldSourcePrefix =
mkComputedFieldTableAlias sourcePrefix fieldName
(selectSource, nodeExtractors) <-

View File

@ -47,7 +47,7 @@ import qualified Hasura.Tracing as Tracing
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.GraphQL.ParameterizedQueryHash
import Hasura.GraphQL.Parser.Column (UnpreparedValue)
import Hasura.GraphQL.Parser.Column (UnpreparedValue (..))
import Hasura.GraphQL.Parser.Directives
import Hasura.GraphQL.Parser.Monad
import Hasura.GraphQL.RemoteServer (execRemoteGQ)

View File

@ -43,6 +43,7 @@ import Data.Has
import Data.IORef
import Data.Set (Set)
import Data.Text.Extended
import Data.Text.NonEmpty
import qualified Hasura.Backends.Postgres.SQL.DML as S
import qualified Hasura.Backends.Postgres.Translate.Select as RS
@ -243,7 +244,7 @@ resolveAsyncActionQuery userInfo annAction =
AsyncTypename t -> RS.AFExpression t
AsyncOutput annFields ->
let inputTableArgument = RS.AETableRow $ Just $ Identifier "response_payload"
in RS.AFComputedField ()
in RS.AFComputedField () (ComputedFieldName $$(nonEmptyText "__action_computed_field"))
$ RS.CFSTable jsonAggSelect
$ processOutputSelectionSet inputTableArgument outputType definitionList annFields stringifyNumerics

View File

@ -19,6 +19,7 @@ import qualified Data.List.NonEmpty as NE
import Control.Lens
import Hasura.GraphQL.Execute.RemoteJoin.Types
import Hasura.GraphQL.Parser.Column (UnpreparedValue (..))
import Hasura.RQL.IR
import Hasura.RQL.IR.Returning
import Hasura.RQL.Types
@ -51,9 +52,9 @@ import Hasura.RQL.Types
-- | 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)
:: (Backend b)
=> QueryDB b r (UnpreparedValue b)
-> (QueryDB b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoins = \case
QDBMultipleRows s -> first QDBMultipleRows $ getRemoteJoinsSelect s
QDBSingleRow s -> first QDBSingleRow $ getRemoteJoinsSelect s
@ -62,33 +63,33 @@ getRemoteJoins = \case
-- | Traverse through 'AnnSimpleSel' and collect remote join fields (if any).
getRemoteJoinsSelect
:: Backend b
=> AnnSimpleSelectG b r u
-> (AnnSimpleSelectG b (Const Void) u, Maybe RemoteJoins)
:: (Backend b)
=> AnnSimpleSelectG b r (UnpreparedValue b)
-> (AnnSimpleSelectG b (Const Void) (UnpreparedValue b), 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)
:: (Backend b)
=> AnnAggregateSelectG b r (UnpreparedValue b)
-> (AnnAggregateSelectG b (Const Void) (UnpreparedValue b), 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)
:: (Backend b)
=> ConnectionSelect b r (UnpreparedValue b)
-> (ConnectionSelect b (Const Void) (UnpreparedValue b), 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)
:: (Backend b)
=> MutationOutputG b r (UnpreparedValue b)
-> (MutationOutputG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoinsMutationOutput =
second mapToNonEmpty . flip runState mempty . transformMutationOutput mempty
where
@ -110,16 +111,16 @@ getRemoteJoinsMutationOutput =
-- local helpers
getRemoteJoinsAnnFields
:: Backend b
=> AnnFieldsG b r u
-> (AnnFieldsG b (Const Void) u, Maybe RemoteJoins)
:: (Backend b)
=> AnnFieldsG b r (UnpreparedValue b)
-> (AnnFieldsG b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoinsAnnFields =
second mapToNonEmpty . flip runState mempty . transformAnnFields mempty
getRemoteJoinsMutationDB
:: Backend b
=> MutationDB b r u
-> (MutationDB b (Const Void) u, Maybe RemoteJoins)
:: (Backend b)
=> MutationDB b r (UnpreparedValue b)
-> (MutationDB b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoinsMutationDB = \case
MDBInsert insert ->
first MDBInsert $ getRemoteJoinsInsert insert
@ -144,16 +145,16 @@ getRemoteJoinsMutationDB = \case
getRemoteJoinsSyncAction
:: (Backend b)
=> AnnActionExecution b r v
-> (AnnActionExecution b (Const Void) v, Maybe RemoteJoins)
=> AnnActionExecution b r (UnpreparedValue b)
-> (AnnActionExecution b (Const Void) (UnpreparedValue b), 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)
=> ActionQuery b r (UnpreparedValue b)
-> (ActionQuery b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoinsActionQuery = \case
AQQuery sync ->
first AQQuery $ getRemoteJoinsSyncAction sync
@ -179,19 +180,18 @@ getRemoteJoinsActionQuery = \case
getRemoteJoinsActionMutation
:: (Backend b)
=> ActionMutation b r v
-> (ActionMutation b (Const Void) v, Maybe RemoteJoins)
=> ActionMutation b r (UnpreparedValue b)
-> (ActionMutation b (Const Void) (UnpreparedValue b), Maybe RemoteJoins)
getRemoteJoinsActionMutation = \case
AMSync sync ->
first AMSync $ getRemoteJoinsSyncAction sync
AMSync sync -> first AMSync $ getRemoteJoinsSyncAction sync
AMAsync async -> (AMAsync async, Nothing)
transformSelect
:: Backend b
:: (Backend b)
=> FieldPath
-> AnnSimpleSelectG b r u
-> State RemoteJoinMap (AnnSimpleSelectG b (Const Void) u)
-> AnnSimpleSelectG b r (UnpreparedValue b)
-> State RemoteJoinMap (AnnSimpleSelectG b (Const Void) (UnpreparedValue b))
transformSelect path sel = do
let fields = _asnFields sel
-- Transform selects in array, object and computed fields
@ -199,10 +199,10 @@ transformSelect path sel = do
pure sel{_asnFields = transformedFields}
transformAggregateSelect
:: Backend b
:: (Backend b)
=> FieldPath
-> AnnAggregateSelectG b r u
-> State RemoteJoinMap (AnnAggregateSelectG b (Const Void) u)
-> AnnAggregateSelectG b r (UnpreparedValue b)
-> State RemoteJoinMap (AnnAggregateSelectG b (Const Void) (UnpreparedValue b))
transformAggregateSelect path sel = do
let aggFields = _asnFields sel
transformedFields <- forM aggFields $ \(fieldName, aggField) ->
@ -213,10 +213,10 @@ transformAggregateSelect path sel = do
pure sel{_asnFields = transformedFields}
transformConnectionSelect
:: Backend b
:: (Backend b)
=> FieldPath
-> ConnectionSelect b r u
-> State RemoteJoinMap (ConnectionSelect b (Const Void) u)
-> ConnectionSelect b r (UnpreparedValue b)
-> State RemoteJoinMap (ConnectionSelect b (Const Void) (UnpreparedValue b))
transformConnectionSelect path ConnectionSelect{..} = do
let connectionFields = _asnFields _csSelect
transformedFields <- forM connectionFields $ \(fieldName, field) ->
@ -236,21 +236,21 @@ transformConnectionSelect path ConnectionSelect{..} = do
EdgeNode <$> transformAnnFields (appendPath fieldName edgePath) annFields
transformObjectSelect
:: Backend b
:: (Backend b)
=> FieldPath
-> AnnObjectSelectG b r u
-> State RemoteJoinMap (AnnObjectSelectG b (Const Void) u)
-> AnnObjectSelectG b r (UnpreparedValue b)
-> State RemoteJoinMap (AnnObjectSelectG b (Const Void) (UnpreparedValue b))
transformObjectSelect path sel = do
let fields = _aosFields sel
transformedFields <- transformAnnFields path fields
pure sel{_aosFields = transformedFields}
transformAnnFields
:: forall b r u
. Backend b
:: forall b r
. (Backend b)
=> FieldPath
-> AnnFieldsG b r u
-> State RemoteJoinMap (AnnFieldsG b (Const Void) u)
-> AnnFieldsG b r (UnpreparedValue b)
-> State RemoteJoinMap (AnnFieldsG b (Const Void) (UnpreparedValue b))
transformAnnFields path fields = do
-- TODO: Check for correctness. I think this entire function seems to be
@ -258,15 +258,21 @@ transformAnnFields path fields = do
-- 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
let columnsInSelSet = HS.fromList $ map (pgiColumn . _acfInfo . snd) $ getFields _AFColumn fields
scalarComputedFieldsInSelSet = HS.fromList $ map ((^. _2) . snd) $ getFields _AFComputedField 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)
let RemoteSelect argsMap selSet hasuraFields remoteFields rsi = remoteSelect
hasuraFieldNames = HS.map dbJoinFieldToName hasuraFields
-- See Note [Phantom fields in Remote Joins]
fieldPresentInSelection = \case
JoinColumn columnInfo -> HS.member (pgiColumn columnInfo) columnsInSelSet
JoinComputedField computedFieldInfo -> HS.member (_scfName computedFieldInfo) scalarComputedFieldsInSelSet
phantomFields = HS.filter (not . fieldPresentInSelection) hasuraFields
phantomFieldNames = toList $ HS.map dbJoinFieldToName phantomFields
in (phantomFields, RemoteJoin fieldName argsMap selSet hasuraFieldNames remoteFields rsi phantomFieldNames)
transformedFields <- forM fields $ \(fieldName, field') -> do
let fieldPath = appendPath fieldName path
@ -281,8 +287,8 @@ transformAnnFields path fields = do
AFArrayRelation . ASAggregate <$> transformAnnRelation (transformAggregateSelect fieldPath) aggRel
AFArrayRelation (ASConnection annRel) ->
AFArrayRelation . ASConnection <$> transformAnnRelation (transformConnectionSelect fieldPath) annRel
AFComputedField x computedField ->
AFComputedField x <$> case computedField of
AFComputedField x n computedField ->
AFComputedField x n <$> case computedField of
CFSScalar cfss cbe -> pure $ CFSScalar cfss cbe
CFSTable jas annSel -> CFSTable jas <$> transformSelect fieldPath annSel
AFRemote rs -> pure $ AFRemote rs
@ -293,9 +299,13 @@ transformAnnFields path fields = do
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
let annotatedPhantomFields = (toList $ HS.unions $ map fst remoteJoins) <&> \phantomField ->
(dbJoinFieldToName phantomField,) $ case phantomField of
JoinColumn columnInfo -> AFColumn $ AnnColumnField columnInfo False Nothing Nothing
JoinComputedField computedFieldInfo -> mkScalarComputedFieldSelect computedFieldInfo
modify (Map.insert path $ fmap snd nonEmptyRemoteJoins)
pure $ transformedFields <> phantomColumns
pure $ transformedFields <> annotatedPhantomFields
where
getFields f = mapMaybe (sequence . second (^? f))
@ -304,6 +314,23 @@ transformAnnFields path fields = do
transformedSelect <- f select
pure $ AnnRelationSelectG name maps transformedSelect
mkScalarComputedFieldSelect :: ScalarComputedField b -> (AnnFieldG b (Const Void) (UnpreparedValue b))
mkScalarComputedFieldSelect ScalarComputedField{..} =
let functionArgs = flip FunctionArgsExp mempty
$ functionArgsWithTableRowAndSession _scfTableArgument _scfSessionArgument
fieldSelect = flip CFSScalar Nothing
$ ComputedFieldScalarSelect _scfFunction functionArgs _scfType Nothing
in AFComputedField _scfXField _scfName fieldSelect
where
functionArgsWithTableRowAndSession
:: FunctionTableArgument
-> Maybe FunctionSessionArgument
-> [ArgumentExp b (UnpreparedValue b)]
functionArgsWithTableRowAndSession _ Nothing = [AETableRow Nothing] -- No session argument
functionArgsWithTableRowAndSession (FTAFirst) _ = [AETableRow Nothing, AESession UVSession]
functionArgsWithTableRowAndSession (FTANamed _ 0) _ = [AETableRow Nothing, AESession UVSession] -- Index is 0 implies table argument is first
functionArgsWithTableRowAndSession _ _ = [AESession UVSession, AETableRow Nothing]
mapToNonEmpty :: RemoteJoinMap -> Maybe RemoteJoins
mapToNonEmpty = NE.nonEmpty . Map.toList

View File

@ -22,6 +22,40 @@ newtype FieldPath = FieldPath {unFieldPath :: [FieldName]}
appendPath :: FieldName -> FieldPath -> FieldPath
appendPath fieldName = FieldPath . (<> [fieldName]) . unFieldPath
{- Note [Phantom fields in Remote Joins]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Usually, a join appears when we establish a relationship between two entities.
In this case, a remote join is a relationship between a table from a database of
any source to a remote GraphQL schema. The relationship is defined as native
fields (column or scalar computed field) acts as input argument for a field in the
remote schema. In order to make a call to remote schema we need to have values of
the table fields. The fields may or may not be present in the actual selection set.
If they aren't present, we need to fetch them explicitly from the database by inserting
them in the generated SQL statement. Thus they're called phantom fields. In post-fetching
joining process we need to remove the phantom fields and send the response to client.
Limitation for scalar computed fields:
--------------------------------------
We need to ensure a scalar computed field is to be included as phantom field if not
present in the query selection set. But if the SQL function associated with the scalar
computed field has input arguments other than table row and hasura session inputs, we
cannot determine their values. Hence, we only accept scalar computed fields with no
input arguments except table row and hasura session arguments in forming remote relationships.
Example: Let's say we have a computed field 'calculate_something' whose SQL function accepts
input argument 'xfactor: Integer' other than table row input and hasura session argument.
A remote relationship 'something_from_calculated' is defined and included in query selection set.
The 'calculate_something' is absent in the selection set, so we need to include it in the phantom
fields. Now, what's the value we should consider for the 'xfactor' input argument? So, we do
restrict these scalar computed fields in forming the remote relationships at metadata API level.
Solution:
--------
A potential solution for aforementioned limitation is to update the create_remote_relationship
metadata API to accept the computed fields with values of input arguments other than table row
and hasura session arguments.
-}
-- | A 'RemoteJoin' represents the context of remote relationship to be extracted from 'AnnFieldG's.
data RemoteJoin
= RemoteJoin
@ -33,9 +67,8 @@ data RemoteJoin
, _rjRemoteSchema :: !RemoteSchemaInfo -- ^ The remote schema server info.
, _rjPhantomFields :: ![FieldName]
-- ^ Hasura fields which are not in the selection set, but are required as
-- parameters to satisfy the remote join.
-- parameters to satisfy the remote join. See Note [Phantom fields in Remote Joins].
} deriving (Eq)
type RemoteJoins = NE.NonEmpty (FieldPath, NE.NonEmpty RemoteJoin)
type RemoteJoinMap = Map.HashMap FieldPath (NE.NonEmpty RemoteJoin)

View File

@ -1043,7 +1043,7 @@ computedFieldPG sourceName ComputedFieldInfo{..} parentTable selectPermissions =
fieldArgsParser = do
args <- functionArgsParser
colOp <- jsonPathArg $ ColumnScalar scalarReturnType
pure $ IR.AFComputedField _cfiXComputedFieldInfo
pure $ IR.AFComputedField _cfiXComputedFieldInfo _cfiName
(IR.CFSScalar (IR.ComputedFieldScalarSelect
{ IR._cfssFunction = _cffName _cfiFunction
, IR._cfssType = scalarReturnType
@ -1061,7 +1061,7 @@ computedFieldPG sourceName ComputedFieldInfo{..} parentTable selectPermissions =
let fieldArgsParser = liftA2 (,) functionArgsParser selectArgsParser
pure $ P.subselection fieldName (Just fieldDescription) fieldArgsParser selectionSetParser <&>
\((functionArgs', args), fields) ->
IR.AFComputedField _cfiXComputedFieldInfo $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG
IR.AFComputedField _cfiXComputedFieldInfo _cfiName $ IR.CFSTable JASMultipleRows $ IR.AnnSelectG
{ IR._asnFields = fields
, IR._asnFrom = IR.FromFunction (_cffName _cfiFunction) functionArgs' Nothing
, IR._asnPerm = tablePermissionsInfo remotePerms
@ -1112,7 +1112,7 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
hoistMaybe $ Map.lookup remoteSchemaName remoteRelationshipQueryCtx
let fieldDefns = map P.fDefinition (piQuery parsedIntrospection)
role <- askRoleName
let hasuraFieldNames = Set.map (FieldName . toTxt . pgiColumn) hasuraFields
let hasuraFieldNames = Set.map dbJoinFieldToName hasuraFields
remoteRelationship = RemoteRelationship name source table hasuraFieldNames remoteSchemaName remoteFields
(newInpValDefns, remoteFieldParamMap) <-
if | role == adminRoleName ->
@ -1121,10 +1121,10 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
-- was created
pure (remoteSchemaInputValueDefns, _rfiParamMap remoteFieldInfo)
| otherwise -> do
fieldInfoMap <- (_tciFieldInfoMap . _tiCoreInfo) <$> askTableInfo @b source table
roleRemoteField <-
afold @(Either _) $
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, roleIntrospectionResult) $
Set.toList hasuraFields
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, roleIntrospectionResult) fieldInfoMap
pure $ (_rfiInputValueDefinitions roleRemoteField, _rfiParamMap roleRemoteField)
let RemoteSchemaIntrospection typeDefns = irDoc roleIntrospectionResult
-- add the new input value definitions created by the remote relationship
@ -1153,7 +1153,7 @@ remoteRelationshipField remoteFieldInfo = runMaybeT do
pure $ IR.AFRemote $ IR.RemoteSelect
{ _rselArgs = remoteArgs
, _rselSelection = selSet
, _rselHasuraColumns = _rfiHasuraFields remoteFieldInfo
, _rselHasuraFields = _rfiHasuraFields remoteFieldInfo
, _rselFieldCall = unRemoteFields $ _rfiRemoteFields remoteFieldInfo
, _rselRemoteSchema = _rfiRemoteSchema remoteFieldInfo
}

View File

@ -87,18 +87,18 @@ buildRemoteFieldInfo
:: forall m b
. (Backend b, QErrM m)
=> RemoteRelationship b
-> [ColumnInfo b]
-> FieldInfoMap (FieldInfo b)
-> RemoteSchemaMap
-> m (RemoteFieldInfo b, [SchemaDependency])
buildRemoteFieldInfo remoteRelationship
pgColumns
fields
remoteSchemaMap = do
let remoteSchemaName = rtrRemoteSchema remoteRelationship
(RemoteSchemaCtx _name introspectionResult remoteSchemaInfo _ _ _permissions) <-
onNothing (Map.lookup remoteSchemaName remoteSchemaMap)
$ throw400 RemoteSchemaError $ "remote schema with name " <> remoteSchemaName <<> " not found"
eitherRemoteField <- runExceptT $
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) pgColumns
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) fields
remoteField <- onLeft eitherRemoteField $ throw400 RemoteSchemaError . errorToText
let table = rtrTable remoteRelationship
source = rtrSource remoteRelationship
@ -108,17 +108,13 @@ buildRemoteFieldInfo remoteRelationship
$ AB.mkAnyBackend
$ SOITable @b table)
DRTable
columnsDep =
map
(flip SchemaDependency DRRemoteRelationship
. SOSourceObj source
. AB.mkAnyBackend
. SOITableObj @b table
. TOCol @b
. pgiColumn)
$ S.toList $ _rfiHasuraFields remoteField
fieldsDep = S.toList (_rfiHasuraFields remoteField) <&> \case
JoinColumn columnInfo ->
mkColDep @b DRRemoteRelationship source table $ pgiColumn columnInfo
JoinComputedField computedFieldInfo ->
mkComputedFieldDep @b DRRemoteRelationship source table $ _scfName computedFieldInfo
remoteSchemaDep =
SchemaDependency (SORemoteSchema remoteSchemaName) DRRemoteSchema
in (tableDep : remoteSchemaDep : columnsDep)
in (tableDep : remoteSchemaDep : fieldsDep)
pure (remoteField, schemaDependencies)

View File

@ -18,6 +18,7 @@ import Data.Text.Extended
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.RemoteRelationship
import Hasura.RQL.Types.RemoteSchema
import Hasura.RQL.Types.SchemaCache
@ -34,13 +35,16 @@ data ValidationError (b :: BackendType)
| TypeNotFound !G.Name
| TableNotFound !(TableName b)
| TableFieldNonexistent !(TableName b) !FieldName
| TableFieldNotSupported !FieldName
| TableComputedFieldWithInputArgs !FieldName !(FunctionName b)
| ExpectedTypeButGot !G.GType !G.GType
| InvalidType !G.GType !Text
| InvalidVariable !G.Name !(HM.HashMap G.Name (ColumnInfo b))
| InvalidVariable !G.Name !(HM.HashMap G.Name (DBJoinField b))
| NullNotAllowedHere
| InvalidGTypeForStripping !G.GType
| UnsupportedMultipleElementLists
| UnsupportedEnum
| UnsupportedTableComputedField !(TableName b) !ComputedFieldName
| InvalidGraphQLName !Text
| IDTypeJoin !G.Name
-- | This is the case where the type of the columns that are mapped do not
@ -70,6 +74,11 @@ errorToText = \case
"table with name " <> name <<> " not found"
TableFieldNonexistent table fieldName ->
"field with name " <> fieldName <<> " not found in table " <>> table
TableFieldNotSupported fieldName ->
"field with name " <> fieldName <<> " not supported; only columns and scalar computed fields"
TableComputedFieldWithInputArgs fieldName function ->
"computed field " <> fieldName <<> " is associated with SQL function " <> function
<<> " has input arguments other than table row type and hasura session"
ExpectedTypeButGot expTy actualTy ->
"expected type " <> G.getBaseType expTy <<> " but got " <>> G.getBaseType actualTy
InvalidType ty err ->
@ -84,6 +93,8 @@ errorToText = \case
"multiple elements in list value is not supported"
UnsupportedEnum ->
"enum value is not supported"
UnsupportedTableComputedField tableName fieldName ->
"computed field " <> fieldName <<> " returns set of " <> tableName <<> ", is not supported"
InvalidGraphQLName t ->
t <<> " is not a valid GraphQL identifier"
IDTypeJoin typeName ->
@ -99,26 +110,34 @@ validateRemoteRelationship
. (Backend b, MonadError (ValidationError b) m)
=> RemoteRelationship b
-> (RemoteSchemaInfo, IntrospectionResult)
-> [ColumnInfo b]
-> FieldInfoMap (FieldInfo b)
-> m (RemoteFieldInfo b)
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) pgColumns = do
validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionResult) fields = do
let remoteSchemaName = rtrRemoteSchema remoteRelationship
table = rtrTable remoteRelationship
hasuraFields <- forM (toList $ rtrHasuraFields remoteRelationship) $
\fieldName -> onNothing (find ((==) fieldName . fromCol @b . pgiColumn) pgColumns) $
throwError $ TableFieldNonexistent table fieldName
pgColumnsVariables <- mapM (\(k,v) -> do
variableName <- pgColumnToVariable k
pure $ (variableName,v)
) $ HM.toList (mapFromL pgiColumn pgColumns)
let pgColumnsVariablesMap = HM.fromList pgColumnsVariables
hasuraFields <- forM (toList $ rtrHasuraFields remoteRelationship) $ \fieldName -> do
fieldInfo <- onNothing (HM.lookup fieldName fields) $ throwError $ TableFieldNonexistent table fieldName
case fieldInfo of
FIColumn columnInfo -> pure $ JoinColumn columnInfo
FIComputedField ComputedFieldInfo{..} -> do
scalarType <- case _cfiReturnType of
CFRScalar ty -> pure ty
CFRSetofTable{} -> throwError $ UnsupportedTableComputedField table _cfiName
let ComputedFieldFunction{..} = _cfiFunction
case toList _cffInputArgs of
[] -> pure $ JoinComputedField $ ScalarComputedField _cfiXComputedFieldInfo _cfiName _cffName
_cffTableArgument _cffSessionArgument scalarType
_ -> throwError $ TableComputedFieldWithInputArgs fieldName _cffName
_ -> throwError $ TableFieldNotSupported fieldName
hasuraFieldsVariablesMap <-
fmap HM.fromList $ for hasuraFields $ \field -> (, field) <$> hasuraFieldToVariable field
let schemaDoc = irDoc introspectionResult
queryRootName = irQueryRoot introspectionResult
queryRoot <- onNothing (lookupObject schemaDoc queryRootName) $
throwError $ FieldNotFoundInRemoteSchema queryRootName
(_, (leafParamMap, leafTypeMap)) <-
foldlM
(buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc)
(buildRelationshipTypeInfo hasuraFieldsVariablesMap schemaDoc)
(queryRoot, (mempty, mempty))
(unRemoteFields $ rtrRemoteField remoteRelationship)
pure $ RemoteFieldInfo
@ -153,7 +172,7 @@ validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionRe
_ -> False
buildRelationshipTypeInfo
:: HashMap G.Name (ColumnInfo b)
:: HashMap G.Name (DBJoinField b)
-> RemoteSchemaIntrospection
-> (G.ObjectTypeDefinition RemoteSchemaInputValueDefinition,
( (HashMap G.Name RemoteSchemaInputValueDefinition)
@ -162,13 +181,13 @@ validateRemoteRelationship remoteRelationship (remoteSchemaInfo, introspectionRe
-> m ( G.ObjectTypeDefinition RemoteSchemaInputValueDefinition
, ( HashMap G.Name RemoteSchemaInputValueDefinition
, HashMap G.Name (G.TypeDefinition [G.Name] RemoteSchemaInputValueDefinition)))
buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc (objTyInfo,(_,typeMap)) fieldCall = do
buildRelationshipTypeInfo hasuraFieldsVariablesMap schemaDoc (objTyInfo,(_,typeMap)) fieldCall = do
objFldDefinition <- lookupField (fcName fieldCall) objTyInfo
let providedArguments = getRemoteArguments $ fcArguments fieldCall
(validateRemoteArguments
(mapFromL (G._ivdName . _rsitdDefinition) (G._fldArgumentsDefinition objFldDefinition))
providedArguments
pgColumnsVariablesMap
hasuraFieldsVariablesMap
schemaDoc)
let eitherParamAndTypeMap =
runStateT
@ -326,13 +345,15 @@ renameNamedType rename =
G.unsafeMkName . rename . G.unName
-- | Convert a field name to a variable name.
pgColumnToVariable
hasuraFieldToVariable
:: (Backend b, MonadError (ValidationError b) m)
=> (Column b)
=> (DBJoinField b)
-> m G.Name
pgColumnToVariable pgCol =
let pgColText = toTxt pgCol
in G.mkName pgColText `onNothing` throwError (InvalidGraphQLName pgColText)
hasuraFieldToVariable hasuraField = do
let fieldText = case hasuraField of
JoinColumn columnInfo -> toTxt $ pgiColumn columnInfo
JoinComputedField computedFieldInfo -> toTxt $ _scfName computedFieldInfo
G.mkName fieldText `onNothing` throwError (InvalidGraphQLName fieldText)
-- | Lookup the field in the schema.
lookupField
@ -354,7 +375,7 @@ validateRemoteArguments
:: (Backend b, MonadError (ValidationError b) m)
=> HM.HashMap G.Name RemoteSchemaInputValueDefinition
-> HM.HashMap G.Name (G.Value G.Name)
-> HM.HashMap G.Name (ColumnInfo b)
-> HM.HashMap G.Name (DBJoinField b)
-> RemoteSchemaIntrospection
-> m ()
validateRemoteArguments expectedArguments providedArguments permittedVariables schemaDocument = do
@ -376,7 +397,7 @@ unwrapGraphQLType = \case
-- | Validate a value against a type.
validateType
:: (Backend b, MonadError (ValidationError b) m)
=> HM.HashMap G.Name (ColumnInfo b)
=> HM.HashMap G.Name (DBJoinField b)
-> G.Value G.Name
-> G.GType
-> RemoteSchemaIntrospection
@ -387,7 +408,7 @@ validateType permittedVariables value expectedGType schemaDocument =
case HM.lookup variable permittedVariables of
Nothing -> throwError (InvalidVariable variable permittedVariables)
Just fieldInfo -> do
namedType <- columnInfoToNamedType fieldInfo
namedType <- dbJoinFieldToNamedType fieldInfo
isTypeCoercible (mkGraphQLType namedType) expectedGType
G.VInt {} -> do
let intScalarGType = mkGraphQLType intScalar
@ -477,17 +498,21 @@ assertListType actualType =
(throwError $ InvalidType actualType "is not a list type")
-- | Convert a field info to a named type, if possible.
columnInfoToNamedType
dbJoinFieldToNamedType
:: forall b m .
(Backend b, MonadError (ValidationError b) m)
=> ColumnInfo b
=> DBJoinField b
-> m G.Name
columnInfoToNamedType pci =
case pgiType pci of
ColumnScalar scalarType ->
onLeft (scalarTypeGraphQLName @b scalarType)
(const $ throwError $ CannotGenerateGraphQLTypeName scalarType)
_ -> throwError UnsupportedEnum
dbJoinFieldToNamedType hasuraField = do
scalarType <- case hasuraField of
JoinColumn pci -> case pgiType pci of
ColumnScalar scalarType -> pure scalarType
_ -> throwError UnsupportedEnum
JoinComputedField cfi -> pure $ _scfType cfi
-- CFRScalar scalarType -> pure scalarType
-- CFRSetofTable table -> throwError $ UnsupportedTableComputedField table $ _cfiName cfi
onLeft (scalarTypeGraphQLName @b scalarType) $
const $ throwError $ CannotGenerateGraphQLTypeName scalarType
getBaseTyWithNestedLevelsCount :: G.GType -> (G.Name, Int)
getBaseTyWithNestedLevelsCount ty = go ty 0

View File

@ -69,12 +69,18 @@ addNonColumnFields = proc ( source
buildComputedField
-< (HS.fromList $ M.keys rawTableInfo, map (source, pgFunctions, _nctiTable,) _nctiComputedFields)
let columnsAndComputedFields =
let columnFields = columns <&> FIColumn
computedFields = M.fromList $ flip map (M.toList computedFieldInfos) $
\(cfName, (cfInfo, _)) -> (fromComputedField cfName, FIComputedField cfInfo)
in M.union columnFields computedFields
rawRemoteRelationshipInfos
<- buildInfoMapPreservingMetadata
(_rrmName . (^. _3))
(mkRemoteRelationshipMetadataObject @b)
buildRemoteRelationship
-< ((M.elems columns, remoteSchemaMap), map (source, _nctiTable,) _nctiRemoteRelationships)
-< ((columnsAndComputedFields, remoteSchemaMap), map (source, _nctiTable,) _nctiRemoteRelationships)
let relationshipFields = mapKeys fromRel relationshipInfos
computedFieldFields = mapKeys fromComputedField computedFieldInfos
@ -251,7 +257,7 @@ buildRemoteRelationship
:: forall b arr m
. ( ArrowChoice arr, ArrowWriter (Seq CollectedInfo) arr
, ArrowKleisli m arr, MonadError QErr m, BackendMetadata b)
=> ( ([ColumnInfo b], RemoteSchemaMap)
=> ( (FieldInfoMap (FieldInfo b), RemoteSchemaMap)
, (SourceName, TableName b, RemoteRelationshipMetadata)
) `arr` Maybe (RemoteFieldInfo b)
buildRemoteRelationship = proc ( (pgColumns, remoteSchemaMap)

View File

@ -45,6 +45,7 @@ import Hasura.RQL.IR.OrderBy
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.Function
import Hasura.RQL.Types.Instances ()
import Hasura.RQL.Types.Relationship
@ -186,7 +187,7 @@ data AnnFieldG (b :: BackendType) (r :: BackendType -> Type) v
= AFColumn !(AnnColumnField b v)
| AFObjectRelation !(ObjectRelationSelectG b r v)
| AFArrayRelation !(ArraySelectG b r v)
| AFComputedField !(XComputedField b) !(ComputedFieldSelect b r v)
| AFComputedField !(XComputedField b) !ComputedFieldName !(ComputedFieldSelect b r v)
| AFRemote !(RemoteSelect b)
| AFDBRemote !(AB.AnyBackend (DBRemoteSelect b r))
| AFNodeId !(XRelay b) !(TableName b) !(PrimaryKeyColumns b)
@ -210,7 +211,6 @@ mkAnnColumnFieldAsText
mkAnnColumnFieldAsText ci =
AFColumn (AnnColumnField ci True Nothing Nothing)
-- Aggregation fields
data TableAggregateFieldG (b :: BackendType) (r :: BackendType -> Type) v
@ -270,7 +270,6 @@ type ConnectionFields b r v = Fields (ConnectionField b r v)
type PageInfoFields = Fields PageInfoField
type EdgeFields b r v = Fields (EdgeField b r v)
-- Column
data AnnColumnField (b :: BackendType) v
@ -372,11 +371,11 @@ data RemoteFieldArgument
data RemoteSelect (b :: BackendType)
= RemoteSelect
{ _rselArgs :: ![RemoteFieldArgument]
, _rselSelection :: !(G.SelectionSet G.NoFragments RemoteSchemaVariable)
, _rselHasuraColumns :: !(HashSet (ColumnInfo b))
, _rselFieldCall :: !(NonEmpty FieldCall)
, _rselRemoteSchema :: !RemoteSchemaInfo
{ _rselArgs :: ![RemoteFieldArgument]
, _rselSelection :: !(G.SelectionSet G.NoFragments RemoteSchemaVariable)
, _rselHasuraFields :: !(HashSet (DBJoinField b))
, _rselFieldCall :: !(NonEmpty FieldCall)
, _rselRemoteSchema :: !RemoteSchemaInfo
}

View File

@ -3,6 +3,9 @@ module Hasura.RQL.Types.RemoteRelationship
, remoteRelationshipNameToText
, fromRemoteRelationship
, RemoteFields(..)
, ScalarComputedField(..)
, DBJoinField(..)
, dbJoinFieldToName
, RemoteFieldInfo(..)
, RemoteRelationship(..)
, RemoteRelationshipDef(..)
@ -17,22 +20,23 @@ module Hasura.RQL.Types.RemoteRelationship
import Hasura.Prelude
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import qualified Language.GraphQL.Draft.Syntax as G
import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import qualified Language.GraphQL.Draft.Syntax as G
import Control.Lens (makeLenses)
import Control.Lens (makeLenses)
import Data.Aeson
import Data.Aeson.TH
import Data.Scientific
import Data.Text.Extended
import Data.Text.NonEmpty
import Hasura.Incremental (Cacheable)
import Hasura.Incremental (Cacheable)
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.RemoteSchema
import Hasura.SQL.Backend
@ -50,6 +54,48 @@ remoteRelationshipNameToText = unNonEmptyText . unRemoteRelationshipName
fromRemoteRelationship :: RemoteRelationshipName -> FieldName
fromRemoteRelationship = FieldName . remoteRelationshipNameToText
data ScalarComputedField (b :: BackendType)
= ScalarComputedField
{ _scfXField :: !(XComputedField b)
, _scfName :: !ComputedFieldName
, _scfFunction :: !(FunctionName b)
, _scfTableArgument :: !FunctionTableArgument
, _scfSessionArgument :: !(Maybe FunctionSessionArgument)
, _scfType :: !(ScalarType b)
} deriving (Generic)
deriving instance Backend b => Eq (ScalarComputedField b)
deriving instance Backend b => Show (ScalarComputedField b)
instance Backend b => Cacheable (ScalarComputedField b)
instance Backend b => Hashable (ScalarComputedField b)
instance Backend b => ToJSON (ScalarComputedField b) where
toJSON ScalarComputedField{..} =
object [ "name" .= _scfName
, "function" .= _scfFunction
, "table_argument" .= _scfTableArgument
, "session_argument" .= _scfSessionArgument
, "type" .= _scfType
]
data DBJoinField (b :: BackendType)
= JoinColumn !(ColumnInfo b)
| JoinComputedField !(ScalarComputedField b)
deriving (Generic)
deriving instance Backend b => Eq (DBJoinField b)
deriving instance Backend b => Show (DBJoinField b)
instance Backend b => Cacheable (DBJoinField b)
instance Backend b => Hashable (DBJoinField b)
instance (Backend b) => ToJSON (DBJoinField b) where
toJSON = \case
JoinColumn columnInfo -> toJSON columnInfo
JoinComputedField computedField -> toJSON computedField
dbJoinFieldToName :: forall b. (Backend b) => DBJoinField b -> FieldName
dbJoinFieldToName = \case
JoinColumn columnInfo -> fromCol @b $ pgiColumn $ columnInfo
JoinComputedField computedFieldInfo -> fromComputedField $ _scfName computedFieldInfo
-- | Resolved remote relationship
data RemoteFieldInfo (b :: BackendType)
= RemoteFieldInfo
@ -61,7 +107,7 @@ data RemoteFieldInfo (b :: BackendType)
-- include the arguments to the remote field that is being joined. The
-- names of the arguments here are modified, it will be in the format of
-- <Original Field Name>_remote_rel_<hasura table schema>_<hasura table name><remote relationship name>
, _rfiHasuraFields :: !(HashSet (ColumnInfo b))
, _rfiHasuraFields :: !(HashSet (DBJoinField b))
-- ^ Hasura fields used to join the remote schema node
, _rfiRemoteFields :: !RemoteFields
, _rfiRemoteSchema :: !RemoteSchemaInfo
@ -242,9 +288,9 @@ data RemoteRelationship b =
, rtrTable :: !(TableName b)
-- ^ (SourceName, QualifiedTable) determines the table on which the relationship
-- is defined
, rtrHasuraFields :: !(HashSet FieldName) -- TODO change to PGCol
, rtrHasuraFields :: !(HashSet FieldName)
-- ^ The hasura fields from 'rtrTable' that will be in scope when resolving
-- the remote objects in 'rtrRemoteField'.
-- the remote objects in 'rtrRemoteField'. Supports columns and computed fields.
, rtrRemoteSchema :: !RemoteSchemaName
-- ^ Identifier for this mapping.
, rtrRemoteField :: !RemoteFields

View File

@ -0,0 +1,21 @@
description: Fetch remote join involving a computed field
url: /v1/graphql
status: 200
query:
query: |
query {
students{
id
name
grade
}
}
response:
data:
students:
- id: 1
name: alice
grade: S
- id: 2
name: bob
grade: B

View File

@ -0,0 +1,24 @@
description: Fetch remote join involving a computed field with session argument
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: admin
x-hasura-offset: '10'
query:
query: |
query {
students{
id
name
grade_session
}
}
response:
data:
students:
- id: 1
name: alice
grade_session: A
- id: 2
name: bob
grade_session: B

View File

@ -35,6 +35,25 @@ args:
name text
);
insert into employees (name) values ('alice'),(NULL),('bob');
create table students (
id serial primary key,
name text not null,
physics integer,
maths integer
);
insert into students (name, physics, maths) values ('alice', 45, 48), ('bob', 32, 40);
create function total_marks(student_row students)
returns integer as $$
select student_row.physics + student_row.maths
$$ language sql stable;
create function total_marks_offset(student_row students, "offset" integer)
returns integer as $$
select student_row.physics + student_row.maths - "offset"
$$ language sql stable;
create function total_marks_session(student_row students, hasura_session json)
returns integer as $$
select student_row.physics + student_row.maths - (hasura_session ->> 'x-hasura-offset')::integer
$$ language sql stable;
- type: track_table
args:
@ -57,3 +76,33 @@ args:
args:
schema: public
name: employees
- type: track_table
args:
schema: public
name: students
- type: add_computed_field
args:
table: students
name: total_marks
definition:
function: total_marks
table_argument: student_row
- type: add_computed_field
args:
table: students
name: total_marks_offset
definition:
function: total_marks_offset
table_argument: student_row
- type: add_computed_field
args:
table: students
name: total_marks_session
definition:
function: total_marks_session
table_argument: student_row
session_argument: hasura_session

View File

@ -0,0 +1,11 @@
type: create_remote_relationship
args:
name: grade_offset
table: students
hasura_fields:
- total_marks_offset
remote_schema: my-remote-schema
remote_field:
getGrade:
arguments:
marks: "$total_marks_offset"

View File

@ -0,0 +1,25 @@
type: bulk
args:
- type: create_remote_relationship
args:
name: grade
table: students
hasura_fields:
- total_marks
remote_schema: my-remote-schema
remote_field:
getGrade:
arguments:
marks: "$total_marks"
- type: create_remote_relationship
args:
name: grade_session
table: students
hasura_fields:
- total_marks_session
remote_schema: my-remote-schema
remote_field:
getGrade:
arguments:
marks: "$total_marks_session"

View File

@ -7,6 +7,10 @@ args:
drop table if exists user_profiles;
drop table if exists authors;
drop table if exists employees;
drop function if exists total_marks(students);
drop function if exists total_marks_offset(students, integer);
drop function if exists total_marks_session(students, json);
drop table if exists students;
# also drops remote relationship as direct dep
- type: remove_remote_schema

View File

@ -79,6 +79,7 @@ const typeDefs = gql`
communications(id: Int): [Communication]
search(id: Int!): SearchResult
getOccupation(name: ID!): Occupation!
getGrade(marks: Int!): String
}
`;
@ -224,6 +225,17 @@ const resolvers = {
default:
throw new ApolloError("invalid argument - " + name, "invalid ");
}
},
getGrade: (_, { marks }) => {
if(marks > 90) {
return 'S'
}
else if (marks > 80) {
return 'A'
}
else {
return 'B'
}
}
},
Communication: {

View File

@ -66,6 +66,9 @@ class TestCreateRemoteRelationship:
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_with_enum.yaml')
assert st_code == 200, resp
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_computed_fields.yaml')
assert st_code == 200, resp
def test_create_invalid(self, hge_ctx):
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_hasura_field.yaml')
assert st_code == 400, resp
@ -94,6 +97,9 @@ class TestCreateRemoteRelationship:
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_array.yaml')
assert st_code == 400, resp
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_invalid_remote_rel_computed_field.yaml')
assert st_code == 400, resp
def test_generation(self, hge_ctx):
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_basic.yaml')
assert st_code == 200, resp
@ -361,3 +367,26 @@ class TestWithRelay:
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_basic.yaml')
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir() + "with_relay.yaml")
class TestComputedFieldsInRemoteRelationship:
@classmethod
def dir(cls):
return "queries/remote_schemas/remote_relationships/"
@pytest.fixture(autouse=True)
def transact(self, hge_ctx, graphql_service):
print("In setup method")
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup.yaml')
assert st_code == 200, resp
st_code, resp = hge_ctx.v1q_f(self.dir() + 'setup_remote_rel_computed_fields.yaml')
assert st_code == 200, resp
yield
st_code, resp = hge_ctx.v1q_f(self.dir() + 'teardown.yaml')
assert st_code == 200, resp
def test_remote_join_with_computed_field(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + 'remote_join_with_computed_field.yaml')
def test_remote_join_with_computed_field_session(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + 'remote_join_with_computed_field_session.yaml')