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.Except
19 import Control.Monad.Random
20 import Control.Monad.Reader
21 import Control.Monad.Trans.Control (MonadBaseControl)
22 import Data.Aeson (Result(Error,Success), fromJSON, FromJSON)
23 import Data.ByteString.Char8 (hPutStrLn)
24 import Data.Either.Extra (Either(Left, Right))
25 import Data.Ini (readIniFile, lookupValue)
26 import Data.Pool (Pool, withResource)
27 import Data.Profunctor.Product.Default (Default)
28 import Data.Text (unpack, pack)
29 import Data.Word (Word16)
30 import Database.PostgreSQL.Simple (Connection, connect)
31 import Database.PostgreSQL.Simple.FromField ( Conversion, ResultError(ConversionFailed), fromField, returnError)
32 import Database.PostgreSQL.Simple.Internal (Field)
33 import Gargantext.Prelude
34 import Gargantext.Prelude.Config (GargConfig())
35 import Opaleye (Query, Unpackspec, showSqlForPostgres, FromFields, Select, runQuery, PGJsonb, QueryRunnerColumnDefault)
36 import Opaleye.Aggregate (countRows)
37 import System.IO (FilePath)
38 import System.IO (stderr)
39 import Text.Read (read)
40 import qualified Data.ByteString as DB
41 import qualified Data.List as DL
42 import qualified Database.PostgreSQL.Simple as PGS
44 -------------------------------------------------------
45 class HasConnectionPool env where
46 connPool :: Getter env (Pool Connection)
48 instance HasConnectionPool (Pool Connection) where
51 class HasConfig env where
52 hasConfig :: Getter env GargConfig
54 instance HasConfig GargConfig where
57 -------------------------------------------------------
58 type JSONB = QueryRunnerColumnDefault PGJsonb
59 -------------------------------------------------------
61 type CmdM'' env err m =
64 , MonadBaseControl IO m
68 type CmdM' env err m =
71 , MonadBaseControl IO m
77 , HasConnectionPool env
81 type CmdRandom env err m =
83 , HasConnectionPool env
88 type Cmd'' env err a = forall m. CmdM'' env err m => m a
89 type Cmd' env err a = forall m. CmdM' env err m => m a
90 type Cmd err a = forall m env. CmdM env err m => m a
91 type CmdR err a = forall m env. CmdRandom env err m => m a
95 fromInt64ToInt :: Int64 -> Int
96 fromInt64ToInt = fromIntegral
98 -- TODO: ideally there should be very few calls to this functions.
99 mkCmd :: (Connection -> IO a) -> Cmd err a
101 pool <- view connPool
102 withResource pool (liftBase . k)
104 runCmd :: (HasConnectionPool env)
108 runCmd env m = runExceptT $ runReaderT m env
110 runOpaQuery :: Default FromFields fields haskells
112 -> Cmd err [haskells]
113 runOpaQuery q = mkCmd $ \c -> runQuery c q
115 runCountOpaQuery :: Select a -> Cmd err Int
116 runCountOpaQuery q = do
117 counts <- mkCmd $ \c -> runQuery c $ countRows q
118 -- countRows is guaranteed to return a list with exactly one row so DL.head is safe here
119 pure $ fromInt64ToInt $ DL.head counts
121 formatPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err DB.ByteString
122 formatPGSQuery q a = mkCmd $ \conn -> PGS.formatQuery conn q a
124 -- TODO use runPGSQueryDebug everywhere
125 runPGSQuery' :: (PGS.ToRow a, PGS.FromRow b) => PGS.Query -> a -> Cmd err [b]
126 runPGSQuery' q a = mkCmd $ \conn -> PGS.query conn q a
128 runPGSQuery :: ( CmdM env err m
129 , PGS.FromRow r, PGS.ToRow q
131 => PGS.Query -> q -> m [r]
132 runPGSQuery q a = mkCmd $ \conn -> catch (PGS.query conn q a) (printError conn)
134 printError c (SomeException e) = do
135 q' <- PGS.formatQuery c q a
137 throw (SomeException e)
140 execPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err Int64
141 execPGSQuery q a = mkCmd $ \conn -> PGS.execute conn q a
143 ------------------------------------------------------------------------
145 databaseParameters :: FilePath -> IO PGS.ConnectInfo
146 databaseParameters fp = do
147 ini <- readIniFile fp
148 let ini'' = case ini of
149 Left e -> panic (pack $ "No ini file error" <> show e)
152 let val x = case (lookupValue (pack "database") (pack x) ini'') of
153 Left _ -> panic (pack $ "no" <> x)
154 Right p' -> unpack p'
156 pure $ PGS.ConnectInfo { PGS.connectHost = val "DB_HOST"
157 , PGS.connectPort = read (val "DB_PORT") :: Word16
158 , PGS.connectUser = val "DB_USER"
159 , PGS.connectPassword = val "DB_PASS"
160 , PGS.connectDatabase = val "DB_NAME"
163 connectGargandb :: FilePath -> IO Connection
164 connectGargandb fp = databaseParameters fp >>= \params -> connect params
166 fromField' :: (Typeable b, FromJSON b) => Field -> Maybe DB.ByteString -> Conversion b
167 fromField' field mb = do
168 v <- fromField field mb
171 valueToHyperdata v = case fromJSON v of
173 Error _err -> returnError ConversionFailed field
174 $ DL.intercalate " " [ "cannot parse hyperdata for JSON: "
178 printSqlOpa :: Default Unpackspec a a => Query a -> IO ()
179 printSqlOpa = putStrLn . maybe "Empty query" identity . showSqlForPostgres