Db.hs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. {-# OPTIONS -Wno-orphans #-}
  2. {-# LANGUAGE LambdaCase #-}
  3. {-# LANGUAGE OverloadedStrings #-}
  4. module TFB.Db
  5. ( Pool,
  6. mkPool,
  7. Config (..),
  8. queryWorldById,
  9. queryWorldByIds,
  10. updateWorlds,
  11. queryFortunes,
  12. Error,
  13. )
  14. where
  15. import Control.Exception (catch, try)
  16. import Control.Monad (forM)
  17. import Data.Bifunctor qualified as Bi
  18. import Data.ByteString (ByteString)
  19. import Data.ByteString.Char8 qualified as BSC
  20. import Data.Either qualified as Either
  21. import Data.Pool qualified as Pool
  22. import Database.PostgreSQL.Simple (SomePostgreSqlException)
  23. import Database.PostgreSQL.Simple qualified as PG
  24. import Database.PostgreSQL.Simple.FromRow (FromRow (fromRow), field)
  25. import System.IO.Error qualified as Error
  26. import TFB.Types qualified as Types
  27. -------------------------------------------------------------------------------
  28. -- * Database
  29. data Config
  30. = Config
  31. { configHost :: String,
  32. configName :: ByteString,
  33. configUser :: ByteString,
  34. configPass :: ByteString,
  35. configStripes :: Int,
  36. configPoolSize :: Int
  37. }
  38. instance Show Config where
  39. show c =
  40. "Config {"
  41. <> " configHost = "
  42. <> configHost c
  43. <> ", configName = "
  44. <> BSC.unpack (configName c)
  45. <> ", configUser = "
  46. <> BSC.unpack (configUser c)
  47. <> ", configPass = REDACTED"
  48. <> ", configStripes = "
  49. <> show (configStripes c)
  50. <> ", configPoolSize = "
  51. <> show (configPoolSize c)
  52. <> " }"
  53. instance FromRow Types.World where
  54. fromRow = Types.World <$> field <*> field
  55. instance FromRow Types.Fortune where
  56. fromRow = Types.Fortune <$> field <*> field
  57. type Connection = PG.Connection
  58. type Pool = Pool.Pool Connection
  59. data Error
  60. = DbError ByteString
  61. | DbErrors [ByteString]
  62. | NotFound
  63. deriving (Show)
  64. connect :: Config -> IO Connection
  65. connect c = catch (PG.connect pgc) failError
  66. where
  67. failError :: PG.SomePostgreSqlException -> IO a
  68. failError = Error.ioError . Error.userError . show
  69. pgc =
  70. PG.defaultConnectInfo
  71. { PG.connectHost = configHost c,
  72. PG.connectDatabase = BSC.unpack $ configName c,
  73. PG.connectUser = BSC.unpack $ configUser c,
  74. PG.connectPassword = BSC.unpack $ configPass c
  75. }
  76. close :: Connection -> IO ()
  77. close = PG.close
  78. mkPool :: Config -> IO Pool
  79. mkPool c =
  80. Pool.newPool $
  81. Pool.setNumStripes (Just $ configStripes c) $
  82. Pool.defaultPoolConfig
  83. (connect c)
  84. close
  85. 0.5
  86. (configPoolSize c)
  87. -------------------------------------------------------------------------------
  88. -- * World
  89. queryWorldByIdInner :: Types.QId -> Connection -> IO (Either Error Types.World)
  90. queryWorldByIdInner wId conn = do
  91. let query = PG.query conn "SELECT * FROM World WHERE id = ?" (PG.Only wId :: PG.Only Types.QId) :: IO [Types.World]
  92. res <- try @SomePostgreSqlException query
  93. pure $ Either.either (Left . DbError . BSC.pack . show) mkW res
  94. where
  95. mkW [] = Left NotFound
  96. mkW (w : _) = pure w
  97. queryWorldById :: Pool -> Types.QId -> IO (Either Error Types.World)
  98. queryWorldById dbPool wId = Pool.withResource dbPool (queryWorldByIdInner wId)
  99. queryWorldByIds :: Pool -> [Types.QId] -> IO (Either Error [Types.World])
  100. queryWorldByIds dbPool wIds = Pool.withResource dbPool $ \conn -> do
  101. rows <- forM wIds $ \wId -> queryWorldByIdInner wId conn
  102. let (errs, rowsList) = Either.partitionEithers rows
  103. return $ case errs of
  104. [] -> pure rowsList
  105. _ ->
  106. Left . DbErrors $
  107. map
  108. ( \case
  109. DbError e -> e
  110. _ -> error "Unexpected error"
  111. )
  112. errs
  113. updateWorlds :: Pool -> [(Types.World, Int)] -> IO (Either Error [Types.World])
  114. updateWorlds dbPool wsUpdates = Pool.withResource dbPool $ \conn -> do
  115. let worlds = Bi.first Types.wId <$> wsUpdates
  116. res <-
  117. try @SomePostgreSqlException $
  118. PG.executeMany
  119. conn
  120. "UPDATE World SET randomNumber = upd.rnd FROM (VALUES (?,?)) as upd(wid,rnd) WHERE World.id = upd.wid"
  121. worlds
  122. _ <- case res of
  123. Left e -> print e
  124. Right _ -> return ()
  125. pure $ Bi.bimap (DbError . BSC.pack . show) (const $ map (uncurry Types.World) worlds) res
  126. -------------------------------------------------------------------------------
  127. -- * Fortunes
  128. queryFortunes :: Pool -> IO (Either Error [Types.Fortune])
  129. queryFortunes dbPool = Pool.withResource dbPool $ \conn -> do
  130. let query = PG.query_ conn "SELECT * FROM Fortune" :: IO [Types.Fortune]
  131. res <- try @SomePostgreSqlException query
  132. pure $ Bi.first (DbError . BSC.pack . show) res