packages feed

persistent-postgresql 2.12.1.0 → 2.12.1.1

raw patch · 5 files changed

+343/−277 lines, 5 filesdep +hspec-expectations-liftedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: hspec-expectations-lifted

API changes (from Hackage documentation)

- Database.Persist.Postgresql: upsertManyWhere :: forall record backend m. (backend ~ PersistEntityBackend record, BackendCompatible SqlBackend backend, PersistEntityBackend record ~ SqlBackend, PersistEntity record, MonadIO m) => [record] -> [HandleUpdateCollision record] -> [Update record] -> [Filter record] -> ReaderT backend m ()
+ Database.Persist.Postgresql: upsertManyWhere :: forall record backend m. (backend ~ PersistEntityBackend record, BackendCompatible SqlBackend backend, PersistEntityBackend record ~ SqlBackend, PersistEntity record, OnlyOneUniqueKey record, MonadIO m) => [record] -> [HandleUpdateCollision record] -> [Update record] -> [Filter record] -> ReaderT backend m ()
- Database.Persist.Postgresql: upsertWhere :: (backend ~ PersistEntityBackend record, PersistEntity record, PersistEntityBackend record ~ SqlBackend, MonadIO m, PersistStore backend, BackendCompatible SqlBackend backend) => record -> [Update record] -> [Filter record] -> ReaderT backend m ()
+ Database.Persist.Postgresql: upsertWhere :: (backend ~ PersistEntityBackend record, PersistEntity record, PersistEntityBackend record ~ SqlBackend, MonadIO m, PersistStore backend, BackendCompatible SqlBackend backend, OnlyOneUniqueKey record) => record -> [Update record] -> [Filter record] -> ReaderT backend m ()

Files

