New distinct, distinctOn. Deprecate old specializations.

This commit is contained in:
Felipe Lessa 2015-06-19 15:45:15 -03:00
parent e23ae2f328
commit a1a09e736f
4 changed files with 178 additions and 62 deletions

View File

@ -38,7 +38,8 @@ module Database.Esqueleto
-- $gettingstarted -- $gettingstarted
-- * @esqueleto@'s Language -- * @esqueleto@'s Language
Esqueleto( where_, on, groupBy, orderBy, rand, asc, desc, limit, offset, having Esqueleto( where_, on, groupBy, orderBy, rand, asc, desc, limit, offset
, distinct, distinctOn, having
, sub_select, sub_selectDistinct, (^.), (?.) , sub_select, sub_selectDistinct, (^.), (?.)
, val, isNothing, just, nothing, joinV, countRows, count, not_ , val, isNothing, just, nothing, joinV, countRows, count, not_
, (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.) , (==.), (>=.), (>.), (<=.), (<.), (!=.), (&&.), (||.)

View File

@ -179,6 +179,61 @@ class (Functor query, Applicative query, Monad query) =>
-- | @OFFSET@. Usually used with 'limit'. -- | @OFFSET@. Usually used with 'limit'.
offset :: Int64 -> query () offset :: Int64 -> query ()
-- | @DISTINCT@. Change the current @SELECT@ into @SELECT
-- DISTINCT@. For example:
--
-- @
-- select $ distinct $
-- 'from' \\foo -> do
-- ...
-- @
--
-- Note that this also has the same effect:
--
-- @
-- select $
-- 'from' \\foo -> do
-- distinct (return ())
-- ...
-- @
--
-- /Since: 2.2.4/
distinct :: query a -> query a
-- | @DISTINCT ON@. Change the current @SELECT@ into
-- @SELECT DISTINCT ON (expressions)@. For example:
--
-- @
-- select $
-- 'from' \\foo ->
-- distinctOn [foo ^. FooName, foo ^. FooState] $ do
-- ...
-- @
--
-- You can also chain different calls to 'distinctOn' whenever
-- your fields have different types:
--
-- @
-- select $
-- 'from' \\foo ->
-- distinctOn [foo ^. FooName] $
-- distinctOn [foo ^. FooState] $ do
-- ...
-- @
--
-- Each call to 'distinctOn' adds more expressions. Calls to
-- 'distinctOn' override any calls to 'distinct'.
--
-- Note that PostgreSQL requires the expressions on @DISTINCT
-- ON@ to be the first ones to appear on a @ORDER BY@. This is
-- not managed automatically by esqueleto, keeping its spirit
-- of trying to be close to raw SQL.
--
-- Supported by PostgreSQL only.
--
-- /Since: 2.2.4/
distinctOn :: [expr (Value b)] -> query a -> query a
-- | @ORDER BY random()@ clause. -- | @ORDER BY random()@ clause.
-- --
-- /Since: 1.3.10/ -- /Since: 1.3.10/
@ -390,6 +445,10 @@ class (Functor query, Applicative query, Monad query) =>
-- /Since: 2.1.2/ -- /Since: 2.1.2/
case_ :: PersistField a => [(expr (Value Bool), expr (Value a))] -> expr (Value a) -> expr (Value a) case_ :: PersistField a => [(expr (Value Bool), expr (Value a))] -> expr (Value a) -> expr (Value a)
{-# DEPRECATED sub_selectDistinct "Since 2.2.4: use 'sub_select' and 'distinct'." #-}
{-# DEPRECATED subList_selectDistinct "Since 2.2.4: use 'subList_select' and 'distinct'." #-}
-- Fixity declarations -- Fixity declarations
infixl 9 ^. infixl 9 ^.
infixl 7 *., /. infixl 7 *., /.

View File

@ -103,19 +103,34 @@ type SqlEntity ent = (PersistEntity ent, PersistEntityBackend ent ~ SqlBackend)
-- | Side data written by 'SqlQuery'. -- | Side data written by 'SqlQuery'.
data SideData = SideData { sdFromClause :: ![FromClause] data SideData = SideData { sdDistinctClause :: !DistinctClause
, sdSetClause :: ![SetClause] , sdFromClause :: ![FromClause]
, sdWhereClause :: !WhereClause , sdSetClause :: ![SetClause]
, sdGroupByClause :: !GroupByClause , sdWhereClause :: !WhereClause
, sdHavingClause :: !HavingClause , sdGroupByClause :: !GroupByClause
, sdOrderByClause :: ![OrderByClause] , sdHavingClause :: !HavingClause
, sdLimitClause :: !LimitClause , sdOrderByClause :: ![OrderByClause]
, sdLimitClause :: !LimitClause
} }
instance Monoid SideData where instance Monoid SideData where
mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty
SideData f s w g h o l `mappend` SideData f' s' w' g' h' o' l' = SideData d f s w g h o l `mappend` SideData d' f' s' w' g' h' o' l' =
SideData (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')
-- | The @DISTINCT@ "clause".
data DistinctClause =
DistinctAll -- ^ The default, everything.
| DistinctStandard -- ^ Only @DISTINCT@, SQL standard.
| DistinctOn [SqlExpr (Value ())] -- ^ @DISTINCT ON@, PostgreSQL extension.
instance Monoid DistinctClause where
mempty = DistinctAll
DistinctOn a `mappend` DistinctOn b = DistinctOn (a <> b)
DistinctOn a `mappend` _ = DistinctOn a
DistinctStandard `mappend` _ = DistinctStandard
DistinctAll `mappend` b = b
-- | A part of a @FROM@ clause. -- | A part of a @FROM@ clause.
@ -383,8 +398,12 @@ instance Esqueleto SqlQuery SqlExpr SqlBackend where
limit n = Q $ W.tell mempty { sdLimitClause = Limit (Just n) Nothing } limit n = Q $ W.tell mempty { sdLimitClause = Limit (Just n) Nothing }
offset n = Q $ W.tell mempty { sdLimitClause = Limit Nothing (Just n) } offset n = Q $ W.tell mempty { sdLimitClause = Limit Nothing (Just n) }
distinct act = Q (W.tell mempty { sdDistinctClause = DistinctStandard }) >> act
distinctOn exprs act = Q (W.tell mempty { sdDistinctClause = DistinctOn exprs' }) >> act
where exprs' = map veryUnsafeCoerceSqlExprValue exprs
sub_select = sub SELECT sub_select = sub SELECT
sub_selectDistinct = sub SELECT_DISTINCT sub_selectDistinct = sub_select . distinct
(^.) :: forall val typ. (PersistEntity val, PersistField typ) (^.) :: forall val typ. (PersistEntity val, PersistField typ)
=> SqlExpr (Entity val) -> EntityField val typ -> SqlExpr (Value typ) => SqlExpr (Entity val) -> EntityField val typ -> SqlExpr (Value typ)
@ -451,7 +470,7 @@ instance Esqueleto SqlQuery SqlExpr SqlBackend where
(++.) = unsafeSqlBinOp " || " (++.) = unsafeSqlBinOp " || "
subList_select = EList . sub_select subList_select = EList . sub_select
subList_selectDistinct = EList . sub_selectDistinct subList_selectDistinct = subList_select . distinct
valList [] = EEmptyList valList [] = EEmptyList
valList vals = EList $ ERaw Parens $ const ( uncommas ("?" <$ vals) valList vals = EList $ ERaw Parens $ const ( uncommas ("?" <$ vals)
@ -801,11 +820,8 @@ selectDistinctSource
, MonadResource m ) , MonadResource m )
=> SqlQuery a => SqlQuery a
-> C.Source (SqlPersistT m) r -> C.Source (SqlPersistT m) r
selectDistinctSource query = do selectDistinctSource = selectSource . distinct
src <- lift $ do {-# DEPRECATED selectDistinctSource "Since 2.2.4: use 'selectSource' and 'distinct'." #-}
res <- rawSelectSource SELECT_DISTINCT query
fmap snd $ allocateAcquire res
src
-- | Execute an @esqueleto@ @SELECT DISTINCT@ query inside -- | Execute an @esqueleto@ @SELECT DISTINCT@ query inside
@ -813,10 +829,8 @@ selectDistinctSource query = do
selectDistinct :: ( SqlSelect a r selectDistinct :: ( SqlSelect a r
, MonadIO m ) , MonadIO m )
=> SqlQuery a -> SqlPersistT m [r] => SqlQuery a -> SqlPersistT m [r]
selectDistinct query = do selectDistinct = select . distinct
res <- rawSelectSource SELECT_DISTINCT query {-# DEPRECATED selectDistinct "Since 2.2.4: use 'select' and 'distinct'." #-}
conn <- R.ask
liftIO $ with res $ flip R.runReaderT conn . runSource
-- | (Internal) Run a 'C.Source' of rows. -- | (Internal) Run a 'C.Source' of rows.
@ -925,7 +939,8 @@ toRawSql mode (conn, firstIdentState) query =
flip S.runState firstIdentState $ flip S.runState firstIdentState $
W.runWriterT $ W.runWriterT $
unQ query unQ query
SideData fromClauses SideData distinctClause
fromClauses
setClauses setClauses
whereClauses whereClauses
groupByClause groupByClause
@ -939,7 +954,7 @@ toRawSql mode (conn, firstIdentState) query =
info = (conn, finalIdentState) info = (conn, finalIdentState)
in mconcat in mconcat
[ makeInsertInto info mode ret [ makeInsertInto info mode ret
, makeSelect info mode ret , makeSelect info mode distinctClause ret
, makeFrom info mode fromClauses , makeFrom info mode fromClauses
, makeSet info setClauses , makeSet info setClauses
, makeWhere info whereClauses , makeWhere info whereClauses
@ -953,11 +968,9 @@ toRawSql mode (conn, firstIdentState) query =
-- | (Internal) Mode of query being converted by 'toRawSql'. -- | (Internal) Mode of query being converted by 'toRawSql'.
data Mode = data Mode =
SELECT SELECT
| SELECT_DISTINCT
| DELETE | DELETE
| UPDATE | UPDATE
| INSERT_INTO Mode | INSERT_INTO
-- ^ 'Mode' should be either 'SELECT' or 'SELECT_DISTINCT'.
uncommas :: [TLB.Builder] -> TLB.Builder uncommas :: [TLB.Builder] -> TLB.Builder
@ -971,21 +984,26 @@ uncommas' = (uncommas *** mconcat) . unzip
makeInsertInto :: SqlSelect a r => IdentInfo -> Mode -> a -> (TLB.Builder, [PersistValue]) makeInsertInto :: SqlSelect a r => IdentInfo -> Mode -> a -> (TLB.Builder, [PersistValue])
makeInsertInto info (INSERT_INTO _) ret = sqlInsertInto info ret makeInsertInto info INSERT_INTO ret = sqlInsertInto info ret
makeInsertInto _ _ _ = mempty makeInsertInto _ _ _ = mempty
makeSelect :: SqlSelect a r => IdentInfo -> Mode -> a -> (TLB.Builder, [PersistValue]) makeSelect :: SqlSelect a r => IdentInfo -> Mode -> DistinctClause -> a -> (TLB.Builder, [PersistValue])
makeSelect info mode_ ret = process mode_ makeSelect info mode_ distinctClause ret = process mode_
where where
process mode = process mode =
case mode of case mode of
SELECT -> withCols "SELECT " SELECT -> withCols selectKind
SELECT_DISTINCT -> withCols "SELECT DISTINCT " DELETE -> plain "DELETE "
DELETE -> plain "DELETE " UPDATE -> plain "UPDATE "
UPDATE -> plain "UPDATE " INSERT_INTO -> process SELECT
INSERT_INTO mode' -> process mode' selectKind =
withCols v = first (v <>) (sqlSelectCols info ret) case distinctClause of
DistinctAll -> ("SELECT ", [])
DistinctStandard -> ("SELECT DISTINCT ", [])
DistinctOn exprs -> first (("SELECT DISTINCT ON (" <>) . (<> ") ")) $
uncommas' (materializeExpr info <$> exprs)
withCols v = v <> (sqlSelectCols info ret)
plain v = (v, []) plain v = (v, [])
@ -1176,17 +1194,22 @@ instance PersistEntity a => SqlSelect (SqlExpr (Maybe (Entity a))) (Maybe (Entit
-- | You may return any single value (i.e. a single column) from -- | You may return any single value (i.e. a single column) from
-- a 'select' query. -- a 'select' query.
instance PersistField a => SqlSelect (SqlExpr (Value a)) (Value a) where instance PersistField a => SqlSelect (SqlExpr (Value a)) (Value a) where
sqlSelectCols info (ERaw p f) = sqlSelectCols = materializeExpr
let (b, vals) = f info
in (parensM p b, vals)
sqlSelectCols info (ECompositeKey f) =
let bs = f info
in (uncommas $ map (parensM Parens) bs, [])
sqlSelectColCount = const 1 sqlSelectColCount = const 1
sqlSelectProcessRow [pv] = Value <$> fromPersistValue pv sqlSelectProcessRow [pv] = Value <$> fromPersistValue pv
sqlSelectProcessRow pvs = Value <$> fromPersistValue (PersistList pvs) sqlSelectProcessRow pvs = Value <$> fromPersistValue (PersistList pvs)
-- | Materialize a @SqlExpr (Value a)@.
materializeExpr :: IdentInfo -> SqlExpr (Value a) -> (TLB.Builder, [PersistValue])
materializeExpr info (ERaw p f) =
let (b, vals) = f info
in (parensM p b, vals)
materializeExpr info (ECompositeKey f) =
let bs = f info
in (uncommas $ map (parensM Parens) bs, [])
-- | You may return tuples (up to 16-tuples) and tuples of tuples -- | You may return tuples (up to 16-tuples) and tuples of tuples
-- from a 'select' query. -- from a 'select' query.
instance ( SqlSelect a ra instance ( SqlSelect a ra
@ -1681,16 +1704,11 @@ to16 ((a,b),(c,d),(e,f),(g,h),(i,j),(k,l),(m,n),(o,p)) = (a,b,c,d,e,f,g,h,i,j,k,
-- | Insert a 'PersistField' for every selected value. -- | Insert a 'PersistField' for every selected value.
insertSelect :: (MonadIO m, PersistEntity a) => insertSelect :: (MonadIO m, PersistEntity a) =>
SqlQuery (SqlExpr (Insertion a)) -> SqlPersistT m () SqlQuery (SqlExpr (Insertion a)) -> SqlPersistT m ()
insertSelect = insertGeneralSelect SELECT insertSelect = liftM (const ()) . rawEsqueleto INSERT_INTO . fmap EInsertFinal
-- | Insert a 'PersistField' for every unique selected value. -- | Insert a 'PersistField' for every unique selected value.
insertSelectDistinct :: (MonadIO m, PersistEntity a) => insertSelectDistinct :: (MonadIO m, PersistEntity a) =>
SqlQuery (SqlExpr (Insertion a)) -> SqlPersistT m () SqlQuery (SqlExpr (Insertion a)) -> SqlPersistT m ()
insertSelectDistinct = insertGeneralSelect SELECT_DISTINCT insertSelectDistinct = insertSelect . distinct
{-# DEPRECATED insertSelectDistinct "Since 2.2.4: use 'insertSelect' and 'distinct'." #-}
insertGeneralSelect :: (MonadIO m, PersistEntity a) =>
Mode -> SqlQuery (SqlExpr (Insertion a)) -> SqlPersistT m ()
insertGeneralSelect mode =
liftM (const ()) . rawEsqueleto (INSERT_INTO mode) . fmap EInsertFinal

View File

@ -24,6 +24,8 @@ 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.List (sortBy)
import Data.Ord (comparing)
import Database.Esqueleto import Database.Esqueleto
#if defined (WITH_POSTGRESQL) #if defined (WITH_POSTGRESQL)
import Database.Persist.Postgresql (withPostgresqlConn) import Database.Persist.Postgresql (withPostgresqlConn)
@ -51,6 +53,12 @@ import Data.Char (toLower, toUpper)
-- Test schema -- Test schema
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase| share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
Foo
name Int
Primary name
Bar
quux FooId
Person Person
name String name String
age Int Maybe age Int Maybe
@ -773,18 +781,48 @@ main = do
liftIO $ map entityVal eps `shouldBe` reverse ps liftIO $ map entityVal eps `shouldBe` reverse ps
describe "selectDistinct" $ describe "SELECT DISTINCT" $ do
it "works on a simple example" $ let selDistTest
:: ( forall m. RunDbMonad m
=> SqlQuery (SqlExpr (Value String))
-> SqlPersistT (R.ResourceT m) [Value String])
-> IO ()
selDistTest q =
run $ do
p1k <- insert p1
let (t1, t2, t3) = ("a", "b", "c")
mapM_ (insert . flip BlogPost p1k) [t1, t3, t2, t2, t1]
ret <- q $
from $ \b -> do
let title = b ^. BlogPostTitle
orderBy [asc title]
return title
liftIO $ ret `shouldBe` [ Value t1, Value t2, Value t3 ]
it "works on a simple example (selectDistinct)" $
selDistTest selectDistinct
it "works on a simple example (select . distinct)" $
selDistTest (select . distinct)
it "works on a simple example (distinct (return ()))" $
selDistTest (\act -> select $ distinct (return ()) >> act)
#if defined(WITH_POSTGRESQL)
describe "SELECT DISTINCT ON" $ do
it "works on a simple example" $ do
run $ do run $ do
p1k <- insert p1 [p1k, p2k, _] <- mapM insert [p1, p2, p3]
let (t1, t2, t3) = ("a", "b", "c") [bpA, bpB, bpC] <- mapM insert'
mapM_ (insert . flip BlogPost p1k) [t1, t3, t2, t2, t1] [ BlogPost "A" p1k
ret <- selectDistinct $ , BlogPost "B" p1k
from $ \b -> do , BlogPost "C" p2k ]
let title = b ^. BlogPostTitle ret <- select $
orderBy [asc title] from $ \bp ->
return title distinctOn [bp ^. BlogPostAuthorId] $ do
liftIO $ ret `shouldBe` [ Value t1, Value t2, Value t3 ] orderBy [asc (bp ^. BlogPostAuthorId), desc (bp ^. BlogPostTitle)]
return bp
liftIO $ ret `shouldBe` sortBy (comparing (blogPostAuthorId . entityVal)) [bpB, bpC]
#endif
describe "coalesce/coalesceDefault" $ do describe "coalesce/coalesceDefault" $ do
it "works on a simple example" $ it "works on a simple example" $