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 - console: add support for MS SQL Server
- server: Prohibit Invalid slashes, duplicate variables, subscriptions for REST endpoints - server: Prohibit Invalid slashes, duplicate variables, subscriptions for REST endpoints
- server: Prohibit non-singular query definitions 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 ## 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-one or ``object relationships`` (e.g. ``author``).
- one-to-many or ``array relationships`` (e.g. ``articles``). - 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:
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 ``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. 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 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: .. _pg_manual_obj_relationship:
2. Manual configuration 3. Manual configuration
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
This is an advanced feature which is mostly used to define relationships on or 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-one or ``object relationships`` (e.g. ``author``).
- one-to-many or ``array relationships`` (e.g. ``articles``). - 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:
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 ``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. 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 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: .. _manual_obj_relationship:
2. Manual configuration 3. Manual configuration
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
This is an advanced feature which is mostly used to define relationships on or This is an advanced feature which is mostly used to define relationships on or

View File

@ -306,7 +306,6 @@ RelationshipName
String String
.. _table_config: .. _table_config:
Table Config Table Config
@ -507,11 +506,11 @@ ObjRelUsing
- Description - Description
* - foreign_key_constraint_on * - foreign_key_constraint_on
- false - false
- :ref:`PGColumn <PGColumn>` - :ref:`ObjRelUsingChoice <ObjRelUsingChoice>`
- The column with foreign key constraint - The column with foreign key constraint or the remote table and column
* - manual_configuration * - manual_configuration
- false - false
- ObjRelUsingManualMapping_ - :ref:`ObjRelUsingManualMapping <ObjRelUsingManualMapping>`
- Manual mapping of table and columns - Manual mapping of table and columns
.. note:: .. note::
@ -519,6 +518,39 @@ ObjRelUsing
There has to be at least one and only one of ``foreign_key_constraint_on`` There has to be at least one and only one of ``foreign_key_constraint_on``
and ``manual_configuration``. 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 ObjRelUsingManualMapping
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
@ -538,6 +570,28 @@ ObjRelUsingManualMapping
- true - true
- Object (:ref:`PGColumn` : :ref:`PGColumn`) - Object (:ref:`PGColumn` : :ref:`PGColumn`)
- Mapping of columns from current table to remote table - 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: .. _ArrRelUsing:

View File

@ -94,7 +94,7 @@ convColRhs tableQual = \case
bExps = map (mkFieldCompExp tableQual colFld) opExps bExps = map (mkFieldCompExp tableQual colFld) opExps
return $ foldr (S.BEBin S.AndOp) (S.BELit True) bExps 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 -- Convert the where clause on the relationship
curVarNum <- get curVarNum <- get
put $ curVarNum + 1 put $ curVarNum + 1

View File

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

View File

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

View File

