2 Module : Gargantext.Database.Prelude
3 Description : Specific Prelude for Database management
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
12 {-# LANGUAGE ConstraintKinds #-}
14 module Gargantext.Database.Prelude where
16 import Control.Exception
17 import Control.Lens (Getter, view)
18 import Control.Monad.Error.Class -- (MonadError(..), Error)
19 import Control.Monad.Except
20 import Control.Monad.Reader
21 import Control.Monad.Random
22 import Control.Monad.Trans.Control (MonadBaseControl)
23 import Data.Aeson (Result(Error,Success), fromJSON, FromJSON)
24 import Data.ByteString.Char8 (hPutStrLn)
25 import Data.Either.Extra (Either(Left, Right))
26 import Data.Ini (readIniFile, lookupValue)
27 import Data.Pool (Pool, withResource)
28 import Data.Profunctor.Product.Default (Default)
29 import Data.Text (unpack, pack)
30 import Data.Word (Word16)
31 import Database.PostgreSQL.Simple (Connection, connect)
32 import Database.PostgreSQL.Simple.FromField ( Conversion, ResultError(ConversionFailed), fromField, returnError)
33 import Database.PostgreSQL.Simple.Internal (Field)
34 import Opaleye (Query, Unpackspec, showSqlForPostgres, FromFields, Select, runQuery, PGJsonb, QueryRunnerColumnDefault)
35 import Opaleye.Aggregate (countRows)
36 import System.IO (FilePath)
37 import System.IO (stderr)
38 import Text.Read (read)
39 import qualified Data.ByteString as DB
40 import qualified Data.List as DL
41 import qualified Database.PostgreSQL.Simple as PGS
43 import Gargantext.Prelude
44 import Gargantext.Prelude.Config (GargConfig())
46 -------------------------------------------------------
47 class HasConnectionPool env where
48 connPool :: Getter env (Pool Connection)
50 instance HasConnectionPool (Pool Connection) where
53 class HasConfig env where
54 config :: Getter env GargConfig
56 instance HasConfig GargConfig where
59 -------------------------------------------------------
60 type JSONB = QueryRunnerColumnDefault PGJsonb
61 -------------------------------------------------------
63 type CmdM'' env err m =
66 , MonadBaseControl IO m
70 type CmdM' env err m =
73 , MonadBaseControl IO m
79 , HasConnectionPool env
83 type CmdRandom env err m =
85 , HasConnectionPool env
90 type Cmd'' env err a = forall m. CmdM'' env err m => m a
91 type Cmd' env err a = forall m. CmdM' env err m => m a
92 type Cmd err a = forall m env. CmdM env err m => m a
93 type CmdR err a = forall m env. CmdRandom env err m => m a
97 fromInt64ToInt :: Int64 -> Int
98 fromInt64ToInt = fromIntegral
100 -- TODO: ideally there should be very few calls to this functions.
101 mkCmd :: (Connection -> IO a) -> Cmd err a
103 pool <- view connPool
104 withResource pool (liftBase . k)
106 runCmd :: (HasConnectionPool env)
110 runCmd env m = runExceptT $ runReaderT m env
112 runOpaQuery :: Default FromFields fields haskells
114 -> Cmd err [haskells]
115 runOpaQuery q = mkCmd $ \c -> runQuery c q
117 runCountOpaQuery :: Select a -> Cmd err Int
118 runCountOpaQuery q = do
119 counts <- mkCmd $ \c -> runQuery c $ countRows q
120 -- countRows is guaranteed to return a list with exactly one row so DL.head is safe here
121 pure $ fromInt64ToInt $ DL.head counts
123 formatPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err DB.ByteString
124 formatPGSQuery q a = mkCmd $ \conn -> PGS.formatQuery conn q a
126 -- TODO use runPGSQueryDebug everywhere
127 runPGSQuery' :: (PGS.ToRow a, PGS.FromRow b) => PGS.Query -> a -> Cmd err [b]
128 runPGSQuery' q a = mkCmd $ \conn -> PGS.query conn q a
130 runPGSQuery :: ( CmdM env err m
131 , PGS.FromRow r, PGS.ToRow q
133 => PGS.Query -> q -> m [r]
134 runPGSQuery q a = mkCmd $ \conn -> catch (PGS.query conn q a) (printError conn)
136 printError c (SomeException e) = do
137 q' <- PGS.formatQuery c q a
139 throw (SomeException e)
142 execPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err Int64
143 execPGSQuery q a = mkCmd $ \conn -> PGS.execute conn q a
145 ------------------------------------------------------------------------
147 databaseParameters :: FilePath -> IO PGS.ConnectInfo
148 databaseParameters fp = do
149 ini <- readIniFile fp
150 let ini'' = case ini of
151 Left e -> panic (pack $ "No ini file error" <> show e)
154 let val x = case (lookupValue (pack "database") (pack x) ini'') of
155 Left _ -> panic (pack $ "no" <> x)
156 Right p' -> unpack p'
158 pure $ PGS.ConnectInfo { PGS.connectHost = val "DB_HOST"
159 , PGS.connectPort = read (val "DB_PORT") :: Word16
160 , PGS.connectUser = val "DB_USER"
161 , PGS.connectPassword = val "DB_PASS"
162 , PGS.connectDatabase = val "DB_NAME"
165 connectGargandb :: FilePath -> IO Connection
166 connectGargandb fp = databaseParameters fp >>= \params -> connect params
168 fromField' :: (Typeable b, FromJSON b) => Field -> Maybe DB.ByteString -> Conversion b
169 fromField' field mb = do
170 v <- fromField field mb
173 valueToHyperdata v = case fromJSON v of
175 Error _err -> returnError ConversionFailed field
176 $ DL.intercalate " " [ "cannot parse hyperdata for JSON: "
180 printSqlOpa :: Default Unpackspec a a => Query a -> IO ()
181 printSqlOpa = putStrLn . maybe "Empty query" identity . showSqlForPostgres