{-# LANGUAGE Arrows #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} -- |Module containing definition of and functions for maintaining the in-database state storage for Refurb. module Refurb.Store where import ClassyPrelude import Composite.Opaleye (defaultRecTable) import Composite.Opaleye.TH (deriveOpaleyeEnum) import Composite.Record ((:->), Record) import Composite.TH (withLensesAndProxies) import Control.Arrow (returnA) import Control.Lens (view) import Control.Monad.Base (liftBase) import Control.Monad.Catch (MonadMask) import Control.Monad.Logger (MonadLogger, logDebug) import Control.Monad.Trans.Control (MonadBaseControl) import Data.These (These(This, These, That)) import qualified Database.PostgreSQL.Simple as PG import Opaleye (Field, SelectArr, SqlBool, SqlInt4, SqlFloat8, SqlText, SqlTimestamptz, Table, asc, orderBy, runSelect, selectTable, tableWithSchema) import Refurb.MigrationUtils (doesTableExist, qqSqls) import Refurb.Types (Migration, migrationQualifiedKey) -- |Result of running a migration, either success or failure. data MigrationResult = MigrationSuccess | MigrationFailure deriving (Eq, Show) deriveOpaleyeEnum ''MigrationResult "refurb.migration_result_enum" (stripPrefix "migration" . toLower) withLensesAndProxies [d| type FId = "id" :-> Int32 type FIdMay = "id" :-> Maybe Int32 type CId = "id" :-> Field SqlInt4 type CIdMay = "id" :-> Maybe (Field SqlInt4) type FQualifiedKey = "qualified_key" :-> Text type CQualifiedKey = "qualified_key" :-> Field SqlText type FApplied = "applied" :-> UTCTime type CApplied = "applied" :-> Field SqlTimestamptz type FOutput = "output" :-> Text type COutput = "output" :-> Field SqlText type FResult = "result" :-> MigrationResult type CResult = "result" :-> Field PGMigrationResult type FDuration = "duration" :-> Double type CDuration = "duration" :-> Field SqlFloat8 type FProdSystem = "prod_system" :-> Bool type CProdSystem = "prod_system" :-> Field SqlBool |] -- |Fields of a migration log entry in memory fetched from the database (with ID) type MigrationLog = '[FId , FQualifiedKey, FApplied, FOutput, FResult, FDuration] -- |Fields of a migration log entry to insert in the database (with the ID column optional) type MigrationLogW = '[FIdMay, FQualifiedKey, FApplied, FOutput, FResult, FDuration] -- |Columns of a migration log when reading from the database (with ID) type MigrationLogColsR = '[CId , CQualifiedKey, CApplied, COutput, CResult, CDuration] -- |Columns of a migration log when inserting into the database (with ID column optional) type MigrationLogColsW = '[CIdMay, CQualifiedKey, CApplied, COutput, CResult, CDuration] -- |Fields of the Refurb config in memory type RefurbConfig = '[FProdSystem] -- |Columns of the Refurb config in the database type RefurbConfigCols = '[CProdSystem] -- |The migration log table which records all executed migrations and their results migrationLog :: Table (Record MigrationLogColsW) (Record MigrationLogColsR) migrationLog = tableWithSchema "refurb" "migration_log" defaultRecTable -- |The refurb config table which controls whether this database is considered a production one or not refurbConfig :: Table (Record RefurbConfigCols) (Record RefurbConfigCols) refurbConfig = tableWithSchema "refurb" "config" defaultRecTable -- |Test to see if the schema seems to be installed by looking for an existing refurb_config table isSchemaPresent :: (MonadBaseControl IO m, MonadMask m, MonadLogger m) => PG.Connection -> m Bool isSchemaPresent conn = do $logDebug "Checking if schema present" runReaderT (doesTableExist "refurb" "config") conn -- |Check if this database is configured as a production database by reading the refurb config table isProdSystem :: (MonadBaseControl IO m, MonadLogger m) => PG.Connection -> m Bool isProdSystem conn = do $logDebug "Checking if this is a prod system" map (fromMaybe False . headMay) . liftBase . runSelect conn $ proc () -> do config <- selectTable refurbConfig -< () returnA -< view cProdSystem config -- |Create the refurb schema elements. Will fail if they already exist. initializeSchema :: (MonadBaseControl IO m, MonadLogger m) => PG.Connection -> m () initializeSchema conn = do $logDebug "Initializing refurb schema" liftBase $ traverse_ (void . PG.execute_ conn) [qqSqls| create schema refurb; set search_path = 'refurb'; create type migration_result_enum as enum('success', 'failure'); create table config (prod_system boolean not null); insert into config (prod_system) values (false); create sequence migration_log_serial; create table migration_log ( id int not null primary key default nextval('migration_log_serial') , qualified_key text not null unique , applied timestamptz not null , output text not null , result migration_result_enum not null , duration double precision not null ); |] -- |Read the migration log and stitch it together with the expected migration list, forming a list in the same order as the known migrations but with -- 'These' representing whether the migration log for the known migration is present or not. -- -- * @'This' migration@ represents a known migration that has no log entry. -- * @'That' migrationLog@ represents an unknown migration that was applied in the past. -- * @'These' migration migrationLog@ represents a migration that has an attempted application in the log. readMigrationStatus :: (MonadBaseControl IO m, MonadLogger m) => PG.Connection -> [Migration] -> SelectArr (Record MigrationLogColsR) () -> m [These Migration (Record MigrationLog)] readMigrationStatus conn migrations restriction = do $logDebug "Reading migration status" migrationStatus <- liftBase $ runSelect conn . orderBy (asc $ view cQualifiedKey) $ proc () -> do mlog <- selectTable migrationLog -< () restriction -< mlog returnA -< mlog let migrationLogByKey = mapFromList . map (view fQualifiedKey &&& id) $ migrationStatus alignMigration :: Migration -> ([These Migration (Record MigrationLog)], Map Text (Record MigrationLog)) -> ([These Migration (Record MigrationLog)], Map Text (Record MigrationLog)) alignMigration m@(migrationQualifiedKey -> k) (t, l) = first ((:t) . maybe (This m) (These m)) (updateLookupWithKey (\ _ _ -> Nothing) k l) (aligned, extra) = foldr alignMigration ([], migrationLogByKey) migrations pure $ map That (toList extra) ++ aligned