mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
server: postgres multiple updates
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4837 GitOrigin-RevId: 505f669298298fd004dfc4e84eaa0d21df055216
This commit is contained in:
parent
4f3fc9853b
commit
d76aab99e1
140
CHANGELOG.md
140
CHANGELOG.md
@ -55,6 +55,146 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Update multiple records for Postgres
|
||||
|
||||
We are introducing a new way to allow updating multiple records in the same
|
||||
transaction for Postgres sources (#2768).
|
||||
|
||||
For example, the following query can be used to run the equivalent of two
|
||||
`update_by_pk` in a single transaction:
|
||||
|
||||
```graphql
|
||||
update_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 1 } },
|
||||
_set: { name: "new name", description: "other" }
|
||||
}
|
||||
{ where: { id: { _eq: 2 } },
|
||||
_set: { name: "new name" }
|
||||
}
|
||||
]
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, this feature allows arbitrary updates, even when they overlap:
|
||||
|
||||
|
||||
```graphql
|
||||
update_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _lte: 3 } },
|
||||
_set: { name: "first", description: "other" }
|
||||
}
|
||||
{ where: { id: { _eq: 2 } },
|
||||
_set: { name: "second" }
|
||||
}
|
||||
{ where: { id: { _gt: 2 } },
|
||||
_set: { name: "third", description: "hello" }
|
||||
}
|
||||
{ where: { id: { _eq: 1 } },
|
||||
_set: { name: "done" }
|
||||
}
|
||||
]
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Given the table looked like this before the query:
|
||||
|
||||
id | name | description
|
||||
-- | ---- | -----------
|
||||
1 | one | d1
|
||||
2 | two | d2
|
||||
3 | three | d3
|
||||
4 | four | d4
|
||||
|
||||
After executing the query, the table will look like:
|
||||
|
||||
id | name | description
|
||||
-- | ---- | -----------
|
||||
1 | done | other
|
||||
2 | second | other
|
||||
3 | third | hello
|
||||
4 | third | hello
|
||||
|
||||
The returned data will look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"update_artist_many": [
|
||||
{
|
||||
"affected_rows": 3,
|
||||
"returning": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "first"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"affected_rows": 1,
|
||||
"returning": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "second"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"affected_rows": 2,
|
||||
"returning": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "third"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "third"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"affected_rows": 1,
|
||||
"returning": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "done"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The way it works is:
|
||||
- we allow arbitrary `where` clauses (just like in a regular `update`)
|
||||
- we allow arbitrary `update`s (`_set`, `_inc`, etc., depending on the field
|
||||
type)
|
||||
- we run each update in sequence, in a transaction (if one of them fails,
|
||||
everything is rolled back)
|
||||
- we collect the return value of each query and return a list of results
|
||||
|
||||
Please submit any feedback you may have for this feature at https://github.com/hasura/graphql-engine/issues/2768.
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
|
@ -404,7 +404,7 @@ library
|
||||
, Data.Parser.CacheControl
|
||||
, Data.Parser.Expires
|
||||
, Data.Parser.JSONPath
|
||||
, Data.Sequence.NonEmpty
|
||||
, Data.Sequence.NESeq
|
||||
, Data.SqlCommenter
|
||||
, Data.SerializableBlob
|
||||
, Data.Text.Casing
|
||||
@ -1217,8 +1217,11 @@ test-suite tests-hspec
|
||||
-- Test
|
||||
Test.ArrayParamPermissionSpec
|
||||
Test.ArrayRelationshipsSpec
|
||||
Test.BasicFieldsSpec
|
||||
Test.BackendOnlyPermissionsSpec
|
||||
Test.BasicFieldsSpec
|
||||
Test.BigQuery.ComputedFieldSpec
|
||||
Test.BigQuery.GraphQLQueryBasicSpec
|
||||
Test.BigQuery.Metadata.ComputedFieldSpec
|
||||
Test.ColumnPresetsSpec
|
||||
Test.CustomFieldNamesSpec
|
||||
Test.CustomRootFieldsSpec
|
||||
@ -1227,8 +1230,16 @@ test-suite tests-hspec
|
||||
Test.DataConnector.QuerySpec
|
||||
Test.DataConnector.SelectPermissionsSpec
|
||||
Test.DirectivesSpec
|
||||
Test.EventTriggersRunSQLSpec
|
||||
Test.DisableRootFields.Common
|
||||
Test.DisableRootFields.DefaultRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.DisableAllRootFieldsRelationshipSpec
|
||||
Test.DisableRootFields.SelectPermission.DisableAllRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.EnableAggSpec
|
||||
Test.DisableRootFields.SelectPermission.EnableAllRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.EnablePKSpec
|
||||
Test.EventTriggersRecreationSpec
|
||||
Test.EventTriggersRunSQLSpec
|
||||
Test.GatheringUniqueConstraintsSpec
|
||||
Test.HelloWorldSpec
|
||||
Test.InsertCheckPermissionSpec
|
||||
Test.InsertDefaultsSpec
|
||||
@ -1240,7 +1251,6 @@ test-suite tests-hspec
|
||||
Test.ObjectRelationshipsSpec
|
||||
Test.OrderingSpec
|
||||
Test.PostgresTypesSpec
|
||||
Test.GatheringUniqueConstraintsSpec
|
||||
Test.PrimaryKeySpec
|
||||
Test.RemoteRelationship.FromRemoteSchemaSpec
|
||||
Test.RemoteRelationship.MetadataAPI.ClearMetadataSpec
|
||||
@ -1250,22 +1260,13 @@ test-suite tests-hspec
|
||||
Test.RemoteRelationship.XToDBArrayRelationshipSpec
|
||||
Test.RemoteRelationship.XToDBObjectRelationshipSpec
|
||||
Test.RemoteRelationship.XToRemoteSchemaRelationshipSpec
|
||||
Test.DisableRootFields.Common
|
||||
Test.DisableRootFields.DefaultRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.EnablePKSpec
|
||||
Test.DisableRootFields.SelectPermission.EnableAggSpec
|
||||
Test.DisableRootFields.SelectPermission.EnableAllRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.DisableAllRootFieldsSpec
|
||||
Test.DisableRootFields.SelectPermission.DisableAllRootFieldsRelationshipSpec
|
||||
Test.RequestHeadersSpec
|
||||
Test.RunSQLSpec
|
||||
Test.BigQuery.ComputedFieldSpec
|
||||
Test.BigQuery.GraphQLQueryBasicSpec
|
||||
Test.BigQuery.Metadata.ComputedFieldSpec
|
||||
Test.SQLServer.InsertVarcharColumnSpec
|
||||
Test.SelectSpec
|
||||
Test.SerializationSpec
|
||||
Test.ServiceLivenessSpec
|
||||
Test.SQLServer.InsertVarcharColumnSpec
|
||||
Test.UpdateManySpec
|
||||
Test.ViewsSpec
|
||||
Test.WhereSpec
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
-- we can't use onNothing without creating a dependency cycle
|
||||
{- HLINT ignore "Use onNothing" -}
|
||||
|
||||
module Data.Sequence.NonEmpty
|
||||
module Data.Sequence.NESeq
|
||||
( NESeq,
|
||||
pattern (:<||),
|
||||
pattern (:||>),
|
@ -9,7 +9,7 @@ import Data.Environment (Environment)
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||
import Data.Sequence qualified as Seq
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Data.Text qualified as Text
|
||||
import Data.Text.Extended (toTxt, (<<>), (<>>))
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
|
@ -11,7 +11,7 @@ import Data.FileEmbed (embedFile, makeRelativeToProject)
|
||||
import Data.HashMap.Strict qualified as HM
|
||||
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
|
||||
import Data.HashSet qualified as HS
|
||||
import Data.Sequence.NonEmpty qualified as SNE
|
||||
import Data.Sequence.NESeq qualified as SNE
|
||||
import Data.String (fromString)
|
||||
import Database.MySQL.Base (Connection)
|
||||
import Database.MySQL.Base.Types (Field (..))
|
||||
|
@ -14,7 +14,7 @@ import Data.HashMap.Strict qualified as Map
|
||||
import Data.List (delete)
|
||||
import Data.List.NonEmpty qualified as NE
|
||||
import Data.Sequence qualified as Seq
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Data.Text.Extended
|
||||
import Database.PG.Query qualified as Q
|
||||
import Hasura.Backends.Postgres.Connection
|
||||
|
@ -117,11 +117,18 @@ execUpdateQuery ::
|
||||
(AnnotatedUpdate ('Postgres pgKind), DS.Seq Q.PrepArg) ->
|
||||
m EncJSON
|
||||
execUpdateQuery strfyNum userInfo (u, p) =
|
||||
runMutation
|
||||
(mkMutation userInfo (_auTable u) (MCCheckConstraint updateCTE, p) (_auOutput u) (_auAllCols u) strfyNum)
|
||||
case updateCTE of
|
||||
Update singleUpdate -> runCTE singleUpdate
|
||||
MultiUpdate ctes -> encJFromList <$> traverse runCTE ctes
|
||||
where
|
||||
updateCTE :: UpdateCTE
|
||||
updateCTE = mkUpdateCTE u
|
||||
|
||||
runCTE :: S.TopLevelCTE -> m EncJSON
|
||||
runCTE cte =
|
||||
runMutation
|
||||
(mkMutation userInfo (_auTable u) (MCCheckConstraint cte, p) (_auOutput u) (_auAllCols u) strfyNum)
|
||||
|
||||
execDeleteQuery ::
|
||||
forall pgKind m.
|
||||
( MonadTx m,
|
||||
|
@ -39,7 +39,7 @@ import Hasura.Backends.Postgres.SQL.Value qualified as PG
|
||||
import Hasura.Backends.Postgres.Translate.Select (PostgresAnnotatedFieldJSON)
|
||||
import Hasura.Backends.Postgres.Translate.Select qualified as DS
|
||||
import Hasura.Backends.Postgres.Types.Function qualified as PG
|
||||
import Hasura.Backends.Postgres.Types.Update
|
||||
import Hasura.Backends.Postgres.Types.Update qualified as BackendUpdate
|
||||
import Hasura.Base.Error (QErr)
|
||||
import Hasura.EncJSON (EncJSON, encJFromBS, encJFromJValue)
|
||||
import Hasura.GraphQL.Execute.Backend
|
||||
@ -216,7 +216,7 @@ convertUpdate ::
|
||||
convertUpdate userInfo updateOperation stringifyNum = do
|
||||
queryTags <- ask
|
||||
preparedUpdate <- traverse (prepareWithoutPlan userInfo) updateOperation
|
||||
if null $ updateOperations . IR._auBackend $ updateOperation
|
||||
if BackendUpdate.isEmpty $ IR._auBackend updateOperation
|
||||
then pure $ pure $ IR.buildEmptyMutResp $ IR._auOutput preparedUpdate
|
||||
else
|
||||
pure $
|
||||
|
@ -7,7 +7,7 @@
|
||||
--
|
||||
-- Defines a 'Hasura.GraphQL.Schema.Backend.BackendSchema' type class instance for Postgres.
|
||||
module Hasura.Backends.Postgres.Instances.Schema
|
||||
( updateOperators,
|
||||
(
|
||||
)
|
||||
where
|
||||
|
||||
@ -63,6 +63,7 @@ import Hasura.GraphQL.Schema.Update qualified as SU
|
||||
import Hasura.Name qualified as Name
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR.BoolExp
|
||||
import Hasura.RQL.IR.Returning (MutationOutputG (..))
|
||||
import Hasura.RQL.IR.Root (RemoteRelationshipField)
|
||||
import Hasura.RQL.IR.Select
|
||||
( QueryDB (QDBConnection),
|
||||
@ -75,7 +76,7 @@ import Hasura.RQL.Types.Column
|
||||
import Hasura.RQL.Types.Function (FunctionInfo)
|
||||
import Hasura.RQL.Types.Source
|
||||
import Hasura.RQL.Types.SourceCustomization
|
||||
import Hasura.RQL.Types.Table (RolePermInfo (..), TableInfo, UpdPermInfo)
|
||||
import Hasura.RQL.Types.Table (CustomRootField (..), RolePermInfo (..), TableConfig (..), TableCoreInfoG (..), TableCustomRootFields (..), TableInfo (..), UpdPermInfo (..), ViewInfo (..), isMutable, tableInfoName)
|
||||
import Hasura.SQL.Backend (BackendType (Postgres), PostgresKind (Citus, Vanilla))
|
||||
import Hasura.SQL.Tag (HasTag)
|
||||
import Hasura.SQL.Types
|
||||
@ -209,6 +210,7 @@ buildTableRelayQueryFields sourceName tableName tableInfo gqlName pkeyColumns =
|
||||
selectTableConnection sourceName tableInfo rootFieldName fieldDesc pkeyColumns
|
||||
|
||||
pgkBuildTableUpdateMutationFields ::
|
||||
PostgresSchema pgKind =>
|
||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
||||
BackendTableSelectSchema ('Postgres pgKind) =>
|
||||
Scenario ->
|
||||
@ -221,18 +223,102 @@ pgkBuildTableUpdateMutationFields ::
|
||||
-- | field display name
|
||||
C.GQLNameIdentifier ->
|
||||
m [FieldParser n (IR.AnnotatedUpdateG ('Postgres pgKind) (RemoteRelationshipField IR.UnpreparedValue) (IR.UnpreparedValue ('Postgres pgKind)))]
|
||||
pgkBuildTableUpdateMutationFields scenario sourceName tableName tableInfo gqlName =
|
||||
pgkBuildTableUpdateMutationFields scenario sourceInfo tableName tableInfo gqlName =
|
||||
concat . maybeToList <$> runMaybeT do
|
||||
updatePerms <- MaybeT $ _permUpd <$> tablePermissions tableInfo
|
||||
lift $
|
||||
GSB.buildTableUpdateMutationFields
|
||||
-- TODO: https://github.com/hasura/graphql-engine-mono/issues/2955
|
||||
(\ti -> fmap BackendUpdate <$> updateOperators ti updatePerms)
|
||||
scenario
|
||||
sourceName
|
||||
tableName
|
||||
tableInfo
|
||||
gqlName
|
||||
lift $ do
|
||||
-- update_table and update_table_by_pk
|
||||
singleUpdates <-
|
||||
GSB.buildTableUpdateMutationFields
|
||||
-- TODO: https://github.com/hasura/graphql-engine-mono/issues/2955
|
||||
(\ti -> fmap BackendUpdate <$> updateOperators ti updatePerms)
|
||||
scenario
|
||||
sourceInfo
|
||||
tableName
|
||||
tableInfo
|
||||
gqlName
|
||||
|
||||
-- update_table_many
|
||||
multiUpdate <-
|
||||
updateTableMany
|
||||
scenario
|
||||
sourceInfo
|
||||
tableInfo
|
||||
gqlName
|
||||
|
||||
pure $ singleUpdates ++ maybeToList multiUpdate
|
||||
|
||||
-- | Create a parser for 'update_table_many'. This function is very similar to
|
||||
-- both 'GSB.buildTableUpdateMutationFields' and
|
||||
-- 'Hasura.GraphQL.Schema.Update.updateTable'.
|
||||
--
|
||||
-- It is similar to the former because of its shape: has to deal with grabbing
|
||||
-- the casing, deals with update permissions, etc.
|
||||
--
|
||||
-- It is similar to the latter because it deals with creating the
|
||||
-- parser/subselection/etc.
|
||||
--
|
||||
-- The reason this function exists here is because it is Postgres specific. It
|
||||
-- would not fit very well next to the functions mentioned above.
|
||||
--
|
||||
-- However, if you are trying to implement this feature for other backends,
|
||||
-- please consider making this function similar to /updateTable/ and moving it
|
||||
-- there.
|
||||
-- Note: this will likely require adding a type or a function to
|
||||
-- 'BackendSchema'.
|
||||
updateTableMany ::
|
||||
forall pgKind r m n.
|
||||
PostgresSchema pgKind =>
|
||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
||||
Scenario ->
|
||||
SourceInfo ('Postgres pgKind) ->
|
||||
TableInfo ('Postgres pgKind) ->
|
||||
C.GQLNameIdentifier ->
|
||||
m (Maybe (P.FieldParser n (IR.AnnotatedUpdateG ('Postgres pgKind) (RemoteRelationshipField IR.UnpreparedValue) (IR.UnpreparedValue ('Postgres pgKind)))))
|
||||
updateTableMany scenario sourceInfo tableInfo gqlName = runMaybeT do
|
||||
tCase <- asks getter
|
||||
let columns = tableColumns tableInfo
|
||||
viewInfo = _tciViewInfo $ _tiCoreInfo tableInfo
|
||||
guard $ isMutable viIsUpdatable viewInfo
|
||||
updatePerms <- MaybeT $ _permUpd <$> tablePermissions tableInfo
|
||||
guard $ not $ scenario == Frontend && upiBackendOnly updatePerms
|
||||
updates <- lift (mkMultiRowUpdateParser sourceInfo tableInfo updatePerms)
|
||||
selection <- lift $ P.multiple <$> GSB.mutationSelectionSet sourceInfo tableInfo
|
||||
updateName <- mkRootFieldName $ GSB.setFieldNameCase tCase tableInfo _tcrfUpdateMany mkUpdateManyField gqlName
|
||||
let argsParser = liftA2 (,) updates (pure annBoolExpTrue)
|
||||
pure $
|
||||
P.subselection updateName updateDesc argsParser selection
|
||||
<&> SU.mkUpdateObject tableName columns updatePerms . fmap MOutMultirowFields
|
||||
where
|
||||
tableName = tableInfoName tableInfo
|
||||
defaultUpdateDesc = "update multiples rows of table: " <>> tableName
|
||||
updateDesc = GSB.buildFieldDescription defaultUpdateDesc $ _crfComment _tcrfUpdateMany
|
||||
TableCustomRootFields {..} = _tcCustomRootFields . _tciCustomConfig $ _tiCoreInfo tableInfo
|
||||
|
||||
-- | Create a parser for the updates section of the `update_table_many` update.
|
||||
--
|
||||
-- It parses a list with two fields: 'where', and an update expression
|
||||
-- (set/inc/etc).
|
||||
mkMultiRowUpdateParser ::
|
||||
forall pgKind r m n.
|
||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
||||
SourceInfo ('Postgres pgKind) ->
|
||||
TableInfo ('Postgres pgKind) ->
|
||||
UpdPermInfo ('Postgres pgKind) ->
|
||||
m (P.InputFieldsParser n (PGIR.BackendUpdate pgKind (IR.UnpreparedValue ('Postgres pgKind))))
|
||||
mkMultiRowUpdateParser sourceInfo tableInfo updatePerms = do
|
||||
tableGQLName <- getTableGQLName tableInfo
|
||||
updatesObjectName <- mkTypename $ tableGQLName <> $$(G.litName "_updates")
|
||||
fmap BackendMultiRowUpdate
|
||||
. P.field Name._updates (Just updatesDesc)
|
||||
. P.list
|
||||
. P.object updatesObjectName Nothing
|
||||
<$> do
|
||||
mruWhere <- P.field Name._where Nothing <$> boolExp sourceInfo tableInfo
|
||||
mruExpression <- updateOperators tableInfo updatePerms
|
||||
pure $ MultiRowUpdate <$> mruWhere <*> mruExpression
|
||||
where
|
||||
updatesDesc = "updates to execute, in order"
|
||||
|
||||
buildFunctionRelayQueryFields ::
|
||||
forall pgKind m n r.
|
||||
|
@ -88,7 +88,7 @@ instance
|
||||
type ComputedFieldImplicitArguments ('Postgres pgKind) = PG.ComputedFieldImplicitArguments
|
||||
type ComputedFieldReturn ('Postgres pgKind) = PG.ComputedFieldReturn
|
||||
|
||||
type BackendUpdate ('Postgres pgKind) = PG.BackendUpdate
|
||||
type BackendUpdate ('Postgres pgKind) = PG.BackendUpdate pgKind
|
||||
|
||||
type ExtraTableMetadata ('Postgres pgKind) = PgExtraTableMetadata pgKind
|
||||
type BackendInsert ('Postgres pgKind) = PG.BackendInsert pgKind
|
||||
|
@ -38,7 +38,7 @@ module Hasura.Backends.Postgres.SQL.DML
|
||||
SQLInsert (SQLInsert, siCols, siConflict, siRet, siTable, siValues),
|
||||
SQLOp (SQLOp),
|
||||
ColumnOp (..),
|
||||
SQLUpdate (SQLUpdate),
|
||||
SQLUpdate (..),
|
||||
Select (Select, selCTEs, selDistinct, selExtr, selFrom, selLimit, selOffset, selOrderBy, selWhere),
|
||||
SelectWith,
|
||||
SelectWithG (SelectWith),
|
||||
@ -1009,11 +1009,11 @@ data SQLDelete = SQLDelete
|
||||
deriving (Show, Eq)
|
||||
|
||||
data SQLUpdate = SQLUpdate
|
||||
{ upTable :: !QualifiedTable,
|
||||
upSet :: !SetExp,
|
||||
upFrom :: !(Maybe FromExp),
|
||||
upWhere :: !(Maybe WhereFrag),
|
||||
upRet :: !(Maybe RetExp)
|
||||
{ upTable :: QualifiedTable,
|
||||
upSet :: SetExp,
|
||||
upFrom :: Maybe FromExp,
|
||||
upWhere :: Maybe WhereFrag,
|
||||
upRet :: Maybe RetExp
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
-- Translates IR update to Postgres-specific SQL UPDATE statements.
|
||||
module Hasura.Backends.Postgres.Translate.Update
|
||||
( mkUpdateCTE,
|
||||
UpdateCTE (..),
|
||||
)
|
||||
where
|
||||
|
||||
@ -21,24 +22,65 @@ import Hasura.RQL.Types.Column
|
||||
import Hasura.SQL.Backend
|
||||
import Hasura.SQL.Types
|
||||
|
||||
data UpdateCTE
|
||||
= -- | Used for /update_table/ and /update_table_by_pk/.
|
||||
Update S.TopLevelCTE
|
||||
| -- | Used for /update_table_many/.
|
||||
MultiUpdate [S.TopLevelCTE]
|
||||
|
||||
-- | Create the update CTE.
|
||||
mkUpdateCTE ::
|
||||
forall pgKind.
|
||||
Backend ('Postgres pgKind) =>
|
||||
AnnotatedUpdate ('Postgres pgKind) ->
|
||||
S.TopLevelCTE
|
||||
mkUpdateCTE (AnnotatedUpdateG tn (permFltr, wc) chk (BackendUpdate opExps) _ columnsInfo) =
|
||||
S.CTEUpdate update
|
||||
where
|
||||
update =
|
||||
S.SQLUpdate tn setExp Nothing tableFltr
|
||||
. Just
|
||||
. S.RetExp
|
||||
$ [ S.selectStar,
|
||||
asCheckErrorExtractor $ insertCheckConstraint checkExpr
|
||||
]
|
||||
setExp = S.SetExp $ map (expandOperator columnsInfo) (Map.toList opExps)
|
||||
tableFltr = Just $ S.WhereFrag tableFltrExpr
|
||||
tableFltrExpr = toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps permFltr wc
|
||||
checkExpr = toSQLBoolExp (S.QualTable tn) chk
|
||||
UpdateCTE
|
||||
mkUpdateCTE (AnnotatedUpdateG tn (permFltr, wc) chk backendUpdate _ columnsInfo) =
|
||||
case backendUpdate of
|
||||
BackendUpdate opExps ->
|
||||
Update $ S.CTEUpdate update
|
||||
where
|
||||
update =
|
||||
S.SQLUpdate
|
||||
{ upTable = tn,
|
||||
upSet =
|
||||
S.SetExp $ map (expandOperator columnsInfo) (Map.toList opExps),
|
||||
upFrom = Nothing,
|
||||
upWhere =
|
||||
Just
|
||||
. S.WhereFrag
|
||||
. toSQLBoolExp (S.QualTable tn)
|
||||
$ andAnnBoolExps permFltr wc,
|
||||
upRet =
|
||||
Just $
|
||||
S.RetExp
|
||||
[ S.selectStar,
|
||||
asCheckErrorExtractor $
|
||||
insertCheckConstraint $
|
||||
toSQLBoolExp (S.QualTable tn) chk
|
||||
]
|
||||
}
|
||||
BackendMultiRowUpdate updates ->
|
||||
MultiUpdate $ translateUpdate <$> updates
|
||||
where
|
||||
translateUpdate :: MultiRowUpdate pgKind S.SQLExp -> S.TopLevelCTE
|
||||
translateUpdate MultiRowUpdate {..} =
|
||||
S.CTEUpdate
|
||||
S.SQLUpdate
|
||||
{ upTable = tn,
|
||||
upSet =
|
||||
S.SetExp $ map (expandOperator columnsInfo) (Map.toList mruExpression),
|
||||
upFrom = Nothing,
|
||||
upWhere =
|
||||
Just . S.WhereFrag $ toSQLBoolExp (S.QualTable tn) mruWhere,
|
||||
upRet =
|
||||
Just $
|
||||
S.RetExp
|
||||
[ S.selectStar,
|
||||
asCheckErrorExtractor
|
||||
. insertCheckConstraint
|
||||
$ toSQLBoolExp (S.QualTable tn) chk
|
||||
]
|
||||
}
|
||||
|
||||
expandOperator :: [ColumnInfo ('Postgres pgKind)] -> (PGCol, UpdateOpExpression S.SQLExp) -> S.SetExpItem
|
||||
expandOperator infos (column, op) = S.SetExpItem $
|
||||
|
@ -1,25 +1,123 @@
|
||||
{-# LANGUAGE StandaloneKindSignatures #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
-- | Postgres Types Update
|
||||
--
|
||||
-- This module defines the Update-related IR types specific to Postgres.
|
||||
module Hasura.Backends.Postgres.Types.Update
|
||||
( BackendUpdate (..),
|
||||
isEmpty,
|
||||
UpdateOpExpression (..),
|
||||
MultiRowUpdate (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.Kind (Type)
|
||||
import Data.Monoid (All (..))
|
||||
import Data.Typeable (Typeable)
|
||||
import Hasura.Backends.Postgres.SQL.Types (PGCol)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR.BoolExp (AnnBoolExp, AnnBoolExpFld)
|
||||
import Hasura.RQL.Types.Backend (Backend (BooleanOperators, FunctionArgumentExp))
|
||||
import Hasura.SQL.Backend (BackendType (Postgres), PostgresKind)
|
||||
|
||||
-- | Represents an entry in an /update_table_many/ update.
|
||||
type MultiRowUpdate :: PostgresKind -> Type -> Type
|
||||
data MultiRowUpdate pgKind v = MultiRowUpdate
|
||||
{ -- | The /where/ clause for each individual update.
|
||||
--
|
||||
-- Note that the /single/ updates do not have a where clause, because it
|
||||
-- uses the one found in 'Hasura.RQL.IR.Update.AnnotatedUpdateG'. However,
|
||||
-- we have one for each update for /update_many/.
|
||||
mruWhere :: AnnBoolExp ('Postgres pgKind) v,
|
||||
-- | The /update/ expression, e.g, "set", "inc", etc., for each column.
|
||||
mruExpression :: HashMap PGCol (UpdateOpExpression v)
|
||||
}
|
||||
deriving stock (Generic)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Functor (MultiRowUpdate pgKind)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Foldable (MultiRowUpdate pgKind)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Traversable (MultiRowUpdate pgKind)
|
||||
|
||||
deriving instance
|
||||
( Data v,
|
||||
Typeable pgKind,
|
||||
Data (AnnBoolExpFld ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Data (MultiRowUpdate pgKind v)
|
||||
|
||||
deriving instance
|
||||
( Show v,
|
||||
Show (BooleanOperators ('Postgres pgKind) v),
|
||||
Show (FunctionArgumentExp ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Show (MultiRowUpdate pgKind v)
|
||||
|
||||
deriving instance
|
||||
( Eq v,
|
||||
Eq (BooleanOperators ('Postgres pgKind) v),
|
||||
Eq (FunctionArgumentExp ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Eq (MultiRowUpdate pgKind v)
|
||||
|
||||
-- | The PostgreSQL-specific data of an Update expression.
|
||||
--
|
||||
-- This is parameterised over @v@ which enables different phases of IR
|
||||
-- transformation to maintain the overall structure while enriching/transforming
|
||||
-- the data at the leaves.
|
||||
data BackendUpdate v = BackendUpdate
|
||||
{ -- | The update operations to perform on each colum.
|
||||
updateOperations :: !(HashMap PGCol (UpdateOpExpression v))
|
||||
}
|
||||
deriving (Functor, Foldable, Traversable, Generic, Data, Show, Eq)
|
||||
type BackendUpdate :: PostgresKind -> Type -> Type
|
||||
data BackendUpdate pgKind v
|
||||
= -- | The update operations to perform on each colum.
|
||||
BackendUpdate (HashMap PGCol (UpdateOpExpression v))
|
||||
| -- | The update operations to perform, in sequence, for an
|
||||
-- /update_table_many/ operation.
|
||||
BackendMultiRowUpdate [MultiRowUpdate pgKind v]
|
||||
deriving stock (Generic)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Functor (BackendUpdate pgKind)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Foldable (BackendUpdate pgKind)
|
||||
|
||||
deriving instance Backend ('Postgres pgKind) => Traversable (BackendUpdate pgKind)
|
||||
|
||||
deriving instance
|
||||
( Data v,
|
||||
Typeable pgKind,
|
||||
Data (AnnBoolExpFld ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Data (BackendUpdate pgKind v)
|
||||
|
||||
deriving instance
|
||||
( Show v,
|
||||
Show (BooleanOperators ('Postgres pgKind) v),
|
||||
Show (FunctionArgumentExp ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Show (BackendUpdate pgKind v)
|
||||
|
||||
deriving instance
|
||||
( Eq v,
|
||||
Eq (BooleanOperators ('Postgres pgKind) v),
|
||||
Eq (FunctionArgumentExp ('Postgres pgKind) v),
|
||||
Backend ('Postgres pgKind)
|
||||
) =>
|
||||
Eq (BackendUpdate pgKind v)
|
||||
|
||||
-- | Are we updating anything?
|
||||
isEmpty :: BackendUpdate pgKind v -> Bool
|
||||
isEmpty =
|
||||
\case
|
||||
BackendUpdate hm ->
|
||||
Map.null hm
|
||||
BackendMultiRowUpdate xs ->
|
||||
getAll $ foldMap (All . Map.null . mruExpression) xs
|
||||
|
||||
-- | The various @update operators@ supported by PostgreSQL,
|
||||
-- i.e. the @_set@, @_inc@ operators that appear in the schema.
|
||||
|
@ -49,6 +49,8 @@ module Hasura.GraphQL.Schema.Build
|
||||
buildTableQueryAndSubscriptionFields,
|
||||
buildTableStreamingSubscriptionFields,
|
||||
buildTableUpdateMutationFields,
|
||||
setFieldNameCase,
|
||||
buildFieldDescription,
|
||||
)
|
||||
where
|
||||
|
||||
|
@ -23,7 +23,7 @@ import Data.Aeson qualified as J
|
||||
import Data.Aeson.Types qualified as J
|
||||
import Data.HashMap.Strict qualified as Map
|
||||
import Data.Sequence qualified as Seq
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Hasura.Backends.Postgres.SQL.Types qualified as PG
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR qualified as IR
|
||||
|
@ -15,7 +15,7 @@ import Data.Aeson.Types qualified as J
|
||||
import Data.Align (align)
|
||||
import Data.Has
|
||||
import Data.HashMap.Strict.Extended qualified as Map
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Data.Text qualified as T
|
||||
import Data.Text.Extended
|
||||
import Data.These (partitionThese)
|
||||
|
@ -35,7 +35,7 @@ import Data.Has
|
||||
import Data.HashMap.Strict.Extended qualified as Map
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty qualified as NE
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Data.Text.Extended
|
||||
import Hasura.Backends.Postgres.SQL.Types qualified as PG
|
||||
import Hasura.Base.Error
|
||||
|
@ -12,6 +12,7 @@ module Hasura.GraphQL.Schema.Update
|
||||
incOp,
|
||||
updateTable,
|
||||
updateTableByPk,
|
||||
mkUpdateObject,
|
||||
)
|
||||
where
|
||||
|
||||
|
@ -432,6 +432,9 @@ _insert = [G.name|insert|]
|
||||
_update :: G.Name
|
||||
_update = [G.name|update|]
|
||||
|
||||
_updates :: G.Name
|
||||
_updates = [G.name|updates|]
|
||||
|
||||
_delete :: G.Name
|
||||
_delete = [G.name|delete|]
|
||||
|
||||
@ -450,6 +453,9 @@ _objects = [G.name|objects|]
|
||||
_one :: G.Name
|
||||
_one = [G.name|one|]
|
||||
|
||||
_many :: G.Name
|
||||
_many = [G.name|many|]
|
||||
|
||||
_returning :: G.Name
|
||||
_returning = [G.name|returning|]
|
||||
|
||||
|
@ -128,7 +128,7 @@ import Data.Monoid as M (getAlt)
|
||||
import Data.Ord as M (comparing)
|
||||
import Data.Semigroup as M (Semigroup (..))
|
||||
import Data.Sequence as M (Seq)
|
||||
import Data.Sequence.NonEmpty as M (NESeq)
|
||||
import Data.Sequence.NESeq as M (NESeq)
|
||||
import Data.String as M (IsString)
|
||||
import Data.Text as M (Text)
|
||||
import Data.Text qualified as T
|
||||
|
@ -17,7 +17,7 @@ where
|
||||
import Data.HashMap.Strict qualified as M
|
||||
import Data.HashMap.Strict.NonEmpty qualified as NEHashMap
|
||||
import Data.Sequence qualified as Seq
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Backend
|
||||
import Hasura.RQL.Types.Column
|
||||
|
@ -24,6 +24,10 @@ import Hasura.SQL.Backend
|
||||
|
||||
data AnnotatedUpdateG (b :: BackendType) (r :: Type) v = AnnotatedUpdateG
|
||||
{ _auTable :: !(TableName b),
|
||||
-- | The where clause for /update_table/ and /update_table_by_pk/ along with
|
||||
-- the permissions filter.
|
||||
-- In the case of /update_table_many/, this will be empty and the actual
|
||||
-- where clauses (one per update) are found in 'BackendUpdate'.
|
||||
_auWhere :: !(AnnBoolExp b v, AnnBoolExp b v),
|
||||
_auCheck :: !(AnnBoolExp b v),
|
||||
-- | All the backend-specific data related to an update mutation
|
||||
|
@ -34,6 +34,7 @@ module Hasura.RQL.Types.SourceCustomization
|
||||
mkInsertOneField,
|
||||
mkUpdateField,
|
||||
mkUpdateByPkField,
|
||||
mkUpdateManyField,
|
||||
mkDeleteField,
|
||||
mkDeleteByPkField,
|
||||
mkRelayConnectionField,
|
||||
@ -283,6 +284,9 @@ mkUpdateField name = C.fromName Name._update <> name
|
||||
mkUpdateByPkField :: GQLNameIdentifier -> GQLNameIdentifier
|
||||
mkUpdateByPkField name = C.fromName Name._update <> name <> C.fromTuple $$(G.litGQLIdentifier ["by", "pk"])
|
||||
|
||||
mkUpdateManyField :: GQLNameIdentifier -> GQLNameIdentifier
|
||||
mkUpdateManyField name = C.fromName Name._update <> name <> C.fromName Name._many
|
||||
|
||||
mkDeleteField :: GQLNameIdentifier -> GQLNameIdentifier
|
||||
mkDeleteField name = C.fromName Name._delete <> name
|
||||
|
||||
|
@ -162,6 +162,7 @@ data TableCustomRootFields = TableCustomRootFields
|
||||
_tcrfInsertOne :: CustomRootField,
|
||||
_tcrfUpdate :: CustomRootField,
|
||||
_tcrfUpdateByPk :: CustomRootField,
|
||||
_tcrfUpdateMany :: CustomRootField,
|
||||
_tcrfDelete :: CustomRootField,
|
||||
_tcrfDeleteByPk :: CustomRootField
|
||||
}
|
||||
@ -184,6 +185,7 @@ instance ToJSON TableCustomRootFields where
|
||||
"insert_one" .= _tcrfInsertOne,
|
||||
"update" .= _tcrfUpdate,
|
||||
"update_by_pk" .= _tcrfUpdateByPk,
|
||||
"update_many" .= _tcrfUpdateMany,
|
||||
"delete" .= _tcrfDelete,
|
||||
"delete_by_pk" .= _tcrfDeleteByPk
|
||||
]
|
||||
@ -200,6 +202,7 @@ instance FromJSON TableCustomRootFields where
|
||||
<*> (obj .:? "insert_one" .!= defaultCustomRootField)
|
||||
<*> (obj .:? "update" .!= defaultCustomRootField)
|
||||
<*> (obj .:? "update_by_pk" .!= defaultCustomRootField)
|
||||
<*> (obj .:? "update_many" .!= defaultCustomRootField)
|
||||
<*> (obj .:? "delete" .!= defaultCustomRootField)
|
||||
<*> (obj .:? "delete_by_pk" .!= defaultCustomRootField)
|
||||
|
||||
@ -222,22 +225,24 @@ emptyCustomRootFields =
|
||||
_tcrfInsertOne = defaultCustomRootField,
|
||||
_tcrfUpdate = defaultCustomRootField,
|
||||
_tcrfUpdateByPk = defaultCustomRootField,
|
||||
_tcrfUpdateMany = defaultCustomRootField,
|
||||
_tcrfDelete = defaultCustomRootField,
|
||||
_tcrfDeleteByPk = defaultCustomRootField
|
||||
}
|
||||
|
||||
getAllCustomRootFields :: TableCustomRootFields -> [CustomRootField]
|
||||
getAllCustomRootFields (TableCustomRootFields select selectByPk selectAgg selectStream insert insertOne update updateByPk delete deleteByPk) =
|
||||
[ select,
|
||||
selectByPk,
|
||||
selectAgg,
|
||||
selectStream,
|
||||
insert,
|
||||
insertOne,
|
||||
update,
|
||||
updateByPk,
|
||||
delete,
|
||||
deleteByPk
|
||||
getAllCustomRootFields TableCustomRootFields {..} =
|
||||
[ _tcrfSelect,
|
||||
_tcrfSelectByPk,
|
||||
_tcrfSelectAggregate,
|
||||
_tcrfSelectStream,
|
||||
_tcrfInsert,
|
||||
_tcrfInsertOne,
|
||||
_tcrfUpdate,
|
||||
_tcrfUpdateByPk,
|
||||
_tcrfUpdateMany,
|
||||
_tcrfDelete,
|
||||
_tcrfDeleteByPk
|
||||
]
|
||||
|
||||
data FieldInfo (b :: BackendType)
|
||||
|
@ -28,20 +28,22 @@ type PG = 'Postgres 'Vanilla
|
||||
-- Given a table with one or two columns, perform a simple update. There are no
|
||||
-- permission restrictions. It's also only using text fields and 'UpdateSet'.
|
||||
spec :: Spec
|
||||
spec = describe "Simple update" do
|
||||
it "single column" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [(nameColumn, [AEQ True oldValue])],
|
||||
utbUpdate = [(nameColumn, UpdateSet newValue)]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
spec = do
|
||||
describe "Update parsers" do
|
||||
describe "update where" do
|
||||
it "single column" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [(nameColumn, [AEQ True oldValue])],
|
||||
utbUpdate = UpdateTable [(nameColumn, UpdateSet newValue)]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
update_artist(
|
||||
where: { name: { _eq: "old name"}},
|
||||
_set: { name: "new name" }
|
||||
@ -49,24 +51,25 @@ update_artist(
|
||||
affected_rows
|
||||
}
|
||||
|]
|
||||
}
|
||||
}
|
||||
|
||||
it "two columns" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn, descColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [(nameColumn, [AEQ True oldValue])],
|
||||
utbUpdate =
|
||||
[ (nameColumn, UpdateSet newValue),
|
||||
(descColumn, UpdateSet otherValue)
|
||||
]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
it "two columns" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn, descColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [(nameColumn, [AEQ True oldValue])],
|
||||
utbUpdate =
|
||||
UpdateTable
|
||||
[ (nameColumn, UpdateSet newValue),
|
||||
(descColumn, UpdateSet otherValue)
|
||||
]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
update_artist(
|
||||
where: { name: { _eq: "old name"}},
|
||||
_set: { name: "new name", description: "other" }
|
||||
@ -74,14 +77,144 @@ update_artist(
|
||||
affected_rows
|
||||
}
|
||||
|]
|
||||
}
|
||||
}
|
||||
describe "update many" do
|
||||
it "one update" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn, descColumn, idColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [],
|
||||
utbUpdate =
|
||||
UpdateMany
|
||||
[ MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerOne])],
|
||||
mrubUpdate =
|
||||
[ (nameColumn, UpdateSet newValue),
|
||||
(descColumn, UpdateSet otherValue)
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
update_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 1 } },
|
||||
_set: { name: "new name", description: "other" }
|
||||
}
|
||||
]
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
|]
|
||||
}
|
||||
|
||||
it "two updates, complex where clause" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn, descColumn, idColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [],
|
||||
utbUpdate =
|
||||
UpdateMany
|
||||
[ MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerOne])],
|
||||
mrubUpdate =
|
||||
[ (nameColumn, UpdateSet newValue),
|
||||
(descColumn, UpdateSet otherValue)
|
||||
]
|
||||
},
|
||||
MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerTwo])],
|
||||
mrubUpdate = [(descColumn, UpdateSet otherValue)]
|
||||
}
|
||||
]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
update_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 1 } }
|
||||
_set: { name: "new name", description: "other" }
|
||||
}
|
||||
{ where: { id: { _eq: 2 } }
|
||||
_set: { description: "other" }
|
||||
}
|
||||
]
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
|]
|
||||
}
|
||||
|
||||
it "three updates, ordering" do
|
||||
runUpdateFieldTest
|
||||
UpdateTestSetup
|
||||
{ utsTable = "artist",
|
||||
utsColumns = [nameColumn, descColumn, idColumn],
|
||||
utsExpect =
|
||||
UpdateExpectationBuilder
|
||||
{ utbOutput = MOutMultirowFields [("affected_rows", MCount)],
|
||||
utbWhere = [],
|
||||
utbUpdate =
|
||||
UpdateMany
|
||||
[ MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerOne])],
|
||||
mrubUpdate = [(nameColumn, UpdateSet newValue)]
|
||||
},
|
||||
MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerOne])],
|
||||
mrubUpdate = [(nameColumn, UpdateSet oldValue)]
|
||||
},
|
||||
MultiRowUpdateBuilder
|
||||
{ mrubWhere = [(idColumn, [AEQ True integerTwo])],
|
||||
mrubUpdate = [(nameColumn, UpdateSet otherValue)]
|
||||
}
|
||||
]
|
||||
},
|
||||
utsField =
|
||||
[GQL.field|
|
||||
update_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 1 } }
|
||||
_set: { name: "new name" }
|
||||
}
|
||||
{ where: { id: { _eq: 1 } }
|
||||
_set: { name: "old name" }
|
||||
}
|
||||
{ where: { id: { _eq: 2 } }
|
||||
_set: { name: "other" }
|
||||
}
|
||||
]
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
|]
|
||||
}
|
||||
|
||||
idColumn :: ColumnInfoBuilder
|
||||
idColumn =
|
||||
ColumnInfoBuilder
|
||||
{ cibName = "id",
|
||||
cibType = ColumnScalar PGInteger,
|
||||
cibNullable = False,
|
||||
cibIsPrimaryKey = True
|
||||
}
|
||||
|
||||
nameColumn :: ColumnInfoBuilder
|
||||
nameColumn =
|
||||
ColumnInfoBuilder
|
||||
{ cibName = "name",
|
||||
cibType = ColumnScalar PGText,
|
||||
cibNullable = False
|
||||
cibNullable = False,
|
||||
cibIsPrimaryKey = False
|
||||
}
|
||||
|
||||
oldValue :: UnpreparedValue PG
|
||||
@ -105,7 +238,8 @@ descColumn =
|
||||
ColumnInfoBuilder
|
||||
{ cibName = "description",
|
||||
cibType = ColumnScalar PGText,
|
||||
cibNullable = False
|
||||
cibNullable = False,
|
||||
cibIsPrimaryKey = False
|
||||
}
|
||||
|
||||
otherValue :: UnpreparedValue PG
|
||||
@ -115,3 +249,19 @@ otherValue =
|
||||
{ cvType = ColumnScalar PGText,
|
||||
cvValue = PGValText "other"
|
||||
}
|
||||
|
||||
integerOne :: UnpreparedValue PG
|
||||
integerOne =
|
||||
UVParameter Nothing $
|
||||
ColumnValue
|
||||
{ cvType = ColumnScalar PGInteger,
|
||||
cvValue = PGValInteger 1
|
||||
}
|
||||
|
||||
integerTwo :: UnpreparedValue PG
|
||||
integerTwo =
|
||||
UVParameter Nothing $
|
||||
ColumnValue
|
||||
{ cvType = ColumnScalar PGInteger,
|
||||
cvValue = PGValInteger 2
|
||||
}
|
||||
|
@ -361,6 +361,7 @@ instance Arbitrary TableCustomRootFields where
|
||||
<*> arbitrary
|
||||
<*> arbitrary
|
||||
<*> arbitrary
|
||||
<*> arbitrary
|
||||
)
|
||||
`suchThat` allFieldNamesAreUnique
|
||||
where
|
||||
|
@ -5,6 +5,8 @@
|
||||
module Test.Parser.Expectation
|
||||
( UpdateTestSetup (..),
|
||||
UpdateExpectationBuilder (..),
|
||||
BackendUpdateBuilder (..),
|
||||
MultiRowUpdateBuilder (..),
|
||||
runUpdateFieldTest,
|
||||
module I,
|
||||
)
|
||||
@ -13,9 +15,10 @@ where
|
||||
import Data.Bifunctor (bimap)
|
||||
import Data.HashMap.Strict qualified as HM
|
||||
import Hasura.Backends.Postgres.SQL.Types (QualifiedTable)
|
||||
import Hasura.Backends.Postgres.Types.Update (BackendUpdate (..), UpdateOpExpression (..))
|
||||
import Hasura.Backends.Postgres.Types.Update (BackendUpdate (..), MultiRowUpdate (..), UpdateOpExpression (..))
|
||||
import Hasura.GraphQL.Parser.Internal.Parser (FieldParser (..))
|
||||
import Hasura.GraphQL.Parser.Variable
|
||||
import Hasura.GraphQL.Parser.Schema (Definition (..))
|
||||
import Hasura.GraphQL.Parser.Variable (Variable (..))
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR.BoolExp (AnnBoolExpFld (..), GBoolExp (..), OpExpG (..))
|
||||
import Hasura.RQL.IR.Returning (MutationOutputG (..))
|
||||
@ -41,7 +44,7 @@ type Field = Syntax.Field Syntax.NoFragments Variable
|
||||
|
||||
type Where = (ColumnInfoBuilder, [OpExpG PG (UnpreparedValue PG)])
|
||||
|
||||
type Update = (ColumnInfoBuilder, UpdateOpExpression (UnpreparedValue PG))
|
||||
type Update = BackendUpdateBuilder ColumnInfoBuilder
|
||||
|
||||
-- | Holds all the information required to setup and run a field parser update
|
||||
-- test.
|
||||
@ -71,7 +74,7 @@ data UpdateExpectationBuilder = UpdateExpectationBuilder
|
||||
-- ColumnInfoBuilder@ and @newValue :: UnpreparedValue PG@:
|
||||
--
|
||||
-- > [(namecolumn, UpdateSet newValue)]
|
||||
utbUpdate :: [Update]
|
||||
utbUpdate :: Update
|
||||
}
|
||||
|
||||
-- | Run a test given the schema and field.
|
||||
@ -79,14 +82,20 @@ runUpdateFieldTest :: UpdateTestSetup -> Expectation
|
||||
runUpdateFieldTest UpdateTestSetup {..} =
|
||||
case mkParser table utsColumns of
|
||||
SchemaTestT [] -> expectationFailure "expected at least one parser"
|
||||
SchemaTestT (FieldParser {fParser} : _xs) ->
|
||||
case fParser utsField of
|
||||
ParserTestT (Right annUpdate) -> do
|
||||
coerce annUpdate `shouldBe` expected
|
||||
ParserTestT (Left err) -> err
|
||||
SchemaTestT parsers ->
|
||||
case find (byName (Syntax._fName utsField)) parsers of
|
||||
Nothing -> expectationFailure $ "could not find parser " <> show (Syntax._fName utsField)
|
||||
Just FieldParser {..} ->
|
||||
case fParser utsField of
|
||||
ParserTestT (Right annUpdate) ->
|
||||
coerce annUpdate `shouldBe` expected
|
||||
ParserTestT (Left err) -> err
|
||||
where
|
||||
UpdateExpectationBuilder {..} = utsExpect
|
||||
|
||||
byName :: Syntax.Name -> Parser -> Bool
|
||||
byName name FieldParser {..} = name == dName fDefinition
|
||||
|
||||
table :: QualifiedTable
|
||||
table = mkTable utsTable
|
||||
|
||||
@ -98,8 +107,10 @@ runUpdateFieldTest UpdateTestSetup {..} =
|
||||
aubOutput = utbOutput,
|
||||
aubColumns = mkColumnInfo <$> utsColumns,
|
||||
aubWhere = first mkColumnInfo <$> utbWhere,
|
||||
aubUpdate = first mkColumnInfo <$> utbUpdate
|
||||
aubUpdate = mkUpdateColumns utbUpdate
|
||||
}
|
||||
mkUpdateColumns :: BackendUpdateBuilder ColumnInfoBuilder -> BackendUpdateBuilder (ColumnInfo PG)
|
||||
mkUpdateColumns = fmap mkColumnInfo
|
||||
|
||||
-- | Internal use only. The intended use is through 'runUpdateFieldTest'.
|
||||
--
|
||||
@ -114,9 +125,20 @@ data AnnotatedUpdateBuilder = AnnotatedUpdateBuilder
|
||||
-- | the where clause(s)
|
||||
aubWhere :: [(ColumnInfo PG, [OpExpG PG (UnpreparedValue PG)])],
|
||||
-- | the update statement(s)
|
||||
aubUpdate :: [(ColumnInfo PG, UpdateOpExpression (UnpreparedValue PG))]
|
||||
aubUpdate :: BackendUpdateBuilder (ColumnInfo PG)
|
||||
}
|
||||
|
||||
data BackendUpdateBuilder col
|
||||
= UpdateTable [(col, UpdateOpExpression (UnpreparedValue PG))]
|
||||
| UpdateMany [MultiRowUpdateBuilder col]
|
||||
deriving stock (Functor)
|
||||
|
||||
data MultiRowUpdateBuilder col = MultiRowUpdateBuilder
|
||||
{ mrubWhere :: [(col, [OpExpG PG (UnpreparedValue PG)])],
|
||||
mrubUpdate :: [(col, UpdateOpExpression (UnpreparedValue PG))]
|
||||
}
|
||||
deriving stock (Functor)
|
||||
|
||||
-- | 'RemoteRelationshipField' cannot have Eq/Show instances, so we're wrapping
|
||||
-- it.
|
||||
newtype RemoteRelationshipFieldWrapper vf = RemoteRelationshipFieldWrapper (RemoteRelationshipField vf)
|
||||
@ -135,23 +157,33 @@ mkAnnotatedUpdate ::
|
||||
AnnotatedUpdateG PG (RemoteRelationshipFieldWrapper UnpreparedValue) (UnpreparedValue PG)
|
||||
mkAnnotatedUpdate AnnotatedUpdateBuilder {..} = AnnotatedUpdateG {..}
|
||||
where
|
||||
toBoolExp :: [(ColumnInfo PG, [OpExpG PG (UnpreparedValue PG)])] -> BoolExp
|
||||
toBoolExp = BoolAnd . fmap (\(c, ops) -> BoolField $ AVColumn c ops)
|
||||
|
||||
_auTable :: QualifiedTable
|
||||
_auTable = aubTable
|
||||
|
||||
_auWhere :: (BoolExp, BoolExp)
|
||||
_auWhere =
|
||||
( column [],
|
||||
BoolAnd $ fmap (\(c, ops) -> BoolField $ AVColumn c ops) aubWhere
|
||||
)
|
||||
_auWhere = (column [], toBoolExp aubWhere)
|
||||
|
||||
_auCheck :: BoolExp
|
||||
_auCheck = BoolAnd []
|
||||
|
||||
_auBackend :: BackendUpdate (UnpreparedValue PG)
|
||||
_auBackend :: BackendUpdate 'Vanilla (UnpreparedValue PG)
|
||||
_auBackend =
|
||||
BackendUpdate
|
||||
{ updateOperations =
|
||||
HM.fromList $ fmap (bimap ciColumn id) aubUpdate
|
||||
case aubUpdate of
|
||||
UpdateTable items ->
|
||||
BackendUpdate $
|
||||
HM.fromList $
|
||||
fmap (first ciColumn) items
|
||||
UpdateMany rows ->
|
||||
BackendMultiRowUpdate $ fmap mapRows rows
|
||||
|
||||
mapRows :: MultiRowUpdateBuilder (ColumnInfo PG) -> MultiRowUpdate 'Vanilla (UnpreparedValue PG)
|
||||
mapRows MultiRowUpdateBuilder {..} =
|
||||
MultiRowUpdate
|
||||
{ mruWhere = toBoolExp mrubWhere,
|
||||
mruExpression = HM.fromList $ fmap (bimap ciColumn id) mrubUpdate
|
||||
}
|
||||
|
||||
_auOutput :: Output
|
||||
|
@ -5,29 +5,30 @@ module Test.Parser.Internal
|
||||
ColumnInfoBuilder (..),
|
||||
mkColumnInfo,
|
||||
mkParser,
|
||||
Parser,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.HashMap.Strict qualified as HM
|
||||
import Data.HashSet qualified as HS
|
||||
import Data.Sequence.NESeq qualified as NESeq
|
||||
import Data.Text.Casing qualified as C
|
||||
import Hasura.Backends.Postgres.Instances.Schema (updateOperators)
|
||||
import Hasura.Backends.Postgres.SQL.Types (QualifiedObject (..), QualifiedTable, TableName (..), unsafePGCol)
|
||||
import Hasura.Backends.Postgres.Types.Update (BackendUpdate (..))
|
||||
import Hasura.GraphQL.Schema.Build qualified as Build
|
||||
import Hasura.Backends.Postgres.Instances.Schema ()
|
||||
import Hasura.Backends.Postgres.SQL.Types (ConstraintName (..), QualifiedObject (..), QualifiedTable, TableName (..), unsafePGCol)
|
||||
import Hasura.GraphQL.Schema.Backend
|
||||
import Hasura.GraphQL.Schema.Common (Scenario (Frontend))
|
||||
import Hasura.GraphQL.Schema.Parser (FieldParser, InputFieldsParser)
|
||||
import Hasura.GraphQL.Schema.Parser (FieldParser)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.IR.BoolExp (AnnBoolExpFld (..), GBoolExp (..), PartialSQLExp (..))
|
||||
import Hasura.RQL.IR.Root (RemoteRelationshipField)
|
||||
import Hasura.RQL.IR.Update (AnnotatedUpdateG (..))
|
||||
import Hasura.RQL.IR.Value (UnpreparedValue (..))
|
||||
import Hasura.RQL.Types.Column (ColumnInfo (..), ColumnMutability (..), ColumnType (..))
|
||||
import Hasura.RQL.Types.Common (Comment (..), FieldName (..))
|
||||
import Hasura.RQL.Types.Common (Comment (..), FieldName (..), OID (..))
|
||||
import Hasura.RQL.Types.Instances ()
|
||||
import Hasura.RQL.Types.Permission (AllowedRootFields (..))
|
||||
import Hasura.RQL.Types.Source (SourceInfo)
|
||||
import Hasura.RQL.Types.Table (CustomRootField (..), FieldInfo (..), RolePermInfo (..), SelPermInfo (..), TableConfig (..), TableCoreInfoG (..), TableCustomRootFields (..), TableInfo (..), UpdPermInfo (..))
|
||||
import Hasura.RQL.Types.Table (Constraint (..), CustomRootField (..), FieldInfo (..), PrimaryKey (..), RolePermInfo (..), SelPermInfo (..), TableConfig (..), TableCoreInfoG (..), TableCustomRootFields (..), TableInfo (..), UpdPermInfo (..))
|
||||
import Hasura.SQL.Backend (BackendType (Postgres), PostgresKind (Vanilla))
|
||||
import Language.GraphQL.Draft.Syntax (unsafeMkName)
|
||||
import Test.Parser.Monad
|
||||
@ -53,7 +54,9 @@ data ColumnInfoBuilder = ColumnInfoBuilder
|
||||
-- > ColumnScalar PGText
|
||||
cibType :: ColumnType PG,
|
||||
-- | whether the column is nullable or not
|
||||
cibNullable :: Bool
|
||||
cibNullable :: Bool,
|
||||
-- | is it a primary key?
|
||||
cibIsPrimaryKey :: Bool
|
||||
}
|
||||
|
||||
-- | Create a column using the provided 'ColumnInfoBuilder' and defaults.
|
||||
@ -85,20 +88,13 @@ mkColumnInfo ColumnInfoBuilder {..} =
|
||||
-- This will not work for inserts and deletes (see @rolePermInfo@ below).
|
||||
mkParser :: QualifiedTable -> [ColumnInfoBuilder] -> SchemaTestT [Parser]
|
||||
mkParser table cib =
|
||||
Build.buildTableUpdateMutationFields
|
||||
backendUpdateParser
|
||||
buildTableUpdateMutationFields
|
||||
Frontend
|
||||
sourceInfo
|
||||
table
|
||||
tableInfo
|
||||
name
|
||||
where
|
||||
backendUpdateParser ::
|
||||
TableInfo PG ->
|
||||
SchemaTestT (InputFieldsParser ParserTestT (BackendUpdate (UnpreparedValue PG)))
|
||||
backendUpdateParser ti =
|
||||
fmap BackendUpdate <$> updateOperators ti updPermInfo
|
||||
|
||||
updPermInfo :: UpdPermInfo PG
|
||||
updPermInfo =
|
||||
UpdPermInfo
|
||||
@ -114,6 +110,11 @@ mkParser table cib =
|
||||
columnInfos :: [ColumnInfo PG]
|
||||
columnInfos = mkColumnInfo <$> cib
|
||||
|
||||
pks :: Maybe (NESeq (ColumnInfo PG))
|
||||
pks = case mkColumnInfo <$> filter cibIsPrimaryKey cib of
|
||||
[] -> Nothing
|
||||
(x : xs) -> Just $ foldl (<>) (NESeq.singleton x) $ fmap NESeq.singleton xs
|
||||
|
||||
upiFilter :: GBoolExp PG (AnnBoolExpFld PG (PartialSQLExp PG))
|
||||
upiFilter = BoolAnd $ fmap (\ci -> BoolField $ AVColumn ci []) columnInfos
|
||||
|
||||
@ -137,7 +138,7 @@ mkParser table cib =
|
||||
{ _tciName = table,
|
||||
_tciDescription = Nothing,
|
||||
_tciFieldInfoMap = fieldInfoMap,
|
||||
_tciPrimaryKey = Nothing,
|
||||
_tciPrimaryKey = pk,
|
||||
_tciUniqueConstraints = mempty,
|
||||
_tciForeignKeys = mempty,
|
||||
_tciViewInfo = Nothing,
|
||||
@ -146,6 +147,20 @@ mkParser table cib =
|
||||
_tciExtraTableMetadata = ()
|
||||
}
|
||||
|
||||
pk :: Maybe (PrimaryKey PG (ColumnInfo PG))
|
||||
pk = case pks of
|
||||
Nothing -> Nothing
|
||||
Just primaryColumns ->
|
||||
Just
|
||||
PrimaryKey
|
||||
{ _pkConstraint =
|
||||
Constraint
|
||||
{ _cName = ConstraintName "",
|
||||
_cOid = OID 0
|
||||
},
|
||||
_pkColumns = primaryColumns
|
||||
}
|
||||
|
||||
rolePermInfo :: RolePermInfo PG
|
||||
rolePermInfo =
|
||||
RolePermInfo
|
||||
@ -194,6 +209,7 @@ mkParser table cib =
|
||||
_tcrfInsertOne = customRootField,
|
||||
_tcrfUpdate = customRootField,
|
||||
_tcrfUpdateByPk = customRootField,
|
||||
_tcrfUpdateMany = customRootField,
|
||||
_tcrfDelete = customRootField,
|
||||
_tcrfDeleteByPk = customRootField
|
||||
}
|
||||
@ -206,7 +222,7 @@ mkParser table cib =
|
||||
}
|
||||
------------------------------------------
|
||||
name :: C.GQLNameIdentifier
|
||||
name = C.fromName $ unsafeMkName "test"
|
||||
name = C.fromName $ unsafeMkName $ getTableTxt $ qName table
|
||||
|
||||
toHashPair :: ColumnInfoBuilder -> (FieldName, FieldInfo PG)
|
||||
toHashPair cib = (coerce $ cibName cib, FIColumn $ mkColumnInfo cib)
|
||||
|
@ -7,6 +7,7 @@ module Harness.Test.Schema
|
||||
Reference (..),
|
||||
Column (..),
|
||||
ScalarType (..),
|
||||
defaultSerialType,
|
||||
ScalarValue (..),
|
||||
UniqueConstraint (..),
|
||||
BackendScalarType (..),
|
||||
@ -232,6 +233,17 @@ backendScalarValue bsv fn = case fn bsv of
|
||||
Nothing -> error $ "backendScalarValue: Retrieved value is Nothing, passed " <> show bsv
|
||||
Just scalarValue -> scalarValue
|
||||
|
||||
defaultSerialType :: ScalarType
|
||||
defaultSerialType =
|
||||
TCustomType $
|
||||
defaultBackendScalarType
|
||||
{ bstMysql = Nothing,
|
||||
bstMssql = Just "INT IDENTITY(1,1)",
|
||||
bstCitus = Just "SERIAL",
|
||||
bstPostgres = Just "SERIAL",
|
||||
bstBigQuery = Nothing
|
||||
}
|
||||
|
||||
-- | Helper function to construct 'Column's with common defaults
|
||||
column :: Text -> ScalarType -> Column
|
||||
column name typ = Column name typ False Nothing
|
||||
|
@ -72,7 +72,7 @@ alldefaults :: Schema.Table
|
||||
alldefaults =
|
||||
(table "alldefaults")
|
||||
{ tableColumns =
|
||||
[ Schema.column "id" defaultSerialType,
|
||||
[ Schema.column "id" Schema.defaultSerialType,
|
||||
Schema.column "dt" defaultDateTimeType
|
||||
],
|
||||
tablePrimaryKey = ["id"]
|
||||
@ -82,7 +82,7 @@ somedefaults :: Schema.Table
|
||||
somedefaults =
|
||||
(table "somedefaults")
|
||||
{ tableColumns =
|
||||
[ Schema.column "id" defaultSerialType,
|
||||
[ Schema.column "id" Schema.defaultSerialType,
|
||||
Schema.column "dt" defaultDateTimeType,
|
||||
Schema.column "name" Schema.TStr
|
||||
],
|
||||
@ -93,7 +93,7 @@ withrelationship :: Schema.Table
|
||||
withrelationship =
|
||||
(table "withrelationship")
|
||||
{ tableColumns =
|
||||
[ Schema.column "id" defaultSerialType,
|
||||
[ Schema.column "id" Schema.defaultSerialType,
|
||||
Schema.column "nickname" Schema.TStr,
|
||||
Schema.column "time_id" Schema.TInt
|
||||
],
|
||||
@ -101,17 +101,6 @@ withrelationship =
|
||||
tableReferences = [Schema.Reference "time_id" "alldefaults" "id"]
|
||||
}
|
||||
|
||||
defaultSerialType :: Schema.ScalarType
|
||||
defaultSerialType =
|
||||
Schema.TCustomType $
|
||||
Schema.defaultBackendScalarType
|
||||
{ Schema.bstMysql = Nothing,
|
||||
Schema.bstMssql = Just "INT IDENTITY(1,1)",
|
||||
Schema.bstCitus = Just "SERIAL",
|
||||
Schema.bstPostgres = Just "SERIAL",
|
||||
Schema.bstBigQuery = Nothing
|
||||
}
|
||||
|
||||
defaultDateTimeType :: Schema.ScalarType
|
||||
defaultDateTimeType =
|
||||
Schema.TCustomType $
|
||||
|
239
server/tests-hspec/Test/UpdateManySpec.hs
Normal file
239
server/tests-hspec/Test/UpdateManySpec.hs
Normal file
@ -0,0 +1,239 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
-- | Test multiple updates (update_table_many).
|
||||
module Test.UpdateManySpec (spec) where
|
||||
|
||||
import Harness.Backend.Postgres qualified as Postgres
|
||||
import Harness.GraphqlEngine qualified as GraphqlEngine
|
||||
import Harness.Quoter.Graphql (graphql)
|
||||
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
||||
import Harness.Test.Context qualified as Context
|
||||
import Harness.Test.Schema (Table (..), table)
|
||||
import Harness.Test.Schema qualified as Schema
|
||||
import Harness.TestEnvironment (TestEnvironment)
|
||||
import Test.Hspec (SpecWith, describe, it)
|
||||
import Prelude
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- ** Preamble
|
||||
|
||||
spec :: SpecWith TestEnvironment
|
||||
spec =
|
||||
describe "UpdateManySpec" $ Context.run [postgresContext] tests
|
||||
where
|
||||
postgresContext =
|
||||
Context.Context
|
||||
{ name = Context.Backend Context.Postgres,
|
||||
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
|
||||
setup = Postgres.setup schema,
|
||||
teardown = Postgres.teardown schema,
|
||||
customOptions = Nothing
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- ** Schema
|
||||
|
||||
schema :: [Schema.Table]
|
||||
schema =
|
||||
[ artist,
|
||||
album
|
||||
]
|
||||
|
||||
artist :: Schema.Table
|
||||
artist =
|
||||
(table "artist")
|
||||
{ tableColumns =
|
||||
[ Schema.column "id" Schema.defaultSerialType,
|
||||
Schema.column "name" Schema.TStr
|
||||
],
|
||||
tablePrimaryKey = ["id"],
|
||||
tableData =
|
||||
[ [Schema.VInt 1, Schema.VStr "first"],
|
||||
[Schema.VInt 2, Schema.VStr "second"],
|
||||
[Schema.VInt 3, Schema.VStr "third"]
|
||||
]
|
||||
}
|
||||
|
||||
album :: Schema.Table
|
||||
album =
|
||||
(table "album")
|
||||
{ tableColumns =
|
||||
[ Schema.column "id" Schema.defaultSerialType,
|
||||
Schema.column "albumname" Schema.TStr,
|
||||
Schema.column "artist_id" Schema.TInt
|
||||
],
|
||||
tablePrimaryKey = ["albumname"],
|
||||
tableReferences = [Schema.Reference "artist_id" "artist" "id"],
|
||||
tableData =
|
||||
[ [Schema.VInt 1, Schema.VStr "first album", Schema.VInt 1],
|
||||
[Schema.VInt 2, Schema.VStr "second album", Schema.VInt 2],
|
||||
[Schema.VInt 3, Schema.VStr "third album", Schema.VInt 3]
|
||||
]
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- * Tests
|
||||
|
||||
tests :: Context.Options -> SpecWith TestEnvironment
|
||||
tests opts = do
|
||||
it "Update no records" $ \testEnvironment ->
|
||||
shouldReturnYaml
|
||||
opts
|
||||
( GraphqlEngine.postGraphql
|
||||
testEnvironment
|
||||
[graphql|
|
||||
mutation {
|
||||
update_hasura_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 10 } }
|
||||
_set: { name: "test" }
|
||||
}
|
||||
]
|
||||
){
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
|]
|
||||
)
|
||||
[yaml|
|
||||
data:
|
||||
update_hasura_artist_many:
|
||||
- affected_rows: 0
|
||||
|]
|
||||
|
||||
it "Update single record" $ \testEnvironment ->
|
||||
shouldReturnYaml
|
||||
opts
|
||||
( GraphqlEngine.postGraphql
|
||||
testEnvironment
|
||||
[graphql|
|
||||
mutation {
|
||||
update_hasura_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 10 } }
|
||||
_set: { name: "test" }
|
||||
}
|
||||
{ where: { id: { _gt: 2 } }
|
||||
_set: { name: "test" }
|
||||
}
|
||||
]
|
||||
){
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
|]
|
||||
)
|
||||
[yaml|
|
||||
data:
|
||||
update_hasura_artist_many:
|
||||
- affected_rows: 0
|
||||
- affected_rows: 1
|
||||
|]
|
||||
|
||||
it "Update record multiple times" $ \testEnvironment ->
|
||||
shouldReturnYaml
|
||||
opts
|
||||
( GraphqlEngine.postGraphql
|
||||
testEnvironment
|
||||
[graphql|
|
||||
mutation {
|
||||
update_hasura_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _gt: 2 } }
|
||||
_set: { name: "test" }
|
||||
}
|
||||
{ where: { name: { _eq: "test" } }
|
||||
_set: { name: "changed name" }
|
||||
}
|
||||
]
|
||||
){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|]
|
||||
)
|
||||
[yaml|
|
||||
data:
|
||||
update_hasura_artist_many:
|
||||
- affected_rows: 1
|
||||
returning:
|
||||
- name: "test"
|
||||
- affected_rows: 1
|
||||
returning:
|
||||
- name: "changed name"
|
||||
|]
|
||||
|
||||
it "Update record multiple times with overlapping conditions" $ \testEnvironment ->
|
||||
shouldReturnYaml
|
||||
opts
|
||||
( GraphqlEngine.postGraphql
|
||||
testEnvironment
|
||||
[graphql|
|
||||
mutation {
|
||||
update_hasura_artist_many(
|
||||
updates: [
|
||||
{ where: { id: { _gt: 2 } }
|
||||
_set: { name: "test" }
|
||||
}
|
||||
{ where: { id: { _eq: 3 } }
|
||||
_set: { name: "changed name" }
|
||||
}
|
||||
]
|
||||
){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|]
|
||||
)
|
||||
[yaml|
|
||||
data:
|
||||
update_hasura_artist_many:
|
||||
- affected_rows: 1
|
||||
returning:
|
||||
- name: "test"
|
||||
- affected_rows: 1
|
||||
returning:
|
||||
- name: "changed name"
|
||||
|]
|
||||
|
||||
it "Revert on error" $ \testEnvironment ->
|
||||
shouldReturnYaml
|
||||
opts
|
||||
( GraphqlEngine.postGraphql
|
||||
testEnvironment
|
||||
[graphql|
|
||||
mutation {
|
||||
update_hasura_album_many(
|
||||
updates: [
|
||||
{ where: { id: { _eq: 1 } }
|
||||
_set: { albumname: "test" }
|
||||
}
|
||||
{ where: { id: { _eq: 1 } }
|
||||
_set: { artist_id: 4 }
|
||||
}
|
||||
]
|
||||
){
|
||||
affected_rows
|
||||
returning {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|]
|
||||
)
|
||||
[yaml|
|
||||
errors:
|
||||
- extensions:
|
||||
code: validation-failed
|
||||
path: $.selectionSet.update_hasura_album_many.selectionSet.returning.selectionSet.name
|
||||
message: 'field "name" not found in type: ''hasura_album'''
|
||||
|]
|
@ -103,6 +103,12 @@ response:
|
||||
- name: UpdateExplicitCommentInMetadata
|
||||
description: Explicit comment on update
|
||||
|
||||
- name: UpdateManyAutomaticNoCommentInDb
|
||||
description: 'update multiples rows of table: "automatic_no_comment_in_db"'
|
||||
|
||||
- name: UpdateManyExplicitCommentInMetadata
|
||||
description: Explicit comment on update_many
|
||||
|
||||
- name: delete_automatic_comment_in_db
|
||||
description: 'delete data from the table: "automatic_comment_in_db"'
|
||||
|
||||
@ -133,8 +139,14 @@ response:
|
||||
- name: update_automatic_comment_in_db_by_pk
|
||||
description: 'update single row of the table: "automatic_comment_in_db"'
|
||||
|
||||
- name: update_automatic_comment_in_db_many
|
||||
description: 'update multiples rows of table: "automatic_comment_in_db"'
|
||||
|
||||
- name: update_explicit_no_comment_in_metadata
|
||||
description: null
|
||||
|
||||
- name: update_explicit_no_comment_in_metadata_by_pk
|
||||
description: null
|
||||
|
||||
- name: update_explicit_no_comment_in_metadata_many
|
||||
description: null
|
||||
|
@ -22,6 +22,7 @@ args:
|
||||
insert: InsertAutomaticNoCommentInDb
|
||||
insert_one: InsertOneAutomaticNoCommentInDb
|
||||
update: UpdateAutomaticNoCommentInDb
|
||||
update_many: UpdateManyAutomaticNoCommentInDb
|
||||
update_by_pk: UpdateByPkAutomaticNoCommentInDb
|
||||
delete: DeleteAutomaticNoCommentInDb
|
||||
delete_by_pk: DeleteByPkAutomaticNoCommentInDb
|
||||
@ -60,6 +61,9 @@ args:
|
||||
update_by_pk:
|
||||
name: UpdateByPkExplicitCommentInMetadata
|
||||
comment: Explicit comment on update_by_pk
|
||||
update_many:
|
||||
name: UpdateManyExplicitCommentInMetadata
|
||||
comment: Explicit comment on update_many
|
||||
delete:
|
||||
name: DeleteExplicitCommentInMetadata
|
||||
comment: Explicit comment on delete
|
||||
@ -105,6 +109,9 @@ args:
|
||||
update_by_pk:
|
||||
name: null
|
||||
comment: ""
|
||||
update_many:
|
||||
name: null
|
||||
comment: ""
|
||||
delete:
|
||||
name: null
|
||||
comment: ""
|
||||
|
@ -4,7 +4,7 @@ url: /v1/query
|
||||
response:
|
||||
code: unexpected
|
||||
error: 'found duplicate fields in selection set for mutation root: update_article,
|
||||
delete_article, insert_article_one, update_article_by_pk, delete_article_by_pk,
|
||||
delete_article, update_article_many, insert_article_one, update_article_by_pk, delete_article_by_pk,
|
||||
insert_article'
|
||||
path: $.args
|
||||
query:
|
||||
|
@ -16,7 +16,7 @@
|
||||
, "^Data.Aeson.Ordered.*"
|
||||
, "^Data.HashMap.Strict.Multi.*"
|
||||
, "^Data.HashMap.Strict.NonEmpty.*"
|
||||
, "^Data.Sequence.NonEmpty.*"
|
||||
, "^Data.Sequence.NESeq.*"
|
||||
, "^Data.Text.Casing.*"
|
||||
, "^Data.Text.Extended.*"
|
||||
, "^Data.Trie.*"
|
||||
|
Loading…
Reference in New Issue
Block a user