{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Database.Beam.Postgres.Full
(
PgWithLocking, PgLockedTables
, PgSelectLockingStrength(..), PgSelectLockingOptions(..)
, lockingAllTablesFor_, lockingFor_
, locked_, lockAll_, withLocks_
, lateral_
, insert, insertReturning
, insertDefaults
, runPgInsertReturningList
, PgInsertReturning(..)
, PgInsertOnConflict(..), PgInsertOnConflictTarget(..)
, PgConflictAction(..)
, onConflictDefault, onConflict, anyConflict, conflictingFields
, conflictingFieldsWhere, conflictingConstraint
, onConflictDoNothing, onConflictUpdateSet
, onConflictUpdateSetWhere, onConflictUpdateInstead
, onConflictSetAll
, PgUpdateReturning(..)
, runPgUpdateReturningList
, updateReturning
, PgDeleteReturning(..)
, runPgDeleteReturningList
, deleteReturning
, PgReturning(..)
) where
import Database.Beam hiding (insert, insertValues)
import Database.Beam.Query.Internal
import Database.Beam.Backend.SQL
import Database.Beam.Schema.Tables
import Database.Beam.Postgres.Types
import Database.Beam.Postgres.Syntax
import Control.Monad.Free.Church
import Control.Monad.Writer (execWriter, tell)
import Data.Functor.Const
import Data.Proxy (Proxy(..))
import qualified Data.Text as T
#if !MIN_VERSION_base(4, 11, 0)
import Data.Semigroup
#endif
newtype PgLockedTables s = PgLockedTables [ T.Text ]
deriving (Semigroup, Monoid)
data PgWithLocking s a = PgWithLocking (PgLockedTables s) a
instance ProjectibleWithPredicate c be res a => ProjectibleWithPredicate c be res (PgWithLocking s a) where
project' p be mutateM (PgWithLocking tbls a) =
PgWithLocking tbls <$> project' p be mutateM a
projectSkeleton' ctxt be mkM =
PgWithLocking mempty <$> projectSkeleton' ctxt be mkM
lockAll_ :: a -> PgWithLocking s a
lockAll_ = PgWithLocking mempty
withLocks_ :: a -> PgLockedTables s -> PgWithLocking s a
withLocks_ = flip PgWithLocking
locked_ :: (Beamable tbl, Database Postgres db)
=> DatabaseEntity Postgres db (TableEntity tbl)
-> Q Postgres db s (PgLockedTables s, tbl (QExpr Postgres s))
locked_ (DatabaseEntity dt) = do
(nm, joined) <- Q (liftF (QAll (\_ -> fromTable (tableNamed (tableName (dbTableSchema dt) (dbTableCurrentName dt))) .
Just . (,Nothing))
(tableFieldsToExpressions (dbTableSettings dt))
(\_ -> Nothing) id))
pure (PgLockedTables [nm], joined)
lockingFor_ :: forall a db s
. ( Database Postgres db, Projectible Postgres a, ThreadRewritable (QNested s) a )
=> PgSelectLockingStrength
-> Maybe PgSelectLockingOptions
-> Q Postgres db (QNested s) (PgWithLocking (QNested s) a)
-> Q Postgres db s (WithRewrittenThread (QNested s) s a)
lockingFor_ lockStrength mLockOptions (Q q) =
Q (liftF (QForceSelect (\(PgWithLocking (PgLockedTables tblNms) _) tbl ords limit offset ->
let locking = PgSelectLockingClauseSyntax lockStrength tblNms mLockOptions
in pgSelectStmt tbl ords limit offset (Just locking))
q (\(PgWithLocking _ a) -> rewriteThread (Proxy @s) a)))
lockingAllTablesFor_ :: ( Database Postgres db, Projectible Postgres a, ThreadRewritable (QNested s) a )
=> PgSelectLockingStrength
-> Maybe PgSelectLockingOptions
-> Q Postgres db (QNested s) a
-> Q Postgres db s (WithRewrittenThread (QNested s) s a)
lockingAllTablesFor_ lockStrength mLockOptions q =
lockingFor_ lockStrength mLockOptions (lockAll_ <$> q)
insertDefaults :: SqlInsertValues Postgres tbl
insertDefaults = SqlInsertValues (PgInsertValuesSyntax (emit "DEFAULT VALUES"))
insert :: DatabaseEntity Postgres db (TableEntity table)
-> SqlInsertValues Postgres (table (QExpr Postgres s))
-> PgInsertOnConflict table
-> SqlInsert Postgres table
insert tbl@(DatabaseEntity dt@(DatabaseTable {})) values onConflict_ =
case insertReturning tbl values onConflict_
(Nothing :: Maybe (table (QExpr Postgres PostgresInaccessible) -> QExpr Postgres PostgresInaccessible Int)) of
PgInsertReturning a ->
SqlInsert (dbTableSettings dt) (PgInsertSyntax a)
PgInsertReturningEmpty ->
SqlInsertNoRows
data PgInsertReturning a
= PgInsertReturning PgSyntax
| PgInsertReturningEmpty
insertReturning :: Projectible Postgres a
=> DatabaseEntity Postgres be (TableEntity table)
-> SqlInsertValues Postgres (table (QExpr Postgres s))
-> PgInsertOnConflict table
-> Maybe (table (QExpr Postgres PostgresInaccessible) -> a)
-> PgInsertReturning (QExprToIdentity a)
insertReturning _ SqlInsertValuesEmpty _ _ = PgInsertReturningEmpty
insertReturning (DatabaseEntity tbl@(DatabaseTable {}))
(SqlInsertValues (PgInsertValuesSyntax insertValues_))
(PgInsertOnConflict mkOnConflict)
mMkProjection =
PgInsertReturning $
emit "INSERT INTO " <> fromPgTableName (tableName (dbTableSchema tbl) (dbTableCurrentName tbl)) <>
emit "(" <> pgSepBy (emit ", ") (allBeamValues (\(Columnar' f) -> pgQuotedIdentifier (_fieldName f)) tblSettings) <> emit ") " <>
insertValues_ <> emit " " <> fromPgInsertOnConflict (mkOnConflict tblFields) <>
(case mMkProjection of
Nothing -> mempty
Just mkProjection ->
emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t")))
where
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr (\_ -> fieldE (unqualifiedField (_fieldName f))))) tblSettings
tblFields = changeBeamRep (\(Columnar' f) -> Columnar' (QField True (dbTableCurrentName tbl) (_fieldName f))) tblSettings
tblSettings = dbTableSettings tbl
runPgInsertReturningList
:: ( MonadBeam be m
, BeamSqlBackendSyntax be ~ PgCommandSyntax
, FromBackendRow be a
)
=> PgInsertReturning a
-> m [a]
runPgInsertReturningList = \case
PgInsertReturningEmpty -> pure []
PgInsertReturning syntax -> runReturningList $ PgCommandSyntax PgCommandTypeDataUpdateReturning syntax
newtype PgInsertOnConflict (tbl :: (* -> *) -> *) =
PgInsertOnConflict (tbl (QField PostgresInaccessible) -> PgInsertOnConflictSyntax)
newtype PgInsertOnConflictTarget (tbl :: (* -> *) -> *) =
PgInsertOnConflictTarget (tbl (QExpr Postgres PostgresInaccessible) -> PgInsertOnConflictTargetSyntax)
newtype PgConflictAction (tbl :: (* -> *) -> *) =
PgConflictAction (tbl (QField PostgresInaccessible) -> PgConflictActionSyntax)
lateral_ :: forall s a b db
. ( ThreadRewritable s a, ThreadRewritable (QNested s) b, Projectible Postgres b )
=> a -> (WithRewrittenThread s (QNested s) a -> Q Postgres db (QNested s) b)
-> Q Postgres db s (WithRewrittenThread (QNested s) s b)
lateral_ using mkSubquery = do
let Q subquery = mkSubquery (rewriteThread (Proxy @(QNested s)) using)
Q (liftF (QArbitraryJoin subquery
(\a b on' ->
case on' of
Nothing ->
PgFromSyntax $
fromPgFrom a <> emit " CROSS JOIN LATERAL " <> fromPgFrom b
Just on'' ->
PgFromSyntax $
fromPgFrom a <> emit " JOIN LATERAL " <> fromPgFrom b <> emit " ON " <> fromPgExpression on'')
(\_ -> Nothing)
(rewriteThread (Proxy @s))))
onConflictDefault :: PgInsertOnConflict tbl
onConflictDefault = PgInsertOnConflict (\_ -> PgInsertOnConflictSyntax mempty)
onConflict :: Beamable tbl
=> PgInsertOnConflictTarget tbl
-> PgConflictAction tbl
-> PgInsertOnConflict tbl
onConflict (PgInsertOnConflictTarget tgt) (PgConflictAction update_) =
PgInsertOnConflict $ \tbl ->
let exprTbl = changeBeamRep (\(Columnar' (QField _ _ nm)) ->
Columnar' (QExpr (\_ -> fieldE (unqualifiedField nm))))
tbl
in PgInsertOnConflictSyntax $
emit "ON CONFLICT " <> fromPgInsertOnConflictTarget (tgt exprTbl)
<> fromPgConflictAction (update_ tbl)
anyConflict :: PgInsertOnConflictTarget tbl
anyConflict = PgInsertOnConflictTarget (\_ -> PgInsertOnConflictTargetSyntax mempty)
conflictingFields :: Projectible Postgres proj
=> (tbl (QExpr Postgres PostgresInaccessible) -> proj)
-> PgInsertOnConflictTarget tbl
conflictingFields makeProjection =
PgInsertOnConflictTarget $ \tbl ->
PgInsertOnConflictTargetSyntax $
pgParens (pgSepBy (emit ", ") $
map fromPgExpression $
project (Proxy @Postgres) (makeProjection tbl) "t") <>
emit " "
conflictingFieldsWhere :: Projectible Postgres proj
=> (tbl (QExpr Postgres PostgresInaccessible) -> proj)
-> (tbl (QExpr Postgres PostgresInaccessible) ->
QExpr Postgres PostgresInaccessible Bool)
-> PgInsertOnConflictTarget tbl
conflictingFieldsWhere makeProjection makeWhere =
PgInsertOnConflictTarget $ \tbl ->
PgInsertOnConflictTargetSyntax $
pgParens (pgSepBy (emit ", ") $
map fromPgExpression (project (Proxy @Postgres)
(makeProjection tbl) "t")) <>
emit " WHERE " <>
pgParens (let QExpr mkE = makeWhere tbl
PgExpressionSyntax e = mkE "t"
in e) <>
emit " "
conflictingConstraint :: T.Text -> PgInsertOnConflictTarget tbl
conflictingConstraint nm =
PgInsertOnConflictTarget $ \_ ->
PgInsertOnConflictTargetSyntax $
emit "ON CONSTRAINT " <> pgQuotedIdentifier nm <> emit " "
onConflictDoNothing :: PgConflictAction tbl
onConflictDoNothing = PgConflictAction $ \_ -> PgConflictActionSyntax (emit "DO NOTHING")
onConflictUpdateSet :: Beamable tbl
=> (tbl (QField PostgresInaccessible) ->
tbl (QExpr Postgres PostgresInaccessible) ->
QAssignment Postgres PostgresInaccessible)
-> PgConflictAction tbl
onConflictUpdateSet mkAssignments =
PgConflictAction $ \tbl ->
let QAssignment assignments = mkAssignments tbl tblExcluded
tblExcluded = changeBeamRep (\(Columnar' (QField _ _ nm)) -> Columnar' (QExpr (\_ -> fieldE (qualifiedField "excluded" nm)))) tbl
assignmentSyntaxes =
[ fromPgFieldName fieldNm <> emit "=" <> pgParens (fromPgExpression expr)
| (fieldNm, expr) <- assignments ]
in PgConflictActionSyntax $
emit "DO UPDATE SET " <> pgSepBy (emit ", ") assignmentSyntaxes
onConflictUpdateSetWhere :: Beamable tbl
=> (tbl (QField PostgresInaccessible) ->
tbl (QExpr Postgres PostgresInaccessible) ->
QAssignment Postgres PostgresInaccessible)
-> (tbl (QExpr Postgres PostgresInaccessible) -> QExpr Postgres PostgresInaccessible Bool)
-> PgConflictAction tbl
onConflictUpdateSetWhere mkAssignments where_ =
PgConflictAction $ \tbl ->
let QAssignment assignments = mkAssignments tbl tblExcluded
QExpr where_' = where_ (changeBeamRep (\(Columnar' f) -> Columnar' (current_ f)) tbl)
tblExcluded = changeBeamRep (\(Columnar' (QField _ _ nm)) -> Columnar' (QExpr (\_ -> fieldE (qualifiedField "excluded" nm)))) tbl
assignmentSyntaxes =
[ fromPgFieldName fieldNm <> emit "=" <> pgParens (fromPgExpression expr)
| (fieldNm, expr) <- assignments ]
in PgConflictActionSyntax $
emit "DO UPDATE SET " <> pgSepBy (emit ", ") assignmentSyntaxes <> emit " WHERE " <> fromPgExpression (where_' "t")
onConflictUpdateInstead :: (Beamable tbl, ProjectibleWithPredicate AnyType () T.Text proj)
=> (tbl (Const T.Text) -> proj)
-> PgConflictAction tbl
onConflictUpdateInstead mkProj =
onConflictUpdateSet $ \tbl _ ->
let tblFields = changeBeamRep (\(Columnar' (QField _ _ nm) :: Columnar' (QField PostgresInaccessible) a) -> Columnar' (Const nm) :: Columnar' (Const T.Text) a) tbl
proj = execWriter (project' (Proxy @AnyType) (Proxy @((), T.Text))
(\_ _ e -> tell [e] >> pure e)
(mkProj tblFields))
in QAssignment (map (\fieldNm -> (unqualifiedField fieldNm, fieldE (qualifiedField "excluded" fieldNm))) proj)
onConflictSetAll :: ( Beamable tbl
, ProjectibleWithPredicate AnyType () T.Text (tbl (Const T.Text)) )
=> PgConflictAction tbl
onConflictSetAll = onConflictUpdateInstead id
data PgUpdateReturning a
= PgUpdateReturning PgSyntax
| PgUpdateReturningEmpty
updateReturning :: Projectible Postgres a
=> DatabaseEntity Postgres be (TableEntity table)
-> (forall s. table (QField s) -> QAssignment Postgres s)
-> (forall s. table (QExpr Postgres s) -> QExpr Postgres s Bool)
-> (table (QExpr Postgres PostgresInaccessible) -> a)
-> PgUpdateReturning (QExprToIdentity a)
updateReturning table@(DatabaseEntity (DatabaseTable { dbTableSettings = tblSettings }))
mkAssignments
mkWhere
mkProjection =
case update table mkAssignments mkWhere of
SqlUpdate _ pgUpdate ->
PgUpdateReturning $
fromPgUpdate pgUpdate <>
emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t"))
SqlIdentityUpdate -> PgUpdateReturningEmpty
where
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr (pure (fieldE (unqualifiedField (_fieldName f)))))) tblSettings
runPgUpdateReturningList
:: ( MonadBeam be m
, BeamSqlBackendSyntax be ~ PgCommandSyntax
, FromBackendRow be a
)
=> PgUpdateReturning a
-> m [a]
runPgUpdateReturningList = \case
PgUpdateReturningEmpty -> pure []
PgUpdateReturning syntax -> runReturningList $ PgCommandSyntax PgCommandTypeDataUpdateReturning syntax
newtype PgDeleteReturning a = PgDeleteReturning PgSyntax
deleteReturning :: Projectible Postgres a
=> DatabaseEntity Postgres be (TableEntity table)
-> (forall s. table (QExpr Postgres s) -> QExpr Postgres s Bool)
-> (table (QExpr Postgres PostgresInaccessible) -> a)
-> PgDeleteReturning (QExprToIdentity a)
deleteReturning table@(DatabaseEntity (DatabaseTable { dbTableSettings = tblSettings }))
mkWhere
mkProjection =
PgDeleteReturning $
fromPgDelete pgDelete <>
emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t"))
where
SqlDelete _ pgDelete = delete table mkWhere
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr (pure (fieldE (unqualifiedField (_fieldName f)))))) tblSettings
runPgDeleteReturningList
:: ( MonadBeam be m
, BeamSqlBackendSyntax be ~ PgCommandSyntax
, FromBackendRow be a
)
=> PgDeleteReturning a
-> m [a]
runPgDeleteReturningList (PgDeleteReturning syntax) = runReturningList $ PgCommandSyntax PgCommandTypeDataUpdateReturning syntax
class PgReturning cmd where
type PgReturningType cmd :: * -> *
returning :: (Beamable tbl, Projectible Postgres a)
=> cmd Postgres tbl -> (tbl (QExpr Postgres PostgresInaccessible) -> a)
-> PgReturningType cmd (QExprToIdentity a)
instance PgReturning SqlInsert where
type PgReturningType SqlInsert = PgInsertReturning
returning SqlInsertNoRows _ = PgInsertReturningEmpty
returning (SqlInsert tblSettings (PgInsertSyntax syntax)) mkProjection =
PgInsertReturning $
syntax <> emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t"))
where
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr . pure . fieldE . unqualifiedField . _fieldName $ f)) tblSettings
instance PgReturning SqlUpdate where
type PgReturningType SqlUpdate = PgUpdateReturning
returning SqlIdentityUpdate _ = PgUpdateReturningEmpty
returning (SqlUpdate tblSettings (PgUpdateSyntax syntax)) mkProjection =
PgUpdateReturning $
syntax <> emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t"))
where
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr . pure . fieldE . unqualifiedField . _fieldName $ f)) tblSettings
instance PgReturning SqlDelete where
type PgReturningType SqlDelete = PgDeleteReturning
returning (SqlDelete tblSettings (PgDeleteSyntax syntax)) mkProjection =
PgDeleteReturning $
syntax <> emit " RETURNING " <>
pgSepBy (emit ", ") (map fromPgExpression (project (Proxy @Postgres) (mkProjection tblQ) "t"))
where
tblQ = changeBeamRep (\(Columnar' f) -> Columnar' (QExpr . pure . fieldE . unqualifiedField . _fieldName $ f)) tblSettings