beam-mysql-0.2.0.0: Database/Beam/MySQL/Connection.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE CPP #-}
module Database.Beam.MySQL.Connection
( MySQL(..), MySQL.Connection
, MySQLM(..)
, runBeamMySQL, runBeamMySQLDebug
, MysqlCommandSyntax(..)
, MysqlSelectSyntax(..), MysqlInsertSyntax(..)
, MysqlUpdateSyntax(..), MysqlDeleteSyntax(..)
, MysqlExpressionSyntax(..)
, MySQL.connect, MySQL.close
, mysqlUriSyntax ) where
import Database.Beam.MySQL.Syntax
import Database.Beam.MySQL.FromField
import Database.Beam.Backend
import Database.Beam.Backend.URI
import Database.Beam.Query
import Database.Beam.Query.SQL92
import Database.MySQL.Base as MySQL
import qualified Database.MySQL.Base.Types as MySQL
import Control.Exception
import Control.Monad.Except
import Control.Monad.Fail (MonadFail)
import qualified Control.Monad.Fail as Fail
import Control.Monad.Free.Church
import Control.Monad.Reader
import qualified Data.Aeson as A (Value)
import Data.ByteString.Builder
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Data.Int
import Data.List
import Data.Maybe
import Data.Ratio
import Data.Scientific
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import Data.Time (Day, LocalTime, NominalDiffTime, TimeOfDay)
import Data.Word
import Network.URI
import Text.Read hiding (step)
data MySQL = MySQL
instance BeamSqlBackendIsString MySQL String
instance BeamSqlBackendIsString MySQL T.Text
instance BeamBackend MySQL where
type BackendFromField MySQL = FromField
instance BeamSqlBackend MySQL
type instance BeamSqlBackendSyntax MySQL = MysqlCommandSyntax
newtype MySQLM a = MySQLM (ReaderT (String -> IO (), Connection) IO a)
deriving (Monad, MonadIO, Applicative, Functor)
instance MonadFail MySQLM where
fail e = fail $ "Internal Error with: " <> show e
data NotEnoughColumns
= NotEnoughColumns
{ _errColCount :: Int
} deriving Show
instance Exception NotEnoughColumns where
displayException (NotEnoughColumns colCnt) =
mconcat [ "Not enough columns while reading MySQL row. Only have "
, show colCnt, " column(s)" ]
data CouldNotReadColumn
= CouldNotReadColumn
{ _errColIndex :: Int
, _errColMsg :: String }
deriving Show
instance Exception CouldNotReadColumn where
displayException (CouldNotReadColumn idx msg) =
mconcat [ "Could not read column ", show idx, ": ", msg ]
runBeamMySQLDebug :: (String -> IO ()) -> Connection -> MySQLM a -> IO a
runBeamMySQLDebug = withMySQL
runBeamMySQL :: Connection -> MySQLM a -> IO a
runBeamMySQL = runBeamMySQLDebug (\_ -> pure ())
instance MonadBeam MySQL MySQLM where
runReturningMany (MysqlCommandSyntax (MysqlSyntax cmd))
(consume :: MySQLM (Maybe x) -> MySQLM a) =
MySQLM . ReaderT $ \(dbg, conn) -> do
cmdBuilder <- cmd (\_ b _ -> pure b) (MySQL.escape conn) mempty conn
let cmdStr = BL.toStrict (toLazyByteString cmdBuilder)
dbg (T.unpack (TE.decodeUtf8 cmdStr))
MySQL.query conn cmdStr
bracket (useResult conn) freeResult $ \res -> do
fieldDescs <- MySQL.fetchFields res
let fetchRow' :: MySQLM (Maybe x)
fetchRow' =
MySQLM . ReaderT $ \_ -> do
fields <- MySQL.fetchRow res
case fields of
[] -> pure Nothing
_ -> do
let FromBackendRowM go = fromBackendRow
rowRes <- runF go (\x _ _ -> pure (Right x)) step
0 (zip fieldDescs fields)
case rowRes of
Left err -> throwIO err
Right x -> pure (Just x)
parseField :: forall field. FromField field
=> MySQL.Field -> Maybe BS.ByteString
-> IO (Either ColumnParseError field)
parseField ty d = runExceptT (fromField ty d)
step :: forall y
. FromBackendRowF MySQL (Int -> [(MySQL.Field, Maybe BS.ByteString)] -> IO (Either BeamRowReadError y))
-> Int -> [(MySQL.Field, Maybe BS.ByteString)] -> IO (Either BeamRowReadError y)
step (ParseOneField _) curCol [] =
pure (Left (BeamRowReadError (Just curCol) (ColumnNotEnoughColumns curCol)))
step (ParseOneField next) curCol ((desc, field):fields) =
do d <- parseField desc field
case d of
Left e -> pure (Left (BeamRowReadError (Just curCol) e))
Right d' -> next d' (curCol + 1) fields
step (Alt (FromBackendRowM a) (FromBackendRowM b) next) curCol cols =
do aRes <- runF a (\x curCol' cols' -> pure (Right (next x curCol' cols'))) step curCol cols
case aRes of
Right next' -> next'
Left aErr -> do
bRes <- runF b (\x curCol' cols' -> pure (Right (next x curCol' cols'))) step curCol cols
case bRes of
Right next' -> next'
Left _ -> pure (Left aErr)
step (FailParseWith err) _ _ = pure (Left err)
MySQLM doConsume = consume fetchRow'
runReaderT doConsume (dbg, conn)
withMySQL :: (String -> IO ()) -> Connection
-> MySQLM a -> IO a
withMySQL dbg conn (MySQLM a) =
runReaderT a (dbg, conn)
mysqlUriSyntax :: c MySQL Connection MySQLM
-> BeamURIOpeners c
mysqlUriSyntax =
mkUriOpener (withMySQL (const (pure ()))) "mysql:"
(\uri ->
let stripSuffix s a =
reverse <$> stripPrefix (reverse s) (reverse a)
(user, pw) =
fromMaybe ("root", "") $ do
userInfo <- fmap uriUserInfo (uriAuthority uri)
userInfo' <- stripSuffix "@" userInfo
let (user', pw') = break (== ':') userInfo'
pw'' = fromMaybe "" (stripPrefix ":" pw')
pure (user', pw'')
host =
fromMaybe "localhost" .
fmap uriRegName . uriAuthority $ uri
port =
fromMaybe 3306 $ do
portStr <- fmap uriPort (uriAuthority uri)
portStr' <- stripPrefix ":" portStr
readMaybe portStr'
db = fromMaybe "test" $
stripPrefix "/" (uriPath uri)
options =
fromMaybe [CharsetName "utf-8"] $ do
opts <- stripPrefix "?" (uriQuery uri)
let getKeyValuePairs "" a = a []
getKeyValuePairs d a =
let (keyValue, d') = break (=='&') d
attr = parseKeyValue keyValue
in getKeyValuePairs d' (a . maybe id (:) attr)
pure (getKeyValuePairs opts id)
parseBool (Just "true") = pure True
parseBool (Just "false") = pure False
parseBool _ = Nothing
parseKeyValue kv = do
let (key, value) = break (==':') kv
value' = stripPrefix ":" value
case (key, value') of
("connectTimeout", Just secs) ->
ConnectTimeout <$> readMaybe secs
( "compress", _) -> pure Compress
( "namedPipe", _ ) -> pure NamedPipe
( "initCommand", Just cmd ) ->
pure (InitCommand (BS.pack cmd))
( "readDefaultFile", Just fp ) ->
pure (ReadDefaultFile fp)
( "readDefaultGroup", Just grp ) ->
pure (ReadDefaultGroup (BS.pack grp))
( "charsetDir", Just fp ) ->
pure (CharsetDir fp)
( "charsetName", Just nm ) ->
pure (CharsetName nm)
( "localInFile", b ) ->
LocalInFile <$> parseBool b
( "protocol", Just p) ->
case p of
"tcp" -> pure (Protocol TCP)
"socket" -> pure (Protocol Socket)
"pipe" -> pure (Protocol Pipe)
"memory" -> pure (Protocol Memory)
_ -> Nothing
( "sharedMemoryBaseName", Just fp ) ->
pure (SharedMemoryBaseName (BS.pack fp))
( "readTimeout", Just secs ) ->
ReadTimeout <$> readMaybe secs
( "writeTimeout", Just secs ) ->
WriteTimeout <$> readMaybe secs
( "useRemoteConnection", _ ) -> pure UseRemoteConnection
( "useEmbeddedConnection", _ ) -> pure UseEmbeddedConnection
( "guessConnection", _ ) -> pure GuessConnection
( "clientIp", Just fp) ->
pure (ClientIP (BS.pack fp))
( "secureAuth", b ) ->
SecureAuth <$> parseBool b
( "reportDataTruncation", b ) ->
ReportDataTruncation <$> parseBool b
( "reconnect", b ) ->
Reconnect <$> parseBool b
( "sslVerifyServerCert", b) ->
SSLVerifyServerCert <$> parseBool b
( "foundRows", _ ) -> pure FoundRows
( "ignoreSIGPIPE", _ ) -> pure IgnoreSIGPIPE
( "ignoreSpace", _ ) -> pure IgnoreSpace
( "interactive", _ ) -> pure Interactive
( "localFiles", _ ) -> pure LocalFiles
( "multiResults", _ ) -> pure MultiResults
( "multiStatements", _ ) -> pure MultiStatements
( "noSchema", _ ) -> pure NoSchema
_ -> Nothing
connInfo = ConnectInfo
{ connectHost = host, connectPort = port
, connectUser = user, connectPassword = pw
, connectDatabase = db, connectOptions = options
, connectPath = "", connectSSL = Nothing }
in connect connInfo >>= \hdl -> pure (hdl, close hdl))
#define FROM_BACKEND_ROW(ty) instance FromBackendRow MySQL ty
FROM_BACKEND_ROW(Bool)
FROM_BACKEND_ROW(Word)
FROM_BACKEND_ROW(Word8)
FROM_BACKEND_ROW(Word16)
FROM_BACKEND_ROW(Word32)
FROM_BACKEND_ROW(Word64)
FROM_BACKEND_ROW(Int)
FROM_BACKEND_ROW(Int8)
FROM_BACKEND_ROW(Int16)
FROM_BACKEND_ROW(Int32)
FROM_BACKEND_ROW(Int64)
FROM_BACKEND_ROW(Float)
FROM_BACKEND_ROW(Double)
FROM_BACKEND_ROW(Scientific)
FROM_BACKEND_ROW((Ratio Integer))
FROM_BACKEND_ROW(BS.ByteString)
FROM_BACKEND_ROW(BL.ByteString)
FROM_BACKEND_ROW(T.Text)
FROM_BACKEND_ROW(TL.Text)
FROM_BACKEND_ROW(LocalTime)
FROM_BACKEND_ROW(A.Value)
FROM_BACKEND_ROW(SqlNull)
-- * Equality checks
#define HAS_MYSQL_EQUALITY_CHECK(ty) \
instance HasSqlEqualityCheck MySQL (ty); \
instance HasSqlQuantifiedEqualityCheck MySQL (ty);
HAS_MYSQL_EQUALITY_CHECK(Bool)
HAS_MYSQL_EQUALITY_CHECK(Double)
HAS_MYSQL_EQUALITY_CHECK(Float)
HAS_MYSQL_EQUALITY_CHECK(Int)
HAS_MYSQL_EQUALITY_CHECK(Int8)
HAS_MYSQL_EQUALITY_CHECK(Int16)
HAS_MYSQL_EQUALITY_CHECK(Int32)
HAS_MYSQL_EQUALITY_CHECK(Int64)
HAS_MYSQL_EQUALITY_CHECK(Integer)
HAS_MYSQL_EQUALITY_CHECK(Word)
HAS_MYSQL_EQUALITY_CHECK(Word8)
HAS_MYSQL_EQUALITY_CHECK(Word16)
HAS_MYSQL_EQUALITY_CHECK(Word32)
HAS_MYSQL_EQUALITY_CHECK(Word64)
HAS_MYSQL_EQUALITY_CHECK(T.Text)
HAS_MYSQL_EQUALITY_CHECK(TL.Text)
HAS_MYSQL_EQUALITY_CHECK([Char])
HAS_MYSQL_EQUALITY_CHECK(Scientific)
HAS_MYSQL_EQUALITY_CHECK(Day)
HAS_MYSQL_EQUALITY_CHECK(TimeOfDay)
HAS_MYSQL_EQUALITY_CHECK(NominalDiffTime)
HAS_MYSQL_EQUALITY_CHECK(LocalTime)
instance HasQBuilder MySQL where
buildSqlQuery = buildSql92Query' True