packages feed

beam-migrate-0.5.5.0: Database/Beam/Migrate/TempTable.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Database.Beam.Migrate.TempTable
  ( -- * Temporary tables

    -- ** @CREATE TEMPORARY TABLE@
    runCreateTempTable

    -- ** Options
  , TempTableCreateMode(..)
  , TempTableOptions(..)
  , defaultTempTableOptions

    -- ** Backend support for temporary tables
  , BeamHasTempTables(..)

    -- ** Schema constraint
  , HasDefaultTempTableSchema

    -- ** Internals
  , GNullableConstraintsForSchema(..)
  , GDefaultBackendFieldSchemas(..)
  ) where

import           Control.Monad.Writer (execWriter, tell)

import           Data.Functor.Const (Const(..))
import           Data.Functor.Identity (Identity(..))
import           Data.Kind (Type)
import           Data.Proxy (Proxy(..))
import qualified Data.Text as T
import           GHC.Generics

import           Database.Beam.Backend.SQL
  ( MonadBeam(..), BeamSqlBackendSyntax )
import           Database.Beam.Backend.Types (Nullable)
import           Database.Beam.Migrate.Generics (HasDefaultSqlDataType(..), NullableStatus)
import           Database.Beam.Migrate.SQL.SQL92
  ( IsSql92ColumnSchemaSyntax(..)
  , IsSql92ColumnConstraintDefinitionSyntax(..)
  , IsSql92ColumnConstraintSyntax(..)
  )
import           Database.Beam.Migrate.SQL.Types
  ( BeamMigrateSqlBackend
  , BeamSqlBackendColumnSchemaSyntax
  , BeamSqlBackendColumnConstraintDefinitionSyntax
  )
import           Database.Beam.Query (HasQBuilder)
import           Database.Beam.Schema.Tables
  ( DatabaseEntity(..), DatabaseEntityDescriptor(..)
  , TableEntity
  , TableSettings, TableField(..)
  , defTblFieldSettings, allBeamValues, zipBeamFieldsM
  , Columnar'(..), Table(..)
  , GDefaultTableFieldSettings
  )

--------------------------------------------------------------------------------

-- | How to handle an existing temporary table when calling 'runCreateTempTable'.
data TempTableCreateMode
  = CreateIfNotExists
    -- ^ Emit @CREATE TEMPORARY TABLE IF NOT EXISTS@.
    -- If the table already exists, do nothing.
  | DropAndCreate
    -- ^ Emit @DROP TABLE IF EXISTS@ followed by @CREATE TEMPORARY TABLE@.
    -- The table is always (re)created fresh.
  deriving (Eq, Ord, Show)

-- | Options for 'runCreateTempTable'.
data TempTableOptions = TempTableOptions
  { tempTableCreateMode :: TempTableCreateMode
    -- ^ Whether to preserve an existing table or recreate it from scratch.
  }

-- | Default options: use @CREATE TEMPORARY TABLE IF NOT EXISTS@.
defaultTempTableOptions :: TempTableOptions
defaultTempTableOptions =
  TempTableOptions { tempTableCreateMode = CreateIfNotExists }

-- | Support for temporary tables in a Beam backend.
class (BeamMigrateSqlBackend be, HasQBuilder be) => BeamHasTempTables be where
  -- | Render a @CREATE TEMPORARY TABLE@ command.
  createTempTableCmd
    :: T.Text
       -- ^ table name
    -> [(T.Text, BeamSqlBackendColumnSchemaSyntax be)]
       -- ^ ordered (column name, column schema) pairs
    -> [T.Text]
       -- ^ primary key column names (empty for no primary key constraint)
    -> Bool
       -- ^ include @IF NOT EXISTS@?
    -> BeamSqlBackendSyntax be

  -- | Render a @DROP TABLE IF EXISTS@ command.
  dropTempTableIfExistsCmd
    :: T.Text
       -- ^ table name
    -> BeamSqlBackendSyntax be


-- | Constraint used for deriving the schema of a temporary table.
type HasDefaultTempTableSchema be tbl =
  ( BeamHasTempTables be
  , Generic (tbl (Const (BeamSqlBackendColumnSchemaSyntax be)))
  , GDefaultBackendFieldSchemas be
      (Rep (tbl Identity))
      (Rep (tbl (Const (BeamSqlBackendColumnSchemaSyntax be))))
  , Table tbl
  , Generic (TableSettings tbl)
  , GDefaultTableFieldSettings (Rep (TableSettings tbl) ())
  )

-- ---------------------------------------------------------------------------
-- CREATE TEMPORARY TABLE

-- | Execute @CREATE TEMPORARY TABLE@, returning the created temporary table
-- entity.
--
-- Note that the @db@ parameter is phantom; if you need to change it you can
-- use 'Data.Coerce.coerce'.
runCreateTempTable
  :: forall be db tbl m
  .  ( HasDefaultTempTableSchema be tbl
     , MonadBeam be m )
  => TempTableOptions
  -> T.Text -- ^ temporary table name
  -> m (DatabaseEntity be db (TableEntity tbl))
