Db.hs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. {-# LANGUAGE OverloadedStrings #-}
  2. module TFB.Db
  3. ( Pool,
  4. mkPool,
  5. Config (..),
  6. queryWorldById,
  7. queryWorldByIds,
  8. updateWorlds,
  9. queryFortunes,
  10. Error,
  11. )
  12. where
  13. import Control.Monad (forM, forM_)
  14. import Data.ByteString (ByteString)
  15. import Data.ByteString.Char8 qualified as BSC
  16. import Data.Either qualified as Either
  17. import Data.Pool qualified as Pool
  18. import Data.Text (Text)
  19. import Data.Text qualified as Text
  20. import Database.MySQL.Base qualified as MySQL
  21. import System.IO.Streams qualified as Streams
  22. import TFB.Types qualified as Types
  23. -------------------------------------------------------------------------------
  24. -- * Database
  25. data Config
  26. = Config
  27. { configHost :: String,
  28. configName :: ByteString,
  29. configUser :: ByteString,
  30. configPass :: ByteString,
  31. configStripes :: Int,
  32. configPoolSize :: Int
  33. }
  34. instance Show Config where
  35. show c =
  36. "Config {"
  37. <> " configHost = "
  38. <> configHost c
  39. <> ", configName = "
  40. <> BSC.unpack (configName c)
  41. <> ", configUser = "
  42. <> BSC.unpack (configUser c)
  43. <> ", configPass = REDACTED"
  44. <> ", configStripes = "
  45. <> show (configStripes c)
  46. <> ", configPoolSize = "
  47. <> show (configPoolSize c)
  48. <> " }"
  49. type Connection = MySQL.MySQLConn
  50. type Pool = Pool.Pool Connection
  51. type Error = Text
  52. type DbRow = [MySQL.MySQLValue]
  53. connect :: Config -> IO Connection
  54. connect c = MySQL.connect myc
  55. where
  56. myc =
  57. MySQL.defaultConnectInfoMB4
  58. { MySQL.ciHost = configHost c,
  59. MySQL.ciDatabase = configName c,
  60. MySQL.ciUser = configUser c,
  61. MySQL.ciPassword = configPass c
  62. }
  63. close :: Connection -> IO ()
  64. close = MySQL.close
  65. mkPool :: Config -> IO Pool
  66. mkPool c =
  67. Pool.newPool $
  68. Pool.setNumStripes (Just $ configStripes c) $
  69. Pool.defaultPoolConfig
  70. (connect c)
  71. close
  72. 0.5
  73. (configPoolSize c)
  74. {-# SPECIALIZE intValEnc :: Int -> MySQL.MySQLValue #-}
  75. {-# SPECIALIZE intValEnc :: Types.QId -> MySQL.MySQLValue #-}
  76. intValEnc :: (Integral a) => a -> MySQL.MySQLValue
  77. intValEnc = MySQL.MySQLInt16U . fromIntegral
  78. intValDec :: MySQL.MySQLValue -> Either Text Int
  79. intValDec (MySQL.MySQLInt8U i) = pure . fromIntegral $ i
  80. intValDec (MySQL.MySQLInt8 i) = pure . fromIntegral $ i
  81. intValDec (MySQL.MySQLInt16U i) = pure . fromIntegral $ i
  82. intValDec (MySQL.MySQLInt16 i) = pure . fromIntegral $ i
  83. intValDec (MySQL.MySQLInt32U i) = pure . fromIntegral $ i
  84. intValDec (MySQL.MySQLInt32 i) = pure . fromIntegral $ i
  85. intValDec (MySQL.MySQLInt64U i) = pure . fromIntegral $ i
  86. intValDec (MySQL.MySQLInt64 i) = pure . fromIntegral $ i
  87. intValDec x = Left $ "Expected MySQLInt*, received" <> Text.pack (show x)
  88. textValDec :: MySQL.MySQLValue -> Either Text Text
  89. textValDec (MySQL.MySQLText t) = pure t
  90. textValDec x = Left $ "Expected Text, received" <> Text.pack (show x)
  91. -------------------------------------------------------------------------------
  92. -- * World
  93. decodeWorld :: DbRow -> Either Error Types.World
  94. decodeWorld [] = Left "MarshalError: Expected 2 columns for World, found 0"
  95. decodeWorld [_] = Left "MarshalError: Expected 2 columns for World, found 1"
  96. decodeWorld (c1 : c2 : _) = Types.World <$> intValDec c1 <*> intValDec c2
  97. queryWorldById :: Pool -> Types.QId -> IO (Either Error Types.World)
  98. queryWorldById dbPool wId = Pool.withResource dbPool $ \conn -> do
  99. (_, rowsS) <- MySQL.query conn s [intValEnc wId]
  100. rows <- Streams.toList rowsS
  101. let eWorlds = fmap decodeWorld rows
  102. let (err, oks) = Either.partitionEithers eWorlds
  103. return $ case err of
  104. [] -> case oks of
  105. [] -> Left "World not found!"
  106. w : _ -> pure w
  107. _ -> Left . mconcat $ err
  108. where
  109. s = "SELECT * FROM World WHERE id = ?"
  110. queryWorldByIds :: Pool -> [Types.QId] -> IO (Either Error [Types.World])
  111. queryWorldByIds _ [] = pure . pure $ mempty
  112. queryWorldByIds dbPool wIds = Pool.withResource dbPool $ \conn -> do
  113. sId <- MySQL.prepareStmt conn "SELECT * FROM World WHERE id = ?"
  114. res <- forM wIds $ \wId -> do
  115. (_, rowsS) <- MySQL.queryStmt conn sId [intValEnc wId]
  116. rows <- Streams.toList rowsS
  117. return . fmap decodeWorld $ rows
  118. MySQL.closeStmt conn sId
  119. let (errs, ws) = Either.partitionEithers . mconcat $ res
  120. return $ case errs of
  121. [] -> pure ws
  122. _ -> Left . mconcat $ errs
  123. updateWorlds :: Pool -> [(Types.World, Int)] -> IO (Either Error [Types.World])
  124. updateWorlds _ [] = pure . pure $ mempty
  125. updateWorlds dbPool wsUpdates = Pool.withResource dbPool $ \conn -> do
  126. let ws = fmap updateW wsUpdates
  127. sId <- MySQL.prepareStmt conn "UPDATE World SET randomNumber = ? WHERE id = ?"
  128. forM_ wsUpdates $ \(w, wNum) ->
  129. MySQL.executeStmt conn sId [intValEnc wNum, intValEnc $ Types.wId w]
  130. MySQL.closeStmt conn sId
  131. return . pure $ ws
  132. where
  133. updateW (w, wNum) = w {Types.wRandomNumber = wNum}
  134. -------------------------------------------------------------------------------
  135. -- * Fortunes
  136. decodeFortune :: DbRow -> Either Error Types.Fortune
  137. decodeFortune [] = Left "MarshalError: Expected 2 columns for Fortune, found 0"
  138. decodeFortune [_] = Left "MarshalError: Expected 2 columns for Fortune, found 1"
  139. decodeFortune (c1 : c2 : _) = Types.Fortune <$> intValDec c1 <*> textValDec c2
  140. queryFortunes :: Pool -> IO (Either Error [Types.Fortune])
  141. queryFortunes dbPool = Pool.withResource dbPool $ \conn -> do
  142. (_, rowsS) <- MySQL.query_ conn "SELECT * FROM Fortune"
  143. rows <- Streams.toList rowsS
  144. let eFortunes = fmap decodeFortune rows
  145. let (err, oks) = Either.partitionEithers eFortunes
  146. return $ case err of
  147. [] -> pure oks
  148. w : _ -> Left w