packages feed

generic-persistence 0.6.0 → 0.7.0.0

raw patch · 23 files changed

+785/−492 lines, 23 filessetup-changedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Database.GP: [$sel:connection:Conn] :: Conn -> conn
- Database.GP: [$sel:implicitCommit:Conn] :: Conn -> Bool
- Database.GP: setupTableFor :: forall a. Entity a => Database -> Conn -> IO ()
- Database.GP.Conn: [$sel:connection:Conn] :: Conn -> conn
- Database.GP.Conn: [$sel:implicitCommit:Conn] :: Conn -> Bool
- Database.GP.Entity: [$sel:connection:Conn] :: Conn -> conn
- Database.GP.Entity: [$sel:implicitCommit:Conn] :: Conn -> Bool
- Database.GP.GenericPersistence: [$sel:connection:Conn] :: Conn -> conn
- Database.GP.GenericPersistence: [$sel:implicitCommit:Conn] :: Conn -> Bool
- Database.GP.GenericPersistence: setupTableFor :: forall a. Entity a => Database -> Conn -> IO ()
- Database.GP.GenericPersistenceSafe: [$sel:connection:Conn] :: Conn -> conn
- Database.GP.GenericPersistenceSafe: [$sel:implicitCommit:Conn] :: Conn -> Bool
- Database.GP.GenericPersistenceSafe: setupTableFor :: forall a. Entity a => Database -> Conn -> IO ()
- Database.GP.SqlGenerator: instance GHC.Classes.Eq Database.GP.SqlGenerator.Database
- Database.GP.SqlGenerator: instance GHC.Show.Show Database.GP.SqlGenerator.Database
+ Database.GP: count :: forall a. Entity a => Conn -> WhereClauseExpr -> IO Int
+ Database.GP: defaultPostgresMapping :: ColumnTypeMapping
+ Database.GP: defaultSqliteMapping :: ColumnTypeMapping
+ Database.GP: deleteById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO ()
+ Database.GP: deleteManyById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> [id] -> IO ()
+ Database.GP: setupTable :: forall a. Entity a => Conn -> ColumnTypeMapping -> IO ()
+ Database.GP: upsert :: forall a. Entity a => Conn -> a -> IO ()
+ Database.GP.GenericPersistence: count :: forall a. Entity a => Conn -> WhereClauseExpr -> IO Int
+ Database.GP.GenericPersistence: defaultPostgresMapping :: ColumnTypeMapping
+ Database.GP.GenericPersistence: defaultSqliteMapping :: ColumnTypeMapping
+ Database.GP.GenericPersistence: deleteById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO ()
+ Database.GP.GenericPersistence: deleteManyById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> [id] -> IO ()
+ Database.GP.GenericPersistence: setupTable :: forall a. Entity a => Conn -> ColumnTypeMapping -> IO ()
+ Database.GP.GenericPersistence: upsert :: forall a. Entity a => Conn -> a -> IO ()
+ Database.GP.GenericPersistenceSafe: count :: forall a. Entity a => Conn -> WhereClauseExpr -> IO (Either PersistenceException Int)
+ Database.GP.GenericPersistenceSafe: defaultPostgresMapping :: ColumnTypeMapping
+ Database.GP.GenericPersistenceSafe: defaultSqliteMapping :: ColumnTypeMapping
+ Database.GP.GenericPersistenceSafe: deleteById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO (Either PersistenceException ())
+ Database.GP.GenericPersistenceSafe: deleteManyById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> [id] -> IO (Either PersistenceException ())
+ Database.GP.GenericPersistenceSafe: removeAutoIncIdField :: forall a. Entity a => [SqlValue] -> [SqlValue]
+ Database.GP.GenericPersistenceSafe: setupTable :: forall a. Entity a => Conn -> ColumnTypeMapping -> IO ()
+ Database.GP.GenericPersistenceSafe: upsert :: forall a. Entity a => Conn -> a -> IO (Either PersistenceException ())
+ Database.GP.SqlGenerator: countStmtFor :: forall a. Entity a => WhereClauseExpr -> String
+ Database.GP.SqlGenerator: defaultPostgresMapping :: ColumnTypeMapping
+ Database.GP.SqlGenerator: defaultSqliteMapping :: ColumnTypeMapping
+ Database.GP.SqlGenerator: type ColumnTypeMapping = String -> String
+ Database.GP.SqlGenerator: upsertStmtFor :: forall a. Entity a => String
- Database.GP.SqlGenerator: columnTypeFor :: forall a. Entity a => Database -> String -> String
+ Database.GP.SqlGenerator: columnTypeFor :: forall a. Entity a => ColumnTypeMapping -> String -> String
- Database.GP.SqlGenerator: createTableStmtFor :: forall a. Entity a => Database -> String
+ Database.GP.SqlGenerator: createTableStmtFor :: forall a. Entity a => ColumnTypeMapping -> String

Files

