better handling for one-to-one relationships

Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
GitOrigin-RevId: 1bb5bc0c4ac8109ee1d20563d23cf98e0906a483
This commit is contained in:
Vladimir Ciobanu 2021-03-03 15:02:00 +02:00 committed by hasura-bot
parent 15ed0cf536
commit d5ff1acf2d
22 changed files with 469 additions and 65 deletions

View File

@ -35,6 +35,7 @@ keys in the response body.
- console: add support for MS SQL Server
- server: Prohibit Invalid slashes, duplicate variables, subscriptions for REST endpoints
- server: Prohibit non-singular query definitions for REST endpoints
- server: better handling for one-to-one relationships via both `manual_configuration` and `foreign_key_constraint_on` (#2576)
## v1.4.0-alpha.1

View File

@ -30,6 +30,16 @@ There are two kinds of relationships:
- one-to-one or ``object relationships`` (e.g. ``author``).
- one-to-many or ``array relationships`` (e.g. ``articles``).
The above represents the same table relationship from different perspectives:
there is a single ``author`` for every ``article`` (one-to-one), but there
may be multiple ``articles`` for every ``author`` (one-to-many).
A table relationship may be one-to-one from both perspectives. For
example, given tables ``author`` and ``author_details``, if the ``author_details``
table has a primary key ``author_id`` which is a foreign key to the
``author`` table's primary key ``id``. In this case there will be a single ``author``
for every ``author_details`` and a single ``details`` for every ``author``
.. _pg_create_object_relationship:
pg_create_object_relationship
@ -38,7 +48,7 @@ pg_create_object_relationship
``create_object_relationship`` is used to create an object relationship on a
table. There cannot be an existing column or relationship with the same name.
There are 2 ways in which you can create an object relationship.
There are 3 ways in which you can create an object relationship.
1. Using foreign key constraint on a column
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -64,10 +74,40 @@ Create an ``object relationship`` ``author`` on ``article`` *table*, *using* th
}
}
2. Using foreign key constraint on a remote table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Create an ``object relationship`` ``details`` on ``author`` *table*, *using* the
*foreign_key_constraint_on* the ``author`` *table*'s ``id`` *column*:
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "create_object_relationship",
"args": {
"table": "author",
"name": "details",
"using": {
"foreign_key_constraint_on" : {
"table": "author_details",
"column": "id"
}
}
}
}
.. admonition:: Supported from
Relationships via remote table are supported for versions ``v2.0.0-alpha.3`` and above.
.. _pg_manual_obj_relationship:
2. Manual configuration
3. Manual configuration
^^^^^^^^^^^^^^^^^^^^^^^
This is an advanced feature which is mostly used to define relationships on or

View File

@ -30,6 +30,16 @@ There are two kinds of relationships:
- one-to-one or ``object relationships`` (e.g. ``author``).
- one-to-many or ``array relationships`` (e.g. ``articles``).
The above represents the same table relationship from different perspectives:
there is a single ``author`` for every ``article`` (one-to-one), but there
may be multiple ``articles`` for every ``author`` (one-to-many).
A table relationship may be one-to-one from both perspectives. For
example, given tables ``author`` and ``author_details``, if the ``author_details``
table has a primary key ``author_id`` which is a foreign key to the
``author`` table's primary key ``id``. In this case there will be a single ``author``
for every ``author_details`` and a single ``details`` for every ``author``
.. _create_object_relationship:
create_object_relationship
@ -38,7 +48,7 @@ create_object_relationship
``create_object_relationship`` is used to create an object relationship on a
table. There cannot be an existing column or relationship with the same name.
There are 2 ways in which you can create an object relationship.
There are 3 ways in which you can create an object relationship.
1. Using foreign key constraint on a column
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -63,10 +73,40 @@ Create an ``object relationship`` ``author`` on ``article`` *table*, *using* th
}
}
2. Using foreign key constraint on a remote table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Create an ``object relationship`` ``details`` on ``author`` *table*, *using* the
*foreign_key_constraint_on* the ``author_details`` *table*'s ``id`` *column*:
.. code-block:: http
POST /v1/query HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "create_object_relationship",
"args": {
"table": "author",
"name": "details",
"using": {
"foreign_key_constraint_on" : {
"table": "author_details",
"column": "id"
}
}
}
}
.. admonition:: Supported from
Relationships via remote table are supported for versions ``v2.0.0-alpha.3`` and above.
.. _manual_obj_relationship:
2. Manual configuration
3. Manual configuration
^^^^^^^^^^^^^^^^^^^^^^^
This is an advanced feature which is mostly used to define relationships on or

