mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
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:
parent
15ed0cf536
commit
d5ff1acf2d
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"
|
@ -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"
|
@ -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
|
||||
|
@ -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: |
|
||||
|
@ -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);
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user