packages feed

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 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