View File

@ -306,7 +306,6 @@ RelationshipName
String
.. _table_config:
Table Config
@ -507,11 +506,11 @@ ObjRelUsing
- Description
* - foreign_key_constraint_on
- false
- :ref:`PGColumn <PGColumn>`
- The column with foreign key constraint
- :ref:`ObjRelUsingChoice <ObjRelUsingChoice>`
- The column with foreign key constraint or the remote table and column
* - manual_configuration
- false
- ObjRelUsingManualMapping_
- :ref:`ObjRelUsingManualMapping <ObjRelUsingManualMapping>`
- Manual mapping of table and columns
.. note::
@ -519,6 +518,39 @@ ObjRelUsing
There has to be at least one and only one of ``foreign_key_constraint_on``
and ``manual_configuration``.
.. _ObjRelUsingChoice:
ObjRelUsingChoice
^^^^^^^^^^^^^^^^^
.. parsed-literal::
:class: haskell-pre
SameTable_ | RemoteTable_
SameTable
^^^^^^^^^
.. parsed-literal::
PGColumn_
RemoteTable
^^^^^^^^^^^
.. parsed-literal::
:class: haskell-pre
{
"table" : TableName_,
"column" : PGColumn_
}
.. admonition:: Supported from
Supported in ``v2.0.0-alpha.3`` and above.
.. _ObjRelUsingManualMapping:
ObjRelUsingManualMapping
^^^^^^^^^^^^^^^^^^^^^^^^
@ -538,6 +570,28 @@ ObjRelUsingManualMapping
- true
- Object (:ref:`PGColumn` : :ref:`PGColumn`)
- Mapping of columns from current table to remote table
* - insertion_order
- false
- :ref:`InsertOrder`
- insertion order: before or after parent (default: before)
.. _InsertOrder:
InsertOrder
^^^^^^^^^^^
Describes when should the referenced table row be inserted in relation to the
current table row in case of a nested insert. Defaults to "before_parent".
.. parsed-literal::
:class: haskell-pre
"before_parent" | "after_parent"
.. admonition:: Supported from
Supported in ``v2.0.0-alpha.3`` and above.
.. _ArrRelUsing:

View File

@ -94,7 +94,7 @@ convColRhs tableQual = \case
bExps = map (mkFieldCompExp tableQual colFld) opExps
return $ foldr (S.BEBin S.AndOp) (S.BELit True) bExps
AVRel (RelInfo _ _ colMapping relTN _ _) nesAnn -> do
AVRel (RelInfo _ _ colMapping relTN _ _ _) nesAnn -> do
-- Convert the where clause on the relationship
curVarNum <- get
put $ curVarNum + 1

View File

@ -671,7 +671,7 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields orderByItems =
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ toIdentifier $ pgiColumn pgColInfo
AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do
let RelInfo relName _ colMapping relTable _ _ = relInfo
let RelInfo relName _ colMapping relTable _ _ _ = relInfo
relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
fieldName = mkOrderByFieldName relName
(relOrderByAlias, relOrdByExp) <-
@ -686,7 +686,7 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields orderByItems =
)
AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do
let RelInfo relName _ colMapping relTable _ _ = relInfo
let RelInfo relName _ colMapping relTable _ _ _ = relInfo
fieldName = mkOrderByFieldName relName
relSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias
similarArrayFields fieldName

View File

