packages feed

mysql-simple-quasi-1.0.0.0: Database/MySQL/Simple/Quasi.hs

{-# LANGUAGE GADTs, QuasiQuotes, TemplateHaskell #-}
{-# OPTIONS_GHC -Wall #-}

module Database.MySQL.Simple.Quasi (
-- * Motivation
-- | So, you want to access a MySQL database from Haskell.
-- In an ideal world, your database schema would be known in its entirety to the compiler,
-- and your MySQL queries would be fully parsed at compile-time and type-checked.
--
-- In reality, constructing a full parser for a MySQL query is a huge job,
-- as is creating an EDSL for MySQL queries.  But, still, the mysql-simple package by itself is a little
-- too type-unsafe.  For example, there's nothing stopping you writing this:
--
-- > (a, b) <- query conn "select a, b, c from table where id = ? and name = ?" (True, "x", 7)
--
-- That is: there's no guarantee that the number of params or results in the query matches
-- the number you try to pass in\/receive out of the @query@ call.  Additionally, there's no
-- type checking on the inputs or outputs to the query.

-- * Description
-- | This module provides a quasi-quoter that is a half-way house between mysql-simple and
-- a full-on database library/EDSL.  You write your queries as a quasi-quote, and provide
-- type annotations /in the query itself/.  So for the previous example you would instead write:
--
-- > (a, b) <- query [qquery|select a{Int}, b{Bool}, c{Maybe String} from table where id = ?{Int} and name = ?{String}"|] (True, "x", 7)
--
-- You would then receive two type errors.  One that your tuple @(a, b)@ does not match @(Int, Bool, Maybe String)@,
-- and the other that @(True, \"x\", 7)@ does not match @(Int, String)@.
--
-- So, using the 'qquery' quasi-quoter gives the following benefits:
--
--   * It types the query with the annotated return type (and if any @?@ are used, the param types).
-- 
--   * It automatically chooses to use @QQuery@ or @QQuery_@ based on whether there are @?@ present.
-- 
--   * It automatically wraps/unwraps @Only@ types that mysql-simple uses to disambiguate its instances (but introduces no amibiguity).
-- 
--   * It prevents splicing together strings to make a query.  You must provide the entire query in one literal, and use
--   wildcards to adjust any values.  (I think this is a benefit, right?  Enforced discipline.)
--
-- One technique that I find useful to combine with this quasi-quoter is typing integer keys differently.
-- For example, let's say that you have two tables, @\"users\"@ and @\"locations\"@, each with an @\"id\"@ field.  You want
-- an inner join of these tables where the @\"id\"@s are less than a given amount.  If you use say, @Int32@, you can
-- easily get confused, even with this quasi-quoter:
--
-- > (loc, user) {- mistake -} <- [qquery|select users.id{Int32}, locations.id{Int32}
-- >   from users inner join locations
-- >   where users.location_type = locations.type and users.id < ?{Int32} and locations.id < ?{Int32}|]
-- >     (locIdThreshold, userIdThreshold) {- mistake -}
--
-- If instead you type them differently (with the appropriate instances), you will get two errors when doing this:
--
-- > newtype UserId = UserId Int
-- > newtype LocationId = LocationId Int
--
-- > (loc, user) {- mistake -} <- [qquery|select users.id{UserId}, locations.id{LocationId}
-- >   from users inner join locations
-- >   where users.location_type = locations.type and users.id < ?{UserId} and locations.id < ?{LocationId}|]
-- >     (locIdThreshold, userIdThreshold) {- mistake -}

-- * Quasi-Quoters
  qquery, qexec,
-- * Query Types
  QQuery, QQuery_, QExecute, QExecute_,
-- * Running queries
  execute, execute_, executeMany, fold, fold_, forEach, forEach_, query, query_,
-- * Internal access
  QExtractable(extractQuery)) where

import Control.Applicative ((<$>))
import Control.Monad (when)
import Data.Int (Int64)
import Data.String (fromString)

import Database.MySQL.Simple (Connection, Only(..))
import qualified Database.MySQL.Simple as Orig (Query, execute, execute_, executeMany, fold, fold_, forEach, forEach_, query, query_)
import Database.MySQL.Simple.Param (Param)
import Database.MySQL.Simple.QueryParams (QueryParams)
import Database.MySQL.Simple.QueryResults (QueryResults)

import Language.Haskell.TH.Syntax (Q, Exp(AppE, ConE, LitE, SigE, TupE, VarE), Lit(StringL), Name,
  Pred(ClassP), Type(AppT, ConT, ForallT, TupleT, VarT), TyVarBndr(PlainTV), mkName)
import Language.Haskell.TH.Quote (QuasiQuoter(..))

import Language.Haskell.Meta.Parse (parseType)

-- | A select-like query that takes `q` as its parameters and returns a list of `r` as its results.
data QQuery q r where
  QQuery :: (QueryParams q', QueryResults r') => (q -> q') -> (r' -> r) -> (String, Orig.Query) -> QQuery q r

-- | A select-like query that has no parameters, and returns a list of `r` as its results.
data QQuery_ r where
  QQuery_ :: QueryResults r' => (r' -> r) -> (String, Orig.Query) -> QQuery_ r

-- | An execute-like query that takes `q` as its parameters.
data QExecute q where
  QExecute :: QueryParams q' => (q -> q') -> (String, Orig.Query) -> QExecute q

-- | An execute-like query that has no parameters.  There's very little gain in using
-- this over using `Database.MySQL.Simple.execute_` from mysql-simple directly, but it's provided for completeness.
data QExecute_ where
  QExecute_ :: (String, Orig.Query) -> QExecute_

class QExtractable q where
  -- | Extracts the query.  This loses all the type safety
  -- of the original query and the whole point of using the library,
  -- but presumably you know what you're doing.
  extractQuery :: q -> String

instance QExtractable (QQuery q r) where extractQuery (QQuery _ _ q) = fst q

instance QExtractable (QQuery_ r) where extractQuery (QQuery_ _ q) = fst q

instance QExtractable (QExecute q) where extractQuery (QExecute _ q) = fst q

instance QExtractable QExecute_ where extractQuery (QExecute_ q) = fst q

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.query' function.
--
-- Note that no instances are required for @q@ or @r@ because the 'QQuery' type
-- witnesses them at its construction.
query :: Connection -> QQuery q r -> q -> IO [r]
query conn (QQuery qf rf (_, s)) = fmap (map rf) . Orig.query conn s . qf

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.query_' function.
--
-- Note that no instances are required for @r@ because the 'QQuery_' type
-- witnesses them at its construction.
query_ :: Connection -> QQuery_ r -> IO [r]
query_ conn (QQuery_ rf (_, s)) = fmap (map rf) $ Orig.query_ conn s

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.fold' function.
--
-- Note that no instances are required for @q@ or @r@ because the 'QQuery' type
-- witnesses them at its construction.
fold :: Connection -> QQuery q r -> q -> a -> (a -> r -> IO a) -> IO a
fold conn (QQuery qf rf (_, s)) q x f = Orig.fold conn s (qf q) x (\y r -> f y (rf r))

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.fold_' function.
--
-- Note that no instances are required for @r@ because the 'QQuery_' type
-- witnesses them at its construction.
fold_ :: Connection -> QQuery_ r -> a -> (a -> r -> IO a) -> IO a
fold_ conn (QQuery_ rf (_, s)) x f = Orig.fold_ conn s x (\y r -> f y (rf r))

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.forEach' function.
--
-- Note that no instances are required for @q@ or @r@ because the 'QQuery' type
-- witnesses them at its construction.
forEach :: Connection -> QQuery q r -> q -> (r -> IO ()) -> IO ()
forEach conn (QQuery qf rf (_, s)) q f = Orig.forEach conn s (qf q) (f . rf)

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.forEach_' function.
--
-- Note that no instances are required for @r@ because the 'QQuery_' type
-- witnesses them at its construction.
forEach_ :: Connection -> QQuery_ r -> (r -> IO ()) -> IO ()
forEach_ conn (QQuery_ rf (_, s)) f = Orig.forEach_ conn s (f . rf)

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.execute' function.
--
-- Note that no instances are required for @q@ because the 'QExecute' type
-- witnesses them at its construction.
execute :: Connection -> QExecute q -> q -> IO Int64
execute conn (QExecute qf (_, s)) = Orig.execute conn s . qf

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.execute_' function.
execute_ :: Connection -> QExecute_ -> IO Int64
execute_ conn (QExecute_ (_, s)) = Orig.execute_ conn s

-- | A wrapper
-- for mysql-simple's 'Database.MySQL.Simple.executeMany' function.
--
-- Note that no instances are required for @q@ because the 'QExecute' type
-- witnesses them at its construction.
executeMany :: Connection -> QExecute q -> [q] -> IO Int64
executeMany conn (QExecute qf (_, s)) = Orig.executeMany conn s . map qf

-- | A quasi-quoter that takes the param and result types from the query
-- string and generates a typed query.  For example:
--
-- > [qquery|select * from users|]
--
-- will turn into an expression of type @QueryResults r => QQuery_ r@.
-- This is not particularly useful.  However, this:
--
-- > [qquery|select id{Int32}, name{String} from users|]
-- 
-- becomes @QQuery_ (Int32, String)@.
--
-- Furthermore, this:
--
-- > [qquery|select id{Int32} from users where name = ?{String}|]
--
-- becomes: @QQuery String Int32@.  And this:
--
-- > [qquery| select a.*{Int, Maybe String, String}, b.value{Double}
-- >            from a inner join b on a.id = b.id
-- >            where a.name = ?{String} and b.num = ?{Int}|]
--
-- becomes: @QQuery (String, Int) (Int, Maybe String, String, Double)@.
--
-- In general:
-- 
-- * Any non-escaped question mark in the String is taken to be one
--   substitution.  It is given type @QueryParam a => a@ unless it is
--   followed immediately (no spaces) by curly brackets with a type in it,
--   in which case it uses that type.
-- 
-- * A question mark preceded by a backslash is turned into a single question mark.
-- 
-- * To insert an actual backslash, use double backslash.
-- 
-- * Any other instances of curly brackets in the String are taken to
--   be a comma-separated list of result types, which are all tupled
--   (in the order they appear in the String) into a single result type.
--   To get a literal curly bracket, put a backslash before it.
-- 
-- * If there is only a single substitution or single result, @Only@ is automatically added/removed
--   when passing it through to the mysql-simple library.
-- 
-- * If there is no @?@ substitution in the query, the resulting type is @QQuery_ r@.  If there are
--   substitutions, the resulting type is @QQuery q r@.
qquery :: QuasiQuoter
qquery = qqExp "qquery" qqueryExp

-- | Same as `qquery`, except that it produces a query of type QExecute/QExecute_
-- instead of QQuery/QQuery_, and it gives an error if there are any result annotations
-- (since executes don't return any results).
qexec :: QuasiQuoter
qexec = qqExp "qexec" qexecExp

qqExp :: String -> (String -> Q Exp) -> QuasiQuoter
qqExp name qq = QuasiQuoter {quoteExp = qq, quotePat = err, quoteType = err, quoteDec = err}
  where
    err :: String -> Q a
    err _ = fail $ name ++ " quasi-quoter only valid in expression"

emptyToNothing :: [a] -> [Maybe a]
emptyToNothing [] = [Nothing]
emptyToNothing xs = map Just xs

qqueryExp :: String -> Q Exp
qqueryExp input = do
  processed <- either fail return $ parse input
  let result = tuple QResult $ emptyToNothing $ returnTypes processed
      param = tuple QParam $ paramTypes processed
      noParams = null (paramTypes processed)
  return $
   SigE
    ((if noParams
        then ConE 'QQuery_ `AppE` wrapUnwrap result
        else (ConE 'QQuery `AppE` wrapUnwrap param) `AppE` wrapUnwrap result)
      `AppE` mkQuery (stripped processed))
    (makeContext (typeContext param ++ typeContext result) $
       if noParams
         then ConT ''QQuery_ `AppT` qtype result
         else (ConT ''QQuery `AppT` qtype param) `AppT` qtype result)
  -- Result looks like these:
  -- (QQuery_ id "select ...") :: QQuery_ (Int, String)
  -- (QQuery_ fromOnly "select ...") :: QQuery_ Int
  -- (QQuery_ id "select ...") :: (QueryResults a => QQuery_ a)
  -- (QQuery fromOnly Only "select ...") :: QQuery Int String
  -- (QQuery id Only "select ...") :: QQuery Int (String, String)
  -- (QQuery id id "select ...") :: QQuery (Int, Int) (Bool, String)
  -- (QQuery id id "select ...") :: (QueryResults a, QueryParam b) => (QQuery b a))

-- | A version of qqueryExp, above, that allows no return types:
qexecExp :: String -> Q Exp
qexecExp input = do
    processed <- either fail return $ parse input
    when (not . null $ returnTypes processed) $
      fail "Found return annotations in qexec query"
    let param = tuple QParam $ paramTypes processed
        noParams = null $ paramTypes processed
    return $
      SigE
        ((if noParams
            then ConE 'QExecute_
            else ConE 'QExecute `AppE` wrapUnwrap param)
         `AppE` mkQuery (stripped processed))
        (if noParams
           then ConT ''QExecute_
           else makeContext (typeContext param) $ ConT ''QExecute `AppT` qtype param)

mkQuery :: String -> Exp
mkQuery s = TupE [litS, VarE 'fromString `AppE` litS]
  where
    litS = LitE (StringL s)

data QResultOrParam = QResult | QParam

data QType = QT {
  -- | The expression needed to wrap/unwrap a type
  -- (Needed to add/remove the Only wrapper on single types)
  wrapUnwrap :: Exp,
  -- | Required context: class name, var name
  typeContext :: [(Name, Name)],
  -- | The actual type (may be a single type or a tuple):
  qtype :: Type }

-- | Takes a list of types and turns them 
tuple :: QResultOrParam -> [Maybe Type] -> QType
tuple rp mts = QT {wrapUnwrap = w, typeContext = ctxt, qtype = case subst (map VarT vars) mts of
  [t] -> t
  ts -> foldl AppT (TupleT (length ts)) ts}
  where
    w = case (mts, rp) of
          ([Just _], QResult) -> VarE 'fromOnly
          ([_], QParam) -> ConE 'Only
          _ -> VarE 'id
    (c, clz) = case rp of
                 QResult -> ('r', ''QueryResults)
                 QParam -> ('q', ''Param)

    ctxt = zip (repeat clz) vars
    vars = take (countNothings mts) [mkName $ c : suffix | suffix <- "" : map show [(1::Int)..]]

makeContext :: [(Name, Name)] -> (Type -> Type)
makeContext [] = id
makeContext classVars
  = ForallT (map (PlainTV . snd) classVars)
            [ClassP cls [VarT var] | (cls, var) <- classVars]

countNothings :: [Maybe a] -> Int
countNothings xs = length [() | Nothing <- xs]

subst :: [a] -> [Maybe a] -> [a]
subst _ [] = []
subst (s:ss) (Nothing:xs) = s : subst ss xs
subst [] (Nothing : _) = error "Internal error: not enough types to substitue"
subst ss (Just x : xs) = x : subst ss xs

(<:>) :: Functor m => Char -> m ProcessedQuery -> m ProcessedQuery
(<:>) c = fmap (\p -> p { stripped = c : stripped p })

toCloseCurly :: String -> Either String (String, String)
toCloseCurly src = case span (/= '}') src of
  (before, '}':after) -> return (before, after)
  _ -> Left $ "Unclosed '{' in remainder of expression: " ++ src

data ProcessedQuery = PQ { returnTypes :: [Type], paramTypes :: [Maybe Type], stripped :: String }

-- | Parses a String looking for various annotations. Gives back either an error,
-- or the query string with the annotations removed and processed into lists of types.
parse :: String -> Either String ProcessedQuery
-- Double backslash becomes single backslash:
parse ('\\':'\\':rest) = '\\' <:> parse rest
-- Backslashed question mark or curly bracket makes it literal:
parse ('\\':'{':rest) = '{' <:> parse rest
parse ('\\':'?':rest) = '?' <:> parse rest
-- Question mark then curly bracket is a param type:
parse ('?':'{':rest) = '?' <:> do
  (before, after) <- toCloseCurly rest
  types <- parseTypes before
  remainder <- parse after
  case types of
    [single] -> return $ remainder { paramTypes = Just single : paramTypes remainder }
    _ -> fail $ "Multiple types given for substitution: " ++ before
-- Any other curly bracket is a return type:
parse ('{':rest) = do
  (before, after) <- toCloseCurly rest
  types <- parseTypes before
  remainder <- parse after
  return $ remainder { returnTypes = types ++ returnTypes remainder }
parse ('?':rest) = '?' <:> do
  remainder <- parse rest
  return $ remainder { paramTypes = Nothing : paramTypes remainder }
parse (c:cs) = c <:> parse cs
parse "" = return $ PQ [] [] ""

-- | Takes a comma-separated list of types and parses them.
parseTypes :: String -> Either String [Type]
-- Slight hack; make sure a single type parses as a tuple too, by adding another type
-- so that it's at least size 2:
parseTypes s = case flattenAppT <$> parseType ("( ()," ++ s ++ ")") of
  Right (TupleT n : _ : xs) | n == 1 + length xs -> Right xs
  Left e -> Left e
  Right t -> Left $ "Bad type: " ++ show t

-- | Takes a type that is a series of nested applications and flattens it into a list.
-- For example, the type (Int, String, Bool) is really (,,) Int String Bool,
-- which is represented in an AST as:
-- ((( (,,) `AppT` Int) `AppT` String) `AppT` Bool)
-- or rather:
-- AppT (AppT (AppT (,,) Int) String) Bool
--
-- This function takes something like that and turns it into:
--
-- [(,,), Int, String, Bool]
flattenAppT :: Type -> [Type]
flattenAppT (AppT f t) = flattenAppT f ++ [t]
flattenAppT t = [t]