{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} module Database.PSQL.Types ( TablePrefix , PSQLPool , PSQL , HasPSQL , psqlPool , tablePrefix , SimpleEnv , simpleEnv , HasOtherEnv , otherEnv , TableName , getTableName , Columns , createTable , constraintPrimaryKey , getIndexName , IndexName , createIndex , getOnly , getOnlyDefault , insert , insertRet , insertOrUpdate , update , delete , delete_ , count , count_ , select , selectOnly , select_ , selectOnly_ , selectOne , selectOneOnly , VersionList , mergeDatabase -- re-exports , FromRow (..) , field , Only (..) , SqlError (..) , OrderBy , asc , desc , none ) where import Control.Monad (void) import Data.Hashable (Hashable (..)) import Data.Int (Int64) import Data.List (intercalate) import Data.Maybe (listToMaybe) import Data.Pool (Pool) import Data.String (IsString (..)) import Database.PostgreSQL.Simple (Connection, Only (..), SqlError (..), ToRow, execute, execute_, query, query_) import Database.PostgreSQL.Simple.FromRow (FromRow (..), field) import GHC.Generics (Generic) type From = Int64 type Size = Int64 newtype TablePrefix = TablePrefix String deriving (Show) instance IsString TablePrefix where fromString = TablePrefix type PSQL a = TablePrefix -> Connection -> IO a type PSQLPool = Pool Connection class HasPSQL u where psqlPool :: u -> PSQLPool tablePrefix :: u -> TablePrefix class HasOtherEnv u a where otherEnv :: a -> u data SimpleEnv u = SimpleEnv { pc :: Pool Connection , pf :: TablePrefix , pu :: u } instance HasPSQL (SimpleEnv u) where psqlPool = pc tablePrefix = pf instance HasOtherEnv u (SimpleEnv u) where otherEnv = pu simpleEnv :: Pool Connection -> TablePrefix -> u -> SimpleEnv u simpleEnv pool prefix env0 = SimpleEnv{pc=pool, pf = prefix, pu = env0} newtype TableName = TableName String deriving (Show) instance IsString TableName where fromString = TableName getTableName :: TablePrefix -> TableName -> String getTableName (TablePrefix "") (TableName name) = concat ["\"", name, "\"" ] getTableName (TablePrefix prefix) (TableName name) = concat ["\"", prefix, "_", name, "\"" ] newtype Column = Column { unColumn :: String } deriving (Show) instance IsString Column where fromString = Column type Columns = [Column] columnsToString :: Columns -> String columnsToString = intercalate ", " . map unColumn constraintPrimaryKey :: TablePrefix -> TableName -> Columns -> Column constraintPrimaryKey prefix tn columns = Column . concat $ [ "CONSTRAINT " , getIndexName prefix tn "pkey" , " PRIMARY KEY (", columnsToString columns, ")" ] createTable :: TableName -> Columns -> PSQL Int64 createTable tn cols prefix conn = execute_ conn sql where sql = fromString $ concat [ "CREATE TABLE IF NOT EXISTS ", getTableName prefix tn, " (" , columnsToString cols , ")" ] newtype IndexName = IndexName String deriving (Show) instance IsString IndexName where fromString = IndexName getIndexName :: TablePrefix -> TableName -> IndexName -> String getIndexName (TablePrefix "") (TableName tn) (IndexName name) = concat [ "\"", tn, "_", name, "\"" ] getIndexName (TablePrefix prefix) (TableName tn) (IndexName name) = concat [ "\"", prefix, "_", tn , "_", name, "\"" ] createIndex :: Bool -> TableName -> IndexName -> Columns -> PSQL Int64 createIndex uniq tn idxN cols prefix conn = execute_ conn sql where sql = fromString $ concat [ "CREATE ", uniqWord, "INDEX IF NOT EXISTS ", getIndexName prefix tn idxN , " ON " , getTableName prefix tn, "(", columnsToString cols, ")" ] uniqWord = if uniq then "UNIQUE " else "" getOnly :: FromRow (Only a) => [Only a] -> Maybe a getOnly = fmap fromOnly . listToMaybe getOnlyDefault :: FromRow (Only a) => a -> [Only a] -> a getOnlyDefault a = maybe a fromOnly . listToMaybe insert :: ToRow a => TableName -> Columns -> a -> PSQL Int64 insert tn cols a prefix conn = execute conn sql a where v = take (length cols) $ cycle ["?"] sql = fromString $ concat [ "INSERT INTO ", getTableName prefix tn , " (", columnsToString cols, ")" , " VALUES" , " (", columnsToString v, ")" ] insertRet :: (ToRow a, FromRow (Only b)) => TableName -> Columns -> Column -> a -> b -> PSQL b insertRet tn cols col a def prefix conn = getOnlyDefault def <$> query conn sql a where v = take (length cols) $ cycle ["?"] sql = fromString $ concat [ "INSERT INTO ", getTableName prefix tn , " (", columnsToString cols, ")" , " VALUES" , " (", columnsToString v, ")" , " returning ", unColumn col ] insertOrUpdate :: ToRow a => TableName -> Columns -> Columns -> Columns -> a -> PSQL Int64 insertOrUpdate tn uniqCols valCols otherCols a prefix conn = execute conn sql a where cols = uniqCols ++ valCols ++ otherCols v = replicate (length cols) "?" setSql = intercalate ", " $ map appendSet valCols appendSet :: Column -> String appendSet (Column col) | '=' `elem` col = col | otherwise = col ++ " = excluded." ++ col doSql = if null valCols then " DO NOTHING" else " DO UPDATE SET " ++ setSql sql = fromString $ concat [ "INSERT INTO ", getTableName prefix tn , " (", columnsToString cols, ")" , " VALUES" , " (", columnsToString v, ")" , " ON CONFLICT (", columnsToString uniqCols, ")" , doSql ] update :: ToRow a => TableName -> Columns -> String -> a -> PSQL Int64 update tn cols partSql a prefix conn = execute conn sql a where setSql = intercalate ", " $ map appendSet cols whereSql = if null partSql then "" else " WHERE " ++ partSql sql = fromString $ concat [ "UPDATE ", getTableName prefix tn , " SET ", setSql , whereSql ] appendSet :: Column -> String appendSet (Column col) | '=' `elem` col = col | otherwise = col ++ " = ?" delete :: ToRow a => TableName -> String -> a -> PSQL Int64 delete tn partSql a prefix conn = execute conn sql a where whereSql = " WHERE " ++ partSql sql = fromString $ concat [ "DELETE FROM ", getTableName prefix tn, whereSql ] delete_ :: TableName -> PSQL Int64 delete_ tn prefix conn = execute_ conn sql where sql = fromString $ concat [ "DELETE FROM ", getTableName prefix tn ] count :: ToRow a => TableName -> String -> a -> PSQL Int64 count tn partSql a prefix conn = getOnlyDefault 0 <$> query conn sql a where whereSql = " WHERE " ++ partSql sql = fromString $ concat [ "SELECT count(*) FROM ", getTableName prefix tn, whereSql ] count_ :: TableName -> PSQL Int64 count_ tn prefix conn = getOnlyDefault 0 <$> query_ conn sql where sql = fromString $ concat [ "SELECT count(*) FROM ", getTableName prefix tn ] select :: (ToRow a, FromRow b) => TableName -> Columns -> String -> a -> From -> Size -> OrderBy -> PSQL [b] select tn cols partSql a from size o prefix conn = query conn sql a where whereSql = " WHERE " ++ partSql sql = fromString $ concat [ "SELECT ", columnsToString cols, " FROM ", getTableName prefix tn , whereSql , " ", show o , " LIMIT ", show size , " OFFSET ", show from ] selectOnly :: (ToRow a, FromRow (Only b)) => TableName -> Column -> String -> a -> From -> Size -> OrderBy -> PSQL [b] selectOnly tn col partSql a from size o prefix conn = map fromOnly <$> select tn [col] partSql a from size o prefix conn select_ :: FromRow b => TableName -> Columns -> From -> Size -> OrderBy -> PSQL [b] select_ tn cols from size o prefix conn = query_ conn sql where sql = fromString $ concat [ "SELECT ", columnsToString cols, " FROM ", getTableName prefix tn , " ", show o , " LIMIT ", show size , " OFFSET ", show from ] selectOnly_ :: FromRow (Only b) => TableName -> Column -> From -> Size -> OrderBy -> PSQL [b] selectOnly_ tn col from size o prefix conn = map fromOnly <$> select_ tn [col] from size o prefix conn selectOne :: (ToRow a, FromRow b) => TableName -> Columns -> String -> a -> PSQL (Maybe b) selectOne tn cols partSql a prefix conn = listToMaybe <$> query conn sql a where whereSql = " WHERE " ++ partSql sql = fromString $ concat [ "SELECT ", columnsToString cols, " FROM ", getTableName prefix tn , whereSql ] selectOneOnly :: (ToRow a, FromRow (Only b)) => TableName -> Column -> String -> a -> PSQL (Maybe b) selectOneOnly tn col partSql a prefix conn = fmap fromOnly <$> selectOne tn [col] partSql a prefix conn createVersionTable :: PSQL Int64 createVersionTable prefix conn = createTable "version" [ "name VARCHAR(10) NOT NULL" , "version INT DEFAULT '0'" , "PRIMARY KEY (name)" ] prefix conn getCurrentVersion :: PSQL Int64 getCurrentVersion prefix conn = do void $ createVersionTable prefix conn ts <- selectOneOnly "version" "version" "name = ?" (Only ("version" :: String)) prefix conn case ts of Just v -> pure v Nothing -> insertRet "version" ["name", "version"] "version" ("version" :: String, 0 :: Int) 0 prefix conn updateVersion :: Int64 -> PSQL () updateVersion ts prefix conn = void $ update "version" ["version"] "name = ?" (ts, "version" :: String) prefix conn type Version a = (Int64, [PSQL a]) type VersionList a = [Version a] mergeDatabase :: VersionList a -> PSQL () mergeDatabase versionList prefix conn = do version <- getCurrentVersion prefix conn mapM_ (\v -> processAction version v prefix conn) versionList processAction :: Int64 -> Version a -> PSQL () processAction version (ts, actions) prefix conn = if ts > version then do updateVersion ts prefix conn mapM_ (\o -> void $ o prefix conn) actions else pure () data OrderBy = Desc String | Asc String | None deriving (Generic, Eq) instance Hashable OrderBy desc :: String -> OrderBy desc = Desc asc :: String -> OrderBy asc = Asc none :: OrderBy none = None instance Show OrderBy where show (Desc f) = "ORDER BY " ++ f ++ " DESC" show (Asc f) = "ORDER BY " ++ f ++ " ASC" show None = ""