runCreateTempTable opts nm = do
  let tblSettings = defTblFieldSettings :: TableSettings tbl
      colSchemas  = deriveTblColSchemas (Proxy @be) (Proxy @tbl)

      -- Collect (columnName, columnSchema) pairs in field declaration order.
      fieldDefs :: [(T.Text, BeamSqlBackendColumnSchemaSyntax be)]
      fieldDefs = execWriter $
        zipBeamFieldsM
          (\(Columnar' tf) c@(Columnar' (Const schema)) ->
              do tell [(_fieldName tf, schema)]
                 return c
          )
          tblSettings colSchemas

      pkFields =
        allBeamValues (\(Columnar' tf) -> _fieldName tf) (primaryKey tblSettings)

  case tempTableCreateMode opts of
    CreateIfNotExists ->
      runNoReturn (createTempTableCmd @be nm fieldDefs pkFields True)
    DropAndCreate -> do
      runNoReturn (dropTempTableIfExistsCmd @be nm)
      runNoReturn (createTempTableCmd @be nm fieldDefs pkFields False)

  let entity = DatabaseEntity $ DatabaseTable
        { dbTableSchema      = Nothing
        , dbTableOrigName    = nm
        , dbTableCurrentName = nm
        , dbTableSettings    = tblSettings
        }
  pure entity

deriveTblColSchemas
  :: forall be tbl
  .  HasDefaultTempTableSchema be tbl
  => Proxy be -> Proxy tbl -> tbl (Const (BeamSqlBackendColumnSchemaSyntax be))
deriveTblColSchemas pBe _ =
  to $ gDefaultBackendFieldSchemas pBe (Proxy @(Rep (tbl Identity))) False

------------------------------------------------------------------------------
-- Generics machinery (internal)

class BeamMigrateSqlBackend be
    => GNullableConstraintsForSchema (nullable :: Bool) be where
  nullableConstraintsForSchema
    :: Proxy nullable
    -> Proxy be
    -> [BeamSqlBackendColumnConstraintDefinitionSyntax be]

instance BeamMigrateSqlBackend be
    => GNullableConstraintsForSchema 'False be where
  nullableConstraintsForSchema _ _ =
    [ constraintDefinitionSyntax Nothing notNullConstraintSyntax Nothing ]

instance BeamMigrateSqlBackend be
    => GNullableConstraintsForSchema 'True be where
  nullableConstraintsForSchema _ _ = []

class BeamMigrateSqlBackend be
    => GDefaultBackendFieldSchemas be (i :: Type -> Type) (o :: Type -> Type) where
  gDefaultBackendFieldSchemas :: Proxy be -> Proxy i -> Bool -> o ()

instance ( BeamMigrateSqlBackend be
         , GDefaultBackendFieldSchemas be xId schemaId )
    => GDefaultBackendFieldSchemas be (M1 t s xId) (M1 t s schemaId) where
  gDefaultBackendFieldSchemas pBe _ embedded =
    M1 (gDefaultBackendFieldSchemas pBe (Proxy @xId) embedded)

instance ( BeamMigrateSqlBackend be
         , GDefaultBackendFieldSchemas be aId aSchema
         , GDefaultBackendFieldSchemas be bId bSchema )
    => GDefaultBackendFieldSchemas be (aId :*: bId) (aSchema :*: bSchema) where
  gDefaultBackendFieldSchemas pBe _ embedded =
    gDefaultBackendFieldSchemas pBe (Proxy @aId) embedded :*:
    gDefaultBackendFieldSchemas pBe (Proxy @bId) embedded


instance ( HasDefaultSqlDataType be haskTy
         , GNullableConstraintsForSchema (NullableStatus haskTy) be
         , cs ~ BeamSqlBackendColumnSchemaSyntax be )
    => GDefaultBackendFieldSchemas be
         (Rec0 haskTy)
         (Rec0 (Const cs haskTy)) where
  gDefaultBackendFieldSchemas pBe _ embedded =
    K1 $ Const $ columnSchemaSyntax
      (defaultSqlDataType (Proxy @haskTy) pBe embedded)
      Nothing
      (nullableConstraintsForSchema (Proxy @(NullableStatus haskTy)) pBe)
      Nothing

instance ( BeamMigrateSqlBackend be
         , cs ~ BeamSqlBackendColumnSchemaSyntax be
         , Generic (embeddedTbl (Const cs))
         , GDefaultBackendFieldSchemas be
             (Rep (embeddedTbl Identity))
             (Rep (embeddedTbl (Const cs))) )
    => GDefaultBackendFieldSchemas be
         (Rec0 (embeddedTbl Identity))
         (Rec0 (embeddedTbl (Const cs))) where
  gDefaultBackendFieldSchemas pBe _ _ =
    K1 $ to $ gDefaultBackendFieldSchemas pBe
      (Proxy @(Rep (embeddedTbl Identity))) True

instance ( BeamMigrateSqlBackend be
         , cs ~ BeamSqlBackendColumnSchemaSyntax be
         , Generic (embeddedTbl (Nullable (Const cs)))
         , GDefaultBackendFieldSchemas be
             (Rep (embeddedTbl (Nullable Identity)))
             (Rep (embeddedTbl (Nullable (Const cs)))) )
    => GDefaultBackendFieldSchemas be
         (Rec0 (embeddedTbl (Nullable Identity)))
         (Rec0 (embeddedTbl (Nullable (Const cs)))) where
  gDefaultBackendFieldSchemas pBe _ _ =
    K1 $ to $ gDefaultBackendFieldSchemas pBe
      (Proxy @(Rep (embeddedTbl (Nullable Identity)))) True