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,23 +74,53 @@ 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
to views. We cannot rely on foreign key constraints as they are not valid to or
from views. So, when using manual configuration, we have to specify the remote
table and how columns in this table are mapped to the columns of the
remote table.
remote table.
Let's say we have a view called ``article_detail`` which has three columns
``article_id`` and ``view_count`` and ``average_rating``. We can now define an
object relationship called ``article_detail`` on the ``article`` table as
follows:
follows:
.. code-block:: http
POST /v1/metadata HTTP/1.1

View File

@ -30,15 +30,25 @@ 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
--------------------------
``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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -63,23 +73,53 @@ 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
to views. We cannot rely on foreign key constraints as they are not valid to or
from views. So, when using manual configuration, we have to specify the remote
table and how columns in this table are mapped to the columns of the
remote table.
remote table.
Let's say we have a view called ``article_detail`` which has three columns
``article_id`` and ``view_count`` and ``average_rating``. We can now define an
object relationship called ``article_detail`` on the ``article`` table as
follows:
follows:
.. code-block:: http
POST /v1/query HTTP/1.1
@ -146,7 +186,7 @@ create_array_relationship
-------------------------
``create_array_relationship`` is used to create an array 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 array relationship.
@ -260,7 +300,7 @@ drop_relationship
a table. If there are other objects dependent on this relationship like
permissions and query templates, etc., the request will fail and report the dependencies
unless ``cascade`` is set to ``true``. If ``cascade`` is set to ``true``, the
dependent objects are also dropped.
dependent objects are also dropped.
An example:
@ -314,7 +354,7 @@ set_relationship_comment
------------------------
``set_relationship_comment`` is used to set/update the comment on a
relationship. Setting the comment to ``null`` removes it.
relationship. Setting the comment to ``null`` removes it.
An example:

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

@ -38,8 +38,9 @@ instance (ToJSON a) => ToAesonPairs (RelDef a) where
data RelManualConfig (b :: BackendType)
= RelManualConfig
{ rmTable :: !(TableName b)
, rmColumns :: !(HashMap (Column b) (Column b))
{ 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)
@ -195,12 +220,13 @@ instance (Backend b) => FromJSON (RenameRel b) where
-- 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
{ 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)

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")