packages feed

dbmigrations 0.9.1 → 1.0

raw patch · 15 files changed

+519/−78 lines, 15 filesdep +MissingHPVP ok

version bump matches the API change (PVP)

Dependencies added: MissingH

API changes (from Hackage documentation)

- Moo.Core: ConfigData :: String -> String -> String -> ConfigData
- Moo.Core: [_dbConnStr] :: ConfigData -> String
- Moo.Core: [_dbTypeStr] :: ConfigData -> String
- Moo.Core: [_fileStorePath] :: ConfigData -> String
- Moo.Core: data ConfigData
- Moo.Core: fromConfigurator :: Config -> IO (Maybe Configuration)
- Moo.Core: fromShellEnvironment :: ShellEnvironment -> Maybe Configuration
- Moo.Core: type ShellEnvironment = [(String, String)]
+ Database.Schema.Migrations.Migration: emptyMigration :: String -> Migration
+ Database.Schema.Migrations.Store: leafMigrations :: StoreData -> [String]
+ Moo.CommandUtils: getCurrentTimestamp :: IO String
+ Moo.Core: [_appLinearMigrations] :: AppState -> Bool
+ Moo.Core: [_appTimestampFilenames] :: AppState -> Bool
+ Moo.Core: [_linearMigrations] :: Configuration -> Bool
+ Moo.Core: [_timestampFilenames] :: Configuration -> Bool
+ Moo.Core: envLinearMigrations :: String
+ Moo.Core: instance GHC.Show.Show Moo.Core.Configuration
+ Moo.Core: instance GHC.Show.Show Moo.Core.LoadConfig
+ Moo.Main: [_linearMigrations] :: Configuration -> Bool
+ Moo.Main: [_timestampFilenames] :: Configuration -> Bool
- Moo.Core: AppState :: CommandOptions -> Command -> [String] -> [String] -> MigrationStore -> DbConnDescriptor -> String -> StoreData -> AppState
+ Moo.Core: AppState :: CommandOptions -> Command -> [String] -> [String] -> MigrationStore -> DbConnDescriptor -> String -> StoreData -> Bool -> Bool -> AppState
- Moo.Core: Configuration :: String -> String -> FilePath -> Configuration
+ Moo.Core: Configuration :: String -> String -> FilePath -> Bool -> Bool -> Configuration
- Moo.Main: Configuration :: String -> String -> FilePath -> Configuration
+ Moo.Main: Configuration :: String -> String -> FilePath -> Bool -> Bool -> Configuration

Files

