support where clause in on_conflict of insert mutation (close #2795) (#3002)

This commit is contained in:
Rakesh Emmadi 2019-10-09 15:39:20 +05:30 committed by Alexis King
parent d3f80265f5
commit f3b418c631
8 changed files with 173 additions and 15 deletions

View File

@ -52,7 +52,7 @@ Insert / upsert syntax
**E.g. INSERT**:
.. code-block:: graphql
mutation insert_article {
insert_article(
objects: [
@ -73,7 +73,7 @@ Insert / upsert syntax
**E.g. UPSERT**:
.. code-block:: graphql
mutation upsert_author {
insert_author (
objects: [
@ -161,7 +161,7 @@ Update syntax
**E.g. UPDATE**:
.. code-block:: graphql
mutation update_author{
update_author(
where: {id: {_eq: 3}},
@ -212,7 +212,7 @@ Delete syntax
**E.g. DELETE**:
.. code-block:: graphql
mutation delete_articles {
delete_article(
where: {author: {id: {_eq: 7}}}
@ -226,7 +226,7 @@ Delete syntax
.. note::
For more examples and details of usage, please see :doc:`this <../../mutations/index>`.
Syntax definitions
@ -287,7 +287,7 @@ E.g.:
E.g.:
.. code-block:: graphql
objects: [
{
title: "Software is eating the world",
@ -310,10 +310,11 @@ permissions before editing an existing row in case of a conflict. Hence the conf
table has *update* permissions defined.
.. code-block:: none
on_conflict: {
constraint: table_constraint!
update_columns: [table_update_column!]!
where: table_bool_exp
}
E.g.:
@ -323,6 +324,7 @@ E.g.:
on_conflict: {
constraint: author_name_key
update_columns: [name]
where: {id: {_lt: 1}}
}
.. _whereArgExp:

View File

@ -44,11 +44,11 @@ The upsert functionality is sometimes confused with the update functionality. Ho
differently. An upsert mutation is used in the case when it's not clear if the respective row is already present
in the database. If it's known that the row is present in the database, ``update`` is the functionality to use.
For an upsert, **all columns need to be passed**.
For an upsert, **all columns need to be passed**.
**How it works**
1. Postgres tries to insert a row (hence all the columns need to be present)
1. Postgres tries to insert a row (hence all the columns need to be present)
2. If this fails because of some constraint, it updates the specified columns
@ -104,6 +104,53 @@ the columns specified in ``update_columns``:
The ``published_on`` column is left unchanged as it wasn't present in ``update_columns``.
Update selected columns on conflict using a filter
--------------------------------------------------
Insert a new object in the ``article`` table, or if the primary key constraint ``article_pkey`` is violated, update
the columns specified in ``update_columns`` only if the provided ``where`` condition is met:
.. graphiql::
:view_only:
:query:
mutation upsert_article {
insert_article (
objects: [
{
id: 2,
published_on: "2018-10-12"
}
],
on_conflict: {
constraint: article_pkey,
update_columns: [published_on],
where: {
published_on: {_lt: "2018-10-12"}
}
}
) {
returning {
id
published_on
}
}
}
:response:
{
"data": {
"insert_article": {
"returning": [
{
"id": 2,
"published_on": "2018-10-12"
}
]
}
}
}
The ``published_on`` column is updated only if the new value is greater than the old value.
Ignore request on conflict
--------------------------
If ``update_columns`` is an **empty array** then the GraphQL engine ignores changes on conflict. Insert a new object into

View File

@ -2,6 +2,7 @@ module Hasura.GraphQL.Resolve.Insert
(convertInsert)
where
import Control.Arrow ((>>>))
import Data.Has
import Hasura.EncJSON
import Hasura.Prelude
@ -21,6 +22,7 @@ import qualified Hasura.RQL.DML.Returning as RR
import qualified Hasura.SQL.DML as S
import Hasura.GraphQL.Resolve.BoolExp
import Hasura.GraphQL.Resolve.Context
import Hasura.GraphQL.Resolve.InputValue
import Hasura.GraphQL.Resolve.Mutation
@ -85,7 +87,7 @@ data AnnInsObj
} deriving (Show, Eq)
mkAnnInsObj
:: (MonadResolve m, Has InsCtxMap r, MonadReader r m)
:: (MonadResolve m, Has InsCtxMap r, MonadReader r m, Has FieldMap r)
=> RelationInfoMap
-> PGColGNameMap
-> AnnGObject
@ -96,7 +98,7 @@ mkAnnInsObj relInfoMap allColMap annObj =
emptyInsObj = AnnInsObj [] [] []
traverseInsObj
:: (MonadResolve m, Has InsCtxMap r, MonadReader r m)
:: (MonadResolve m, Has InsCtxMap r, MonadReader r m, Has FieldMap r)
=> RelationInfoMap
-> PGColGNameMap
-> (G.Name, AnnInpVal)
@ -159,7 +161,7 @@ traverseInsObj rim allColMap (gName, annVal) defVal@(AnnInsObj cols objRels arrR
bool withNonEmptyArrData (return defVal) $ null arrDataVals
parseOnConflict
:: (MonadResolve m)
:: (MonadResolve m, MonadReader r m, Has FieldMap r)
=> QualifiedTable
-> Maybe UpdPermForIns
-> PGColGNameMap
@ -178,8 +180,10 @@ parseOnConflict tn updFiltrM allColMap val = withPathK "on_conflict" $
updFltrRes <- traverseAnnBoolExp
(convPartialSQLExp sessVarFromCurrentSetting)
updFiltr
return $ RI.CP1Update constraint updCols preSetRes $
toSQLBoolExp (S.mkQual tn) updFltrRes
whereExp <- parseWhereExp obj
let updateBoolExp = toSQLBoolExp (S.mkQual tn) updFltrRes
whereCondition = S.BEBin S.AndOp updateBoolExp whereExp
return $ RI.CP1Update constraint updCols preSetRes whereCondition
where
getUpdCols o = do
@ -193,6 +197,11 @@ parseOnConflict tn updFiltrM allColMap val = withPathK "on_conflict" $
(_, enumVal) <- asEnumVal v
return $ ConstraintName $ G.unName $ G.unEnumValue enumVal
parseWhereExp =
OMap.lookup "where"
>>> traverse (parseBoolExp >=> traverse (traverse resolveValTxt))
>>> fmap (maybe (S.BELit True) (toSQLBoolExp (S.mkQual tn)))
toSQLExps
:: (MonadError QErr m, MonadState PrepArgs m)
=> [PGColWithValue]

View File

@ -10,6 +10,7 @@ import qualified Data.HashMap.Strict as Map
import qualified Language.GraphQL.Draft.Syntax as G
import Hasura.GraphQL.Resolve.Types
import Hasura.GraphQL.Schema.BoolExp
import Hasura.GraphQL.Schema.Common
import Hasura.GraphQL.Schema.Mutation.Common
import Hasura.GraphQL.Validate.Types
@ -124,6 +125,7 @@ mkInsInp tn insCols relInfoMap =
input table_on_conflict {
constraint: table_constraint!
update_columns: [table_column!]
where: table_bool_exp
}
-}
@ -131,7 +133,7 @@ input table_on_conflict {
mkOnConflictInp :: QualifiedTable -> InpObjTyInfo
mkOnConflictInp tn =
mkHsraInpTyInfo (Just desc) (mkOnConflictInpTy tn) $ fromInpValL
[constraintInpVal, updateColumnsInpVal]
[constraintInpVal, updateColumnsInpVal, whereInpVal]
where
desc = G.Description $
"on conflict condition type for table " <>> tn
@ -141,6 +143,9 @@ mkOnConflictInp tn =
updateColumnsInpVal = InpValInfo Nothing (G.Name "update_columns") Nothing $
G.toGT $ G.toNT $ G.toLT $ G.toNT $ mkUpdColumnInpTy tn
whereInpVal = InpValInfo Nothing (G.Name "where") Nothing $
G.toGT $ mkBoolExpTy tn
{-
insert_table(

View File

@ -0,0 +1,47 @@
- description: Upsert into orders only if new value is greater than old value
url: /v1/graphql
status: 200
response:
data:
insert_orders:
affected_rows: 0
query:
variables:
placed: '2017-08-18 14:22:11.802755+02'
query: |
mutation ($placed: timestamptz) {
insert_orders(
objects: [{id: 1, placed: $placed}]
on_conflict: {
constraint:orders_pkey
update_columns: [placed]
where: {placed: {_lt: $placed}}
}
){
affected_rows
}
}
- description: Upsert into orders only if new value is greater than old value
url: /v1/graphql
status: 200
response:
data:
insert_orders:
affected_rows: 1
query:
variables:
placed: '2017-08-20 14:22:11.802755+02'
query: |
mutation ($placed: timestamptz) {
insert_orders(
objects: [{id: 1, placed: $placed}]
on_conflict: {
constraint:orders_pkey
update_columns: [placed]
where: {placed: {_lt: $placed}}
}
){
affected_rows
}
}

View File

@ -21,3 +21,9 @@ args:
- content: Sample article content
title: Article 3
#Insert into orders
- type: insert
args:
table: orders
objects:
- placed: '2017-08-19 14:22:11.802755+02'

View File

@ -0,0 +1,36 @@
description: Upsert into resident table only if age is greater
url: /v1/graphql
status: 200
response:
data:
insert_resident:
affected_rows: 1
returning:
- id: 6
age: 23
query:
variables:
age: 23
query: |
mutation ($age:Int){
insert_resident(
objects: [
{
id: 6
age: $age
name: "Resident 6"
}
]
on_conflict:{
constraint: resident_pkey
update_columns: [age]
where: {age: {_lt: $age}}
}
){
affected_rows
returning{
id
age
}
}
}

View File

@ -58,6 +58,9 @@ class TestGraphqlInsertOnConflict(DefaultTestMutations):
def test_err_unexpected_constraint(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/article_unexpected_on_conflict_constraint_error.yaml")
def test_order_on_conflict_where(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/order_on_conflict_where.yaml')
@classmethod
def dir(cls):
return "queries/graphql_mutation/insert/onconflict"
@ -120,6 +123,9 @@ class TestGraphqlInsertPermission(DefaultTestMutations):
def test_resident_5_modifies_resident_6_upsert(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/resident_5_modifies_resident_6_upsert.yaml")
def test_resident_on_conflict_where(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/resident_on_conflict_where.yaml")
def test_blog_on_conflict_update_preset(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + "/blog_on_conflict_update_preset.yaml")