dbm-0.3: src/Main.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad
import Data.Ini
import Data.List (elem, isSuffixOf, sort)
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.Directory
import System.Exit
import Options.Applicative
import Utility
--------------------------------------------------------------------------------
-- These are the things you need to extend if you wish to add another backend. -
--------------------------------------------------------------------------------
import qualified SQLite
data Config =
SQLiteConfig { sqliteConfigPath :: T.Text }
getConfigForEnv :: String -> Ini -> IO Config
getConfigForEnv env ini = do
backend <- getConfigValue ini (T.pack env) "backend"
case backend of
"sqlite" -> SQLiteConfig <$> getConfigValue ini (T.pack env) "path"
_ -> error "No valid backend configured."
initializeEnv :: Config -> IO ()
initializeEnv (SQLiteConfig path) = SQLite.initialize (T.unpack path)
getLastMigration :: Config -> IO (Maybe Integer)
getLastMigration (SQLiteConfig path) = SQLite.getLastMigration (T.unpack path)
doMigration :: Config -> FilePath -> Integer -> IO ()
doMigration (SQLiteConfig path) sql mid =
SQLite.performMigration (T.unpack path) sql mid
--------------------------------------------------------------------------------
data Command =
Configs
| Initialize String
| Last String
| Status String
| Migrate String
envParser :: Parser String
envParser = argument str (metavar "ENVIRONMENT"
<> help "The configuration environment to initialize")
initializeParser, lastParser, statusParser, migrateParser :: Parser Command
initializeParser = Initialize <$> envParser
lastParser = Last <$> envParser
statusParser = Status <$> envParser
migrateParser = Migrate <$> envParser
parser :: Parser Command
parser = subparser $
(command "configs"
(info (pure Configs)
(progDesc "Print available configurations.")))
<> (command "initialize"
(info initializeParser
(progDesc "Set up a database for use with dbm and apply all migrations.")))
<> (command "last"
(info lastParser
(progDesc "Get the id of the last migration performed.")))
<> (command "status"
(info statusParser
(progDesc "Get the current migration status.")))
<> (command "migrate"
(info migrateParser
(progDesc "Migrate the database completely.")))
run :: Command -> Ini -> IO ()
run Configs ini = showConfigs ini
run (Initialize env) ini = getConfigForEnv env ini >>= initializeEnv
run (Last env) ini = getConfigForEnv env ini >>= showLastMigration
run (Status env) ini = getConfigForEnv env ini >>= showMigrationStatus
run (Migrate env) ini = getConfigForEnv env ini >>= performMigrations
opts :: ParserInfo Command
opts = info (parser <**> helper) idm
main :: IO ()
main = do
current <- listDirectory "."
if not ("sql" `elem` current)
then do
putStrLn "`dbm` must be run in a directory with an 'sql' subdirectory."
exitFailure
else do
ini <- getConfigFile
opts <- execParser opts
run opts ini
getConfigFile :: IO Ini
getConfigFile = do
config <- readIniFile "sql/.dbm"
case config of
Right x -> return x
Left err -> putStrLn err >> exitFailure
getConfigValue :: Ini -> T.Text -> T.Text -> IO T.Text
getConfigValue c s l = do
case lookupValue s l c of
Right x -> return x
Left err -> putStrLn err >> exitFailure
showConfigs :: Ini -> IO ()
showConfigs config = do
mapM_ T.putStrLn (sections config)
showLastMigration :: Config -> IO ()
showLastMigration cfg = do
mid <- getLastMigration cfg
case mid of
Just mid -> putStrLn $ "Last migration id: " ++ show mid
Nothing -> putStrLn "No migrations have been performed. :-("
getNecessaryMigrations :: Config -> IO [(Integer, FilePath)]
getNecessaryMigrations cfg = do
mid <- fromMaybe 0 <$> getLastMigration cfg
migrations <- filter (\x -> ".sql" `isSuffixOf` x) <$> listDirectory "sql"
let migrationList = fmap (\x -> (parseMigrationId x, "sql/" ++ x)) migrations
migrationFiltered <- filterM (getNecessaryMigrations' mid) (sort migrationList)
return [(x, y) | (Just x, y) <- migrationFiltered]
where
getNecessaryMigrations' _ (Nothing, fn) = do
putStrLn $ "Invalid migration filename: " ++ fn
exitFailure >> return False
getNecessaryMigrations' current (Just mid, fn) = return (mid > current)
showMigrationStatus :: Config -> IO ()
showMigrationStatus cfg = do
migrations <- getNecessaryMigrations cfg
if null migrations
then putStrLn "Database schema is up to date."
else do
putStrLn "Need to migrate:"
mapM_ (\(_, x) -> putStrLn ("* " ++ x)) migrations
performMigrations :: Config -> IO ()
performMigrations cfg = do
showMigrationStatus cfg
migrations <- getNecessaryMigrations cfg
-- This pattern match is "safe" because we `error` in getNecessaryMigrations
-- if it were never a `Nothing`.
mapM_ (\(mid, x) -> doMigration' x mid) migrations
where
doMigration' sql mid = do
putStr $ "Migrating " ++ sql ++ "... "
doMigration cfg sql mid
putStrLn "Done."