packages feed

peregrin 0.1.1 → 0.2.0

raw patch · 4 files changed

+107/−86 lines, 4 filesdep +bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: bytestring

API changes (from Hackage documentation)

- Database.Peregrin.Metadata: Table :: Schema -> Text -> Table
- Database.Peregrin.Metadata: Typ :: Schema -> Text -> Typ
- Database.Peregrin.Metadata: class ToSQL a
- Database.Peregrin.Metadata: data Table
- Database.Peregrin.Metadata: data Typ
- Database.Peregrin.Metadata: instance Database.Peregrin.Metadata.ToSQL Database.Peregrin.Metadata.Schema
- Database.Peregrin.Metadata: instance Database.Peregrin.Metadata.ToSQL Database.Peregrin.Metadata.Table
- Database.Peregrin.Metadata: instance Database.Peregrin.Metadata.ToSQL Database.Peregrin.Metadata.Typ
- Database.Peregrin.Metadata: instance Database.PostgreSQL.Simple.ToField.ToField Database.Peregrin.Metadata.Table
- Database.Peregrin.Metadata: instance Database.PostgreSQL.Simple.ToField.ToField Database.Peregrin.Metadata.Typ
- Database.Peregrin.Metadata: toSQL :: ToSQL a => a -> Text
+ Database.Peregrin: QP :: p -> QP
+ Database.Peregrin: data QP
+ Database.Peregrin: instance Database.PostgreSQL.Simple.ToRow.ToRow Database.Peregrin.QP
+ Database.Peregrin.Metadata: Identifier :: Text -> Identifier
+ Database.Peregrin.Metadata: QIdentifier :: Schema -> Text -> QIdentifier
+ Database.Peregrin.Metadata: data Identifier
+ Database.Peregrin.Metadata: data QIdentifier
+ Database.Peregrin.Metadata: instance Database.PostgreSQL.Simple.ToField.ToField Database.Peregrin.Metadata.Identifier
+ Database.Peregrin.Metadata: instance Database.PostgreSQL.Simple.ToField.ToField Database.Peregrin.Metadata.QIdentifier
- Database.Peregrin: migrate :: Connection -> Schema -> [(Text, Text)] -> IO ()
+ Database.Peregrin: migrate :: ToRow p => Connection -> Schema -> [(Text, Query, p)] -> IO ()

Files

