diff --git a/dbmigrations.cabal b/dbmigrations.cabal
--- a/dbmigrations.cabal
+++ b/dbmigrations.cabal
@@ -1,5 +1,5 @@
 Name:                dbmigrations
-Version:             0.1
+Version:             0.1.1
 Synopsis:            An implementation of relational database "migrations"
 Description:         A library and program for the creation,
                      management, and installation of schema updates
@@ -59,6 +59,11 @@
           Database.Schema.Migrations.Backend
           Database.Schema.Migrations.Backend.HDBC
           Database.Schema.Migrations.Store
+
+  Other-Modules:
+          Database.Schema.Migrations.CycleDetection
+          Database.Schema.Migrations.Filesystem.Parse
+          Database.Schema.Migrations.Filesystem.Serialize
 
 Executable test
   Build-Depends:
diff --git a/src/Database/Schema/Migrations/CycleDetection.hs b/src/Database/Schema/Migrations/CycleDetection.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Schema/Migrations/CycleDetection.hs
@@ -0,0 +1,64 @@
+module Database.Schema.Migrations.CycleDetection
+    ( hasCycle
+    )
+where
+
+import Data.Graph.Inductive.Graph
+    ( Graph(..)
+    , Node
+    , nodes
+    , edges
+    )
+
+import Control.Monad.State ( State, evalState, gets, get, put )
+import Control.Monad ( forM )
+
+import Data.Maybe ( fromJust )
+import Data.List ( findIndex )
+
+data Mark = White | Gray | Black
+type CycleDetectionState = [(Node, Mark)]
+
+-- Cycle detection algorithm taken from http://www.cs.berkeley.edu/~kamil/teaching/sp03/041403.pdf
+hasCycle :: Graph g => g a b -> Bool
+hasCycle g = evalState (hasCycle' g) [(n, White) | n <- nodes g]
+
+getMark :: Int -> State CycleDetectionState Mark
+getMark n = gets (fromJust . lookup n) >>= return
+
+replace :: [a] -> Int -> a -> [a]
+replace elems index val
+    | index > length elems = error "replacement index too large"
+    | otherwise = (take index elems) ++
+                  [val] ++
+                  (reverse $ take ((length elems) - (index + 1)) $ reverse elems)
+
+setMark :: Int -> Mark -> State CycleDetectionState ()
+setMark n mark = do
+  st <- get
+  let index = fromJust $ findIndex (\(n', _) -> n' == n) st
+  put $ replace st index (n, mark)
+
+hasCycle' :: Graph g => g a b -> State CycleDetectionState Bool
+hasCycle' g = do
+  result <- forM (nodes g) $ \n -> do
+                   m <- getMark n
+                   case m of
+                     White -> visit g n
+                     _ -> return False
+  return $ or result
+
+visit :: Graph g => g a b -> Node -> State CycleDetectionState Bool
+visit g n = do
+  setMark n Gray
+  result <- forM [ v | (u,v) <- edges g, u == n ] $ \node -> do
+              m <- getMark node
+              case m of
+                Gray -> return True
+                White -> visit g node
+                _ -> return False
+  case or result of
+    True -> return True
+    False -> do
+              setMark n Black
+              return False
diff --git a/src/Database/Schema/Migrations/Filesystem/Parse.hs b/src/Database/Schema/Migrations/Filesystem/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Schema/Migrations/Filesystem/Parse.hs
@@ -0,0 +1,76 @@
+module Database.Schema.Migrations.Filesystem.Parse
+    ( migrationParser
+    , parseDepsList
+    , FieldName
+    , Field
+    , FieldSet
+    )
+where
+
+import Data.Time.Clock ()
+import Data.Maybe ( catMaybes )
+
+import Text.ParserCombinators.Parsec
+
+type FieldName = String
+type Field = (FieldName, String)
+type FieldSet = [Field]
+
+-- |Parse a migration document and return a list of parsed fields and
+-- a list of claimed dependencies.
+migrationParser :: Parser [Field]
+migrationParser = do
+  result <- many (parseField <|> parseComment <|> parseEmptyLine)
+  return $ catMaybes result
+
+parseDepsList :: Parser [String]
+parseDepsList =
+    let parseMID = many1 (alphaNum <|> oneOf "-._")
+    in do
+      deps <- sepBy parseMID whitespace
+      eol
+      return deps
+
+discard :: Parser a -> Parser ()
+discard = (>> return ())
+
+eol :: Parser ()
+eol = (discard newline) <|> (discard eof)
+
+whitespace :: Parser Char
+whitespace = oneOf " \t"
+
+requiredWhitespace :: Parser String
+requiredWhitespace = many1 whitespace
+
+parseFieldName :: Parser FieldName
+parseFieldName = many1 (alphaNum <|> char '-')
+
+parseComment :: Parser (Maybe Field)
+parseComment = do
+  discard $ do
+    many whitespace
+    char '#'
+    manyTill anyChar eol
+  return Nothing
+
+parseEmptyLine :: Parser (Maybe Field)
+parseEmptyLine = newline >> return Nothing
+
+parseField :: Parser (Maybe Field)
+parseField = do
+  name <- parseFieldName
+  char ':'
+  many whitespace
+  rest <- manyTill anyChar eol
+  otherLines <- otherContentLines
+  let value = rest ++ (concat otherLines)
+  return $ Just (name, value)
+
+otherContentLines :: Parser [String]
+otherContentLines =
+    many $ try $ (discard newline >> return "") <|> do
+      ws <- requiredWhitespace
+      rest <- manyTill anyChar eol
+      -- Retain leading whitespace and trailing newline
+      return $ ws ++ rest ++ "\n"
diff --git a/src/Database/Schema/Migrations/Filesystem/Serialize.hs b/src/Database/Schema/Migrations/Filesystem/Serialize.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Schema/Migrations/Filesystem/Serialize.hs
@@ -0,0 +1,69 @@
+module Database.Schema.Migrations.Filesystem.Serialize
+    ( serializeMigration
+    )
+where
+
+import Data.Time () -- for UTCTime Show instance
+import Data.Maybe ( catMaybes )
+import Data.List ( intercalate )
+
+import Database.Schema.Migrations.Migration
+    ( Migration(..)
+    )
+
+type FieldSerializer = Migration -> Maybe String
+
+fieldSerializers :: [FieldSerializer]
+fieldSerializers = [ serializeDesc
+                   , serializeTimestamp
+                   , serializeDepends
+                   , serializeApply
+                   , serializeRevert
+                   ]
+
+serializeDesc :: FieldSerializer
+serializeDesc m =
+    case mDesc m of
+      Nothing -> Nothing
+      Just desc -> Just $ "Description: " ++ desc
+
+serializeTimestamp :: FieldSerializer
+serializeTimestamp m = Just $ "Created: " ++ (show $ mTimestamp m)
+
+serializeDepends :: FieldSerializer
+serializeDepends m = Just $ "Depends: " ++ (intercalate " " $ mDeps m)
+
+serializeRevert :: FieldSerializer
+serializeRevert m =
+    case mRevert m of
+      Nothing -> Nothing
+      Just revert -> Just $ "Revert:\n" ++
+                     (serializeMultiline revert)
+
+serializeApply :: FieldSerializer
+serializeApply m = Just $ "Apply:\n" ++ (serializeMultiline $ mApply m)
+
+commonPrefix :: String -> String -> String
+commonPrefix a b = map fst $ takeWhile (uncurry (==)) (zip a b)
+
+commonPrefixLines :: [String] -> String
+commonPrefixLines [] = ""
+commonPrefixLines theLines = foldl1 commonPrefix theLines
+
+serializeMultiline :: String -> String
+serializeMultiline s =
+    let sLines = lines s
+        prefix = case commonPrefixLines sLines of
+                   -- If the lines already have a common prefix that
+                   -- begins with whitespace, no new prefix is
+                   -- necessary.
+                   (' ':_) -> ""
+                   -- Otherwise, use a new prefix of two spaces.
+                   _ -> "  "
+
+    in unlines $ map (prefix ++) sLines
+
+serializeMigration :: Migration -> String
+serializeMigration m = intercalate "\n" fields
+    where
+      fields = catMaybes [ f m | f <- fieldSerializers ]
