Add support to simple locking clauses.

This commit is contained in:
Felipe Lessa 2015-06-27 12:30:13 -03:00
parent e5150463ad
commit 9552180629
4 changed files with 123 additions and 18 deletions

View File

@ -39,7 +39,7 @@ module Database.Esqueleto
-- * @esqueleto@'s Language -- * @esqueleto@'s Language
Esqueleto( where_, on, groupBy, orderBy, rand, asc, desc, limit, offset Esqueleto( where_, on, groupBy, orderBy, rand, asc, desc, limit, offset
, distinct, distinctOn, don, distinctOnOrderBy, having , distinct, distinctOn, don, distinctOnOrderBy, having, locking
, sub_select, sub_selectDistinct, (^.), (?.) , sub_select, sub_selectDistinct, (^.), (?.)
, val, isNothing, just, nothing, joinV, countRows, count, not_ , val, isNothing, just, nothing, joinV, countRows, count, not_
, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.) , (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)
@ -61,6 +61,7 @@ module Database.Esqueleto
, ValueList(..) , ValueList(..)
, OrderBy , OrderBy
, DistinctOn , DistinctOn
, LockingKind(..)
-- ** Joins -- ** Joins
, InnerJoin(..) , InnerJoin(..)
, CrossJoin(..) , CrossJoin(..)

View File

@ -30,6 +30,7 @@ module Database.Esqueleto.Internal.Language
, DistinctOn , DistinctOn
, Update , Update
, Insertion , Insertion
, LockingKind(..)
-- * The guts -- * The guts
, JoinKind(..) , JoinKind(..)
, IsJoinKind(..) , IsJoinKind(..)
@ -273,6 +274,15 @@ class (Functor query, Applicative query, Monad query) =>
-- /Since: 1.2.2/ -- /Since: 1.2.2/
having :: expr (Value Bool) -> query () having :: expr (Value Bool) -> query ()
-- | Add a locking clause to the query. Please read
-- 'LockingKind' documentation and your RDBMS manual.
--
-- If multiple calls to 'locking' are made on the same query,
-- the last one is used.
--
-- /Since: 2.2.7/
locking :: LockingKind -> query ()
-- | Execute a subquery @SELECT@ in an expression. Returns a -- | Execute a subquery @SELECT@ in an expression. Returns a
-- simple value so should be used only when the @SELECT@ query -- simple value so should be used only when the @SELECT@ query
-- is guaranteed to return just one row. -- is guaranteed to return just one row.
@ -703,6 +713,31 @@ data Update typ
data Insertion a data Insertion a
-- | Different kinds of locking clauses supported by 'locking'.
--
-- Note that each RDBMS has different locking support. The
-- constructors of this datatype specify only the /syntax/ of the
-- locking mechanism, not its /semantics/. For example, even
-- though both MySQL and PostgreSQL support 'ForUpdate', there
-- are no guarantees that they will behave the same.
--
-- /Since: 2.2.7/
data LockingKind =
ForUpdate
-- ^ @FOR UPDATE@ syntax. Supported by MySQL, Oracle and
-- PostgreSQL.
--
-- /Since: 2.2.7/
| ForShare
-- ^ @FOR SHARE@ syntax. Supported by PostgreSQL.
--
-- /Since: 2.2.7/
| LockInShareMode
-- ^ @LOCK IN SHARE MODE@ syntax. Supported by MySQL.
--
-- /Since: 2.2.7/
-- | @FROM@ clause: bring entities into scope. -- | @FROM@ clause: bring entities into scope.
-- --
-- This function internally uses two type classes in order to -- This function internally uses two type classes in order to

View File

@ -58,7 +58,7 @@ import Control.Monad.Trans.Class (lift)
import qualified Control.Monad.Trans.Reader as R import qualified Control.Monad.Trans.Reader as R
import Data.Int (Int64) import Data.Int (Int64)
import Data.List (intersperse) import Data.List (intersperse)
import Data.Monoid (Monoid(..), (<>)) import Data.Monoid (Last(..), Monoid(..), (<>))
import Data.Proxy (Proxy(..)) import Data.Proxy (Proxy(..))
import Database.Esqueleto.Internal.PersistentImport import Database.Esqueleto.Internal.PersistentImport
import Database.Persist.Sql.Util ( import Database.Persist.Sql.Util (
@ -111,12 +111,13 @@ data SideData = SideData { sdDistinctClause :: !DistinctClause
, sdHavingClause :: !HavingClause , sdHavingClause :: !HavingClause
, sdOrderByClause :: ![OrderByClause] , sdOrderByClause :: ![OrderByClause]
, sdLimitClause :: !LimitClause , sdLimitClause :: !LimitClause
, sdLockingClause :: !LockingClause
} }
instance Monoid SideData where instance Monoid SideData where
mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty mempty
SideData d f s w g h o l `mappend` SideData d' f' s' w' g' h' o' l' = SideData d f s w g h o l k `mappend` SideData d' f' s' w' g' h' o' l' k' =
SideData (d <> d') (f <> f') (s <> s') (w <> w') (g <> g') (h <> h') (o <> o') (l <> l') SideData (d <> d') (f <> f') (s <> s') (w <> w') (g <> g') (h <> h') (o <> o') (l <> l') (k <> k')
-- | The @DISTINCT@ "clause". -- | The @DISTINCT@ "clause".
@ -211,6 +212,10 @@ instance Monoid LimitClause where
-- "reversed" arguments. -- "reversed" arguments.
-- | A locking clause.
type LockingClause = Last LockingKind
---------------------------------------------------------------------- ----------------------------------------------------------------------
@ -392,6 +397,8 @@ instance Esqueleto SqlQuery SqlExpr SqlBackend where
having expr = Q $ W.tell mempty { sdHavingClause = Where expr } having expr = Q $ W.tell mempty { sdHavingClause = Where expr }
locking kind = Q $ W.tell mempty { sdLockingClause = Last (Just kind) }
orderBy exprs = Q $ W.tell mempty { sdOrderByClause = exprs } orderBy exprs = Q $ W.tell mempty { sdOrderByClause = exprs }
asc = EOrderBy ASC asc = EOrderBy ASC
desc = EOrderBy DESC desc = EOrderBy DESC
@ -956,7 +963,8 @@ toRawSql mode (conn, firstIdentState) query =
groupByClause groupByClause
havingClause havingClause
orderByClauses orderByClauses
limitClause = sd limitClause
lockingClause = sd
-- Pass the finalIdentState (containing all identifiers -- Pass the finalIdentState (containing all identifiers
-- that were used) to the subsequent calls. This ensures -- that were used) to the subsequent calls. This ensures
-- that no name clashes will occur on subqueries that may -- that no name clashes will occur on subqueries that may
@ -972,6 +980,7 @@ toRawSql mode (conn, firstIdentState) query =
, makeHaving info havingClause , makeHaving info havingClause
, makeOrderBy info orderByClauses , makeOrderBy info orderByClauses
, makeLimit info limitClause orderByClauses , makeLimit info limitClause orderByClauses
, makeLocking lockingClause
] ]
@ -1112,6 +1121,15 @@ makeLimit (conn, _) (Limit ml mo) orderByClauses =
in (TLB.fromText limitRaw, mempty) in (TLB.fromText limitRaw, mempty)
makeLocking :: LockingClause -> (TLB.Builder, [PersistValue])
makeLocking = flip (,) [] . maybe mempty toTLB . getLast
where
toTLB ForUpdate = "\nFOR UPDATE"
toTLB ForShare = "\nFOR SHARE"
toTLB LockInShareMode = "\nLOCK IN SHARE MODE"
parens :: TLB.Builder -> TLB.Builder parens :: TLB.Builder -> TLB.Builder
parens b = "(" <> (b <> ")") parens b = "(" <> (b <> ")")

View File

@ -26,7 +26,9 @@ import Control.Monad.IO.Class (MonadIO(liftIO))
import Control.Monad.Logger (MonadLogger(..), runStderrLoggingT, runNoLoggingT) import Control.Monad.Logger (MonadLogger(..), runStderrLoggingT, runNoLoggingT)
import Control.Monad.Trans.Control (MonadBaseControl(..)) import Control.Monad.Trans.Control (MonadBaseControl(..))
import Control.Monad.Trans.Reader (ReaderT) import Control.Monad.Trans.Reader (ReaderT)
import Data.Char (toLower, toUpper)
import Data.List (sortBy) import Data.List (sortBy)
import Data.Monoid ((<>))
import Data.Ord (comparing) import Data.Ord (comparing)
import Database.Esqueleto import Database.Esqueleto
#if defined (WITH_POSTGRESQL) #if defined (WITH_POSTGRESQL)
@ -48,9 +50,10 @@ import Database.Persist.TH
import Test.Hspec import Test.Hspec
import qualified Control.Monad.Trans.Resource as R import qualified Control.Monad.Trans.Resource as R
import qualified Data.Set as S
import qualified Data.List as L import qualified Data.List as L
import Data.Char (toLower, toUpper) import qualified Data.Set as S
import qualified Data.Text.Lazy.Builder as TLB
import qualified Database.Esqueleto.Internal.Sql as EI
-- Test schema -- Test schema
@ -1268,6 +1271,48 @@ main = do
liftIO $ ret `shouldBe` [ Value (3) ] liftIO $ ret `shouldBe` [ Value (3) ]
describe "locking" $ do
-- The locking clause is the last one, so try to use many
-- others to test if it's at the right position. We don't
-- care about the text of the rest, nor with the RDBMS'
-- reaction to the clause.
let sanityCheck kind syntax = do
let complexQuery =
from $ \(p1 `InnerJoin` p2) -> do
on (p1 ^. PersonName ==. p2 ^. PersonName)
where_ (p1 ^. PersonFavNum >. val 2)
orderBy [desc (p2 ^. PersonAge)]
limit 3
offset 9
groupBy (p1 ^. PersonId)
having (countRows <. val (0 :: Int))
return (p1, p2)
queryWithClause1 = do
r <- complexQuery
locking kind
return r
queryWithClause2 = do
locking ForUpdate
r <- complexQuery
locking ForShare
locking kind
return r
queryWithClause3 = do
locking kind
complexQuery
toText conn q =
let (tlb, _) = EI.toRawSql EI.SELECT (conn, EI.initialIdentState) q
in TLB.toLazyText tlb
[complex, with1, with2, with3] <-
runNoLoggingT $ withConn $ \conn -> return $
map (toText conn) [complexQuery, queryWithClause1, queryWithClause2, queryWithClause3]
let expected = complex <> "\n" <> syntax
(with1, with2, with3) `shouldBe` (expected, expected, expected)
it "looks sane for ForUpdate" $ sanityCheck ForUpdate "FOR UPDATE"
it "looks sane for ForShare" $ sanityCheck ForShare "FOR SHARE"
it "looks sane for LockInShareMode" $ sanityCheck LockInShareMode "LOCK IN SHARE MODE"
---------------------------------------------------------------------- ----------------------------------------------------------------------
@ -1321,23 +1366,29 @@ verbose = True
run_worker :: RunDbMonad m => SqlPersistT (R.ResourceT m) a -> m a run_worker :: RunDbMonad m => SqlPersistT (R.ResourceT m) a -> m a
run_worker act = run_worker act = withConn $ runSqlConn (migrateIt >> act)
migrateIt :: RunDbMonad m => SqlPersistT (R.ResourceT m) ()
migrateIt = do
void $ runMigrationSilent migrateAll
#if defined (WITH_POSTGRESQL) || defined (WITH_MYSQL)
cleanDB
#endif
withConn :: RunDbMonad m => (SqlBackend -> R.ResourceT m a) -> m a
withConn =
R.runResourceT . R.runResourceT .
#if defined(WITH_POSTGRESQL) #if defined(WITH_POSTGRESQL)
withPostgresqlConn "host=localhost port=5432 user=test dbname=test" . withPostgresqlConn "host=localhost port=5432 user=test dbname=test"
#elif defined (WITH_MYSQL) #elif defined (WITH_MYSQL)
withMySQLConn defaultConnectInfo withMySQLConn defaultConnectInfo
{ connectHost = "localhost" { connectHost = "localhost"
, connectUser = "test" , connectUser = "test"
, connectPassword = "test" , connectPassword = "test"
, connectDatabase = "test" , connectDatabase = "test"
} . }
#else #else
withSqliteConn ":memory:" . withSqliteConn ":memory:"
#endif
runSqlConn .
#if defined (WITH_POSTGRESQL) || defined (WITH_MYSQL)
(runMigrationSilent migrateAll >>) $ (cleanDB >> act)
#else
(runMigrationSilent migrateAll >>) $ act
#endif #endif