sqlite-easy-1.1.0.0: src/Database/Sqlite/Easy/Internal.hs
{-# language RecordWildCards #-}
{-# language DerivingVia #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
-- | The implementation of sqlite-easy.
--
-- This module is unstable and may change at any time.
module Database.Sqlite.Easy.Internal where
import Database.SQLite3 (Database, SQLData, Statement)
import qualified Database.SQLite3 as Direct
import Data.String (IsString, fromString)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable
import Data.Pool
import Control.Monad.Reader
import Control.Exception
import Control.Monad.IO.Unlift
-- * Connection
-- | A SQLite3 connection string
newtype ConnectionString
= ConnectionString
{ unConnectionString :: Text
}
deriving IsString via Text
deriving Show
-- | Create a pool of a sqlite3 db with a specific connection string.
-- This also sets a few default pragmas.
createSqlitePool :: ConnectionString -> IO (Pool Database)
createSqlitePool connStr =
newPool $ defaultPoolConfig
(openWith connStr defaultPragmas)
Direct.close
180
50
-- | Create a pool of a sqlite3 db with a specific connection string.
-- This will also run the supplied SQL statements after establishing
-- each connection.
createSqlitePoolWith :: ConnectionString -> [SQL] -> IO (Pool Database)
createSqlitePoolWith connStr statements =
newPool $ defaultPoolConfig
(openWith connStr statements)
Direct.close
180
50
-- | Open a connection to a database, run the supplied statements,
-- and return the connection.
openWith :: ConnectionString -> [SQL] -> IO Database
openWith (ConnectionString connStr) sqls = do
conn <- Direct.open connStr
Direct.exec
conn
(T.unlines $ map (\(SQL sql) -> sql <> "; ") sqls)
pure conn
-- | Default pragmas to be set when opening a connection from a pool.
defaultPragmas :: [SQL]
defaultPragmas =
[ -- https://www.sqlite.org/pragma.html#pragma_foreign_keys
"PRAGMA foreign_keys = on"
, -- https://www.sqlite.org/pragma.html#pragma_busy_timeout
"PRAGMA busy_timeout = 30000"
]
-- | Open a database, run some stuff, close the database.
withDb :: ConnectionString -> SQLite a -> IO a
withDb (ConnectionString connStr) =
bracket (Direct.open connStr) Direct.close . flip runSQLite
-- | Use an active database connection to run some stuff on a database.
withDatabase :: Database -> SQLite a -> IO a
withDatabase = runSQLite
-- | Use a resource pool to run some stuff on a database.
withPool :: Pool Database -> SQLite a -> IO a
withPool pool = withResource pool . flip runSQLite
-- * Execution
-- | A SQL statement
newtype SQL
= SQL
{ unSQL :: Text
}
deriving (Semigroup, IsString) via Text
deriving Show
-- | Run a SQL statement on a database and fetch the results.
run :: SQL -> SQLite [[SQLData]]
run (SQL stmt) = do
db <- getDB
liftIO $ bracket (Direct.prepare db stmt) Direct.finalize fetchAll
-- | Run a SQL statement with certain parameters on a database and fetch the results.
runWith :: SQL -> [SQLData] -> SQLite [[SQLData]]
runWith (SQL stmt) params = do
db <- getDB
liftIO $ do
bracket (Direct.prepare db stmt) Direct.finalize $ \preparedStmt -> do
Direct.bind preparedStmt params
fetchAll preparedStmt
-- | Run a statement and fetch all of the data.
fetchAll :: Statement -> IO [[SQLData]]
fetchAll stmt = do
res <- Direct.step stmt
case res of
Direct.Row -> do
row <- Direct.columns stmt
rows <- fetchAll stmt
pure (row : rows)
Direct.Done ->
pure []
-- * Transaction
-- | The type of actions to run on a SQLite database.
-- In essence, it is almost the same as @Database -> IO a@.
--
-- 'SQLite' actions can be created with the 'run' and 'runWith'
-- functions, and can be composed using the type class instances.
--
-- 'SQLite' actions can be run with the 'withDb', 'withDatabase',
-- and 'withPool' functions.
newtype SQLite a
= SQLite
{ unSQLite :: SQLiteStuff -> IO a
}
deriving (Functor, Applicative, Monad, MonadIO, MonadFail, MonadUnliftIO)
via ReaderT SQLiteStuff IO
instance Semigroup a => Semigroup (SQLite a) where
a <> b = (<>) <$> a <*> b
instance Monoid a => Monoid (SQLite a) where
mempty = pure mempty
data SQLiteStuff
= SQLiteStuff
{ dbConn :: Database
, transactionNumber :: Maybe Int
}
getDB :: SQLite Database
getDB = SQLite (\(SQLiteStuff dbConn _) -> pure dbConn)
runSQLite :: Database -> SQLite a -> IO a
runSQLite db t = (unSQLite t) (SQLiteStuff db Nothing)
-- | Run operations as a transaction.
-- If the action throws an error, the transaction is rolled back.
-- For more information, visit: <https://www.sqlite.org/lang_transaction.html>
transaction :: forall a. Typeable a => SQLite a -> SQLite a
transaction action = do
SQLiteStuff {..} <- SQLite $ \stuff -> pure stuff
case transactionNumber of
Nothing -> do
let
runIO sql = (unSQLite (run sql)) (SQLiteStuff dbConn Nothing)
commit = runIO "COMMIT"
rollback' = runIO "ROLLBACK"
[] <- run "BEGIN"
liftIO $ catches
((unSQLite action) (SQLiteStuff dbConn (Just 1)) <* commit)
[ Handler $ \(RollbackCurrent a) -> rollback' *> pure a
, Handler $ \(RollbackAll a) -> rollback' *> pure a
, Handler $ \(ex :: SomeException) -> rollback' *> throwIO ex
]
Just n -> do
let
runIO sql = (unSQLite (run sql)) (SQLiteStuff dbConn Nothing)
transactionName = "'sqlite_easy_transaction_" <> fromString (show n) <> "'"
release = runIO $ "RELEASE SAVEPOINT " <> transactionName
rollbackCurrent = runIO $ "ROLLBACK TRANSACTION TO SAVEPOINT " <> transactionName
[] <- run $ "SAVEPOINT " <> transactionName
liftIO $ catches
((unSQLite action) (SQLiteStuff dbConn (Just (n + 1))) <* release)
[ Handler $ \(RollbackCurrent a) -> rollbackCurrent *> pure a
, Handler $ \(ex :: RollbackAll a) -> rollbackCurrent *> throwIO ex
, Handler $ \(ex :: SomeException) -> rollbackCurrent *> throwIO ex
]
asTransaction' :: Database -> IO a -> IO a
asTransaction' db action = do
let
runIO sql = (unSQLite (run sql)) (SQLiteStuff db Nothing)
[] <- runIO "BEGIN"
catches
(action <* runIO "COMMIT")
[ Handler $ \(ex :: SomeException) -> runIO "ROLLBACK" *> throwIO ex
]
-- | Rollback the current (inner-most) transaction by supplying the return value.
-- To be used inside transactions.
rollback :: Typeable a => a -> SQLite a
rollback = liftIO . throwIO . RollbackCurrent
-- | Rollback all transaction structure by supplying the return value.
-- To be used inside transactions.
rollbackAll :: Typeable a => a -> SQLite a
rollbackAll = liftIO . throwIO . RollbackAll
data RollbackCurrent a
= RollbackCurrent a
instance Show (RollbackCurrent a) where
show RollbackCurrent{} = "RollbackCurrent"
instance (Typeable a) => Exception (RollbackCurrent a)
data RollbackAll a
= RollbackAll a
instance Show (RollbackAll a) where
show RollbackAll{} = "RollbackAll"
instance (Typeable a) => Exception (RollbackAll a)