peregrin.cabal view
@@ -1,5 +1,5 @@ Name:                peregrin-Version:             0.1.1+Version:             0.2.0 Synopsis:            Database migration support for use in other libraries. Description:         Database migration support for use in other libraries.                      Currently only supports PostgreSQL.@@ -17,6 +17,7 @@  Library   build-depends:      base >= 4.9 && < 5+                    , bytestring >= 0.10 && < 0.11                     , postgresql-simple >= 0.5.2.1 && < 0.6                     , text >= 1.1.0 && < 2   default-language:   Haskell2010
src-test/Database/PeregrinSpec.hs view
@@ -13,7 +13,7 @@ import           Database.Peregrin.Metadata import           Data.Pool (Pool, withResource, destroyAllResources) import           Data.Text (Text)-import           Database.PostgreSQL.Simple (Connection, Query, Only(..), FromRow)+import           Database.PostgreSQL.Simple (Connection, Query, Only(..), FromRow, ToRow) import qualified Database.PostgreSQL.Simple as PS import           Test.Hspec (Spec, Selector, describe) import qualified Test.Hspec as Hspec@@ -30,40 +30,71 @@     describe ("migrate" ++ extra) $ do       it "can apply a single migration" $ do         -- Exercise:-        migrate' schema [ (cid0, createXSql) ]+        migrate' schema [ (cid0, createXSql, ()) ]         -- Verify:         assertCanSelectFromX        it "ignores migrations that have already been applied (single call)" $ do         -- Exercise:-        migrate' schema [ (cid0, createXSql)-                        , (cid0, createXSql) -- Would fail if applied again+        migrate' schema [ (cid0, createXSql, ())+                        , (cid0, createXSql, ()) -- Would fail if applied again                         ]         -- Verify:         assertCanSelectFromX        it "ignores migrations that have already been applied (multiple calls)" $ do         -- Exercise:-        migrate' schema [ (cid0, createXSql) ]-        migrate' schema [ (cid0, createXSql) ] -- Would fail if applied again+        migrate' schema [ (cid0, createXSql, ()) ]+        migrate' schema [ (cid0, createXSql, ()) ] -- Would fail if applied again         -- Verify:         assertCanSelectFromX        it "throws an error if SQL is changed for a given change set ID" $ do         -- Exercise:-        migrate' schema [ (cid0, createXSql) ]-        migrate' schema [ (cid0, createXSqlBad) ]+        migrate' schema [ (cid0, createXSql, ()) ]+        migrate' schema [ (cid0, createXSqlBad, ()) ]           -- Verify: Should throw here           `shouldThrow` (== MigrationModifiedError cid0)        it "can apply multiple distinct migrations in a single call" $ do         -- Exercise-        migrate' schema [ (cid0, createXSql)-                        , (cid1, createYSql)+        migrate' schema [ (cid0, createXSql, ())+                        , (cid1, createYSql, ())                         ]         -- Verify: Make sure both migrations have been applied         assertCanSelectFromXY +      it "can apply parameterized migrations" $ do+        -- Setup+        let table = Only $ QIdentifier schema "X"+        -- Exercise+        migrate' schema [ (cid0, createTableSql, table)+                        ]+        -- Verify:+        assertCanSelectFromP table++      it "can apply identical parameterized migrations with different parameters" $ do+        -- Setup:+        let tableX = QIdentifier schema "X"+        let tableY = QIdentifier schema "Y"+        -- Exercise:+        migrate' schema [ (cid0, createTableSql, Only tableX)+                        , (cid1, createTableSql, Only tableY)+                        ]+        -- Verify:+        assertCanSelectFromPP (tableX, tableY)++      it "can apply parameterized migrations with different parameter 'shapes'" $ do+        -- Setup:+        let tableX = QIdentifier schema "X"+        let tableY = QIdentifier schema "Y"+        -- Exercise:+        migrate' schema [ (cid0, "CREATE TABLE ? (X INT)", QP $ Only $ tableX)+                        , (cid1, "CREATE TABLE ? (? INT)", QP $ (tableY, Identifier "Y"))+                        ]+        -- Verify:+        assertCanSelectFromPP (tableX, tableY)+   where     it msg action = Hspec.it msg $       bracket mkConnectionPool destroyAllResources $ runReaderT action@@ -72,32 +103,42 @@     createYSql = "CREATE TABLE Y (B INT)"     createXSqlBad = "CREATE TABLE X (Y CHAR(1))" +    createTableSql = "CREATE TABLE ? (X INT)"+     assertCanSelectFromX =       assertCanQuery_ "SELECT * FROM X"+    assertCanSelectFromP p =+      assertCanQuery "SELECT * FROM ?" p+    assertCanSelectFromPP p =+      assertCanQuery "SELECT * FROM ?, ?" p      assertCanSelectFromXY =-      assertCanQuery_ "SELECT * FROM X, Y where X.A = Y.B"+      assertCanQuery_ "SELECT * FROM X, Y"      cid0 = "a328156d-9875-4471-8192-0c86959badb3"     cid1 = "00c6159c-c7f6-4cec-b63f-f70c1c4c7bb1"  assertCanQuery_ :: Query -> ReaderT (Pool Connection) IO () assertCanQuery_ q = do-  _ :: [Only Int] <- query_ q+  assertCanQuery q ()++assertCanQuery :: ToRow p => Query -> p -> ReaderT (Pool Connection) IO ()+assertCanQuery q p = do+  _ :: [Only Int] <- query q p   return () -query_ :: FromRow a => Query -> ReaderT (Pool Connection) IO [a]-query_ sql = do+query :: (ToRow p, FromRow a) => Query -> p -> ReaderT (Pool Connection) IO [a]+query q p = do   connectionPool <- ask   withResource connectionPool $ \connection ->-    lift $ PS.query_ connection sql+    lift $ PS.query connection q p  shouldThrow :: Exception e => ReaderT (Pool Connection) IO a -> Selector e -> ReaderT (Pool Connection) IO () shouldThrow action selector = do   connectionPool <- ask   lift (Hspec.shouldThrow (runReaderT action connectionPool) selector) -migrate' :: Schema -> [(Text, Text)] -> ReaderT (Pool Connection) IO ()+migrate' :: ToRow p => Schema -> [(Text, Query, p)] -> ReaderT (Pool Connection) IO () migrate' schema migrations = do   connectionPool <- ask   lift $ withResource connectionPool $ \connection ->
src/Database/Peregrin.hs view
@@ -1,27 +1,29 @@+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Database.Peregrin     ( migrate     , MigrationError(..)+    , QP(..)     ) where  import           Control.Applicative ((<$>)) import           Control.Exception (Exception, throwIO) import           Control.Monad (forM_, when, void)-import           Database.Peregrin.Metadata+import           Data.ByteString (ByteString) import           Data.Text (Text)-import qualified Data.Text as T import           Data.Int (Int32, Int64) import           Data.Maybe (listToMaybe, fromMaybe)-import           Data.String (fromString)-import           Database.PostgreSQL.Simple (Connection, Only(..), Query)+import           Database.Peregrin.Metadata+import           Database.PostgreSQL.Simple (Connection, Only(..), formatQuery) import qualified Database.PostgreSQL.Simple as P import           Database.PostgreSQL.Simple.ToRow (ToRow(..))+import           Database.PostgreSQL.Simple.Types (Query(..)) import           Database.PostgreSQL.Simple.FromRow (FromRow(..), field) import           Database.PostgreSQL.Simple.Transaction (withTransactionLevel, IsolationLevel(..))  -- | Migration information stored in 'migration' table.-data Migration = Migration Text Text+data Migration = Migration Text ByteString  instance FromRow Migration where   fromRow = Migration <$> field <*> field@@ -37,14 +39,21 @@ instance Exception MigrationError  -- | Context for migrations.-data MigrationContext = MigrationContext { mcMetaMigrationTable :: Table-                                         , mcMigrationTable :: Table+data MigrationContext = MigrationContext { mcMetaMigrationTable :: QIdentifier+                                         , mcMigrationTable :: QIdentifier                                          } +-- | Parameter wrapper for a query. Used in when there are several+-- sets of parameters which must have the same type.+data QP = forall p . ToRow p => QP p++instance ToRow QP where+  toRow (QP qp) = toRow qp+ -- | Apply a list of migrations to a database. For example, ----- > migrate conn schema [("a", "CREATE TABLE ...")]--- >                     [("b", "INSERT INTO TABLE ...")]+-- > migrate conn schema [("a", "CREATE TABLE ...", QP $ Only $ Table schema "foo")]+-- >                     [("b", "INSERT INTO TABLE ...", QP $ (Table schema "foo", "bar"))] -- -- will apply the given SQL statements __in order__ and track them by -- the identifiers "a" and "b". It is recommended to use __fixed__,@@ -54,6 +63,10 @@ -- can run the command `uuidgen -r` on the command line and paste that -- into your migration list. --+-- If the parameter sets are all of the same "shape" (type), then the+-- `P $` prefix may be omitted — it serves only to make sure that+-- the types match up.+-- -- The given 'Schema' parameter indicates the schema used for the -- /metadata/ stored to track which migrations have been applied. It -- does not affect the migrations themselves in any way. Therefore,@@ -68,15 +81,15 @@ -- "@\__peregrin_migration_meta\__@" and "@\__peregrin_migration\__@", -- which will automatically be created in the given 'Schema'. ---migrate :: Connection -> Schema -> [(Text, Text)] -> IO ()+migrate :: ToRow p => Connection -> Schema -> [(Text, Query, p)] -> IO () migrate connection schema =     migrate' tables connection schema   where-    tables = MigrationContext { mcMetaMigrationTable = Table schema "__peregrin_migration_meta__"-                              , mcMigrationTable = Table schema "__peregrin_migration__"+    tables = MigrationContext { mcMetaMigrationTable = QIdentifier schema "__peregrin_migration_meta__"+                              , mcMigrationTable = QIdentifier schema "__peregrin_migration__"                               } -migrate' :: MigrationContext -> Connection -> Schema -> [(Text, Text)] -> IO ()+migrate' :: ToRow p => MigrationContext -> Connection -> Schema -> [(Text, Query, p)] -> IO () migrate' tables c schema migrations = do   -- Must always create the "migration_meta" table (and its   -- schema) if necessary. Having just created this table without@@ -97,8 +110,10 @@   -- Apply all the migrations; we do it one-by-one since our lock is   -- itself automatically released by PostgreSQL at the end of each of   -- each transaction.-  forM_ migrations $ \(mid, sql) ->+  forM_ migrations $ \(mid, q, p) ->     withLock $ do+      -- Subsitute parameters+      sql <- formatQuery c q p       -- Check if change set has already been applied       existingMigration :: (Maybe Migration) <-         listToMaybe <$> query sqlFindMigration ( migrationTable@@ -114,7 +129,7 @@                                             , mid                                             , sql                                             )-          void $ execute_ $ fromString $ T.unpack sql+          void $ execute_ $ Query sql    where 
src/Database/Peregrin/Metadata.hs view
@@ -1,16 +1,28 @@ {-# LANGUAGE OverloadedStrings #-} module Database.Peregrin.Metadata-    ( Schema(..)-    , Table(..)-    , ToSQL(..)-    , Typ(..)+    ( Identifier(..)+    , QIdentifier(..)+    , Schema(..)     ) where -import qualified Data.Text as T import           Data.Text (Text) import           Database.PostgreSQL.Simple.ToField (ToField(..))-import           Database.PostgreSQL.Simple.Types (Identifier(..), QualifiedIdentifier(..))+import qualified Database.PostgreSQL.Simple.Types as PST +-- | An /unqalified/ identifier of an object in the database, i.e.+-- an identifier without an attached schema.+data Identifier = Identifier Text++instance ToField Identifier where+  toField (Identifier i) = toField $ PST.Identifier i++-- | A /qualified/ identifier of an object in the database, i.e.+-- an identifier with an attached schema.+data QIdentifier = QIdentifier Schema Text++instance ToField QIdentifier where+  toField (QIdentifier schema i) = toField $ PST.QualifiedIdentifier (Just $ schemaToText schema) i+ -- | A schema designation. data Schema = DefaultSchema             | NamedSchema Text@@ -22,51 +34,3 @@ schemaToText :: Schema -> Text schemaToText DefaultSchema = "public" schemaToText (NamedSchema schemaId) = schemaId---- | Create a qualified identifier.-mkQualifiedIdentifier :: Schema -> Text -> QualifiedIdentifier-mkQualifiedIdentifier schema = QualifiedIdentifier (Just $ schemaToText schema)---- | Table name, including which schema it is in.-data Table = Table Schema Text--instance ToField Table where-  toField (Table schema name) = toField $ mkQualifiedIdentifier schema name---- | Type name, including which schema it is in.-data Typ = Typ Schema Text--instance ToField Typ where-  toField (Typ schema name) = toField $ mkQualifiedIdentifier schema name---- | Quote a PostgreSQL object identifier. Useful in circumstances--- where you need to quote a dynamically generated identifier.-quoteToSQL :: Text -> Text-quoteToSQL i = T.concat [ singleQt-                        , T.replace singleQt doubleQt i-                        , singleQt-                        ]-  where-    singleQt = "\""-    doubleQt = "\"\""---- | Convert metadata object identifier to its quoted SQL--- representation.-class ToSQL a where-    toSQL :: a -> Text------- Instances-----instance ToSQL Schema where-  toSQL DefaultSchema = quoteToSQL "public"-  toSQL (NamedSchema schemaId) = quoteToSQL schemaId--instance ToSQL Table where-  toSQL (Table schema tableId) =-    T.concat [toSQL schema, ".", quoteToSQL tableId]--instance ToSQL Typ where-  toSQL (Typ schema tableId) =-    T.concat [toSQL schema, ".", quoteToSQL tableId]