beam-postgres-0.6.2.0: Database/Beam/Postgres/Extensions.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Postgres extensions are run-time loadable plugins that can extend Postgres
-- functionality. Extensions are part of the database schema.
--
-- Beam fully supports including Postgres extensions in Beam databases. The
-- 'PgExtensionEntity' type constructor can be used to declare the existence of
-- the extension in a particular backend. @beam-postgres@ provides predicates
-- and checks for @beam-migrate@ which allow extensions to be included as
-- regular parts of beam migrations.
module Database.Beam.Postgres.Extensions (
-- * Handling extensions
PgExtensionEntity,
getPgExtension,
-- * Defining extensions
IsPgExtension(..),
-- ** Helpers
PgExpr,
LiftPg,
funcE,
-- * Migrations
PgHasExtension(..),
pgCreateExtension,
pgDropExtension,
pgExtensionActionProvider,
) where
import Database.Beam
import Database.Beam.Backend.SQL ( IsSql92ExpressionSyntax(..), IsSql92FieldNameSyntax(..),
IsSql99ExpressionSyntax, IsSql99FunctionExpressionSyntax(..)
)
import Database.Beam.Schema.Tables
import Database.Beam.Postgres.Types
import Database.Beam.Postgres.Syntax
import Database.Beam.Migrate
import Control.Monad
import Data.Aeson
import qualified Data.HashSet as HS
import Data.Hashable (Hashable)
import Data.Proxy
import Data.Text (Text)
-- *** Embedding extensions in databases
-- | Represents an extension in a database.
--
-- For example, to include the "Database.Beam.Postgres.PgCrypto" extension in a
-- database,
--
-- @
-- import Database.Beam.Postgres.PgCrypto
--
-- data MyDatabase entity
-- = MyDatabase
-- { _table1 :: entity (TableEntity Table1)
-- , _cryptoExtension :: entity (PgExtensionEntity PgCrypto)
-- }
--
-- migratableDbSettings :: CheckedDatabaseSettings Postgres MyDatabase
-- migratableDbSettings = defaultMigratableDbSettings
--
-- dbSettings :: DatabaseSettings Postgres MyDatabase
-- dbSettings = unCheckDatabase migratableDbSettings
-- @
--
-- Note that our database now only works in the 'Postgres' backend.
--
-- Extensions are implemented as records of functions and values that expose
-- extension functionality. For example, the @pgcrypto@ extension (implemented
-- by 'PgCrypto') provides cryptographic functions. Thus, 'PgCrypto' is a record
-- of functions over 'QGenExpr' which wrap the underlying postgres
-- functionality.
--
-- You get access to these functions by retrieving them from the entity in the
-- database.
--
-- For example, to use the @pgcrypto@ extension in the database above:
--
-- @
-- let PgCrypto { pgCryptoDigestText = digestText
-- , pgCryptoCrypt = crypt } = getPgExtension (_cryptoExtension dbSettings)
-- in fmap_ (\tbl -> (tbl, crypt (_field1 tbl) (_salt tbl))) (all_ (table1 dbSettings))
-- @
--
-- To implement your own extension, create a record type, and implement the
-- 'IsPgExtension' type class.
data PgExtensionEntity extension
-- | Type class implemented by any Postgresql extension
class IsPgExtension extension where
-- | Return the name of this extension. This should be the string that is
-- passed to @CREATE EXTENSION@. For example, 'PgCrypto' returns @"pgcrypto"@.
pgExtensionName :: Proxy extension -> Text
-- | Return a value of this extension type. This should fill in all fields in
-- the record. For example, 'PgCrypto' builds a record where each function
-- wraps the underlying Postgres one.
pgExtensionBuild :: extension
-- | There are no fields to rename when defining entities
instance RenamableWithRule (FieldRenamer (DatabaseEntityDescriptor Postgres (PgExtensionEntity e))) where
renamingFields _ = FieldRenamer id
instance IsDatabaseEntity Postgres (PgExtensionEntity extension) where
data DatabaseEntityDescriptor Postgres (PgExtensionEntity extension) where
PgDatabaseExtension :: IsPgExtension extension
=> Text
-> extension
-> DatabaseEntityDescriptor Postgres (PgExtensionEntity extension)
type DatabaseEntityDefaultRequirements Postgres (PgExtensionEntity extension) =
( IsPgExtension extension )
type DatabaseEntityRegularRequirements Postgres (PgExtensionEntity extension) =
( IsPgExtension extension )
dbEntityName f (PgDatabaseExtension nm ext) = fmap (\nm' -> PgDatabaseExtension nm' ext) (f nm)
dbEntitySchema _ n = pure n
dbEntityAuto _ = PgDatabaseExtension (pgExtensionName (Proxy @extension)) pgExtensionBuild
instance IsCheckedDatabaseEntity Postgres (PgExtensionEntity extension) where
newtype CheckedDatabaseEntityDescriptor Postgres (PgExtensionEntity extension) =
CheckedPgExtension (DatabaseEntityDescriptor Postgres (PgExtensionEntity extension))
type CheckedDatabaseEntityDefaultRequirements Postgres (PgExtensionEntity extension) =
DatabaseEntityRegularRequirements Postgres (PgExtensionEntity extension)
unChecked f (CheckedPgExtension ext) = CheckedPgExtension <$> f ext
collectEntityChecks (CheckedPgExtension (PgDatabaseExtension {})) =
[ SomeDatabasePredicate (PgHasExtension (pgExtensionName (Proxy @extension))) ]
checkedDbEntityAuto = CheckedPgExtension . dbEntityAuto
-- | Get the extension record from a database entity. See the documentation for
-- 'PgExtensionEntity'.
getPgExtension :: DatabaseEntity Postgres db (PgExtensionEntity extension)
-> extension
getPgExtension (DatabaseEntity (PgDatabaseExtension _ ext)) = ext
-- *** Helpers to write postgres user-defined extensions
-- | @since 0.6.2.0
type PgExpr ctxt s = QGenExpr ctxt Postgres s
-- | @since 0.6.2.0
type family LiftPg ctxt s fn where
LiftPg ctxt s (Maybe a -> b) = Maybe (PgExpr ctxt s a) -> LiftPg ctxt s b
LiftPg ctxt s (a -> b) = PgExpr ctxt s a -> LiftPg ctxt s b
LiftPg ctxt s a = PgExpr ctxt s a
-- | @since 0.6.2.0
funcE :: IsSql99ExpressionSyntax expr => Text -> [expr] -> expr
funcE nm = functionCallE (fieldE (unqualifiedField nm))
-- *** Migrations support for extensions
-- | 'Migration' representing the Postgres @CREATE EXTENSION@ command. Because
-- the extension name is statically known by the extension type and
-- 'IsPgExtension' type class, this simply produces the checked extension
-- entity.
--
-- If you need to use the extension in subsequent migration steps, use
-- 'getPgExtension' and 'unCheck' to get access to the underlying
-- 'DatabaseEntity'.
pgCreateExtension :: forall extension db
. IsPgExtension extension
=> Migration Postgres (CheckedDatabaseEntity Postgres db (PgExtensionEntity extension))
pgCreateExtension =
let entity = checkedDbEntityAuto ""
extName = pgExtensionName (Proxy @extension)
in upDown (pgCreateExtensionSyntax extName) Nothing >>
pure (CheckedDatabaseEntity entity (collectEntityChecks entity))
-- | 'Migration' representing the Postgres @DROP EXTENSION@. After this
-- executes, you should expect any further uses of the extension to fail.
-- Unfortunately, without linear types, we cannot check this.
pgDropExtension :: forall extension
. CheckedDatabaseEntityDescriptor Postgres (PgExtensionEntity extension)
-> Migration Postgres ()
pgDropExtension (CheckedPgExtension (PgDatabaseExtension {})) =
upDown (pgDropExtensionSyntax (pgExtensionName (Proxy @extension))) Nothing
-- | Postgres-specific database predicate asserting the existence of an
-- extension in the database. The 'pgExtensionActionProvider' properly provides
-- @CREATE EXTENSION@ and @DROP EXTENSION@ statements to the migration finder.
newtype PgHasExtension = PgHasExtension Text {- Extension Name -}
deriving (Show, Eq, Generic, Hashable)
instance DatabasePredicate PgHasExtension where
englishDescription (PgHasExtension extName) =
"Postgres extension " ++ show extName ++ " is loaded"
predicateSpecificity _ = PredicateSpecificityOnlyBackend "postgres"
serializePredicate (PgHasExtension nm) =
object [ "has-postgres-extension" .= nm ]
pgExtensionActionProvider :: ActionProvider Postgres
pgExtensionActionProvider = pgCreateExtensionProvider <> pgDropExtensionProvider
pgCreateExtensionProvider, pgDropExtensionProvider :: ActionProvider Postgres
pgCreateExtensionProvider =
ActionProvider $ \findPre findPost ->
do extP@(PgHasExtension ext) <- findPost
ensuringNot_ $
do PgHasExtension ext' <- findPre
guard (ext == ext')
let cmd = pgCreateExtensionSyntax ext
pure (PotentialAction mempty (HS.fromList [p extP])
(pure (MigrationCommand cmd MigrationKeepsData))
("Load the postgres extension " <> ext) 1)
pgDropExtensionProvider =
ActionProvider $ \findPre findPost ->
do extP@(PgHasExtension ext) <- findPre
ensuringNot_ $
do PgHasExtension ext' <- findPost
guard (ext == ext')
let cmd = pgDropExtensionSyntax ext
pure (PotentialAction (HS.fromList [p extP]) mempty
(pure (MigrationCommand cmd MigrationKeepsData))
("Unload the postgres extension " <> ext) 1)