packages feed

beam-0.3.2.0: src/Database/Beam/Query.hs

{-# OPTIONS_GHC -fwarn-incomplete-patterns #-}
module Database.Beam.Query
    ( -- * Query type
      module Database.Beam.Query.Types

    -- * General query combinators
    , module Database.Beam.Query.Combinators

    -- * Run queries in MonadIO
    , beamTxn, insertInto, query, queryList, getOne
    , updateWhere, saveTo
    , deleteWhere, deleteFrom )
    where

import Database.Beam.Query.Types
import Database.Beam.Query.Combinators
import Database.Beam.Query.Internal

import Database.Beam.Schema.Tables
import Database.Beam.Schema.Fields
import Database.Beam.Internal
import Database.Beam.SQL
import Database.Beam.SQL.Types

import Control.Applicative
import Control.Arrow
import Control.Monad.Trans
import Control.Monad.Writer (tell, execWriter, Writer)
import Control.Monad.State
import Control.Monad.Error
import Control.Monad.Identity

import Data.Monoid hiding (All)
import Data.Proxy
import Data.Data
import Data.List (find)
import Data.Maybe
import Data.Convertible
import Data.String (fromString)
import Data.Conduit
import qualified Data.Conduit.List as C
import qualified Data.Set as S
import qualified Data.Text as T

import Database.HDBC

import Unsafe.Coerce

-- * Query

runSQL' :: IConnection conn => Bool -> conn -> SQLCommand -> IO (Either String (IO (Maybe [SqlValue])))
runSQL' debug conn cmd = do
  let (sql, vals) = ppSQL cmd
  when debug (putStrLn ("Will execute " ++ sql ++ " with " ++ show vals))
  stmt <- prepare conn sql
  execute stmt vals
  return (Right (fetchRow stmt))

-- * Data insertion, updating, and deletion

insertToSQL :: Table table => T.Text -> table Identity -> SQLInsert
insertToSQL name (table :: table Identity) = SQLInsert name
                                                       (fieldAllValues (\(Columnar' (SqlValue' x)) -> x) (makeSqlValues table))

