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.Exception
19 import Control.Lens (Getter, view)
20 import Control.Monad.Except
21 --import Control.Monad.Logger (MonadLogger)
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.Prelude
38 import Gargantext.Prelude.Config (GargConfig(), readIniFile', val)
39 import Opaleye (Unpackspec, showSql, FromFields, Select, runSelect, SqlJsonb, DefaultFromField, toFields, matchMaybe, MaybeFields)
40 import Opaleye.Aggregate (countRows)
41 import qualified Opaleye.Internal.Constant
42 import qualified Opaleye.Internal.Operators
43 import System.IO (FilePath, stderr)
44 import Text.Read (readMaybe)
45 import qualified Data.ByteString as DB
46 import qualified Data.List as DL
47 import qualified Database.PostgreSQL.Simple as PGS
49 -------------------------------------------------------
50 class HasConnectionPool env where
51 connPool :: Getter env (Pool Connection)
53 instance HasConnectionPool (Pool Connection) where
56 class HasConfig env where
57 hasConfig :: Getter env GargConfig
59 instance HasConfig GargConfig where
62 -------------------------------------------------------
63 type JSONB = DefaultFromField SqlJsonb
64 -------------------------------------------------------
66 type CmdM'' env err m =
69 , MonadBaseControl IO m
74 type CmdM' env err m =
77 , MonadBaseControl IO m
84 , HasConnectionPool env
89 type CmdRandom env err m =
91 , HasConnectionPool env
97 type Cmd'' env err a = forall m. CmdM'' env err m => m a
98 type Cmd' env err a = forall m. CmdM' env err m => m a
99 type Cmd err a = forall m env. CmdM env err m => m a
100 type CmdR err a = forall m env. CmdRandom env err m => m a
104 fromInt64ToInt :: Int64 -> Int
105 fromInt64ToInt = fromIntegral
107 -- TODO: ideally there should be very few calls to this functions.
108 mkCmd :: (Connection -> IO a) -> Cmd err a
110 pool <- view connPool
111 withResource pool (liftBase . k)
113 runCmd :: (HasConnectionPool env)
117 runCmd env m = runExceptT $ runReaderT m env
119 runOpaQuery :: Default FromFields fields haskells
121 -> Cmd err [haskells]
122 runOpaQuery q = mkCmd $ \c -> runSelect c q
124 runCountOpaQuery :: Select a -> Cmd err Int
125 runCountOpaQuery q = do
126 counts <- mkCmd $ \c -> runSelect c $ countRows q
127 -- countRows is guaranteed to return a list with exactly one row so DL.head is safe here
128 pure $ fromInt64ToInt $ DL.head counts
130 formatPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err DB.ByteString
131 formatPGSQuery q a = mkCmd $ \conn -> PGS.formatQuery conn q a
133 -- TODO use runPGSQueryDebug everywhere
134 runPGSQuery' :: (PGS.ToRow a, PGS.FromRow b) => PGS.Query -> a -> Cmd err [b]
135 runPGSQuery' q a = mkCmd $ \conn -> PGS.query conn q a
137 runPGSQuery :: ( CmdM env err m
138 , PGS.FromRow r, PGS.ToRow q
140 => PGS.Query -> q -> m [r]
141 runPGSQuery q a = mkCmd $ \conn -> catch (PGS.query conn q a) (printError conn)
143 printError c (SomeException e) = do
144 q' <- PGS.formatQuery c q a
146 throw (SomeException e)
150 runPGSQueryFold :: ( CmdM env err m
153 => PGS.Query -> a -> (a -> r -> IO a) -> m a
154 runPGSQueryFold q initialState consume = mkCmd $ \conn -> catch (PGS.fold_ conn initialState consume) (printError conn)
156 printError c (SomeException e) = do
157 q' <- PGS.formatQuery c q
159 throw (SomeException e)
164 -- | TODO catch error
165 runPGSQuery_ :: ( CmdM env err m
168 => PGS.Query -> m [r]
169 runPGSQuery_ q = mkCmd $ \conn -> catch (PGS.query_ conn q) printError
171 printError (SomeException e) = do
172 hPutStrLn stderr (fromQuery q)
173 throw (SomeException e)
175 execPGSQuery :: PGS.ToRow a => PGS.Query -> a -> Cmd err Int64
176 execPGSQuery q a = mkCmd $ \conn -> PGS.execute conn q a
178 ------------------------------------------------------------------------
179 databaseParameters :: FilePath -> IO PGS.ConnectInfo
180 databaseParameters fp = do
181 ini <- readIniFile' fp
182 let val' key = unpack $ val ini "database" key
183 let dbPortRaw = val' "DB_PORT"
184 let dbPort = case (readMaybe dbPortRaw :: Maybe Word16) of
185 Nothing -> panic $ "DB_PORT incorrect: " <> (pack dbPortRaw)
188 pure $ PGS.ConnectInfo { PGS.connectHost = val' "DB_HOST"
189 , PGS.connectPort = dbPort
190 , PGS.connectUser = val' "DB_USER"
191 , PGS.connectPassword = val' "DB_PASS"
192 , PGS.connectDatabase = val' "DB_NAME"
195 connectGargandb :: FilePath -> IO Connection
196 connectGargandb fp = databaseParameters fp >>= \params -> connect params
198 fromField' :: (Typeable b, FromJSON b) => Field -> Maybe DB.ByteString -> Conversion b
199 fromField' field mb = do
200 v <- fromField field mb
203 valueToHyperdata v = case fromJSON v of
205 Error _err -> returnError ConversionFailed field
206 $ DL.intercalate " " [ "cannot parse hyperdata for JSON: "
210 printSqlOpa :: Default Unpackspec a a => Select a -> IO ()
211 printSqlOpa = putStrLn . maybe "Empty query" identity . showSql
213 dbCheck :: CmdM env err m => m Bool
215 r :: [PGS.Only Text] <- runPGSQuery_ "select username from public.auth_user"
220 restrictMaybe :: ( Default Opaleye.Internal.Operators.IfPP b b
221 , (Default Opaleye.Internal.Constant.ToFields Bool b))
222 => MaybeFields a -> (a -> b) -> b
223 restrictMaybe v cond = matchMaybe v $ \case
224 Nothing -> toFields True