MOO.TXT view
@@ -6,7 +6,8 @@ "moo", is responsible for creating, installing, and reverting migrations in your database backend. -At present, PostgreSQL and Sqlite3 are the only supported database backends.+At present, PostgreSQL and Sqlite3 are the only supported database+backends.  The moo tool works by creating migration files in a specific location, called a migration store, on your filesystem. This directory is where@@ -126,7 +127,19 @@   DBM_DATABASE_TYPE = "sqlite3"   DBM_DATABASE = "/path/to/database.db"   DBM_MIGRATION_STORE = "/path/to/migration/store"+  DBM_LINEAR_MIGRATIONS = on/off (or true/false; defaults to off)+  DBM_TIMESTAMP_FILENAMES = on/off (or true/false; defaults to off) +Alternatively, you may save your settings to "moo.cfg" file in the current+directory (probably a project root) and moo will load it automatically, if+present. Specifying --config-file disables this behavior.++If you use a config file (either the default one or the one specified with+--config-file option) but the environment variables are set, they will+override settings from the file. You may use this to have project settings+specified in a file and use environment to specify user-local configuration+options.+ Migration file format --------------------- @@ -177,7 +190,8 @@ Environment ----------- -Moo depends on these environment variables:+Moo depends on these environment variables / configuration file+settings:    DBM_DATABASE_TYPE @@ -211,15 +225,25 @@     schema.  Initially, you'll probably set this to an extant (but     empty) directory.  moo will not create it for you. +  DBM_LINEAR_MIGRATIONS++    If set to true/on, the linear migrations feature will be enabled.+    Defaults to off. See 'Linear migrations' section for more details.++  DBM_TIMESTAMP_FILENAMES++    If set to true/on, the migration filename for new migrations will+    have a timestamp embedded in it.+ Commands --------    new <migration name>: create a new migration with the given name and     save it in the migration store.  This command will prompt you for-    dependencies on other migrations and ask for confirmation before-    creating the migration in the store.  If you use the --no-ask-    flag, the migration will be created immediately with no-    dependencies.+    dependencies on other migrations (if the 'linear migrations'+    feature is disabled) and ask for confirmation before creating the+    migration in the store.  If you use the --no-ask flag, the migration+    will be created immediately with no dependencies.    apply <migration name>: apply the specified migration (and its     dependencies) to the database.  This operation will be performed@@ -257,3 +281,14 @@     rolled back; otherwise it will be committed.  This is mostly     useful in development when a migration applies but is incorrect     and needs to be tweaked and reapplied.++Linear migrations+-----------------++If you know that every migration needs to depend on all previous ones,+consider enabling this feature. When enabled, 'moo new' will automatically+select smallest subset of existing migrations that will make the new one+indirectly depend on every other already in the store. This in turn makes+the store linear-ish (in terms of order of execution) and helps managing the+migrations by always depending on previous work. Also, this may easily be used+to see how the database changed in time.
dbmigrations.cabal view
@@ -1,5 +1,5 @@ Name:                dbmigrations-Version:             0.9.1+Version:             1.0 Synopsis:            An implementation of relational database "migrations" Description:         A library and program for the creation,                      management, and installation of schema updates@@ -108,11 +108,14 @@     template-haskell,     yaml-light >= 0.1,     bytestring >= 0.9,+    MissingH,     HDBC >= 2.2.1,     HDBC-postgresql,     HDBC-sqlite3,     HUnit >= 1.2,-    process >= 1.1+    process >= 1.1,+    configurator >= 0.2,+    text >= 0.11    other-modules:     BackendTest@@ -126,6 +129,9 @@     MigrationsTest     StoreTest     TestDriver+    InMemoryStore+    LinearMigrationsTest+    ConfigurationTest    if impl(ghc >= 6.12.0)     ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields
src/Database/Schema/Migrations/Filesystem.hs view
@@ -29,7 +29,7 @@  import Database.Schema.Migrations.Migration     ( Migration(..)-    , newMigration+    , emptyMigration     ) import Database.Schema.Migrations.Filesystem.Serialize import Database.Schema.Migrations.Store@@ -98,7 +98,7 @@        case length missing of         0 -> do-          let newM = newMigration name+          let newM = emptyMigration name           case migrationFromFields newM fields of             Nothing -> throwFS $ "Error in " ++ (show path) ++ ": unrecognized field found"             Just m -> return m
src/Database/Schema/Migrations/Migration.hs view
@@ -1,6 +1,7 @@ module Database.Schema.Migrations.Migration     ( Migration(..)     , newMigration+    , emptyMigration     ) where @@ -22,12 +23,19 @@     depsOf = mDeps     depId = mId -newMigration :: String -> Migration-newMigration theId =+emptyMigration :: String -> Migration+emptyMigration name =   Migration { mTimestamp = Nothing-            , mId = theId-            , mApply = "(Apply SQL here.)"+            , mId = name+            , mApply = ""             , mRevert = Nothing-            , mDesc = Just "(Describe migration here.)"+            , mDesc = Nothing             , mDeps = []             }++newMigration :: String -> Migration+newMigration theId = +  (emptyMigration theId) +    { mApply = "(Apply SQL here.)"+    , mDesc = Just "(Describe migration here.)"+    }
src/Database/Schema/Migrations/Store.hs view
@@ -19,6 +19,7 @@     , depGraphFromMapping     , validateMigrationMap     , validateSingleMigration+    , leafMigrations     ) where @@ -26,6 +27,7 @@ import Control.Monad ( mzero ) import Control.Applicative ( (<$>) ) import qualified Data.Map as Map+import Data.Graph.Inductive.Graph ( labNodes, indeg )  import Database.Schema.Migrations.Migration     ( Migration(..)@@ -148,3 +150,9 @@ -- won't want to use this directly; use 'loadMigrations' instead. depGraphFromMapping :: MigrationMap -> Either String (DependencyGraph Migration) depGraphFromMapping mapping = mkDepGraph $ Map.elems mapping++-- |Finds migrations that no other migration depends on (effectively finds all+-- vertices with in-degree equal to zero).+leafMigrations :: StoreData -> [String]+leafMigrations s = [l | (n, l) <- labNodes g, indeg g n == 0]+    where g = depGraph $ storeDataGraph s
src/Moo/CommandHandlers.hs view
@@ -20,25 +20,31 @@  newCommand :: CommandHandler newCommand storeData = do-  required <- asks _appRequiredArgs-  store    <- asks _appStore-  let [migrationId] = required+  required   <- asks _appRequiredArgs+  store      <- asks _appStore+  linear     <- asks _appLinearMigrations+  timestamp  <- asks _appTimestampFilenames+  timeString <- (++"_") <$> liftIO getCurrentTimestamp++  let [migrationId] = if timestamp+      then fmap (timeString++) required+      else required   noAsk <- _noAsk <$> asks _appOptions    liftIO $ do     fullPath <- fullMigrationName store migrationId-     when (isJust $ storeLookup storeData migrationId) $          do            putStrLn $ "Migration " ++ (show fullPath) ++ " already exists"            exitWith (ExitFailure 1) -    -- Default behavior: ask for dependencies-    deps <- if noAsk then (return []) else-            do-              putStrLn $ "Selecting dependencies for new \-                         \migration: " ++ migrationId-              interactiveAskDeps storeData+    -- Default behavior: ask for dependencies if linear mode is disabled+    deps <- if linear then (return $ leafMigrations storeData) else+           if noAsk then (return []) else+           do+             putStrLn $ "Selecting dependencies for new \+                        \migration: " ++ migrationId+             interactiveAskDeps storeData      result <- if noAsk then (return True) else               (confirmCreation migrationId deps)@@ -47,14 +53,14 @@       True -> do                now <- Clock.getCurrentTime                status <- createNewMigration store $ (newMigration migrationId) { mDeps = deps-                                                                               , mTimestamp = Just now-                                                                               }+                                                    , mTimestamp = Just now+                                                    }                case status of-                 Left e -> putStrLn e >> (exitWith (ExitFailure 1))-                 Right _ -> putStrLn $ "Migration created successfully: " ++-                            show fullPath+                    Left e -> putStrLn e >> (exitWith (ExitFailure 1))+                    Right _ -> putStrLn $ "Migration created successfully: " +++                               show fullPath       False -> do-               putStrLn "Migration creation cancelled."+              putStrLn "Migration creation cancelled."  upgradeCommand :: CommandHandler upgradeCommand storeData = do
src/Moo/CommandUtils.hs view
@@ -7,13 +7,15 @@        , revert        , withBackend        , makeBackend+       , getCurrentTimestamp        ) where  import Control.Exception ( bracket ) import Control.Monad ( when, forM_, unless ) import Control.Monad.Reader ( asks ) import Control.Monad.Trans ( liftIO )-import Data.List ( intercalate, sortBy )+import Data.List ( intercalate, sortBy, isPrefixOf )+import Data.Time.Clock (getCurrentTime) import Data.Maybe ( fromJust, isJust ) import System.Exit ( exitWith, ExitCode(..) ) import System.IO ( stdout, hFlush, hGetBuffering@@ -28,6 +30,10 @@                                         ) import Moo.Core +getCurrentTimestamp :: IO String+getCurrentTimestamp =+  replace ":" "-" . replace " " "_" . take 19 . show <$> getCurrentTime+ apply :: Migration -> StoreData -> Backend -> Bool -> IO [Migration] apply m storeData backend complain = do   -- Get the list of migrations to apply@@ -217,3 +223,38 @@                  , ('d', (Done, Just "done, do not ask me about more dependencies"))                  , ('q', (Quit, Just "cancel this operation and quit"))                  ]++-- The following code is vendored from MissingH Data.List.Utils:++{- | Similar to Data.List.span, but performs the test on the entire remaining+list instead of just one element.++@spanList p xs@ is the same as @(takeWhileList p xs, dropWhileList p xs)@+-}+spanList :: ([a] -> Bool) -> [a] -> ([a], [a])++spanList _ [] = ([],[])+spanList func list@(x:xs) =+    if func list+       then (x:ys,zs)+       else ([],list)+    where (ys,zs) = spanList func xs++{- | Similar to Data.List.break, but performs the test on the entire remaining+list instead of just one element.+-}+breakList :: ([a] -> Bool) -> [a] -> ([a], [a])+breakList func = spanList (not . func)++replace :: Eq a => [a] -> [a] -> [a] -> [a]+replace old new = intercalate new . split old++split :: Eq a => [a] -> [a] -> [[a]]+split _ [] = []+split delim str =+  let (firstline, remainder) = breakList (isPrefixOf delim) str+   in firstline : case remainder of+                       [] -> []+                       x -> if x == delim+                               then [[]]+                               else split delim (drop (length delim) x)
src/Moo/Core.hs view
@@ -1,15 +1,31 @@ {-# LANGUAGE ExistentialQuantification #-}-module Moo.Core where+module Moo.Core+    ( AppT+    , CommandHandler+    , CommandOptions (..)+    , Command (..)+    , AppState (..)+    , Configuration (..)+    , DbConnDescriptor (..)+    , databaseTypes+    , envDatabaseName+    , envDatabaseType+    , envLinearMigrations+    , envStoreName+    , loadConfiguration) where  import Control.Applicative ((<$>), (<*>)) import Control.Monad.Reader (ReaderT) import qualified Data.Configurator as C-import Data.Configurator.Types (Config)+import Data.Configurator.Types (Config, Configured) import qualified Data.Text as T+import Data.Char (toLower) import Database.HDBC.PostgreSQL (connectPostgreSQL) import Database.HDBC.Sqlite3 (connectSqlite3) import System.Environment (getEnvironment)+import Data.Maybe (isJust, fromMaybe) + import Database.Schema.Migrations () import Database.Schema.Migrations.Store (MigrationStore, StoreData) import Database.Schema.Migrations.Backend@@ -22,14 +38,16 @@ type CommandHandler = StoreData -> AppT ()  -- |Application state which can be accessed by any command handler.-data AppState = AppState { _appOptions         :: CommandOptions-                         , _appCommand         :: Command-                         , _appRequiredArgs    :: [String]-                         , _appOptionalArgs    :: [String]-                         , _appStore           :: MigrationStore-                         , _appDatabaseConnStr :: DbConnDescriptor-                         , _appDatabaseType    :: String-                         , _appStoreData       :: StoreData+data AppState = AppState { _appOptions          :: CommandOptions+                         , _appCommand          :: Command+                         , _appRequiredArgs     :: [String]+                         , _appOptionalArgs     :: [String]+                         , _appStore            :: MigrationStore+                         , _appDatabaseConnStr  :: DbConnDescriptor+                         , _appDatabaseType     :: String+                         , _appStoreData        :: StoreData+                         , _appLinearMigrations :: Bool+                         , _appTimestampFilenames :: Bool                          }  type ShellEnvironment = [(String, String)]@@ -38,42 +56,106 @@     { _connectionString   :: String     , _databaseType       :: String     , _migrationStorePath :: FilePath-    }+    , _linearMigrations   :: Bool+    , _timestampFilenames :: Bool+    } deriving Show -loadConfiguration :: Maybe FilePath -> IO (Either String Configuration)-loadConfiguration pth = do-    mCfg <- case pth of-        Nothing -> fromShellEnvironment <$> getEnvironment-        Just path -> fromConfigurator =<< C.load [C.Required path]+-- |Intermediate type used during config loading.+data LoadConfig = LoadConfig+    { _lcConnectionString   :: Maybe String+    , _lcDatabaseType       :: Maybe String+    , _lcMigrationStorePath :: Maybe FilePath+    , _lcLinearMigrations   :: Maybe Bool+    , _lcTimestampFilenames :: Maybe Bool+    } deriving Show -    case mCfg of-        Nothing -> do-            case pth of-                Nothing -> return $ Left "Missing required environment variables"-                Just path -> return $ Left $ "Could not load configuration from " ++ path-        Just cfg -> return $ Right cfg+defConfigFile :: String+defConfigFile = "moo.cfg" -fromShellEnvironment :: ShellEnvironment -> Maybe Configuration-fromShellEnvironment env = Configuration <$> connectionString-                                         <*> databaseType-                                         <*> migrationStorePath+newLoadConfig :: LoadConfig+newLoadConfig = LoadConfig Nothing Nothing Nothing Nothing Nothing++isValidConfig :: LoadConfig -> Bool+isValidConfig (LoadConfig a b c _ _) = all isJust [a, b, c]++loadConfigToConfig :: LoadConfig -> Configuration+loadConfigToConfig (LoadConfig (Just cs) (Just dt) (Just msp) lm ts) =+  Configuration cs dt msp+    (fromMaybe False lm)+    (fromMaybe False ts)+loadConfigToConfig _ = error "LoadConfig is invalid!"++-- |Setters for fields of 'LoadConfig'.+lcConnectionString, lcDatabaseType, lcMigrationStorePath+    :: LoadConfig -> Maybe String -> LoadConfig+lcConnectionString c v   = c { _lcConnectionString   = v }+lcDatabaseType c v       = c { _lcDatabaseType       = v }+lcMigrationStorePath c v = c { _lcMigrationStorePath = v }++lcLinearMigrations :: LoadConfig -> Maybe Bool -> LoadConfig+lcLinearMigrations c v   = c { _lcLinearMigrations   = v }+lcTimestampFilenames c v = c { _lcTimestampFilenames = v }+++-- | @f .= v@ invokes f only if v is 'Just'+(.=) :: (Monad m) => (a -> Maybe b -> a) -> m (Maybe b) -> m (a -> a)+(.=) f v' = do+    v <- v'+    return $ case v of+      Just _ -> flip f v+      _      -> id++-- |It's just @flip '<*>'@+(&) :: (Applicative m) => m a -> m (a -> b) -> m b+(&) = flip (<*>)++infixr 3 .=+infixl 2 &++applyEnvironment :: ShellEnvironment -> LoadConfig -> IO LoadConfig+applyEnvironment env lc =+    return lc & lcConnectionString   .= f envDatabaseName+              & lcDatabaseType       .= f envDatabaseType+              & lcMigrationStorePath .= f envStoreName+              & lcLinearMigrations   .= readFlag <$> f envLinearMigrations+    where f n = return $ lookup n env++applyConfigFile :: Config -> LoadConfig -> IO LoadConfig+applyConfigFile cfg lc =+    return lc & lcConnectionString   .= f envDatabaseName+              & lcDatabaseType       .= f envDatabaseType+              & lcMigrationStorePath .= f envStoreName+              & lcLinearMigrations   .= f envLinearMigrations+              & lcTimestampFilenames .= f envTimestampFilenames     where-      connectionString = envLookup envDatabaseName-      databaseType = envLookup envDatabaseType-      migrationStorePath = envLookup envStoreName-      envLookup = (\evar -> lookup evar env)+        f :: Configured a => String -> IO (Maybe a)+        f = C.lookup cfg . T.pack -fromConfigurator :: Config -> IO (Maybe Configuration)-fromConfigurator conf = do-    let configLookup = C.lookup conf . T.pack-    connectionString <- configLookup envDatabaseName-    databaseType <- configLookup envDatabaseType-    migrationStorePath <- configLookup envStoreName+-- |Loads config file (falling back to default one if not specified) and then+-- overrides configuration with an environment.+loadConfiguration :: Maybe FilePath -> IO (Either String Configuration)+loadConfiguration pth = do+    file <- maybe (C.load [C.Optional defConfigFile])+                  (\p -> C.load [C.Required p]) pth+    env <- getEnvironment+    cfg <- applyConfigFile file newLoadConfig >>= applyEnvironment env -    return $ Configuration <$> connectionString-                           <*> databaseType-                           <*> migrationStorePath+    if isValidConfig cfg+       then return $ Right $ loadConfigToConfig cfg+       else return $ Left "Configuration is invalid, check if everything is set." +-- |Converts @Just "on"@ and @Just "true"@ (case insensitive) to @True@,+-- anything else to @False@.+readFlag :: Maybe String -> Maybe Bool+readFlag Nothing  = Nothing+readFlag (Just v) = go $ map toLower v+    where+        go "on"    = Just True+        go "true"  = Just True+        go "off"   = Just False+        go "false" = Just False+        go _       = Nothing+ -- |CommandOptions are those options that can be specified at the command -- prompt to modify the behavior of a command. data CommandOptions = CommandOptions { _configFilePath :: Maybe String@@ -91,12 +173,6 @@                        , _cHandler        :: CommandHandler                        } --- |ConfigOptions are those options read from configuration file-data ConfigData = ConfigData { _dbTypeStr     :: String-                             , _dbConnStr     :: String-                             , _fileStorePath :: String-                             }- newtype DbConnDescriptor = DbConnDescriptor String  -- |The values of DBM_DATABASE_TYPE and their corresponding connection@@ -114,3 +190,9 @@  envStoreName :: String envStoreName = "DBM_MIGRATION_STORE"++envLinearMigrations :: String+envLinearMigrations = "DBM_LINEAR_MIGRATIONS"++envTimestampFilenames :: String+envTimestampFilenames = "DBM_TIMESTAMP_FILENAMES"
src/Moo/Main.hs view
@@ -32,6 +32,7 @@   putStrLn $ "  " ++ envDatabaseType ++ ": database type, one of " ++            intercalate "," (map fst databaseTypes)   putStrLn $ "  " ++ envStoreName ++ ": path to migration store"+  putStrLn $ "  " ++ envLinearMigrations ++ ": whether to use linear migrations (defaults to False)"   putStrLn "Commands:"   forM_ commands $ \command -> do           putStrLn $ "  " ++ usageString command@@ -67,6 +68,7 @@       dbType = _databaseType conf       storePathStr = _migrationStorePath conf       store = filesystemStore $ FSStore { storePath = storePathStr }+      linear = _linearMigrations conf    if length required < length ( _cRequired command) then       usageSpecific command else@@ -85,6 +87,8 @@                               , _appDatabaseType = dbType                               , _appStore = store                               , _appStoreData = storeData+                              , _appLinearMigrations = linear+                              , _appTimestampFilenames = _timestampFilenames conf                               }             runReaderT (_cHandler command storeData) st `catchSql` reportSqlError 
test/Common.hs view
@@ -3,12 +3,15 @@     ( TestDependable(..)     , repoRoot     , testFile+    , satisfies+    , (.&&.)     ) where  import CommonTH import System.FilePath ( (</>) ) import Language.Haskell.TH.Syntax (lift)+import Test.HUnit  import Database.Schema.Migrations.Dependencies ( Dependable(..) ) @@ -26,3 +29,14 @@                          , tdDeps :: [String]                          }                       deriving (Show, Eq, Ord)+++satisfies :: String -> a -> (a -> Bool) -> IO Test+satisfies m v f = return $ TestCase $ assertBool m (f v)++(.&&.) :: Test -> Test -> Test+(TestList xs) .&&. (TestList ys) = TestList (xs ++ ys)+(TestList xs) .&&. y = TestList (xs ++ [y])+x .&&. (TestList ys) = TestList (x:ys)+a .&&. b = TestList [a, b]+infixl 0 .&&.
+ test/ConfigurationTest.hs view
@@ -0,0 +1,104 @@+module ConfigurationTest (tests) where++import           Control.Exception  (SomeException, try)+import           Data.Either        (isLeft, isRight)+import           System.Directory+import           System.Environment (setEnv, unsetEnv)+import           Test.HUnit++import           Common+import           Moo.Core++tests :: IO [Test]+tests = sequence [prepareTestEnv >> e | e <- entries]+    where entries = [ loadsConfigFile+                    , loadsPropertiesFromFile+                    , loadsDefaultConfigFile+                    , environmentOverridesProperties+                    , ifNoConfigFileIsAvailableEnvironmentIsUsed+                    , throwsWhenConfigFileIsInvalid+                    , returnsErrorWhenNotAllPropertiesAreSet+                    , canReadTimestampsConfig+                    ]++prepareTestEnv :: IO ()+prepareTestEnv = do+    setCurrentDirectory $ testFile "config_loading"+    unsetEnv "DBM_DATABASE_TYPE"+    unsetEnv "DBM_DATABASE"+    unsetEnv "DBM_MIGRATION_STORE"+    unsetEnv "DBM_LINEAR_MIGRATIONS"+    unsetEnv "DBM_TIMESTAMP_FILENAMES"++canReadTimestampsConfig :: IO Test+canReadTimestampsConfig = do+  Right cfg <- loadConfiguration (Just "cfg_ts.cfg")+  satisfies "Timestamp not set" cfg _timestampFilenames++loadsConfigFile :: IO Test+loadsConfigFile = do+    cfg' <- loadConfiguration (Just "cfg1.cfg")+    satisfies "File not loaded" cfg' isRight++loadsPropertiesFromFile :: IO Test+loadsPropertiesFromFile = do+    Right cfg <- loadConfiguration (Just "cfg1.cfg")+    return+        (+        _connectionString   cfg ~?= "connection" .&&.+        _databaseType       cfg ~?= "dbtype"     .&&.+        _migrationStorePath cfg ~?= "store"      .&&.+        _linearMigrations   cfg ~?= True+        )++loadsDefaultConfigFile :: IO Test+loadsDefaultConfigFile = do+    Right cfg <- loadConfiguration Nothing+    return+        (+        _connectionString   cfg ~?= "mooconn"  .&&.+        _databaseType       cfg ~?= "moodb"    .&&.+        _migrationStorePath cfg ~?= "moostore" .&&.+        _linearMigrations   cfg ~?= True+        )++environmentOverridesProperties :: IO Test+environmentOverridesProperties = do+    setEnv "DBM_DATABASE_TYPE" "envdb"+    setEnv "DBM_DATABASE" "envconn"+    setEnv "DBM_MIGRATION_STORE" "envstore"+    setEnv "DBM_LINEAR_MIGRATIONS" "off"+    Right cfg <- loadConfiguration (Just "cfg1.cfg")+    return+        (+        _connectionString   cfg ~?= "envconn"  .&&.+        _databaseType       cfg ~?= "envdb"    .&&.+        _migrationStorePath cfg ~?= "envstore" .&&.+        _linearMigrations   cfg ~?= False+        )++ifNoConfigFileIsAvailableEnvironmentIsUsed :: IO Test+ifNoConfigFileIsAvailableEnvironmentIsUsed = do+    setCurrentDirectory $ testFile ""+    setEnv "DBM_DATABASE_TYPE" "envdb"+    setEnv "DBM_DATABASE" "envconn"+    setEnv "DBM_MIGRATION_STORE" "envstore"+    setEnv "DBM_LINEAR_MIGRATIONS" "off"+    Right cfg <- loadConfiguration Nothing+    return+        (+        _connectionString   cfg ~?= "envconn"  .&&.+        _databaseType       cfg ~?= "envdb"    .&&.+        _migrationStorePath cfg ~?= "envstore" .&&.+        _linearMigrations   cfg ~?= False+        )++returnsErrorWhenNotAllPropertiesAreSet :: IO Test+returnsErrorWhenNotAllPropertiesAreSet = do+    cfg <- loadConfiguration (Just "missing.cfg")+    satisfies "Should return error" cfg isLeft++throwsWhenConfigFileIsInvalid :: IO Test+throwsWhenConfigFileIsInvalid = do+    c <- try $ loadConfiguration (Just "invalid.cfg")+    satisfies "Should throw" c (isLeft :: Either SomeException a -> Bool)
test/FilesystemParseTest.hs view
@@ -116,4 +116,4 @@  migrationParsingTests :: IO [Test] migrationParsingTests =-    sequence $ map mkParsingTest migrationParsingTestCases+    traverse mkParsingTest migrationParsingTestCases
+ test/InMemoryStore.hs view
@@ -0,0 +1,32 @@+module InMemoryStore (inMemoryStore) where++import           Control.Concurrent.MVar+import           Database.Schema.Migrations.Migration+import           Database.Schema.Migrations.Store++type InMemoryData = [(String, Migration)]++-- |Builds simple in-memory store that uses 'MVar' to preserve a list of+-- migrations.+inMemoryStore :: IO MigrationStore+inMemoryStore = do+    store <- newMVar []+    return MigrationStore {+      loadMigration = loadMigrationInMem store+    , saveMigration = saveMigrationInMem store+    , getMigrations = getMigrationsInMem store+    , fullMigrationName = return+    }++loadMigrationInMem :: MVar InMemoryData -> String -> IO (Either String Migration)+loadMigrationInMem store migId = withMVar store $ \migrations -> do+    let mig = lookup migId migrations+    return $ case mig of+        Just m -> Right m+        _      -> Left "Migration not found"++saveMigrationInMem :: MVar InMemoryData -> Migration -> IO ()+saveMigrationInMem store m = modifyMVar_ store $ return . ((mId m, m):)++getMigrationsInMem :: MVar InMemoryData -> IO [String]+getMigrationsInMem store = withMVar store $ return . fmap fst
+ test/LinearMigrationsTest.hs view
@@ -0,0 +1,95 @@+module LinearMigrationsTest (tests) where++import           InMemoryStore+import           Test.HUnit++import           Common+import           Control.Monad.Reader                 (runReaderT)+import           Data.Either                          (isRight)+import           Data.Maybe                           (isNothing)+import           Database.Schema.Migrations.Migration+import           Database.Schema.Migrations.Store+import           Moo.CommandHandlers+import           Moo.Core++tests :: IO [Test]+tests = sequence [ addsMigration+                 , selectsLatestMigrationAsDep+                 , selectsOnlyLeavesAsDeps+                 , doesNotAddDependencyWhenLinearMigrationsAreDisabled+                 ]++addsMigration :: IO Test+addsMigration = do+    state <- prepareState "first"+    mig <- addTestMigration state+    satisfies "Migration not added" mig isRight++selectsLatestMigrationAsDep :: IO Test+selectsLatestMigrationAsDep = do+    state1 <- prepareState "first"+    _ <- addTestMigration state1+    state2 <- prepareStateWith state1 "second"+    Right mig <- addTestMigration state2+    return $ ["first"] ~=? mDeps mig++selectsOnlyLeavesAsDeps :: IO Test+selectsOnlyLeavesAsDeps = do+    state1 <- prepareNormalState "first"+    addTestMigrationWithDeps state1 []+    state2 <- prepareStateWith state1 "second"+    addTestMigrationWithDeps state2 ["first"]+    state3 <- prepareStateWith state2 "third"+    addTestMigrationWithDeps state3 ["first"]+    state4' <- prepareStateWith state3 "fourth"+    let state4 = state4' { _appLinearMigrations = True }+    Right mig <- addTestMigration state4+    return $ ["second", "third"] ~=? mDeps mig++doesNotAddDependencyWhenLinearMigrationsAreDisabled :: IO Test+doesNotAddDependencyWhenLinearMigrationsAreDisabled = do+    state1 <- prepareNormalState "first"+    _ <- addTestMigration state1+    state2 <- prepareStateWith state1 "second"+    Right mig <- addTestMigration state2+    satisfies "Dependencies should be empty" (mDeps mig) null++addTestMigration :: AppState -> IO (Either String Migration)+addTestMigration state = do+    let store = _appStore state+        [migrationId] = _appRequiredArgs state+    runReaderT (newCommand $ _appStoreData state) state+    loadMigration store migrationId++addTestMigrationWithDeps :: AppState -> [String] -> IO ()+addTestMigrationWithDeps state deps = do+    let store = _appStore state+    let [migrationId] = _appRequiredArgs state+    saveMigration store (newMigration migrationId) { mDeps = deps }++prepareState :: String -> IO AppState+prepareState m = do+    store <- inMemoryStore+    Right storeData <- loadMigrations store+    return AppState {+      _appOptions = CommandOptions Nothing False True+    , _appCommand = undefined -- Not used by newCommand+    , _appRequiredArgs = [m]+    , _appOptionalArgs = []+    , _appStore = store+    , _appDatabaseConnStr = DbConnDescriptor ""+    , _appDatabaseType = "none"+    , _appStoreData = storeData+    , _appLinearMigrations = True+    , _appTimestampFilenames = False+    }++prepareStateWith :: AppState -> String -> IO AppState+prepareStateWith state m = do+    Right storeData <- loadMigrations $ _appStore state+    return state { _appRequiredArgs = [m], _appStoreData = storeData }++prepareNormalState :: String -> IO AppState+prepareNormalState m = do+    state <- prepareState m+    return $ state { _appLinearMigrations = False }
test/TestDriver.hs view
@@ -13,6 +13,8 @@ import qualified FilesystemTest import qualified CycleDetectionTest import qualified StoreTest+import qualified LinearMigrationsTest+import qualified ConfigurationTest  import Control.Monad ( forM ) import Control.Exception ( finally, catch, SomeException )@@ -40,6 +42,10 @@                            return $ "Filesystem Parsing" ~: test fspTests                       , do fsTests <- FilesystemTest.tests                            return $ "Filesystem general" ~: test fsTests+                      , do linTests <- LinearMigrationsTest.tests+                           return $ "Linear migrations" ~: test linTests+                      , do cfgTests <- ConfigurationTest.tests+                           return $ "Configuration tests" ~: test cfgTests                       ]   return $ concat [ backendTests                   , ioTests