server: postgres multiple updates

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4837
GitOrigin-RevId: 505f669298298fd004dfc4e84eaa0d21df055216
This commit is contained in:
Evie Ciobanu 2022-07-18 18:15:34 +03:00 committed by hasura-bot
parent 4f3fc9853b
commit d76aab99e1
35 changed files with 1019 additions and 165 deletions

View File

@ -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

View File

@ -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

View File

@ -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 (:||>),

View File

@ -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

View File

@ -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 (..))

View File

@ -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

View File

@ -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,

View File

@ -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 $

View File

@ -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.

View File

@ -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

View File

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

View File

@ -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 $

View File

@ -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.

View File

@ -49,6 +49,8 @@ module Hasura.GraphQL.Schema.Build
buildTableQueryAndSubscriptionFields,
buildTableStreamingSubscriptionFields,
buildTableUpdateMutationFields,
setFieldNameCase,
buildFieldDescription,
)
where

View File

@ -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

View File

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

View File

@ -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

View File

@ -12,6 +12,7 @@ module Hasura.GraphQL.Schema.Update
incOp,
updateTable,
updateTableByPk,
mkUpdateObject,
)
where

View File

@ -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|]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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
}

View File

@ -361,6 +361,7 @@ instance Arbitrary TableCustomRootFields where
<*> arbitrary
<*> arbitrary
<*> arbitrary
<*> arbitrary
)
`suchThat` allFieldNamesAreUnique
where

View File

@ -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

View File

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

View File

@ -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

View File

@ -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 $

View 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'''
|]

View File

@ -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

View File

@ -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: ""

View File

@ -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:

View File

@ -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.*"