README.md view
@@ -1,5 +1,6 @@ # GenericPersistence - A Haskell Persistence Layer using Generics +[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause) [![Actions Status](https://github.com/thma/generic-persistence/workflows/Haskell%20CI/badge.svg)](https://github.com/thma/generic-persistence/actions) [![codecov](https://codecov.io/gh/thma/generic-persistence/graph/badge.svg?token=DBCFLEA8JZ)](https://codecov.io/gh/thma/generic-persistence) [![Available on Hackage](https://img.shields.io/hackage/v/generic-persistence.svg?style=flat)](https://github.com/thma/generic-persistence)@@ -14,6 +15,7 @@ - [Status](#status) - [Available on Hackage](#available-on-hackage) - [Short demo](#short-demo)+- [Real world examples](#real-world-examples) - [Deal with runtime exceptions or use total functions? Your choice!](#deal-with-runtime-exceptions-or-use-total-functions-your-choice)   - [Exceptions in the default API](#exceptions-in-the-default-api)   - [Total functions in the safe API](#total-functions-in-the-safe-api)@@ -155,6 +157,31 @@   disconnect conn ``` +## Real world examples++To learn how to use the library in more complex scenarios, I recommend looking at the following examples:++### Building a REST service with Servant and GenericPersistence++[This example](https://github.com/thma/servant-gp) shows how to use servant to build a REST API that provides CRUD operations for a medium-complex data model.+GenericPersistence is used to execute the CRUD operation against a SQLite database.+A Swagger UI is provided to interact with the API.++### Building a REST service with Scotty and GenericPersistence+[This example](https://github.com/thma/scotty-gp-service) shows how to use Scotty to build a REST API that provides CRUD operations for a simple data model.+GenericPersistence is used to execute the CRUD operation against a SQLite database.+This example also demonstrate how a paging mechanism can be implemented with GenericPersistence.+The code also shows how to use GenericPersistence to manage BearerTokens for validating incoming requests.++### The Elephantine library review++The Elephantine library review provides a good overview of the different libraries available for working with PostgreSQL in Haskell. It also contains a section on *Generic-Persistence*:++[How to use PostgreSQL with Haskell. Elephantine Library Review 2023](https://github.com/Zelenya/elephants#generic-persistence)++In this review all libraries are compared by implementing the same real world application scenario with each library. The source code for *Generic-Persistence* can be found [here](https://github.com/Zelenya/elephants/blob/main/src/Elephants/GenericPersistence.hs).++ ## Deal with runtime exceptions or use total functions? Your choice!  GenericPersistence provides two different APIs for accessing the database:@@ -454,18 +481,18 @@   fromRow :: Conn -> [SqlValue] -> IO Article   fromRow conn row = do         authorById <- fromJust <$> selectById conn (row !! 2)  -- load author by foreign key-    return $ rawArticle {author = authorById}                -- add author to article+    return $ rawArticle {author = authorById}              -- add author to article     where-      rawArticle = Article (col 0) (col 1)                   -- create article from row, -                           (Author (col 2) "" "") (col 3)    -- using a dummy author+      rawArticle = Article (col 0) (col 1)                 -- create article from row, +                           (Author (col 2) "" "") (col 3)  -- using a dummy author         where           col i = fromSql (row !! i)    toRow :: Conn -> Article -> IO [SqlValue]   toRow conn a = do-    persist conn (author a)                                  -- persist author first-    return [toSql (articleID a), toSql (title a),            -- return row for article table where -            toSql $ authorID (author a), toSql (year a)]     -- authorID is foreign key to author table +    upsert conn (author a)                                  -- persist author first+    return [toSql (articleID a), toSql (title a),           -- return row for article table where +            toSql $ authorID (author a), toSql (year a)]    -- authorID is foreign key to author table  ```  Persisting the `Author`as a side effect in `toRow` may sound like an *interesting* idea...@@ -518,7 +545,7 @@    toRow :: Conn -> Author -> IO [SqlValue]   toRow conn a = do-    mapM_ (persist conn) (articles a)                     -- persist all articles of this author (update or insert)+    mapM_ (upsert conn) (articles a)                      -- persist all articles of this author (update or insert)     return [toSql (authorID a),                           -- return the author as a list of SqlValues             toSql (name a), toSql (address a)] ```@@ -683,3 +710,5 @@  You'll find a more complete example in the [servant-gp repo](https://github.com/thma/servant-gp/blob/main/src/ServerUtils.hs#L45). There I have set up a sample REST service based on Servant that uses *Generic-Persistence* and a connection pool to manage the database connections.++
Setup.hs view
@@ -1,2 +1,3 @@-import Distribution.Simple+import           Distribution.Simple+ main = defaultMain
generic-persistence.cabal view
@@ -1,12 +1,12 @@ cabal-version:      1.12 name:               generic-persistence-version:            0.6.0+version:            0.7.0.0 license:            BSD3 license-file:       LICENSE-copyright:          2023 Thomas Mahler+copyright:          2023,2024 Thomas Mahler maintainer:         thma@apache.org author:             Thomas Mahler-tested-with:        ghc ==9.2.5 ghc ==9.4.4+tested-with:        ghc ==9.2 ghc ==9.4 ghc ==9.6 ghc ==9.8 homepage:           https://github.com/thma/generic-persistence#readme bug-reports:        https://github.com/thma/generic-persistence/issues synopsis:           Database persistence using generics@@ -37,7 +37,7 @@     default-language:   GHC2021     default-extensions:         DuplicateRecordFields DeriveAnyClass DerivingStrategies-        OverloadedRecordDot TemplateHaskell QuasiQuotes+        OverloadedRecordDot TemplateHaskell QuasiQuotes LambdaCase      ghc-options:         -Wall -Wcompat -Widentities -Wincomplete-record-updates@@ -76,7 +76,7 @@     default-language:   GHC2021     default-extensions:         DuplicateRecordFields DeriveAnyClass DerivingStrategies-        OverloadedRecordDot TemplateHaskell QuasiQuotes+        OverloadedRecordDot TemplateHaskell QuasiQuotes LambdaCase      ghc-options:         -Wall -Wcompat -Widentities -Wincomplete-record-updates
src/Database/GP.hs view
@@ -1,19 +1,25 @@ module Database.GP   ( selectById,     select,+    count,     entitiesFromRows,     sql,     persist,+    upsert,     insert,     insertMany,     update,     updateMany,     delete,+    deleteById,     deleteMany,-    setupTableFor,-    Conn(..),+    deleteManyById,+    setupTable,+    defaultSqliteMapping,+    defaultPostgresMapping,+    Conn (..),     connect,-    Database(..),+    Database (..),     TxHandling (..),     ConnectionPool,     createConnPool,@@ -25,7 +31,7 @@     maybeFieldTypeFor,     TypeInfo (..),     typeInfo,-    PersistenceException(..),+    PersistenceException (..),     WhereClauseExpr,     Field,     field,@@ -49,7 +55,7 @@     SortOrder (..),     limit,     limitOffset,-    NonEmpty(..),+    NonEmpty (..),     SqlValue,     fromSql,     toSql,@@ -59,12 +65,13 @@     rollback,     withTransaction,     runRaw,-    disconnect+    disconnect,   ) where  -- We are just re-exporting from the GenericPersistence module. import           Database.GP.GenericPersistence-import           Database.HDBC (SqlValue, fromSql, toSql, quickQuery, run, -                                commit, rollback, withTransaction,-                                IConnection(runRaw, disconnect))+import           Database.HDBC                  (IConnection (disconnect, runRaw),+                                                 SqlValue, commit, fromSql,+                                                 quickQuery, rollback, run,+                                                 toSql, withTransaction)
src/Database/GP/Conn.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE LambdaCase                #-} module Database.GP.Conn   ( Conn (..),     connect,@@ -27,24 +26,18 @@ --  This module also defines a ConnectionPool type, which provides basic connection pooling functionality.  -- | A wrapper around an HDBC IConnection.-data Conn = forall conn.-  IConnection conn =>-  Conn-  { -    -- | If True, the GenericPersistence functions will commit the transaction after each operation.-    implicitCommit :: Bool,-    -- | The wrapped connection-    connection     :: conn-  }+data Conn = forall conn. IConnection conn => +  Conn +    Bool -- | If True, the GenericPersistence functions will commit the transaction after each operation.+    conn -- | The wrapped HDBC IConnection -data TxHandling = AutoCommit | ExplicitCommit  +data TxHandling = AutoCommit | ExplicitCommit  -- | a smart constructor for the Conn type.-connect :: forall conn. IConnection conn => TxHandling -> conn ->  Conn+connect :: forall conn. IConnection conn => TxHandling -> conn -> Conn connect = \case   AutoCommit     -> Conn True   ExplicitCommit -> Conn False-    -- | allows to execute a function that requires an `IConnection` argument on a `Conn`. withWConn :: forall b. Conn -> (forall conn. IConnection conn => conn -> b) -> b@@ -72,8 +65,9 @@ type ConnectionPool = Pool Conn  -- | Creates a connection pool.-createConnPool :: IConnection conn =>-  -- | the transaction mode +createConnPool ::+  IConnection conn =>+  -- | the transaction mode   TxHandling ->   -- | the connection string   String ->
src/Database/GP/Entity.hs view
@@ -13,8 +13,8 @@     GToRow,     GFromRow,     maybeFieldTypeFor,-    Conn(..),-    TxHandling(..),+    Conn (..),+    TxHandling (..),     maybeIdFieldIndex,     fieldIndex,   )@@ -23,36 +23,32 @@ import           Data.Char            (toLower) import           Data.Convertible import           Data.Kind+import           Data.List            (elemIndex) import           Data.Typeable        (Proxy (..), TypeRep)+import           Database.GP.Conn import           Database.GP.TypeInfo import           Database.HDBC        (SqlValue) import           GHC.Generics import           GHC.TypeNats-import           Database.GP.Conn-import Data.List (elemIndex) -{- | This is the Entity class. It is a type class that is used to define the mapping-between a Haskell product type in record notation and a database table.-The class has a default implementation for all methods.-The default implementation uses the type information to determine a simple 1:1 mapping.--That means that-- the type name is used as the table name and the-- field names are used as the column names.-- A field named '<lowercase typeName>ID' is used as the primary key field.--The default implementation can be overridden by defining a custom instance for a type.--Please note the following constraints, which apply to all valid Entity type,-but that are not explicitely encoded in the type class definition:--- The type must be a product type in record notation.-- The type must have exactly one constructor.-- There must be single primary key field, compund primary keys are not supported.---}--+-- | This is the Entity class. It is a type class that is used to define the mapping+-- between a Haskell product type in record notation and a database table.+-- The class has a default implementation for all methods.+-- The default implementation uses the type information to determine a simple 1:1 mapping.+--+-- That means that+-- - the type name is used as the table name and the+-- - field names are used as the column names.+-- - A field named '<lowercase typeName>ID' is used as the primary key field.+--+-- The default implementation can be overridden by defining a custom instance for a type.+--+-- Please note the following constraints, which apply to all valid Entity type,+-- but that are not explicitely encoded in the type class definition:+--+-- - The type must be a product type in record notation.+-- - The type must have exactly one constructor.+-- - There must be single primary key field, compound primary keys are not supported. class (Generic a, HasConstructor (Rep a), HasSelectors (Rep a)) => Entity a where   -- | Converts a database row to a value of type 'a'.   fromRow :: Conn -> [SqlValue] -> IO a
src/Database/GP/GenericPersistence.hs view
@@ -1,20 +1,27 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+ module Database.GP.GenericPersistence   ( selectById,     select,+    count,     entitiesFromRows,     sql,     persist,+    upsert,     insert,     insertMany,     update,     updateMany,     delete,+    deleteById,     deleteMany,-    setupTableFor,-    Conn(..),+    deleteManyById,+    setupTable,+    defaultSqliteMapping,+    defaultPostgresMapping,+    Conn (..),     connect,-    Database(..),+    Database (..),     TxHandling (..),     ConnectionPool,     createConnPool,@@ -26,7 +33,7 @@     maybeFieldTypeFor,     TypeInfo (..),     typeInfo,-    PersistenceException(..),+    PersistenceException (..),     WhereClauseExpr,     Field,     field,@@ -39,9 +46,9 @@     (<=.),     (<>.),     like,-    --contains,+    -- contains,     between,-    in',  +    in',     isNull,     not',     sqlFun,@@ -51,16 +58,16 @@     SortOrder (..),     limit,     limitOffset,-    NonEmpty(..)+    NonEmpty (..),   ) where  import           Control.Exception---import           Control.Monad                      (when) import           Data.Convertible                   (Convertible) import           Database.GP.Conn import           Database.GP.Entity-import           Database.GP.GenericPersistenceSafe (PersistenceException, sql, setupTableFor)+import           Database.GP.GenericPersistenceSafe (PersistenceException,+                                                     setupTable, sql) import qualified Database.GP.GenericPersistenceSafe as GpSafe import           Database.GP.SqlGenerator import           Database.GP.TypeInfo@@ -86,16 +93,6 @@     Left ex                        -> throw ex     Right entity                   -> pure $ Just entity --- | This function retrieves all entities of type `a` from a database.---  The function takes an HDBC connection as parameter.---  The type `a` is determined by the context of the function call.--- retrieveAll :: forall a. (Entity a) => Conn -> IO [a]--- retrieveAll conn = do---   eitherExRow <- GpSafe.retrieveAll @a conn---   case eitherExRow of---     Left ex    -> throw ex---     Right rows -> pure rows- -- | This function retrieves all entities of type `a` that match some query criteria. --   The function takes an HDBC connection and a `WhereClauseExpr` as parameters. --   The type `a` is determined by the context of the function call.@@ -108,6 +105,13 @@     Left ex        -> throw ex     Right entities -> pure entities +count :: forall a. (Entity a) => Conn -> WhereClauseExpr -> IO Int+count conn whereClause = do+  eitherExCount <- GpSafe.count @a conn whereClause+  case eitherExCount of+    Left ex   -> throw ex+    Right cnt -> pure cnt+ fromEitherExOrA :: IO (Either PersistenceException a) -> IO a fromEitherExOrA ioEitherExUnit = do   eitherExUnit <- ioEitherExUnit@@ -127,9 +131,18 @@ -- The function takes an HDBC connection and an entity as parameters. -- The entity is either inserted or updated, depending on whether it already exists in the database. -- The required SQL statements are generated dynamically using Haskell generics and reflection+-- deprecated: use upsert instead+{-# DEPRECATED persist "use upsert instead" #-} persist :: forall a. (Entity a) => Conn -> a -> IO ()-persist = (fromEitherExOrA .) . GpSafe.persist+persist = upsert +-- | A function that upserts an entity into a database.+-- The function takes an HDBC connection and an entity as parameters.+-- The entity is either inserted or updated, depending on whether it already exists in the database.+-- The required SQL statements are generated dynamically using Haskell generics and reflection+upsert :: forall a. (Entity a) => Conn -> a -> IO ()+upsert = (fromEitherExOrA .) . GpSafe.upsert+ -- | A function that explicitely inserts an entity into a database. insert :: forall a. (Entity a) => Conn -> a -> IO a insert = (fromEitherExOrA .) . GpSafe.insert@@ -155,16 +168,20 @@ delete :: forall a. (Entity a) => Conn -> a -> IO () delete = (fromEitherExOrA .) . GpSafe.delete +-- | A function that deletes an entity from a database.+--   The function takes an HDBC connection and an entity id as parameters.+deleteById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO ()+deleteById = (fromEitherExOrA .) . GpSafe.deleteById @a+ -- | A function that deletes a list of entities from a database. --   The function takes an HDBC connection and a list of entities as parameters. --   The delete-statement is compiled only once and then executed for each entity. deleteMany :: forall a. (Entity a) => Conn -> [a] -> IO () deleteMany = (fromEitherExOrA .) . GpSafe.deleteMany --- -- | set up a table for a given entity type. The table is dropped (if existing) and recreated.--- --   The function takes an HDBC connection as parameter.--- setupTableFor :: forall a. (Entity a) => Conn -> IO ()--- setupTableFor conn = do---   runRaw conn $ dropTableStmtFor @a---   runRaw conn $ createTableStmtFor @a (db conn)---   when (implicitCommit conn) $ commit conn+-- | A function that deletes a list of entities from a database.+--   The function takes an HDBC connection and a list of entity ids as parameters.+deleteManyById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> [id] -> IO ()+deleteManyById = (fromEitherExOrA .) . GpSafe.deleteManyById @a++
src/Database/GP/GenericPersistenceSafe.hs view
@@ -1,24 +1,28 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE DeriveAnyClass      #-}-{-# OPTIONS_GHC -Wno-orphans     #-}-{-# LANGUAGE LambdaCase          #-}+{-# OPTIONS_GHC -Wno-orphans #-}  module Database.GP.GenericPersistenceSafe   ( selectById,     select,+    count,     entitiesFromRows,     sql,     persist,+    upsert,     insert,     insertMany,     update,     updateMany,     delete,+    deleteById,     deleteMany,-    setupTableFor,-    Conn(..),+    deleteManyById,+    setupTable,+    defaultSqliteMapping,+    defaultPostgresMapping,+    Conn (..),     connect,-    Database(..),+    Database (..),     TxHandling (..),     ConnectionPool,     createConnPool,@@ -30,7 +34,7 @@     maybeFieldTypeFor,     TypeInfo (..),     typeInfo,-    PersistenceException(..),+    PersistenceException (..),     WhereClauseExpr,     Field,     field,@@ -55,36 +59,36 @@     limit,     limitOffset,     fieldIndex,-    handleDuplicateInsert+    handleDuplicateInsert,+    removeAutoIncIdField,   ) where -import           Control.Exception        (Exception, SomeException, try)-import           Control.Monad            (when)-import           Data.Convertible         (ConvertResult, Convertible)-import           Data.Convertible.Base    (Convertible (safeConvert))-import           Data.List                (isInfixOf)+import           Control.Exception         (Exception, SomeException, try)+import           Control.Monad             (when)+import           Data.Convertible          (ConvertResult, Convertible)+import           Data.Convertible.Base     (Convertible (safeConvert))+import           Data.List                 (isInfixOf) import           Database.GP.Conn import           Database.GP.Entity import           Database.GP.SqlGenerator import           Database.GP.TypeInfo import           Database.HDBC-import           Text.RawString.QQ import           Language.Haskell.TH.Quote (QuasiQuoter)--{- |- This is the "safe" version of the module Database.GP.GenericPersistence. It uses Either to return errors.-- This module defines RDBMS Persistence operations for Record Data Types that are instances of 'Data'.- I call instances of such a data type Entities.+import           Text.RawString.QQ         (r) - The Persistence operations are using Haskell generics to provide compile time reflection capabilities.- HDBC is used to access the RDBMS.--}+-- |+-- This is the "safe" version of the module Database.GP.GenericPersistence. It uses Either to return errors.+--+-- This module defines RDBMS Persistence operations for Record Data Types that are instances of 'Data'.+-- I call instances of such a data type Entities.+--+-- The Persistence operations are using Haskell generics to provide compile time reflection capabilities.+-- HDBC is used to access the RDBMS.  -- | exceptions that may occur during persistence operations-data PersistenceException =-    EntityNotFound String+data PersistenceException+  = EntityNotFound String   | DuplicateInsert String   | DatabaseError String   | NoUniqueKey String@@ -97,7 +101,7 @@ -- An error is thrown if there are more than one entity with the given id. selectById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO (Either PersistenceException a) selectById conn idx = do-  --print stmt+  -- print stmt   eitherExResultRows <- try $ quickQuery conn stmt [eid]   case eitherExResultRows of     Left ex -> return $ Left $ fromException ex@@ -118,15 +122,14 @@ fromException :: SomeException -> PersistenceException fromException ex = DatabaseError $ show ex - -- | This function retrieves all entities of type `a` that match some query criteria. --   The function takes an HDBC connection and a `WhereClauseExpr` as parameters. --   The type `a` is determined by the context of the function call. --   The function returns a (possibly empty) list of all matching entities.---   The `WhereClauseExpr` is typically constructed using any tiny query dsl based on infix operators.+--   The `WhereClauseExpr` is typically constructed using a tiny query dsl based on infix operators. select :: forall a. (Entity a) => Conn -> WhereClauseExpr -> IO (Either PersistenceException [a]) select conn whereClause = do-  --print stmt+  -- print stmt   eitherExRows <- tryPE $ quickQuery conn stmt values   case eitherExRows of     Left ex          -> return $ Left ex@@ -135,6 +138,21 @@     stmt = selectFromStmt @a whereClause     values = whereClauseValues whereClause +-- | This function retrieves the number of entities of type `a` that match some query criteria.+--   The function takes an HDBC connection and a `WhereClauseExpr` as parameters.+--   The type `a` is determined by the context of the function call.+--   The function returns the number of all matching entities.+--   The `WhereClauseExpr` is typically constructed using a tiny query dsl based on infix operators.+count :: forall a. (Entity a) => Conn -> WhereClauseExpr -> IO (Either PersistenceException Int)+count conn whereClause = do+  eitherExRows <- tryPE $ quickQuery conn stmt values+  case eitherExRows of+    Left ex          -> return $ Left ex+    Right resultRows -> return $ Right $ fromSql $ head $ head resultRows -- using head twice is safe here+  where+    stmt = countStmtFor @a whereClause+    values = whereClauseValues whereClause+ -- | This function converts a list of database rows, represented as a `[[SqlValue]]` to a list of entities. --   The function takes an HDBC connection and a list of database rows as parameters. --   The type `a` is determined by the context of the function call.@@ -148,39 +166,49 @@ -- The function takes an HDBC connection and an entity as parameters. -- The entity is either inserted or updated, depending on whether it already exists in the database. -- The required SQL statements are generated dynamically using Haskell generics and reflection+upsert :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException ())+upsert conn entity = do+  eitherExOrA <- try $ do+    row <- toRow conn entity+    _ <- quickQuery' conn (upsertStmtFor @a) (row <> row)+    commitIfAutoCommit conn+  case eitherExOrA of+    Left ex -> return $ Left $ fromException ex+    Right _ -> return $ Right ()++-- | A function that persists an entity to a database.+-- The function takes an HDBC connection and an entity as parameters.+-- The entity is either inserted or updated, depending on whether it already exists in the database.+-- The required SQL statements are generated dynamically using Haskell generics and reflection+-- deprecated: use upsert instead+{-# DEPRECATED persist "use upsert instead" #-} persist :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException ())-persist conn entity = do-  eitherExRes <- try $ do-    eid <- idValue conn entity-    let stmt = selectFromStmt @a byIdColumn-    quickQuery conn stmt [eid] >>=-      \case-        []           -> insert conn entity >> return (Right ())-        [_singleRow] -> update conn entity-        _            -> return $ Left $ NoUniqueKey $ "More than one entity found for id " ++ show eid-  case eitherExRes of-    Left ex   -> return $ Left $ fromException ex-    Right res -> return res+persist = upsert + -- | A function that commits a transaction if the connection is in auto commit mode. --   The function takes an HDBC connection as parameter. commitIfAutoCommit :: Conn -> IO ()-commitIfAutoCommit conn = when (implicitCommit conn) $ commit conn+commitIfAutoCommit (Conn autoCommit conn) = when autoCommit $ commit conn  -- | A function that explicitely inserts an entity into a database.+--   The function takes an HDBC connection and an entity as parameters.+--   The entity, as retrieved from the database, is returned as a Right value if the entity was successfully inserted.+--   (this allows to handle automatically updated fields like auto-incremented primary keys)+--   If the entity could not be inserted, a Left value is returned, containing a PersistenceException. insert :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException a) insert conn entity = do   eitherExOrA <- try $ do     row <- toRow conn entity-    [singleRow] <- quickQuery' conn (insertReturningStmtFor @a) (removeIdField @a row)+    [singleRow] <- quickQuery' conn (insertReturningStmtFor @a) (removeAutoIncIdField @a row)     commitIfAutoCommit conn     fromRow @a conn singleRow   case eitherExOrA of     Left ex -> return $ Left $ handleDuplicateInsert ex-    Right a -> return $ Right a    +    Right a -> return $ Right a -removeIdField :: forall a. (Entity a) => [SqlValue] -> [SqlValue]-removeIdField row =+removeAutoIncIdField :: forall a. (Entity a) => [SqlValue] -> [SqlValue]+removeAutoIncIdField row =   if autoIncrement @a     then case maybeIdFieldIndex @a of       Nothing      -> row@@ -188,9 +216,11 @@     else row  handleDuplicateInsert :: SomeException -> PersistenceException-handleDuplicateInsert ex = -  if "UNIQUE constraint failed" `isInfixOf` show ex ||-     "duplicate key value violates unique constraint" `isInfixOf` show ex+handleDuplicateInsert ex =+  if "UNIQUE constraint failed"+    `isInfixOf` show ex+    || "duplicate key value violates unique constraint"+    `isInfixOf` show ex     then DuplicateInsert "Entity already exists in DB, use update instead"     else fromException ex @@ -209,14 +239,14 @@   eitherExUnit <- try $ do     rows <- mapM (toRow conn) entities     stmt <- prepare conn (insertStmtFor @a)-    executeMany stmt (map (removeIdField @a) rows)+    executeMany stmt (map (removeAutoIncIdField @a) rows)     commitIfAutoCommit conn   case eitherExUnit of     Left ex -> return $ Left $ handleDuplicateInsert ex     Right _ -> return $ Right () - -- | A function that explicitely updates an entity in a database.+--  The function takes an HDBC connection and an entity as parameters. update :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException ()) update conn entity = do   eitherExUnit <- try $ do@@ -260,6 +290,30 @@     Left ex      -> return $ Left $ fromException ex     Right result -> return result +-- | A function that deletes an entity from a database by its id.+--   The function takes an HDBC connection and an entity id as parameters.+deleteById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> id -> IO (Either PersistenceException ())+deleteById conn idx = do+  eitherExRes <- try $ do+    let eid = toSql idx+    rowCount <- run conn (deleteStmtFor @a) [eid]+    if rowCount == 0+      then return (Left (EntityNotFound (constructorName (typeInfo @a) ++ " " ++ show eid ++ " does not exist")))+      else do+        commitIfAutoCommit conn+        return $ Right ()+  case eitherExRes of+    Left ex      -> return $ Left $ fromException ex+    Right result -> return result++-- | A function that deletes a list of entities from a database by their ids.+--   The function takes an HDBC connection and a list of entity ids as parameters.+deleteManyById :: forall a id. (Entity a, Convertible id SqlValue) => Conn -> [id] -> IO (Either PersistenceException ())+deleteManyById conn ids = tryPE $ do+  stmt <- prepare conn (deleteStmtFor @a)+  executeMany stmt (map ((: []) . toSql) ids)+  commitIfAutoCommit conn+ -- | A function that deletes a list of entities from a database. --   The function takes an HDBC connection and a list of entities as parameters. --   The delete-statement is compiled only once and then executed for each entity.@@ -271,15 +325,12 @@   commitIfAutoCommit conn  -- | set up a table for a given entity type. The table is dropped (if existing) and recreated.---   The function takes an HDBC connection as parameter.-setupTableFor :: forall a. (Entity a) => Database -> Conn -> IO ()-setupTableFor db conn = do-  --print stmt+--   The function takes an HDBC connection and a column type mapping as parameters.+setupTable :: forall a. (Entity a) => Conn -> ColumnTypeMapping -> IO ()+setupTable conn mapping = do   runRaw conn $ dropTableStmtFor @a-  runRaw conn $ stmt -- createTableStmtFor @a (db conn)+  runRaw conn $ createTableStmtFor @a mapping   commitIfAutoCommit conn-  where-    stmt = createTableStmtFor @a db   -- | A function that returns the primary key value of an entity as a SqlValue. --   The function takes an HDBC connection and an entity as parameters.@@ -290,7 +341,7 @@   where     idFieldIndex = fieldIndex @a (idField @a) --- an alias for a simple quasiqouter+-- | an alias for a simple quasiqouter sql :: QuasiQuoter sql = r 
src/Database/GP/Query.hs view
@@ -29,7 +29,7 @@     SortOrder (..),     limit,     limitOffset,-    NonEmpty(..)+    NonEmpty (..),   ) where @@ -45,12 +45,12 @@  import           Data.Convertible   (Convertible) import           Data.List          (intercalate)+import           Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE import           Database.GP.Entity (Entity, columnNameFor, idField) import           Database.HDBC      (SqlValue, toSql)-import qualified Data.List.NonEmpty as NE-import           Data.List.NonEmpty (NonEmpty(..)) -data CompareOp = Eq | Gt | Lt | GtEq | LtEq | NotEq | Like +data CompareOp = Eq | Gt | Lt | GtEq | LtEq | NotEq | Like  data Field = Field [String] String @@ -69,7 +69,7 @@   | Limit WhereClauseExpr Int   | LimitOffset WhereClauseExpr Int Int -data SortOrder = ASC | DESC +data SortOrder = ASC | DESC   deriving (Show)  field :: String -> Field@@ -85,7 +85,7 @@ (||.) :: WhereClauseExpr -> WhereClauseExpr -> WhereClauseExpr (||.) = Or -infixl 4 =., >., <., >=., <=., <>., `like`, `between`, `in'` +infixl 4 =., >., <., >=., <=., <>., `like`, `between`, `in'`  (=.), (>.), (<.), (>=.), (<=.), (<>.), like :: (Convertible b SqlValue) => Field -> b -> WhereClauseExpr a =. b = Where a Eq (toSql b)@@ -126,12 +126,11 @@ orderBy = OrderBy  limit :: WhereClauseExpr -> Int -> WhereClauseExpr-limit = Limit +limit = Limit  limitOffset :: WhereClauseExpr -> (Int, Int) -> WhereClauseExpr limitOffset c (offset, lim) = LimitOffset c offset lim - whereClauseExprToSql :: forall a. (Entity a) => WhereClauseExpr -> String whereClauseExprToSql (Where f op _) = columnToSql @a f ++ " " ++ opToSql op ++ " ?" whereClauseExprToSql (And e1 e2) = "(" ++ whereClauseExprToSql @a e1 ++ ") AND (" ++ whereClauseExprToSql @a e2 ++ ")"@@ -145,22 +144,22 @@ whereClauseExprToSql (WhereIsNull f) = columnToSql @a f ++ " IS NULL" whereClauseExprToSql All = "1=1" whereClauseExprToSql (ById _eid) = idColumn @a ++ " = ?"-whereClauseExprToSql ByIdColumn  = idColumn @a ++ " = ?"+whereClauseExprToSql ByIdColumn = idColumn @a ++ " = ?" whereClauseExprToSql (OrderBy clause pairs) = whereClauseExprToSql @a clause ++ " ORDER BY " ++ renderedPairs pairs   where     renderedPairs :: NonEmpty (Field, SortOrder) -> String-    renderedPairs ne = intercalate ", " (NE.toList (NE.map (\(f,order) -> columnToSql @a f ++ " " ++ show order) ne))+    renderedPairs ne = intercalate ", " (NE.toList (NE.map (\(f, order) -> columnToSql @a f ++ " " ++ show order) ne)) whereClauseExprToSql (Limit clause x) = whereClauseExprToSql @a clause ++ " LIMIT " ++ show x-whereClauseExprToSql (LimitOffset clause offset lim) = whereClauseExprToSql @a clause ++ " LIMIT " ++ show lim ++ " OFFSET " ++ show offset -    +whereClauseExprToSql (LimitOffset clause offset lim) = whereClauseExprToSql @a clause ++ " LIMIT " ++ show lim ++ " OFFSET " ++ show offset+ opToSql :: CompareOp -> String-opToSql Eq       = "="-opToSql Gt       = ">"-opToSql Lt       = "<"-opToSql GtEq     = ">="-opToSql LtEq     = "<="-opToSql NotEq    = "<>"-opToSql Like     = "LIKE"+opToSql Eq    = "="+opToSql Gt    = ">"+opToSql Lt    = "<"+opToSql GtEq  = ">="+opToSql LtEq  = "<="+opToSql NotEq = "<>"+opToSql Like  = "LIKE"  columnToSql :: forall a. (Entity a) => Field -> String columnToSql = expandFunctions @a@@ -168,7 +167,7 @@ idColumn :: forall a. (Entity a) => String idColumn = columnNameFor @a (idField @a) -expandFunctions :: forall a. (Entity a) => Field -> String -- -> String+expandFunctions :: forall a. (Entity a) => Field -> String expandFunctions (Field [] name) = columnNameFor @a name expandFunctions (Field (f : fs) name) = f ++ "(" ++ expandFunctions @a (Field fs name) ++ ")" 
src/Database/GP/SqlGenerator.hs view
@@ -1,11 +1,12 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE LambdaCase          #-}  module Database.GP.SqlGenerator   ( insertStmtFor,     insertReturningStmtFor,     updateStmtFor,+    upsertStmtFor,     selectFromStmt,+    countStmtFor,     deleteStmtFor,     createTableStmtFor,     dropTableStmtFor,@@ -35,14 +36,17 @@     SortOrder (..),     limit,     limitOffset,-    NonEmpty(..),+    NonEmpty (..),     Database (..),+    defaultSqliteMapping,+    defaultPostgresMapping,+    ColumnTypeMapping,   ) where  import           Data.List          (intercalate)-import Database.GP.Entity-import Database.GP.Query+import           Database.GP.Entity+import           Database.GP.Query  -- | --  This module defines some basic SQL statements for Record Data Types that are instances of 'Entity'.@@ -57,34 +61,52 @@   "INSERT INTO "     ++ tableName @a     ++ " ("-    ++ intercalate ", " insertColumns+    ++ intercalate ", " insertCols     ++ ") VALUES ("-    ++ intercalate ", " (params (length insertColumns))+    ++ intercalate ", " (params (length insertCols))     ++ ");"   where+    insertCols = insertColumns @a++upsertStmtFor :: forall a. Entity a => String+upsertStmtFor =+  "INSERT INTO "+    ++ tableName @a+    ++ " ("+    ++ intercalate ", " insertCols+    ++ ") VALUES ("+    ++ intercalate ", " (params (length insertCols))+    ++ ") ON CONFLICT ("+    ++ idColumn @a+    ++ ") DO UPDATE SET "+    ++ intercalate ", " updatePairs+    ++ ";"+  where+    insertCols = columnNamesFor @a+    updatePairs = map (++ " = ?") insertCols++insertColumns :: forall a. Entity a => [String]+insertColumns = +  if autoIncrement @a+    then filter (/= idColumn @a) columns+    else columns +  where     columns = columnNamesFor @a-    insertColumns = if autoIncrement @a -                  then filter (/= idColumn @a) columns -                  else columns  insertReturningStmtFor :: forall a. Entity a => String insertReturningStmtFor =   "INSERT INTO "     ++ tableName @a     ++ " ("-    ++ intercalate ", " insertColumns+    ++ intercalate ", " insertCols     ++ ") VALUES ("-    ++ intercalate ", " (params (length insertColumns))+    ++ intercalate ", " (params (length insertCols))     ++ ") RETURNING "-    ++ returnColumns+    ++ returnCols     ++ ";"   where-    columns = columnNamesFor @a  -    insertColumns = if autoIncrement @a -                      then filter (/= idColumn @a) columns -                      else columns-    returnColumns = intercalate ", " columns-+    insertCols = insertColumns @a+    returnCols = intercalate ", " (columnNamesFor @a)  columnNamesFor :: forall a. Entity a => [String] columnNamesFor = map snd fieldColumnPairs@@ -117,6 +139,17 @@     ++ whereClauseExprToSql @a whereClauseExpr     ++ ";" +-- | A function that returns an SQL count statement for an entity. Type 'a' must be an instance of Entity.+--   The function takes a where clause expression as parameter. This expression is used to filter the result set.+countStmtFor :: forall a. (Entity a) => WhereClauseExpr -> String+countStmtFor whereClauseExpr =+  "SELECT COUNT(*) FROM "+    ++ tableName @a+    ++ " WHERE "+    ++ whereClauseExprToSql @a whereClauseExpr+    ++ ";"++-- | A function that returns an SQL delete statement for an entity. Type 'a' must be an instance of Entity. deleteStmtFor :: forall a. (Entity a) => String deleteStmtFor =   "DELETE FROM "@@ -125,58 +158,63 @@     ++ idColumn @a     ++ " = ?;" --- | An enumeration of the supported database types.-data Database = Postgres | SQLite -- | Oracle | MSSQL | MySQL-  deriving (Show, Eq)+-- | An enumeration of the supported database types. +data Database = Postgres | SQLite -createTableStmtFor :: forall a. (Entity a) => Database -> String-createTableStmtFor dbServer =+-- | A function that returns an SQL create table statement for an entity type. Type 'a' must be an instance of Entity.+createTableStmtFor :: forall a. (Entity a) => ColumnTypeMapping -> String+createTableStmtFor columnTypeMapping =   "CREATE TABLE "     ++ tableName @a     ++ " ("-    ++ intercalate ", " (map (\(f, c) -> c ++ " " ++ columnTypeFor @a dbServer f ++ optionalPK f) (fieldsToColumns @a))+    ++ intercalate ", " (map (\(f, c) -> c ++ " " ++ columnTypeFor @a columnTypeMapping f ++ optionalPK f) (fieldsToColumns @a))     ++ ");"   where-    optionalPK f = if isIdField @a f -                    then case dbServer of-                      SQLite   -> " PRIMARY KEY AUTOINCREMENT"-                      Postgres -> " PRIMARY KEY"-                    else ""+    optionalPK f = if isIdField @a f then " PRIMARY KEY" else ""  isIdField :: forall a. (Entity a) => String -> Bool isIdField f = f == idField @a  -- | A function that returns the column type for a field of an entity.--- TODO: Support other databases than just SQLite and Postgres.-columnTypeFor :: forall a. (Entity a) => Database -> String -> String-columnTypeFor dbDialect fieldName =-  case dbDialect of-    SQLite   -> columnTypeForSQLite fType-    Postgres -> if isIdField @a fieldName -                  then "serial"-                  else columnTypeForPostgres fType+--   The function takes a column type mapping function as parameter.+--   This function is used to map Haskell field types to SQL column types.+columnTypeFor :: forall a. (Entity a) => ColumnTypeMapping -> String -> String+columnTypeFor columnTypeMapping fieldName = columnTypeMapping fType   where-    maybeFType = maybeFieldTypeFor @a fieldName-    fType = maybe "OTHER" show maybeFType-  -    columnTypeForSQLite :: String -> String-    columnTypeForSQLite = \case  -        "Int"    -> "INTEGER"-        "[Char]" -> "TEXT"-        "Double" -> "REAL"-        "Float"  -> "REAL"-        "Bool"   -> "INT"-        _        -> "TEXT"+    fType = +      if isIdField @a fieldName +        then "primaryKey"+        else maybe "OTHER" show $ maybeFieldTypeFor @a fieldName -    columnTypeForPostgres :: String -> String-    columnTypeForPostgres = \case  -        "Int"    -> "numeric"-        "[Char]" -> "varchar"-        "Double" -> "numeric"-        "Float"  -> "numeric"-        "Bool"   -> "boolean"-        _        -> "varchar"+-- | A type alias for mapping a Haskell field type to a SQL column type.+--   this type can be used to define custom mappings for field types.+type ColumnTypeMapping = String -> String +-- | The default mapping for SQLite databases.+--   This mapping is used when no custom mapping is provided.+defaultSqliteMapping :: ColumnTypeMapping+defaultSqliteMapping = \case+  "primaryKey" -> "INTEGER"+  "Int"    -> "INTEGER"+  "[Char]" -> "TEXT"+  "Double" -> "REAL"+  "Float"  -> "REAL"+  "Bool"   -> "INT"+  _        -> "TEXT"++-- | The default mapping for Postgres databases.+--   This mapping is used when no custom mapping is provided.+defaultPostgresMapping :: ColumnTypeMapping+defaultPostgresMapping = \case+  "primaryKey" -> "serial"+  "Int"    -> "numeric"+  "[Char]" -> "varchar"+  "Double" -> "numeric"+  "Float"  -> "numeric"+  "Bool"   -> "boolean"+  _        -> "varchar"++-- | This function generates a DROP TABLE statement for an entity type. dropTableStmtFor :: forall a. (Entity a) => String dropTableStmtFor =   "DROP TABLE IF EXISTS "
src/Database/GP/TypeInfo.hs view
@@ -34,7 +34,7 @@     { constructorName = gConstrName (undefined :: a),       fieldNames = map fst (gSelectors (undefined :: a)),       fieldTypes = map snd (gSelectors (undefined :: a))-    } +    }  -- Generic implementations gConstrName :: (HasConstructor (Rep a), Generic a) => a -> String
test/ConnSpec.hs view
@@ -1,6 +1,3 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}- module ConnSpec   ( test,     spec,@@ -8,13 +5,13 @@ where  import           Database.GP+import           Database.HDBC import           Database.HDBC.Sqlite3 import           GHC.Generics import           Test.Hspec-import Database.HDBC  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -22,7 +19,7 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn+  setupTable @Article conn defaultSqliteMapping   return conn  data Article = Article@@ -36,9 +33,9 @@ spec = do   describe "Connection Handling" $ do     it "can work with embedded connection" $ do-      Conn{implicitCommit=ic, connection=conn} <- prepareDB+      (Conn ic conn) <- prepareDB       ic `shouldBe` True-      +       runRaw conn "DROP TABLE IF EXISTS Person;"       let conn' = connect AutoCommit conn @@ -47,9 +44,9 @@      it "can handle rollback" $ do       conn <- prepareDB-      let conn' = conn{implicitCommit=False}+      let conn' = connect ExplicitCommit conn       let article = Article 1 "Hello" 2023-      +       _ <- insert conn' article       allArticles <- select conn' allEntries :: IO [Article]       allArticles `shouldBe` [article]@@ -59,10 +56,10 @@      it "provide the IConnection methods" $ do       conn <- prepareDB-      allTables <- getTables conn +      allTables <- getTables conn       allTables `shouldContain` ["Article"] -      desc <- describeTable conn "Article" +      desc <- describeTable conn "Article"       length desc `shouldBe` 3        let driverName = hdbcDriverName conn@@ -83,14 +80,6 @@       let txSupport = dbTransactionSupport conn       txSupport `shouldBe` True -      clonedConn <- clone conn-      implicitCommit clonedConn `shouldBe` implicitCommit conn-----      -      -      --+      let (Conn autoCommit _) = conn+      (Conn clonedAutoCommit _) <- clone conn+      clonedAutoCommit `shouldBe` autoCommit
test/DemoSpec.hs view
@@ -9,8 +9,8 @@ import           Database.GP import           Database.HDBC.Sqlite3 (connectSqlite3) import           GHC.Generics-import           Test.Hspec import           Prelude               hiding (print)+import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is -- not needed for automatic spec discovery.@@ -41,7 +41,7 @@       conn <- connect AutoCommit <$> connectSqlite3 "sqlite.db"        -- initialize Person table-      setupTableFor @Person SQLite conn+      setupTable @Person conn defaultSqliteMapping        alice <- insert conn Person {name = "Alice", age = 25, address = "Elmstreet 1"}       print alice
test/EmbeddedSpec.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-}- module EmbeddedSpec   ( test,     spec,@@ -13,7 +11,7 @@ import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -21,7 +19,7 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn+  setupTable @Article conn defaultSqliteMapping   return conn  data Article = Article
test/EnumSpec.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-}- module EnumSpec   ( test,     spec,@@ -14,7 +12,7 @@ import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -22,7 +20,7 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Book SQLite conn+  setupTable @Book conn defaultSqliteMapping   return conn  data Book = Book
test/ExceptionsSpec.hs view
@@ -1,21 +1,18 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}- module ExceptionsSpec   ( test,     spec,   ) where +import           Control.Exception import           Database.GP.GenericPersistenceSafe+import           Database.HDBC import           Database.HDBC.Sqlite3 import           GHC.Generics import           Test.Hspec-import           Database.HDBC-import           Control.Exception  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -23,7 +20,7 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn+  setupTable @Article conn defaultSqliteMapping   return conn  data Article = Article@@ -35,20 +32,21 @@  instance Entity Article where   autoIncrement = False+  idField = "articleID"  data Bogus = Bogus-  { bogusID :: Int,+  { bogusID    :: Int,     bogusTitle :: String,-    bogusYear :: Int+    bogusYear  :: Int   }   deriving (Generic, Show, Eq)  instance Entity Bogus where   tableName = "Article" --  fieldsToColumns :: [(String, String)]                  -- ommitting the articles field, -  fieldsToColumns =                                      -- as this can not be mapped to a single column+  fieldsToColumns :: [(String, String)] -- ommitting the articles field,+  fieldsToColumns =+    -- as this can not be mapped to a single column     [ ("bogusID", "articleID"),       ("bogusTitle", "title"),       ("bogusYear", "year")@@ -57,7 +55,6 @@   fromRow :: Conn -> [SqlValue] -> IO Bogus   fromRow _conn _row = throw $ DatabaseError "can't create bogus" - yearField :: Field yearField = field "year" @@ -76,39 +73,35 @@       eitherExRes <- insert conn article :: IO (Either PersistenceException Article)       case eitherExRes of         Left di@(DuplicateInsert _msg) -> show di `shouldContain` "Entity already exists"-        _                          -> expectationFailure "Expected DuplicateInsert exception"+        _ -> expectationFailure "Expected DuplicateInsert exception"     it "detects duplicate inserts in insertMany" $ do       conn <- prepareDB       _ <- insert conn article-      eitherExRes <- insertMany conn [article,article] :: IO (Either PersistenceException ())+      eitherExRes <- insertMany conn [article, article] :: IO (Either PersistenceException ())       case eitherExRes of         Left (DuplicateInsert msg) -> msg `shouldContain` "Entity already exists"-        _                          -> expectationFailure "Expected DuplicateInsert exception"-    it "detects duplicate inserts in persist" $ do-      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-      runRaw conn "CREATE TABLE article (articleID INTEGER, title TEXT, year INTEGER)"-      _ <- insert conn article-      _ <- insert conn article{title="another title"}-      eitherExRes <- persist conn article :: IO (Either PersistenceException ())+        _ -> expectationFailure "Expected DuplicateInsert exception"+    it "returns () for successful upsert" $ do+      conn <- prepareDB+      eitherExRes <- upsert conn article :: IO (Either PersistenceException ())       case eitherExRes of-        Left (NoUniqueKey msg) -> msg `shouldContain` "More than one entity found for id SqlInt64 1"-        Left pe                -> expectationFailure $ "Expected NoUniqueKey exception, got " ++ show pe-        _                      -> expectationFailure "Expected NoUniqueKey exception"+        Right () -> expectationSuccess+        _        -> expectationFailure "Expected ()"     it "detects missing entities in selectById" $ do       conn <- prepareDB       eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Article)       case eitherExRes of         Left (EntityNotFound msg) -> msg `shouldContain` "not found"-        _                         -> expectationFailure "Expected EntityNotFound exception"+        _ -> expectationFailure "Expected EntityNotFound exception"     it "detects non unique entities in selectById" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       runRaw conn "CREATE TABLE article (articleID INTEGER, title TEXT, year INTEGER)"       _ <- insert conn article-      _ <- insert conn article{title="another title"}+      _ <- insert conn article {title = "another title"}       eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Article)       case eitherExRes of         Left (NoUniqueKey msg) -> msg `shouldContain` "More than one Article found for id SqlString \"1\""-        _                      -> expectationFailure "Expected DuplicateEntity exception"+        _ -> expectationFailure "Expected DuplicateEntity exception"      it "detects bogus data in selectById" $ do       conn <- prepareDB@@ -116,36 +109,48 @@       eitherExRes <- selectById conn "1" :: IO (Either PersistenceException Bogus)       case eitherExRes of         Left (DatabaseError msg) -> msg `shouldContain` "can't create bogus"-        Left pe                  -> expectationFailure $ "Expected DatabaseError exception, got " ++ show pe-        _                        -> expectationFailure "Expected DatabaseError exception"+        Left pe -> expectationFailure $ "Expected DatabaseError exception, got " ++ show pe+        _ -> expectationFailure "Expected DatabaseError exception"      it "detects missing entities in update" $ do       conn <- prepareDB       eitherExRes <- update conn article :: IO (Either PersistenceException ())       case eitherExRes of         Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"-        _                         -> expectationFailure "Expected EntityNotFound exception"+        _ -> expectationFailure "Expected EntityNotFound exception"     it "detects missing entities in delete" $ do       conn <- prepareDB       eitherExRes <- delete conn article :: IO (Either PersistenceException ())       case eitherExRes of         Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"-        _                         -> expectationFailure "Right: Expected EntityNotFound exception"+        _ -> expectationFailure "Right: Expected EntityNotFound exception"+    it "detects missing entities in deleteById" $ do+      conn <- prepareDB+      eitherExRes <- deleteById @Article conn "1" :: IO (Either PersistenceException ())+      case eitherExRes of+        Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"+        _ -> expectationFailure "Expected EntityNotFound exception"+    it "detects general db issues in deleteById" $ do+      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"+      eitherExRes <- deleteById @Article conn "1" :: IO (Either PersistenceException ())+      case eitherExRes of+        Left (DatabaseError msg) -> msg `shouldContain` "SqlError"+        _ -> expectationFailure "Expected DatabaseError exception"     it "detects general backend issues" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       eitherExRes <- update conn article :: IO (Either PersistenceException ())       case eitherExRes of         Left (DatabaseError msg) -> msg `shouldContain` "SqlError"-        _                        -> expectationFailure "Expected DatabaseError exception"+        _ -> expectationFailure "Expected DatabaseError exception"     it "has no leaking backend exceptions" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       _ <- update conn article :: IO (Either PersistenceException ())       _ <- insert conn article :: IO (Either PersistenceException Article)-      _ <- persist conn article :: IO (Either PersistenceException ())+      _ <- upsert conn article :: IO (Either PersistenceException ())       _ <- delete conn article :: IO (Either PersistenceException ())       _ <- selectById conn "1" :: IO (Either PersistenceException Article)       _ <- select conn allEntries :: IO (Either PersistenceException [Article])-      _ <- select conn (yearField =. "2023")  :: IO (Either PersistenceException [Article])+      _ <- select conn (yearField =. "2023") :: IO (Either PersistenceException [Article])       _ <- insertMany conn [article] :: IO (Either PersistenceException ())       _ <- updateMany conn [article] :: IO (Either PersistenceException ())       _ <- deleteMany conn [article] :: IO (Either PersistenceException ())@@ -164,5 +169,42 @@       print index `shouldThrow` errorCall "expectJust Field unknown is not present in type Article"      it "handles duplicate insert exceptions" $ do-      handleDuplicateInsert (toException $ EntityNotFound "24" ) `shouldBe` DatabaseError "EntityNotFound \"24\""-    +      handleDuplicateInsert (toException $ EntityNotFound "24") `shouldBe` DatabaseError "EntityNotFound \"24\""++    it "handles autoincrement edge cases" $ do+      conn <- connect AutoCommit <$> connectSqlite3 ":memory:"+      let article' = Article 200 "title" 2023+          expectedRow = [SqlInt64 200, SqlString "title", SqlInt64 2023]+      articleRow <- toRow conn article'+      articleRow `shouldBe` expectedRow+      -- Article is defined with autoIncrement = False+      removeAutoIncIdField @Article articleRow `shouldBe` expectedRow++      -- ArticleWithoutPK is defined with autoIncrement = True, but has no primary key field+      let articleWithoutPK = ArticleWithoutPK "title" 2023+          rowWithoutPK = [SqlString "title", SqlInt64 2023]+      articleRowWithoutPK <- toRow conn articleWithoutPK+      removeAutoIncIdField @ArticleWithoutPK articleRowWithoutPK `shouldBe` rowWithoutPK++      -- ArticleWithPKatEnd is defined with autoIncrement = True and has a primary key field as last column+      let articleWithPKatEnd = ArticleWithPKatEnd "title" 2023 200+          expectedRow2 = [SqlString "title", SqlInt64 2023]+      articleRowWithPKatEnd <- toRow conn articleWithPKatEnd+      removeAutoIncIdField @ArticleWithPKatEnd articleRowWithPKatEnd `shouldBe` expectedRow2++data ArticleWithoutPK = ArticleWithoutPK+  { title1 :: String,+    year1  :: Int+  }+  deriving (Generic, Show, Entity, Eq)++data ArticleWithPKatEnd = ArticleWithPKatEnd+  { title2     :: String,+    year2      :: Int,+    articleID2 :: Int+  }+  deriving (Generic, Show, Eq)++instance Entity ArticleWithPKatEnd where+  autoIncrement = True+  idField = "articleID2"
test/GenericPersistenceSpec.hs view
@@ -1,6 +1,6 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE QuasiQuotes #-}+{-# OPTIONS_GHC -Wno-deprecations #-}+{-# OPTIONS_GHC -Wno-missing-fields #-}  module GenericPersistenceSpec   ( test,@@ -8,15 +8,15 @@   ) where +import           Control.Exception import           Database.GP.GenericPersistence import           Database.HDBC import           Database.HDBC.Sqlite3-import           GHC.Generics hiding (Selector)+import           GHC.Generics                   hiding (Selector) import           Test.Hspec-import           Control.Exception  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -24,8 +24,10 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Person SQLite conn-  setupTableFor @Book SQLite conn+  setupTable @Person conn defaultSqliteMapping+  setupTable @Book conn defaultSqliteMapping+  setupTable @Car conn defaultSqliteMapping+  setupTable @Boat conn defaultSqliteMapping   return conn  -- | A data type with several fields, using record syntax.@@ -38,14 +40,27 @@   deriving (Generic, Show, Eq)  instance Entity Person where-  autoIncrement = False +  autoIncrement = False  data Car = Car-  { carID :: Int,-    carType  :: String+  { carID   :: Int,+    carType :: String   }-  deriving (Generic, Entity, Show, Eq)+  deriving (Generic, Show, Eq) +instance Entity Car where+  autoIncrement = True+  idField = "carID"++data Boat = Boat+  { boatID :: Int,+    boatType :: String+  }+  deriving (Generic, Show, Eq)++instance Entity Boat where+  autoIncrement = False+ data Book = Book   { book_id  :: Int,     title    :: String,@@ -60,8 +75,13 @@  instance Entity Book where   idField = "book_id"-  fieldsToColumns = [("book_id", "bookId"), ("title", "bookTitle"), ("author", "bookAuthor"), -                     ("year", "bookYear"), ("category", "bookCategory")]+  fieldsToColumns =+    [ ("book_id", "bookId"),+      ("title", "bookTitle"),+      ("author", "bookAuthor"),+      ("year", "bookYear"),+      ("category", "bookCategory")+    ]   tableName = "BOOK_TBL"   fromRow _c row = pure $ Book (col 0) (col 1) (col 2) (col 3) (col 4)     where@@ -99,17 +119,28 @@       head allPersons `shouldBe` bob       person' <- selectById conn (1 :: Int) :: IO (Maybe Person)       person' `shouldBe` Just bob+    it "retrieves the number of selected Entities" $ do+      conn <- prepareDB+      insertMany conn manyPersons+      countPersons <- count @Person conn allEntries+      countPersons `shouldBe` 6+    it "signals if counting fails" $ do+      conn <- connect AutoCommit <$> connectSqlite3 ":memory:" -- no tables in db+      eitherEA <- try (count @Person conn allEntries)+      case eitherEA of+        Left (DatabaseError msg) -> msg `shouldContain` "no such table: Person"+        _ -> expectationFailure "Expected DatabaseError"     it "selectById returns Nothing if no Entity was found" $ do       conn <- prepareDB       person' <- selectById conn (1 :: Int) :: IO (Maybe Person)       person' `shouldBe` Nothing-    it "selectById throws a DatabaseError if things go wrong in the DB" $ do +    it "selectById throws a DatabaseError if things go wrong in the DB" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       eitherEA <- try (selectById conn (1 :: Int) :: IO (Maybe Person))       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "no such table: Person"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError"  +        Left _                   -> expectationFailure "Expected DatabaseError"+        Right _                  -> expectationFailure "Expected DatabaseError"     it "retrieves Entities using user implementation" $ do       conn <- prepareDB       let hobbit = Book 1 "The Hobbit" "J.R.R. Tolkien" 1937 Fiction@@ -120,35 +151,60 @@       length allBooks `shouldBe` 3       head allBooks `shouldBe` hobbit       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)-      book' `shouldBe` Just hobbit     +      book' `shouldBe` Just hobbit     it "select returns Nothing if no Entity was found" $ do       conn <- prepareDB       allPersons' <- select @Person conn allEntries       allPersons' `shouldBe` []-    it "select throws a DatabaseError if things go wrong" $ do +    it "select throws a DatabaseError if things go wrong" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       eitherEA <- try (select @Person conn allEntries)       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "no such table: Person"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError"  -    it "can materialize Entities from user defined SQL queries" $ do +        Left _                   -> expectationFailure "Expected DatabaseError"+        Right _                  -> expectationFailure "Expected DatabaseError"+    it "can materialize Entities from user defined SQL queries" $ do       conn <- prepareDB       insertMany conn manyPersons-      let stmt = [sql|-                    SELECT * -                    FROM Person +      let stmt =+            [sql|+                    SELECT *+                    FROM Person                     WHERE age >= ?                     ORDER BY age ASC                     |]       resultRows <- quickQuery conn stmt [toSql (40 :: Int)]       allPersons <- entitiesFromRows @Person conn resultRows       length allPersons `shouldBe` 3+    it "can upsert Entities with AutoIncrement" $ do+      conn <- prepareDB+      let car = Car 1 "Honda Jazz"+      -- insert the car+      upsert conn car+      car' <- selectById conn (1 :: Int) :: IO (Maybe Car)+      car' `shouldBe` Just car+      -- now update the car +      let car2 = Car 1 "Honda Civic"+      upsert conn car2+      car2' <- selectById conn (1 :: Int) :: IO (Maybe Car)+      car2' `shouldBe` Just car2+    it "can upsert Entities without autoIncrement" $ do+      conn <- prepareDB+      let boat = Boat 1 "Sailboat"+      -- insert the boat+      upsert conn boat+      boat' <- selectById conn (1 :: Int) :: IO (Maybe Boat)+      boat' `shouldBe` Just boat+      -- now update the boat +      let boat2 = Boat 1 "Motorboat"+      upsert conn boat2+      boat2' <- selectById conn (1 :: Int) :: IO (Maybe Boat)+      boat2' `shouldBe` Just boat2     it "persists new Entities using Generics" $ do       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]       length allPersons `shouldBe` 0-      persist conn person+      upsert conn person       allPersons' <- select conn allEntries :: IO [Person]       length allPersons' `shouldBe` 1       person' <- selectById conn (123456 :: Int) :: IO (Maybe Person)@@ -162,13 +218,13 @@       length allbooks' `shouldBe` 1       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)       book' `shouldBe` Just book-    it "persist throws an exception if things go wrong" $ do +    it "persist throws an exception if things go wrong" $ do       conn <- connect AutoCommit <$> connectSqlite3 ":memory:"       eitherEA <- try (persist conn book)       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "no such table: BOOK_TBL"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError" +        Left _ -> expectationFailure "Expected DatabaseError"+        Right _ -> expectationFailure "Expected DatabaseError"     it "persists existing Entities using Generics" $ do       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]@@ -188,7 +244,7 @@       length allbooks' `shouldBe` 1       eitherEA <- try $ persist conn book {year = 1938} :: IO (Either PersistenceException ())       case eitherEA of-        Left  _ -> expectationFailure "should not throw an exception"+        Left _  -> expectationFailure "should not throw an exception"         Right x -> x `shouldBe` ()       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)       book' `shouldBe` Just book {year = 1938}@@ -196,15 +252,14 @@       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]       length allPersons `shouldBe` 0-      _ <-  insert conn person+      _ <- insert conn person       allPersons' <- select conn allEntries :: IO [Person]       length allPersons' `shouldBe` 1       person' <- selectById conn (123456 :: Int) :: IO (Maybe Person)       person' `shouldBe` Just person     it "inserts Entities with autoincrement handling" $ do       conn <- prepareDB-      _ <- run conn "CREATE TABLE Car (carID INTEGER PRIMARY KEY AUTOINCREMENT, carType TEXT);" []-      myCar@(Car carId _) <- insert conn (Car 0 "Honda Jazz")+      myCar@(Car carId _) <- insert conn Car {carType = "Honda Jazz"}       myCar' <- selectById conn carId :: IO (Maybe Car)       myCar' `shouldBe` Just myCar     it "inserts many Entities re-using a single prepared stmt" $ do@@ -214,6 +269,12 @@       insertMany conn manyPersons       allPersons' <- select conn allEntries :: IO [Person]       length allPersons' `shouldBe` 6+    it "inserts many alsodoes autoincrement handling" $ do+      conn <- prepareDB+      insertMany conn [Car {carType = "Honda Jazz"}]+      [Car carId typ] <- select @Car conn allEntries+      typ `shouldBe` "Honda Jazz"+      carId `shouldBe` 1     it "updates many Entities re-using a single prepared stmt" $ do       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]@@ -231,11 +292,32 @@       length allPersons `shouldBe` 0       insertMany conn manyPersons       allPersons' <- select conn allEntries :: IO [Person]-      length allPersons' `shouldBe` 6   +      length allPersons' `shouldBe` 6       deleteMany conn allPersons'       allPersons'' <- select conn allEntries :: IO [Person]       length allPersons'' `shouldBe` 0 +    it "deletes a single entity by a given id" $ do+      conn <- prepareDB+      insertMany conn manyPersons+      res <- deleteById @Person conn (6 :: Int)+      res `shouldBe` ()+      allPersons <- select conn allEntries :: IO [Person]+      length allPersons `shouldBe` 5+    it "signals if deleting by id fails" $ do+      conn <- prepareDB+      eitherEA <- try (deleteById @Person conn (6 :: Int))+      case eitherEA of+        Left (EntityNotFound msg) -> msg `shouldContain` "does not exist"+        _ -> expectationFailure "Expected EntityNotFound exception"++    it "deletes multiple entities by a list of ids" $ do+      conn <- prepareDB+      insertMany conn manyPersons+      deleteManyById @Person conn ([2,4,6] :: [Int])+      allPersons <- select conn allEntries :: IO [Person]+      length allPersons `shouldBe` 3+     it "inserts Entities using user implementation" $ do       conn <- prepareDB       allbooks <- select conn allEntries :: IO [Book]@@ -274,9 +356,9 @@       allBooks' <- select conn allEntries :: IO [Book]       length allBooks' `shouldBe` 0     it "provides a Connection Pool" $ do-      connPool <- sqlLitePool ":memory:" +      connPool <- sqlLitePool ":memory:"       withResource connPool $ \conn -> do-        setupTableFor @Person SQLite conn+        setupTable @Person conn defaultSqliteMapping         _ <- insert conn person         allPersons <- select conn allEntries :: IO [Person]         length allPersons `shouldBe` 1
test/OneToManySafeSpec.hs view
@@ -1,21 +1,18 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}- module OneToManySafeSpec   ( test,     spec,   ) where +import           Data.Either                        (fromRight) import           Database.GP.GenericPersistenceSafe import           Database.HDBC import           Database.HDBC.Sqlite3 import           GHC.Generics import           Test.Hspec-import           Data.Either                    (fromRight)  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl -- test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -23,8 +20,8 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn-  setupTableFor @Author SQLite conn+  setupTable @Article conn defaultSqliteMapping+  setupTable @Author conn defaultSqliteMapping   return conn  data Article = Article@@ -36,7 +33,7 @@   deriving (Generic, Show, Eq)  instance Entity Article where-  autoIncrement = False +  autoIncrement = False  data Author = Author   { authorID :: Int,@@ -47,8 +44,9 @@   deriving (Generic, Show, Eq)  instance Entity Author where-  fieldsToColumns :: [(String, String)]                  -- ommitting the articles field, -  fieldsToColumns =                                      -- as this can not be mapped to a single column+  fieldsToColumns :: [(String, String)] -- ommitting the articles field,+  fieldsToColumns =+    -- as this can not be mapped to a single column     [ ("authorID", "authorID"),       ("name", "name"),       ("address", "address")@@ -56,18 +54,21 @@    fromRow :: Conn -> [SqlValue] -> IO Author   fromRow conn row = do-    let authID = head row                                 -- authorID is the first column-    articlesBy <- (fromRight []) <$> select @Article conn (field "authorId" =. authID) -- retrieve all articles by this author-    return rawAuthor {articles = articlesBy}              -- add the articles to the author+    let authID = head row -- authorID is the first column+    articlesBy <- fromRight [] <$> select @Article conn (field "authorId" =. authID) -- retrieve all articles by this author+    return rawAuthor {articles = articlesBy} -- add the articles to the author     where-      rawAuthor = Author (col 0) (col 1) (col 2) []       -- create the author from row (w/o articles)-      col i = fromSql (row !! i)                          -- helper function to convert SqlValue to Haskell type+      rawAuthor = Author (col 0) (col 1) (col 2) [] -- create the author from row (w/o articles)+      col i = fromSql (row !! i) -- helper function to convert SqlValue to Haskell type    toRow :: Conn -> Author -> IO [SqlValue]   toRow conn a = do-    mapM_ (persist conn) (articles a)                     -- persist all articles of this author (update or insert)-    return [toSql (authorID a),                           -- return the author as a list of SqlValues-            toSql (name a), toSql (address a)]+    mapM_ (persist conn) (articles a) -- persist all articles of this author (update or insert)+    return+      [ toSql (authorID a), -- return the author as a list of SqlValues+        toSql (name a),+        toSql (address a)+      ]   autoIncrement = False  article1 :: Article@@ -127,7 +128,7 @@         Left _ -> fail "should not happen"         Right author -> do           length (articles author) `shouldBe` 2-      +       _ <- persist conn arthur {address = "New York"}       eitherPeAuthor' <- selectById @Author conn "2"       eitherPeAuthor' `shouldBe` Right arthur {address = "New York"}@@ -141,10 +142,10 @@       eitherPeUnit <- delete conn arthur       case eitherPeUnit of         Left (DatabaseError msg) -> msg `shouldContain` "no such table: Author"-        _ -> fail "should not happen"+        _                        -> fail "should not happen"     it "insertMany works with references" $ do       conn <- prepareDB-      let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}]+      let authors = [arthur, arthur {name = "Bob", authorID = 3, articles = []}]       eitherPeUnit <- insertMany conn authors       eitherPeUnit `shouldBe` Right ()       eitherPeAuthors <- select @Author conn allEntries@@ -158,7 +159,7 @@       eitherPeAuthor `shouldBe` Right arthur {address = "New York"}     it "updateMany works with references" $ do       conn <- prepareDB-      let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}]+      let authors = [arthur, arthur {name = "Bob", authorID = 3, articles = []}]       _ <- insertMany conn authors       eitherPeUnit <- updateMany conn (map (\a -> a {address = "New York"}) authors)       eitherPeUnit `shouldBe` Right ()@@ -166,11 +167,9 @@       eitherPeAuthors `shouldBe` Right (map (\a -> a {address = "New York"}) authors)     it "deleteMany works with references" $ do       conn <- prepareDB-      let authors = [arthur, arthur{name="Bob", authorID=3, articles=[]}]+      let authors = [arthur, arthur {name = "Bob", authorID = 3, articles = []}]       _ <- insertMany conn authors       eitherPeUnit <- deleteMany conn authors       eitherPeUnit `shouldBe` Right ()       eitherPeAuthors <- select @Author conn allEntries       eitherPeAuthors `shouldBe` Right []--
test/OneToManySpec.hs view
@@ -1,6 +1,3 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}- module OneToManySpec   ( test,     spec,@@ -15,7 +12,7 @@ import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl -- test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -23,8 +20,8 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn-  setupTableFor @Author SQLite conn+  setupTable @Article conn defaultSqliteMapping+  setupTable @Author conn defaultSqliteMapping   return conn  data Article = Article@@ -36,7 +33,7 @@   deriving (Generic, Show, Eq)  instance Entity Article where-  autoIncrement = False   +  autoIncrement = False  data Author = Author   { authorID :: Int,@@ -47,8 +44,9 @@   deriving (Generic, Show, Eq)  instance Entity Author where-  fieldsToColumns :: [(String, String)]                  -- ommitting the articles field, -  fieldsToColumns =                                      -- as this can not be mapped to a single column+  fieldsToColumns :: [(String, String)] -- ommitting the articles field,+  fieldsToColumns =+    -- as this can not be mapped to a single column     [ ("authorID", "authorID"),       ("name", "name"),       ("address", "address")@@ -57,19 +55,21 @@    fromRow :: Conn -> [SqlValue] -> IO Author   fromRow conn row = do-    let authID = head row                                 -- authorID is the first column+    let authID = head row -- authorID is the first column     articlesBy <- select conn (field "authorId" =. authID) -- retrieve all articles by this author-    return rawAuthor {articles = articlesBy}              -- add the articles to the author+    return rawAuthor {articles = articlesBy} -- add the articles to the author     where-      rawAuthor = Author (col 0) (col 1) (col 2) []       -- create the author from row (w/o articles)-      col i = fromSql (row !! i)                          -- helper function to convert SqlValue to Haskell type+      rawAuthor = Author (col 0) (col 1) (col 2) [] -- create the author from row (w/o articles)+      col i = fromSql (row !! i) -- helper function to convert SqlValue to Haskell type    toRow :: Conn -> Author -> IO [SqlValue]   toRow conn a = do-    mapM_ (persist conn) (articles a)                     -- persist all articles of this author (update or insert)-    return [toSql (authorID a),                           -- return the author as a list of SqlValues-            toSql (name a), toSql (address a)]-+    mapM_ (persist conn) (articles a) -- persist all articles of this author (update or insert)+    return+      [ toSql (authorID a), -- return the author as a list of SqlValues+        toSql (name a),+        toSql (address a)+      ]  article1 :: Article article1 =
test/PostgresQuerySpec.hs view
@@ -1,9 +1,8 @@-{-# LANGUAGE DeriveAnyClass #-}-module PostgresQuerySpec -( test-, spec-)-where +module PostgresQuerySpec+  ( test,+    spec,+  )+where  import           Database.GP import           Database.GP.SqlGenerator@@ -12,15 +11,15 @@ import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec  prepareDB :: IO Conn prepareDB = do-  conn <- connect ExplicitCommit <$> connectPostgreSQL  "host=localhost dbname=postgres user=postgres password=admin port=5431" -  setupTableFor @Person Postgres conn+  conn <- connect ExplicitCommit <$> connectPostgreSQL "host=localhost dbname=postgres user=postgres password=admin port=5431"+  setupTable @Person conn defaultPostgresMapping    insertMany conn [alice, bob, charlie]   commit conn@@ -36,7 +35,7 @@   deriving (Generic, Show, Eq)  instance Entity Person where-  autoIncrement = False +  autoIncrement = False  alice, bob, charlie, dave :: Person bob = Person 1 "Bob" 36 "West Street 79"@@ -50,10 +49,10 @@ addressField = field "address"  lower :: Field -> Field-lower = sqlFun "LOWER";+lower = sqlFun "LOWER"  upper :: Field -> Field-upper = sqlFun "UPPER";+upper = sqlFun "UPPER"  spec :: Spec spec = do@@ -64,12 +63,12 @@       length one `shouldBe` 1       head one `shouldBe` bob       commit conn-    -- TODO: FIXME  +    -- TODO: FIXME     it "supports disjunction with ||." $ do       conn <- prepareDB       two <- select @Person conn (nameField =. "Bob" ||. ageField =. (25 :: Int))       length two `shouldBe` 2-      two `shouldContain` [alice,bob]+      two `shouldContain` [alice, bob]       commit conn     it "supports LIKE" $ do       conn <- prepareDB@@ -77,7 +76,7 @@       length three `shouldBe` 3       commit conn     it "supports NOT" $ do-      conn <- prepareDB  +      conn <- prepareDB       empty <- select conn (not' $ addressField `like` "West Street %") :: IO [Person]       length empty `shouldBe` 0       commit conn@@ -119,7 +118,7 @@       commit conn     it "supports SQL functions on columns" $ do       conn <- prepareDB-      peopleFromWestStreet <- select conn (lower(upper addressField) `like` "west street %") :: IO [Person]+      peopleFromWestStreet <- select conn (lower (upper addressField) `like` "west street %") :: IO [Person]       length peopleFromWestStreet `shouldBe` 3       commit conn     it "supports selection by id" $ do@@ -130,14 +129,14 @@       commit conn     it "supports ORDER BY" $ do       conn <- prepareDB-      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField,ASC) :| [])+      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField, ASC) :| [])       length sortedPersons `shouldBe` 3       sortedPersons `shouldBe` [alice, charlie, bob]       commit conn     it "supports multiple columns in ORDER BY" $ do       conn <- prepareDB       _ <- insert conn dave -- dave and charlie have the same age-      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField,ASC) :| [(nameField,DESC)])+      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField, ASC) :| [(nameField, DESC)])       length sortedPersons `shouldBe` 4       sortedPersons `shouldBe` [alice, dave, charlie, bob]       commit conn@@ -150,34 +149,33 @@     it "supports LIMIT OFFSET" $ do       conn <- prepareDB       _ <- insert conn dave-      limitedPersons <- select @Person conn (allEntries `limitOffset` (2,1))+      limitedPersons <- select @Person conn (allEntries `limitOffset` (2, 1))       length limitedPersons `shouldBe` 1       head limitedPersons `shouldBe` charlie       commit conn     it "can create column types for a SqlLite" $ do-      columnTypeFor @SomeRecord SQLite "someRecordID" `shouldBe` "INTEGER"-      columnTypeFor @SomeRecord SQLite "someRecordName" `shouldBe` "TEXT"-      columnTypeFor @SomeRecord SQLite "someRecordAge" `shouldBe` "REAL"-      columnTypeFor @SomeRecord SQLite "someRecordTax" `shouldBe` "REAL"-      columnTypeFor @SomeRecord SQLite "someRecordFlag" `shouldBe` "INT"-      columnTypeFor @SomeRecord SQLite "someRecordDate" `shouldBe` "TEXT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordID" `shouldBe` "INTEGER"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordName" `shouldBe` "TEXT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordAge" `shouldBe` "REAL"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordTax" `shouldBe` "REAL"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordFlag" `shouldBe` "INT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordDate" `shouldBe` "TEXT"     it "can create column types for a Postgres" $ do-      columnTypeFor @SomeRecord Postgres "someRecordID" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordName" `shouldBe` "varchar"-      columnTypeFor @SomeRecord Postgres "someRecordAge" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordTax" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordFlag" `shouldBe` "boolean"-      columnTypeFor @SomeRecord Postgres "someRecordDate" `shouldBe` "varchar"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordID" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordName" `shouldBe` "varchar"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordAge" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordTax" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordFlag" `shouldBe` "boolean"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordDate" `shouldBe` "varchar"     it "can create whereclauses" $ do       whereClauseValues byIdColumn `shouldBe` []-      + data SomeRecord = SomeRecord-  { someRecordID :: Int,+  { someRecordID   :: Int,     someRecordName :: String,-    someRecordAge :: Double,-    someRecordTax :: Float,+    someRecordAge  :: Double,+    someRecordTax  :: Float,     someRecordFlag :: Bool,     someRecordDate :: Integer   }   deriving (Generic, Entity, Show, Eq)-
test/PostgresSpec.hs view
@@ -1,5 +1,4 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}+{-# OPTIONS_GHC -Wno-deprecations #-}  module PostgresSpec   ( test,@@ -7,24 +6,24 @@   ) where +import           Control.Exception import           Database.GP import           Database.HDBC.PostgreSQL-import           GHC.Generics hiding (Selector)+import           GHC.Generics             hiding (Selector) import           Test.Hspec-import           Control.Exception  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec  prepareDB :: IO Conn prepareDB = do-  con <- connect AutoCommit <$> connectPostgreSQL  "host=localhost dbname=postgres user=postgres password=admin port=5431" -  let conn = con{implicitCommit=False}-  setupTableFor @Person Postgres conn-  setupTableFor @Book Postgres conn+  conn <- connect ExplicitCommit <$> connectPostgreSQL "host=localhost dbname=postgres user=postgres password=admin port=5431"+  setupTable @Person conn defaultPostgresMapping+  setupTable @Book conn defaultPostgresMapping+  setupTable @Boat conn defaultPostgresMapping    _ <- run conn "DROP TABLE IF EXISTS Car;" []   _ <- run conn "CREATE TABLE Car (carID serial4 PRIMARY KEY, carType varchar);" []   commit conn@@ -40,14 +39,23 @@   deriving (Generic, Show, Eq)  instance Entity Person where-  autoIncrement = False   +  autoIncrement = False  data Car = Car-  { carID :: Int,-    carType  :: String+  { carID   :: Int,+    carType :: String   }   deriving (Generic, Entity, Show, Eq) +data Boat = Boat+  { boatID :: Int,+    boatType :: String+  }+  deriving (Generic, Show, Eq)++instance Entity Boat where+  autoIncrement = False+ data Book = Book   { book_id  :: Int,     title    :: String,@@ -62,8 +70,13 @@  instance Entity Book where   idField = "book_id"-  fieldsToColumns = [("book_id", "bookId"), ("title", "bookTitle"), ("author", "bookAuthor"), -                     ("year", "bookYear"), ("category", "bookCategory")]+  fieldsToColumns =+    [ ("book_id", "bookId"),+      ("title", "bookTitle"),+      ("author", "bookAuthor"),+      ("year", "bookYear"),+      ("category", "bookCategory")+    ]   tableName = "BOOK_TBL"   fromRow _c row = pure $ Book (col 0) (col 1) (col 2) (col 3) (col 4)     where@@ -111,15 +124,15 @@       person' <- selectById conn (1 :: Int) :: IO (Maybe Person)       person' `shouldBe` Nothing       commit conn-    it "selectById throws a DatabaseError if things go wrong in the DB" $ do +    it "selectById throws a DatabaseError if things go wrong in the DB" $ do       conn <- prepareDB       runRaw conn "DROP TABLE Person;"       commit conn       eitherEA <- try (selectById conn (1 :: Int) :: IO (Maybe Person))       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "does not exist"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError"  +        Left _                   -> expectationFailure "Expected DatabaseError"+        Right _                  -> expectationFailure "Expected DatabaseError"       rollback conn     it "retrieves Entities using user implementation" $ do       conn <- prepareDB@@ -132,22 +145,22 @@       length allBooks `shouldBe` 3       head allBooks `shouldBe` hobbit       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)-      book' `shouldBe` Just hobbit     +      book' `shouldBe` Just hobbit       commit conn     it "select returns Nothing if no Entity was found" $ do       conn <- prepareDB       allPersons' <- select @Person conn allEntries       allPersons' `shouldBe` []       commit conn-    it "select throws a DatabaseError if things go wrong" $ do +    it "select throws a DatabaseError if things go wrong" $ do       conn <- prepareDB       runRaw conn "DROP TABLE Person;"       commit conn       eitherEA <- try (select @Person conn allEntries)       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "does not exist"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError"  +        Left _                   -> expectationFailure "Expected DatabaseError"+        Right _                  -> expectationFailure "Expected DatabaseError"       rollback conn     it "persists new Entities using Generics" $ do       conn <- prepareDB@@ -171,24 +184,24 @@       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)       book' `shouldBe` Just book       commit conn-    it "persist throws an exception if things go wrong" $ do +    it "persist throws an exception if things go wrong" $ do       conn <- prepareDB       runRaw conn "DROP TABLE BOOK_TBL;"       commit conn       eitherEA <- try (persist conn book)       case eitherEA of         Left (DatabaseError msg) -> msg `shouldContain` "does not exist"-        Left  _ -> expectationFailure "Expected DatabaseError"-        Right _ -> expectationFailure "Expected DatabaseError" +        Left _                   -> expectationFailure "Expected DatabaseError"+        Right _                  -> expectationFailure "Expected DatabaseError"     it "persists existing Entities using Generics" $ do       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]       length allPersons `shouldBe` 0-      persist conn person+      upsert conn person       commit conn       allPersons' <- select conn allEntries :: IO [Person]       length allPersons' `shouldBe` 1-      persist conn person {age = 26}+      upsert conn person {age = 26}       commit conn       person' <- selectById conn (123456 :: Int) :: IO (Maybe Person)       person' `shouldBe` Just person {age = 26}@@ -203,11 +216,39 @@       length allbooks' `shouldBe` 1       eitherEA <- try $ persist conn book {year = 1938} :: IO (Either PersistenceException ())       case eitherEA of-        Left  _ -> expectationFailure "should not throw an exception"+        Left _  -> expectationFailure "should not throw an exception"         Right x -> x `shouldBe` ()       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)       book' `shouldBe` Just book {year = 1938}       commit conn+    it "can upsert Entities with AutoIncrement" $ do+      conn <- prepareDB+      let car = Car 1 "Honda Jazz"+      -- insert the car+      upsert conn car+      car' <- selectById conn (1 :: Int) :: IO (Maybe Car)+      car' `shouldBe` Just car+      -- now update the car +      let car2 = Car 1 "Honda Civic"+      upsert conn car2+      car2' <- selectById conn (1 :: Int) :: IO (Maybe Car)+      car2' `shouldBe` Just car2+      commit conn++    it "can upsert Entities without autoIncrement" $ do+      conn <- prepareDB+      let boat = Boat 1 "Sailboat"+      -- insert the boat+      upsert conn boat+      boat' <- selectById conn (1 :: Int) :: IO (Maybe Boat)+      boat' `shouldBe` Just boat+      -- now update the boat +      let boat2 = Boat 1 "Motorboat"+      upsert conn boat2+      boat2' <- selectById conn (1 :: Int) :: IO (Maybe Boat)+      boat2' `shouldBe` Just boat2+      commit conn+     it "inserts Entities using Generics" $ do       conn <- prepareDB       allPersons <- select conn allEntries :: IO [Person]@@ -255,7 +296,7 @@       insertMany conn manyPersons       commit conn       allPersons' <- select conn allEntries :: IO [Person]-      length allPersons' `shouldBe` 6   +      length allPersons' `shouldBe` 6       deleteMany conn allPersons'       commit conn       allPersons'' <- select conn allEntries :: IO [Person]@@ -271,7 +312,7 @@       length allbooks' `shouldBe` 1       book' <- selectById conn (1 :: Int) :: IO (Maybe Book)       book' `shouldBe` Just book-      commit conn +      commit conn     it "updates Entities using Generics" $ do       conn <- prepareDB       _ <- insert conn person@@ -307,9 +348,9 @@       length allBooks' `shouldBe` 0       commit conn     it "provides a Connection Pool" $ do-      connPool <- postgreSQLPool "host=localhost dbname=postgres user=postgres password=admin port=5431" +      connPool <- postgreSQLPool "host=localhost dbname=postgres user=postgres password=admin port=5431"       withResource connPool $ \conn -> do-        setupTableFor @Person Postgres conn+        setupTable @Person conn defaultPostgresMapping         _ <- insert conn person         allPersons <- select conn allEntries :: IO [Person]         length allPersons `shouldBe` 1
test/QuerySpec.hs view
@@ -1,18 +1,17 @@-{-# LANGUAGE DeriveAnyClass #-}-module QuerySpec -( test-, spec-)-where +module QuerySpec+  ( test,+    spec,+  )+where  import           Database.GP import           Database.GP.SqlGenerator-import Database.HDBC.Sqlite3 ( connectSqlite3 )+import           Database.HDBC.Sqlite3    (connectSqlite3) import           GHC.Generics import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -20,7 +19,7 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Person SQLite conn+  setupTable @Person conn defaultSqliteMapping    insertMany conn [alice, bob, charlie]   return conn@@ -49,10 +48,10 @@ addressField = field "address"  lower :: Field -> Field-lower = sqlFun "LOWER";+lower = sqlFun "LOWER"  upper :: Field -> Field-upper = sqlFun "UPPER";+upper = sqlFun "UPPER"  spec :: Spec spec = do@@ -72,7 +71,7 @@       three <- select conn (addressField `like` "West Street %") :: IO [Person]       length three `shouldBe` 3     it "supports NOT" $ do-      conn <- prepareDB  +      conn <- prepareDB       empty <- select conn (not' $ addressField `like` "West Street %") :: IO [Person]       length empty `shouldBe` 0     it "supports fieldwise comparisons like >" $ do@@ -108,7 +107,7 @@       length allPersons `shouldBe` 3     it "supports SQL functions on columns" $ do       conn <- prepareDB-      peopleFromWestStreet <- select conn (lower(upper addressField) `like` "west street %") :: IO [Person]+      peopleFromWestStreet <- select conn (lower (upper addressField) `like` "west street %") :: IO [Person]       length peopleFromWestStreet `shouldBe` 3     it "supports selection by id" $ do       conn <- prepareDB@@ -117,13 +116,13 @@       head charlie' `shouldBe` charlie     it "supports ORDER BY" $ do       conn <- prepareDB-      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField,ASC) :| [])+      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField, ASC) :| [])       length sortedPersons `shouldBe` 3       sortedPersons `shouldBe` [alice, charlie, bob]     it "supports multiple columns in ORDER BY" $ do       conn <- prepareDB       _ <- insert conn dave -- dave and charlie have the same age-      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField,ASC) :| [(nameField,DESC)])+      sortedPersons <- select @Person conn (allEntries `orderBy` (ageField, ASC) :| [(nameField, DESC)])       length sortedPersons `shouldBe` 4       sortedPersons `shouldBe` [alice, dave, charlie, bob]     it "supports LIMIT" $ do@@ -134,33 +133,42 @@     it "supports LIMIT OFFSET" $ do       conn <- prepareDB       _ <- insert conn dave-      limitedPersons <- select @Person conn (allEntries `limitOffset` (2,1))+      limitedPersons <- select @Person conn (allEntries `limitOffset` (2, 1))       length limitedPersons `shouldBe` 1       head limitedPersons `shouldBe` charlie     it "can create column types for a SqlLite" $ do-      columnTypeFor @SomeRecord SQLite "someRecordID" `shouldBe` "INTEGER"-      columnTypeFor @SomeRecord SQLite "someRecordName" `shouldBe` "TEXT"-      columnTypeFor @SomeRecord SQLite "someRecordAge" `shouldBe` "REAL"-      columnTypeFor @SomeRecord SQLite "someRecordTax" `shouldBe` "REAL"-      columnTypeFor @SomeRecord SQLite "someRecordFlag" `shouldBe` "INT"-      columnTypeFor @SomeRecord SQLite "someRecordDate" `shouldBe` "TEXT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordID" `shouldBe` "INTEGER"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordName" `shouldBe` "TEXT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordAge" `shouldBe` "REAL"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordTax" `shouldBe` "REAL"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordFlag" `shouldBe` "INT"+      columnTypeFor @SomeRecord defaultSqliteMapping "someRecordDate" `shouldBe` "TEXT"     it "can create column types for a Postgres" $ do-      columnTypeFor @SomeRecord Postgres "someRecordID" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordName" `shouldBe` "varchar"-      columnTypeFor @SomeRecord Postgres "someRecordAge" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordTax" `shouldBe` "numeric"-      columnTypeFor @SomeRecord Postgres "someRecordFlag" `shouldBe` "boolean"-      columnTypeFor @SomeRecord Postgres "someRecordDate" `shouldBe` "varchar"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordID" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordName" `shouldBe` "varchar"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordAge" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordTax" `shouldBe` "numeric"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordFlag" `shouldBe` "boolean"+      columnTypeFor @SomeRecord defaultPostgresMapping "someRecordDate" `shouldBe` "varchar"     it "can create whereclauses" $ do       whereClauseValues byIdColumn `shouldBe` []-      +    it "allows to write custom SQL queries" $ do+      conn <- prepareDB+      _ <- insert conn dave+      let stmt = [sql|+        SELECT * +        FROM person +        WHERE name = (?)|]+      persons <- entitiesFromRows @Person conn =<< quickQuery conn stmt [toSql "Dave"]+      length persons `shouldBe` 1+      head persons `shouldBe` dave+ data SomeRecord = SomeRecord-  { someRecordID :: Int,+  { someRecordID   :: Int,     someRecordName :: String,-    someRecordAge :: Double,-    someRecordTax :: Float,+    someRecordAge  :: Double,+    someRecordTax  :: Float,     someRecordFlag :: Bool,     someRecordDate :: Integer   }   deriving (Generic, Entity, Show, Eq)-
test/ReferenceSpec.hs view
@@ -1,6 +1,3 @@--- allows automatic derivation from Entity type class-{-# LANGUAGE DeriveAnyClass #-}- module ReferenceSpec   ( test,     spec,@@ -15,7 +12,7 @@ import           Test.Hspec  -- `test` is here so that this module can be run from GHCi on its own.  It is--- not needed for automatic spec discovery. +-- not needed for automatic spec discovery. -- (start up stack repl --test to bring up ghci and have access to all the test functions) test :: IO () test = hspec spec@@ -23,8 +20,8 @@ prepareDB :: IO Conn prepareDB = do   conn <- connect AutoCommit <$> connectSqlite3 ":memory:"-  setupTableFor @Article SQLite conn-  setupTableFor @Author SQLite conn+  setupTable @Article conn defaultSqliteMapping+  setupTable @Author conn defaultSqliteMapping   return conn  data Article = Article@@ -43,35 +40,44 @@   deriving (Generic, Show, Eq)  instance Entity Author where-  autoIncrement = False +  autoIncrement = False  instance Entity Article where   autoIncrement :: Bool   autoIncrement = False-  -  fieldsToColumns :: [(String, String)]                      -- ommitting the author field,-  fieldsToColumns =                                          -- as this can not be mapped to a single column-    [ ("articleID", "articleID"),                            -- instead we invent a new column authorID         ++  fieldsToColumns :: [(String, String)] -- ommitting the author field,+  fieldsToColumns =+    -- as this can not be mapped to a single column+    [ ("articleID", "articleID"), -- instead we invent a new column authorID       ("title", "title"),       ("authorID", "authorID"),       ("year", "year")     ]    fromRow :: Conn -> [SqlValue] -> IO Article-  fromRow conn row = do    -    authorById <- fromJust <$> selectById conn (row !! 2)  -- load author by foreign key-    return $ rawArticle {author = authorById}                -- add author to article+  fromRow conn row = do+    authorById <- fromJust <$> selectById conn (row !! 2) -- load author by foreign key+    return $ rawArticle {author = authorById} -- add author to article     where-      rawArticle = Article (col 0) (col 1)                   -- create article from row, -                           (Author (col 2) "" "") (col 3)    -- using a dummy author+      rawArticle =+        Article+          (col 0)+          (col 1) -- create article from row,+          (Author (col 2) "" "")+          (col 3) -- using a dummy author         where           col i = fromSql (row !! i)    toRow :: Conn -> Article -> IO [SqlValue]   toRow conn a = do-    persist conn (author a)                                  -- persist author first-    return [toSql (articleID a), toSql (title a),            -- return row for article table where -            toSql $ authorID (author a), toSql (year a)]     -- authorID is foreign key to author table +    persist conn (author a) -- persist author first+    return+      [ toSql (articleID a),+        toSql (title a), -- return row for article table where+        toSql $ authorID (author a),+        toSql (year a) -- authorID is foreign key to author table+      ]  article :: Article article =