-- SPDX-FileCopyrightText: 2022-23 Gregor Kleen ,Steffen Jost ,Sarah Vaupel ,Steffen Jost -- -- SPDX-License-Identifier: AGPL-3.0-or-later {-# LANGUAGE UndecidableInstances, GeneralizedNewtypeDeriving #-} module Model.Migration ( migrateAll , requiresMigration , ManualMigration(..), getMigrationTime ) where import Import.NoModel hiding (Max(..), Last(..)) import Model import Foundation.Type import Model.Migration.Definitions -- SEE HERE: this module contains the actual migration code import qualified Model.Migration.Types as Legacy import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Conduit.List as C import Database.Persist.Sql import Database.Persist.Sql.Raw.QQ import Database.Persist.Postgresql import qualified Database.Esqueleto.Legacy as E import Control.Monad.Except (MonadError(..)) import Web.ServerSession.Backend.Persistent.Memcached (migrateMemcachedSqlStorage) -- import qualified Control.Monad.State.Class as State -- _manualMigration :: Fold (Legacy.Migration Version, Legacy.Version) ManualMigration -- _manualMigration = folding $ \case -- ([Legacy.migrationVersion|initial|], [Legacy.version|0.0.0|]) -> Just Migration20180813SimplifyUserTheme -- ([Legacy.migrationVersion|44.0.0|], [Legacy.version|45.0.0|]) -> Just Migration20201119RoomTypes -- _other -> Nothing -- AppliedMigrationMigration changed vom ManualMigration to Text (via PathPiece) so that removed extra migrations within DB are harmless (before achieved through where-clause) share [mkPersist sqlSettings, mkMigrate "migrateDBVersioning"] [persistLowerCase| AppliedMigration json migration Text time UTCTime Primary migration deriving Show Eq Ord |] migrateAll' :: Migration migrateAll' = sequence_ [ migrateUniWorX , migrateMemcachedSqlStorage , migrateManual ] migrateAll :: ( MonadLogger m , MonadResource m , MonadUnliftIO m -- , MonadReader UniWorX m ) => ReaderT SqlBackend m () migrateAll = do $logDebugS "Migration" "Initial migration" mapM_ ($logInfoS "Migration") =<< runMigrationSilent initialMigration missingMigrations <- getMissingMigrations let doCustomMigration acc manualMigration migration = acc <* do let appliedMigrationMigration = toPathPiece manualMigration $logInfoS "Migration" appliedMigrationMigration appliedMigrationTime <- liftIO getCurrentTime _ <- migration insert AppliedMigration{..} -- Map.foldlWithKey traverses migrations in ascending order of AppliedMigrationKey $logDebugS "Migration" "Apply missing migrations" Map.foldlWithKey doCustomMigration (return ()) missingMigrations $logDebugS "Migration" "Persistent automatic migration" mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAll' $logDebugS "Migration" "Migrations marked as ‘always safe’" mapM_ ($logInfoS "Migration") =<< runMigrationSilent migrateAlwaysSafe requiresMigration :: forall m. ( MonadLogger m , MonadResource m ) => ReaderT SqlBackend m Bool requiresMigration = mapReaderT (exceptT return return) $ do initial <- either id (map snd) <$> parseMigration initialMigration unless (null initial) $ do $logInfoS "Migration" $ intercalate "; " initial throwError True customs <- mapReaderT lift $ getMissingMigrations @_ @(ReaderT UniWorX m) unless (Map.null customs) $ do $logInfoS "Migration" . intercalate ", " . map tshow $ Map.keys customs throwError True automatic <- either id (map snd) <$> parseMigration migrateAll' unless (null automatic) $ do $logInfoS "Migration" $ intercalate "; " automatic throwError True -- Does not consider `migrateAlwaysSafe` return False initialMigration :: Migration -- ^ Manual migrations to go to InitialVersion below: initialMigration = do mapM_ migrateEnableExtension ["citext", "pgcrypto"] lift . lift . hoist runResourceT . whenM (columnExists "applied_migration" "from") $ do let getAppliedMigrations = [queryQQ|SELECT "from", "to", "time" FROM "applied_migration"|] migrateAppliedMigration [ fromPersistValue -> Right (fromV :: Legacy.MigrationVersion), fromPersistValue -> Right (toV :: Legacy.Version), fromPersistValue -> Right (_time :: UTCTime) ] = do lift [executeQQ|DELETE FROM "applied_migration" WHERE "from" = #{fromV} AND "to" = #{toV}|] -- State.modify . Map.unionWith min . Map.fromSet (const time) $ setOf _manualMigration (fromV, toV) migrateAppliedMigration _ = return () insertMigrations ms = do [executeQQ| ALTER TABLE "applied_migration" DROP CONSTRAINT "applied_migration_pkey"; ALTER TABLE "applied_migration" DROP CONSTRAINT "unique_applied_migration"; ALTER TABLE "applied_migration" DROP COLUMN "from"; ALTER TABLE "applied_migration" DROP COLUMN "to"; ALTER TABLE "applied_migration" ADD COLUMN "migration" text NOT NULL CONSTRAINT "applied_migration_pkey" PRIMARY KEY; |] iforM_ ms $ \appliedMigrationMigration appliedMigrationTime -> insert AppliedMigration{..} in runConduit $ getAppliedMigrations .| execStateC Map.empty (C.mapM_ migrateAppliedMigration) >>= lift . insertMigrations migrateDBVersioning getMissingMigrations :: forall m m'. ( MonadLogger m , MonadIO m , MonadResource m' -- , MonadReader UniWorX m' ) => ReaderT SqlBackend m (Map ManualMigration (ReaderT SqlBackend m' ())) getMissingMigrations = do $logDebugS "Migration" "Retrieve applied migrations" appliedMigrations <- fmap (map E.unValue) . E.select . E.from $ \appliedMigration -> do return $ appliedMigration E.^. AppliedMigrationMigration let migNotDone m _ = toPathPiece m `Set.notMember` Set.fromList appliedMigrations return $ Map.filterWithKey migNotDone customMigrations getMigrationTime :: ( MonadIO m , BaseBackend backend ~ SqlBackend , PersistStoreRead backend ) => ManualMigration -> ReaderT backend m (Maybe UTCTime) getMigrationTime = fmap (fmap appliedMigrationTime) . get . AppliedMigrationKey . toPathPiece