{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DeriveAnyClass #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# LANGUAGE LambdaCase #-} module Database.GP.GenericPersistenceSafe ( selectById, select, entitiesFromRows, sql, persist, insert, insertMany, update, updateMany, delete, deleteMany, setupTableFor, Conn(..), connect, Database(..), TxHandling (..), ConnectionPool, createConnPool, withResource, Entity (..), GToRow, GFromRow, columnNameFor, maybeFieldTypeFor, TypeInfo (..), typeInfo, PersistenceException(..), WhereClauseExpr, Field, field, (&&.), (||.), (=.), (>.), (<.), (>=.), (<=.), (<>.), like, between, in', isNull, not', sqlFun, allEntries, byId, orderBy, SortOrder (..), limit, limitOffset, fieldIndex, handleDuplicateInsert ) 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 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. 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 | DuplicateInsert String | DatabaseError String | NoUniqueKey String deriving (Show, Eq, Exception) -- | A function that retrieves an entity from a database. -- The function takes entity id as parameter. -- If an entity with the given id exists in the database, it is returned as a Just value. -- If no such entity exists, Nothing is returned. -- 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 eitherExResultRows <- try $ quickQuery conn stmt [eid] case eitherExResultRows of Left ex -> return $ Left $ fromException ex Right resultRowsSqlValues -> case resultRowsSqlValues of [] -> return $ Left $ EntityNotFound $ constructorName ti ++ " " ++ show eid ++ " not found" [singleRow] -> do eitherExEntity <- try $ fromRow conn singleRow case eitherExEntity of Left ex -> return $ Left $ fromException ex Right entity -> return $ Right entity _ -> return $ Left $ NoUniqueKey $ "More than one " ++ constructorName ti ++ " found for id " ++ show eid where ti = typeInfo @a stmt = selectFromStmt @a byIdColumn eid = toSql idx 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. select :: forall a. (Entity a) => Conn -> WhereClauseExpr -> IO (Either PersistenceException [a]) select conn whereClause = do --print stmt eitherExRows <- tryPE $ quickQuery conn stmt values case eitherExRows of Left ex -> return $ Left ex Right resultRows -> entitiesFromRows conn resultRows where stmt = selectFromStmt @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. -- The function returns a (possibly empty) list of all matching entities. -- The function is used internally by `retrieveAll` and `retrieveAllWhere`. -- But it can also be used to convert the result of a custom SQL query to a list of entities. entitiesFromRows :: forall a. (Entity a) => Conn -> [[SqlValue]] -> IO (Either PersistenceException [a]) entitiesFromRows = (tryPE .) . mapM . fromRow -- | 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 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 -- | 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 -- | A function that explicitely inserts an entity into a database. 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) commitIfAutoCommit conn fromRow @a conn singleRow case eitherExOrA of Left ex -> return $ Left $ handleDuplicateInsert ex Right a -> return $ Right a removeIdField :: forall a. (Entity a) => [SqlValue] -> [SqlValue] removeIdField row = if autoIncrement @a then case maybeIdFieldIndex @a of Nothing -> row Just idIndex -> take idIndex row ++ drop (idIndex + 1) row else row handleDuplicateInsert :: SomeException -> PersistenceException 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 tryPE :: IO a -> IO (Either PersistenceException a) tryPE action = do eitherExResult <- try action case eitherExResult of Left ex -> return $ Left $ fromException ex Right result -> return $ Right result -- | A function that inserts a list of entities into a database. -- The function takes an HDBC connection and a list of entities as parameters. -- The insert-statement is compiled only once and then executed for each entity. insertMany :: forall a. (Entity a) => Conn -> [a] -> IO (Either PersistenceException ()) insertMany conn entities = do eitherExUnit <- try $ do rows <- mapM (toRow conn) entities stmt <- prepare conn (insertStmtFor @a) executeMany stmt (map (removeIdField @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. update :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException ()) update conn entity = do eitherExUnit <- try $ do eid <- idValue conn entity row <- toRow conn entity rowcount <- run conn (updateStmtFor @a) (row ++ [eid]) if rowcount == 0 then return (Left (EntityNotFound (constructorName (typeInfo @a) ++ " " ++ show eid ++ " does not exist"))) else do commitIfAutoCommit conn return $ Right () case eitherExUnit of Left ex -> return $ Left $ fromException ex Right result -> return result -- | A function that updates a list of entities in a database. -- The function takes an HDBC connection and a list of entities as parameters. -- The update-statement is compiled only once and then executed for each entity. updateMany :: forall a. (Entity a) => Conn -> [a] -> IO (Either PersistenceException ()) updateMany conn entities = tryPE $ do eids <- mapM (idValue conn) entities rows <- mapM (toRow conn) entities stmt <- prepare conn (updateStmtFor @a) -- the update statement has one more parameter than the row: the id value for the where clause executeMany stmt (zipWith (\l x -> l ++ [x]) rows eids) commitIfAutoCommit conn -- | A function that deletes an entity from a database. -- The function takes an HDBC connection and an entity as parameters. delete :: forall a. (Entity a) => Conn -> a -> IO (Either PersistenceException ()) delete conn entity = do eitherExRes <- try $ do eid <- idValue conn entity 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. -- 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 (Either PersistenceException ()) deleteMany conn entities = tryPE $ do eids <- mapM (idValue conn) entities stmt <- prepare conn (deleteStmtFor @a) executeMany stmt (map (: []) eids) 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 runRaw conn $ dropTableStmtFor @a runRaw conn $ stmt -- createTableStmtFor @a (db conn) 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. idValue :: forall a. (Entity a) => Conn -> a -> IO SqlValue idValue conn x = do sqlValues <- toRow conn x return (sqlValues !! idFieldIndex) where idFieldIndex = fieldIndex @a (idField @a) -- an alias for a simple quasiqouter sql :: QuasiQuoter sql = r -- | These instances are needed to make the Convertible type class work with Enum types out of the box. -- This is needed because the Convertible type class is used to convert SqlValues to Haskell types. instance {-# OVERLAPS #-} forall a. (Enum a) => Convertible SqlValue a where safeConvert :: SqlValue -> ConvertResult a safeConvert = Right . toEnum . fromSql instance {-# OVERLAPS #-} forall a. (Enum a) => Convertible a SqlValue where safeConvert :: a -> ConvertResult SqlValue safeConvert = Right . toSql . fromEnum