{-# LANGUAGE ApplicativeDo #-} -- | This module contains the building blocks for parsing @on_conflict@ clauses, -- which in the Postgres backend are used to implement upsert functionality. -- These are used by 'Hasura.Backends.Postgres.Instances.Schema.backendInsertParser' to -- construct a postgres-specific schema parser for insert (and upsert) mutations. module Hasura.Backends.Postgres.Schema.OnConflict ( onConflictFieldParser, ) where import Data.Text.Extended import Hasura.GraphQL.Parser ( InputFieldsParser, Kind (..), Parser, UnpreparedValue (..), ) import Hasura.GraphQL.Parser qualified as P import Hasura.GraphQL.Parser.Class import Hasura.GraphQL.Schema.Backend import Hasura.GraphQL.Schema.BoolExp import Hasura.GraphQL.Schema.Common import Hasura.GraphQL.Schema.Table import Hasura.Prelude import Hasura.RQL.IR.Insert qualified as IR import Hasura.RQL.Types import Language.GraphQL.Draft.Syntax qualified as G -- | Parser for a field name @on_conflict@ of type @tablename_on_conflict@. -- -- The @tablename_on_conflict@ object is used to generate the @ON CONFLICT@ -- SQL clause, indicating what should be done if an insert raises a conflict. -- -- The types ordinarily produced by this parser are only created if the table has -- unique or primary keys constraints. -- -- If there are no columns for which the current role has update permissions, we -- must still accept an empty list for @update_columns@ in the name of -- backwards compatibility. We do this by adding a placeholder value to the -- enum. See . onConflictFieldParser :: forall pgKind r m n. MonadBuildSchema ('Postgres pgKind) r m n => SourceName -> TableInfo ('Postgres pgKind) -> Maybe (SelPermInfo ('Postgres pgKind)) -> Maybe (UpdPermInfo ('Postgres pgKind)) -> m (InputFieldsParser n (Maybe (IR.ConflictClauseP1 ('Postgres pgKind) (UnpreparedValue ('Postgres pgKind))))) onConflictFieldParser sourceName tableInfo selectPerms updatePerms = do let maybeConstraints = tciUniqueOrPrimaryKeyConstraints . _tiCoreInfo $ tableInfo let maybeConflictObject = conflictObjectParser sourceName tableInfo <$> maybeConstraints <*> pure selectPerms <*> updatePerms case maybeConflictObject of Just conflictObject -> conflictObject <&> P.fieldOptional $$(G.litName "on_conflict") (Just "upsert condition") Nothing -> return $ pure Nothing -- | Create a parser for the @_on_conflict@ object of the given table. conflictObjectParser :: forall pgKind r m n. MonadBuildSchema ('Postgres pgKind) r m n => SourceName -> TableInfo ('Postgres pgKind) -> NonEmpty (Constraint ('Postgres pgKind)) -> Maybe (SelPermInfo ('Postgres pgKind)) -> UpdPermInfo ('Postgres pgKind) -> m (Parser 'Input n (IR.ConflictClauseP1 ('Postgres pgKind) (UnpreparedValue ('Postgres pgKind)))) conflictObjectParser sourceName tableInfo constraints selectPerms updatePerms = do updateColumnsEnum <- updateColumnsPlaceholderParser constraintParser <- conflictConstraint constraints sourceName tableInfo whereExpParser <- boolExp sourceName tableInfo selectPerms tableGQLName <- getTableGQLName tableInfo objectName <- P.mkTypename $ tableGQLName <> $$(G.litName "_on_conflict") let presetColumns = partialSQLExpToUnpreparedValue <$> upiSet updatePerms updateFilter = fmap partialSQLExpToUnpreparedValue <$> upiFilter updatePerms objectDesc = G.Description $ "on_conflict condition type for table " <>> tableName constraintName = $$(G.litName "constraint") columnsName = $$(G.litName "update_columns") whereExpName = $$(G.litName "where") pure $ P.object objectName (Just objectDesc) $ do constraint <- IR.CTConstraint <$> P.field constraintName Nothing constraintParser whereExp <- P.fieldOptional whereExpName Nothing whereExpParser updateColumns <- P.fieldWithDefault columnsName Nothing (G.VList []) (P.list updateColumnsEnum) `P.bindFields` \cs -> -- this can only happen if the placeholder was used sequenceA cs `onNothing` parseError "erroneous column name" pure $ case updateColumns of [] -> IR.CP1DoNothing $ Just constraint _ -> IR.CP1Update constraint updateColumns presetColumns $ BoolAnd $ updateFilter : maybeToList whereExp where tableName = tableInfoName tableInfo -- If there's no column for which the current user has "update" -- permissions, this functions returns an enum that only contains a -- placeholder, so as to still allow this type to exist in the schema. updateColumnsPlaceholderParser :: m (Parser 'Both n (Maybe (Column ('Postgres pgKind)))) updateColumnsPlaceholderParser = do maybeEnum <- tableUpdateColumnsEnum tableInfo updatePerms case maybeEnum of Just e -> pure $ Just <$> e Nothing -> do tableGQLName <- getTableGQLName tableInfo enumName <- P.mkTypename $ tableGQLName <> $$(G.litName "_update_column") pure $ P.enum enumName (Just $ G.Description $ "placeholder for update columns of table " <> tableName <<> " (current role has no relevant permissions)") $ pure ( P.Definition @P.EnumValueInfo $$(G.litName "_PLACEHOLDER") (Just $ G.Description "placeholder (do not use)") P.EnumValueInfo, Nothing ) -- | Constructs a Parser for the name of the constraints on a given table. -- -- The TableCoreInfo of a given table contains a list of unique or primary key -- constraints. Given the list of such constraints, this function creates a -- parser for an enum type that matches it. This function makes no attempt at -- de-duplicating contraint names, and assumes they are correct. -- -- This function can fail if a constraint has a name that cannot be translated -- to a GraphQL name (see hasura/graphql-engine-mono#1748). conflictConstraint :: forall pgKind r m n. MonadBuildSchema ('Postgres pgKind) r m n => NonEmpty (Constraint ('Postgres pgKind)) -> SourceName -> TableInfo ('Postgres pgKind) -> m (Parser 'Both n (ConstraintName ('Postgres pgKind))) conflictConstraint constraints sourceName tableInfo = memoizeOn 'conflictConstraint (sourceName, tableName) $ do tableGQLName <- getTableGQLName tableInfo constraintEnumValues <- for constraints \constraint -> do name <- textToName $ toTxt $ _cName constraint pure ( P.Definition name (Just "unique or primary key constraint") P.EnumValueInfo, _cName constraint ) enumName <- P.mkTypename $ tableGQLName <> $$(G.litName "_constraint") let enumDesc = G.Description $ "unique or primary key constraints on table " <>> tableName pure $ P.enum enumName (Just enumDesc) constraintEnumValues where tableName = tableInfoName tableInfo