{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# 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(..)
, onConflictDefault, onConflict
, conflictingConstraint
, BeamHasInsertOnConflict(..)
, onConflictUpdateAll
, onConflictUpdateInstead
, 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.Backend.SQL.BeamExtensions
import Database.Beam.Schema.Tables
import Database.Beam.Postgres.Types
import Database.Beam.Postgres.Syntax
import Control.Monad.Free.Church
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 QInternal) -> PgInsertOnConflictSyntax)
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
=> SqlConflictTarget Postgres tbl
-> SqlConflictAction Postgres 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)
conflictingConstraint :: T.Text -> SqlConflictTarget Postgres tbl
conflictingConstraint nm =
PgInsertOnConflictTarget $ \_ ->
PgInsertOnConflictTargetSyntax $
emit "ON CONSTRAINT " <> pgQuotedIdentifier nm <> emit " "
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
instance BeamHasInsertOnConflict Postgres where
newtype SqlConflictTarget Postgres table =
PgInsertOnConflictTarget (table (QExpr Postgres QInternal) -> PgInsertOnConflictTargetSyntax)
newtype SqlConflictAction Postgres table =
PgConflictAction (table (QField QInternal) -> PgConflictActionSyntax)
insertOnConflict tbl vs target action = insert tbl vs $ onConflict target action
anyConflict = PgInsertOnConflictTarget (\_ -> PgInsertOnConflictTargetSyntax mempty)
onConflictDoNothing = PgConflictAction $ \_ -> PgConflictActionSyntax (emit "DO NOTHING")
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 mkAssignments where_ =
PgConflictAction $ \tbl ->
let QAssignment assignments = mkAssignments tbl tblExcluded
QExpr where_' = where_ 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 <> emit " WHERE " <> fromPgExpression (where_' "t")
conflictingFields makeProjection =
PgInsertOnConflictTarget $ \tbl ->
PgInsertOnConflictTargetSyntax $
pgParens (pgSepBy (emit ", ") $
map fromPgExpression $
project (Proxy @Postgres) (makeProjection tbl) "t") <>
emit " "
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 " "