mallard (empty) → 0.5.0.0
raw patch · 11 files changed
+704/−0 lines, 11 filesdep +Interpolationdep +basedep +byteablesetup-changed
Dependencies added: Interpolation, base, byteable, bytestring, cryptohash, exceptions, fgl, file-embed, hashable, hasql, hasql-optparse-applicative, hasql-pool, hasql-transaction, lens, mallard, megaparsec, mtl, optparse-applicative, optparse-text, path, path-io, text, unordered-containers
Files
- LICENSE +20/−0
- Setup.hs +2/−0
- app/Mallard.hs +92/−0
- lib/Database/Mallard.hs +15/−0
- lib/Database/Mallard/File.hs +39/−0
- lib/Database/Mallard/Graph.hs +56/−0
- lib/Database/Mallard/Parser.hs +122/−0
- lib/Database/Mallard/Postgre.hs +160/−0
- lib/Database/Mallard/Types.hs +58/−0
- lib/Database/Mallard/Validation.hs +67/−0
- mallard.cabal +73/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2016 Andrew Rademacher+++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Mallard.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++module Main where++import Control.Lens hiding (argument, noneOf)+import Control.Monad.Catch+import Control.Monad.IO.Class+import Control.Monad.Reader+import Control.Monad.State.Strict+import qualified Data.HashMap.Strict as Map+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import Data.Text.Lens hiding (text)+import Database.Mallard+import qualified Hasql.Connection as Sql+import Hasql.Options.Applicative+import qualified Hasql.Pool as Pool+import Options.Applicative hiding (Parser, runParser)+import Options.Applicative+import Options.Applicative.Text+import Path+import Path.IO++data AppOptions+ = AppOptions+ { _optionsRootDirectory :: Text+ , _optionsPostgreSettings :: Sql.Settings+ }+ deriving (Show)++$(makeClassy ''AppOptions)++appOptionsParser :: Parser AppOptions+appOptionsParser = AppOptions+ <$> argument text (metavar "ROOT")+ <*> connectionSettings Nothing++data AppState+ = AppState+ { _statePostgreConnection :: Pool.Pool+ }++$(makeClassy ''AppState)++instance HasPostgreConnection AppState where postgreConnection = statePostgreConnection++main :: IO ()+main = do+ appOpts <- execParser opts+ pool <- Pool.acquire (1, 30, appOpts ^. optionsPostgreSettings)+ let initState = AppState pool++ _ <- (flip runReaderT appOpts . flip runStateT initState) run+ `catchAll` (\e -> putStrLn (displayException e) >> return ((), initState))++ Pool.release pool+ where+ opts = info (appOptionsParser <**> helper)+ ( fullDesc+ <> progDesc "Apply migrations to a database server."+ <> header "migrator - applies PSQL database migrations." )++parseRelOrAbsDir :: (MonadThrow m, MonadCatch m, MonadIO m) => FilePath -> m (Path Abs Dir)+parseRelOrAbsDir file = parseAbsDir file `catch` (\(_::PathParseException) -> makeAbsolute =<< parseRelDir file)++run :: (MonadIO m, MonadCatch m, MonadReader AppOptions m, MonadState AppState m, MonadThrow m) => m ()+run = do+ --+ ensureMigratonSchema+ --+ appOpts <- ask+ root <- parseRelOrAbsDir (appOpts ^. optionsRootDirectory . unpacked)+ --+ mPlanned <- importMigrationDirectory root+ --+ mApplied <- getAppliedMigrations+ --+ let mGraph = fromJust $ mkMigrationGraph mPlanned+ --++ validateAppliedMigrations mPlanned mApplied++ --+ let unapplied = getUnappliedMigrations mGraph (Map.keys mApplied)+ toApply <- inflateMigrationIds mPlanned unapplied+ applyMigrations toApply+ --
+ lib/Database/Mallard.hs view
@@ -0,0 +1,15 @@+module Database.Mallard+ ( module File+ , module Graph+ , module Parser+ , module Postgre+ , module Types+ , module Validation+ ) where++import Database.Mallard.File as File+import Database.Mallard.Graph as Graph+import Database.Mallard.Parser as Parser+import Database.Mallard.Postgre as Postgre+import Database.Mallard.Types as Types+import Database.Mallard.Validation as Validation
+ lib/Database/Mallard/File.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Database.Mallard.File+ ( importMigrationDirectory+ , importMigrationFile+ ) where++import Control.Exception+import Control.Lens+import Control.Monad.Catch+import Control.Monad.Reader+import qualified Data.ByteString as BS+import qualified Data.HashMap.Strict as Map+import Database.Mallard.Parser+import Database.Mallard.Types+import Path+import Path.IO+import Text.Megaparsec++scanDirectoryForFiles :: (MonadIO m, MonadThrow m) => Path Abs Dir -> m [Path Abs File]+scanDirectoryForFiles dir = concat <$> walkDirAccum Nothing (\_ _ c -> return [c]) dir++importMigrationDirectory :: (MonadIO m, MonadThrow m) => Path Abs Dir -> m MigrationTable+importMigrationDirectory root = do+ files <- scanDirectoryForFiles root+ migrations <- mapM importMigrationFile' files+ return $ Map.fromList (fmap (\m -> (m ^. migrationName, m)) (concat migrations))++importMigrationFile :: (MonadIO m, MonadThrow m) => Path Abs File -> m MigrationTable+importMigrationFile file = Map.fromList . fmap (\m -> (m ^. migrationName, m)) <$> importMigrationFile' file++importMigrationFile' :: (MonadIO m, MonadThrow m) => Path Abs File -> m [Migration]+importMigrationFile' file = do+ fileContent <- liftIO $ BS.readFile (toFilePath file)+ let parseResult = runParser parseMigrations (toFilePath file) fileContent+ case parseResult of+ Left er -> throw $ ParserException file er+ Right m -> return m
+ lib/Database/Mallard/Graph.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Database.Mallard.Graph+ ( MigrationGraph+ , mkMigrationGraph+ , getUnappliedMigrations+ , emptyMigrationGraph+ ) where++import Control.Lens+import qualified Data.Graph.Inductive.Basic as G+import qualified Data.Graph.Inductive.Graph as G+import qualified Data.Graph.Inductive.PatriciaTree as G+import qualified Data.Graph.Inductive.Query.DFS as G+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import qualified Data.HashSet as Set+import Database.Mallard.Types++data MigrationGraph+ = MigrationGraph (HashMap MigrationId G.Node) (G.Gr MigrationId ())++emptyMigrationGraph :: MigrationGraph+emptyMigrationGraph = MigrationGraph Map.empty G.empty++-- | Graph will only build if there are no circular references.+mkMigrationGraph :: MigrationTable -> Maybe MigrationGraph+mkMigrationGraph mTable =+ if hasCircle graph+ then Nothing+ else Just $ MigrationGraph nodeLookupMap graph+ where+ migrations = Map.elems mTable+ nodeAssignment = zip [1..] (fmap (^. migrationName) migrations)+ nodeLookupMap = Map.fromList $ fmap (\(a, b) -> (b, a)) nodeAssignment+ lookupNode mName =+ case Map.lookup mName nodeLookupMap of+ Nothing -> error "This migration requires a migration that doesn't exist. (Non recoverable, contact andrewrademacher@icloud.com)"+ Just n -> n+ replaceRequires m = fmap lookupNode (m ^. migrationRequires)+ graph = G.grev+ $ G.insEdges (concatMap (\m' -> zip3 (repeat (lookupNode (m' ^. migrationName))) (replaceRequires m') (repeat ())) migrations)+ $ G.insNodes nodeAssignment G.empty++hasCircle :: G.Gr a b -> Bool+hasCircle g = or $ fmap (\g' -> length g' /= 1) $ G.scc g++getUnappliedMigrations :: MigrationGraph -> [MigrationId] -> [MigrationId]+getUnappliedMigrations (MigrationGraph mNodeTable mGraph) applied = G.topsort' unappliedGraph+ where+ appliedMigrationIds = Set.fromList applied+ unappliedGraph = flip G.delNodes mGraph+ $ fmap (\(_, v) -> v) $ Map.toList+ $ Map.filterWithKey (\k _ -> Set.member k appliedMigrationIds)+ $ mNodeTable
+ lib/Database/Mallard/Parser.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Database.Mallard.Parser+ ( ParserException (..)+ , parseMigration+ , parseMigrations+ ) where++import Control.Exception+import Control.Lens hiding (noneOf)+import Control.Monad+import Crypto.Hash+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import qualified Data.List.NonEmpty as NonEmpty+import Data.String.Interpolation+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Text.Lens+import Database.Mallard.Types+import Path+import Text.Megaparsec hiding (tab)+import Text.Megaparsec.ByteString+import qualified Text.Megaparsec.Lexer as L++type Description = Text+type Requires = MigrationId++data FieldValue+ = TextField Text+ | ListField [Text]++spaceConsumer :: Parser ()+spaceConsumer = L.space (void spaceChar) (L.skipLineComment "#") (L.skipBlockComment "##" "##")++symbol :: String -> Parser String+symbol = L.symbol spaceConsumer++brackets :: Parser a -> Parser a+brackets = between (symbol "[") (symbol "]")++comma :: Parser ()+comma = char ',' >> space++parseMigrations :: Parser [Migration]+parseMigrations = manyTill (space >> parseMigration) eof++parseMigration :: Parser Migration+parseMigration = do+ (name, description, requires) <- parseHeader+ content <- T.pack <$> manyTill anyChar (string "--/")+ return $ Migration+ { _migrationName = name+ , _migrationDescription = description+ , _migrationRequires = requires+ , _migrationChecksum = hash (T.encodeUtf8 content)+ , _migrationScript = content+ }++parseHeader :: Parser (MigrationId, Description, [Requires])+parseHeader = do+ fields <- parseHeaderFields+ case Map.lookup "name" fields of+ Nothing -> fail "The name field was not provided in the header."+ Just (ListField _) -> fail "The name field cannot be a list."+ Just (TextField name) ->+ case Map.lookup "description" fields of+ Nothing -> fail "The description field was not provided in the header."+ Just (ListField _) -> fail "The description field cannot be a list."+ Just (TextField description) ->+ case Map.lookup "requires" fields of+ Nothing -> return (MigrationId name, description, [])+ Just (TextField requires) -> return (MigrationId name, description, [MigrationId requires])+ Just (ListField requires) -> return (MigrationId name, description, fmap MigrationId requires)++parseFieldValue :: Parser FieldValue+parseFieldValue = parseTextValue <|> parseListValue+ where+ parseTextValue = TextField <$> parseQuotedText+ parseListValue = ListField <$> brackets (parseQuotedText `sepBy` comma)++parseHeaderFields :: Parser (HashMap Text FieldValue)+parseHeaderFields = Map.fromList <$> between (symbol "--/") (symbol "--|") (field `sepBy` newline)+ where+ field :: Parser (Text, FieldValue)+ field = do+ string "-- "+ name <- many (noneOf (" :"::String))+ space+ char ':'+ space+ value <- parseFieldValue+ return (T.pack name, value)++parseQuotedText :: Parser Text+parseQuotedText = do+ char '"'+ val <- many (noneOf ("\""::String))+ char '"'+ return $ val ^. packed++-- Exceptions++data ParserException+ = ParserException+ { _peFile :: (Path Abs File)+ , _peError :: ParseError Char Dec+ }+ deriving (Show)++instance Exception ParserException where+ displayException e = [str|+ Unable to parse file: $:toFilePath (_peFile e)$++ $tab$Line: $:sourceLine (NonEmpty.head (errorPos (_peError e)))$+ $tab$Column: $:sourceColumn (NonEmpty.head (errorPos (_peError e)))$+ $tab$Expected: $:errorExpected (_peError e)$+ $tab$Occurred: $:errorUnexpected (_peError e)$+ |]
+ lib/Database/Mallard/Postgre.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Database.Mallard.Postgre+ ( HasPostgreConnection (..)+ , DigestMismatchException (..)+ , ensureMigratonSchema+ , getAppliedMigrations+ , applyMigration+ , applyMigrations+ ) where++import Control.Exception+import Control.Lens+import Control.Monad.IO.Class+import Control.Monad.State+import Crypto.Hash+import Data.Byteable+import Data.ByteString (ByteString)+import Data.Foldable+import qualified Data.HashMap.Strict as Map+import Data.Int+import Data.Monoid+import Data.String.Interpolation+import qualified Data.Text.Encoding as T+import Database.Mallard.Types+import Database.Mallard.Validation+import qualified Hasql.Decoders as D+import qualified Hasql.Encoders as E+import qualified Hasql.Pool as Pool+import Hasql.Query+import Hasql.Session+import Hasql.Transaction (IsolationLevel (..), Mode (..),+ Transaction)+import qualified Hasql.Transaction as HT+import qualified Hasql.Transaction.Sessions as HT++class HasPostgreConnection a where+ postgreConnection :: Lens' a Pool.Pool++data MigrationSchemaVersion+ = NotInit+ | MigrationVersion Int64+ deriving (Eq, Show)++instance Ord MigrationSchemaVersion where+ compare NotInit NotInit = EQ+ compare NotInit (MigrationVersion _) = LT+ compare (MigrationVersion _) NotInit = GT+ compare (MigrationVersion a) (MigrationVersion b) = compare a b++ensureMigratonSchema :: (MonadIO m, MonadState s m, HasPostgreConnection s) => m ()+ensureMigratonSchema = do+ mversion <- getMigrationSchemaVersion+ let toApply =+ case mversion of+ NotInit -> scriptsMigrationSchema+ MigrationVersion v -> drop (fromIntegral (v + 1)) scriptsMigrationSchema+ flip mapM_ toApply $ \a@(version,_) -> do+ runDB $ HT.transaction Serializable Write $ applyMigrationSchemaMigraiton a+ liftIO $ putStrLn $ "Migrator Version: " <> show version++runDB :: (MonadIO m, MonadState s m, HasPostgreConnection s) => Session a -> m a+runDB session = do+ pool <- fmap (^. postgreConnection) get+ res <- liftIO $ Pool.use pool session+ case res of+ Left err -> error $ show err+ Right value -> return value++getAppliedMigrations+ :: (MonadIO m, MonadState s m, HasPostgreConnection s)+ => m MigrationTable+getAppliedMigrations = runDB $ do+ lst <- query () (statement stmt encoder decoder True)+ return $ Map.fromList $ fmap (\m -> (m ^. migrationName, m)) lst+ where+ stmt = "SELECT name, description, requires, checksum, script_text FROM mallard.applied_migrations;"+ encoder = E.unit+ decoder = D.rowsList $ Migration+ <$> D.value (MigrationId <$> D.text)+ <*> D.value D.text+ <*> D.value (D.array (D.arrayDimension replicateM (D.arrayValue (MigrationId <$> D.text))))+ <*> D.value valueDigest+ <*> D.value D.text++valueDigest :: (HashAlgorithm a) => D.Value (Digest a)+valueDigest = D.custom $ \_ bs ->+ case digestFromByteString bs of+ Nothing -> Left "ByteString was incorrect length for selected Digest type."+ Just v -> Right v++applyMigrations :: (MonadIO m, MonadState s m, HasPostgreConnection s) => [Migration] -> m ()+applyMigrations = mapM_ applyMigration++applyMigration :: (MonadIO m, MonadState s m, HasPostgreConnection s) => Migration -> m ()+applyMigration m = do+ runDB $ HT.transaction Serializable Write $ do+ HT.sql (T.encodeUtf8 (m ^. migrationScript))+ HT.query m (statement stmt encoder decoder True)+ liftIO $ putStrLn $ "Applied migration: " <> show (m ^. migrationName)+ where+ stmt = "INSERT INTO mallard.applied_migrations (name, description, requires, checksum, script_text) VALUES ($1, $2, $3, $4, $5)"+ encoder =+ contramap (unMigrationId . _migrationName) (E.value E.text) <>+ contramap _migrationDescription (E.value E.text) <>+ contramap (fmap unMigrationId . _migrationRequires) (E.value (E.array (E.arrayDimension foldl' (E.arrayValue E.text)))) <>+ contramap (toBytes . _migrationChecksum) (E.value E.bytea) <>+ contramap _migrationScript (E.value E.text)+ decoder = D.unit+++applyMigrationSchemaMigraiton :: (Int64, ByteString) -> Transaction ()+applyMigrationSchemaMigraiton (version, script) = do+ HT.sql script+ HT.query version (statement stmt encoder decoder True)+ where+ stmt = "INSERT INTO mallard.migrator_version (version) VALUES ($1)"+ encoder = E.value E.int8+ decoder = D.unit++getMigrationSchemaVersion+ :: (MonadIO m, MonadState s m, HasPostgreConnection s)+ => m MigrationSchemaVersion+getMigrationSchemaVersion = runDB $ do+ isInit <- isMigrationVersionZero+ if isInit+ then do+ version <- query () (statement stmt E.unit (D.maybeRow (D.value D.int8)) True)+ case version of+ Nothing -> return $ MigrationVersion 0+ Just x -> return $ MigrationVersion x+ else return NotInit+ where+ stmt = "SELECT coalesce(max(version), 0) as max_version FROM mallard.migrator_version"++isMigrationVersionZero :: Session Bool+isMigrationVersionZero = do+ mtable <- query () (statement stmt E.unit (D.maybeRow (D.value D.text)) True)+ case mtable of+ Nothing -> return False+ Just _ -> return True+ where+ stmt = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'mallard' AND table_name = 'migrator_version';"++-- Exceptions++data DigestSizeMismatchException+ = DigestSizeMismatchException+ deriving (Show)++instance Exception DigestSizeMismatchException where+ displayException _ = [str|+ The size of the applied migration's checksum is not valid. This may imply the+ algorithm used by this tool has changed.+ |]++ -- TODO: Add ability to reset all checksums in migration table.
+ lib/Database/Mallard/Types.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TemplateHaskell #-}++module Database.Mallard.Types where++import Control.Lens+import Control.Monad.Catch+import Crypto.Hash+import Data.ByteString (ByteString)+import Data.FileEmbed+import Data.Hashable+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as Map+import Data.Int+import Data.Text (Text)+import Data.Text.Lens++type MigrationTable = HashMap MigrationId Migration++inflateMigrationIds :: (MonadThrow m) => MigrationTable -> [MigrationId] -> m [Migration]+inflateMigrationIds mTable = mapM inflate+ where+ inflate mid =+ case Map.lookup mid mTable of+ Nothing -> throwM $ MigrationMissingFromTable mid+ Just m -> return m++data MigrationMissingFromTable+ = MigrationMissingFromTable MigrationId+ deriving (Show)++instance Exception MigrationMissingFromTable++newtype MigrationId = MigrationId { unMigrationId :: Text }+ deriving (Eq, Ord, Hashable)++instance Show MigrationId where+ show (MigrationId txt) = txt ^. unpacked++type MigrationDigest = Digest SHA256++data Migration+ = Migration+ { _migrationName :: MigrationId+ , _migrationDescription :: Text+ , _migrationRequires :: [MigrationId]+ , _migrationChecksum :: MigrationDigest+ , _migrationScript :: Text+ }+ deriving (Show)++$(makeClassy ''Migration)++scriptsMigrationSchema :: [(Int64, ByteString)]+scriptsMigrationSchema =+ [ (0, $(embedFile "sql/mallard/0000-setup.sql"))+ , (1, $(embedFile "sql/mallard/0001-applied-migrations.sql"))+ ]
+ lib/Database/Mallard/Validation.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Database.Mallard.Validation+ ( validateAppliedMigrations+ , validateAppliedMigration+ , AppliedMigrationMissingException (..)+ , DigestMismatchException (..)+ ) where++import Control.Exception+import Control.Lens+import Control.Monad.IO.Class+import qualified Data.HashMap.Strict as Map+import Data.String.Interpolation+import Database.Mallard.Types++-- | Validates applied migrations against planned migrations+validateAppliedMigrations :: (MonadIO m) => MigrationTable -> MigrationTable -> m ()+validateAppliedMigrations planned applied = mapM_ (validateAppliedMigration planned) (Map.elems applied)++validateAppliedMigration :: (MonadIO m) => MigrationTable -> Migration -> m ()+validateAppliedMigration plan aMig =+ case Map.lookup mid plan of+ Nothing -> throw $ AppliedMigrationMissingException mid+ Just pMig ->+ let pCheck = pMig ^. migrationChecksum+ aCheck = aMig ^. migrationChecksum+ in if aCheck == pCheck+ then return ()+ else throw $ DigestMismatchException mid pCheck mid aCheck+ where+ mid = aMig ^. migrationName++-- Exceptions++data AppliedMigrationMissingException+ = AppliedMigrationMissingException+ { _ammeAppliedMigrationName :: MigrationId+ }+ deriving (Show)++instance Exception AppliedMigrationMissingException where+ displayException e = [str|+ A migration that was previously applied is missing from the current migration plan.++ $tab$Applied Migration: $:_ammeAppliedMigrationName e$+ |]++data DigestMismatchException+ = DigestMismatchException+ { _dmePlannedMigrationName :: MigrationId+ , _dmePlannedMigrationChecksum :: MigrationDigest+ , _dmeAppliedMigrationName :: MigrationId+ , _dmeAppliedMigrationChecksum :: MigrationDigest+ }+ deriving (Show)++instance Exception DigestMismatchException where+ displayException d = [str|+ Mis-matching checksums indicate that a migration file has changed since it was applied.++ $tab$Planned Migration: $:_dmePlannedMigrationName d$ ($:_dmePlannedMigrationChecksum d$)+ $tab$Applied Migration: $:_dmeAppliedMigrationName d$ ($:_dmeAppliedMigrationChecksum d$)+ |]
+ mallard.cabal view
@@ -0,0 +1,73 @@+name: mallard+version: 0.5.0.0+synopsis: Database migration and testing as a library.+description: Please see README.md+homepage: https://github.com/AndrewRademacher/mallard+license: MIT+license-file: LICENSE+author: Andrew Rademacher+maintainer: andrewrademacher@icloud.com+copyright: 2017 Andrew Rademacher <andrewrademacher@icloud.com>+category: Database+build-type: Simple+cabal-version: >=1.10++library+ hs-source-dirs: lib+ default-language: Haskell2010++ ghc-options: -Wall -Wno-unused-do-bind++ exposed-modules: Database.Mallard.File+ Database.Mallard.Graph+ Database.Mallard.Parser+ Database.Mallard.Postgre+ Database.Mallard.Types+ Database.Mallard.Validation+ Database.Mallard++ build-depends: base >= 4 && < 5++ , byteable+ , bytestring+ , cryptohash+ , exceptions+ , fgl+ , file-embed+ , hashable+ , hasql+ , hasql-pool+ , hasql-transaction+ , Interpolation+ , lens+ , megaparsec+ , mtl+ , path+ , path-io+ , text+ , unordered-containers++executable mallard+ main-is: Mallard.hs+ hs-source-dirs: app+ default-language: Haskell2010++ ghc-options: -Wall++ build-depends: base >= 4 && < 5++ , mallard++ , exceptions+ , fgl+ , hasql+ , hasql-optparse-applicative+ , hasql-pool+ , lens+ , mtl+ , optparse-applicative+ , optparse-text+ , path+ , path-io+ , text+ , unordered-containers