mysql-simple-quasi (empty) → 1.0.0.0
raw patch · 5 files changed
+561/−0 lines, 5 filesdep +Cabaldep +basedep +haskell-src-metasetup-changed
Dependencies added: Cabal, base, haskell-src-meta, mysql-simple, syb, template-haskell
Files
- Database/MySQL/Simple/Quasi.hs +401/−0
- Database/MySQL/Simple/Quasi/Test.hs +100/−0
- LICENSE +31/−0
- Setup.hs +2/−0
- mysql-simple-quasi.cabal +27/−0
+ Database/MySQL/Simple/Quasi.hs view
@@ -0,0 +1,401 @@+{-# 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]
+ Database/MySQL/Simple/Quasi/Test.hs view
@@ -0,0 +1,100 @@+{-# OPTIONS_GHC -Wall -fno-warn-missing-signatures #-}+--module Database.MySQL.Simple.QQ.Test where++import Control.Applicative ((<$>))+import System.Exit (ExitCode(..), exitWith)++import Data.Generics (everywhere, mkT)+import Data.Generics.Text (gshow)+import Data.List (isPrefixOf)++import Database.MySQL.Simple.Quasi++import Language.Haskell.TH (runQ)+import Language.Haskell.TH.Ppr (pprint)+import Language.Haskell.TH.Quote (quoteExp)+import qualified Language.Haskell.TH.Syntax as TH+import Language.Haskell.Meta.Parse (parseExp)++qual :: [(String, String)] -> String -> String+qual prefixTgts = go prefixTgts+ where+ go _ [] = []+ go ((prefix, tgt):more) src+ | tgt `isPrefixOf` src = prefix ++ tgt ++ go prefixTgts (drop (length tgt) src)+ | otherwise = go more src+ go [] (c:cs) = c : go prefixTgts cs+++collapseForall :: TH.Exp -> TH.Exp+collapseForall (TH.SigE e t) = TH.SigE e $ collapseForallT t+ where+ collapseForallT (TH.ForallT ts [] (TH.ForallT [] ctxt inner)) = TH.ForallT ts ctxt inner+ collapseForallT x = x+collapseForall e = e++-- | Code from qquery uses NameG but haskell-src-meta uses NameQ.+-- This hack turns all NameG into NameQ ready for comparison:+canonName :: TH.Name -> TH.Name+canonName = TH.mkName . show ++unqualName :: TH.Name -> TH.Name+unqualName = TH.mkName . TH.nameBase++asCode :: TH.Exp -> String+asCode = pprint . everywhere (mkT unqualName)++dupeQueries :: String -> String+dupeQueries ('$':'\"':rest)+ = let (queryStr, '\"':more) = span (/= '\"') rest+ in "(\"" ++ queryStr ++ "\", Data.String.fromString \"" ++ queryStr ++ "\")" ++ dupeQueries more+dupeQueries (c:cs) = c : dupeQueries cs+dupeQueries [] = []++(==>) :: String -> String -> IO Bool+(==>) src expS = do+ act <- everywhere (mkT canonName) <$> runQ (quoteExp qquery src)+ let expS' = qual [("Database.MySQL.Simple.QQ.", "QQuery") -- Handles QQuery_ too+ ,("Database.MySQL.Simple.QueryResults.", "QueryResults")+ ,("Database.MySQL.Simple.Param.", "Param")+ ,("Database.MySQL.Simple.Types.", "fromOnly") -- Try fromOnly before Only+ ,("Database.MySQL.Simple.Types.", "Only")+ ] . dupeQueries $ expS+ case collapseForall <$> parseExp expS' of+ Left e -> do putStrLn $ "Problem parsing expected output: " ++ e+ return False+ Right x | x == act -> return True+ | otherwise -> do mapM_ putStrLn+ [ "Mismatch. Expected:", " " ++ asCode x, " " ++ gshow x+ , "Actual:", " " ++ asCode act, " " ++ gshow act]+ return False++simple = "select * from users" ==> "QQuery_ GHC.Base.id $\"select * from users\" :: forall r. QueryResults r => QQuery_ r"++ret1 = "select id{Int} from users" ==> "QQuery_ fromOnly $\"select id from users\" :: QQuery_ Int" ++ret2 = "select id{Int}, name{String} from users" ==> "QQuery_ GHC.Base.id $\"select id, name from users\" :: QQuery_ (Int, String)"++ret4 = "select id{Int}, f.*{String, Bool}, name{Double} from users, f" ==>+ "QQuery_ GHC.Base.id $\"select id, f.*, name from users, f\" :: QQuery_ (Int, String, Bool, Double)"++subst1 = "select * from users where name = ?{String}" ==>+ "QQuery Only GHC.Base.id $\"select * from users where name = ?\" :: forall r. QueryResults r => QQuery String r"++subst1b = "select * from users where name = ?" ==>+ "QQuery Only GHC.Base.id $\"select * from users where name = ?\" :: forall q r. (Param q, QueryResults r) => QQuery q r"++subst3 = "select id{Maybe Int} from users where name = ?{Int} and name2 = ?{Bool} and name3 = ?{Maybe Double}" ==>+ "QQuery GHC.Base.id fromOnly $\"select id from users where name = ? and name2 = ? and name3 = ?\" :: QQuery (Int, Bool, Maybe Double) (Maybe Int)"++subst3b = "select * from users where name = ? and name2 = ? and name3 = ?" ==>+ "QQuery GHC.Base.id GHC.Base.id $\"select * from users where name = ? and name2 = ? and name3 = ?\" :: forall q q1 q2 r. (Param q, Param q1, Param q2, QueryResults r) => QQuery (q, q1, q2) r"+++escaped = "select \\{curled} from whatever where text = 'hello\\?'" ==>+ "QQuery_ GHC.Base.id $\"select {curled} from whatever where text = 'hello?'\" :: forall r. QueryResults r => QQuery_ r"++main :: IO ()+main = do passed <- and <$> sequence [simple, ret1, ret2, ret4, escaped, subst1, subst1b,+ subst3, subst3b]+ exitWith $ if passed then ExitSuccess else ExitFailure 1
+ LICENSE view
@@ -0,0 +1,31 @@+mysql-simple-quasi library+Copyright (c) 2013, Neil Brown++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Neil Brown nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mysql-simple-quasi.cabal view
@@ -0,0 +1,27 @@+-- Initial mysql-simple-qq.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/++name: mysql-simple-quasi+version: 1.0.0.0+synopsis: Quasi-quoter for use with mysql-simple.+description: See the "Database.MySQL.Simple.Quasi" module documentation for more details.+license: BSD3+license-file: LICENSE+author: Neil Brown+maintainer: neil@twistedsquare.com+-- copyright: +category: Database+build-type: Simple+cabal-version: >=1.8++library+ exposed-modules: Database.MySQL.Simple.Quasi+ build-depends: base == 4.*,+ haskell-src-meta, + mysql-simple == 0.2.*,+ template-haskell++Test-Suite test-qq+ type: exitcode-stdio-1.0+ main-is: Database/MySQL/Simple/Quasi/Test.hs+ build-depends: base, Cabal >= 1.18, haskell-src-meta, mysql-simple == 0.2.*, syb, template-haskell