runInsert :: (MonadIO m, Table table, FromSqlValues (table Identity)) => T.Text -> table Identity -> Beam d m -> m (Either String (table Identity))
runInsert tableName (table :: table Identity) beam =
    do let insertCmd = Insert sqlInsert
           sqlInsert@(SQLInsert tblName sqlValues) = insertToSQL tableName table
       withHDBCConnection beam (\conn -> liftIO (runSQL' (beamDebug beam) conn insertCmd))

       -- There are three possibilities here:
       --
       --   * we have no autoincrement keys, and so we simply have to return the
       --     newly created QueryTable, or
       --   * we have autoincrement keys, but all the fields marked autoincrement
       --     were non-null. In this case, we have all the information needed to
       --     construct the QueryTable, or
       --   * we have autoincrement keys, and some of the fields were marked null.
       --     In this case, we need to ask the backend for the last inserted row.
       let tableSchema = reifyTableSchema (Proxy :: Proxy table)

           autoIncrementsAreNull = zipWith (\(_, columnSchema) value -> hasAutoIncrementConstraint columnSchema && value == SqlNull) tableSchema sqlValues
           hasNullAutoIncrements = or autoIncrementsAreNull

           hasAutoIncrementConstraint SQLColumnSchema { csConstraints = cs } = isJust (find (== SQLAutoIncrement) cs)

       insertedValues <- if hasNullAutoIncrements
                         then getLastInsertedRow beam tblName
                         else return sqlValues
       return (fromSqlValues insertedValues)

-- | Insert the given row value into the table specified by the first argument.
insertInto :: (MonadIO m, Functor m, Table table, FromSqlValues (table Identity)) =>
              DatabaseTable db table -> table Identity -> BeamT e db m (table Identity)
insertInto (DatabaseTable _ name) data_ =
    BeamT (\beam -> toBeamResult <$> runInsert name data_ beam)

updateToSQL :: Table table => T.Text -> table (QExpr s) -> QExpr s Bool -> Maybe SQLUpdate
updateToSQL tblName (setTo :: table (QExpr s)) where_ =
    let setExprs = fieldAllValues (\(Columnar' x) -> optimizeExpr x) setTo
        setColumns = fieldAllValues (\(Columnar' fieldS) -> _fieldName fieldS) (tblFieldSettings :: TableSettings table)

        isInteresting columnName (SQLFieldE (SQLFieldName fName))
            | fName == columnName = Nothing
        isInteresting columnName newE = Just (SQLFieldName columnName, newE)

        assignments = catMaybes (zipWith isInteresting setColumns setExprs)

        where_' = case optimizeExpr where_ of
                    SQLValE (SqlBool True) -> Nothing
                    where_' -> Just where_'

    in case assignments of
         [] -> Nothing
         _  -> Just SQLUpdate
               { uTableNames = [tblName]
               , uAssignments = assignments
               , uWhere = where_' }

-- | Update every entry in the given table where the third argument yields true, using the second
-- argument to give the new values.
updateWhere :: (MonadIO m, Table tbl) => DatabaseTable db tbl -> (tbl (QExpr s) -> tbl (QExpr s)) -> (tbl (QExpr s) -> QExpr s Bool) -> BeamT e db m ()
updateWhere tbl@(DatabaseTable _ name :: DatabaseTable db tbl) mkAssignments mkWhere =
    do let assignments = mkAssignments tblExprs
           where_ = mkWhere tblExprs

           tblExprs = changeRep (\(Columnar' fieldS) -> Columnar' (QExpr (SQLFieldE (QField name Nothing (_fieldName fieldS))))) (tblFieldSettings :: TableSettings tbl)

       case updateToSQL name assignments where_ of
         Nothing -> pure () -- Assignments were empty, so do nothing
         Just upd ->
             let updateCmd = Update upd
             in BeamT $ \beam ->
                 withHDBCConnection beam $ \conn ->
                     do liftIO (runSQL' (beamDebug beam) conn updateCmd)
                        pure (Success ())

-- | Use the 'PrimaryKey' of the given table entry to update the corresponding table row in the
-- database.
saveTo :: (MonadIO m, Table tbl) => DatabaseTable db tbl -> tbl Identity -> BeamT e db m ()
saveTo tbl (newValues :: tbl Identity) =
    updateWhere tbl (\_ -> tableVal newValues) (val_ (primaryKey newValues) `references_`)

-- | Delete all entries in the given table matched by the expression
deleteWhere :: (MonadIO m, Table tbl) => DatabaseTable db tbl -> (tbl (QExpr s) -> QExpr s Bool) -> BeamT e db m ()
deleteWhere (DatabaseTable _ name :: DatabaseTable db tbl) mkWhere =
    let tblExprs = changeRep (\(Columnar' fieldS) -> Columnar' (QExpr (SQLFieldE (QField name Nothing (_fieldName fieldS))))) (tblFieldSettings :: TableSettings tbl)

        cmd = Delete SQLDelete
              { dTableName = name
              , dWhere = case optimizeExpr (mkWhere tblExprs) of
                           SQLValE (SqlBool True) -> Nothing
                           where_ -> Just where_ }
    in BeamT $ \beam ->
        withHDBCConnection beam $ \conn ->
            do liftIO (runSQL' (beamDebug beam) conn cmd)
               pure (Success ())

-- | Delete the entry referenced by the given 'PrimaryKey' in the given table.
deleteFrom :: (MonadIO m, Table tbl) => DatabaseTable db tbl -> PrimaryKey tbl Identity -> BeamT e db m ()
deleteFrom tbl pkToDelete = deleteWhere tbl (\tbl -> primaryKey tbl ==. val_ pkToDelete)

-- * BeamT actions

-- | Run the 'BeamT' action in a database transaction. On successful
-- completion, the transaction will be committed. Use 'throwError' to
-- stop the transaction and report an error.
beamTxn :: MonadIO m => Beam db m -> (DatabaseSettings db -> BeamT e db m a) -> m (BeamResult e a)
beamTxn beam action = do res <- runBeamT (action (beamDbSettings beam)) beam
                         withHDBCConnection beam $
                             case res of
                               Success  x -> liftIO . commit
                               Rollback e -> liftIO . rollback
                         return res

toBeamResult :: Either String a -> BeamResult e a
toBeamResult = (Rollback . InternalError) ||| Success

runQuery :: ( MonadIO m
            , FromSqlValues (QExprToIdentity a)
            , Projectible a
            , IsQuery q ) =>
            q db s a -> Beam db m -> m (Either String (Source m (QExprToIdentity a)))
runQuery q beam =
    do let selectCmd = Select select
           (_, _, select) = queryToSQL' (toQ q) 0

       res <- withHDBCConnection beam $ \conn ->
                liftIO $ runSQL' (beamDebug beam) conn selectCmd

       case res of
         Left err -> return (Left err)
         Right fetchRow -> do let source = do row <- liftIO fetchRow
                                              case row of
                                                Just row ->
                                                    case fromSqlValues row of
                                                      Left err -> fail err
                                                      Right  q -> do yield q
                                                                     source
                                                Nothing -> return ()
                              return (Right source)

-- | Run the given query in the transaction and yield a 'Source' that can be used to read results
-- incrementally. If your result set is small and you want to just get a list, use 'queryList'.
query :: (IsQuery q, MonadIO m, Functor m, FromSqlValues (QExprToIdentity a), Projectible a) => q db () a -> BeamT e db m (Source (BeamT e db m) (QExprToIdentity a))
query q = BeamT $ \beam ->
          do res <- runQuery q beam
             case res of
               Right x -> return (Success (transPipe (BeamT . const . fmap Success) x))
               Left err -> return (Rollback (InternalError err))

-- | Execute 'query' and use the 'Data.Conduit.List.consume' function to return a list of
-- results. Best used for small result sets.
queryList :: (IsQuery q, MonadIO m, Functor m, FromSqlValues (QExprToIdentity a), Projectible a) => q db () a -> BeamT e db m [QExprToIdentity a]
queryList q = do src <- query q
                 src $$ C.consume

-- | Execute the query using 'query' and return exactly one result. The return value will be
-- 'Nothing' if either zero or more than one values were returned.
getOne :: (IsQuery q, MonadIO m, Functor m, FromSqlValues (QExprToIdentity a), Projectible a) => q db () a -> BeamT e db m (Maybe (QExprToIdentity a))
getOne q =
    do let justOneSink = await >>= \x ->
                         case x of
                           Nothing -> return Nothing
                           Just  x -> noMoreSink x
           noMoreSink x = await >>= \nothing ->
                          case nothing of
                            Nothing -> return (Just x)
                            Just  _ -> return Nothing
       src <- query q
       src $$ justOneSink

fromSqlValues :: FromSqlValues a => [SqlValue] -> Either String a
fromSqlValues vals =
    case runState (runErrorT fromSqlValues') vals of
      (Right a, []) -> Right a
      (Right _,  _) -> Left "fromSqlValues: Not all values were consumed"
      (Left err, _) -> Left err