selda-0.5.2.0: src/Database/Selda/Frontend.hs
{-# LANGUAGE ScopedTypeVariables, FlexibleContexts, OverloadedStrings #-}
-- | API for running Selda operations over databases.
module Database.Selda.Frontend
( Result, Res, MonadIO (..), MonadSelda (..), SeldaT, OnError (..)
, query, queryInto
, insert, insert_, insertWithPK, tryInsert, insertWhen, insertUnless
, update, update_, upsert
, deleteFrom, deleteFrom_
, createTable, tryCreateTable, createTableWithoutIndexes, createTableIndexes
, dropTable, tryDropTable
, transaction, withoutForeignKeyEnforcement
) where
import Database.Selda.Backend.Internal
( SqlValue,
Param,
SeldaT,
MonadSelda(..),
SeldaBackend(runStmtWithPK, disableForeignKeys, ppConfig, runStmt),
QueryRunner,
SeldaError(SqlError),
withBackend )
import Database.Selda.Column ( Row, Col )
import Database.Selda.Compile
( Result,
Res,
compileWith,
compileInsert,
compileUpdate,
compileDelete,
buildResult )
import Database.Selda.Generic ( Relational )
import Database.Selda.Query.Type ( Query )
import Database.Selda.SqlType (ID, invalidId, toId)
import Database.Selda.Table.Type
( Table(tableName, tableHasAutoPK) )
import Database.Selda.Table.Compile
( OnError(..),
compileCreateTable,
compileCreateIndexes,
compileDropTable )
import Database.Selda.Types (fromTableName)
import Data.Proxy ( Proxy(..) )
import Data.Text (Text)
import Control.Monad ( void )
import Control.Monad.Catch
( bracket_,
onException,
try,
MonadCatch,
MonadMask(mask),
MonadThrow(throwM) )
import Control.Monad.IO.Class ( MonadIO(..) )
-- | Run a query within a Selda monad. In practice, this is often a 'SeldaT'
-- transformer on top of some other monad.
-- Selda transformers are entered using backend-specific @withX@ functions,
-- such as 'withSQLite' from the SQLite backend.
query :: (MonadSelda m, Result a) => Query (Backend m) a -> m [Res a]
query q = withBackend (flip queryWith q . runStmt)
-- | Perform the given query, and insert the result into the given table.
-- Returns the number of inserted rows.
queryInto :: (MonadSelda m, Relational a)
=> Table a
-> Query (Backend m) (Row (Backend m) a)
-> m Int
queryInto tbl q = withBackend $ \b -> do
let (qry, ps) = compileWith (ppConfig b) q
qry' = mconcat ["INSERT INTO ", tblName, " ", qry]
fmap fst . liftIO $ runStmt b qry' ps
where
tblName = fromTableName (tableName tbl)
-- | Insert the given values into the given table. All columns of the table
-- must be present. If your table has an auto-incrementing primary key,
-- use the special value 'def' for that column to get the auto-incrementing
-- behavior.
-- Returns the number of rows that were inserted.
--
-- To insert a list of tuples into a table with auto-incrementing primary key:
--
-- > data Person = Person
-- > { id :: ID Person
-- > , name :: Text
-- > , age :: Int
-- > , pet :: Maybe Text
-- > } deriving Generic
-- > instance SqlResult Person
-- >
-- > people :: Table Person
-- > people = table "people" [autoPrimary :- id]
-- >
-- > main = withSQLite "my_database.sqlite" $ do
-- > insert_ people
-- > [ Person def "Link" 125 (Just "horse")
-- > , Person def "Zelda" 119 Nothing
-- > , ...
-- > ]
--
-- Note that if one or more of the inserted rows would cause a constraint
-- violation, NO rows will be inserted; the whole insertion fails atomically.
insert :: (MonadSelda m, Relational a) => Table a -> [a] -> m Int
insert _ [] = do
return 0
insert t cs = withBackend $ \b -> do
sum <$> mapM (uncurry exec) (compileInsert (ppConfig b) t cs)
-- | Attempt to insert a list of rows into a table, but don't raise an error
-- if the insertion fails. Returns @True@ if the insertion succeeded, otherwise
-- @False@.
--
-- Like 'insert', if even one of the inserted rows would cause a constraint
-- violation, the whole insert operation fails.
tryInsert :: (MonadSelda m, MonadCatch m, Relational a) => Table a -> [a] -> m Bool
tryInsert tbl row = do
mres <- try $ insert tbl row
case mres of
Right _ -> return True
Left (SqlError _) -> return False
Left e -> throwM e
-- | Attempt to perform the given update. If no rows were updated, insert the
-- given row.
-- Returns the primary key of the inserted row, if the insert was performed.
-- Calling this function on a table which does not have a primary key will
-- return @Just id@ on a successful insert, where @id@ is a row identifier
-- guaranteed to not match any row in any table.
--
-- Note that this may perform two separate queries: one update, potentially
-- followed by one insert.
upsert :: (MonadSelda m, MonadMask m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> (Row (Backend m) a -> Row (Backend m) a)
-> [a]
-> m (Maybe (ID a))
upsert tbl check upd rows = transaction $ do
updated <- update tbl check upd
if updated == 0
then Just <$> insertWithPK tbl rows
else pure Nothing
-- | Perform the given insert, if no rows already present in the table match
-- the given predicate.
-- Returns the primary key of the last inserted row,
-- if the insert was performed.
-- If called on a table which doesn't have an auto-incrementing primary key,
-- @Just id@ is always returned on successful insert, where @id@ is a row
-- identifier guaranteed to not match any row in any table.
insertUnless :: (MonadSelda m, MonadMask m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> [a]
-> m (Maybe (ID a))
insertUnless tbl check rows = upsert tbl check id rows
-- | Like 'insertUnless', but performs the insert when at least one row matches
-- the predicate.
insertWhen :: (MonadSelda m, MonadMask m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> [a]
-> m (Maybe (ID a))
insertWhen tbl check rows = transaction $ do
matches <- update tbl check id
if matches > 0
then Just <$> insertWithPK tbl rows
else pure Nothing
-- | Like 'insert', but does not return anything.
-- Use this when you really don't care about how many rows were inserted.
insert_ :: (MonadSelda m, Relational a) => Table a -> [a] -> m ()
insert_ t cs = void $ insert t cs
-- | Like 'insert', but returns the primary key of the last inserted row.
-- Attempting to run this operation on a table without an auto-incrementing
-- primary key will always return a row identifier that is guaranteed to not
-- match any row in any table.
insertWithPK :: (MonadSelda m, Relational a) => Table a -> [a] -> m (ID a)
insertWithPK t cs = withBackend $ \b -> do
if tableHasAutoPK t
then do
res <- liftIO $ do
mapM (uncurry (runStmtWithPK b)) $ compileInsert (ppConfig b) t cs
return $ toId (last res)
else do
insert_ t cs
return invalidId
-- | Update the given table using the given update function, for all rows
-- matching the given predicate. Returns the number of updated rows.
update :: (MonadSelda m, Relational a)
=> Table a -- ^ Table to update.
-> (Row (Backend m) a -> Col (Backend m) Bool) -- ^ Predicate.
-> (Row (Backend m) a -> Row (Backend m) a) -- ^ Update function.
-> m Int
update tbl check upd = withBackend $ \b -> do
res <- uncurry exec $ compileUpdate (ppConfig b) tbl upd check
return res
-- | Like 'update', but doesn't return the number of updated rows.
update_ :: (MonadSelda m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> (Row (Backend m) a -> Row (Backend m) a)
-> m ()
update_ tbl check upd = void $ update tbl check upd
-- | From the given table, delete all rows matching the given predicate.
-- Returns the number of deleted rows.
deleteFrom :: (MonadSelda m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> m Int
deleteFrom tbl f = withBackend $ \b -> do
res <- uncurry exec $ compileDelete (ppConfig b) tbl f
return res
-- | Like 'deleteFrom', but does not return the number of deleted rows.
deleteFrom_ :: (MonadSelda m, Relational a)
=> Table a
-> (Row (Backend m) a -> Col (Backend m) Bool)
-> m ()
deleteFrom_ tbl f = void $ deleteFrom tbl f
-- | Create a table from the given schema.
createTable :: MonadSelda m => Table a -> m ()
createTable tbl = do
createTableWithoutIndexes Fail tbl
createTableIndexes Fail tbl
-- | Create a table from the given schema, but don't create any indexes.
createTableWithoutIndexes :: MonadSelda m => OnError -> Table a -> m ()
createTableWithoutIndexes onerror tbl = withBackend $ \b -> do
void $ exec (compileCreateTable (ppConfig b) onerror tbl) []
-- | Create all indexes for the given table. Fails if any of the table's indexes
-- already exists.
createTableIndexes :: MonadSelda m => OnError -> Table a -> m ()
createTableIndexes ifex tbl = withBackend $ \b -> do
mapM_ (flip exec []) $ compileCreateIndexes (ppConfig b) ifex tbl
-- | Create a table from the given schema, unless it already exists.
tryCreateTable :: MonadSelda m => Table a -> m ()
tryCreateTable tbl = do
createTableWithoutIndexes Ignore tbl
createTableIndexes Ignore tbl
-- | Drop the given table.
dropTable :: MonadSelda m => Table a -> m ()
dropTable = void . flip exec [] . compileDropTable Fail
-- | Drop the given table, if it exists.
tryDropTable :: MonadSelda m => Table a -> m ()
tryDropTable = void . flip exec [] . compileDropTable Ignore
-- | Perform the given computation atomically.
-- If an exception is raised during its execution, the entire transaction
-- will be rolled back and the exception re-thrown, even if the exception
-- is caught and handled within the transaction.
transaction :: (MonadSelda m, MonadMask m) => m a -> m a
transaction m = mask $ \restore -> transact $ do
void $ exec "BEGIN TRANSACTION" []
x <- restore m `onException` void (exec "ROLLBACK" [])
void $ exec "COMMIT" []
return x
-- | Run the given computation as a transaction without enforcing foreign key
-- constraints.
--
-- If the computation finishes with the database in an inconsistent state
-- with regards to foreign keys, the resulting behavior is undefined.
-- Use with extreme caution, preferably only for migrations.
--
-- On the PostgreSQL backend, at least PostgreSQL 9.6 is required.
--
-- Using this should be avoided in favor of deferred foreign key
-- constraints. See SQL backend documentation for deferred constraints.
withoutForeignKeyEnforcement :: (MonadSelda m, MonadMask m) => m a -> m a
withoutForeignKeyEnforcement m = withBackend $ \b -> do
bracket_ (liftIO $ disableForeignKeys b True)
(liftIO $ disableForeignKeys b False)
m
-- | Build the final result from a list of result columns.
queryWith :: forall m a. (MonadSelda m, Result a)
=> QueryRunner (Int, [[SqlValue]]) -> Query (Backend m) a -> m [Res a]
queryWith run q = withBackend $ \b -> do
res <- fmap snd . liftIO . uncurry run $ compileWith (ppConfig b) q
return $ mkResults (Proxy :: Proxy a) res
-- | Generate the final result of a query from a list of untyped result rows.
mkResults :: Result a => Proxy a -> [[SqlValue]] -> [Res a]
mkResults p = map (buildResult p)
{-# INLINE exec #-}
-- | Execute a statement without a result.
exec :: MonadSelda m => Text -> [Param] -> m Int
exec q ps = withBackend $ \b -> liftIO $ execIO b q ps
{-# INLINE execIO #-}
-- | Like 'exec', but in 'IO'.
execIO :: SeldaBackend b -> Text -> [Param] -> IO Int
execIO backend q ps = fmap fst $ runStmt backend q ps