packages feed

postgresql-simple-migration 0.1.0.0 → 0.1.1.0

raw patch · 7 files changed

+246/−49 lines, 7 filesdep +timePVP ok

version bump matches the API change (PVP)

Dependencies added: time

API changes (from Hackage documentation)

+ Database.PostgreSQL.Simple.Internal.Util: existsTable :: Connection -> String -> IO Bool
+ Database.PostgreSQL.Simple.Migration: MigrationValidation :: MigrationCommand -> MigrationCommand
+ Database.PostgreSQL.Simple.Migration: SchemaMigration :: ByteString -> Checksum -> LocalTime -> SchemaMigration
+ Database.PostgreSQL.Simple.Migration: data SchemaMigration
+ Database.PostgreSQL.Simple.Migration: getMigrations :: Connection -> IO [SchemaMigration]
+ Database.PostgreSQL.Simple.Migration: instance Eq SchemaMigration
+ Database.PostgreSQL.Simple.Migration: instance FromRow SchemaMigration
+ Database.PostgreSQL.Simple.Migration: instance Ord SchemaMigration
+ Database.PostgreSQL.Simple.Migration: instance Read SchemaMigration
+ Database.PostgreSQL.Simple.Migration: instance Show SchemaMigration
+ Database.PostgreSQL.Simple.Migration: instance ToRow SchemaMigration
+ Database.PostgreSQL.Simple.Migration: schemaMigrationChecksum :: SchemaMigration -> Checksum
+ Database.PostgreSQL.Simple.Migration: schemaMigrationExecutedAt :: SchemaMigration -> LocalTime
+ Database.PostgreSQL.Simple.Migration: schemaMigrationName :: SchemaMigration -> ByteString
+ Database.PostgreSQL.Simple.Migration: type Checksum = ByteString

Files

