From b5638fd4da2c8ce34143989a440c1b9e38f1a0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 15 May 2019 22:27:46 +0200 Subject: [PATCH 01/10] Upgrade to lts-13.10 --- stack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack.yaml b/stack.yaml index 1fd280c..3bcd3ee 100644 --- a/stack.yaml +++ b/stack.yaml @@ -4,4 +4,4 @@ flags: packages: - '.' extra-deps: [] -resolver: lts-6.23 +resolver: lts-13.10 From 2e1619f85176dcfa903835104d211cae28082e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 15 May 2019 22:28:06 +0200 Subject: [PATCH 02/10] Implement generic derivations for `QueryParams` and `QueryResults` I moved `convertError` from `Database.MySQL.Simpe.QueryResults` into `Database.MySQL.Simple.Result` to avoid a cyclic dependency between the `Database.MySQL.Simpe.QueryResults` and `Database.MySQL.Simpe.QueryResults.Generic` that both make use of said function. Note that this change requires GHC Haskell. We may want to hide this behind a preprocessor flag for that reason. I don't quite know if it makes sense to derive instances for `U1` (types with a single constructor and no fields). Although it is certainly helpful during development to not get a type error when trying to derive instances for a type that you are in the process of defining, like: data User = User I have also noted a concern with my generic implementation for `(:*:)`. I'll reproduce it here: I'm concerned about this implementation since it's biased towards one side. Meaning that it's expecting the chain of `(:*:)` to lean to the right. As an example of the problem consider the following: > data T = T Int Int Int deriving (Generic) > from (T 1 2 3) M1 {unM1 = M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}} :*: (M1 {unM1 = K1 {unK1 = 2}} :*: M1 {unM1 = K1 {unK1 = 3}})}} If the result in stead had been like this (note the re-bracketing): M1 {unM1 = (M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}} :*: M1 {unM1 = K1 {unK1 = 2}}}) :*: M1 {unM1 = K1 {unK1 = 3}}} Then the generic derivation for `T` would fail. Furthermore, to be able to supply the arity in the call to `convertError` I think we might need a `Typeable (a :*: b)` constraint. Lastly, the existing implementations for `QueryResults` uses strictness annotations in certain places. I don't know if these are strictly (pun!) necessary and haven't used them in the generic implementation. --- Database/MySQL/Simple/QueryParams.hs | 25 ++++++++ Database/MySQL/Simple/QueryParams/Generic.hs | 27 +++++++++ Database/MySQL/Simple/QueryResults.hs | 60 +++++++++---------- Database/MySQL/Simple/QueryResults/Generic.hs | 57 ++++++++++++++++++ Database/MySQL/Simple/Result.hs | 27 ++++++++- mysql-simple.cabal | 6 +- 6 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 Database/MySQL/Simple/QueryParams/Generic.hs create mode 100644 Database/MySQL/Simple/QueryResults/Generic.hs diff --git a/Database/MySQL/Simple/QueryParams.hs b/Database/MySQL/Simple/QueryParams.hs index eeb74b8..2966f15 100644 --- a/Database/MySQL/Simple/QueryParams.hs +++ b/Database/MySQL/Simple/QueryParams.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DefaultSignatures, FlexibleContexts #-} -- | -- Module: Database.MySQL.Simple.QueryParams -- Copyright: (c) 2011 MailRank, Inc. @@ -19,16 +20,40 @@ module Database.MySQL.Simple.QueryParams import Database.MySQL.Simple.Param (Action(..), Param(..)) import Database.MySQL.Simple.Types (Only(..)) +import qualified Database.MySQL.Simple.QueryParams.Generic as Generic +import GHC.Generics (Generic, Rep) +import GHC.Generics as Generics -- | A collection type that can be turned into a list of rendering -- 'Action's. -- -- Instances should use the 'render' method of the 'Param' class -- to perform conversion of each element of the collection. +-- +-- === Generic derivation +-- +-- Since version 0.4.6 it's possible to generically derive instances +-- for some types. One conditition is that the type must only have a +-- single constructor. In those cases an instance can derived thus: +-- +-- @ +-- deriving anyclass instance 'QueryParams' User +-- @ +-- +-- This requires @-XDeriveAnyClass@ and @-XDerivingStrategies@. Here +-- @User@ is the example from +-- 'Database.MySQL.Simple.QueryResults.QueryResults'. class QueryParams a where renderParams :: a -> [Action] -- ^ Render a collection of values. + default renderParams + :: Generic a + => Generic.QueryParams (Rep a) + => a + -> [Action] + renderParams = Generic.renderParams . Generics.from + instance QueryParams () where renderParams _ = [] diff --git a/Database/MySQL/Simple/QueryParams/Generic.hs b/Database/MySQL/Simple/QueryParams/Generic.hs new file mode 100644 index 0000000..ea32b82 --- /dev/null +++ b/Database/MySQL/Simple/QueryParams/Generic.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, KindSignatures #-} +module Database.MySQL.Simple.QueryParams.Generic + ( QueryParams(..) + ) where + +import Data.Kind +import GHC.Generics + +import Database.MySQL.Simple.Param (Action(..), Param(..)) + +class QueryParams (f :: Type -> Type) where + renderParams :: f a -> [Action] + +-- This instance might not make sense, though the signature of +-- 'renderParams' sort of implies that it does since it returns a +-- '[]''s in stead of a 'NonEmpty''s. +instance QueryParams U1 where + renderParams = const mempty + +instance Param a => QueryParams (K1 i a) where + renderParams (K1 a) = pure $ render a + +instance QueryParams a => QueryParams (M1 i c a) where + renderParams (M1 a) = renderParams a + +instance (QueryParams a, QueryParams b) => QueryParams (a :*: b) where + renderParams (a :*: b) = renderParams a <> renderParams b diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index d35366c..80c5497 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -1,4 +1,5 @@ -{-# LANGUAGE BangPatterns, OverloadedStrings #-} +{-# LANGUAGE BangPatterns, OverloadedStrings, DefaultSignatures, + FlexibleContexts #-} -- | -- Module: Database.MySQL.Simpe.QueryResults @@ -20,12 +21,14 @@ module Database.MySQL.Simple.QueryResults , convertError ) where -import Control.Exception (throw) import Data.ByteString (ByteString) -import qualified Data.ByteString.Char8 as B -import Database.MySQL.Base.Types (Field(fieldType, fieldName)) -import Database.MySQL.Simple.Result (ResultError(..), Result(..)) +import Database.MySQL.Base.Types (Field) +import Database.MySQL.Simple.Result (Result(..), convertError) + import Database.MySQL.Simple.Types (Only(..)) +import qualified Database.MySQL.Simple.QueryResults.Generic as Generic +import GHC.Generics (Generic, Rep) +import qualified GHC.Generics as Generics -- | A collection type that can be converted from a list of strings. -- @@ -67,7 +70,18 @@ import Database.MySQL.Simple.Types (Only(..)) -- !b = 'convert' fb vb -- 'convertResults' fs vs = 'convertError' fs vs 2 -- @ - +-- +-- === Generic derivation +-- +-- Since version 0.4.6 it's possible to generically derive instances +-- for some types. One conditition is that the type must only have a +-- single constructor. In those cases an instance can derived thus: +-- +-- @ +-- deriving anyclass instance 'QueryResults' User +-- @ +-- +-- This requires @-XDeriveAnyClass@ and @-XDerivingStrategies@. class QueryResults a where convertResults :: [Field] -> [Maybe ByteString] -> a -- ^ Convert values from a row into a Haskell collection. @@ -75,6 +89,16 @@ class QueryResults a where -- This function will throw a 'ResultError' if conversion of the -- collection fails. + default convertResults + :: Generic a + => Generic.QueryResults (Rep a) + => [Field] + -> [Maybe ByteString] + -> a + convertResults xs ys + = Generics.to + $ Generic.convertResults xs ys + instance (Result a) => QueryResults (Only a) where convertResults [fa] [va] = Only a where !a = convert fa va @@ -378,27 +402,3 @@ instance (Result a, Result b, Result c, Result d, Result e, Result f, !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. -convertError :: [Field] - -- ^ Descriptors of fields to be converted. - -> [Maybe ByteString] - -- ^ Contents of the row to be converted. - -> Int - -- ^ Number of columns expected for conversion. For - -- instance, if converting to a 3-tuple, the number to - -- provide here would be 3. - -> a -convertError fs vs n = throw $ ConversionFailed - (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 -ellipsis bs - | B.length bs > 15 = B.take 10 bs `B.append` "[...]" - | otherwise = bs diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs new file mode 100644 index 0000000..501888a --- /dev/null +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -0,0 +1,57 @@ +{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, KindSignatures #-} +module Database.MySQL.Simple.QueryResults.Generic + ( QueryResults(..) + ) where + +import Prelude +import Data.Kind +import Data.ByteString (ByteString) +import GHC.Generics + +import Database.MySQL.Base.Types (Field) +import Database.MySQL.Simple.Result (Result(convert), convertError) + +class QueryResults (f :: Type -> Type) where + convertResults :: [Field] -> [Maybe ByteString] -> (f a) + +-- This instance might not make sense, though the signature of +-- 'convertResults' sort of implies that it does since it takes in two +-- '[]''s in stead of two 'NonEmpty''s. +instance QueryResults U1 where + convertResults xs ys = case zip xs ys of + [] -> U1 + _ -> convertError xs ys 0 + +instance Result a => QueryResults (K1 i a) where + convertResults xs ys = case zip xs ys of + [(x, y)] -> K1 $ convert x y + _ -> convertError xs ys 1 + +instance QueryResults a => QueryResults (M1 i c a) where + convertResults xs ys = + M1 $ convertResults xs ys + +instance (QueryResults a, QueryResults b) => QueryResults (a :*: b) where + convertResults :: forall x . [Field] -> [Maybe ByteString] -> (:*:) a b x + convertResults (x:xs) (y:ys) = + -- I'm concerned about this implementation since it's biased + -- towards one side. Meaning that it's expecting the chain of + -- ':*:' to lean to the right. As an example of the problem + -- consider the following: + -- + -- > data T = T Int Int Int deriving (Generic) + -- > from (T 1 2 3) + -- M1 {unM1 = M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}} :*: (M1 {unM1 = K1 {unK1 = 2}} :*: M1 {unM1 = K1 {unK1 = 3}})}} + -- + -- If the result in stead had been like this (note the re-bracketing): + -- + -- M1 {unM1 = (M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}} :*: M1 {unM1 = K1 {unK1 = 2}}}) :*: M1 {unM1 = K1 {unK1 = 3}}} + -- + -- Then the generic derivation for 'T' would fail. + convertResults [x] [y] :*: convertResults xs ys + convertResults xs ys = convertError xs ys c + where + -- My guess is that we'd need @Typeable (a :*: b)@ to determine + -- the number of constructors we're expecting here. + c :: Int + c = -1 diff --git a/Database/MySQL/Simple/Result.hs b/Database/MySQL/Simple/Result.hs index 16e117e..0102b0c 100644 --- a/Database/MySQL/Simple/Result.hs +++ b/Database/MySQL/Simple/Result.hs @@ -25,6 +25,7 @@ module Database.MySQL.Simple.Result ( Result(..) , ResultError(..) + , convertError ) where #include "MachDeps.h" @@ -40,7 +41,7 @@ import Data.Ratio (Ratio) import Data.Time.Calendar (Day, fromGregorian) import Data.Time.Clock (UTCTime(..)) import Data.Time.Format (parseTime) -import Data.Time.LocalTime (TimeOfDay, makeTimeOfDayValid) +import Data.Time.LocalTime (TimeOfDay) import Data.Typeable (TypeRep, Typeable, typeOf) import Data.Word (Word, Word8, Word16, Word32, Word64) import Database.MySQL.Base.Types (Field(..), Type(..)) @@ -246,3 +247,27 @@ atto types p0 f = doConvert f types $ go undefined p0 case parseOnly p s of Left err -> conversionFailed f (show (typeOf dummy)) err Right v -> v + +-- | 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. +convertError :: [Field] + -- ^ Descriptors of fields to be converted. + -> [Maybe ByteString] + -- ^ Contents of the row to be converted. + -> Int + -- ^ Number of columns expected for conversion. For + -- instance, if converting to a 3-tuple, the number to + -- provide here would be 3. + -> a +convertError fs vs n = throw $ ConversionFailed + (show (length fs) ++ " values: " ++ show (zip (map fieldType fs) + (map (fmap ellipsis) vs))) + (show n ++ " slots in target type") + (show (map (SB.unpack . fieldName) fs)) + "mismatch between number of columns to convert and number in target type" + +ellipsis :: ByteString -> ByteString +ellipsis bs + | SB.length bs > 15 = SB.take 10 bs `SB.append` "[...]" + | otherwise = bs diff --git a/mysql-simple.cabal b/mysql-simple.cabal index 6c6ff67..8c5222e 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -1,5 +1,5 @@ name: mysql-simple -version: 0.4.5 +version: 0.4.6 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. @@ -41,6 +41,10 @@ library Database.MySQL.Simple.Result Database.MySQL.Simple.Types + other-modules: + Database.MySQL.Simple.QueryParams.Generic + Database.MySQL.Simple.QueryResults.Generic + build-depends: attoparsec >= 0.10.0.0, base < 5, From ee3bba33b860a1787eb8e2513390bda91bf1de5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 15 May 2019 22:28:17 +0200 Subject: [PATCH 03/10] Add a SQL quasi-quoter. --- Database/MySQL/Simple/QQ.hs | 45 +++++++++++++++++++++++++++++++++++++ mysql-simple.cabal | 6 +++-- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 Database/MySQL/Simple/QQ.hs diff --git a/Database/MySQL/Simple/QQ.hs b/Database/MySQL/Simple/QQ.hs new file mode 100644 index 0000000..9312f49 --- /dev/null +++ b/Database/MySQL/Simple/QQ.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE TemplateHaskell, TypeApplications #-} +module Database.MySQL.Simple.QQ + ( sql + ) where + +import Prelude +import Language.Haskell.TH (Exp, Q, appE, stringE) +import Language.Haskell.TH.Quote (QuasiQuoter (..)) +import Database.MySQL.Simple (Query, Query) +import Text.Printf (printf) + +-- | A quasi-quoter for SQL expressions. +-- +-- The quasi-quoter does not do any sort of parsing of the SQL. It's +-- simply a convenience for writing multi-line SQL statements. So in stead of: +-- +-- > query +-- > = "select * " +-- > <> "from users " +-- > <> "where email is null;" +-- +-- You could write +-- +-- > query = [sql| +-- > select * +-- > from users +-- > where email is null; +-- > |] +-- +-- Note the quasi-quoter is only valid in expression contexts. +-- +-- @since 0.4.7 +sql :: QuasiQuoter +sql = QuasiQuoter + { quotePat = err "pattern" + , quoteType = error "Database.MySQL.Simple.QQ.sql: quasiquoter used in type context" + , quoteDec = error "Database.MySQL.Simple.QQ.sql: quasiquoter used in declaration context" + , quoteExp = quote + } + where + err :: String -> a + err ctxt = error (printf "Database.MySQL.Simple.QQ.sql: quasiquoter used in %s context" ctxt) + +quote :: String -> Q Exp +quote = appE [| fromString @Query |] . stringE diff --git a/mysql-simple.cabal b/mysql-simple.cabal index 8c5222e..ba8b9da 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -1,5 +1,5 @@ name: mysql-simple -version: 0.4.6 +version: 0.4.7 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. @@ -36,6 +36,7 @@ library exposed-modules: Database.MySQL.Simple Database.MySQL.Simple.Param + Database.MySQL.Simple.QQ Database.MySQL.Simple.QueryParams Database.MySQL.Simple.QueryResults Database.MySQL.Simple.Result @@ -57,7 +58,8 @@ library pcre-light, old-locale, text >= 0.11.0.2, - time + time, + template-haskell if !impl(ghc >= 8.0) build-depends: semigroups >= 0.11 && < 0.19 From 27661064325ab6cc9e82eb26fe75ff0676e1c2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Thu, 16 May 2019 17:14:16 +0200 Subject: [PATCH 04/10] Backwards compatibility, turn on -Wall -Werror Goodbye to these lovely GHC extensions :( * TypeApplications * KindSignatures Also turns on -Wall and -Werror in some modules. --- Database/MySQL/Simple/Compat.hs | 8 ++++++++ Database/MySQL/Simple/Prelude.hs | 8 ++++++++ Database/MySQL/Simple/QQ.hs | 9 ++++++--- Database/MySQL/Simple/QueryParams/Generic.hs | 8 +++++--- Database/MySQL/Simple/QueryResults/Generic.hs | 8 +++++--- mysql-simple.cabal | 2 ++ 6 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 Database/MySQL/Simple/Compat.hs create mode 100644 Database/MySQL/Simple/Prelude.hs diff --git a/Database/MySQL/Simple/Compat.hs b/Database/MySQL/Simple/Compat.hs new file mode 100644 index 0000000..fe01596 --- /dev/null +++ b/Database/MySQL/Simple/Compat.hs @@ -0,0 +1,8 @@ +{-# OPTIONS_GHC -Wall -Werror #-} + +module Database.MySQL.Simple.Compat + ( module X + ) where + +import Data.Semigroup as X (Semigroup(..)) +import Data.String as X (fromString) diff --git a/Database/MySQL/Simple/Prelude.hs b/Database/MySQL/Simple/Prelude.hs new file mode 100644 index 0000000..ebbaa83 --- /dev/null +++ b/Database/MySQL/Simple/Prelude.hs @@ -0,0 +1,8 @@ +{-# OPTIONS_GHC -Wall -Werror #-} + +module Database.MySQL.Simple.Prelude + ( module X + ) where + +import Prelude as X +import Database.MySQL.Simple.Compat as X diff --git a/Database/MySQL/Simple/QQ.hs b/Database/MySQL/Simple/QQ.hs index 9312f49..d54fdc5 100644 --- a/Database/MySQL/Simple/QQ.hs +++ b/Database/MySQL/Simple/QQ.hs @@ -1,9 +1,12 @@ -{-# LANGUAGE TemplateHaskell, TypeApplications #-} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wall -Werror #-} + module Database.MySQL.Simple.QQ ( sql ) where -import Prelude +import Prelude () +import Database.MySQL.Simple.Prelude import Language.Haskell.TH (Exp, Q, appE, stringE) import Language.Haskell.TH.Quote (QuasiQuoter (..)) import Database.MySQL.Simple (Query, Query) @@ -42,4 +45,4 @@ sql = QuasiQuoter err ctxt = error (printf "Database.MySQL.Simple.QQ.sql: quasiquoter used in %s context" ctxt) quote :: String -> Q Exp -quote = appE [| fromString @Query |] . stringE +quote = appE [| fromString :: String -> Query |] . stringE diff --git a/Database/MySQL/Simple/QueryParams/Generic.hs b/Database/MySQL/Simple/QueryParams/Generic.hs index ea32b82..2ca88d5 100644 --- a/Database/MySQL/Simple/QueryParams/Generic.hs +++ b/Database/MySQL/Simple/QueryParams/Generic.hs @@ -1,14 +1,16 @@ -{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, KindSignatures #-} +{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs #-} +{-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.QueryParams.Generic ( QueryParams(..) ) where -import Data.Kind +import Prelude () +import Database.MySQL.Simple.Prelude import GHC.Generics import Database.MySQL.Simple.Param (Action(..), Param(..)) -class QueryParams (f :: Type -> Type) where +class QueryParams f where renderParams :: f a -> [Action] -- This instance might not make sense, though the signature of diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index 501888a..60b7f98 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -1,17 +1,19 @@ {-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, KindSignatures #-} +{-# OPTIONS_GHC -Wall -Werror #-} + module Database.MySQL.Simple.QueryResults.Generic ( QueryResults(..) ) where -import Prelude -import Data.Kind +import Prelude () +import Database.MySQL.Simple.Prelude import Data.ByteString (ByteString) import GHC.Generics import Database.MySQL.Base.Types (Field) import Database.MySQL.Simple.Result (Result(convert), convertError) -class QueryResults (f :: Type -> Type) where +class QueryResults f where convertResults :: [Field] -> [Maybe ByteString] -> (f a) -- This instance might not make sense, though the signature of diff --git a/mysql-simple.cabal b/mysql-simple.cabal index ba8b9da..6b5093d 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -43,6 +43,8 @@ library Database.MySQL.Simple.Types other-modules: + Database.MySQL.Simple.Compat + Database.MySQL.Simple.Prelude Database.MySQL.Simple.QueryParams.Generic Database.MySQL.Simple.QueryResults.Generic From 28ed43c7d425c59a803f5c59bceef37d50a32112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Thu, 23 May 2019 21:28:05 +0200 Subject: [PATCH 05/10] Provide less confusing error message from generic derivation --- Database/MySQL/Simple/QueryResults.hs | 4 +++ Database/MySQL/Simple/QueryResults/Generic.hs | 27 +++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index 80c5497..df3853e 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -82,6 +82,10 @@ import qualified GHC.Generics as Generics -- @ -- -- This requires @-XDeriveAnyClass@ and @-XDerivingStrategies@. +-- +-- Caveat emptor! The error message generated when a conversion +-- failure happens and when using the generic implementation is not as +-- helpful as with hand-written instances. class QueryResults a where convertResults :: [Field] -> [Maybe ByteString] -> a -- ^ Convert values from a row into a Haskell collection. diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index 60b7f98..dd493cf 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -9,9 +9,11 @@ import Prelude () import Database.MySQL.Simple.Prelude import Data.ByteString (ByteString) import GHC.Generics +import Control.Exception (throw) import Database.MySQL.Base.Types (Field) -import Database.MySQL.Simple.Result (Result(convert), convertError) +import Database.MySQL.Simple.Result + (Result(convert), ResultError(..)) class QueryResults f where convertResults :: [Field] -> [Maybe ByteString] -> (f a) @@ -22,12 +24,12 @@ class QueryResults f where instance QueryResults U1 where convertResults xs ys = case zip xs ys of [] -> U1 - _ -> convertError xs ys 0 + _ -> genericConversionError instance Result a => QueryResults (K1 i a) where convertResults xs ys = case zip xs ys of [(x, y)] -> K1 $ convert x y - _ -> convertError xs ys 1 + _ -> genericConversionError instance QueryResults a => QueryResults (M1 i c a) where convertResults xs ys = @@ -51,9 +53,18 @@ instance (QueryResults a, QueryResults b) => QueryResults (a :*: b) where -- -- Then the generic derivation for 'T' would fail. convertResults [x] [y] :*: convertResults xs ys - convertResults xs ys = convertError xs ys c + convertResults _ _ = genericConversionError where - -- My guess is that we'd need @Typeable (a :*: b)@ to determine - -- the number of constructors we're expecting here. - c :: Int - c = -1 + +genericConversionError :: a +genericConversionError = throw $ ConversionFailed + { errSQLType = cantDetermine + , errHaskellType = cantDetermine + , errFieldName = cantDetermine + , errMessage = msg + } + where + msg + = "Database.MySQL.Simple.QueryResults.Generic.convertResult: " + <> "Mis-match between result and target-type." + cantDetermine = "Cannot be determined." From 726d6976b8c1796b62c8ddb7cd80b1199a3d1c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Thu, 23 May 2019 22:03:36 +0200 Subject: [PATCH 06/10] Better error messages with generic derivation --- Database/MySQL/Simple/QueryResults.hs | 12 ++-- Database/MySQL/Simple/QueryResults/Generic.hs | 63 ++++++++++--------- Database/MySQL/Simple/Result.hs | 17 ++++- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index df3853e..4968d13 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -28,7 +28,6 @@ import Database.MySQL.Simple.Result (Result(..), convertError) import Database.MySQL.Simple.Types (Only(..)) import qualified Database.MySQL.Simple.QueryResults.Generic as Generic import GHC.Generics (Generic, Rep) -import qualified GHC.Generics as Generics -- | A collection type that can be converted from a list of strings. -- @@ -83,9 +82,10 @@ import qualified GHC.Generics as Generics -- -- This requires @-XDeriveAnyClass@ and @-XDerivingStrategies@. -- --- Caveat emptor! The error message generated when a conversion --- failure happens and when using the generic implementation is not as --- helpful as with hand-written instances. +-- Caveat emptor! The generic derivation does not figure out how many +-- summands are in the target type. This information is only used for +-- the error message which gets displayed when a conversion error +-- happens. class QueryResults a where convertResults :: [Field] -> [Maybe ByteString] -> a -- ^ Convert values from a row into a Haskell collection. @@ -99,9 +99,7 @@ class QueryResults a where => [Field] -> [Maybe ByteString] -> a - convertResults xs ys - = Generics.to - $ Generic.convertResults xs ys + convertResults = Generic.convert instance (Result a) => QueryResults (Only a) where convertResults [fa] [va] = Only a diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index dd493cf..27fad66 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -1,43 +1,64 @@ -{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, KindSignatures #-} +{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, + KindSignatures, FlexibleContexts #-} {-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.QueryResults.Generic ( QueryResults(..) + , convert ) where import Prelude () import Database.MySQL.Simple.Prelude import Data.ByteString (ByteString) import GHC.Generics +import qualified GHC.Generics as Generics import Control.Exception (throw) import Database.MySQL.Base.Types (Field) import Database.MySQL.Simple.Result - (Result(convert), ResultError(..)) + (Result, ResultError) +import qualified Database.MySQL.Simple.Result as Result + +-- | Generic derivation of +-- "Database.MySQL.Simple.QueryResults.Generic.convertResults" +convert + :: Generic a + => QueryResults (Rep a) + => [Field] + -> [Maybe ByteString] + -> a +convert xs ys = Generics.to $ convertResults err xs ys + where + err = Result.convertException xs ys (-1) class QueryResults f where - convertResults :: [Field] -> [Maybe ByteString] -> (f a) + convertResults :: ResultError -> [Field] -> [Maybe ByteString] -> f a -- This instance might not make sense, though the signature of -- 'convertResults' sort of implies that it does since it takes in two -- '[]''s in stead of two 'NonEmpty''s. instance QueryResults U1 where - convertResults xs ys = case zip xs ys of + convertResults err xs ys = case zip xs ys of [] -> U1 - _ -> genericConversionError + _ -> throw err instance Result a => QueryResults (K1 i a) where - convertResults xs ys = case zip xs ys of - [(x, y)] -> K1 $ convert x y - _ -> genericConversionError + convertResults err xs ys = case zip xs ys of + [(x, y)] -> K1 $ Result.convert x y + _ -> throw err instance QueryResults a => QueryResults (M1 i c a) where - convertResults xs ys = - M1 $ convertResults xs ys + convertResults err xs ys = + M1 $ convertResults err xs ys instance (QueryResults a, QueryResults b) => QueryResults (a :*: b) where - convertResults :: forall x . [Field] -> [Maybe ByteString] -> (:*:) a b x - convertResults (x:xs) (y:ys) = + convertResults + :: forall x + . ResultError + -> [Field] + -> [Maybe ByteString] + -> (:*:) a b x + convertResults err (x:xs) (y:ys) = -- I'm concerned about this implementation since it's biased -- towards one side. Meaning that it's expecting the chain of -- ':*:' to lean to the right. As an example of the problem @@ -52,19 +73,5 @@ instance (QueryResults a, QueryResults b) => QueryResults (a :*: b) where -- M1 {unM1 = (M1 {unM1 = M1 {unM1 = K1 {unK1 = 1}} :*: M1 {unM1 = K1 {unK1 = 2}}}) :*: M1 {unM1 = K1 {unK1 = 3}}} -- -- Then the generic derivation for 'T' would fail. - convertResults [x] [y] :*: convertResults xs ys - convertResults _ _ = genericConversionError - where - -genericConversionError :: a -genericConversionError = throw $ ConversionFailed - { errSQLType = cantDetermine - , errHaskellType = cantDetermine - , errFieldName = cantDetermine - , errMessage = msg - } - where - msg - = "Database.MySQL.Simple.QueryResults.Generic.convertResult: " - <> "Mis-match between result and target-type." - cantDetermine = "Cannot be determined." + convertResults err [x] [y] :*: convertResults err xs ys + convertResults err _ _ = throw err diff --git a/Database/MySQL/Simple/Result.hs b/Database/MySQL/Simple/Result.hs index 0102b0c..ba2db8c 100644 --- a/Database/MySQL/Simple/Result.hs +++ b/Database/MySQL/Simple/Result.hs @@ -25,6 +25,7 @@ module Database.MySQL.Simple.Result ( Result(..) , ResultError(..) + , convertException , convertError ) where @@ -251,7 +252,9 @@ atto types p0 f = doConvert f types $ go undefined p0 -- | 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. -convertError :: [Field] +-- +-- @since 0.4.7 +convertException :: [Field] -- ^ Descriptors of fields to be converted. -> [Maybe ByteString] -- ^ Contents of the row to be converted. @@ -259,14 +262,22 @@ convertError :: [Field] -- ^ Number of columns expected for conversion. For -- instance, if converting to a 3-tuple, the number to -- provide here would be 3. - -> a -convertError fs vs n = throw $ ConversionFailed + -> ResultError +convertException fs vs n = ConversionFailed (show (length fs) ++ " values: " ++ show (zip (map fieldType fs) (map (fmap ellipsis) vs))) (show n ++ " slots in target type") (show (map (SB.unpack . fieldName) fs)) "mismatch between number of columns to convert and number in target type" +-- | Throw a 'ConversionFailed' exception as generated by 'convertException'. +convertError + :: [Field] + -> [Maybe ByteString] + -> Int + -> a +convertError as bs n = throw $ convertException as bs n + ellipsis :: ByteString -> ByteString ellipsis bs | SB.length bs > 15 = SB.take 10 bs `SB.append` "[...]" From 75ebd98046de70c6a93bf072b125197187671ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 29 May 2019 16:43:22 +0200 Subject: [PATCH 07/10] Generically figure out how many constructors a data-type has. --- Database/MySQL/Simple/Arity.hs | 49 +++++++++++++++++++ Database/MySQL/Simple/QQ.hs | 1 + Database/MySQL/Simple/QueryParams/Generic.hs | 1 + Database/MySQL/Simple/QueryResults.hs | 8 ++- Database/MySQL/Simple/QueryResults/Generic.hs | 23 ++++++--- 5 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 Database/MySQL/Simple/Arity.hs diff --git a/Database/MySQL/Simple/Arity.hs b/Database/MySQL/Simple/Arity.hs new file mode 100644 index 0000000..95515c9 --- /dev/null +++ b/Database/MySQL/Simple/Arity.hs @@ -0,0 +1,49 @@ +{-| Generic calculation of the "arity" of data-types. + +This code was originally written by Li-yao Xia. See +. +-} +{-# LANGUAGE + AllowAmbiguousTypes + , ScopedTypeVariables + , TypeFamilies + , UndecidableInstances + , FlexibleContexts + , DataKinds + , TypeOperators +#-} +{-# OPTIONS_GHC -Wall -Werror #-} + +module Database.MySQL.Simple.Arity + ( Arity + , arity + , KnownNat + ) where + +import Data.Kind (Type) +import Data.Proxy (Proxy(Proxy)) +import Numeric.Natural (Natural) +import GHC.Generics (M1, U1, K1, (:*:), Generic, Rep) +import GHC.TypeNats (KnownNat, Nat, type (+), natVal) + +type family Arity (f :: Type -> Type) :: Nat +type instance Arity (M1 _ _ f) = Arity f +type instance Arity (f :*: g) = Arity f + Arity g +type instance Arity U1 = 0 +type instance Arity (K1 i a) = 1 + +-- We need the proxy argument to support GHC version prior to the +-- introduction of '-XTypeApplications'. +-- | @'arity' (Proxy :: a)@ for some type @a@ calculates the arity of +-- its only constructor. @a@ *must* be a type with a single +-- constructor that has @n@ fields. +arity + :: forall a + . Generic a + => KnownNat (Arity (Rep a)) + => Proxy a + -> Natural +arity _ = natVal p + where + p :: Proxy (Arity (Rep a)) + p = Proxy diff --git a/Database/MySQL/Simple/QQ.hs b/Database/MySQL/Simple/QQ.hs index d54fdc5..df61d3b 100644 --- a/Database/MySQL/Simple/QQ.hs +++ b/Database/MySQL/Simple/QQ.hs @@ -1,3 +1,4 @@ +{-| A quasi-quoter for SQL expressions. -} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wall -Werror #-} diff --git a/Database/MySQL/Simple/QueryParams/Generic.hs b/Database/MySQL/Simple/QueryParams/Generic.hs index 2ca88d5..12ad8f6 100644 --- a/Database/MySQL/Simple/QueryParams/Generic.hs +++ b/Database/MySQL/Simple/QueryParams/Generic.hs @@ -1,3 +1,4 @@ +{-| Generic deriviation of 'Database.MySQL.Simple.QueryParams.QueryParams'. -} {-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs #-} {-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.QueryParams.Generic diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index 4968d13..6222739 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -27,6 +27,7 @@ import Database.MySQL.Simple.Result (Result(..), convertError) import Database.MySQL.Simple.Types (Only(..)) import qualified Database.MySQL.Simple.QueryResults.Generic as Generic +import Database.MySQL.Simple.Arity (Arity, KnownNat) import GHC.Generics (Generic, Rep) -- | A collection type that can be converted from a list of strings. @@ -81,11 +82,6 @@ import GHC.Generics (Generic, Rep) -- @ -- -- This requires @-XDeriveAnyClass@ and @-XDerivingStrategies@. --- --- Caveat emptor! The generic derivation does not figure out how many --- summands are in the target type. This information is only used for --- the error message which gets displayed when a conversion error --- happens. class QueryResults a where convertResults :: [Field] -> [Maybe ByteString] -> a -- ^ Convert values from a row into a Haskell collection. @@ -95,6 +91,8 @@ class QueryResults a where default convertResults :: Generic a + -- Used for for error messages. + => KnownNat (Arity (Rep a)) => Generic.QueryResults (Rep a) => [Field] -> [Maybe ByteString] diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index 27fad66..519fe85 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -1,5 +1,11 @@ -{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs, - KindSignatures, FlexibleContexts #-} +{-| Generic deriviation of 'Database.MySQL.Simple.QueryResults.QueryResults'. -} +{-# LANGUAGE + ScopedTypeVariables + , TypeOperators + , InstanceSigs + , FlexibleContexts + , AllowAmbiguousTypes + #-} {-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.QueryResults.Generic @@ -18,19 +24,24 @@ import Database.MySQL.Base.Types (Field) import Database.MySQL.Simple.Result (Result, ResultError) import qualified Database.MySQL.Simple.Result as Result +import Data.Proxy (Proxy(Proxy)) +import Database.MySQL.Simple.Arity (Arity, arity, KnownNat) --- | Generic derivation of --- "Database.MySQL.Simple.QueryResults.Generic.convertResults" +-- | Generic implementation of +-- 'Database.MySQL.Simple.QueryResults.Generic.convertResults'. convert - :: Generic a + :: forall a + . Generic a + => KnownNat (Arity (Rep a)) => QueryResults (Rep a) => [Field] -> [Maybe ByteString] -> a convert xs ys = Generics.to $ convertResults err xs ys where - err = Result.convertException xs ys (-1) + err = Result.convertException xs ys $ fromIntegral $ arity (Proxy :: Proxy a) +-- | The generic counterpart to 'Database.MySQL.Simple.QueryResults.QueryResults'. class QueryResults f where convertResults :: ResultError -> [Field] -> [Maybe ByteString] -> f a From 791521bb74a33e0493f5d53185aa6882bab6b5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 29 May 2019 22:58:10 +0200 Subject: [PATCH 08/10] Better error messages when failing to derive a generic instance for Arity Credit to David Feuer. --- Database/MySQL/Simple/Arity.hs | 36 ++++++++++--------- Database/MySQL/Simple/QueryResults.hs | 2 +- Database/MySQL/Simple/QueryResults/Generic.hs | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Database/MySQL/Simple/Arity.hs b/Database/MySQL/Simple/Arity.hs index 95515c9..8a40942 100644 --- a/Database/MySQL/Simple/Arity.hs +++ b/Database/MySQL/Simple/Arity.hs @@ -3,15 +3,8 @@ This code was originally written by Li-yao Xia. See . -} -{-# LANGUAGE - AllowAmbiguousTypes - , ScopedTypeVariables - , TypeFamilies - , UndecidableInstances - , FlexibleContexts - , DataKinds - , TypeOperators -#-} +{-# LANGUAGE AllowAmbiguousTypes, ScopedTypeVariables, TypeFamilies, + UndecidableInstances, FlexibleContexts, DataKinds, TypeOperators #-} {-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.Arity @@ -23,14 +16,23 @@ module Database.MySQL.Simple.Arity import Data.Kind (Type) import Data.Proxy (Proxy(Proxy)) import Numeric.Natural (Natural) -import GHC.Generics (M1, U1, K1, (:*:), Generic, Rep) +import GHC.Generics (M1, U1, K1, (:*:), Generic, Rep, (:+:), V1) import GHC.TypeNats (KnownNat, Nat, type (+), natVal) +import GHC.TypeLits (TypeError, ErrorMessage(ShowType, Text, (:<>:))) -type family Arity (f :: Type -> Type) :: Nat -type instance Arity (M1 _ _ f) = Arity f -type instance Arity (f :*: g) = Arity f + Arity g -type instance Arity U1 = 0 -type instance Arity (K1 i a) = 1 +type family Arity (x :: Type) (f :: Type -> Type) :: Nat +type instance Arity x (M1 _ _ f) = Arity x f +type instance Arity x (f :*: g) = Arity x f + Arity x g +type instance Arity _ U1 = 0 +type instance Arity _ (K1 i a) = 1 +type instance Arity x (_ :+: _) = + TypeError ('Text "Cannot calculate the arity of " + ':<>: 'ShowType x + ':<>: 'Text " because it has multiple constructors.") +type instance Arity x V1 = + TypeError ('Text "Cannot calculate the arity of " + ':<>: 'ShowType x + ':<>: 'Text " because it has no constructors.") -- We need the proxy argument to support GHC version prior to the -- introduction of '-XTypeApplications'. @@ -40,10 +42,10 @@ type instance Arity (K1 i a) = 1 arity :: forall a . Generic a - => KnownNat (Arity (Rep a)) + => KnownNat (Arity a (Rep a)) => Proxy a -> Natural arity _ = natVal p where - p :: Proxy (Arity (Rep a)) + p :: Proxy (Arity a (Rep a)) p = Proxy diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index 6222739..5bdaaad 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -92,7 +92,7 @@ class QueryResults a where default convertResults :: Generic a -- Used for for error messages. - => KnownNat (Arity (Rep a)) + => KnownNat (Arity a (Rep a)) => Generic.QueryResults (Rep a) => [Field] -> [Maybe ByteString] diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index 519fe85..a12f84e 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -32,7 +32,7 @@ import Database.MySQL.Simple.Arity (Arity, arity, KnownNat) convert :: forall a . Generic a - => KnownNat (Arity (Rep a)) + => KnownNat (Arity a (Rep a)) => QueryResults (Rep a) => [Field] -> [Maybe ByteString] From b361219cd4939b6795425b92592f44fb5ac43b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Wed, 29 May 2019 23:18:18 +0200 Subject: [PATCH 09/10] Backwards compatibility for GHC 7.10.3 --- Database/MySQL/Simple/Arity.hs | 19 +++++++++++++++---- Database/MySQL/Simple/QueryResults.hs | 6 +++++- Database/MySQL/Simple/QueryResults/Generic.hs | 13 ++++++++++++- mysql-simple.cabal | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Database/MySQL/Simple/Arity.hs b/Database/MySQL/Simple/Arity.hs index 8a40942..f8aba0d 100644 --- a/Database/MySQL/Simple/Arity.hs +++ b/Database/MySQL/Simple/Arity.hs @@ -4,18 +4,23 @@ This code was originally written by Li-yao Xia. See . -} {-# LANGUAGE AllowAmbiguousTypes, ScopedTypeVariables, TypeFamilies, - UndecidableInstances, FlexibleContexts, DataKinds, TypeOperators #-} + UndecidableInstances, FlexibleContexts, DataKinds, TypeOperators, + CPP #-} {-# OPTIONS_GHC -Wall -Werror #-} module Database.MySQL.Simple.Arity ( Arity , arity - , KnownNat +#if MIN_VERSION_base(4,10,0) +, KnownNat +#endif ) where -import Data.Kind (Type) -import Data.Proxy (Proxy(Proxy)) import Numeric.Natural (Natural) +import Data.Proxy (Proxy) +#if MIN_VERSION_base(4,10,0) +import Data.Proxy (Proxy(Proxy)) +import Data.Kind (Type) import GHC.Generics (M1, U1, K1, (:*:), Generic, Rep, (:+:), V1) import GHC.TypeNats (KnownNat, Nat, type (+), natVal) import GHC.TypeLits (TypeError, ErrorMessage(ShowType, Text, (:<>:))) @@ -49,3 +54,9 @@ arity _ = natVal p where p :: Proxy (Arity a (Rep a)) p = Proxy +#else +type family Arity (x :: *) (f :: * -> *) :: () + +arity :: Proxy a -> Natural +arity _ = 0 +#endif diff --git a/Database/MySQL/Simple/QueryResults.hs b/Database/MySQL/Simple/QueryResults.hs index 5bdaaad..0e85913 100644 --- a/Database/MySQL/Simple/QueryResults.hs +++ b/Database/MySQL/Simple/QueryResults.hs @@ -1,5 +1,5 @@ {-# LANGUAGE BangPatterns, OverloadedStrings, DefaultSignatures, - FlexibleContexts #-} + FlexibleContexts, CPP #-} -- | -- Module: Database.MySQL.Simpe.QueryResults @@ -27,7 +27,9 @@ import Database.MySQL.Simple.Result (Result(..), convertError) import Database.MySQL.Simple.Types (Only(..)) import qualified Database.MySQL.Simple.QueryResults.Generic as Generic +#if MIN_VERSION_base(4,10,0) import Database.MySQL.Simple.Arity (Arity, KnownNat) +#endif import GHC.Generics (Generic, Rep) -- | A collection type that can be converted from a list of strings. @@ -91,8 +93,10 @@ class QueryResults a where default convertResults :: Generic a +#if MIN_VERSION_base(4,10,0) -- Used for for error messages. => KnownNat (Arity a (Rep a)) +#endif => Generic.QueryResults (Rep a) => [Field] -> [Maybe ByteString] diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index a12f84e..a456ef1 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -5,6 +5,7 @@ , InstanceSigs , FlexibleContexts , AllowAmbiguousTypes + , CPP #-} {-# OPTIONS_GHC -Wall -Werror #-} @@ -24,22 +25,32 @@ import Database.MySQL.Base.Types (Field) import Database.MySQL.Simple.Result (Result, ResultError) import qualified Database.MySQL.Simple.Result as Result +#if MIN_VERSION_base(4,10,0) import Data.Proxy (Proxy(Proxy)) import Database.MySQL.Simple.Arity (Arity, arity, KnownNat) +#endif -- | Generic implementation of -- 'Database.MySQL.Simple.QueryResults.Generic.convertResults'. convert :: forall a . Generic a +#if MIN_VERSION_base(4,10,0) => KnownNat (Arity a (Rep a)) +#endif => QueryResults (Rep a) => [Field] -> [Maybe ByteString] -> a convert xs ys = Generics.to $ convertResults err xs ys where - err = Result.convertException xs ys $ fromIntegral $ arity (Proxy :: Proxy a) + err = Result.convertException xs ys n + n :: Int +#if MIN_VERSION_base(4,10,0) + n = fromIntegral $ arity (Proxy :: Proxy a) +#else + n = -1 +#endif -- | The generic counterpart to 'Database.MySQL.Simple.QueryResults.QueryResults'. class QueryResults f where diff --git a/mysql-simple.cabal b/mysql-simple.cabal index 6b5093d..092f612 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -43,6 +43,7 @@ library Database.MySQL.Simple.Types other-modules: + Database.MySQL.Simple.Arity Database.MySQL.Simple.Compat Database.MySQL.Simple.Prelude Database.MySQL.Simple.QueryParams.Generic From bc73b14b8e1b261e6d7e9440ae9f5b6cb0244ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20Hangh=C3=B8j=20Iversen?= Date: Thu, 30 May 2019 13:17:21 +0200 Subject: [PATCH 10/10] Add test-case for generic implementation of QueryResults --- Database/MySQL/Simple/QueryResults/Generic.hs | 4 +- mysql-simple.cabal | 2 + test/main.hs | 63 +++++++++++++++++-- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/Database/MySQL/Simple/QueryResults/Generic.hs b/Database/MySQL/Simple/QueryResults/Generic.hs index a456ef1..c8bcef7 100644 --- a/Database/MySQL/Simple/QueryResults/Generic.hs +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -65,8 +65,8 @@ instance QueryResults U1 where _ -> throw err instance Result a => QueryResults (K1 i a) where - convertResults err xs ys = case zip xs ys of - [(x, y)] -> K1 $ Result.convert x y + convertResults err xs ys = case zipWith Result.convert xs ys of + [res] -> K1 res _ -> throw err instance QueryResults a => QueryResults (M1 i c a) where diff --git a/mysql-simple.cabal b/mysql-simple.cabal index 092f612..a2ec75e 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -85,6 +85,8 @@ test-suite test , hspec , mysql-simple , text + , mysql + , bytestring source-repository head type: git diff --git a/test/main.hs b/test/main.hs index 4def253..a7215dd 100644 --- a/test/main.hs +++ b/test/main.hs @@ -1,16 +1,22 @@ -{-# LANGUAGE CPP, OverloadedStrings #-} +{-# LANGUAGE CPP, OverloadedStrings, StandaloneDeriving, + DerivingStrategies, DeriveGeneric, DeriveAnyClass, RankNTypes #-} -import Control.Exception (bracket) import Data.Text (Text) import Database.MySQL.Simple import Database.MySQL.Simple.Param +import Database.MySQL.Base.Types (Field(Field), Type(Tiny)) import Test.Hspec +import qualified Data.ByteString.Char8 as ByteString import Blaze.ByteString.Builder (toByteString) #if MIN_VERSION_base(4,8,2) #else import Control.Applicative import Data.Monoid #endif +import GHC.Generics +import Database.MySQL.Simple.QueryResults +import Data.Int (Int8) +import Control.Monad (void) -- This is how to connect to our test database testConn :: ConnectInfo @@ -22,10 +28,27 @@ testConn = defaultConnectInfo { main :: IO () main = - bracket (connect testConn) close $ \conn -> - hspec $ do - unitSpec - integrationSpec conn + hspec $ do + unitSpec + genericQueryResultsSpec + bracket' runIO (connect testConn) close integrationSpec + +-- | Ad-hoc lifted version of 'bracket'. The typical 'liftIO' method +-- is a parameter to this function rather than a constraint since +-- 'SpecM' from "Test.Hspec.Core.Spec" is not an instance of +-- 'MonadIO'. +bracket' + :: Monad io + => (forall x . IO x -> io x) + -> IO a + -> (a -> IO b) + -> (a -> io b) + -> io b +bracket' liftIO' acq rel act = do + conn <- liftIO' acq + a <- act conn + void $ liftIO' $ rel conn + pure a unitSpec :: Spec unitSpec = do @@ -52,3 +75,31 @@ integrationSpec conn = do it "can connect to a database" $ do result <- query_ conn "select 1 + 1" result `shouldBe` [Only (2::Int)] + +data U = U Int8 Int8 Int8 + +deriving stock instance Eq U +deriving stock instance Generic U +deriving stock instance Show U +deriving anyclass instance QueryResults U + +data T = T Int8 Int8 Int8 Int8 Int8 Int8 + +deriving stock instance Eq T +deriving stock instance Generic T +deriving stock instance Show T +deriving anyclass instance QueryResults T + +genericQueryResultsSpec :: Spec +genericQueryResultsSpec = + describe "QueryResults" $ + it "can perform generic conversions" $ do + convert [0..2] `shouldBe` U 0 1 2 + convert [0..5] `shouldBe` T 0 1 2 3 4 5 + where + aField :: Field + aField = Field mempty mempty mempty mempty + mempty mempty mempty 0 0 mempty 0 0 Tiny + convert :: QueryResults a => [Int8] -> a + convert xs = convertResults + (replicate (length xs) aField) $ Just . ByteString.pack . show <$> xs