diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Mallard.hs b/app/Mallard.hs
new file mode 100644
--- /dev/null
+++ b/app/Mallard.hs
@@ -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
+    --
diff --git a/lib/Database/Mallard.hs b/lib/Database/Mallard.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard.hs
@@ -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
diff --git a/lib/Database/Mallard/File.hs b/lib/Database/Mallard/File.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/File.hs
@@ -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
diff --git a/lib/Database/Mallard/Graph.hs b/lib/Database/Mallard/Graph.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/Graph.hs
@@ -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
diff --git a/lib/Database/Mallard/Parser.hs b/lib/Database/Mallard/Parser.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/Parser.hs
@@ -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)$
+    |]
diff --git a/lib/Database/Mallard/Postgre.hs b/lib/Database/Mallard/Postgre.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/Postgre.hs
@@ -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.
diff --git a/lib/Database/Mallard/Types.hs b/lib/Database/Mallard/Types.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/Types.hs
@@ -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"))
+    ]
diff --git a/lib/Database/Mallard/Validation.hs b/lib/Database/Mallard/Validation.hs
new file mode 100644
--- /dev/null
+++ b/lib/Database/Mallard/Validation.hs
@@ -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$)
+    |]
diff --git a/mallard.cabal b/mallard.cabal
new file mode 100644
--- /dev/null
+++ b/mallard.cabal
@@ -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