@ -85,19 +85,21 @@ objRelP2Setup
:: (QErrM m, Backend b) :: (QErrM m, Backend b)
=> SourceName => SourceName
-> TableName b -> TableName b
-> HashSet (ForeignKey b) -> HashMap (TableName b) (HashSet (ForeignKey b))
-> RelDef (ObjRelUsing b) -> RelDef (ObjRelUsing b)
-> m (RelInfo b, [SchemaDependency]) -> m (RelInfo b, [SchemaDependency])
objRelP2Setup source qt foreignKeys (RelDef rn ru _) = case ru of objRelP2Setup source qt foreignKeys (RelDef rn ru _) = case ru of
RUManual rm -> do RUManual rm -> do
let refqt = rmTable rm let refqt = rmTable rm
(lCols, rCols) = unzip $ HM.toList $ rmColumns 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 mkDependency tableName reason col = SchemaDependency (SOSourceObj source $ SOITableObj tableName $ TOCol col) reason
dependencies = map (mkDependency qt DRLeftColumn) lCols dependencies = map (mkDependency qt DRLeftColumn) lCols
<> map (mkDependency refqt DRRightColumn) rCols <> map (mkDependency refqt DRRightColumn) rCols
pure (RelInfo rn ObjRel (rmColumns rm) refqt True True, dependencies) pure (RelInfo rn ObjRel (rmColumns rm) refqt True True io, dependencies)
RUFKeyOn columnName -> do RUFKeyOn (SameTable columnName) -> do
ForeignKey constraint foreignTable colMap <- getRequiredFkey columnName (HS.toList foreignKeys) foreignTableForeignKeys <- findTable qt foreignKeys
ForeignKey constraint foreignTable colMap <- getRequiredFkey columnName (HS.toList foreignTableForeignKeys)
let dependencies = let dependencies =
[ SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOForeignKey (_cName constraint)) DRFkey [ SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOForeignKey (_cName constraint)) DRFkey
, SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol columnName) DRUsingColumn , 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 -- 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 -- we are marking some as non-nullable here. This should really be done by
-- checking nullability in the SQL schema. -- 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 arrRelP2Setup
:: (QErrM m, Backend b) :: (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 (lCols, rCols) = unzip $ HM.toList $ rmColumns rm
deps = map (\c -> SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol c) DRLeftColumn) lCols deps = map (\c -> SchemaDependency (SOSourceObj source $ SOITableObj qt $ TOCol c) DRLeftColumn) lCols
<> map (\c -> SchemaDependency (SOSourceObj source $ SOITableObj refqt $ TOCol c) DRRightColumn) rCols <> 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 RUFKeyOn (ArrRelUsingFKeyOn refqt refCol) -> do
foreignTableForeignKeys <- findTable refqt foreignKeys foreignTableForeignKeys <- findTable refqt foreignKeys
let keysThatReferenceUs = filter ((== qt) . _fkForeignTable) (HS.toList foreignTableForeignKeys) 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 , SchemaDependency (SOSourceObj source $ SOITable refqt) DRRemoteTable
] ]
mapping = HM.fromList $ map swap $ HM.toList colMap 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 purgeRelDep
:: (QErrM m) :: (QErrM m)
@ -175,3 +186,19 @@ getRequiredFkey col fkeys =
"more than one foreign key constraint exists on the given column" "more than one foreign key constraint exists on the given column"
where where
filteredFkeys = filter ((== [col]) . HM.keys . _fkColumnMapping) fkeys 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 let SourceMetadata source tables functions _ = sourceMetadata
tablesMetadata = OMap.elems tables tablesMetadata = OMap.elems tables
(tableInputs, nonColumnInputs, permissions) = unzip3 $ map mkTableInputs tablesMetadata (tableInputs, nonColumnInputs, permissions) = unzip3 $ map mkTableInputs tablesMetadata
eventTriggers :: [(TableName b, [EventTriggerConf])] = map (_tmTable &&& (OMap.elems . _tmEventTriggers)) tablesMetadata eventTriggers = map (_tmTable &&& (OMap.elems . _tmEventTriggers)) tablesMetadata
-- HashMap k a -> HashMap k b -> HashMap k (a, b)
alignTableMap :: HashMap (TableName b) a -> HashMap (TableName b) c -> HashMap (TableName b) (a, c) alignTableMap :: HashMap (TableName b) a -> HashMap (TableName b) c -> HashMap (TableName b) (a, c)
alignTableMap = M.intersectionWith (,) alignTableMap = M.intersectionWith (,)
metadataInvalidationKey = Inc.selectD #_ikMetadata invalidationKeys metadataInvalidationKey = Inc.selectD #_ikMetadata invalidationKeys

View File

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

View File

