esqueleto 2.2.6 → 2.2.7
raw patch · 5 files changed
+124/−19 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Database.Esqueleto: ForShare :: LockingKind
+ Database.Esqueleto: ForUpdate :: LockingKind
+ Database.Esqueleto: LockInShareMode :: LockingKind
+ Database.Esqueleto: data LockingKind
+ Database.Esqueleto: locking :: Esqueleto query expr backend => LockingKind -> query ()
+ Database.Esqueleto.Internal.Language: ForShare :: LockingKind
+ Database.Esqueleto.Internal.Language: ForUpdate :: LockingKind
+ Database.Esqueleto.Internal.Language: LockInShareMode :: LockingKind
+ Database.Esqueleto.Internal.Language: data LockingKind
+ Database.Esqueleto.Internal.Language: locking :: Esqueleto query expr backend => LockingKind -> query ()
Files
- esqueleto.cabal +1/−1
- src/Database/Esqueleto.hs +2/−1
- src/Database/Esqueleto/Internal/Language.hs +35/−0
- src/Database/Esqueleto/Internal/Sql.hs +23/−5
- test/Test.hs +63/−12
esqueleto.cabal view
@@ -1,5 +1,5 @@ name: esqueleto-version: 2.2.6+version: 2.2.7 synopsis: Type-safe EDSL for SQL queries on persistent backends. homepage: https://github.com/prowdsponsor/esqueleto license: BSD3
src/Database/Esqueleto.hs view
@@ -39,7 +39,7 @@ -- * @esqueleto@'s Language 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, (^.), (?.) , val, isNothing, just, nothing, joinV, countRows, count, not_ , (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)@@ -61,6 +61,7 @@ , ValueList(..) , OrderBy , DistinctOn+ , LockingKind(..) -- ** Joins , InnerJoin(..) , CrossJoin(..)
src/Database/Esqueleto/Internal/Language.hs view
@@ -30,6 +30,7 @@ , DistinctOn , Update , Insertion+ , LockingKind(..) -- * The guts , JoinKind(..) , IsJoinKind(..)@@ -273,6 +274,15 @@ -- /Since: 1.2.2/ 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 -- simple value so should be used only when the @SELECT@ query -- is guaranteed to return just one row.@@ -701,6 +711,31 @@ -- | Phantom type used by 'insertSelect'. 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.
src/Database/Esqueleto/Internal/Sql.hs view
@@ -58,7 +58,7 @@ import qualified Control.Monad.Trans.Reader as R import Data.Int (Int64) import Data.List (intersperse)-import Data.Monoid (Monoid(..), (<>))+import Data.Monoid (Last(..), Monoid(..), (<>)) import Data.Proxy (Proxy(..)) import Database.Esqueleto.Internal.PersistentImport import Database.Persist.Sql.Util (@@ -111,12 +111,13 @@ , sdHavingClause :: !HavingClause , sdOrderByClause :: ![OrderByClause] , sdLimitClause :: !LimitClause+ , sdLockingClause :: !LockingClause } instance Monoid SideData where- mempty = SideData 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 <> d') (f <> f') (s <> s') (w <> w') (g <> g') (h <> h') (o <> o') (l <> l')+ mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty mempty+ 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') (k <> k') -- | The @DISTINCT@ "clause".@@ -211,6 +212,10 @@ -- "reversed" arguments. +-- | A locking clause.+type LockingClause = Last LockingKind++ ---------------------------------------------------------------------- @@ -392,6 +397,8 @@ 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 } asc = EOrderBy ASC desc = EOrderBy DESC@@ -956,7 +963,8 @@ groupByClause havingClause orderByClauses- limitClause = sd+ limitClause+ lockingClause = sd -- Pass the finalIdentState (containing all identifiers -- that were used) to the subsequent calls. This ensures -- that no name clashes will occur on subqueries that may@@ -972,6 +980,7 @@ , makeHaving info havingClause , makeOrderBy info orderByClauses , makeLimit info limitClause orderByClauses+ , makeLocking lockingClause ] @@ -1110,6 +1119,15 @@ hasOrderClause = not (null orderByClauses) v = maybe 0 fromIntegral 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
test/Test.hs view
@@ -26,7 +26,9 @@ import Control.Monad.Logger (MonadLogger(..), runStderrLoggingT, runNoLoggingT) import Control.Monad.Trans.Control (MonadBaseControl(..)) import Control.Monad.Trans.Reader (ReaderT)+import Data.Char (toLower, toUpper) import Data.List (sortBy)+import Data.Monoid ((<>)) import Data.Ord (comparing) import Database.Esqueleto #if defined (WITH_POSTGRESQL)@@ -48,9 +50,10 @@ import Test.Hspec import qualified Control.Monad.Trans.Resource as R-import qualified Data.Set as S 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@@ -1268,7 +1271,49 @@ 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 @@ 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 . #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) withMySQLConn defaultConnectInfo { connectHost = "localhost" , connectUser = "test" , connectPassword = "test" , connectDatabase = "test"- } .-#else- withSqliteConn ":memory:" .-#endif- runSqlConn .-#if defined (WITH_POSTGRESQL) || defined (WITH_MYSQL)- (runMigrationSilent migrateAll >>) $ (cleanDB >> act)+ } #else- (runMigrationSilent migrateAll >>) $ act+ withSqliteConn ":memory:" #endif