diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015-2016 Tobias Dammers
+
+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/src/Database/YeshQL.hs b/src/Database/YeshQL.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/YeshQL.hs
@@ -0,0 +1,287 @@
+{-#LANGUAGE TemplateHaskell #-}
+{-|
+Module: Database.YeshQL
+Description: Turn SQL queries into type-safe functions.
+Copyright: (c) 2015 Tobias Dammers
+Maintainer: Tobias Dammers <tdammers@gmail.com>
+Stability: experimental
+License: MIT
+
+Unlike existing libraries such as Esqueleto or Persistent, YeshQL does not try
+to provide full SQL abstraction with added type safety; instead, it gives you
+some simple tools to write the SQL queries yourself and bind them to (typed)
+functions.
+
+= Usage
+
+The main workhorses are the 'yesh1' (to define one query) and 'yesh' (to define
+multiple queries) quasi-quoters.
+
+Both 'yesh' and 'yesh1' can produce either declarations or expressions,
+depending on the context in which they are used.
+
+== Creating Declarations
+
+When used at the top level, or inside a @where@ block, the 'yesh' and 'yesh1'
+quasi-quoters will declare one or more functions, according to the query names
+given in the query definition. Example:
+
+@
+[yesh1|
+    -- name:insertUser :: (Integer)
+    -- :name :: String
+    INSERT INTO users (name) VALUES (:name) RETURNING id |]
+@
+
+...will create a top-level function of type:
+
+@
+    insertUser :: IConnection conn => conn -> String -> IO [Integer]
+@
+
+= Syntax
+
+Because SQL itself does not *quite* provide enough information to generate a
+fully typed Haskell function, we extend SQL syntax a bit.
+
+Here's what a typical YeshQL definition looks like:
+
+@
+[yesh|
+    -- name:insertUser :: (Integer)
+    -- :name :: String
+    INSERT INTO users (name) VALUES (:name) RETURNING id;
+    -- name:deleteUser :: Integer
+    -- :id :: Integer
+    DELETE FROM users WHERE id = :id;
+    -- name:getUser :: (Integer, String)
+    -- :id :: Integer
+    SELECT id, name FROM users WHERE id = :id;
+    -- name:getUserEx :: (Integer, String)
+    -- :id :: Integer
+    -- :filename :: String
+    SELECT id, name FROM users WHERE name = :filename OR id = :id;
+    |]
+@
+
+On top of standard SQL syntax, YeshQL query definitions are preceded by some
+extra information in order to generate well-typed HDBC queries. All that
+information is written in SQL line comments (@-- ...@), such that a valid
+YeshQL definition is also valid SQL by itself (with the exception of
+parameters, which follow the pattern @:paramName@).
+
+Let's break it down:
+
+@
+    -- name:insertUser :: (Integer)
+@
+
+This line tells YeshQL to generate an object called @insertUser@, which should
+be a function of type @IConnection conn => conn -> {...} -> IO (Integer)@
+(where the @{...}@ part depends on query parameters, see below).
+
+The declared return type can be one of the following:
+
+- '()'; the generated function will ignore any and all results from the query
+  and always return '()'.
+- An integer scalar, e.g. @Integer@ or @Int@; the generated function will
+  return a row count from @INSERT@ / @UPDATE@ / ... statements, or 0 from
+  @SELECT@ statements.
+- A tuple, where all elements implement 'FromSql'; the function will return
+  the result set from a @SELECT@ query as a list of tuples, or an empty list
+  for other query types.
+- A "one-tuple", i.e., a type in parentheses. The return value will be a list
+  of scalars, containing the values from the first (or only) column in
+  the result set. Note that, unlike Haskell, YeshQL does distinguish between
+  @Type@ and @(Type)@: the former is a scalar type, while the latter is a
+  one-tuple whose only element is of type @Type@.
+
+@
+    -- :paramName :: Type
+@
+
+Declares a Haskell type for a parameter. The parameter @:paramName@ can then
+be referenced zero or more times in the query itself, and will appear in the
+generated function signature in the order of declaration. So in the above
+example, the last query definition:
+
+@
+    -- name:getUserEx :: (Integer, String)
+    -- :id :: Integer
+    -- :filename :: String
+    SELECT id, name FROM users WHERE name = :filename OR id = :id;
+@
+
+...will produce the function:
+
+@
+getUserEx :: IConnection conn => conn -> Integer -> String -> IO [(Integer, String)]
+getUserEx conn id filename =
+    -- ... generated implementation left out
+@
+
+ -}
+module Database.YeshQL
+( yesh, yesh1
+, mkQueryDecs
+, mkQueryExp
+, parseQuery
+, ParsedQuery (..)
+)
+where
+
+import Language.Haskell.TH
+import Language.Haskell.TH.Quote
+import Data.List (isPrefixOf, foldl')
+import Data.Maybe (catMaybes, fromMaybe)
+import Database.HDBC (fromSql, toSql, run, ConnWrapper, IConnection, quickQuery')
+import qualified Text.Parsec as P
+import Data.Char (chr, ord, toUpper, toLower)
+import Control.Applicative ( (<$>), (<*>) )
+
+import Database.YeshQL.Parser
+
+nthIdent :: Int -> String
+nthIdent i
+    | i < 26 = [chr (ord 'a' + i)]
+    | otherwise = let (j, k) = divMod i 26
+                    in nthIdent j ++ nthIdent k
+
+-- | Generate a top-level declaration or an expression for a single SQL query.
+yesh1 :: QuasiQuoter
+yesh1 = QuasiQuoter
+        { quoteDec = withParsedQuery mkQueryDecs
+        , quoteExp = withParsedQuery mkQueryExp
+        , quoteType = error "yesh1 does not generate types"
+        , quotePat = error "yesh1 does not generate patterns"
+        }
+
+-- | Generate top-level declarations or expressions for several SQL queries.
+yesh :: QuasiQuoter
+yesh = QuasiQuoter
+        { quoteDec = withParsedQueries mkQueryDecsMulti
+        , quoteExp = withParsedQueries mkQueryExpMulti
+        , quoteType = error "yesh does not generate types"
+        , quotePat = error "yesh does not generate patterns"
+        }
+
+queryName :: String -> String -> Name
+queryName prefix basename =
+    mkName $ prefix ++ ucfirst basename
+
+ucfirst :: String -> String
+ucfirst "" = ""
+ucfirst (x:xs) = toUpper x:xs
+
+lcfirst :: String -> String
+lcfirst "" = ""
+lcfirst (x:xs) = toLower x:xs
+
+withParsedQuery :: (ParsedQuery -> Q a) -> String -> Q a
+withParsedQuery = withParsed parseQuery
+
+withParsedQueries :: ([ParsedQuery] -> Q a) -> String -> Q a
+withParsedQueries = withParsed parseQueries
+
+withParsed :: (Monad m, Show e) => (s -> Either e a) -> (a -> m b) -> s -> m b
+withParsed p a src = do
+    let parseResult = p src
+    arg <- case parseResult of
+                Left e -> fail . show $ e
+                Right x -> return x
+    a arg
+
+
+pgQueryType :: ParsedQuery -> TypeQ
+pgQueryType query =
+    [t|IConnection conn =>
+        $(foldr
+            (\a b -> [t| $a -> $b |])
+            [t| conn -> IO $(returnType) |]
+          $ argTypes)
+      |]
+    where
+        argTypes = map (mkType . fromMaybe AutoType . pqTypeFor query) (pqParamNames query)
+        returnType = case pqReturnType query of
+                        Left tn -> mkType tn
+                        Right [] -> tupleT 0
+                        Right (x:[]) -> appT listT $ mkType x
+                        Right xs -> appT listT $ foldl' appT (tupleT $ length xs) (map mkType xs)
+
+mkType :: ParsedType -> Q Type
+mkType (MaybeType n) = [t|Maybe $(conT . mkName $ n)|]
+mkType (PlainType n) = conT . mkName $ n
+mkType AutoType = [t|String|]
+
+mkQueryDecsMulti :: [ParsedQuery] -> Q [Dec]
+mkQueryDecsMulti queries = concat <$> mapM mkQueryDecs queries
+
+mkQueryExpMulti :: [ParsedQuery] -> Q Exp
+mkQueryExpMulti queries =
+    foldl1 (\a b -> VarE '(>>) `AppE` a `AppE` b) <$> mapM mkQueryExp queries
+
+pqNames :: ParsedQuery -> ([Name], [PatQ], String, TypeQ)
+pqNames query =
+    let argNamesStr = pqParamNames query ++ ["conn"]
+        argNames = map mkName argNamesStr
+        patterns = map varP argNames
+        funName = pqQueryName query
+        queryType = pgQueryType query
+    in (argNames, patterns, funName, queryType)
+
+mkQueryDecs :: ParsedQuery -> Q [Dec]
+mkQueryDecs query = do
+    let (argNames, patterns, funName, queryType) = pqNames query
+    sRun <- sigD (mkName . lcfirst $ funName) queryType
+    fRun <- funD (mkName . lcfirst $ funName)
+                [ clause
+                    (map varP argNames)
+                    (normalB . mkQueryBody $ query)
+                    []
+                ]
+    sDescribe <- sigD (queryName "describe" funName) [t|String|]
+    fDescribe <- funD (queryName "describe" funName)
+                    [ clause
+                        []
+                        (normalB . litE . stringL . pqQueryString $ query)
+                        []
+                    ]
+    sDocument <- sigD (queryName "doc" funName) [t|String|]
+    fDocument <- funD (queryName "doc" funName)
+                    [ clause
+                        []
+                        (normalB . litE . stringL . pqDocComment $ query)
+                        []
+                    ]
+    return [sRun, fRun, sDescribe, fDescribe, sDocument, fDocument]
+
+mkQueryExp :: ParsedQuery -> Q Exp
+mkQueryExp query = do
+    let (argNames, patterns, funName, queryType) = pqNames query
+    sigE
+        (lamE patterns (mkQueryBody query))
+        queryType
+
+mkQueryBody :: ParsedQuery -> Q Exp
+mkQueryBody query = do
+    let (argNames, patterns, funName, queryType) = pqNames query
+
+        convert :: ExpQ
+        convert = case pqReturnType query of
+                    Left tn -> varE 'fromInteger
+                    Right [] -> [|\_ -> ()|]
+                    Right (x:[]) -> [|map (fromSql . head)|]
+                    Right xs ->
+                        let varNames = map nthIdent [0..pred (length xs)]
+                        in [|map $(lamE
+                                    -- \[a,b,c,...] ->
+                                    [(listP (map (varP . mkName) varNames))]
+                                    -- (fromSql a, fromSql b, fromSql c, ...)
+                                    (tupE $ (map (\n -> appE (varE 'fromSql) (varE . mkName $ n)) varNames)))|]
+        queryFunc = case pqReturnType query of
+                        Left _ -> [| \qstr params conn -> $convert <$> run conn qstr params |]
+                        Right _ -> [| \qstr params conn -> $convert <$> quickQuery' conn qstr params |]
+    queryFunc
+        `appE` (litE . stringL . pqQueryString $ query)
+        `appE` (listE [ appE (varE 'toSql) (varE . mkName $ n) | (n, t) <- (pqParamsRaw query) ])
+        `appE` (varE . mkName $ "conn")
diff --git a/src/Database/YeshQL/Parser.hs b/src/Database/YeshQL/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/YeshQL/Parser.hs
@@ -0,0 +1,227 @@
+module Database.YeshQL.Parser
+( parseQuery
+, parseQueries
+, ParsedQuery (..)
+, ParsedType (..)
+, pqTypeFor
+)
+where
+
+import Text.Parsec
+import Control.Applicative ( (<$>), (<*>) )
+
+import qualified Data.Map.Strict as Map
+import Data.Map (Map)
+
+import Data.List (foldl', nub)
+import Data.Maybe (catMaybes, fromMaybe)
+
+data ParsedType = PlainType String | MaybeType String | AutoType
+    deriving Show
+
+data ParsedQuery =
+    ParsedQuery
+        { pqQueryName :: String
+        , pqQueryString :: String
+        , pqParamsRaw :: [(String, ParsedType)]
+        , pqParamNames :: [String]
+        , pqParamTypes :: Map String ParsedType
+        , pqReturnType :: Either ParsedType [ParsedType]
+        , pqDocComment :: String
+        }
+        deriving (Show)
+
+pqTypeFor :: ParsedQuery -> String -> Maybe ParsedType
+pqTypeFor q pname = Map.lookup pname (pqParamTypes q)
+
+parsedQuery :: String -> String -> [(String, ParsedType)] -> [(String, ParsedType)] -> Either ParsedType [ParsedType] -> String -> ParsedQuery
+parsedQuery queryName queryString paramsRaw paramsExtra returnType docComment =
+    ParsedQuery
+        queryName
+        queryString
+        paramsRaw
+        (extractParamNames (paramsExtra ++ paramsRaw))
+        (extractParamTypeMap (paramsExtra ++ paramsRaw))
+        returnType
+        docComment
+
+extractParamNames :: [(String, ParsedType)] -> [String]
+extractParamNames xs = 
+    nub . map fst $ xs
+
+extractParamTypeMap :: [(String, ParsedType)] -> Map String ParsedType
+extractParamTypeMap = foldl' applyItem Map.empty
+    where
+        applyItem :: Map String ParsedType -> (String, ParsedType) -> Map String ParsedType
+        applyItem m (n, t) =
+            let tc = Map.lookup n m
+            in case (tc, t) of
+                (Nothing, AutoType) -> m
+                (Nothing, t) -> Map.insert n t m
+                (Just AutoType, AutoType) -> m
+                (Just AutoType, t) -> Map.insert n t m
+                (Just t', AutoType) -> m
+                (Just t', t) -> error $ "Inconsistent types found for parameter '" ++ n ++ "': '" ++ show t' ++ "' vs. '" ++ show t ++ "'"
+
+data ParsedItem = ParsedLiteral String | ParsedParam String ParsedType | ParsedComment String
+
+extractParsedQuery :: [ParsedItem] -> String
+extractParsedQuery = concat . map extractItem
+    where
+        extractItem :: ParsedItem -> String
+        extractItem (ParsedLiteral str) = str
+        extractItem (ParsedComment _) = ""
+        extractItem (ParsedParam _ _) = "?"
+
+extractParsedParams :: [ParsedItem] -> [(String, ParsedType)]
+extractParsedParams = catMaybes . map extractItem
+    where
+        extractItem :: ParsedItem -> Maybe (String, ParsedType)
+        extractItem (ParsedParam n t) = Just (n, t)
+        extractItem _ = Nothing
+
+extractDocComment :: [ParsedItem] -> String
+extractDocComment = unlines . catMaybes . map extractItem
+    where
+        extractItem :: ParsedItem -> Maybe String
+        extractItem (ParsedComment str) = Just str
+        extractItem _ = Nothing
+
+parseQuery :: String -> Either ParseError ParsedQuery
+parseQuery src = runParser mainP () "query" src
+
+parseQueries :: String -> Either ParseError [ParsedQuery]
+parseQueries src = runParser multiP () "queries" src
+
+mainP :: Parsec String () ParsedQuery
+mainP = do
+    q <- queryP
+    eof
+    return q
+
+multiP :: Parsec String () [ParsedQuery]
+multiP = do
+    fmap catMaybes $
+        sepBy queryMayP sepP
+    where
+        sepP :: Parsec String () ()
+        sepP = try (spaces >> char ';' >> notFollowedBy (char ';') >> spaces)
+        queryMayP :: Parsec String () (Maybe ParsedQuery)
+        queryMayP = do
+            spaces
+            fmap Just queryP <|> (eof >> return Nothing) <?> "SQL query"
+
+queryP :: Parsec String () ParsedQuery
+queryP = do
+    spaces
+    (qn, retType) <- option ("query", Left (PlainType "Integer")) $ nameDeclP <|> namelessDeclP
+    extraItems <- many (paramDeclP <|> commentP)
+    items <- many (try commentP <|> try itemP)
+    return $ parsedQuery
+                qn
+                (extractParsedQuery items)
+                (extractParsedParams items)
+                (extractParsedParams extraItems)
+                retType
+                (extractDocComment (extraItems ++ items))
+
+nameDeclP :: Parsec String () (String, Either ParsedType [ParsedType])
+nameDeclP = do
+    try (whitespaceP >> string "--" >> whitespaceP >> string "name" >> whitespaceP >> char ':')
+    whitespaceP
+    qn <- identifierP
+    whitespaceP
+    retType <- option (Left (PlainType "Integer")) (try (string "::" >> whitespaceP >> returnTypeP))
+    whitespaceP
+    newlineP
+    return (qn, retType)
+
+namelessDeclP :: Parsec String () (String, Either ParsedType [ParsedType])
+namelessDeclP = do
+    try (whitespaceP >> string "--" >> whitespaceP >> string "::" >> whitespaceP)
+    retType <- returnTypeP
+    whitespaceP
+    newlineP
+    return ("query", retType)
+
+identifierP :: Parsec String () String
+identifierP =
+    (:) <$> leadCharP <*> many tailCharP
+    where
+        leadCharP = oneOf $ ['a'..'z'] ++ ['A'..'Z'] ++ "_"
+        tailCharP = oneOf $ ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "_"
+
+returnTypeP :: Parsec String () (Either ParsedType [ParsedType])
+returnTypeP = returnTypeMultiP <|> returnTypeSingleP
+
+returnTypeSingleP :: Parsec String () (Either ParsedType [ParsedType])
+returnTypeSingleP = Left <$> typeP
+
+returnTypeMultiP :: Parsec String () (Either ParsedType [ParsedType])
+returnTypeMultiP =
+    Right <$> between
+        (char '(' >> whitespaceP)
+        (char ')' >> whitespaceP)
+        (sepBy (between whitespaceP whitespaceP typeP) (char ','))
+
+typeP :: Parsec String () ParsedType
+typeP = do
+    name <- identifierP
+    option (PlainType name) $ do
+        char '?'
+        return $ MaybeType name
+
+itemP :: Parsec String () ParsedItem
+itemP = paramP <|> quotedP <|> literalP
+
+paramDeclP :: Parsec String () ParsedItem
+paramDeclP = do
+    try $ (whitespaceP >> string "--" >> whitespaceP >> char ':')
+    name <- identifierP
+    whitespaceP
+    t <- option AutoType $ do
+            string "::"
+            whitespaceP
+            t <- typeP
+            whitespaceP
+            return t
+    newlineP
+    return $ ParsedParam name t
+
+commentP :: Parsec String () ParsedItem
+commentP = do
+    try (whitespaceP >> string "--")
+    whitespaceP
+    ParsedComment <$> manyTill (noneOf " \t\n;") (newlineP <|> ignore (char ';'))
+
+paramP :: Parsec String () ParsedItem
+paramP = do
+    char ':'
+    pname <- identifierP
+    ptype <- option AutoType $ do
+                string "::"
+                typeP
+    return $ ParsedParam pname ptype
+
+quotedP :: Parsec String () ParsedItem
+quotedP = do
+    char '\''
+    contents <- many (noneOf "'")
+    char '\''
+    return . ParsedLiteral . ('\'':) . (++ "'") $ contents
+
+literalP :: Parsec String () ParsedItem
+literalP = ParsedLiteral <$> many1 (noneOf ":';")
+
+whitespaceP :: Parsec String () ()
+whitespaceP = do
+    many (oneOf " \t\r")
+    return ()
+
+ignore :: Parsec s u a -> Parsec s u ()
+ignore x = x >> return ()
+
+newlineP :: Parsec String () ()
+newlineP = do
+    (ignore $ char '\n') <|> (ignore . try . string $ ";;")
+    return ()
diff --git a/tests/tests.hs b/tests/tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/tests.hs
@@ -0,0 +1,12 @@
+{-#LANGUAGE TemplateHaskell #-}
+{-#LANGUAGE QuasiQuotes #-}
+module Main where
+
+import Test.Tasty
+import qualified Database.YeshQL.SimulationTests as SimulationTests
+
+main = defaultMain allTests
+    where
+        allTests = testGroup "All Tests"
+            [ testGroup "Simulation Tests" SimulationTests.tests
+            ]
diff --git a/yeshql.cabal b/yeshql.cabal
new file mode 100644
--- /dev/null
+++ b/yeshql.cabal
@@ -0,0 +1,36 @@
+name: yeshql
+version: 0.1.0.0
+synopsis: YesQL-style SQL database abstraction
+description:
+license: MIT
+license-file: LICENSE
+author: Tobias Dammers
+maintainer: tdammers@gmail.com
+copyright: 2015-2016 Tobias Dammers
+category: Database
+build-type: Simple
+-- extra-source-files:
+cabal-version: >=1.10
+
+library
+    exposed-modules: Database.YeshQL
+    other-modules: Database.YeshQL.Parser
+    -- other-extensions:
+    build-depends: base >=4.6 && <5.0
+                 , template-haskell
+                 , HDBC >= 2.4 && <3.0
+                 , parsec >= 3.0 && <4.0
+                 , containers >= 0.5 && < 1.0
+    hs-source-dirs: src
+    default-language: Haskell2010
+test-suite tests
+    type: exitcode-stdio-1.0
+    build-depends: base >=4.6 && <5.0
+                 , yeshql
+                 , stm
+                 , tasty
+                 , tasty-hunit
+                 , tasty-quickcheck
+                 , HDBC
+    hs-source-dirs: tests
+    main-is: tests.hs