@ -606,7 +606,7 @@ recreateSystemMetadata = do
, arrayRel $$(nonEmptyText "logs") $ RUFKeyOn $ , arrayRel $$(nonEmptyText "logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "event_invocation_logs") "event_id" ] ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "event_invocation_logs") "event_id" ]
, table "hdb_catalog" "event_invocation_logs" , 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" []
, table "hdb_catalog" "hdb_function_agg" , table "hdb_catalog" "hdb_function_agg"
[ objectRel $$(nonEmptyText "return_table_info") $ manualConfig "hdb_catalog" "hdb_table" [ 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" ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_events") "trigger_name"
] ]
, table "hdb_catalog" "hdb_cron_events" , 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 $ , arrayRel $$(nonEmptyText "cron_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_event_invocation_logs") "event_id" ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_event_invocation_logs") "event_id"
] ]
, table "hdb_catalog" "hdb_cron_event_invocation_logs" , 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" , table "hdb_catalog" "hdb_scheduled_events"
[ arrayRel $$(nonEmptyText "scheduled_event_logs") $ RUFKeyOn $ [ arrayRel $$(nonEmptyText "scheduled_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_scheduled_event_invocation_logs") "event_id" ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_scheduled_event_invocation_logs") "event_id"
] ]
, table "hdb_catalog" "hdb_scheduled_event_invocation_logs" , 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 objectRel name using = Left $ RelDef (RelName name) using Nothing
arrayRel name using = Right $ RelDef (RelName name) using Nothing arrayRel name using = Right $ RelDef (RelName name) using Nothing
manualConfig schemaName tableName columns = 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) = updateObjRelDef (oldQT, newQT) =
rdUsing %~ \case rdUsing %~ \case
RUFKeyOn fk -> RUFKeyOn fk RUFKeyOn fk -> RUFKeyOn fk
RUManual (RelManualConfig origQT rmCols) -> RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = bool origQT newQT $ oldQT == origQT 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 :: RenameTable b -> ArrRelDef b -> ArrRelDef b
updateArrRelDef (oldQT, newQT) = updateArrRelDef (oldQT, newQT) =
@ -178,9 +178,9 @@ updateRelDefs source qt rn renameTable = do
RUFKeyOn (ArrRelUsingFKeyOn origQT c) -> RUFKeyOn (ArrRelUsingFKeyOn origQT c) ->
let updQT = getUpdQT origQT let updQT = getUpdQT origQT
in RUFKeyOn $ ArrRelUsingFKeyOn updQT c in RUFKeyOn $ ArrRelUsingFKeyOn updQT c
RUManual (RelManualConfig origQT rmCols) -> RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = getUpdQT origQT let updQT = getUpdQT origQT
in RUManual $ RelManualConfig updQT rmCols in RUManual $ RelManualConfig updQT rmCols rmIO
where where
getUpdQT origQT = bool origQT newQT $ oldQT == origQT getUpdQT origQT = bool origQT newQT $ oldQT == origQT
@ -407,8 +407,22 @@ updateColInObjRel
:: (Backend b) :: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> ObjRelUsing b -> ObjRelUsing b => TableName b -> TableName b -> RenameCol b -> ObjRelUsing b -> ObjRelUsing b
updateColInObjRel fromQT toQT rnCol = \case updateColInObjRel fromQT toQT rnCol = \case
RUFKeyOn col -> RUFKeyOn $ getNewCol rnCol fromQT col RUFKeyOn c ->
RUManual manConfig -> RUManual $ updateRelManualConfig fromQT toQT rnCol manConfig 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 updateColInArrRel
:: (Backend b) :: (Backend b)
@ -432,9 +446,9 @@ updateRelManualConfig
:: (Backend b) :: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> RelManualConfig b -> RelManualConfig b => TableName b -> TableName b -> RenameCol b -> RelManualConfig b -> RelManualConfig b
updateRelManualConfig fromQT toQT rnCol manConfig = updateRelManualConfig fromQT toQT rnCol manConfig =
RelManualConfig tn $ updateColMap fromQT toQT rnCol colMap RelManualConfig tn (updateColMap fromQT toQT rnCol colMap) io
where where
RelManualConfig tn colMap = manConfig RelManualConfig tn colMap io = manConfig
updateColMap updateColMap
:: (Backend b) :: (Backend b)

View File

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

View File

@ -10,6 +10,8 @@ module Hasura.RQL.Types.Common
, FieldName(..) , FieldName(..)
, InsertOrder(..)
, ToAesonPairs(..) , ToAesonPairs(..)
, EquatableGType(..) , EquatableGType(..)
@ -76,7 +78,9 @@ import qualified Hasura.Backends.Postgres.SQL.Types as PG
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.Incremental (Cacheable) import Hasura.Incremental (Cacheable)
import Hasura.RQL.DDL.Headers () import Hasura.RQL.DDL.Headers ()
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Error import Hasura.RQL.Types.Error
import Hasura.SQL.Backend (BackendType)
import Hasura.SQL.Types import Hasura.SQL.Types
newtype RelName newtype RelName
@ -130,6 +134,46 @@ instance Q.FromCol RelType where
"array" -> Just ArrRel "array" -> Just ArrRel
_ -> Nothing _ -> 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> -- | Postgres OIDs. <https://www.postgresql.org/docs/12/datatype-oid.html>
newtype OID = OID { unOID :: Int } newtype OID = OID { unOID :: Int }
deriving (Show, Eq, NFData, Hashable, ToJSON, FromJSON, Q.FromCol, Cacheable) deriving (Show, Eq, NFData, Hashable, ToJSON, FromJSON, Q.FromCol, Cacheable)

