From d9777b43b8f0e4f60bc590d5b98db7b6dbf2af76 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Tue, 9 May 2023 16:58:43 +0200 Subject: [PATCH 01/15] feat: add basic UPDATE .. RETURNING * for Postgres With #44 as a starting point, and more ideas in mind. --- src/Database/Esqueleto/Internal/Internal.hs | 41 +++++++++++++++++++-- src/Database/Esqueleto/PostgreSQL.hs | 10 +++++ test/PostgreSQL/Test.hs | 12 ++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index e46516e30..497f15fe9 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -427,6 +427,10 @@ locking kind = putLocking $ LegacyLockingClause kind putLocking :: LockingClause -> SqlQuery () putLocking clause = Q $ W.tell mempty { sdLockingClause = clause } +-- | (Internal) Remember a @RETURNING@ clause in a query +tellReturning :: ReturningClause -> SqlQuery () +tellReturning clause = Q $ W.tell mempty { sdReturningClause = clause } + {-# DEPRECATED sub_select @@ -1835,14 +1839,15 @@ data SideData = SideData , sdLimitClause :: !LimitClause , sdLockingClause :: !LockingClause , sdCteClause :: ![CommonTableExpressionClause] + , sdReturningClause :: !ReturningClause } instance Semigroup SideData where - SideData d f s w g h o l k c <> SideData d' f' s' w' g' h' o' l' k' c' = - SideData (d <> d') (f <> f') (s <> s') (w <> w') (g <> g') (h <> h') (o <> o') (l <> l') (k <> k') (c <> c') + SideData d f s w g h o l k c r <> SideData d' f' s' w' g' h' o' l' k' c' r' = + SideData (d <> d') (f <> f') (s <> s') (w <> w') (g <> g') (h <> h') (o <> o') (l <> l') (k <> k') (c <> c') (r <> r') instance Monoid SideData where - mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty mempty mempty + mempty = SideData mempty mempty mempty mempty mempty mempty mempty mempty mempty mempty mempty mappend = (<>) -- | The @DISTINCT@ "clause". @@ -1879,6 +1884,12 @@ data CommonTableExpressionKind data CommonTableExpressionClause = CommonTableExpressionClause CommonTableExpressionKind Ident (IdentInfo -> (TLB.Builder, [PersistValue])) +data ReturningClause + = ReturningNothing -- ^ The default, absent clause. + | ReturningStar -- ^ @RETURNING *@ + -- | ReturningExprs (NonEmpty (SqlExpr Returning)) + -- ^ @output_expression [ [ AS ] output_name ] [, ...]@ + data SubQueryType = NormalSubQuery | LateralSubQuery @@ -2117,6 +2128,16 @@ instance Monoid LockingClause where mempty = NoLockingClause mappend = (<>) +instance Semigroup ReturningClause where + (<>) ReturningNothing x = x + (<>) x ReturningNothing = x + (<>) ReturningStar ReturningStar = ReturningStar +-- (<>) _ _ = error "instance Semigroup FIXME" + +instance Monoid ReturningClause where + mempty = ReturningNothing + mappend = (<>) + ---------------------------------------------------------------------- -- | Identifier used for table names. @@ -2981,7 +3002,8 @@ toRawSql mode (conn, firstIdentState) query = orderByClauses limitClause lockingClause - cteClause = sd + cteClause + returningClause = 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 @@ -2999,6 +3021,7 @@ toRawSql mode (conn, firstIdentState) query = , makeOrderBy info orderByClauses , makeLimit info limitClause , makeLocking info lockingClause + , makeReturning info returningClause ret ] @@ -3073,6 +3096,7 @@ data Mode | DELETE | UPDATE | INSERT_INTO + | UPDATE_RETSTAR uncommas :: [TLB.Builder] -> TLB.Builder uncommas = intersperseB ", " @@ -3124,6 +3148,7 @@ makeSelect info mode_ distinctClause ret = process mode_ DELETE -> plain "DELETE " UPDATE -> plain "UPDATE " INSERT_INTO -> process SELECT + UPDATE_RETSTAR -> plain "UPDATE " selectKind = case distinctClause of DistinctAll -> ("SELECT ", []) @@ -3151,6 +3176,7 @@ makeFrom info mode fs = ret keyword = case mode of UPDATE -> id + UPDATE_RETSTAR -> id _ -> first ("\nFROM " <>) mk _ (FromStart i def) = base i def @@ -3268,6 +3294,13 @@ makeLocking info (PostgresLockingClauses clauses) = plain v = (v,[]) makeLocking _ NoLockingClause = mempty +makeReturning :: SqlSelect a r + => IdentInfo -> ReturningClause -> a -> (TLB.Builder, [PersistValue]) +makeReturning _ ReturningNothing _ = mempty +makeReturning info ReturningStar ret = ("RETURNING ", []) <> sqlSelectCols info ret +-- makeReturning info (ReturningExprs _) = undefined -- FIXME + + parens :: TLB.Builder -> TLB.Builder parens b = "(" <> (b <> ")") diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 3011741b4..d5552f4ed 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -23,6 +23,7 @@ module Database.Esqueleto.PostgreSQL , chr , now_ , random_ + , updateReturningAll , upsert , upsertBy , insertSelectWithConflict @@ -41,6 +42,7 @@ module Database.Esqueleto.PostgreSQL #if __GLASGOW_HASKELL__ < 804 import Data.Semigroup #endif +import Conduit (withAcquire) import Control.Arrow (first) import Control.Exception (throw) import Control.Monad (void) @@ -477,3 +479,11 @@ forUpdateOf lockableEntities onLockedBehavior = forShareOf :: LockableEntity a => a -> OnLockedBehavior -> SqlQuery () forShareOf lockableEntities onLockedBehavior = putLocking $ PostgresLockingClauses [PostgresLockingKind PostgresForShare (Just $ LockingOfClause lockableEntities) onLockedBehavior] + +updateReturningAll :: (MonadIO m, PersistEntity ent, SqlBackendCanWrite backend, backend ~ PersistEntityBackend ent) + => (SqlExpr (Entity ent) -> SqlQuery (SqlExpr (Entity ent))) + -> R.ReaderT backend m [Entity ent] +updateReturningAll block = do + conn <- R.ask + conduit <- rawSelectSource UPDATE_RETSTAR (tellReturning ReturningStar >> from block) + liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index 9e144e2be..e1f11514c 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1056,6 +1056,17 @@ testUpsert = u3e <- EP.upsert u3 [OneUniqueName =. val "fifth"] liftIO $ entityVal u3e `shouldBe` u1{oneUniqueName="fifth"} +testUpdateDeleteReturning :: SpecDb +testUpdateDeleteReturning = + describe "UPDATE .. RETURNING *" $ do + itDb "Whole updated entity gets returned" $ do + [p1k, p2k, p3k, p4k, p5k] <- mapM insert [p1, p2, p3, p4, p5] + ret <- EP.updateReturningAll $ \p -> do + set p [ PersonFavNum =. val 42 ] + where_ (p ^. PersonFavNum ==. val 4) + return p + asserting $ ret `shouldBe` [Entity p4k p4{ personFavNum = 42 }] + testInsertSelectWithConflict :: SpecDb testInsertSelectWithConflict = describe "insertSelectWithConflict test" $ do @@ -1629,6 +1640,7 @@ spec = beforeAll mkConnectionPool $ do testPostgresqlTextFunctions testInsertUniqueViolation testUpsert + testUpdateDeleteReturning testInsertSelectWithConflict testFilterWhere testCommonTableExpressions From 48d71be1707536ba8e62f7819fd6ef78c4ca404f Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Tue, 9 May 2023 17:06:57 +0200 Subject: [PATCH 02/15] cleanup: deduplicate extensions, fix trivial lints * TypeOperators -- enabled twice on lines 3, 23 * TypeApplications -- enabled twice on lines 4, 21 * DerivingStrategies -- enabled twice on lines 5, 9 * GeneralizedNewtypeDeriving -- enabled twice on lines 5, 15 * MultiParamTypeClasses -- implied by FunctionalDependencies This reduces lints from hlint & compiler warnings. --- src/Database/Esqueleto/Internal/Internal.hs | 16 ++++++---------- src/Database/Esqueleto/PostgreSQL.hs | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 497f15fe9..ed2125cdd 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -1,8 +1,5 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE TypeApplications #-} -{-# language DerivingStrategies, GeneralizedNewtypeDeriving #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} @@ -14,7 +11,6 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE InstanceSigs #-} -{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -669,7 +665,7 @@ isNothing v = first (parensM p) . isNullExpr $ f Never info where isNullExpr :: (TLB.Builder, a) -> (TLB.Builder, a) - isNullExpr = first ((<> " IS NULL")) + isNullExpr = first (<> " IS NULL") -- | An alias for 'isNothing' that avoids clashing with the function from -- "Data.Maybe" 'Data.Maybe.isNothing'. @@ -2445,7 +2441,7 @@ unsafeSqlCase when v = ERaw noMeta buildCase in ( b0 <> " WHEN " <> b1 <> " THEN " <> b2, vals0 <> vals1 <> vals2 ) valueToSql :: SqlExpr (Value a) -> NeedParens -> IdentInfo -> (TLB.Builder, [PersistValue]) - valueToSql (ERaw _ f) p = f p + valueToSql (ERaw _ f) = f -- | (Internal) Create a custom binary operator. You /should/ -- /not/ use this function directly since its type is very @@ -2929,7 +2925,7 @@ deleteCount :: (MonadIO m, SqlBackendCanWrite backend) => SqlQuery () -> R.ReaderT backend m Int64 -deleteCount a = rawEsqueleto DELETE a +deleteCount = rawEsqueleto DELETE -- | Execute an @esqueleto@ @UPDATE@ query inside @persistent@'s -- 'SqlPersistT' monad. Note that currently there are no type @@ -3970,14 +3966,14 @@ insertSelect :: (MonadIO m, PersistEntity a, SqlBackendCanWrite backend) => SqlQuery (SqlExpr (Insertion a)) -> R.ReaderT backend m () -insertSelect a = void $ insertSelectCount a +insertSelect = void . insertSelectCount -- | Insert a 'PersistField' for every selected value, return the count afterward insertSelectCount :: (MonadIO m, PersistEntity a, SqlBackendCanWrite backend) => SqlQuery (SqlExpr (Insertion a)) -> R.ReaderT backend m Int64 -insertSelectCount a = rawEsqueleto INSERT_INTO a +insertSelectCount = rawEsqueleto INSERT_INTO -- | Renders an expression into 'Text'. Only useful for creating a textual -- representation of the clauses passed to an "On" clause. @@ -3987,7 +3983,7 @@ renderExpr :: SqlBackend -> SqlExpr (Value Bool) -> T.Text renderExpr sqlBackend e = case e of ERaw _ mkBuilderValues -> let (builder, _) = mkBuilderValues Never (sqlBackend, initialIdentState) - in (builderToText builder) + in builderToText builder -- | An exception thrown by 'RenderExpr' - it's not designed to handle composite -- keys, and will blow up if you give it one. diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index d5552f4ed..714e6ba46 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -77,7 +77,7 @@ maybeArray :: (PersistField a, PersistField [a]) => SqlExpr (Value (Maybe [a])) -> SqlExpr (Value [a]) -maybeArray x = coalesceDefault [x] (emptyArray) +maybeArray x = coalesceDefault [x] emptyArray -- | Aggregate mode data AggMode From 9788cbbea8edd428adaa2d8d017e86be743e6d80 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Fri, 1 Sep 2023 14:17:14 +0200 Subject: [PATCH 03/15] cleanup: fix syntax of sub_select deprecation notice https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/pragmas.html#warning-and-deprecated-pragmas --- src/Database/Esqueleto/Internal/Internal.hs | 28 ++++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index ed2125cdd..a0c7475f1 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -427,21 +427,19 @@ putLocking clause = Q $ W.tell mempty { sdLockingClause = clause } tellReturning :: ReturningClause -> SqlQuery () tellReturning clause = Q $ W.tell mempty { sdReturningClause = clause } -{-# - DEPRECATED - sub_select - "sub_select \n \ -sub_select is an unsafe function to use. If used with a SqlQuery that \n \ -returns 0 results, then it may return NULL despite not mentioning Maybe \n \ -in the return type. If it returns more than 1 result, then it will throw a \n \ -SQL error.\n\n Instead, consider using one of the following alternatives: \n \ -- subSelect: attaches a LIMIT 1 and the Maybe return type, totally safe. \n \ -- subSelectMaybe: Attaches a LIMIT 1, useful for a query that already \n \ - has a Maybe in the return type. \n \ -- subSelectCount: Performs a count of the query - this is always safe. \n \ -- subSelectUnsafe: Performs no checks or guarantees. Safe to use with \n \ - countRows and friends." - #-} +{-# DEPRECATED sub_select + [ "sub_select is an unsafe function to use. If used with a SqlQuery that" + , "returns 0 results, then it may return NULL despite not mentioning Maybe" + , "in the return type. If it returns more than 1 result, then it will throw a" + , "SQL error.\n\n Instead, consider using one of the following alternatives:" + , "- subSelect: attaches a LIMIT 1 and the Maybe return type, totally safe. " + , "- subSelectMaybe: Attaches a LIMIT 1, useful for a query that already" + , " has a Maybe in the return type." + , "- subSelectCount: Performs a count of the query - this is always safe." + , "- subSelectUnsafe: Performs no checks or guarantees. Safe to use with" + , " countRows and friends." + ] +#-} -- | Execute a subquery @SELECT@ in an SqlExpression. Returns a -- simple value so should be used only when the @SELECT@ query -- is guaranteed to return just one row. From c97d455f0539e48e69ca51b90cbec38449e67d26 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Wed, 10 May 2023 08:37:02 +0200 Subject: [PATCH 04/15] docs: haddock typos --- src/Database/Esqueleto/Internal/Internal.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index a0c7475f1..6a409e011 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -2084,10 +2084,10 @@ instance Monoid GroupByClause where mempty = GroupBy [] mappend = (<>) --- | A @HAVING@ cause. +-- | A @HAVING@ clause. type HavingClause = WhereClause --- | A @ORDER BY@ clause. +-- | An @ORDER BY@ clause. type OrderByClause = SqlExpr OrderBy -- | A @LIMIT@ clause. From c0044f73b115b9b3163dbcfab4fe29b46ce94546 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 12:17:29 +0200 Subject: [PATCH 05/15] feat: generalize, add InferReturning, add test --- src/Database/Esqueleto/Internal/Internal.hs | 33 +++++++++++++++++++++ src/Database/Esqueleto/PostgreSQL.hs | 6 ++-- test/PostgreSQL/Test.hs | 15 +++++++--- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 6a409e011..f24fe0b93 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -2415,6 +2415,39 @@ existsHelper = sub SELECT . (>> return true) true :: SqlExpr (Value Bool) true = val True +-- | (Internal) The types which can appear in @RETURNING@ part of @UPDATE@ or @DELETE@ +-- +-- Many constructs appearing in @SELECT@ can go under @RETURNING@ -- but not all (e.g. +-- certainly not subqueries, @VALUES@ and such). Thus, this is a subclass of 'SqlSelect'. +class SqlSelect a r => InferReturning a r +instance PersistEntity ent => InferReturning (SqlExpr (Entity ent)) (Entity ent) +instance PersistEntity ent => InferReturning (SqlExpr (Maybe (Entity ent))) (Maybe (Entity ent)) +instance PersistField a => InferReturning (SqlExpr (Value a)) (Value a) +instance ( InferReturning a ra, InferReturning b rb) => InferReturning (a, b) (ra, rb) +instance ( InferReturning a ra + , InferReturning b rb + , InferReturning c rc + ) => InferReturning (a, b, c) (ra, rb, rc) +instance ( InferReturning a ra + , InferReturning b rb + , InferReturning c rc + , InferReturning d rd + ) => InferReturning (a, b, c, d) (ra, rb, rc, rd) +instance ( InferReturning a ra + , InferReturning b rb + , InferReturning c rc + , InferReturning d rd + , InferReturning e re + ) => InferReturning (a, b, c, d, e) (ra, rb, rc, rd, re) +instance ( InferReturning a ra + , InferReturning b rb + , InferReturning c rc + , InferReturning d rd + , InferReturning e re + , InferReturning f rf + ) => InferReturning (a, b, c, d, e, f) (ra, rb, rc, rd, re, rf) +-- tuple nesting provides unlimited arity if 6-tuple isn't enough + -- | (Internal) Create a case statement. -- -- Since: 2.1.1 diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 714e6ba46..8593229ed 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -480,9 +480,9 @@ forShareOf :: LockableEntity a => a -> OnLockedBehavior -> SqlQuery () forShareOf lockableEntities onLockedBehavior = putLocking $ PostgresLockingClauses [PostgresLockingKind PostgresForShare (Just $ LockingOfClause lockableEntities) onLockedBehavior] -updateReturningAll :: (MonadIO m, PersistEntity ent, SqlBackendCanWrite backend, backend ~ PersistEntityBackend ent) - => (SqlExpr (Entity ent) -> SqlQuery (SqlExpr (Entity ent))) - -> R.ReaderT backend m [Entity ent] +updateReturningAll :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) + => (from -> SqlQuery ex) + -> R.ReaderT backend m [ret] updateReturningAll block = do conn <- R.ask conduit <- rawSelectSource UPDATE_RETSTAR (tellReturning ReturningStar >> from block) diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index e1f11514c..e15ccddaf 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1059,13 +1059,20 @@ testUpsert = testUpdateDeleteReturning :: SpecDb testUpdateDeleteReturning = describe "UPDATE .. RETURNING *" $ do - itDb "Whole updated entity gets returned" $ do - [p1k, p2k, p3k, p4k, p5k] <- mapM insert [p1, p2, p3, p4, p5] - ret <- EP.updateReturningAll $ \p -> do + itDb "Whole entities, expressions and tuples get returned" $ do + [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] + ret1 <- EP.updateReturningAll $ \p -> do set p [ PersonFavNum =. val 42 ] where_ (p ^. PersonFavNum ==. val 4) return p - asserting $ ret `shouldBe` [Entity p4k p4{ personFavNum = 42 }] + asserting $ ret1 `shouldBe` [Entity p4k p4{ personFavNum = 42 }] + + ret2 <- EP.updateReturningAll $ \p -> do + set p [ PersonAge =. val (Just 0) ] + where_ (isNothing $ p ^. PersonAge) + return (val True, p ^. PersonName, (p ^. PersonFavNum) *. val 100) + asserting $ ret2 `shouldBe` [ (Value True, Value "Rachel", Value 200) + , (Value True, Value "Mitch", Value 500) ] testInsertSelectWithConflict :: SpecDb testInsertSelectWithConflict = From b055b2e8277b2505c5b821733e0487a0b7a70743 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 12:35:48 +0200 Subject: [PATCH 06/15] feat: rename updateReturningAll -> updateReturning, add doc --- src/Database/Esqueleto/PostgreSQL.hs | 26 +++++++++++++++++++++----- test/PostgreSQL/Test.hs | 4 ++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 8593229ed..7961b15be 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -23,7 +23,7 @@ module Database.Esqueleto.PostgreSQL , chr , now_ , random_ - , updateReturningAll + , updateReturning , upsert , upsertBy , insertSelectWithConflict @@ -480,10 +480,26 @@ forShareOf :: LockableEntity a => a -> OnLockedBehavior -> SqlQuery () forShareOf lockableEntities onLockedBehavior = putLocking $ PostgresLockingClauses [PostgresLockingKind PostgresForShare (Just $ LockingOfClause lockableEntities) onLockedBehavior] -updateReturningAll :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) - => (from -> SqlQuery ex) - -> R.ReaderT backend m [ret] -updateReturningAll block = do +-- | `UPDATE .. RETURNING ..` SQL extension supported by Postgres. +-- +-- The instances of 'InferReturning' say what can be returned; currently includes +-- whole entities, 'PersistField's, SQL expressions, tuples (possibly nested). +-- +-- Usage example: +-- +-- @ +-- tuples <- updateReturning $ \p -> do +-- set p [ PersonAge =. val (Just 0) ] +-- where_ (isNothing $ p ^. PersonAge) +-- return (val True, p ^. PersonName, (p ^. PersonFavNum) *. val 100) +-- -- return p -- also works, returning (Entity Person) +-- @ +-- +-- @since 3.5.9.1 +updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) + => (from -> SqlQuery ex) + -> R.ReaderT backend m [ret] +updateReturning block = do conn <- R.ask conduit <- rawSelectSource UPDATE_RETSTAR (tellReturning ReturningStar >> from block) liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index e15ccddaf..3a916c2ae 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1061,13 +1061,13 @@ testUpdateDeleteReturning = describe "UPDATE .. RETURNING *" $ do itDb "Whole entities, expressions and tuples get returned" $ do [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] - ret1 <- EP.updateReturningAll $ \p -> do + ret1 <- EP.updateReturning $ \p -> do set p [ PersonFavNum =. val 42 ] where_ (p ^. PersonFavNum ==. val 4) return p asserting $ ret1 `shouldBe` [Entity p4k p4{ personFavNum = 42 }] - ret2 <- EP.updateReturningAll $ \p -> do + ret2 <- EP.updateReturning $ \p -> do set p [ PersonAge =. val (Just 0) ] where_ (isNothing $ p ^. PersonAge) return (val True, p ^. PersonName, (p ^. PersonFavNum) *. val 100) From 63132c02fbe1d85b3e7acda48f945bdf7ff27122 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 12:41:28 +0200 Subject: [PATCH 07/15] feat: undo addition of UPDATE_RETSTAR, not necessary --- src/Database/Esqueleto/Internal/Internal.hs | 3 --- src/Database/Esqueleto/PostgreSQL.hs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index f24fe0b93..f31ec29c4 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -3123,7 +3123,6 @@ data Mode | DELETE | UPDATE | INSERT_INTO - | UPDATE_RETSTAR uncommas :: [TLB.Builder] -> TLB.Builder uncommas = intersperseB ", " @@ -3175,7 +3174,6 @@ makeSelect info mode_ distinctClause ret = process mode_ DELETE -> plain "DELETE " UPDATE -> plain "UPDATE " INSERT_INTO -> process SELECT - UPDATE_RETSTAR -> plain "UPDATE " selectKind = case distinctClause of DistinctAll -> ("SELECT ", []) @@ -3203,7 +3201,6 @@ makeFrom info mode fs = ret keyword = case mode of UPDATE -> id - UPDATE_RETSTAR -> id _ -> first ("\nFROM " <>) mk _ (FromStart i def) = base i def diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 7961b15be..31f9b7562 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -501,5 +501,5 @@ updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWr -> R.ReaderT backend m [ret] updateReturning block = do conn <- R.ask - conduit <- rawSelectSource UPDATE_RETSTAR (tellReturning ReturningStar >> from block) + conduit <- rawSelectSource UPDATE (tellReturning ReturningStar >> from block) liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource From e9fa6814940f0cc5edf7eb4b102176e21e55e723 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 13:09:53 +0200 Subject: [PATCH 08/15] feat: improve error messages --- src/Database/Esqueleto/Internal/Internal.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index f31ec29c4..600c38f99 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -2419,7 +2419,7 @@ existsHelper = sub SELECT . (>> return true) -- -- Many constructs appearing in @SELECT@ can go under @RETURNING@ -- but not all (e.g. -- certainly not subqueries, @VALUES@ and such). Thus, this is a subclass of 'SqlSelect'. -class SqlSelect a r => InferReturning a r +class SqlSelect a r => InferReturning a r | r -> a, a -> r instance PersistEntity ent => InferReturning (SqlExpr (Entity ent)) (Entity ent) instance PersistEntity ent => InferReturning (SqlExpr (Maybe (Entity ent))) (Maybe (Entity ent)) instance PersistField a => InferReturning (SqlExpr (Value a)) (Value a) From afee7b0f03e57b3d2d96893d24b5b8981e1e281d Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 13:26:04 +0200 Subject: [PATCH 09/15] feat: add deleteReturning and test --- src/Database/Esqueleto/PostgreSQL.hs | 23 +++++++++++++++++++++++ test/PostgreSQL/Test.hs | 16 +++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 31f9b7562..5f076cffa 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -23,6 +23,7 @@ module Database.Esqueleto.PostgreSQL , chr , now_ , random_ + , deleteReturning , updateReturning , upsert , upsertBy @@ -503,3 +504,25 @@ updateReturning block = do conn <- R.ask conduit <- rawSelectSource UPDATE (tellReturning ReturningStar >> from block) liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource + +-- | `DELETE .. RETURNING` SQL extension supported by Postgres. +-- +-- The instances of 'InferReturning' say what can be returned; currently includes +-- whole entities, 'PersistField's, SQL expressions, tuples (possibly nested). +-- +-- Usage example: +-- +-- @ +-- removedRowsWithNames <- deleteReturning $ \p -> do +-- where_ (isNothing $ p ^. PersonWeight) +-- return (p, p ^. PersonName) +-- @ +-- +-- @since 3.5.9.1 +deleteReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) + => (from -> SqlQuery ex) + -> R.ReaderT backend m [ret] +deleteReturning block = do + conn <- R.ask + conduit <- rawSelectSource DELETE (tellReturning ReturningStar >> from block) + liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index 3a916c2ae..52cb73682 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1059,7 +1059,7 @@ testUpsert = testUpdateDeleteReturning :: SpecDb testUpdateDeleteReturning = describe "UPDATE .. RETURNING *" $ do - itDb "Whole entities, expressions and tuples get returned" $ do + itDb "Whole entities, expressions and tuples get returned from UPDATE" $ do [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] ret1 <- EP.updateReturning $ \p -> do set p [ PersonFavNum =. val 42 ] @@ -1074,6 +1074,20 @@ testUpdateDeleteReturning = asserting $ ret2 `shouldBe` [ (Value True, Value "Rachel", Value 200) , (Value True, Value "Mitch", Value 500) ] + itDb "Whole entities, expressions and tuples get returned from DELETE" $ do + [_p1k, p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] + ret1 <- EP.deleteReturning $ \p -> do + where_ (isNothing $ p ^. PersonWeight) + return (val (1 :: Int, 2 :: Int), p ^. PersonName, isNothing (p ^. PersonAge)) + asserting $ ret1 `shouldBe` [ (Value (1, 2), Value "John", Value False) + , (Value (1, 2), Value "Mike", Value False) + , (Value (1, 2), Value "Mitch", Value True) ] + ret2 <- EP.deleteReturning $ \p -> do + -- empty WHERE -- delete everything remaining... but: + return (p, p ^. PersonName) + asserting $ ret2 `shouldBe` [ (Entity p2k p2, Value "Rachel") + , (Entity p4k p4, Value "Livia") ] + testInsertSelectWithConflict :: SpecDb testInsertSelectWithConflict = describe "insertSelectWithConflict test" $ do From b8204ac9d4b273f6e16c28de8f82a79c64e73c21 Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Thu, 11 May 2023 13:36:37 +0200 Subject: [PATCH 10/15] feat: PR prep cleanup --- changelog.md | 1 - src/Database/Esqueleto/Internal/Internal.hs | 9 ++++----- test/PostgreSQL/Test.hs | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 1cf09376b..5df48e61c 100644 --- a/changelog.md +++ b/changelog.md @@ -53,7 +53,6 @@ - [#363](https://github.com/bitemyapp/esqueleto/pull/363) - Add missing `just` to left join examples in the Haddocks - 3.5.9.0 ======= - @9999years diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 600c38f99..9f3c745e9 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -1880,9 +1880,7 @@ data CommonTableExpressionClause = data ReturningClause = ReturningNothing -- ^ The default, absent clause. - | ReturningStar -- ^ @RETURNING *@ - -- | ReturningExprs (NonEmpty (SqlExpr Returning)) - -- ^ @output_expression [ [ AS ] output_name ] [, ...]@ + | ReturningStar -- ^ @RETURNING@ is present. data SubQueryType = NormalSubQuery @@ -2126,7 +2124,6 @@ instance Semigroup ReturningClause where (<>) ReturningNothing x = x (<>) x ReturningNothing = x (<>) ReturningStar ReturningStar = ReturningStar --- (<>) _ _ = error "instance Semigroup FIXME" instance Monoid ReturningClause where mempty = ReturningNothing @@ -2419,6 +2416,9 @@ existsHelper = sub SELECT . (>> return true) -- -- Many constructs appearing in @SELECT@ can go under @RETURNING@ -- but not all (e.g. -- certainly not subqueries, @VALUES@ and such). Thus, this is a subclass of 'SqlSelect'. +-- +-- The fundeps duplicate those of 'SqlSelect' solely to provide somewhat more directly +-- understandable type errors. class SqlSelect a r => InferReturning a r | r -> a, a -> r instance PersistEntity ent => InferReturning (SqlExpr (Entity ent)) (Entity ent) instance PersistEntity ent => InferReturning (SqlExpr (Maybe (Entity ent))) (Maybe (Entity ent)) @@ -3322,7 +3322,6 @@ makeReturning :: SqlSelect a r => IdentInfo -> ReturningClause -> a -> (TLB.Builder, [PersistValue]) makeReturning _ ReturningNothing _ = mempty makeReturning info ReturningStar ret = ("RETURNING ", []) <> sqlSelectCols info ret --- makeReturning info (ReturningExprs _) = undefined -- FIXME parens :: TLB.Builder -> TLB.Builder diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index 52cb73682..7759c68cc 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1058,7 +1058,7 @@ testUpsert = testUpdateDeleteReturning :: SpecDb testUpdateDeleteReturning = - describe "UPDATE .. RETURNING *" $ do + describe "UPDATE .. RETURNING .." $ do itDb "Whole entities, expressions and tuples get returned from UPDATE" $ do [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] ret1 <- EP.updateReturning $ \p -> do From 8ba4f3a52e817d53b5f5e1eaa2fe1f9f1b38b47b Mon Sep 17 00:00:00 2001 From: Max Ulidtko Date: Wed, 1 May 2024 10:31:27 +0200 Subject: [PATCH 11/15] docs: changelog --- changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/changelog.md b/changelog.md index 5df48e61c..13d2bb40f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,9 @@ +3.5.11.3 (unreleased) +======== +- @ulidtko + - [#362](https://github.com/bitemyapp/esqueleto/pull/362) + - Add `updateReturning`, `deleteReturning` as Postgres extensions. + 3.5.11.2 ======== - @arguri From 2e29743b772e6e20b1863d34fd846895d80bfdaf Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Fri, 1 Sep 2023 14:24:42 +0200 Subject: [PATCH 12/15] Update @since haddoc tags, after upstream did 2 releases while ignoring PR --- src/Database/Esqueleto/PostgreSQL.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 5f076cffa..eda3ae6c3 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -496,7 +496,7 @@ forShareOf lockableEntities onLockedBehavior = -- -- return p -- also works, returning (Entity Person) -- @ -- --- @since 3.5.9.1 +-- @since 3.5.10.2 updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] @@ -518,7 +518,7 @@ updateReturning block = do -- return (p, p ^. PersonName) -- @ -- --- @since 3.5.9.1 +-- @since 3.5.10.2 deleteReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] From 7a757897eb309ad195c203a70e26439f0eacc5fb Mon Sep 17 00:00:00 2001 From: max ulidtko Date: Fri, 1 Sep 2023 14:30:00 +0200 Subject: [PATCH 13/15] Update @since tags, after upstream released 3.5.10.2 while ignoring PR --- src/Database/Esqueleto/PostgreSQL.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index eda3ae6c3..6af1d4974 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -496,7 +496,7 @@ forShareOf lockableEntities onLockedBehavior = -- -- return p -- also works, returning (Entity Person) -- @ -- --- @since 3.5.10.2 +-- @since 3.5.10.3 updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] @@ -518,7 +518,7 @@ updateReturning block = do -- return (p, p ^. PersonName) -- @ -- --- @since 3.5.10.2 +-- @since 3.5.10.3 deleteReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] From 564cb837a8c98cde02ea22a237b0f020d00da44b Mon Sep 17 00:00:00 2001 From: Max Ulidtko Date: Wed, 1 May 2024 10:33:04 +0200 Subject: [PATCH 14/15] Update @since haddoc tags, after upstream did 3 releases while ignoring PR --- src/Database/Esqueleto/PostgreSQL.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 6af1d4974..87b79f3ab 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -496,7 +496,7 @@ forShareOf lockableEntities onLockedBehavior = -- -- return p -- also works, returning (Entity Person) -- @ -- --- @since 3.5.10.3 +-- @since 3.5.11.3 updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] @@ -518,7 +518,7 @@ updateReturning block = do -- return (p, p ^. PersonName) -- @ -- --- @since 3.5.10.3 +-- @since 3.5.11.3 deleteReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWrite backend) => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] From 1b93b6e3318d6a0b877585f9309b2acf3a7d5acd Mon Sep 17 00:00:00 2001 From: Max Ulidtko Date: Wed, 1 May 2024 17:04:11 +0200 Subject: [PATCH 15/15] fmt: indents and newlines --- src/Database/Esqueleto/Internal/Internal.hs | 18 ++++-- src/Database/Esqueleto/PostgreSQL.hs | 12 ++-- test/PostgreSQL/Test.hs | 61 +++++++++++---------- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/Database/Esqueleto/Internal/Internal.hs b/src/Database/Esqueleto/Internal/Internal.hs index 9f3c745e9..d72193ada 100644 --- a/src/Database/Esqueleto/Internal/Internal.hs +++ b/src/Database/Esqueleto/Internal/Internal.hs @@ -2121,13 +2121,13 @@ instance Monoid LockingClause where mappend = (<>) instance Semigroup ReturningClause where - (<>) ReturningNothing x = x - (<>) x ReturningNothing = x - (<>) ReturningStar ReturningStar = ReturningStar + (<>) ReturningNothing x = x + (<>) x ReturningNothing = x + (<>) ReturningStar ReturningStar = ReturningStar instance Monoid ReturningClause where - mempty = ReturningNothing - mappend = (<>) + mempty = ReturningNothing + mappend = (<>) ---------------------------------------------------------------------- @@ -2420,25 +2420,33 @@ existsHelper = sub SELECT . (>> return true) -- The fundeps duplicate those of 'SqlSelect' solely to provide somewhat more directly -- understandable type errors. class SqlSelect a r => InferReturning a r | r -> a, a -> r + instance PersistEntity ent => InferReturning (SqlExpr (Entity ent)) (Entity ent) + instance PersistEntity ent => InferReturning (SqlExpr (Maybe (Entity ent))) (Maybe (Entity ent)) + instance PersistField a => InferReturning (SqlExpr (Value a)) (Value a) + instance ( InferReturning a ra, InferReturning b rb) => InferReturning (a, b) (ra, rb) + instance ( InferReturning a ra , InferReturning b rb , InferReturning c rc ) => InferReturning (a, b, c) (ra, rb, rc) + instance ( InferReturning a ra , InferReturning b rb , InferReturning c rc , InferReturning d rd ) => InferReturning (a, b, c, d) (ra, rb, rc, rd) + instance ( InferReturning a ra , InferReturning b rb , InferReturning c rc , InferReturning d rd , InferReturning e re ) => InferReturning (a, b, c, d, e) (ra, rb, rc, rd, re) + instance ( InferReturning a ra , InferReturning b rb , InferReturning c rc diff --git a/src/Database/Esqueleto/PostgreSQL.hs b/src/Database/Esqueleto/PostgreSQL.hs index 87b79f3ab..1f8685c31 100644 --- a/src/Database/Esqueleto/PostgreSQL.hs +++ b/src/Database/Esqueleto/PostgreSQL.hs @@ -501,9 +501,9 @@ updateReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWr => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] updateReturning block = do - conn <- R.ask - conduit <- rawSelectSource UPDATE (tellReturning ReturningStar >> from block) - liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource + conn <- R.ask + conduit <- rawSelectSource UPDATE (tellReturning ReturningStar >> from block) + liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource -- | `DELETE .. RETURNING` SQL extension supported by Postgres. -- @@ -523,6 +523,6 @@ deleteReturning :: (MonadIO m, From from, InferReturning ex ret, SqlBackendCanWr => (from -> SqlQuery ex) -> R.ReaderT backend m [ret] deleteReturning block = do - conn <- R.ask - conduit <- rawSelectSource DELETE (tellReturning ReturningStar >> from block) - liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource + conn <- R.ask + conduit <- rawSelectSource DELETE (tellReturning ReturningStar >> from block) + liftIO . withAcquire conduit $ flip R.runReaderT conn . runSource diff --git a/test/PostgreSQL/Test.hs b/test/PostgreSQL/Test.hs index 7759c68cc..ef38a1c74 100644 --- a/test/PostgreSQL/Test.hs +++ b/test/PostgreSQL/Test.hs @@ -1058,35 +1058,38 @@ testUpsert = testUpdateDeleteReturning :: SpecDb testUpdateDeleteReturning = - describe "UPDATE .. RETURNING .." $ do - itDb "Whole entities, expressions and tuples get returned from UPDATE" $ do - [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] - ret1 <- EP.updateReturning $ \p -> do - set p [ PersonFavNum =. val 42 ] - where_ (p ^. PersonFavNum ==. val 4) - return p - asserting $ ret1 `shouldBe` [Entity p4k p4{ personFavNum = 42 }] - - ret2 <- EP.updateReturning $ \p -> do - set p [ PersonAge =. val (Just 0) ] - where_ (isNothing $ p ^. PersonAge) - return (val True, p ^. PersonName, (p ^. PersonFavNum) *. val 100) - asserting $ ret2 `shouldBe` [ (Value True, Value "Rachel", Value 200) - , (Value True, Value "Mitch", Value 500) ] - - itDb "Whole entities, expressions and tuples get returned from DELETE" $ do - [_p1k, p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] - ret1 <- EP.deleteReturning $ \p -> do - where_ (isNothing $ p ^. PersonWeight) - return (val (1 :: Int, 2 :: Int), p ^. PersonName, isNothing (p ^. PersonAge)) - asserting $ ret1 `shouldBe` [ (Value (1, 2), Value "John", Value False) - , (Value (1, 2), Value "Mike", Value False) - , (Value (1, 2), Value "Mitch", Value True) ] - ret2 <- EP.deleteReturning $ \p -> do - -- empty WHERE -- delete everything remaining... but: - return (p, p ^. PersonName) - asserting $ ret2 `shouldBe` [ (Entity p2k p2, Value "Rachel") - , (Entity p4k p4, Value "Livia") ] + describe "UPDATE .. RETURNING .." $ do + itDb "Whole entities, expressions and tuples get returned from UPDATE" $ do + [_p1k, _p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] + ret1 <- EP.updateReturning $ \p -> do + set p [ PersonFavNum =. val 42 ] + where_ (p ^. PersonFavNum ==. val 4) + return p + asserting $ ret1 `shouldBe` [Entity p4k p4{ personFavNum = 42 }] + + ret2 <- EP.updateReturning $ \p -> do + set p [ PersonAge =. val (Just 0) ] + where_ (isNothing $ p ^. PersonAge) + return (val True, p ^. PersonName, (p ^. PersonFavNum) *. val 100) + asserting $ ret2 `shouldBe` [ (Value True, Value "Rachel", Value 200) + , (Value True, Value "Mitch", Value 500) ] + + itDb "Whole entities, expressions and tuples get returned from DELETE" $ do + [_p1k, p2k, _p3k, p4k, _p5k] <- mapM insert [p1, p2, p3, p4, p5] + ret1 <- EP.deleteReturning $ \p -> do + where_ (isNothing $ p ^. PersonWeight) + return ( val (1 :: Int, 2 :: Int) + , p ^. PersonName + , isNothing (p ^. PersonAge) + ) + asserting $ ret1 `shouldBe` [ (Value (1, 2), Value "John", Value False) + , (Value (1, 2), Value "Mike", Value False) + , (Value (1, 2), Value "Mitch", Value True) ] + ret2 <- EP.deleteReturning $ \p -> do + -- empty WHERE -- delete everything remaining... but: + return (p, p ^. PersonName) + asserting $ ret2 `shouldBe` [ (Entity p2k p2, Value "Rachel") + , (Entity p4k p4, Value "Livia") ] testInsertSelectWithConflict :: SpecDb testInsertSelectWithConflict =