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 Arrows #-}
13 {-# LANGUAGE ConstraintKinds, ScopedTypeVariables #-}
14 {-# LANGUAGE LambdaCase #-}
16 module Gargantext.Database.Prelude where
18 --import Control.Monad.Logger (MonadLogger)
19 import Control.Exception
20 import Control.Lens (Getter, view)
21 import Control.Monad.Except
22 import Control.Monad.Random
23 import Control.Monad.Reader
24 import Control.Monad.Trans.Control (MonadBaseControl)
25 import Data.Aeson (Result(Error,Success), fromJSON, FromJSON)
26 import Data.ByteString.Char8 (hPutStrLn)
27 import Data.Either.Extra (Either)
28 import Data.Pool (Pool, withResource)
29 import Data.Profunctor.Product.Default (Default)
30 import Data.Text (pack, unpack, Text)
31 import Data.Word (Word16)
32 import Database.PostgreSQL.Simple (Connection, connect)
33 import Database.PostgreSQL.Simple.FromField ( Conversion, ResultError(ConversionFailed), fromField, returnError)
34 import Database.PostgreSQL.Simple.Internal (Field)
35 import Database.PostgreSQL.Simple.Types (Query(..))
36 import Gargantext.Core.Mail.Types (HasMail)
37 import Gargantext.Core.NLP (HasNLPServer)
38 import Gargantext.Prelude
39 import Gargantext.Prelude.Config (GargConfig(), readIniFile', val)
40 import Opaleye (Unpackspec, showSql, FromFields, Select, runSelect, SqlJsonb, DefaultFromField, toFields, matchMaybe, MaybeFields)
41 import Opaleye.Aggregate (countRows)
42 import System.IO (FilePath, stderr)
43 import Text.Read (readMaybe)
44 import qualified Data.ByteString as DB
45 import qualified Data.List as DL
46 import qualified Database.PostgreSQL.Simple as PGS
47 import qualified Opaleye.Internal.Constant
48 import qualified Opaleye.Internal.Operators
50 -------------------------------------------------------
51 class HasConnectionPool env where
52 connPool :: Getter env (Pool Connection)
54 instance HasConnectionPool (Pool Connection) where
57 class HasConfig env where
58 hasConfig :: Getter env GargConfig
60 instance HasConfig GargConfig where
63 -------------------------------------------------------
64 type JSONB = DefaultFromField SqlJsonb
65 -------------------------------------------------------
67 type CmdM'' env err m =
70 , MonadBaseControl IO m
75 type CmdM' env err m =
78 , MonadBaseControl IO m
84 ( HasConnectionPool env
94 type CmdRandom env err m =
96 , HasConnectionPool env
102 type Cmd'' env err a = forall m. CmdM'' env err m => m a
103 type Cmd' env err a = forall m. CmdM' env err m => m a
104 type Cmd err a = forall m env. CmdM env err m => m a
105 type CmdR err a = forall m env. CmdRandom env err m => m a
109 fromInt64ToInt :: Int64 -> Int
110 fromInt64ToInt = fromIntegral
112 -- TODO: ideally there should be very few calls to this functions.
113 mkCmd :: (Connection -> IO a) -> Cmd err a
115 pool <- view connPool
116 withResource pool (liftBase . k)
118 runCmd :: (HasConnectionPool env)
122 runCmd env m = runExceptT $ runReaderT m env
124 runOpaQuery :: Default FromFields fields haskells
126 -> Cmd err [haskells]
127 runOpaQuery q = mkCmd $ \c -> runSelect c q
129 runCountOpaQuery :: Select a -> Cmd err Int
130 runCountOpaQuery q = do
131 counts <- mkCmd $ \c -> runSelect c $ countRows q
132 -- countRows is guaranteed to return a list with exactly one row so DL.head is safe here
133 pure $ fromInt64ToInt $ DL.head counts
135 formatPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err DB.ByteString
136 formatPGSQuery q a = mkCmd $ \conn -> PGS.formatQuery conn q a
138 -- TODO use runPGSQueryDebug everywhere
139 runPGSQuery' :: (PGS.ToRow a, PGS.FromRow b) => PGS.Query -> a -> Cmd err [b]
140 runPGSQuery' q a = mkCmd $ \conn -> PGS.query conn q a
142 runPGSQuery :: ( CmdM env err m
143 , PGS.FromRow r, PGS.ToRow q
145 => PGS.Query -> q -> m [r]
146 runPGSQuery q a = mkCmd $ \conn -> catch (PGS.query conn q a) (printError conn)
148 printError c (SomeException e) = do
149 q' <- PGS.formatQuery c q a
151 throw (SomeException e)
155 runPGSQueryFold :: ( CmdM env err m
158 => PGS.Query -> a -> (a -> r -> IO a) -> m a
159 runPGSQueryFold q initialState consume = mkCmd $ \conn -> catch (PGS.fold_ conn initialState consume) (printError conn)
161 printError c (SomeException e) = do
162 q' <- PGS.formatQuery c q
164 throw (SomeException e)
169 -- | TODO catch error
170 runPGSQuery_ :: ( CmdM env err m
173 => PGS.Query -> m [r]
174 runPGSQuery_ q = mkCmd $ \conn -> catch (PGS.query_ conn q) printError
176 printError (SomeException e) = do
177 hPutStrLn stderr (fromQuery q)
178 throw (SomeException e)
180 execPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err Int64
181 execPGSQuery q a = mkCmd $ \conn -> PGS.execute conn q a
183 ------------------------------------------------------------------------
184 databaseParameters :: FilePath -> IO PGS.ConnectInfo
185 databaseParameters fp = do
186 ini <- readIniFile' fp
187 let val' key = unpack $ val ini "database" key
188 let dbPortRaw = val' "DB_PORT"
189 let dbPort = case (readMaybe dbPortRaw :: Maybe Word16) of
190 Nothing -> panic $ "DB_PORT incorrect: " <> (pack dbPortRaw)
193 pure $ PGS.ConnectInfo { PGS.connectHost = val' "DB_HOST"
194 , PGS.connectPort = dbPort
195 , PGS.connectUser = val' "DB_USER"
196 , PGS.connectPassword = val' "DB_PASS"
197 , PGS.connectDatabase = val' "DB_NAME"
200 connectGargandb :: FilePath -> IO Connection
201 connectGargandb fp = databaseParameters fp >>= \params -> connect params
203 fromField' :: (Typeable b, FromJSON b) => Field -> Maybe DB.ByteString -> Conversion b
204 fromField' field mb = do
205 v <- fromField field mb
208 valueToHyperdata v = case fromJSON v of
210 Error _err -> returnError ConversionFailed field
211 $ DL.intercalate " " [ "cannot parse hyperdata for JSON: "
215 printSqlOpa :: Default Unpackspec a a => Select a -> IO ()
216 printSqlOpa = putStrLn . maybe "Empty query" identity . showSql
218 dbCheck :: CmdM env err m => m Bool
220 r :: [PGS.Only Text] <- runPGSQuery_ "select username from public.auth_user"
225 restrictMaybe :: ( Default Opaleye.Internal.Operators.IfPP b b
226 , (Default Opaleye.Internal.Constant.ToFields Bool b))
227 => MaybeFields a -> (a -> b) -> b
228 restrictMaybe v cond = matchMaybe v $ \case
229 Nothing -> toFields True