View File

@ -40,6 +40,7 @@ data RelManualConfig (b :: BackendType)
= RelManualConfig = RelManualConfig
{ rmTable :: !(TableName b) { rmTable :: !(TableName b)
, rmColumns :: !(HashMap (Column b) (Column b)) , rmColumns :: !(HashMap (Column b) (Column b))
, rmInsertOrder :: !(Maybe InsertOrder)
} deriving (Generic) } deriving (Generic)
deriving instance Backend b => Eq (RelManualConfig b) deriving instance Backend b => Eq (RelManualConfig b)
deriving instance Backend b => Show (RelManualConfig b) deriving instance Backend b => Show (RelManualConfig b)
@ -50,14 +51,16 @@ instance (Backend b) => FromJSON (RelManualConfig b) where
RelManualConfig RelManualConfig
<$> v .: "remote_table" <$> v .: "remote_table"
<*> v .: "column_mapping" <*> v .: "column_mapping"
<*> v .:? "insertion_order"
parseJSON _ = parseJSON _ =
fail "manual_configuration should be an object" fail "manual_configuration should be an object"
instance (Backend b) => ToJSON (RelManualConfig b) where instance (Backend b) => ToJSON (RelManualConfig b) where
toJSON (RelManualConfig qt cm) = toJSON (RelManualConfig qt cm io) =
object [ "remote_table" .= qt object [ "remote_table" .= qt
, "column_mapping" .= cm , "column_mapping" .= cm
, "insertion_order" .= io
] ]
data RelUsing (b :: BackendType) a data RelUsing (b :: BackendType) a
@ -123,12 +126,34 @@ instance (ToAesonPairs a, Backend b) => ToJSON (WithTable b a) where
toJSON (WithTable sourceName tn rel) = toJSON (WithTable sourceName tn rel) =
object $ ("source" .= sourceName):("table" .= tn):toAesonPairs 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 ArrRelUsing b = RelUsing b (ArrRelUsingFKeyOn b)
type ArrRelDef b = RelDef (ArrRelUsing b) type ArrRelDef b = RelDef (ArrRelUsing b)
type CreateArrRel b = WithTable b (ArrRelDef 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 ObjRelDef b = RelDef (ObjRelUsing b)
type CreateObjRel b = WithTable b (ObjRelDef b) type CreateObjRel b = WithTable b (ObjRelDef b)
@ -201,6 +226,7 @@ data RelInfo (b :: BackendType)
, riRTable :: !(TableName b) , riRTable :: !(TableName b)
, riIsManual :: !Bool , riIsManual :: !Bool
, riIsNullable :: !Bool , riIsNullable :: !Bool
, riInsertOrder :: !InsertOrder
} deriving (Generic) } deriving (Generic)
deriving instance Backend b => Show (RelInfo b) deriving instance Backend b => Show (RelInfo b)
deriving instance Backend b => Eq (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, published_on TIMESTAMP,
tags JSON tags JSON
); );
create table author_detail(
id integer primary key references author(id),
phone text
);
CREATE FUNCTION fetch_articles(search text, author_row author) CREATE FUNCTION fetch_articles(search text, author_row author)
RETURNS SETOF article AS $$ RETURNS SETOF article AS $$
@ -43,6 +47,11 @@ args:
schema: public schema: public
name: article name: article
- type: track_table
args:
schema: public
name: author_detail
#Create relationships #Create relationships
- type: create_object_relationship - type: create_object_relationship
args: args:
@ -68,3 +77,26 @@ args:
definition: definition:
function: fetch_articles function: fetch_articles
table_argument: author_row 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 schema: public
name: author 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 - type: run_sql
args: args:
sql: | sql: |

View File

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

View File

@ -292,6 +292,11 @@ class TestGraphqlInsertGeoJson:
# Skipping server upgrade tests for a few tests below # Skipping server upgrade tests for a few tests below
# Those tests capture bugs in the previous release # Those tests capture bugs in the previous release
class TestGraphqlNestedInserts: 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): def test_author_with_articles(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/author_with_articles.yaml") check_query_f(hge_ctx, self.dir() + "/author_with_articles.yaml")