diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,70 @@
+## 0.4.9
+
+* Additional tuple instances of QueryParams (https://github.com/paul-rouse/mysql-simple/pull/57)
+
+## 0.4.8.1
+
+* Allow build on ghc 8.4.x (https://github.com/paul-rouse/mysql-simple/pull/56)
+
+## 0.4.8
+
+* Provide classes to simplify user-defined encoding and decoding of columns.
+
+## 0.4.7.2
+
+* Fix question marks in string literals causing substitution errors (https://github.com/paul-rouse/mysql-simple/pull/54)
+
+## 0.4.7.1
+
+* Use parseTimeM from Data.Time.Format instead of parseTime (https://github.com/paul-rouse/mysql-simple/pull/53)
+
+## 0.4.7
+
+*  Allow JSON fields to be read in as ByteStrings (https://github.com/paul-rouse/mysql-simple/pull/52)
+
+## 0.4.6
+
+* Support GHC-9 via blaze-textual shim (https://github.com/paul-rouse/mysql-simple/pull/51).
+
+## 0.4.5
+
+* Add Semigroup instance for Query
+* Add a `(Param a) => Param (In (Set a))` instance.
+* Drop testing under GHC 7.8 / lts-2
+
+## 0.4.4
+
+* Report table name and database in the UnexpectedNull case of ResultError, using the previously empty errMessage field
+
+## 0.4.3
+
+* Use Data.Time.Format to parse TimeOfDay results properly (missing in https://github.com/paul-rouse/mysql-simple/pull/37).
+
+## 0.4.2.0
+
+* Handle fractional seconds in parameter substitution (without affecting cases where only whole seconds are used).  https://github.com/paul-rouse/mysql-simple/pull/40
+
+## 0.4.1.0
+
+* Add missing tuple instances of QueryResults (https://github.com/paul-rouse/mysql-simple/issues/9)
+* Fix `error "foo"` left in code (https://github.com/paul-rouse/mysql-simple/issues/39)
+* Allow fractional seconds in to be parsed in date/time results https://github.com/paul-rouse/mysql-simple/pull/37
+
+## 0.4.0.1
+
+* Fix https://github.com/paul-rouse/mysql-simple/issues/35
+
+## 0.4.0.0
+
+* Add type VaArgs for non-parenthesized variable-length list of parameters.
+* Include fieldName in other constructors of ResultError.
+
+## 0.3.0.0
+
+* New maintainer and GitHub location - with many thanks to Bryan O'Sullivan for all of the past work on this module, and for facilitating the transfer of maintenance responsibility.
+* Extra instances of QueryParams for larger tuples.
+* Include fieldName in ResultError in the case of the Incompatible constructor.
+
+The changes to ResultError and QueryParams were previously unreleased,
+although present in the GitHub repository.  They are the reason for the
+major version bump.
diff --git a/Database/MySQL/Internal/Blaze.hs b/Database/MySQL/Internal/Blaze.hs
new file mode 100644
--- /dev/null
+++ b/Database/MySQL/Internal/Blaze.hs
@@ -0,0 +1,275 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns, CPP, MagicHash, OverloadedStrings, UnboxedTuples #-}
+
+-- | This module is designed to provide a shim for @blaze-textual@.
+-- @blaze-textual@ does not support GHC 9. A PR has been opened to add that
+-- support for GHC 9 here: https://github.com/bos/blaze-textual/pull/14
+--
+-- When GHC 9 support is merged in, we can delete the CPP in this and
+-- re-export the blaze functions directly, which is what we do for older
+-- versions of base.
+module Database.MySQL.Internal.Blaze
+    ( integral
+    , double
+    , float
+    ) where
+
+#if MIN_VERSION_base(4,15,0)
+
+#define PAIR(a,b) (# a,b #)
+
+import Blaze.ByteString.Builder (Builder, fromByteString)
+import Blaze.ByteString.Builder.Char8 (fromChar)
+import Data.ByteString.Char8 ()
+import Data.Monoid (mappend, mconcat, mempty)
+import qualified Data.Vector as V
+
+import Blaze.ByteString.Builder
+import Blaze.ByteString.Builder.Char8
+import Data.ByteString.Char8 ()
+import Data.Int (Int8, Int16, Int32, Int64)
+import Data.Monoid (mappend, mempty)
+import Data.Word (Word, Word8, Word16, Word32, Word64)
+import GHC.Base (quotInt, remInt)
+import GHC.Num (quotRemInteger)
+-- import GHC.Types (Int(..))
+
+#if defined(INTEGER_GMP)
+import GHC.Integer.GMP.Internals
+#elif defined(INTEGER_SIMPLE)
+import GHC.Integer.Simple.Internals
+#endif
+
+minus :: Builder
+minus = fromWord8 45
+data TInt = TInt !Integer !Int
+putH :: [Integer] -> Builder
+putH (n:ns) = case n `quotRemInteger` maxInt of
+                PAIR(x,y)
+                    | q > 0     -> int q `mappend` pblock r `mappend` putB ns
+                    | otherwise -> int r `mappend` putB ns
+                    where q = fromInteger x
+                          r = fromInteger y
+putH _ = error "putH: the impossible happened"
+int :: Int -> Builder
+int = integral
+{-# INLINE int #-}
+fstT :: TInt -> Integer
+fstT (TInt a _) = a
+maxInt :: Integer
+maxDigits :: Int
+TInt maxInt maxDigits =
+    until ((>mi) . (*10) . fstT) (\(TInt n d) -> TInt (n*10) (d+1)) (TInt 10 1)
+  where mi = fromIntegral (maxBound :: Int)
+integral :: (Integral a, Show a) => a -> Builder
+{-# RULES "integral/Int" integral = bounded :: Int -> Builder #-}
+{-# RULES "integral/Int8" integral = bounded :: Int8 -> Builder #-}
+{-# RULES "integral/Int16" integral = bounded :: Int16 -> Builder #-}
+{-# RULES "integral/Int32" integral = bounded :: Int32 -> Builder #-}
+{-# RULES "integral/Int64" integral = bounded :: Int64 -> Builder #-}
+{-# RULES "integral/Word" integral = nonNegative :: Word -> Builder #-}
+{-# RULES "integral/Word8" integral = nonNegative :: Word8 -> Builder #-}
+{-# RULES "integral/Word16" integral = nonNegative :: Word16 -> Builder #-}
+{-# RULES "integral/Word32" integral = nonNegative :: Word32 -> Builder #-}
+{-# RULES "integral/Word64" integral = nonNegative :: Word64 -> Builder #-}
+{-# RULES "integral/Integer" integral = integer :: Integer -> Builder #-}
+
+-- This definition of the function is here PURELY to be used by ghci
+-- and those rare cases where GHC is being invoked without
+-- optimization, as otherwise the rewrite rules above should fire. The
+-- test for "-0" catches an overflow if we render minBound.
+integral i
+    | i >= 0                 = nonNegative i
+    | toByteString b == "-0" = fromString (show i)
+    | otherwise              = b
+  where b = minus `mappend` nonNegative (-i)
+
+{-# NOINLINE integral #-}
+
+pblock :: Int -> Builder
+pblock = go maxDigits
+  where
+    go !d !n
+        | d == 1    = digit n
+        | otherwise = go (d-1) q `mappend` digit r
+        where q = n `quotInt` 10
+              r = n `remInt` 10
+
+putB :: [Integer] -> Builder
+putB (n:ns) = case n `quotRemInteger` maxInt of
+                PAIR(x,y) -> pblock q `mappend` pblock r `mappend` putB ns
+                    where q = fromInteger x
+                          r = fromInteger y
+putB _ = mempty
+
+bounded :: (Bounded a, Integral a) => a -> Builder
+{-# SPECIALIZE bounded :: Int -> Builder #-}
+{-# SPECIALIZE bounded :: Int8 -> Builder #-}
+{-# SPECIALIZE bounded :: Int16 -> Builder #-}
+{-# SPECIALIZE bounded :: Int32 -> Builder #-}
+{-# SPECIALIZE bounded :: Int64 -> Builder #-}
+bounded i
+    | i >= 0        = nonNegative i
+    | i > minBound  = minus `mappend` nonNegative (-i)
+    | otherwise     = minus `mappend`
+                      nonNegative (negate (k `quot` 10)) `mappend`
+                      digit (negate (k `rem` 10))
+  where k = minBound `asTypeOf` i
+
+nonNegative :: Integral a => a -> Builder
+{-# SPECIALIZE nonNegative :: Int -> Builder #-}
+{-# SPECIALIZE nonNegative :: Int8 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Int16 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Int32 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Int64 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Word -> Builder #-}
+{-# SPECIALIZE nonNegative :: Word8 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Word16 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Word32 -> Builder #-}
+{-# SPECIALIZE nonNegative :: Word64 -> Builder #-}
+nonNegative = go
+  where
+    go n | n < 10    = digit n
+         | otherwise = go (n `quot` 10) `mappend` digit (n `rem` 10)
+
+digit :: Integral a => a -> Builder
+digit n = fromWord8 $! fromIntegral n + 48
+{-# INLINE digit #-}
+
+integer :: Integer -> Builder
+#if defined(INTEGER_GMP)
+integer (S# i#) = int (I# i#)
+#endif
+integer i
+    | i < 0     = minus `mappend` go (-i)
+    | otherwise = go i
+  where
+    go n | n < maxInt = int (fromInteger n)
+         | otherwise  = putH (splitf (maxInt * maxInt) n)
+
+    splitf p n
+      | p > n       = [n]
+      | otherwise   = splith p (splitf (p*p) n)
+
+    splith p (n:ns) = case n `quotRemInteger` p of
+                        PAIR(q,r) | q > 0     -> q : r : splitb p ns
+                                  | otherwise -> r : splitb p ns
+    splith _ _      = error "splith: the impossible happened."
+
+    splitb p (n:ns) = case n `quotRemInteger` p of
+                        PAIR(q,r) -> q : r : splitb p ns
+    splitb _ _      = []
+
+
+-- The code below is originally from GHC.Float, but has been optimised
+-- in quite a few ways.
+
+data T = T [Int] {-# UNPACK #-} !Int
+
+float :: Float -> Builder
+float = double . realToFrac
+
+double :: Double -> Builder
+double f
+    | isInfinite f              = fromByteString $
+                                  if f > 0 then "Infinity" else "-Infinity"
+    | f < 0 || isNegativeZero f = minus `mappend` goGeneric (floatToDigits (-f))
+    | f >= 0                    = goGeneric (floatToDigits f)
+    | otherwise                 = fromByteString "NaN"
+  where
+   goGeneric p@(T _ e)
+     | e < 0 || e > 7 = goExponent p
+     | otherwise      = goFixed    p
+   goExponent (T is e) =
+       case is of
+         []     -> error "putFormattedFloat"
+         [0]    -> fromByteString "0.0e0"
+         [d]    -> digit d `mappend` fromByteString ".0e" `mappend` integral (e-1)
+         (d:ds) -> digit d `mappend` fromChar '.' `mappend` digits ds `mappend`
+                   fromChar 'e' `mappend` integral (e-1)
+   goFixed (T is e)
+       | e <= 0    = fromChar '0' `mappend` fromChar '.' `mappend`
+                     mconcat (replicate (-e) (fromChar '0')) `mappend`
+                     digits is
+       | otherwise = let g 0 rs     = fromChar '.' `mappend` mk0 rs
+                         g n []     = fromChar '0' `mappend` g (n-1) []
+                         g n (r:rs) = digit r `mappend` g (n-1) rs
+                     in g e is
+   mk0 [] = fromChar '0'
+   mk0 rs = digits rs
+
+digits :: [Int] -> Builder
+digits (d:ds) = digit d `mappend` digits ds
+digits _      = mempty
+{-# INLINE digits #-}
+
+floatToDigits :: Double -> T
+floatToDigits 0 = T [0] 0
+floatToDigits x = T (reverse rds) k
+ where
+  (f0, e0)     = decodeFloat x
+  (minExp0, _) = floatRange (undefined::Double)
+  p = floatDigits x
+  b = floatRadix x
+  minExp = minExp0 - p -- the real minimum exponent
+  -- Haskell requires that f be adjusted so denormalized numbers
+  -- will have an impossibly low exponent.  Adjust for this.
+  (# f, e #) =
+   let n = minExp - e0 in
+   if n > 0 then (# f0 `div` (b^n), e0+n #) else (# f0, e0 #)
+  (# r, s, mUp, mDn #) =
+   if e >= 0
+   then let be = b^ e
+        in if f == b^(p-1)
+           then (# f*be*b*2, 2*b, be*b, b #)
+           else (# f*be*2, 2, be, be #)
+   else if e > minExp && f == b^(p-1)
+        then (# f*b*2, b^(-e+1)*2, b, 1 #)
+        else (# f*2, b^(-e)*2, 1, 1 #)
+  k = fixup k0
+   where
+    k0 | b == 2 = (p - 1 + e0) * 3 `div` 10
+        -- logBase 10 2 is slightly bigger than 3/10 so the following
+        -- will err on the low side.  Ignoring the fraction will make
+        -- it err even more.  Haskell promises that p-1 <= logBase b f
+        -- < p.
+       | otherwise = ceiling ((log (fromInteger (f+1) :: Double) +
+                               fromIntegral e * log (fromInteger b)) / log 10)
+    fixup n
+      | n >= 0    = if r + mUp <= exp10 n * s then n else fixup (n+1)
+      | otherwise = if exp10 (-n) * (r + mUp) <= s then n else fixup (n+1)
+
+  gen ds !rn !sN !mUpN !mDnN =
+   let (dn0, rn') = (rn * 10) `divMod` sN
+       mUpN' = mUpN * 10
+       mDnN' = mDnN * 10
+       !dn   = fromInteger dn0
+       !dn'  = dn + 1
+   in case (# rn' < mDnN', rn' + mUpN' > sN #) of
+        (# True,  False #) -> dn : ds
+        (# False, True #)  -> dn' : ds
+        (# True,  True #)  -> if rn' * 2 < sN then dn : ds else dn' : ds
+        (# False, False #) -> gen (dn:ds) rn' sN mUpN' mDnN'
+
+  rds | k >= 0    = gen [] r (s * exp10 k) mUp mDn
+      | otherwise = gen [] (r * bk) s (mUp * bk) (mDn * bk)
+      where bk = exp10 (-k)
+
+exp10 :: Int -> Integer
+exp10 n
+    | n >= 0 && n < maxExpt = V.unsafeIndex expts n
+    | otherwise             = 10 ^ n
+  where expts = V.generate maxExpt (10^)
+        {-# NOINLINE expts #-}
+        maxExpt = 17
+{-# INLINE exp10 #-}
+
+
+
+
+#else
+
+import Blaze.Text (integral, double, float)
+
+#endif
+
diff --git a/Database/MySQL/Simple.hs b/Database/MySQL/Simple.hs
--- a/Database/MySQL/Simple.hs
+++ b/Database/MySQL/Simple.hs
@@ -4,7 +4,7 @@
 -- Module:      Database.MySQL.Simple
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
@@ -48,8 +48,11 @@
     , Connection
     , Query
     , In(..)
+    , VaArgs(..)
     , Binary(..)
     , Only(..)
+    , Param
+    , Result
     -- ** Exceptions
     , FormatError(fmtMessage, fmtQuery, fmtParams)
     , QueryError(qeMessage, qeQuery)
@@ -79,6 +82,15 @@
     -- * Helper functions
     , formatMany
     , formatQuery
+    , splitQuery
+
+    -- | #extension#
+
+    -- * Extension hooks
+    -- $hooks
+
+    , FromField(..)
+    , ToField(..)
     ) where
 
 import Blaze.ByteString.Builder (Builder, fromByteString, toByteString)
@@ -87,17 +99,19 @@
 import Control.Exception (Exception, bracket, onException, throw, throwIO)
 import Control.Monad.Fix (fix)
 import Data.ByteString (ByteString)
+import qualified Data.ByteString.Char8 as BS
 import Data.Int (Int64)
 import Data.List (intersperse)
 import Data.Monoid (mappend, mconcat)
 import Data.Typeable (Typeable)
-import Database.MySQL.Base (Connection, Result)
+import Database.MySQL.Base (Connection)
+import qualified Database.MySQL.Base as Base (Result)
 import Database.MySQL.Base.Types (Field)
-import Database.MySQL.Simple.Param (Action(..), inQuotes)
+import Database.MySQL.Simple.Param (ToField(..), Param, Action(..), inQuotes)
 import Database.MySQL.Simple.QueryParams (QueryParams(..))
 import Database.MySQL.Simple.QueryResults (QueryResults(..))
-import Database.MySQL.Simple.Result (ResultError(..))
-import Database.MySQL.Simple.Types (Binary(..), In(..), Only(..), Query(..))
+import Database.MySQL.Simple.Result (FromField(..), Result, ResultError(..))
+import Database.MySQL.Simple.Types (Binary(..), In(..), VaArgs(..), Only(..), Query(..))
 import Text.Regex.PCRE.Light (compile, caseless, match)
 import qualified Data.ByteString.Char8 as B
 import qualified Database.MySQL.Base as Base
@@ -160,7 +174,7 @@
       return . toByteString . mconcat $ fromByteString before :
                                         intersperse (fromChar ',') bs ++
                                         [fromByteString after]
-    _ -> error "foo"
+    _ -> fmtError "incorrect parameter syntax in query" q []
   where
    re = compile "^([^?]+\\bvalues\\s*)\
                  \(\\(\\s*[?](?:\\s*,\\s*[?])*\\s*\\))\
@@ -168,18 +182,47 @@
         [caseless]
 
 buildQuery :: Connection -> Query -> ByteString -> [Action] -> IO Builder
-buildQuery conn q template xs = zipParams (split template) <$> mapM sub xs
+buildQuery conn q template xs = zipParams queryFragments <$> mapM sub xs
   where sub (Plain b)  = pure b
         sub (Escape s) = (inQuotes . fromByteString) <$> Base.escape conn s
         sub (Many ys)  = mconcat <$> mapM sub ys
-        split s = fromByteString h : if B.null t then [] else split (B.tail t)
-            where (h,t) = B.break (=='?') s
         zipParams (t:ts) (p:ps) = t `mappend` p `mappend` zipParams ts ps
         zipParams [t] []        = t
-        zipParams _ _ = fmtError (show (B.count '?' template) ++
+        zipParams _ _ = fmtError (show fragmentCount ++
                                   " '?' characters, but " ++
                                   show (length xs) ++ " parameters") q xs
+        fragmentCount = length queryFragments - 1
+        queryFragments = splitQuery template
 
+-- | Split a query into fragments separated by @?@ characters. Does not
+-- break a fragment if the question mark is in a string literal.
+splitQuery :: ByteString -> [Builder]
+splitQuery s =
+  reverse $ fmap (fromByteString . BS.pack . reverse) $
+    begin [] (BS.unpack s)
+  where
+  begin = normal []
+
+  normal ret acc [] =
+    acc : ret
+  normal ret acc (c : cs) =
+    case c of
+      '?' ->
+        normal (acc : ret) [] cs
+      '\'' ->
+        quotes ret (c : acc) cs
+      _ ->
+        normal ret (c : acc) cs
+
+  quotes ret acc [] =
+    acc : ret
+  quotes ret acc (c : cs) =
+    case c of
+      '\'' ->
+        normal ret (c : acc) cs
+      _ ->
+        quotes ret (c : acc) cs
+
 -- | Execute an @INSERT@, @UPDATE@, or other SQL query that is not
 -- expected to return results.
 --
@@ -326,7 +369,7 @@
       [] -> return z
       _  -> (f z $! convertResults fs row) >>= loop
 
-withResult :: (IO Result) -> Query -> (Result -> [Field] -> IO a) -> IO a
+withResult :: (IO Base.Result) -> Query -> (Base.Result -> [Field] -> IO a) -> IO a
 withResult fetchResult q act = bracket fetchResult Base.freeResult $ \r -> do
   ncols <- Base.fieldCount (Right r)
   if ncols == 0
@@ -372,7 +415,7 @@
 -- facility to address both ease of use and security.
 
 -- $querytype
--- 
+--
 -- A 'Query' is a @newtype@-wrapped 'ByteString'. It intentionally
 -- exposes a tiny API that is not compatible with the 'ByteString'
 -- API; this makes it difficult to construct queries from fragments of
@@ -617,7 +660,7 @@
 --   For instance, you can always extract a MySQL @TINYINT@ column to
 --   a Haskell 'Int'.  The Haskell 'Float' type can accurately
 --   represent MySQL integer types of size up to @INT24@, so it is
---   considered compatble with those types.
+--   considered compatible with those types.
 --
 -- * A numeric compatibility check is based only on the type of a
 --   column, /not/ on its values. For instance, a MySQL @LONG_LONG@
@@ -631,3 +674,27 @@
 --   UTF-8. If you use some other encoding, decoding may fail or give
 --   wrong results. In such cases, write a @newtype@ wrapper and a
 --   custom 'Result' instance to handle your encoding.
+--
+-- When a user-defined type is represented by a @TEXT@, @BLOB@, @JSON@, or
+-- similar type of column, it can be encoded and decoded using
+-- hooks which take or receive a 'ByteString'.  See the classes 'ToField'
+-- and 'FromField' in the [Extension hooks](#extension) section below.
+
+-- $hooks
+--
+-- These classes provide a simple mechanism for encoding and decoding
+-- user-defined types in cases where the underlying encoding is a
+-- sequence of bytes.
+--
+-- === __Example__
+--
+-- Assuming @Foo@ has instances of 'Data.Aeson.FromJSON', 'Data.Aeson.ToJSON',
+-- and 'Typeable', its decoding and encoding can be specified like this:
+--
+-- > instance FromField Foo where
+-- >     fromField = ([Database.MySQL.Base.Types.Json], Data.Aeson.eitherDecodeStrict')
+-- > instance Result Foo
+-- >
+-- > instance ToField Foo where
+-- >     toField = Data.ByteString.Lazy.toStrict . Data.Aeson.encode
+-- > instance Param Foo
diff --git a/Database/MySQL/Simple/Param.hs b/Database/MySQL/Simple/Param.hs
--- a/Database/MySQL/Simple/Param.hs
+++ b/Database/MySQL/Simple/Param.hs
@@ -1,19 +1,19 @@
 {-# LANGUAGE CPP, DeriveDataTypeable, DeriveFunctor, FlexibleInstances,
-    OverloadedStrings #-}
+    OverloadedStrings, DefaultSignatures #-}
 
 -- |
 -- Module:      Database.MySQL.Simple.Param
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
 -- The 'Param' typeclass, for rendering a parameter to a SQL query.
 
 module Database.MySQL.Simple.Param
-    (
-      Action(..)
+    ( Action(..)
+    , ToField(..)
     , Param(..)
     , inQuotes
     ) where
@@ -21,20 +21,21 @@
 import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString,
                                  toByteString)
 import Blaze.ByteString.Builder.Char8 (fromChar)
-import Blaze.Text (integral, double, float)
 import Data.ByteString (ByteString)
 import qualified Data.ByteString.Base16 as B16
 import qualified Data.ByteString.Base16.Lazy as L16
 import Data.Int (Int8, Int16, Int32, Int64)
 import Data.List (intersperse)
 import Data.Monoid (mappend)
+import Data.Set (Set)
+import qualified Data.Set as Set
 import Data.Time.Calendar (Day, showGregorian)
 import Data.Time.Clock (UTCTime)
 import Data.Time.Format (formatTime)
 import Data.Time.LocalTime (TimeOfDay)
 import Data.Typeable (Typeable)
 import Data.Word (Word, Word8, Word16, Word32, Word64)
-import Database.MySQL.Simple.Types (Binary(..), In(..), Null)
+import Database.MySQL.Simple.Types (Binary(..), In(..), VaArgs(..), Null)
 import qualified Blaze.ByteString.Builder.Char.Utf8 as Utf8
 import qualified Data.ByteString as SB
 import qualified Data.ByteString.Lazy as LB
@@ -42,6 +43,8 @@
 import qualified Data.Text.Encoding as ST
 import qualified Data.Text.Lazy as LT
 
+import Database.MySQL.Internal.Blaze (integral, double, float)
+
 #if MIN_VERSION_time(1,5,0)
 import Data.Time.Format (defaultTimeLocale)
 #else
@@ -68,10 +71,28 @@
     show (Escape b) = "Escape " ++ show b
     show (Many b)   = "Many " ++ show b
 
+-- | A type that can be converted to a 'ByteString' for use as a parameter
+-- to an SQL query.
+--
+-- Any type which is an instance of this class can use the default
+-- implementation of 'Param', which will wrap encodings with 'Escape'.
+--
+-- @since 0.4.8
+--
+class ToField a where
+    toField :: a -> ByteString
+
 -- | A type that may be used as a single parameter to a SQL query.
+--
+-- A default implementation is provided for any type which is an instance
+-- of 'ToField', providing a simple mechanism for user-defined encoding
+-- to text- or blob-like fields (including @JSON@).
+--
 class Param a where
     render :: a -> Action
     -- ^ Prepare a value for substitution into a query string.
+    default render :: ToField a => a -> Action
+    render = Escape . toField
 
 instance Param Action where
     render a = a
@@ -89,6 +110,14 @@
         (intersperse (Plain (fromChar ',')) . map render $ xs) ++
         [Plain (fromChar ')')]
 
+instance (Param a) => Param (In (Set a)) where
+    render = render . fmap Set.toList
+
+instance (Param a) => Param (VaArgs [a]) where
+    render (VaArgs []) = Plain $ fromByteString "null"
+    render (VaArgs xs) = Many $
+        intersperse (Plain (fromChar ',')) . map render $ xs
+
 instance Param (Binary SB.ByteString) where
     render (Binary bs) = Plain $ fromByteString "x'" `mappend`
                                  fromByteString (B16.encode bs) `mappend`
@@ -185,7 +214,7 @@
     {-# INLINE render #-}
 
 instance Param UTCTime where
-    render = Plain . Utf8.fromString . formatTime defaultTimeLocale "'%F %T'"
+    render = Plain . Utf8.fromString . formatTime defaultTimeLocale "'%F %T%Q'"
     {-# INLINE render #-}
 
 instance Param Day where
diff --git a/Database/MySQL/Simple/QueryParams.hs b/Database/MySQL/Simple/QueryParams.hs
--- a/Database/MySQL/Simple/QueryParams.hs
+++ b/Database/MySQL/Simple/QueryParams.hs
@@ -2,7 +2,7 @@
 -- Module:      Database.MySQL.Simple.QueryParams
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
@@ -79,6 +79,130 @@
     renderParams (a,b,c,d,e,f,g,h,i,j) =
         [render a, render b, render c, render d, render e, render f, render g,
          render h, render i, render j]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s, Param t)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s, render t]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s, Param t, Param u)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s, render t, render u]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s, Param t, Param u,
+          Param v)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s, render t, render u,
+         render v]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s, Param t, Param u,
+          Param v, Param w)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s, render t, render u,
+         render v, render w]
+
+instance (Param a, Param b, Param c, Param d, Param e, Param f, Param g,
+          Param h, Param i, Param j, Param k, Param l, Param m, Param n,
+          Param o, Param p, Param q, Param r, Param s, Param t, Param u,
+          Param v, Param w, Param x)
+    => QueryParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x) where
+    renderParams (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x) =
+        [render a, render b, render c, render d, render e, render f, render g,
+         render h, render i, render j, render k, render l, render m, render n,
+         render o, render p, render q, render r, render s, render t, render u,
+         render v, render w, render x]
 
 instance (Param a) => QueryParams [a] where
     renderParams = map render
diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs
--- a/Database/MySQL/Simple/QueryResults.hs
+++ b/Database/MySQL/Simple/QueryResults.hs
@@ -4,7 +4,7 @@
 -- Module:      Database.MySQL.Simpe.QueryResults
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
@@ -23,7 +23,7 @@
 import Control.Exception (throw)
 import Data.ByteString (ByteString)
 import qualified Data.ByteString.Char8 as B
-import Database.MySQL.Base.Types (Field(fieldType))
+import Database.MySQL.Base.Types (Field(fieldType, fieldName))
 import Database.MySQL.Simple.Result (ResultError(..), Result(..))
 import Database.MySQL.Simple.Types (Only(..))
 
@@ -219,6 +219,166 @@
               !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
     convertResults fs vs  = convertError fs vs 15
 
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp] =
+        (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp;
+    convertResults fs vs  = convertError fs vs 16
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq] =
+        (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq;
+    convertResults fs vs  = convertError fs vs 17
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr] =
+        (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+    convertResults fs vs  = convertError fs vs 18
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs;
+    convertResults fs_ vs_  = convertError fs_ vs_ 19
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s, Result t) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs,
+                    ft]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs,
+                    vt]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs; !t = convert ft vt
+    convertResults fs_ vs_  = convertError fs_ vs_ 20
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s, Result t, Result u) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs,
+                    ft,fu]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs,
+                    vt,vu]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs; !t = convert ft vt; !u = convert fu vu
+    convertResults fs_ vs_  = convertError fs_ vs_ 21
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s, Result t, Result u, Result v) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs,
+                    ft,fu,fv]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs,
+                    vt,vu,vv]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs; !t = convert ft vt; !u = convert fu vu
+              !v = convert fv vv;
+    convertResults fs_ vs_  = convertError fs_ vs_ 22
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s, Result t, Result u, Result v, Result w) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs,
+                    ft,fu,fv,fw]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs,
+                    vt,vu,vv,vw]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs; !t = convert ft vt; !u = convert fu vu
+              !v = convert fv vv; !w = convert fw vw;
+    convertResults fs_ vs_  = convertError fs_ vs_ 23
+
+instance (Result a, Result b, Result c, Result d, Result e, Result f,
+          Result g, Result h, Result i, Result j, Result k, Result l,
+          Result m, Result n, Result o, Result p, Result q, Result r,
+          Result s, Result t, Result u, Result v, Result w, Result x) =>
+    QueryResults (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x) where
+    convertResults [fa,fb,fc,fd,fe,ff,fg,fh,fi,fj,fk,fl,fm,fn,fo,fp,fq,fr,fs,
+                    ft,fu,fv,fw,fx]
+                   [va,vb,vc,vd,ve,vf,vg,vh,vi,vj,vk,vl,vm,vn,vo,vp,vq,vr,vs,
+                    vt,vu,vv,vw,vx]
+        = (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x)
+        where !a = convert fa va; !b = convert fb vb; !c = convert fc vc
+              !d = convert fd vd; !e = convert fe ve; !f = convert ff vf
+              !g = convert fg vg; !h = convert fh vh; !i = convert fi vi
+              !j = convert fj vj; !k = convert fk vk; !l = convert fl vl
+              !m = convert fm vm; !n = convert fn vn; !o = convert fo vo
+              !p = convert fp vp; !q = convert fq vq; !r = convert fr vr
+              !s = convert fs vs; !t = convert ft vt; !u = convert fu vu
+              !v = convert fv vv; !w = convert fw vw; !x = convert fx vx;
+    convertResults fs_ vs_  = convertError fs_ vs_ 24
+
 -- | Throw a 'ConversionFailed' exception, indicating a mismatch
 -- between the number of columns in the 'Field' and row, and the
 -- number in the collection to be converted to.
@@ -235,6 +395,7 @@
     (show (length fs) ++ " values: " ++ show (zip (map fieldType fs)
                                                   (map (fmap ellipsis) vs)))
     (show n ++ " slots in target type")
+    (show (map (B.unpack . fieldName) fs))
     "mismatch between number of columns to convert and number in target type"
 
 ellipsis :: ByteString -> ByteString
diff --git a/Database/MySQL/Simple/Result.hs b/Database/MySQL/Simple/Result.hs
--- a/Database/MySQL/Simple/Result.hs
+++ b/Database/MySQL/Simple/Result.hs
@@ -1,10 +1,14 @@
-{-# LANGUAGE CPP, DeriveDataTypeable, FlexibleInstances #-}
+{-# LANGUAGE CPP, DeriveDataTypeable, FlexibleInstances, OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving, DefaultSignatures #-}
+#if MIN_VERSION_time(1,5,0)
+{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
+#endif
 
 -- |
--- Module:      Database.MySQL.Simpe.QueryResults
+-- Module:      Database.MySQL.Simple.Result
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
@@ -19,24 +23,28 @@
 -- two are /not/ considered compatible.
 
 module Database.MySQL.Simple.Result
-    (
-      Result(..)
+    ( FromField(..)
+    , Result(..)
     , ResultError(..)
     ) where
 
 #include "MachDeps.h"
 
+#if !MIN_VERSION_base(4,13,0)
+import Control.Monad.Fail (MonadFail(..))
+#endif
+
 import Control.Applicative ((<$>), (<*>), (<*), pure)
 import Control.Exception (Exception, throw)
-import Data.Attoparsec.Char8 hiding (Result)
+import Data.Attoparsec.ByteString.Char8 hiding (Result)
 import Data.Bits ((.&.), (.|.), shiftL)
 import Data.ByteString (ByteString)
 import Data.Int (Int8, Int16, Int32, Int64)
 import Data.List (foldl')
 import Data.Ratio (Ratio)
 import Data.Time.Calendar (Day, fromGregorian)
-import Data.Time.Clock (UTCTime)
-import Data.Time.Format (parseTime)
+import Data.Time.Clock (UTCTime(..))
+import Data.Time.Format (parseTimeM, ParseTime)
 import Data.Time.LocalTime (TimeOfDay, makeTimeOfDayValid)
 import Data.Typeable (TypeRep, Typeable, typeOf)
 import Data.Word (Word, Word8, Word16, Word32, Word64)
@@ -58,15 +66,18 @@
 -- value fails.
 data ResultError = Incompatible { errSQLType :: String
                                 , errHaskellType :: String
+                                , errFieldName :: String
                                 , errMessage :: String }
                  -- ^ The SQL and Haskell types are not compatible.
                  | UnexpectedNull { errSQLType :: String
                                   , errHaskellType :: String
+                                  , errFieldName :: String
                                   , errMessage :: String }
                  -- ^ A SQL @NULL@ was encountered when the Haskell
                  -- type did not permit it.
                  | ConversionFailed { errSQLType :: String
                                     , errHaskellType :: String
+                                    , errFieldName :: String
                                     , errMessage :: String }
                  -- ^ The SQL value could not be parsed, or could not
                  -- be represented as a valid Haskell value, or an
@@ -76,13 +87,44 @@
 
 instance Exception ResultError
 
+-- | A type that can be converted from a 'ByteString'.  Any type which is
+-- an instance of this class, and is 'Typeable', can use the default
+-- implementation of 'Result'.  This provides a method of implementing
+-- a decoder for any text-like column, such as @TEXT@, @BLOB@, or @JSON@,
+-- instead of implementing 'Result' directly.
+--
+-- The first component of the tuple returned by 'fromField' is a list of
+-- acceptable column types, expressed in terms of
+-- 'Database.MySQL.Base.Types.Type'.
+--
+-- @since 0.4.8
+--
+class FromField a where
+    fromField :: ([Type], ByteString -> Either String a)
+
 -- | A type that may be converted from a SQL type.
+--
+-- A default implementation is provided for any type which is an instance
+-- of both 'FromField' and 'Typeable', providing a simple mechanism for
+-- user-defined decoding from text- or blob-like fields (including @JSON@).
+--
 class Result a where
     convert :: Field -> Maybe ByteString -> a
     -- ^ Convert a SQL value to a Haskell value.
     --
     -- Throws a 'ResultError' if conversion fails.
+    --
+    default convert :: (Typeable a, FromField a)
+                       => Field -> Maybe ByteString -> a
+    convert f =
+        doConvert f (mkCompats allowTypes) $ \bs ->
+            case cvt bs of
+              Right x  -> x
+              Left err -> conversionFailed f (show (typeOf (cvt undefined))) err
+        where
+            (allowTypes, cvt) = fromField
 
+
 instance (Result a) => Result (Maybe a) where
     convert _ Nothing = Nothing
     convert f bs      = Just (convert f bs)
@@ -154,12 +196,16 @@
 instance Result [Char] where
     convert f = ST.unpack . convert f
 
-instance Result UTCTime where
-    convert f = doConvert f ok $ \bs ->
-                case parseTime defaultTimeLocale "%F %T" (B8.unpack bs) of
-                  Just t -> t
-                  Nothing -> conversionFailed f "UTCTime" "could not parse"
-        where ok = mkCompats [DateTime,Timestamp]
+instance FromField UTCTime where
+    fromField =
+        ( [DateTime, Timestamp]
+        , \bs -> if "0000-00-00" `SB.isPrefixOf` bs then
+                     -- https://dev.mysql.com/doc/refman/8.0/en/datetime.html
+                     Right $ UTCTime (fromGregorian 0 0 0) 0
+                 else
+                     parseTimeField "%F %T%Q" (B8.unpack bs)
+        )
+instance Result UTCTime
 
 instance Result Day where
     convert f = flip (atto ok) f $ case fieldType f of
@@ -171,21 +217,27 @@
                                    <*> (decimal <* char '-')
                                    <*> decimal
 
-instance Result TimeOfDay where
-    convert f = flip (atto ok) f $ do
-                hours <- decimal <* char ':'
-                mins <- decimal <* char ':'
-                secs <- decimal :: Parser Int
-                case makeTimeOfDayValid hours mins (fromIntegral secs) of
-                  Just t -> return t
-                  _      -> conversionFailed f "TimeOfDay" "could not parse"
-        where ok = mkCompats [Time]
+instance FromField TimeOfDay where
+    fromField = ([Time], parseTimeField "%T%Q" . B8.unpack)
+instance Result TimeOfDay
 
+-- A specialised version of parseTimeM which builds in the defaults we want,
+-- and produces an Either result.  For the latter provide a wrapper for Either,
+-- local to this module, to add a MonadFail instance.
+--
+newtype Failable a = Failable { failable :: Either String a }
+                     deriving (Functor, Applicative, Monad)
+instance MonadFail Failable where
+    fail err = Failable (Left err)
+
+parseTimeField :: ParseTime t => String -> String -> Either String t
+parseTimeField fmt s = failable $ parseTimeM True defaultTimeLocale fmt s
+
 isText :: Field -> Bool
 isText f = fieldCharSet f /= 63
 
 newtype Compat = Compat Word32
-    
+
 mkCompats :: [Type] -> Compat
 mkCompats = foldl' f (Compat 0) . map mkCompat
   where f (Compat a) (Compat b) = Compat (a .|. b)
@@ -198,7 +250,7 @@
 
 okText, ok8, ok16, ok32, ok64, okWord :: Compat
 okText = mkCompats [VarChar,TinyBlob,MediumBlob,LongBlob,Blob,VarString,String,
-                    Set,Enum]
+                    Set,Enum,Json]
 ok8 = mkCompats [Tiny]
 ok16 = mkCompats [Tiny,Short]
 ok32 = mkCompats [Tiny,Short,Int24,Long]
@@ -215,13 +267,22 @@
     | mkCompat (fieldType f) `compat` types = cvt bs
     | otherwise = incompatible f (typeOf (cvt undefined)) "types incompatible"
 doConvert f _ cvt _ = throw $ UnexpectedNull (show (fieldType f))
-                              (show (typeOf (cvt undefined))) ""
+                              (show (typeOf (cvt undefined)))
+                              (B8.unpack (fieldName f))
+                              ("unexpected null in table "
+                               ++ B8.unpack (fieldTable f)
+                               ++ " of database "
+                               ++ B8.unpack (fieldDB f)
+                              )
 
 incompatible :: Field -> TypeRep -> String -> a
-incompatible f r = throw . Incompatible (show (fieldType f)) (show r)
+incompatible f r = throw . Incompatible (show (fieldType f))
+                                        (show r)
+                                        (B8.unpack (fieldName f))
 
 conversionFailed :: Field -> String -> String -> a
 conversionFailed f s = throw . ConversionFailed (show (fieldType f)) s
+                                 (B8.unpack (fieldName f))
 
 atto :: (Typeable a) => Compat -> Parser a -> Field -> Maybe ByteString -> a
 atto types p0 f = doConvert f types $ go undefined p0
diff --git a/Database/MySQL/Simple/Types.hs b/Database/MySQL/Simple/Types.hs
--- a/Database/MySQL/Simple/Types.hs
+++ b/Database/MySQL/Simple/Types.hs
@@ -4,7 +4,7 @@
 -- Module:      Database.MySQL.Simple.Types
 -- Copyright:   (c) 2011 MailRank, Inc.
 -- License:     BSD3
--- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Maintainer:  Paul Rouse <pyr@doynton.org>
 -- Stability:   experimental
 -- Portability: portable
 --
@@ -15,6 +15,7 @@
       Null(..)
     , Only(..)
     , In(..)
+    , VaArgs(..)
     , Binary(..)
     , Query(..)
     ) where
@@ -22,6 +23,7 @@
 import Blaze.ByteString.Builder (toByteString)
 import Control.Arrow (first)
 import Data.ByteString (ByteString)
+import Data.Semigroup (Semigroup(..))
 import Data.Monoid (Monoid(..))
 import Data.String (IsString(..))
 import Data.Typeable (Typeable)
@@ -68,9 +70,13 @@
 instance IsString Query where
     fromString = Query . toByteString . Utf8.fromString
 
+instance Semigroup Query where
+    (<>) (Query a) (Query b) = Query (B.append a b)
+    {-# INLINE (<>) #-}
+
 instance Monoid Query where
     mempty = Query B.empty
-    mappend (Query a) (Query b) = Query (B.append a b)
+    mappend = (<>)
     {-# INLINE mappend #-}
 
 -- | A single-value \"collection\".
@@ -99,6 +105,18 @@
 -- > query c "select * from whatever where id in ?" (Only (In [3,4,5]))
 newtype In a = In a
     deriving (Eq, Ord, Read, Show, Typeable, Functor)
+
+-- | Wrap a list of values for use in a function with variable arguments.
+-- Replaces a single \"@?@\" character with a non-parenthesized list of
+-- rendered values.
+--
+-- Example:
+--
+-- > query conn
+-- >   "SELECT * FROM example_table ORDER BY field(f,?)"
+-- >   (Only (VaArgs [3,2,1]))
+newtype VaArgs a = VaArgs a
+  deriving (Eq, Ord, Read, Show, Typeable, Functor)
 
 -- | Wrap a mostly-binary string to be escaped in hexadecimal.
 newtype Binary a = Binary a
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -24,19 +24,14 @@
 and other improvements.
 
 Please report bugs via the
-[github issue tracker](http://github.com/bos/mysql-simple/issues).
-
-Master [git repository](http://github.com/bos/mysql-simple):
-
-* `git clone git://github.com/bos/mysql-simple.git`
-
-There's also a [Mercurial mirror](http://bitbucket.org/bos/mysql-simple):
+[github issue tracker](http://github.com/paul-rouse/mysql-simple/issues).
 
-* `hg clone http://bitbucket.org/bos/mysql-simple`
+Master [git repository](http://github.com/paul-rouse/mysql-simple):
 
-(You can create and contribute changes using either git or Mercurial.)
+* `git clone git://github.com/paul-rouse/mysql-simple.git`
 
 # Authors
 
-This library is written and maintained by Bryan O'Sullivan,
-<bos@serpentine.com>.
+This library was written by Bryan O'Sullivan, <bos@serpentine.com>,
+to whom all of the credit is due.
+It is now being maintained by Paul Rouse, <pyr@doynton.org>.
diff --git a/mysql-simple.cabal b/mysql-simple.cabal
--- a/mysql-simple.cabal
+++ b/mysql-simple.cabal
@@ -1,7 +1,7 @@
 name:           mysql-simple
-version:        0.2.2.5
-homepage:       https://github.com/bos/mysql-simple
-bug-reports:    https://github.com/bos/mysql-simple/issues
+version:        0.4.9
+homepage:       https://github.com/paul-rouse/mysql-simple
+bug-reports:    https://github.com/paul-rouse/mysql-simple/issues
 synopsis:       A mid-level MySQL client library.
 description:    
     A mid-level client library for the MySQL database, intended to be
@@ -17,13 +17,15 @@
 license:        BSD3
 license-file:   LICENSE
 author:         Bryan O'Sullivan <bos@serpentine.com>
-maintainer:     Bryan O'Sullivan <bos@serpentine.com>
+maintainer:     Paul Rouse <pyr@doynton.org>
 copyright:      2011 MailRank, Inc.
 category:       Database
 build-type:     Simple
-cabal-version:  >= 1.6
+cabal-version:  >= 1.10
 extra-source-files:
+    ChangeLog.md
     README.markdown
+    stack.yaml
 
 flag developer
   description: operate in developer mode
@@ -38,32 +40,61 @@
     Database.MySQL.Simple.QueryResults
     Database.MySQL.Simple.Result
     Database.MySQL.Simple.Types
+  other-modules:
+    Database.MySQL.Internal.Blaze
 
   build-depends:
-    attoparsec >= 0.8.5.3,
-    base < 5,
+    attoparsec >= 0.10.0.0,
+    base >= 4.9 && < 5,
     base16-bytestring,
     blaze-builder,
-    blaze-textual,
     bytestring >= 0.9,
-    mysql >= 0.1.1.1,
+    containers,
+    mysql >= 0.1.7,
     pcre-light,
     old-locale,
     text >= 0.11.0.2,
-    time
+    time >= 1.5
+  if !impl(ghc >= 8.0)
+    build-depends:
+      semigroups >= 0.11 && < 0.19
 
-  ghc-options: -Wall
-  if impl(ghc >= 6.8)
-    ghc-options: -fwarn-tabs
+  -- hack to support GHC 9 for blaze-textual. see Database.MySQL.Internal.Blaze
+  -- for reasoning
+  if !impl(ghc >= 9.0)
+    build-depends:
+      blaze-textual
+  else
+    build-depends:
+      vector
+
+  ghc-options: -Wall -fwarn-tabs
+  if impl(ghc >= 7.10)
+    ghc-options: -fno-warn-unused-imports
   if flag(developer)
     ghc-prof-options: -auto-all
     ghc-options: -Werror
     cpp-options: -DASSERTS
+  default-language:  Haskell2010
 
-source-repository head
-  type:     git
-  location: http://github.com/bos/mysql-simple
+test-suite test
+    type:            exitcode-stdio-1.0
+    main-is:         main.hs
+    hs-source-dirs:  test
+    other-modules:   Common
+                     CustomTypeSpec
+                     DateTimeSpec
+    ghc-options:     -Wall
+    default-language: Haskell2010
+    build-depends:   base                    >= 4 && < 5
+                   , bytestring
+                   , blaze-builder
+                   , hspec
+                   , mysql
+                   , mysql-simple
+                   , text
+                   , time
 
 source-repository head
-  type:     mercurial
-  location: http://bitbucket.org/bos/mysql-simple
+  type:     git
+  location: http://github.com/paul-rouse/mysql-simple
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,7 @@
+flags:
+  mysql-simple:
+    developer: false
+packages:
+- '.'
+extra-deps: []
+resolver: lts-19.8
diff --git a/test/Common.hs b/test/Common.hs
new file mode 100644
--- /dev/null
+++ b/test/Common.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE StandaloneDeriving #-}
+{-# options_ghc -fno-warn-orphans #-}
+
+module Common where
+
+import Data.ByteString.Builder as BS
+import Database.MySQL.Simple.Param
+
+-- Declare some (orphan) instances needed for test specs
+instance Show BS.Builder where
+  show = show . BS.toLazyByteString
+
+instance Eq BS.Builder where
+  a == b = BS.toLazyByteString a == BS.toLazyByteString b
+
+deriving instance Eq Action
diff --git a/test/CustomTypeSpec.hs b/test/CustomTypeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/CustomTypeSpec.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+module CustomTypeSpec (
+      customTypeUnit
+    , customTypeSpec
+) where
+
+import Control.Monad                (void)
+import qualified Data.ByteString.Char8 as B8
+import Database.MySQL.Simple
+import Database.MySQL.Simple.Param  (Action(..), render)
+import qualified Database.MySQL.Base as MySQL
+import Test.Hspec
+
+import Common                       ()
+
+-- A type which we encode/decode specially (if artificially!) to a Text column
+--
+data Message = English String | Latin String deriving (Eq, Show)
+
+instance ToField Message where
+    toField m = B8.pack ( case m of
+                            English s -> "English: " <> s
+                            Latin s   -> "Latin: " <> s
+                        )
+instance Param Message
+
+instance FromField Message where
+    fromField =
+      ( [MySQL.TinyBlob, MySQL.Blob, MySQL.MediumBlob, MySQL.LongBlob]
+      , \s -> if "English: " `B8.isPrefixOf` s then
+                  Right $ English $ B8.unpack $ B8.drop 9 s
+              else if "Latin: " `B8.isPrefixOf` s then
+                  Right $ Latin $ B8.unpack $ B8.drop 7 s
+              else Left $ "Can't decode " <> show s <> " as Message"
+      )
+instance Result Message
+
+
+customTypeUnit :: Spec
+customTypeUnit =
+  describe "Param for custom type" $ do
+    it "is correctly encoded" $ do
+      render (Latin "nuntium") `shouldBe` Escape "Latin: nuntium"
+
+
+customTypeSpec :: Connection -> Spec
+customTypeSpec conn =
+  beforeAll_
+    ( do  _ <- execute_ conn "drop table if exists custom"
+          _ <- execute_ conn "create table custom (i int, c text)"
+          void $ execute_ conn ( "insert into custom (i,c) values "
+                                 <> "(1,'English: a message'),"
+                                 <> "(2,'French: un message')"
+                               )
+    ) $ do
+      describe "reading a custom type" $ do
+        it "should accept a correctly formatted field" $ do
+          result <- query_ conn "select c from custom where i = 1"
+          result `shouldBe` [Only (English "a message")]
+        it "should reject an improperly formatted field" $ do
+          (query_ conn "select c from custom where i = 2" :: IO [Only Message])
+            `shouldThrow`
+              (\e -> errMessage e == "Can't decode \"French: un message\" as Message")
+      describe "writing a custom type" $ do
+        it "should work with parameter substitution" $ do
+          _ <- execute conn "insert into custom (i,c) values (?,?)"
+                            ((3::Int), Latin "nuntium")
+          result <- query_ conn "select c from custom where i = 3"
+          result `shouldBe` [Only (Latin "nuntium")]
diff --git a/test/DateTimeSpec.hs b/test/DateTimeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/DateTimeSpec.hs
@@ -0,0 +1,82 @@
+{-# LANGUAGE OverloadedStrings #-}
+module DateTimeSpec (
+      dateTimeUnit
+    , dateTimeSpec
+) where
+
+import Control.Monad                (void)
+import Data.Ratio                   ((%))
+import Data.Time.Calendar           (Day(..), toGregorian, fromGregorian)
+import Data.Time.Clock              (UTCTime(..))
+import Data.Time.LocalTime          (TimeOfDay(..))
+import Database.MySQL.Simple
+import Database.MySQL.Simple.Param  (Action(..), render)
+import Test.Hspec
+
+import Common                       ()
+
+-- An arbitrary date and time: 2022-05-25 13:09:34.375 UTC
+testTime :: UTCTime
+testTime = UTCTime (ModifiedJulianDay 59724)
+                   (realToFrac $ (378995 % 8 :: Rational))
+
+testYear :: Day
+testYear =
+    let (y,_,_) = toGregorian (utctDay testTime)
+    in fromGregorian y 1 1
+
+
+dateTimeUnit :: Spec
+dateTimeUnit =
+  describe "Date and time Param types" $ do
+    it "UTCTime is correctly encoded" $
+      render testTime `shouldBe` Plain "'2022-05-25 13:09:34.375'"
+    it "Day is correctly encoded" $
+      render (utctDay testTime) `shouldBe` Plain "'2022-05-25'"
+    it "TimeOfDay is correctly encoded" $
+      render (TimeOfDay 13 9 34.375) `shouldBe` Plain "'13:09:34.375'"
+
+
+dateTimeSpec :: Connection -> Spec
+dateTimeSpec conn =
+  beforeAll_
+    ( do  _ <- execute_ conn "drop table if exists datetime"
+          _ <- execute_ conn ( "create table datetime "
+                               <> "(i int,"
+                               <> " dt datetime(6),"
+                               <> " d date,"
+                               <> " y year,"
+                               <> " ts timestamp(6),"
+                               <> " t time(6))"
+                             )
+          void $ execute_ conn (
+                   "insert into datetime (i,dt,d,y,ts,t) values "
+                   <> "(1,'2022-05-25 13:09:34.375','2022-05-25','2022','2022-05-25 13:09:34.375','13:09:34.375'),"
+                   <> "(2,'0000-00-00 00:00:00','0000-00-00','0000','0000-00-00 00:00:00','00:00:00')"
+                 )
+    ) $ do
+      describe "reading date and time" $ do
+        it "should read DATETIME correctly" $ do
+          result <- query_ conn "select dt from datetime where i = 1"
+          result `shouldBe` [Only testTime]
+        it "should read DATE correctly" $ do
+          result <- query_ conn "select d from datetime where i = 1"
+          result `shouldBe` [Only $ utctDay testTime]
+        it "should read YEAR correctly" $ do
+          result <- query_ conn "select y from datetime where i = 1"
+          result `shouldBe` [Only testYear]
+        it "should read TIMESTAMP correctly" $ do
+          result <- query_ conn "select ts from datetime where i = 1"
+          result `shouldBe` [Only testTime]
+        it "should read TIME correctly" $ do
+          result <- query_ conn "select t from datetime where i = 1"
+          result `shouldBe` [Only $ TimeOfDay 13 9 34.375]
+        it "should be vaguely reasonable given a zero DATETIME" $ do
+          result <- query_ conn "select dt from datetime where i = 2"
+          result `shouldBe` [Only $ UTCTime (fromGregorian 0 0 0) 0]
+        it "should be vaguely reasonable given a zero DATE" $ do
+          result <- query_ conn "select d from datetime where i = 2"
+          result `shouldBe` [Only $ fromGregorian 0 0 0]
+        it "should be vaguely reasonable given a zero YEAR" $ do
+          result <- query_ conn "select y from datetime where i = 2"
+          result `shouldBe` [Only $ fromGregorian 0 0 0]
diff --git a/test/main.hs b/test/main.hs
new file mode 100644
--- /dev/null
+++ b/test/main.hs
@@ -0,0 +1,117 @@
+{-# LANGUAGE CPP, OverloadedStrings #-}
+
+import Control.Applicative   ((<|>))
+import Control.Exception (bracket)
+import Data.Text (Text)
+import Database.MySQL.Simple
+import Database.MySQL.Simple.Param
+import System.Environment    (getEnvironment)
+import Test.Hspec
+import Blaze.ByteString.Builder (toByteString)
+
+import Common                       ()
+import DateTimeSpec                 (dateTimeUnit, dateTimeSpec)
+import CustomTypeSpec               (customTypeUnit, customTypeSpec)
+
+isCI :: IO Bool
+isCI = do
+    env <- getEnvironment
+    return $ case lookup "TRAVIS" env <|> lookup "CI" env of
+               Just "true" -> True
+               _ -> False
+
+-- This is how to connect to our test database
+testConn :: Bool -> ConnectInfo
+testConn ci = defaultConnectInfo {
+                connectHost     = "127.0.0.1"
+              , connectUser     = "test"
+              , connectPassword = "test"
+              , connectDatabase = "test"
+              , connectPort     = if ci then 33306 else 3306
+              }
+
+-- Allow tests to do things which would be prevented in strict SQL mode
+withConnection :: (Connection -> IO()) -> IO()
+withConnection action = do
+    ci <- isCI
+    bracket (do  conn <- connect $ testConn ci
+                 _ <- execute_ conn "set session sql_mode=''"
+                 return conn
+            )
+            close
+            action
+
+main :: IO ()
+main =
+    withConnection $ \conn ->
+      hspec $ do
+        describe "Database.MySQL.Simple.unitSpec" unitSpec
+        describe "Database.MySQL.Simple.integrationSpec" $ integrationSpec conn
+        describe "Database.MySQL.Simple.DateTimeSpec" $ do
+          dateTimeUnit
+          dateTimeSpec conn
+        describe "Database.MySQL.Simple.CustomTypeSpec" $ do
+          customTypeUnit
+          customTypeSpec conn
+
+unitSpec :: Spec
+unitSpec = do
+  describe "Param a => Param (In [a]) instance" $ do
+    it "renders an empty list correctly" $ do
+      let empty :: [Text]
+          empty = mempty
+
+      case render (In empty) of
+        Plain a -> toByteString a `shouldBe` "(null)"
+        _       -> expectationFailure "expected a Plain"
+
+    it "renders a non-empty list correctly" $ do
+      let l :: [Text]
+          l = ["foo", "bar"]
+
+      case render (In l) of
+        Many [Plain _, Escape "foo", Plain _, Escape "bar", Plain _] -> pure ()
+        _ -> expectationFailure "expected a Many with specific contents"
+
+  describe "splitQuery" $ do
+    it "works for a single question mark" $ do
+      splitQuery "select * from foo where name = ?"
+        `shouldBe`
+          ["select * from foo where name = ", ""]
+    it "works with a question mark in a string literal" $ do
+      splitQuery "select 'hello?'"
+        `shouldBe`
+          ["select 'hello?'"]
+    it "works with many question marks" $ do
+      splitQuery "select ? + ? + what from foo where bar = ?"
+        `shouldBe`
+          ["select ", " + ", " + what from foo where bar = ", ""]
+
+integrationSpec :: Connection -> Spec
+integrationSpec conn = do
+  describe "query_" $ do
+    it "can connect to a database" $ do
+      result <- query_ conn "select 1 + 1"
+      result `shouldBe` [Only (2::Int)]
+    it "can have question marks in string literals" $ do
+      result <- query_ conn "select 'hello?'"
+      result `shouldBe` [Only ("hello?" :: Text)]
+  describe "query" $ do
+    it "can have question marks in string literals" $ do
+      result <- query conn "select 'hello?'" ()
+      result `shouldBe` [Only ("hello?" :: Text)]
+    describe "with too many query params" $ do
+      it "should have the right message" $ do
+        (query conn "select 'hello?'" (Only ['a']) :: IO [Only Text])
+          `shouldThrow`
+            (\e -> fmtMessage e == "0 '?' characters, but 1 parameters")
+    describe "with too few query params" $ do
+      it "should have the right message" $ do
+        (query conn "select 'hello?' = ?" () :: IO [Only Text])
+          `shouldThrow`
+            (\e -> fmtMessage e == "1 '?' characters, but 0 parameters")
+  describe "formatQuery" $ do
+    it "should not blow up on a question mark in string literal" $ do
+      formatQuery conn "select 'hello?'" ()
+        `shouldReturn`
+          "select 'hello?'"