@ -8,6 +8,7 @@ import Hasura.Prelude
import qualified Data.Aeson as J
import qualified Data.Environment as Env
import qualified Data.HashMap.Strict as Map
import qualified Data.List as L
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Database.PG.Query as Q
@ -137,7 +138,8 @@ insertMultipleObjects env multiObjIns additionalColumns remoteJoinCtx mutationOu
mutOutputRJ stringifyNum [] $ (, remoteJoinCtx) <$> remoteJoins
insertObject
:: (HasVersion, MonadTx m, MonadIO m, Tracing.MonadTrace m)
:: forall m
. (HasVersion, MonadTx m, MonadIO m, Tracing.MonadTrace m)
=> Env.Environment
-> IR.SingleObjIns 'Postgres PG.SQLExp
-> [(PGCol, PG.SQLExp)]
@ -149,7 +151,7 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
validateInsert (map fst columns) (map IR._riRelInfo objectRels) (map fst additionalColumns)
-- insert all object relations and fetch this insert dependent column values
objInsRes <- forM objectRels $ insertObjRel env planVars remoteJoinCtx stringifyNum
objInsRes <- forM beforeInsert $ insertObjRel env planVars remoteJoinCtx stringifyNum
-- prepare final insert columns
let objRelAffRows = sum $ map fst objInsRes
@ -162,7 +164,7 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
PGE.mutateAndFetchCols table allColumns (PGT.MCCheckConstraint cte, planVars) stringifyNum
colValM <- asSingleObject colVals
arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null arrayRels
arrRelAffRows <- bool (withArrRels colValM) (return 0) $ null allAfterInsertRels
let totAffRows = objRelAffRows + affRows + arrRelAffRows
return (totAffRows, colValM)
@ -170,20 +172,49 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
IR.AnnIns annObj table onConflict checkCond allColumns defaultValues = singleObjIns
IR.AnnInsObj columns objectRels arrayRels = annObj
arrRelDepCols = flip getColInfos allColumns $
concatMap (Map.keys . riMapping . IR._riRelInfo) arrayRels
afterInsert, beforeInsert :: [IR.ObjRelIns 'Postgres PG.SQLExp]
(afterInsert, beforeInsert) =
L.partition ((== AfterParent) . riInsertOrder . IR._riRelInfo) objectRels
allAfterInsertRels :: [IR.ArrRelIns 'Postgres PG.SQLExp]
allAfterInsertRels = arrayRels <> map objToArr afterInsert
afterInsertDepCols :: [ColumnInfo 'Postgres]
afterInsertDepCols = flip getColInfos allColumns $
concatMap (Map.keys . riMapping . IR._riRelInfo) allAfterInsertRels
objToArr :: forall a b. IR.ObjRelIns b a -> IR.ArrRelIns b a
objToArr IR.RelIns {..} = IR.RelIns (singleToMulti _riAnnIns) _riRelInfo
singleToMulti :: forall a b. IR.SingleObjIns b a -> IR.MultiObjIns b a
singleToMulti IR.AnnIns {..} =
IR.AnnIns
[_aiInsObj]
_aiTableName
_aiConflictClause
_aiCheckCond
_aiTableCols
_aiDefVals
withArrRels
:: Maybe (ColumnValues 'Postgres TxtEncodedPGVal)
-> m Int
withArrRels colValM = do
colVal <- onNothing colValM $ throw400 NotSupported cannotInsArrRelErr
arrDepColsWithVal <- fetchFromColVals colVal arrRelDepCols
arrInsARows <- forM arrayRels $ insertArrRel env arrDepColsWithVal remoteJoinCtx planVars stringifyNum
afterInsertDepColsWithVal <- fetchFromColVals colVal afterInsertDepCols
arrInsARows <- forM allAfterInsertRels
$ insertArrRel env afterInsertDepColsWithVal remoteJoinCtx planVars stringifyNum
return $ sum arrInsARows
asSingleObject
:: [ColumnValues 'Postgres TxtEncodedPGVal]
-> m (Maybe (ColumnValues 'Postgres TxtEncodedPGVal))
asSingleObject = \case
[] -> pure Nothing
[r] -> pure $ Just r
_ -> throw500 "more than one row returned"
cannotInsArrRelErr :: Text
cannotInsArrRelErr =
"cannot proceed to insert array relations since insert to table "
<> table <<> " affects zero rows"

View File

@ -93,9 +93,15 @@ instance (Arbitrary a, Backend b) => Arbitrary (RelUsing b a) where
instance (Arbitrary a) => Arbitrary (RelDef a) where
arbitrary = genericArbitrary
instance Arbitrary InsertOrder where
arbitrary = genericArbitrary
instance (Backend b) => Arbitrary (RelManualConfig b) where
arbitrary = genericArbitrary
instance (Backend b) => Arbitrary (ObjRelUsingChoice b) where
arbitrary = genericArbitrary
instance (Backend b) => Arbitrary (ArrRelUsingFKeyOn b) where
arbitrary = genericArbitrary

View File

@ -85,19 +85,21 @@ objRelP2Setup
:: (QErrM m, Backend b)
=> SourceName
-> TableName b
-> HashSet (ForeignKey b)
-> HashMap (TableName b) (HashSet (ForeignKey b))
-> RelDef (ObjRelUsing b)
-> m (RelInfo b, [SchemaDependency])
objRelP2Setup source qt foreignKeys (RelDef rn ru _) = case ru of
RUManual rm -> do
let refqt = rmTable rm
(lCols, rCols) = unzip $ HM.toList $ rmColumns rm
io = fromMaybe BeforeParent $ rmInsertOrder rm
mkDependency tableName reason col = SchemaDependency (SOSourceObj source $ SOITableObj tableName $ TOCol col) reason
dependencies = map (mkDependency qt DRLeftColumn) lCols
<> map (mkDependency refqt DRRightColumn) rCols
pure (RelInfo rn ObjRel (rmColumns rm) refqt True True, dependencies)
RUFKeyOn columnName -> do
ForeignKey constraint foreignTable colMap <- getRequiredFkey columnName (HS.toList foreignKeys)
pure (RelInfo rn ObjRel (rmColumns rm) refqt True True io, dependencies)
RUFKeyOn (SameTable columnName) -> do
foreignTableForeignKeys <- findTable qt foreignKeys
ForeignKey constraint foreignTable colMap <- getRequiredFkey columnName (HS.toList foreignTableForeignKeys)
let dependencies =
[ SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOForeignKey (_cName constraint)) DRFkey
, SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol columnName) DRUsingColumn
@ -108,7 +110,16 @@ objRelP2Setup source qt foreignKeys (RelDef rn ru _) = case ru of
-- TODO(PDV?): this is too optimistic. Some object relationships are nullable, but
-- we are marking some as non-nullable here. This should really be done by
-- checking nullability in the SQL schema.
pure (RelInfo rn ObjRel colMap foreignTable False False, dependencies)
pure (RelInfo rn ObjRel colMap foreignTable False False BeforeParent, dependencies)
RUFKeyOn (RemoteTable remoteTable remoteCol) -> do
foreignTableForeignKeys <- findTable remoteTable foreignKeys
ForeignKey constraint _foreignTable colMap <- getRequiredRemoteFkey remoteCol (HS.toList foreignTableForeignKeys)
let dependencies =
[ SchemaDependency (SOSourceObj source $ SOITableObj remoteTable $ TOForeignKey (_cName constraint)) DRRemoteFkey
, SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol remoteCol) DRUsingColumn
, SchemaDependency (SOSourceObj source $ SOITable remoteTable) DRRemoteTable
]
pure (RelInfo rn ObjRel colMap remoteTable False False AfterParent, dependencies)
arrRelP2Setup
:: (QErrM m, Backend b)
@ -123,7 +134,7 @@ arrRelP2Setup foreignKeys source qt (RelDef rn ru _) = case ru of
(lCols, rCols) = unzip $ HM.toList $ rmColumns rm
deps = map (\c -> SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol c) DRLeftColumn) lCols
<> map (\c -> SchemaDependency (SOSourceObj source $ SOITableObj refqt $ TOCol c) DRRightColumn) rCols
pure (RelInfo rn ArrRel (rmColumns rm) refqt True True, deps)
pure (RelInfo rn ArrRel (rmColumns rm) refqt True True BeforeParent, deps)
RUFKeyOn (ArrRelUsingFKeyOn refqt refCol) -> do
foreignTableForeignKeys <- findTable refqt foreignKeys
let keysThatReferenceUs = filter ((== qt) . _fkForeignTable) (HS.toList foreignTableForeignKeys)
@ -136,7 +147,7 @@ arrRelP2Setup foreignKeys source qt (RelDef rn ru _) = case ru of
, SchemaDependency (SOSourceObj source $ SOITable refqt) DRRemoteTable
]
mapping = HM.fromList $ map swap $ HM.toList colMap
pure (RelInfo rn ArrRel mapping refqt False False, deps)
pure (RelInfo rn ArrRel mapping refqt False False BeforeParent, deps)
purgeRelDep
:: (QErrM m)
@ -175,3 +186,19 @@ getRequiredFkey col fkeys =
"more than one foreign key constraint exists on the given column"
where
filteredFkeys = filter ((== [col]) . HM.keys . _fkColumnMapping) fkeys
getRequiredRemoteFkey
:: QErrM m
=> Backend b
=> Column b
-> [ForeignKey b]
-> m (ForeignKey b)
getRequiredRemoteFkey col fkeys =
case filteredFkeys of
[] -> throw400 ConstraintError
"no foreign constraint exists on the given column"
[k] -> return k
_ -> throw400 ConstraintError
"more than one foreign key constraint exists on the given column"
where
filteredFkeys = filter ((== [col]) . HM.elems . _fkColumnMapping) fkeys

View File

@ -269,8 +269,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
let SourceMetadata source tables functions _ = sourceMetadata
tablesMetadata = OMap.elems tables
(tableInputs, nonColumnInputs, permissions) = unzip3 $ map mkTableInputs tablesMetadata
eventTriggers :: [(TableName b, [EventTriggerConf])] = map (_tmTable &&& (OMap.elems . _tmEventTriggers)) tablesMetadata
-- HashMap k a -> HashMap k b -> HashMap k (a, b)
eventTriggers = map (_tmTable &&& (OMap.elems . _tmEventTriggers)) tablesMetadata
alignTableMap :: HashMap (TableName b) a -> HashMap (TableName b) c -> HashMap (TableName b) (a, c)
alignTableMap = M.intersectionWith (,)
metadataInvalidationKey = Inc.selectD #_ikMetadata invalidationKeys

View File

@ -144,9 +144,7 @@ buildObjectRelationship
)
) `arr` Maybe (RelInfo b)
buildObjectRelationship = proc (fkeysMap, (source, table, relDef)) -> do
let buildRelInfo def = do
fkeys <- findTable table fkeysMap
objRelP2Setup source table fkeys def
let buildRelInfo def = objRelP2Setup source table fkeysMap def
buildRelationship -< (source, table, buildRelInfo, ObjRel, relDef)
buildArrayRelationship

View File

@ -606,7 +606,7 @@ recreateSystemMetadata = do
, arrayRel $$(nonEmptyText "logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "event_invocation_logs") "event_id" ]
, table "hdb_catalog" "event_invocation_logs"
[ objectRel $$(nonEmptyText "event") $ RUFKeyOn "event_id" ]
[ objectRel $$(nonEmptyText "event") $ RUFKeyOn $ SameTable "event_id" ]
, table "hdb_catalog" "hdb_function" []
, table "hdb_catalog" "hdb_function_agg"
[ objectRel $$(nonEmptyText "return_table_info") $ manualConfig "hdb_catalog" "hdb_table"
@ -634,19 +634,19 @@ recreateSystemMetadata = do
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_events") "trigger_name"
]
, table "hdb_catalog" "hdb_cron_events"
[ objectRel $$(nonEmptyText "cron_trigger") $ RUFKeyOn "trigger_name"
[ objectRel $$(nonEmptyText "cron_trigger") $ RUFKeyOn $ SameTable "trigger_name"
, arrayRel $$(nonEmptyText "cron_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_event_invocation_logs") "event_id"
]
, table "hdb_catalog" "hdb_cron_event_invocation_logs"
[ objectRel $$(nonEmptyText "cron_event") $ RUFKeyOn "event_id"
[ objectRel $$(nonEmptyText "cron_event") $ RUFKeyOn $ SameTable "event_id"
]
, table "hdb_catalog" "hdb_scheduled_events"
[ arrayRel $$(nonEmptyText "scheduled_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_scheduled_event_invocation_logs") "event_id"
]
, table "hdb_catalog" "hdb_scheduled_event_invocation_logs"
[ objectRel $$(nonEmptyText "scheduled_event") $ RUFKeyOn "event_id"
[ objectRel $$(nonEmptyText "scheduled_event") $ RUFKeyOn $ SameTable "event_id"
]
]
@ -658,4 +658,4 @@ recreateSystemMetadata = do
objectRel name using = Left $ RelDef (RelName name) using Nothing
arrayRel name using = Right $ RelDef (RelName name) using Nothing
manualConfig schemaName tableName columns =
RUManual $ RelManualConfig (QualifiedObject schemaName tableName) (HM.fromList columns)
RUManual $ RelManualConfig (QualifiedObject schemaName tableName) (HM.fromList columns) Nothing

View File

@ -168,9 +168,9 @@ updateRelDefs source qt rn renameTable = do
updateObjRelDef (oldQT, newQT) =
rdUsing %~ \case
RUFKeyOn fk -> RUFKeyOn fk
RUManual (RelManualConfig origQT rmCols) ->
RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = bool origQT newQT $ oldQT == origQT
in RUManual $ RelManualConfig updQT rmCols
in RUManual $ RelManualConfig updQT rmCols rmIO
updateArrRelDef :: RenameTable b -> ArrRelDef b -> ArrRelDef b
updateArrRelDef (oldQT, newQT) =
@ -178,9 +178,9 @@ updateRelDefs source qt rn renameTable = do
RUFKeyOn (ArrRelUsingFKeyOn origQT c) ->
let updQT = getUpdQT origQT
in RUFKeyOn $ ArrRelUsingFKeyOn updQT c
RUManual (RelManualConfig origQT rmCols) ->
RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = getUpdQT origQT
in RUManual $ RelManualConfig updQT rmCols
in RUManual $ RelManualConfig updQT rmCols rmIO
where
getUpdQT origQT = bool origQT newQT $ oldQT == origQT
@ -407,8 +407,22 @@ updateColInObjRel
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> ObjRelUsing b -> ObjRelUsing b
updateColInObjRel fromQT toQT rnCol = \case
RUFKeyOn col -> RUFKeyOn $ getNewCol rnCol fromQT col
RUManual manConfig -> RUManual $ updateRelManualConfig fromQT toQT rnCol manConfig
RUFKeyOn c ->
RUFKeyOn $ updateRelChoice fromQT toQT rnCol c
RUManual manConfig ->
RUManual $ updateRelManualConfig fromQT toQT rnCol manConfig
updateRelChoice
:: Backend b
=> TableName b
-> TableName b
-> RenameCol b
-> ObjRelUsingChoice b
-> ObjRelUsingChoice b
updateRelChoice fromQT toQT rnCol =
\case
SameTable col -> SameTable $ getNewCol rnCol fromQT col
RemoteTable t c -> RemoteTable t (getNewCol rnCol toQT c)
updateColInArrRel
:: (Backend b)
@ -432,9 +446,9 @@ updateRelManualConfig
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> RelManualConfig b -> RelManualConfig b
updateRelManualConfig fromQT toQT rnCol manConfig =
RelManualConfig tn $ updateColMap fromQT toQT rnCol colMap
RelManualConfig tn (updateColMap fromQT toQT rnCol colMap) io
where
RelManualConfig tn colMap = manConfig
RelManualConfig tn colMap io = manConfig
updateColMap
:: (Backend b)

View File

@ -76,7 +76,7 @@ convSelCol fieldInfoMap _ (SCExtRel rn malias selQ) = do
let pgWhenRelErr = "only relationships can be expanded"
relInfo <- withPathK "name" $
askRelType fieldInfoMap rn pgWhenRelErr
let (RelInfo _ _ _ relTab _ _) = relInfo
let (RelInfo _ _ _ relTab _ _ _) = relInfo
(rfim, rspi) <- fetchRelDet rn relTab
resolvedSelQ <- resolveStar rfim rspi selQ
return [ECRel rn malias resolvedSelQ]
@ -271,7 +271,7 @@ convExtRel fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do
-- Point to the name key
relInfo <- withPathK "name" $
askRelType fieldInfoMap relName pgWhenRelErr
let (RelInfo _ relTy colMapping relTab _ _) = relInfo
let (RelInfo _ relTy colMapping relTab _ _ _) = relInfo
(relCIM, relSPI) <- fetchRelDet relName relTab
annSel <- convSelectQ relTab relCIM relSPI selQ sessVarBldr prepValBldr
case relTy of

View File

@ -10,6 +10,8 @@ module Hasura.RQL.Types.Common
, FieldName(..)
, InsertOrder(..)
, ToAesonPairs(..)
, EquatableGType(..)
@ -76,7 +78,9 @@ import qualified Hasura.Backends.Postgres.SQL.Types as PG
import Hasura.EncJSON
import Hasura.Incremental (Cacheable)
import Hasura.RQL.DDL.Headers ()
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Error
import Hasura.SQL.Backend (BackendType)
import Hasura.SQL.Types
newtype RelName
@ -130,6 +134,46 @@ instance Q.FromCol RelType where
"array" -> Just ArrRel
_ -> Nothing
data InsertOrder = BeforeParent | AfterParent
deriving (Show, Eq, Generic)
instance NFData InsertOrder
instance Hashable InsertOrder
instance Cacheable InsertOrder
instance FromJSON InsertOrder where
parseJSON (String t)
| t == "before_parent" = pure BeforeParent
| t == "after_parent" = pure AfterParent
parseJSON _ =
fail "insertion_order should be 'before_parent' or 'after_parent'"
instance ToJSON InsertOrder where
toJSON = \case
BeforeParent -> String "before_parent"
AfterParent -> String "after_parent"
-- should this be parameterized by both the source and the destination backend?
data RelInfo (b :: BackendType)
= RelInfo
{ riName :: !RelName
, riType :: !RelType
, riMapping :: !(HashMap (Column b) (Column b))
, riRTable :: !(TableName b)
, riIsManual :: !Bool
, riIsNullable :: !Bool
, riInsertOrder :: !InsertOrder
} deriving (Generic)
deriving instance Backend b => Show (RelInfo b)
deriving instance Backend b => Eq (RelInfo b)
instance Backend b => NFData (RelInfo b)
instance Backend b => Cacheable (RelInfo b)
instance Backend b => Hashable (RelInfo b)
instance Backend b => FromJSON (RelInfo b) where
parseJSON = genericParseJSON hasuraJSON
instance Backend b => ToJSON (RelInfo b) where
toJSON = genericToJSON hasuraJSON
-- | Postgres OIDs. <https://www.postgresql.org/docs/12/datatype-oid.html>
newtype OID = OID { unOID :: Int }
deriving (Show, Eq, NFData, Hashable, ToJSON, FromJSON, Q.FromCol, Cacheable)

View File

@ -40,6 +40,7 @@ data RelManualConfig (b :: BackendType)
= RelManualConfig
{ rmTable :: !(TableName b)
, rmColumns :: !(HashMap (Column b) (Column b))
, rmInsertOrder :: !(Maybe InsertOrder)
} deriving (Generic)
deriving instance Backend b => Eq (RelManualConfig b)
deriving instance Backend b => Show (RelManualConfig b)
@ -50,14 +51,16 @@ instance (Backend b) => FromJSON (RelManualConfig b) where
RelManualConfig
<$> v .: "remote_table"
<*> v .: "column_mapping"
<*> v .:? "insertion_order"
parseJSON _ =
fail "manual_configuration should be an object"
instance (Backend b) => ToJSON (RelManualConfig b) where
toJSON (RelManualConfig qt cm) =
toJSON (RelManualConfig qt cm io) =
object [ "remote_table" .= qt
, "column_mapping" .= cm
, "insertion_order" .= io
]
data RelUsing (b :: BackendType) a
@ -123,12 +126,34 @@ instance (ToAesonPairs a, Backend b) => ToJSON (WithTable b a) where
toJSON (WithTable sourceName tn rel) =
object $ ("source" .= sourceName):("table" .= tn):toAesonPairs rel
data ObjRelUsingChoice b
= SameTable !(Column b)
| RemoteTable !(TableName b) !(Column b)
deriving (Generic)
deriving instance Backend b => Eq (ObjRelUsingChoice b)
deriving instance Backend b => Show (ObjRelUsingChoice b)
instance (Backend b) => Cacheable (ObjRelUsingChoice b)
instance (Backend b) => ToJSON (ObjRelUsingChoice b) where
toJSON = \case
SameTable col -> toJSON col
RemoteTable qt lcol ->
object
[ "table" .= qt
, "column" .= lcol
]
instance (Backend b) => FromJSON (ObjRelUsingChoice b) where
parseJSON = \case
v@(String _) -> SameTable <$> parseJSON v
Object o -> RemoteTable <$> o .: "table" <*> o .: "column"
_ -> fail "expected single column or columns/table"
type ArrRelUsing b = RelUsing b (ArrRelUsingFKeyOn b)
type ArrRelDef b = RelDef (ArrRelUsing b)
type CreateArrRel b = WithTable b (ArrRelDef b)
type ObjRelUsing b = RelUsing b (Column b)
type ObjRelUsing b = RelUsing b (ObjRelUsingChoice b)
type ObjRelDef b = RelDef (ObjRelUsing b)
type CreateObjRel b = WithTable b (ObjRelDef b)
@ -201,6 +226,7 @@ data RelInfo (b :: BackendType)
, riRTable :: !(TableName b)
, riIsManual :: !Bool
, riIsNullable :: !Bool
, riInsertOrder :: !InsertOrder
} deriving (Generic)
deriving instance Backend b => Show (RelInfo b)
deriving instance Backend b => Eq (RelInfo b)

View File

@ -0,0 +1,33 @@
description: Insert author and it's articles via nested mutation with manual object relationship
url: /v1/graphql
status: 200
query:
query: |-
mutation nested_author_insert {
insert_author_one (
object: {
name: "Author 3",
detail_manual: {
data: {
phone: "1234567890"
}
}
}
) {
id
name
detail_manual {
id
phone
}
}
}
response:
data:
insert_author_one:
id: 3
name: Author 3
detail_manual:
id: 3
phone: "1234567890"

View File

@ -0,0 +1,33 @@
description: Insert author and it's articles via nested mutation with manual object relationship
url: /v1/graphql
status: 200
query:
query: |-
mutation nested_author_insert {
insert_author_one (
object: {
name: "Author 3",
detail_fk: {
data: {
phone: "1234567890"
}
}
}
) {
id
name
detail_fk {
id
phone
}
}
}
response:
data:
insert_author_one:
id: 3
name: Author 3
detail_fk:
id: 3
phone: "1234567890"

View File

@ -21,6 +21,10 @@ args:
published_on TIMESTAMP,
tags JSON
);
create table author_detail(
id integer primary key references author(id),
phone text
);
CREATE FUNCTION fetch_articles(search text, author_row author)
RETURNS SETOF article AS $$
@ -43,6 +47,11 @@ args:
schema: public
name: article
- type: track_table
args:
schema: public
name: author_detail
#Create relationships
- type: create_object_relationship
args:
@ -68,3 +77,26 @@ args:
definition:
function: fetch_articles
table_argument: author_row
#Create relationships
- type: create_object_relationship
args:
table: author
name: detail_manual
using:
manual_configuration:
remote_table:
name: author_detail
schema: public
column_mapping:
id: id
insertion_order: after_parent
- type: create_object_relationship
args:
table: author
name: detail_fk
using:
foreign_key_constraint_on:
table: author_detail
column: id

View File

@ -8,6 +8,25 @@ args:
schema: public
name: author
- type: drop_relationship
args:
relationship: detail_manual
table:
schema: public
name: author
- type: drop_relationship
args:
relationship: detail_fk
table:
schema: public
name: author
- type: run_sql
args:
sql: |
drop table author_detail cascade
cascade: true
- type: run_sql
args:
sql: |

View File

@ -8,5 +8,7 @@ args:
delete from article;
SELECT setval('article_id_seq', 1, FALSE);
delete from author_detail;
delete from author;
SELECT setval('author_id_seq', 1, FALSE);

View File

@ -292,6 +292,11 @@ class TestGraphqlInsertGeoJson:
# Skipping server upgrade tests for a few tests below
# Those tests capture bugs in the previous release
class TestGraphqlNestedInserts:
def test_author_with_detail(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_with_detail.yaml")
def test_author_with_detail_fk(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_with_detail_fk.yaml")
def test_author_with_articles(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_with_articles.yaml")