diff --git a/Database/MySQL/Simple/Arity.hs b/Database/MySQL/Simple/Arity.hs new file mode 100644 index 0000000..f8aba0d --- /dev/null +++ b/Database/MySQL/Simple/Arity.hs @@ -0,0 +1,62 @@ +{-| 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, + CPP #-} +{-# OPTIONS_GHC -Wall -Werror #-} + +module Database.MySQL.Simple.Arity + ( Arity + , arity +#if MIN_VERSION_base(4,10,0) +, KnownNat +#endif + ) where + +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, (:<>:))) + +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'. +-- | @'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 a (Rep a)) + => Proxy a + -> Natural +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/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 new file mode 100644 index 0000000..df61d3b --- /dev/null +++ b/Database/MySQL/Simple/QQ.hs @@ -0,0 +1,49 @@ +{-| A quasi-quoter for SQL expressions. -} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wall -Werror #-} + +module Database.MySQL.Simple.QQ + ( sql + ) where + +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) +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 :: String -> Query |] . stringE 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..12ad8f6 --- /dev/null +++ b/Database/MySQL/Simple/QueryParams/Generic.hs @@ -0,0 +1,30 @@ +{-| Generic deriviation of 'Database.MySQL.Simple.QueryParams.QueryParams'. -} +{-# LANGUAGE ScopedTypeVariables, TypeOperators, InstanceSigs #-} +{-# OPTIONS_GHC -Wall -Werror #-} +module Database.MySQL.Simple.QueryParams.Generic + ( QueryParams(..) + ) where + +import Prelude () +import Database.MySQL.Simple.Prelude +import GHC.Generics + +import Database.MySQL.Simple.Param (Action(..), Param(..)) + +class QueryParams f 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..0e85913 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, CPP #-} -- | -- Module: Database.MySQL.Simpe.QueryResults @@ -20,12 +21,16 @@ 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 +#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. -- @@ -67,7 +72,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 +91,18 @@ class QueryResults a where -- This function will throw a 'ResultError' if conversion of the -- collection fails. + 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] + -> a + convertResults = Generic.convert + instance (Result a) => QueryResults (Only a) where convertResults [fa] [va] = Only a where !a = convert fa va @@ -378,27 +406,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..c8bcef7 --- /dev/null +++ b/Database/MySQL/Simple/QueryResults/Generic.hs @@ -0,0 +1,99 @@ +{-| Generic deriviation of 'Database.MySQL.Simple.QueryResults.QueryResults'. -} +{-# LANGUAGE + ScopedTypeVariables + , TypeOperators + , InstanceSigs + , FlexibleContexts + , AllowAmbiguousTypes + , CPP + #-} +{-# 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, 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 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 + 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 err xs ys = case zip xs ys of + [] -> U1 + _ -> throw err + +instance Result a => QueryResults (K1 i a) where + 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 + convertResults err xs ys = + M1 $ convertResults err xs ys + +instance (QueryResults a, QueryResults b) => QueryResults (a :*: b) where + 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 + -- 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 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 16e117e..ba2db8c 100644 --- a/Database/MySQL/Simple/Result.hs +++ b/Database/MySQL/Simple/Result.hs @@ -25,6 +25,8 @@ module Database.MySQL.Simple.Result ( Result(..) , ResultError(..) + , convertException + , convertError ) where #include "MachDeps.h" @@ -40,7 +42,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 +248,37 @@ 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. +-- +-- @since 0.4.7 +convertException :: [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. + -> 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` "[...]" + | otherwise = bs diff --git a/mysql-simple.cabal b/mysql-simple.cabal index 6c6ff67..a2ec75e 100644 --- a/mysql-simple.cabal +++ b/mysql-simple.cabal @@ -1,5 +1,5 @@ name: mysql-simple -version: 0.4.5 +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,11 +36,19 @@ 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 Database.MySQL.Simple.Types + other-modules: + Database.MySQL.Simple.Arity + Database.MySQL.Simple.Compat + Database.MySQL.Simple.Prelude + Database.MySQL.Simple.QueryParams.Generic + Database.MySQL.Simple.QueryResults.Generic + build-depends: attoparsec >= 0.10.0.0, base < 5, @@ -53,7 +61,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 @@ -76,6 +85,8 @@ test-suite test , hspec , mysql-simple , text + , mysql + , bytestring source-repository head type: git 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 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