ChangeLog.md view
@@ -1,5 +1,13 @@ # Changelog for persistent-postgresql +# 2.12.1.1++* [#1235](https://github.com/yesodweb/persistent/pull/1235)+    * `upsertWhere` and `upsertManyWhere` only worked in cases where a `Primary`+      key was defined on a record, and no other uniqueness constraints. They+      have been fixed to only work with records that have a single Uniqueness+      constraint defined.+ ## 2.12.1.0  * Added `upsertWhere` and `upsertManyWhere` to `persistent-postgresql`.  [#1222](https://github.com/yesodweb/persistent/pull/1222).
Database/Persist/Postgresql.hs view
@@ -1817,6 +1817,7 @@      , MonadIO m      , PersistStore backend      , BackendCompatible SqlBackend backend+     , OnlyOneUniqueKey record      )   => record   -> [Update record]@@ -1825,83 +1826,98 @@ upsertWhere record updates filts =   upsertManyWhere [record] [] updates filts --- | Exclude any record field if it doesn't match the filter record.  Used only in `upsertWhere` and--- `upsertManyWhere`------ @since 2.12.1.0--- TODO: we could probably make a sum type for the `Filter` record that's passed into the `upserWhere` and--- `upsertManyWhere` methods that has similar behavior to the HandleCollisionUpdate type.-excludeNotEqualToOriginal ::-  (PersistField typ-  , PersistEntity rec) =>-  EntityField rec typ ->-  Filter rec-excludeNotEqualToOriginal field =-  Filter-    { filterField =-        field,-      filterFilter =-        Ne,-      filterValue =-        UnsafeValue $-          PersistLiteral_-            Unescaped-            bsForExcludedField-    }-  where-    bsForExcludedField =-      T.encodeUtf8 $-        "EXCLUDED."-          <> fieldName field- -- | Postgres specific 'upsertManyWhere'. This method does the following: -- It will insert a record if no matching unique key exists. -- If a unique key exists, it will update the relevant field with a user-supplied value, however, -- it will only do this update on a user-supplied condition. -- For example, here's how this method could be called like such: ----- upsertManyWhere [record] [recordField =. newValue] [recordField /= newValue]+-- upsertManyWhere [record] [recordField =. newValue] [recordField !=. newValue] -- -- Called thusly, this method will insert a new record (if none exists) OR update a recordField with a new value -- assuming the condition in the last block is met. ----- -- @since 2.12.1.0-upsertManyWhere ::-    forall record backend m.-    ( backend ~ PersistEntityBackend record,-      BackendCompatible SqlBackend backend,-      PersistEntityBackend record ~ SqlBackend,-      PersistEntity record,-      MonadIO m-    ) =>-    -- | A list of the records you want to insert, or update-    [record] ->-    -- | A list of the fields you want to copy over.-    [HandleUpdateCollision record] ->-    -- | A list of the updates to apply that aren't dependent on the record being inserted.-    [Update record] ->-    -- | A filter condition that dictates the scope of the updates-    [Filter record] ->-    ReaderT backend m ()+-- @since 2.12.1.0+upsertManyWhere+    :: forall record backend m.+    ( backend ~ PersistEntityBackend record+    , BackendCompatible SqlBackend backend+    , PersistEntityBackend record ~ SqlBackend+    , PersistEntity record+    , OnlyOneUniqueKey record+    , MonadIO m+    )+    => [record]+    -- ^ A list of the records you want to insert, or update+    -> [HandleUpdateCollision record]+    -- ^ A list of the fields you want to copy over.+    -> [Update record]+    -- ^ A list of the updates to apply that aren't dependent on the record+    -- being inserted.+    -> [Filter record]+    -- ^ A filter condition that dictates the scope of the updates+    -> ReaderT backend m () upsertManyWhere [] _ _ _ = return () upsertManyWhere records fieldValues updates filters = do-  conn <- asks projectBackend-  uncurry rawExecute $-    mkBulkUpsertQuery records conn fieldValues updates filters+    conn <- asks projectBackend+    let uniqDef = -- onlyOneUniqueDef (Nothing :: Maybe record)+            case entityUniques (entityDef (Nothing :: Maybe record)) of+                [uniq] -> uniq+                _ -> error "impossible due to OnlyOneUniqueKey constraint"+            -- TODO: use onlyOneUniqueDef when it's exported+    uncurry rawExecute $+        mkBulkUpsertQuery records conn fieldValues updates filters uniqDef +-- | Exclude any record field if it doesn't match the filter record.  Used only in `upsertWhere` and+-- `upsertManyWhere`+--+-- TODO: we could probably make a sum type for the `Filter` record that's passed into the `upsertWhere` and+-- `upsertManyWhere` methods that has similar behavior to the HandleCollisionUpdate type.+--+-- @since 2.12.1.0+excludeNotEqualToOriginal+    :: (PersistField typ, PersistEntity rec)+    => EntityField rec typ+    -> Filter rec+excludeNotEqualToOriginal field =+    Filter+        { filterField =+            field+        , filterFilter =+            Ne+        , filterValue =+            UnsafeValue $+                PersistLiteral_+                    Unescaped+                    bsForExcludedField+        }+  where+    bsForExcludedField =+        T.encodeUtf8+            $ "EXCLUDED."+            <> fieldName field+ -- | This creates the query for 'upsertManyWhere'. If you -- provide an empty list of updates to perform, then it will generate -- a dummy/no-op update using the first field of the record. This avoids -- duplicate key exceptions. mkBulkUpsertQuery-    :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)-    => [record] -- ^ A list of the records you want to insert, or update+    :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend, OnlyOneUniqueKey record)+    => [record]+    -- ^ A list of the records you want to insert, or update     -> SqlBackend-    -> [HandleUpdateCollision record] -- ^ A list of the fields you want to copy over.-    -> [Update record] -- ^ A list of the updates to apply that aren't dependent on the record being inserted.-    -> [Filter record] -- ^ A filter condition that dictates the scope of the updates+    -> [HandleUpdateCollision record]+    -- ^ A list of the fields you want to copy over.+    -> [Update record]+    -- ^ A list of the updates to apply that aren't dependent on the record being inserted.+    -> [Filter record]+    -- ^ A filter condition that dictates the scope of the updates+    -> UniqueDef+    -- ^ The specific uniqueness constraint to use on the record. Postgres+    -- rquires that we use exactly one relevant constraint, and it can't do+    -- a catch-all. How frustrating!     -> (Text, [PersistValue])-mkBulkUpsertQuery records conn fieldValues updates filters =+mkBulkUpsertQuery records conn fieldValues updates filters uniqDef =   (q, recordValues <> updsValues <> copyUnlessValues <> whereVals)   where     mfieldDef x = case x of@@ -1910,7 +1926,8 @@     (fieldsToMaybeCopy, updateFieldNames) = partitionEithers $ map mfieldDef fieldValues     fieldDbToText = escapeF . fieldDB     entityDef' = entityDef records-    conflictColumns = escapeF . fieldDB <$> entityKeyFields entityDef'+    conflictColumns =+        map (escapeF . snd) $ uniqueFields uniqDef     firstField = case entityFieldNames of         [] -> error "The entity you're trying to insert does not have any fields."         (field:_) -> field@@ -1918,33 +1935,46 @@     nameOfTable = escapeE . entityDB $ entityDef'     copyUnlessValues = map snd fieldsToMaybeCopy     recordValues = concatMap (map toPersistValue . toPersistFields) records-    recordPlaceholders = Util.commaSeparated $ map (Util.parenWrapped . Util.commaSeparated . map (const "?") . toPersistFields) records+    recordPlaceholders =+        Util.commaSeparated+        $ map (Util.parenWrapped . Util.commaSeparated . map (const "?") . toPersistFields)+        $ records     mkCondFieldSet n _ =-      T.concat-        [ n-        , "=COALESCE("-        ,   "NULLIF("-        ,     "EXCLUDED."-        ,       n-        ,         ","-        ,           "?"-        ,         ")"-        ,       ","-        ,     nameOfTable-        ,   "."-        ,   n-        ,")"-        ]+        T.concat+            [ n+            , "=COALESCE("+            ,   "NULLIF("+            ,     "EXCLUDED."+            ,       n+            ,         ","+            ,           "?"+            ,         ")"+            ,       ","+            ,     nameOfTable+            ,   "."+            ,   n+            ,")"+            ]     condFieldSets = map (uncurry mkCondFieldSet) fieldsToMaybeCopy     fieldSets = map (\n -> T.concat [n, "=EXCLUDED.", n, ""]) updateFieldNames     upds = map (Util.mkUpdateText' (escapeF) (\n -> T.concat [nameOfTable, ".", n])) updates     updsValues = map (\(Update _ val _) -> toPersistValue val) updates-    (wher, whereVals) = if null filters-                          then ("", [])-                          else (filterClauseWithVals (Just PrefixTableName) conn filters)-    updateText = case fieldSets <> upds <> condFieldSets of-        [] -> T.concat [firstField, "=EXCLUDED.", firstField]-        xs -> Util.commaSeparated xs+    (wher, whereVals) =+        if null filters+        then ("", [])+        else (filterClauseWithVals (Just PrefixTableName) conn filters)+    updateText =+        case fieldSets <> upds <> condFieldSets of+            [] ->+                -- This case is really annoying, and probably unlikely to be+                -- actually hit - someone would have had to call something like+                -- `upsertManyWhere [] [] []`, but that would have been caught+                -- by the prior case.+                -- Would be nice to have something like a `NonEmpty (These ...)`+                -- instead of multiple lists...+                T.concat [firstField, "=", nameOfTable, ".", firstField]+            xs ->+                Util.commaSeparated xs     q = T.concat         [ "INSERT INTO "         , nameOfTable
persistent-postgresql.cabal view
@@ -1,5 +1,5 @@ name:            persistent-postgresql-version:         2.12.1.0+version:         2.12.1.1 license:         MIT license-file:    LICENSE author:          Felipe Lessa, Michael Snoyman <michael@snoyman.com>@@ -68,6 +68,7 @@                    , HUnit                    , hspec                >= 2.4                    , hspec-expectations+                   , hspec-expectations-lifted                    , monad-logger                    , QuickCheck                    , quickcheck-instances
test/PgInit.hs view
@@ -1,58 +1,85 @@-{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -module PgInit (-  runConn-  , runConn_-  , runConnAssert-  , runConnAssertUseConf+module PgInit+    ( runConn+    , runConn_+    , runConnAssert+    , runConnAssertUseConf -  , MonadIO-  , persistSettings-  , MkPersistSettings (..)-  , BackendKey(..)-  , GenerateKey(..)+    , MonadIO+    , persistSettings+    , MkPersistSettings (..)+    , BackendKey(..)+    , GenerateKey(..) -   -- re-exports-  , module Control.Monad.Trans.Reader-  , module Control.Monad-  , module Database.Persist.Sql-  , module Database.Persist-  , module Database.Persist.Sql.Raw.QQ-  , module Init-  , module Test.Hspec-  , module Test.HUnit-  , BS.ByteString-  , Int32, Int64-  , liftIO-  , mkPersist, mkMigrate, share, sqlSettings, persistLowerCase, persistUpperCase-  , SomeException-  , Text-  , TestFn(..)-  ) where+     -- re-exports+    , module Control.Monad.Trans.Reader+    , module Control.Monad+    , module Database.Persist.Sql+    , module Database.Persist+    , module Database.Persist.Sql.Raw.QQ+    , module Init+    , module Test.Hspec+    , module Test.Hspec.Expectations.Lifted+    , module Test.HUnit+    , BS.ByteString+    , Int32, Int64+    , liftIO+    , mkPersist, mkMigrate, share, sqlSettings, persistLowerCase, persistUpperCase+    , SomeException+    , Text+    , TestFn(..)+    , LoggingT+    , ResourceT+    ) where  import Init-    ( TestFn(..), truncateTimeOfDay, truncateUTCTime-    , truncateToMicro, arbText, liftA2, GenerateKey(..)-    , (@/=), (@==), (==@), MonadFail-    , assertNotEqual, assertNotEmpty, assertEmpty, asIO-    , isTravis, RunDb-    )+       ( GenerateKey(..)+       , MonadFail+       , RunDb+       , TestFn(..)+       , arbText+       , asIO+       , assertEmpty+       , assertNotEmpty+       , assertNotEqual+       , isTravis+       , liftA2+       , truncateTimeOfDay+       , truncateToMicro+       , truncateUTCTime+       , (==@)+       , (@/=)+       , (@==)+       )  -- re-exports import Control.Exception (SomeException)-import UnliftIO-import Control.Monad (void, replicateM, liftM, when, forM_)+import Control.Monad (forM_, liftM, replicateM, void, when) import Control.Monad.Trans.Reader import Data.Aeson (Value(..))-import Database.Persist.TH (mkPersist, mkMigrate, share, sqlSettings, persistLowerCase, persistUpperCase, MkPersistSettings(..))+import Database.Persist.Postgresql.JSON () import Database.Persist.Sql.Raw.QQ-import Database.Persist.Postgresql.JSON()+import Database.Persist.TH+       ( MkPersistSettings(..)+       , mkMigrate+       , mkPersist+       , persistLowerCase+       , persistUpperCase+       , share+       , sqlSettings+       ) import Test.Hspec+       (Spec, afterAll_, before, beforeAll, describe, fdescribe, fit, it,+       before_, SpecWith, Arg, hspec)+import Test.Hspec.Expectations.Lifted import Test.QuickCheck.Instances ()+import UnliftIO  -- testing-import Test.HUnit ((@?=),(@=?), Assertion, assertFailure, assertBool)+import Test.HUnit (Assertion, assertBool, assertFailure, (@=?), (@?=)) import Test.QuickCheck  import Control.Monad (unless, (>=>))
test/UpsertWhere.hs view
@@ -1,178 +1,178 @@-{-# LANGUAGE DataKinds, FlexibleInstances, MultiParamTypeClasses, ExistentialQuantification #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE StandaloneDeriving #-}  module UpsertWhere where -import Data.List              (sort)+import PgInit  import Database.Persist.Postgresql-import PgInit  share [mkPersist sqlSettings, mkMigrate "upsertWhereMigrate"] [persistLowerCase|-  Item-     name        Text sqltype=varchar(80)-     description Text-     price       Double Maybe-     quantity    Int Maybe -     Primary name-     deriving Eq Show Ord+Item+    name        Text sqltype=varchar(80)+    description Text+    price       Double Maybe+    quantity    Int Maybe +    UniqueName name+    deriving Eq Show Ord+ |] +wipe :: IO ()+wipe = runConnAssert $ do+    deleteWhere ([] :: [Filter Item])++itDb :: String -> SqlPersistT (LoggingT (ResourceT IO)) a -> SpecWith (Arg (IO ()))+itDb msg action = it msg $ runConnAssert $ void action+ specs :: Spec specs = describe "UpsertWhere" $ do-  let item1 = Item "item1" "" (Just 3) Nothing-      item2 = Item "item2" "hello world" Nothing (Just 2)-      items = [item1, item2]+    let item1 = Item "item1" "" (Just 3) Nothing+        item2 = Item "item2" "hello world" Nothing (Just 2)+        items = [item1, item2] -  describe "upsertWhere" $ do-    it "inserts appropriately" $ runConnAssert $ do-      deleteWhere ([] :: [Filter Item])-      upsertWhere item1 [ItemDescription =. "i am item 1"] []-      Just item <- get (ItemKey "item1")-      item @== item1-    it "performs only updates given if record already exists" $ runConnAssert $ do-      deleteWhere ([] :: [Filter Item])-      let newDescription = "I am a new description"-      insert_ item1-      upsertWhere-        (Item "item1" "i am inserted description" (Just 1) (Just 2))-        [ItemDescription =. newDescription] -        []-      Just item <- get (ItemKey "item1")-      item @== item1 { itemDescription = newDescription }+    describe "upsertWhere" $ before_ wipe $ do+        itDb "inserts appropriately" $ do+            upsertWhere item1 [ItemDescription =. "i am item 1"] []+            Just item <- fmap entityVal <$> getBy (UniqueName "item1")+            item `shouldBe` item1+        itDb "performs only updates given if record already exists" $ do+            let newDescription = "I am a new description"+            insert_ item1+            upsertWhere+                (Item "item1" "i am an inserted description" (Just 1) (Just 2))+                [ItemDescription =. newDescription]+                []+            Just item <- fmap entityVal <$> getBy (UniqueName "item1")+            item `shouldBe` item1 { itemDescription = newDescription } -  describe "upsertManyWhere" $ do-    it "inserts fresh records" $ runConnAssert $ do-      deleteWhere ([] :: [Filter Item])-      insertMany_ items-      let newItem = Item "item3" "fresh" Nothing Nothing-      upsertManyWhere-        (newItem : items)-        [copyField ItemDescription]-        []-        []-      dbItems <- map entityVal <$> selectList [] []-      sort dbItems @== sort (newItem : items)-    it "updates existing records" $ runConnAssert $ do-      deleteWhere ([] :: [Filter Item])-      let postUpdate = map (\i -> i { itemQuantity = fmap (+1) (itemQuantity i) }) items-      insertMany_ items-      upsertManyWhere-        items-        []-        [ItemQuantity +=. Just 1]-        []-      dbItems <- sort . fmap entityVal <$> selectList [] []-      dbItems @== sort postUpdate-    it "only copies passing values" $ runConnAssert $ do-      deleteWhere ([] :: [Filter Item])-      insertMany_ items-      let newItems = map (\i -> i { itemQuantity = Just 0, itemPrice = fmap (*2) (itemPrice i) }) items-          postUpdate = map (\i -> i { itemPrice = fmap (*2) (itemPrice i) }) items-      upsertManyWhere-        newItems-        [ -          copyUnlessEq ItemQuantity (Just 0)-        , copyField ItemPrice-        ]-        []-        []-      dbItems <- sort . fmap entityVal <$> selectList [] []-      dbItems @== sort postUpdate-    it "inserts without modifying existing records if no updates specified" $ runConnAssert $ do-      let newItem = Item "item3" "hi friends!" Nothing Nothing-      deleteWhere ([] :: [Filter Item])-      insertMany_ items-      upsertManyWhere-        (newItem : items)-        []-        []-        []-      dbItems <- sort . fmap entityVal <$> selectList [] []-      dbItems @== sort (newItem : items)-    it "inserts without modifying existing records if no updates specified and there's a filter with True condition" $-      runConnAssert $ do-        let newItem = Item "item3" "hi friends!" Nothing Nothing-        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          []-          []-          [ItemDescription ==. "hi friends!"]-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : items)-    it "inserts without updating existing records if there are updates specified but there's a filter with a False condition" $-      runConnAssert $ do-        let newItem = Item "item3" "hi friends!" Nothing Nothing-        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          []-          [ItemQuantity +=. Just 1]-          [ItemDescription ==. "hi friends!"]-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : items)-    it "inserts new records but does not update existing records if there are updates specified but the modification condition is False" $-      runConnAssert $ do-        let newItem = Item "item3" "hi friends!" Nothing Nothing-        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          []-          [ItemQuantity +=. Just 1]-          [excludeNotEqualToOriginal ItemDescription]-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : items)-    it "inserts new records and updates existing records if there are updates specified and the modification condition is True (because it's empty)" $-      runConnAssert $ do-        let newItem = Item "item3" "hello world" Nothing Nothing-            postUpdate = map (\i -> i {itemQuantity = fmap (+ 1) (itemQuantity i)}) items-        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          []-          [ItemQuantity +=. Just 1]-          []-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : postUpdate)-    it "inserts new records and updates existing records if there are updates specified and the modification filter condition is triggered" $-       runConnAssert $ do-        let newItem = Item "item3" "hi friends!" Nothing Nothing-            postUpdate = map (\i -> i {itemQuantity = fmap (+1) (itemQuantity i)}) items-        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          [ -            copyUnlessEq ItemDescription "hi friends!"-          , copyField ItemPrice-          ]-          [ItemQuantity +=. Just 1]-          [ItemDescription !=. "bye friends!"]-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : postUpdate)-    it "inserts an item and doesn't apply the update if the filter condition is triggered" $ -      runConnAssert $ do-        let newItem = Item "item3" "hello world" Nothing Nothing -        deleteWhere ([] :: [Filter Item])-        insertMany_ items-        upsertManyWhere-          (newItem : items)-          []-          [ItemQuantity +=. Just 1]-          [excludeNotEqualToOriginal ItemDescription]-        dbItems <- sort . fmap entityVal <$> selectList [] []-        dbItems @== sort (newItem : items)+    describe "upsertManyWhere" $ do+        itDb "inserts fresh records" $ do+            insertMany_ items+            let newItem = Item "item3" "fresh" Nothing Nothing+            upsertManyWhere+                (newItem : items)+                [copyField ItemDescription]+                []+                []+            dbItems <- map entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)+        itDb "updates existing records" $ do+            let+                postUpdate =+                    map (\i -> i { itemQuantity = fmap (+1) (itemQuantity i) }) items+            insertMany_ items+            upsertManyWhere+                items+                []+                [ItemQuantity +=. Just 1]+                []+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` postUpdate+        itDb "only copies passing values" $ do+            insertMany_ items+            let newItems = map (\i -> i { itemQuantity = Just 0, itemPrice = fmap (*2) (itemPrice i) }) items+                postUpdate = map (\i -> i { itemPrice = fmap (*2) (itemPrice i) }) items+            upsertManyWhere+                newItems+                [ copyUnlessEq ItemQuantity (Just 0)+                , copyField ItemPrice+                ]+                []+                []+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` postUpdate+        itDb "inserts without modifying existing records if no updates specified" $ do+            let newItem = Item "item3" "hi friends!" Nothing Nothing+            insertMany_ items+            upsertManyWhere+                (newItem : items)+                []+                []+                []+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)+        itDb "inserts without modifying existing records if no updates specified and there's a filter with True condition" $+          do+            let newItem = Item "item3" "hi friends!" Nothing Nothing+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              []+              []+              [ItemDescription ==. "hi friends!"]+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)+        itDb "inserts without updating existing records if there are updates specified but there's a filter with a False condition" $+          do+            let newItem = Item "item3" "hi friends!" Nothing Nothing+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              []+              [ItemQuantity +=. Just 1]+              [ItemDescription ==. "hi friends!"]+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)+        itDb "inserts new records but does not update existing records if there are updates specified but the modification condition is False" $+          do+            let newItem = Item "item3" "hi friends!" Nothing Nothing+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              []+              [ItemQuantity +=. Just 1]+              [excludeNotEqualToOriginal ItemDescription]+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)+        itDb "inserts new records and updates existing records if there are updates specified and the modification condition is True (because it's empty)" $+          do+            let newItem = Item "item3" "hello world" Nothing Nothing+                postUpdate = map (\i -> i {itemQuantity = fmap (+ 1) (itemQuantity i)}) items+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              []+              [ItemQuantity +=. Just 1]+              []+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : postUpdate)+        itDb "inserts new records and updates existing records if there are updates specified and the modification filter condition is triggered" $+           do+            let newItem = Item "item3" "hi friends!" Nothing Nothing+                postUpdate = map (\i -> i {itemQuantity = fmap (+1) (itemQuantity i)}) items+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              [+                copyUnlessEq ItemDescription "hi friends!"+              , copyField ItemPrice+              ]+              [ItemQuantity +=. Just 1]+              [ItemDescription !=. "bye friends!"]+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : postUpdate)+        itDb "inserts an item and doesn't apply the update if the filter condition is triggered" $+          do+            let newItem = Item "item3" "hello world" Nothing Nothing+            insertMany_ items+            upsertManyWhere+              (newItem : items)+              []+              [ItemQuantity +=. Just 1]+              [excludeNotEqualToOriginal ItemDescription]+            dbItems <- fmap entityVal <$> selectList [] []+            dbItems `shouldMatchList` (newItem : items)