dbm (empty) → 0.1.0.0
raw patch · 4 files changed
+215/−0 lines, 4 filesdep +basedep +directorydep +inisetup-changed
Dependencies added: base, directory, ini, optparse-applicative, sqlite-simple, text, time
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- dbm.cabal +26/−0
- src/Main.hs +157/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, Ricky Elrod++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Ricky Elrod nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ dbm.cabal view
@@ -0,0 +1,26 @@+name: dbm+version: 0.1.0.0+synopsis: A *simple* database migration tool.+homepage: https://github.com/relrod/dbm+license: BSD3+license-file: LICENSE+author: Ricky Elrod+maintainer: ricky@elrod.me+copyright: (c) 2016 Ricky Elrod+category: Database+build-type: Simple+cabal-version: >= 1.10++executable dbm+ main-is: Main.hs+ -- other-modules:+ -- other-extensions:+ build-depends: base >= 4 && < 5+ , directory+ , ini+ , optparse-applicative+ , sqlite-simple+ , text+ , time+ hs-source-dirs: src+ default-language: Haskell2010
+ src/Main.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Data.Ini+import Data.List (elem, isSuffixOf)+import Data.Maybe (fromMaybe)+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 [(Maybe 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 = filter (getNecessaryMigrations mid) migrationList+ return migrationFiltered+ where+ getNecessaryMigrations _ (Nothing, fn) =+ error $ "Invalid migration filename: " ++ fn+ getNecessaryMigrations current (Just mid, fn) =+ if mid > current+ then True+ else False++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_ (\(Just mid, x) -> doMigration' x mid) migrations+ where+ doMigration' sql mid = do+ putStr $ "Migrating " ++ sql ++ "... "+ doMigration cfg sql mid+ putStrLn "Done."