+ Changelog.markdown view
@@ -0,0 +1,8 @@+# Changelog++## 0.1.1.0+* Support for schema validations.+* Improved Haskell API++## 0.1.0.0+* Support for file-based and Haskell migrations.
Readme.markdown view
@@ -34,6 +34,9 @@ * a time-stamp of the date of execution so you can easily track when a change   happened. +This library also supports migration validation so you can ensure (some)+correctness before your application logic kicks in.+ ## How? This utility can be used in two ways: embedded in your Haskell program or as a standalone binary.@@ -49,6 +52,13 @@ ./dist/build/migrate/migrate migrate $CON $BASE_DIR ``` +To validate already executed scripts, execute the following:+```bash+CON="host=$host dbname=$db user=$user password=$pw"+./dist/build/migrate/migrate init $CON+./dist/build/migrate/migrate validate $CON $BASE_DIR+```+ For more information about the PostgreSQL connection string, see: [libpq-connect](http://www.postgresql.org/docs/9.3/static/libpq-connect.html). @@ -85,9 +95,27 @@     let name = "my script"     let script = "create table users (email varchar not null)";     con <- connectPostgreSQL (BS8.pack url)-    void $ runMigration $ MigrationContext (MigrationScript name script) True con+    void $ runMigration $ MigrationContext +        (MigrationScript name script) True con ``` +Validations wrap _MigrationCommands_. This means that you can re-use all+MigrationCommands to perform a read-only validation of your migrations.++To perform a validation on a directory-based migration, you can use the+following code:++```haskell+main :: IO ()+main = do+    let url = "host=$host dbname=$db user=$user password=$pw"+    let name = "my script"+    let script = "create table users (email varchar not null)";+    con <- connectPostgreSQL (BS8.pack url)+    void $ runMigration $ MigrationContext +        (MigrationValidation (MigrationDirectory dir)) verbose con+```+ ## Compilation and Tests The program is built with the _cabal_ build system. The following command builds the library, the standalone binary and the test package.@@ -106,4 +134,3 @@ ## To Do * Collect executed scripts and check if already executed scripts have been   deleted.-* Validate command for the standalone program.
postgresql-simple-migration.cabal view
@@ -1,5 +1,5 @@ name:                       postgresql-simple-migration-version:                    0.1.0.0+version:                    0.1.1.0 synopsis:                   PostgreSQL Schema Migrations homepage:                   https://github.com/ameingast/postgresql-simple-migration Bug-reports:                https://github.com/ameingast/postgresql-simple-migration/issues@@ -15,9 +15,11 @@  extra-source-files:         License                             Readme.markdown+                            Changelog.markdown                              src/*.hs                             src/Database/PostgreSQL/Simple/*.hs+                            src/Database/PostgreSQL/Simple/Internal/*.hs                              test/*.hs                             test/Database/PostgreSQL/Simple/*.hs@@ -31,6 +33,7 @@  Library     exposed-modules:        Database.PostgreSQL.Simple.Migration+                            Database.PostgreSQL.Simple.Internal.Util     hs-source-dirs:         src     ghc-options:            -Wall -fwarn-tabs -fwarn-incomplete-uni-patterns     default-extensions:     OverloadedStrings@@ -40,7 +43,8 @@                             bytestring                  >= 0.10     && < 0.11,                             cryptohash                  >= 0.11     && < 0.12,                             directory                   >= 1.2      && < 1.3,    -                            postgresql-simple           >= 0.4      && < 0.5+                            postgresql-simple           >= 0.4      && < 0.5,+                            time                        >= 1.4      && < 1.5  Executable migrate     main-is:                Main.hs@@ -53,7 +57,8 @@                             bytestring                  >= 0.10     && < 0.11,                             cryptohash                  >= 0.11     && < 0.12,                             directory                   >= 1.2      && < 1.3,    -                            postgresql-simple           >= 0.4      && < 0.5+                            postgresql-simple           >= 0.4      && < 0.5,+                            time                        >= 1.4      && < 1.5  test-suite tests     main-is:                Main.hs
+ src/Database/PostgreSQL/Simple/Internal/Util.hs view
@@ -0,0 +1,24 @@+-- |+-- Module      : Database.PostgreSQL.Simple.Migration+-- Copyright   : (c) 2014 Andreas Meingast <ameingast@gmail.com>+--+-- License     : BSD-style+-- Maintainer  : ameingast@gmail.com+-- Stability   : experimental+-- Portability : GHC+--+-- Migration library for postgresql-simple.++module Database.PostgreSQL.Simple.Internal.Util+    ( existsTable+    ) where++import           Control.Monad              (liftM)+import           Database.PostgreSQL.Simple (Connection, Only (..), query)++-- | Checks if the table named 'table' exists in the provided database.+existsTable :: Connection -> String -> IO Bool+existsTable con table =+    liftM (not . null) (query con q (Only table) :: IO [[Int]])+    where+        q = "select count(relname) from pg_class where relname = ?"
src/Database/PostgreSQL/Simple/Migration.hs view
@@ -10,23 +10,43 @@ -- Migration library for postgresql-simple.  module Database.PostgreSQL.Simple.Migration-    ( runMigration+    (+    -- * Migration actions+    runMigration++    -- * Migration types     , MigrationContext(..)     , MigrationCommand(..)     , MigrationResult(..)     , ScriptName+    , Checksum++    -- * Migration result actions+    , getMigrations++    -- * Migration result types+    , SchemaMigration(..)     ) where -import           Control.Monad                    (liftM, void, when)-import qualified Crypto.Hash.MD5                  as MD5 (hash)-import qualified Data.ByteString                  as BS (ByteString, readFile)-import qualified Data.ByteString.Base64           as B64 (encode)-import           Data.List                        (isPrefixOf, sort)-import           Data.Monoid                      (mconcat)-import           Database.PostgreSQL.Simple       (Connection, Only (..),-                                                   execute, execute_, query)-import           Database.PostgreSQL.Simple.Types (Query (..))-import           System.Directory                 (getDirectoryContents)+import           Control.Applicative                      ((<$>), (<*>))+import           Control.Monad                            (liftM, void, when)+import qualified Crypto.Hash.MD5                          as MD5 (hash)+import qualified Data.ByteString                          as BS (ByteString,+                                                                 readFile)+import qualified Data.ByteString.Base64                   as B64 (encode)+import           Data.List                                (isPrefixOf, sort)+import           Data.Monoid                              (mconcat)+import           Data.Time                                (LocalTime)+import           Database.PostgreSQL.Simple               (Connection,+                                                           Only (..), execute,+                                                           execute_, query,+                                                           query_)+import           Database.PostgreSQL.Simple.FromRow       (FromRow (..), field)+import           Database.PostgreSQL.Simple.Internal.Util (existsTable)+import           Database.PostgreSQL.Simple.ToField       (ToField (..))+import           Database.PostgreSQL.Simple.ToRow         (ToRow (..))+import           Database.PostgreSQL.Simple.Types         (Query (..))+import           System.Directory                         (getDirectoryContents)  -- | Executes migrations inside the provided 'MigrationContext'. runMigration :: MigrationContext -> IO (MigrationResult String)@@ -39,12 +59,14 @@         executeMigration con verbose name contents     MigrationFile name path ->         executeMigration con verbose name =<< BS.readFile path+    MigrationValidation validationCmd ->+        executeValidation con verbose validationCmd --- | Executes all SQL-file based migrations located in the provided 'dir'.+-- | Executes all SQL-file based migrations located in the provided 'dir'+-- in alphabetical order. executeDirectoryMigration :: Connection -> Bool -> FilePath -> IO (MigrationResult String) executeDirectoryMigration con verbose dir =-    liftM (filter (\x -> not $ "." `isPrefixOf` x))-        (getDirectoryContents dir) >>= go . sort+    scriptsInDirectory dir >>= go     where         go [] = return MigrationSuccess         go (f:fs) = do@@ -55,6 +77,12 @@                 MigrationSuccess ->                     go fs +-- | Lists all files in the given 'FilePath' 'dir' in alphabetical order.+scriptsInDirectory :: FilePath -> IO [String]+scriptsInDirectory dir =+    liftM (sort . filter (\x -> not $ "." `isPrefixOf` x))+        (getDirectoryContents dir)+ -- | Executes a generic SQL migration for the provided script 'name' with -- content 'contents'. executeMigration :: Connection -> Bool -> ScriptName -> BS.ByteString -> IO (MigrationResult String)@@ -88,6 +116,52 @@         , ");"         ] +-- | Validates a 'MigrationCommand'. Validation is defined as follows for these+-- types:+--+-- * 'MigrationInitialization': validate the presence of the meta-information+-- table.+-- * 'MigrationDirectory': validate the presence and checksum of all scripts+-- found in the given directory.+-- * 'MigrationScript': validate the presence and checksum of the given script.+-- * 'MigrationFile': validate the presence and checksum of the given file.+-- * 'MigrationValidation': always succeeds.+executeValidation :: Connection -> Bool -> MigrationCommand -> IO (MigrationResult String)+executeValidation con verbose cmd = case cmd of+    MigrationInitialization ->+        existsTable con "schema_migrations" >>= \r -> return $ if r+            then MigrationSuccess+            else MigrationError "No such table: schema_migrations"+    MigrationDirectory path ->+        scriptsInDirectory path >>= goScripts path+    MigrationScript name contents ->+        validate name contents+    MigrationFile name path ->+        validate name =<< BS.readFile path+    MigrationValidation _ ->+        return MigrationSuccess+    where+        validate name contents =+            checkScript con name (md5Hash contents) >>= \r -> case r of+                ScriptOk -> do+                    when verbose $ putStrLn $ "Ok:\t" ++ name+                    return MigrationSuccess+                ScriptNotExecuted -> do+                    when verbose $ putStrLn $ "Missing:\t" ++ name+                    return (MigrationError $ "Missing: " ++ name)+                ScriptModified _ -> do+                    when verbose $ putStrLn $ "Checksum mismatch:\t" ++ name+                    return (MigrationError $ "Checksum mismatch: " ++ name)++        goScripts _ [] = return MigrationSuccess+        goScripts path (x:xs) = do+            r <- validate x =<< BS.readFile (path ++ "/" ++ x)+            case r of+                e@(MigrationError _) ->+                    return e+                MigrationSuccess ->+                    goScripts path xs+ -- | Checks the status of the script with the given name 'name'. -- If the script has already been executed, the checksum of the script -- is compared against the one that was executed.@@ -133,6 +207,7 @@     -- 'FilePath'.     | MigrationScript ScriptName BS.ByteString     -- ^ Executes a migration based on the provided bytestring.+    | MigrationValidation MigrationCommand     deriving (Show, Eq, Read, Ord)  -- | A sum-type denoting the result of a single migration.@@ -164,3 +239,33 @@     , migrationContextConnection :: Connection     -- ^ The PostgreSQL connection to use for migrations.     }++-- | Produces a list of all executed 'SchemaMigration's.+getMigrations :: Connection -> IO [SchemaMigration]+getMigrations = flip query_ q+    where q = mconcat+            [ "select filename, checksum, executed_at "+            , "from schema_migrations"+            ]++-- | A product type representing a single, executed 'SchemaMigration'.+data SchemaMigration = SchemaMigration+    { schemaMigrationName       :: BS.ByteString+    -- ^ The name of the executed migration.+    , schemaMigrationChecksum   :: Checksum+    -- ^ The calculated MD5 checksum of the executed script.+    , schemaMigrationExecutedAt :: LocalTime+    -- ^ A timestamp without timezone of the date of execution of the script.+    } deriving (Show, Eq, Read)++instance Ord SchemaMigration where+    compare (SchemaMigration nameLeft _ _) (SchemaMigration nameRight _ _) =+        compare nameLeft nameRight++instance FromRow SchemaMigration where+    fromRow = SchemaMigration <$>+        field <*> field <*> field++instance ToRow SchemaMigration where+    toRow (SchemaMigration name checksum executedAt) =+       [toField name, toField checksum, toField executedAt]
src/Main.hs view
@@ -26,14 +26,21 @@     void $ case cmd of         Initialize url -> do             con <- connectPostgreSQL (BS8.pack url)-            runMigration $ MigrationContext MigrationInitialization verbose con+            runMigration $ MigrationContext+                MigrationInitialization verbose con         Migrate url dir -> do             con <- connectPostgreSQL (BS8.pack url)-            runMigration $ MigrationContext (MigrationDirectory dir) verbose con+            runMigration $ MigrationContext+                (MigrationDirectory dir) verbose con+        Validate url dir -> do+            con <- connectPostgreSQL (BS8.pack url)+            runMigration $ MigrationContext+                (MigrationValidation (MigrationDirectory dir)) verbose con  parseCommand :: [String] -> Maybe Command parseCommand ("init":url:_) = Just (Initialize url) parseCommand ("migrate":url:dir:_) = Just (Migrate url dir)+parseCommand ("validate":url:dir:_) = Just (Validate url dir) parseCommand _ = Nothing  printUsage :: IO ()@@ -59,5 +66,6 @@  data Command     = Initialize String-    | Migrate String String+    | Migrate String FilePath+    | Validate String FilePath     deriving (Show, Eq, Read, Ord)
test/Database/PostgreSQL/Simple/MigrationTest.hs view
@@ -2,30 +2,33 @@  module Database.PostgreSQL.Simple.MigrationTest where -import           Control.Monad                        (liftM)-import           Database.PostgreSQL.Simple           (Connection, Only (..),-                                                       query)-import           Database.PostgreSQL.Simple.Migration (MigrationCommand (..),-                                                       MigrationContext (..),-                                                       MigrationResult (..),-                                                       runMigration)-import           Test.Hspec                           (Spec, describe, it,-                                                       shouldBe)+import           Database.PostgreSQL.Simple               (Connection)+import           Database.PostgreSQL.Simple.Internal.Util (existsTable)+import           Database.PostgreSQL.Simple.Migration     (MigrationCommand (..), MigrationContext (..),+                                                           MigrationResult (..),+                                                           SchemaMigration (..),+                                                           getMigrations,+                                                           runMigration)+import           Test.Hspec                               (Spec, describe, it,+                                                           shouldBe)  migrationSpec:: Connection -> Spec-migrationSpec con = describe "runMigration" $ do+migrationSpec con = describe "Migrations" $ do+    let migrationScript = MigrationScript "test.sql" q+    let migrationScriptAltered = MigrationScript "test.sql" ""+    let migrationDir = MigrationDirectory "share/test/scripts"+    let migrationFile = MigrationFile "s.sql" "share/test/script.sql"+     it "initializes a database" $ do-        r <- runMigration $-            MigrationContext MigrationInitialization False con+        r <- runMigration $ MigrationContext MigrationInitialization False con         r `shouldBe` MigrationSuccess -    it "creates the schema_migration table" $ do+    it "creates the schema_migrations table" $ do         r <- existsTable con "schema_migration"         r `shouldBe` True      it "executes a migration script" $ do-        r <- runMigration $-            MigrationContext (MigrationScript "test.sql" q) False con+        r <- runMigration $ MigrationContext migrationScript False con         r `shouldBe` MigrationSuccess      it "creates the table from the executed script" $ do@@ -34,17 +37,15 @@      it "skips execution of the same migration script" $ do         r <- runMigration $-            MigrationContext (MigrationScript "test.sql" q) False con+            MigrationContext migrationScript False con         r `shouldBe` MigrationSuccess      it "reports an error on a different checksum for the same script" $ do-        r <- runMigration $-            MigrationContext (MigrationScript "test.sql" "") False con+        r <- runMigration $ MigrationContext migrationScriptAltered False con         r `shouldBe` MigrationError "test.sql"      it "executes migration scripts inside a folder" $ do-        r <- runMigration $-            MigrationContext (MigrationDirectory "share/test/scripts") False con+        r <- runMigration $ MigrationContext migrationDir False con         r `shouldBe` MigrationSuccess      it "creates the table from the executed scripts" $ do@@ -52,18 +53,37 @@         r `shouldBe` True      it "executes a file based migration script" $ do-        r <- runMigration $-            MigrationContext (MigrationFile "s.sql" "share/test/script.sql") False con+        r <- runMigration $ MigrationContext migrationFile False con         r `shouldBe` MigrationSuccess      it "creates the table from the executed scripts" $ do         r <- existsTable con "t3"         r `shouldBe` True++    it "validates initialization" $ do+        r <- runMigration $ MigrationContext+            (MigrationValidation MigrationInitialization) False con+        r `shouldBe` MigrationSuccess++    it "validates an executed migration script" $ do+        r <- runMigration $ MigrationContext+            (MigrationValidation migrationScript) False con+        r `shouldBe` MigrationSuccess++    it "validates all scripts inside a folder" $ do+        r <- runMigration $ MigrationContext+            (MigrationValidation migrationDir) False con+        r `shouldBe` MigrationSuccess++    it "validates an executed migration file" $ do+        r <- runMigration $ MigrationContext+            (MigrationValidation migrationFile) False con+        r `shouldBe` MigrationSuccess++    it "gets a list of executed migrations" $ do+        r <- getMigrations con+        map schemaMigrationName r `shouldBe` ["test.sql", "1.sql", "s.sql"]+     where         q = "create table t1 (c1 varchar);" -existsTable :: Connection -> String -> IO Bool-existsTable con table =-    liftM (not . null) (query con q (Only table) :: IO [[Int]])-    where-        q = "select count(relname) from pg_class where relname = ?"