1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE ScopedTypeVariables #-}
3{-# LANGUAGE TypeOperators #-}
4
5module Database.Persist.Class.PersistUnique
6  ( PersistUniqueRead(..)
7  , PersistUniqueWrite(..)
8  , OnlyOneUniqueKey(..)
9  , onlyOneUniqueDef
10  , AtLeastOneUniqueKey(..)
11  , atLeastOneUniqueDef
12  , NoUniqueKeysError
13  , MultipleUniqueKeysError
14  , getByValue
15  , getByValueUniques
16  , insertBy
17  , insertUniqueEntity
18  , replaceUnique
19  , checkUnique
20  , onlyUnique
21  , defaultPutMany
22  , persistUniqueKeyValues
23  )
24  where
25
26import Control.Monad (liftM)
27import Control.Monad.IO.Class (MonadIO)
28import Control.Monad.Trans.Reader (ReaderT)
29import Data.Function (on)
30import Data.List ((\\), deleteFirstsBy)
31import Data.List.NonEmpty (NonEmpty(..))
32import qualified Data.List.NonEmpty as NEL
33import qualified Data.Map as Map
34import Data.Maybe (catMaybes)
35import Data.Text (Text)
36import GHC.TypeLits (ErrorMessage(..))
37
38import Database.Persist.Types
39import Database.Persist.Class.PersistStore
40import Database.Persist.Class.PersistEntity
41
42-- | Queries against 'Unique' keys (other than the id 'Key').
43--
44-- Please read the general Persistent documentation to learn how to create
45-- 'Unique' keys.
46--
47-- Using this with an Entity without a Unique key leads to undefined
48-- behavior.  A few of these functions require a /single/ 'Unique', so using
49-- an Entity with multiple 'Unique's is also undefined. In these cases
50-- persistent's goal is to throw an exception as soon as possible, but
51-- persistent is still transitioning to that.
52--
53-- SQL backends automatically create uniqueness constraints, but for MongoDB
54-- you must manually place a unique index on a field to have a uniqueness
55-- constraint.
56--
57class (PersistCore backend, PersistStoreRead backend) =>
58      PersistUniqueRead backend  where
59    -- | Get a record by unique key, if available. Returns also the identifier.
60    --
61    -- === __Example usage__
62    --
63    -- With <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1>:
64    --
65    -- > getBySpjName :: MonadIO m  => ReaderT SqlBackend m (Maybe (Entity User))
66    -- > getBySpjName = getBy $ UniqueUserName "SPJ"
67    --
68    -- > mSpjEnt <- getBySpjName
69    --
70    -- The above query when applied on <#dataset-persist-unique-1 dataset-1>, will get this entity:
71    --
72    -- > +----+------+-----+
73    -- > | id | name | age |
74    -- > +----+------+-----+
75    -- > |  1 | SPJ  |  40 |
76    -- > +----+------+-----+
77    getBy
78        :: (MonadIO m, PersistRecordBackend record backend)
79        => Unique record -> ReaderT backend m (Maybe (Entity record))
80
81-- | Some functions in this module ('insertUnique', 'insertBy', and
82-- 'replaceUnique') first query the unique indexes to check for
83-- conflicts. You could instead optimistically attempt to perform the
84-- operation (e.g. 'replace' instead of 'replaceUnique'). However,
85--
86--  * there is some fragility to trying to catch the correct exception and
87--  determing the column of failure;
88--
89--  * an exception will automatically abort the current SQL transaction.
90class (PersistUniqueRead backend, PersistStoreWrite backend) =>
91      PersistUniqueWrite backend  where
92    -- | Delete a specific record by unique key. Does nothing if no record
93    -- matches.
94    --
95    -- === __Example usage__
96    --
97    -- With <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1>,
98    --
99    -- > deleteBySpjName :: MonadIO m => ReaderT SqlBackend m ()
100    -- > deleteBySpjName = deleteBy UniqueUserName "SPJ"
101    --
102    -- The above query when applied on <#dataset-persist-unique-1 dataset-1>, will produce this:
103    --
104    -- > +-----+------+-----+
105    -- > |id   |name  |age  |
106    -- > +-----+------+-----+
107    -- > |2    |Simon |41   |
108    -- > +-----+------+-----+
109    deleteBy
110        :: (MonadIO m, PersistRecordBackend record backend)
111        => Unique record -> ReaderT backend m ()
112
113    -- | Like 'insert', but returns 'Nothing' when the record
114    -- couldn't be inserted because of a uniqueness constraint.
115    --
116    -- === __Example usage__
117    --
118    -- With <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1>, we try to insert the following two records:
119    --
120    -- > linusId <- insertUnique $ User "Linus" 48
121    -- > spjId   <- insertUnique $ User "SPJ" 90
122    --
123    -- > +-----+------+-----+
124    -- > |id   |name  |age  |
125    -- > +-----+------+-----+
126    -- > |1    |SPJ   |40   |
127    -- > +-----+------+-----+
128    -- > |2    |Simon |41   |
129    -- > +-----+------+-----+
130    -- > |3    |Linus |48   |
131    -- > +-----+------+-----+
132    --
133    -- Linus's record was inserted to <#dataset-persist-unique-1 dataset-1>, while SPJ wasn't because SPJ already exists in <#dataset-persist-unique-1 dataset-1>.
134    insertUnique
135        :: (MonadIO m, PersistRecordBackend record backend)
136        => record -> ReaderT backend m (Maybe (Key record))
137    insertUnique datum = do
138        conflict <- checkUnique datum
139        case conflict of
140            Nothing -> Just `liftM` insert datum
141            Just _ -> return Nothing
142
143    -- | Update based on a uniqueness constraint or insert:
144    --
145    -- * insert the new record if it does not exist;
146    -- * If the record exists (matched via it's uniqueness constraint), then update the existing record with the parameters which is passed on as list to the function.
147    --
148    -- === __Example usage__
149    --
150    -- First, we try to explain 'upsert' using <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1>.
151    --
152    -- > upsertSpj :: MonadIO m => [Update User] -> ReaderT SqlBackend m (Maybe (Entity User))
153    -- > upsertSpj updates = upsert (User "SPJ" 999) upadtes
154    --
155    -- > mSpjEnt <- upsertSpj [UserAge +=. 15]
156    --
157    -- The above query when applied on <#dataset-persist-unique-1 dataset-1>, will produce this:
158    --
159    -- > +-----+-----+--------+
160    -- > |id   |name |age     |
161    -- > +-----+-----+--------+
162    -- > |1    |SPJ  |40 -> 55|
163    -- > +-----+-----+--------+
164    -- > |2    |Simon|41      |
165    -- > +-----+-----+--------+
166    --
167    -- > upsertX :: MonadIO m => [Update User] -> ReaderT SqlBackend m (Maybe (Entity User))
168    -- > upsertX updates = upsert (User "X" 999) updates
169    --
170    -- > mXEnt <- upsertX [UserAge +=. 15]
171    --
172    -- The above query when applied on <#dataset-persist-unique-1 dataset-1>, will produce this:
173    --
174    -- > +-----+-----+--------+
175    -- > |id   |name |age     |
176    -- > +-----+-----+--------+
177    -- > |1    |SPJ  |40      |
178    -- > +-----+-----+--------+
179    -- > |2    |Simon|41      |
180    -- > +-----+-----+--------+
181    -- > |3    |X    |999     |
182    -- > +-----+-----+--------+
183    --
184    -- Next, what if the schema has two uniqueness constraints?
185    -- Let's check it out using <#schema-persist-unique-2 schema-2>:
186    --
187    -- > mSpjEnt <- upsertSpj [UserAge +=. 15]
188    --
189    -- This fails with a compile-time type error alerting us to the fact
190    -- that this record has multiple unique keys, and suggests that we look for
191    -- 'upsertBy' to select the unique key we want.
192    upsert
193        :: (MonadIO m, PersistRecordBackend record backend, OnlyOneUniqueKey record)
194        => record
195        -- ^ new record to insert
196        -> [Update record]
197        -- ^ updates to perform if the record already exists
198        -> ReaderT backend m (Entity record)
199        -- ^ the record in the database after the operation
200    upsert record updates = do
201        uniqueKey <- onlyUnique record
202        upsertBy uniqueKey record updates
203
204    -- | Update based on a given uniqueness constraint or insert:
205    --
206    -- * insert the new record if it does not exist;
207    -- * update the existing record that matches the given uniqueness constraint.
208    --
209    -- === __Example usage__
210    --
211    -- We try to explain 'upsertBy' using <#schema-persist-unique-2 schema-2> and <#dataset-persist-unique-1 dataset-1>.
212    --
213    -- > upsertBySpjName :: MonadIO m => User -> [Update User] -> ReaderT SqlBackend m (Entity User)
214    -- > upsertBySpjName record updates = upsertBy (UniqueUserName "SPJ") record updates
215    --
216    -- > mSpjEnt <- upsertBySpjName (Person "X" 999) [PersonAge += .15]
217    --
218    -- The above query will alter <#dataset-persist-unique-1 dataset-1> to:
219    --
220    -- > +-----+-----+--------+
221    -- > |id   |name |age     |
222    -- > +-----+-----+--------+
223    -- > |1    |SPJ  |40 -> 55|
224    -- > +-----+-----+--------+
225    -- > |2    |Simon|41      |
226    -- > +-----+-----+--------+
227    --
228    -- > upsertBySimonAge :: MonadIO m => User -> [Update User] -> ReaderT SqlBackend m (Entity User)
229    -- > upsertBySimonAge record updates = upsertBy (UniqueUserName "SPJ") record updates
230    --
231    -- > mPhilipEnt <- upsertBySimonAge (User "X" 999) [UserName =. "Philip"]
232    --
233    -- The above query will alter <#dataset-persist-unique-1 dataset-1> to:
234    --
235    -- > +----+-----------------+-----+
236    -- > | id |      name       | age |
237    -- > +----+-----------------+-----+
238    -- > |  1 | SPJ             |  40 |
239    -- > +----+-----------------+-----+
240    -- > |  2 | Simon -> Philip |  41 |
241    -- > +----+-----------------+-----+
242    --
243    -- > upsertByUnknownName :: MonadIO m => User -> [Update User] -> ReaderT SqlBackend m (Entity User)
244    -- > upsertByUnknownName record updates = upsertBy (UniqueUserName "Unknown") record updates
245    --
246    -- > mXEnt <- upsertByUnknownName (User "X" 999) [UserAge +=. 15]
247    --
248    -- This query will alter <#dataset-persist-unique-1 dataset-1> to:
249    --
250    -- > +-----+-----+-----+
251    -- > |id   |name |age  |
252    -- > +-----+-----+-----+
253    -- > |1    |SPJ  |40   |
254    -- > +-----+-----+-----+
255    -- > |2    |Simon|41   |
256    -- > +-----+-----+-----+
257    -- > |3    |X    |999  |
258    -- > +-----+-----+-----+
259    upsertBy
260        :: (MonadIO m, PersistRecordBackend record backend)
261        => Unique record
262        -- ^ uniqueness constraint to find by
263        -> record
264        -- ^ new record to insert
265        -> [Update record]
266        -- ^ updates to perform if the record already exists
267        -> ReaderT backend m (Entity record)
268        -- ^ the record in the database after the operation
269    upsertBy uniqueKey record updates = do
270        mrecord <- getBy uniqueKey
271        maybe (insertEntity record) (`updateGetEntity` updates) mrecord
272      where
273        updateGetEntity (Entity k _) upds =
274            (Entity k) `liftM` (updateGet k upds)
275
276    -- | Put many records into db
277    --
278    -- * insert new records that do not exist (or violate any unique constraints)
279    -- * replace existing records (matching any unique constraint)
280    --
281    -- @since 2.8.1
282    putMany
283        ::
284        ( MonadIO m
285        , PersistRecordBackend record backend
286        )
287        => [record]
288        -- ^ A list of the records you want to insert or replace.
289        -> ReaderT backend m ()
290    putMany = defaultPutMany
291
292-- | This class is used to ensure that 'upsert' is only called on records
293-- that have a single 'Unique' key. The quasiquoter automatically generates
294-- working instances for appropriate records, and generates 'TypeError'
295-- instances for records that have 0 or multiple unique keys.
296--
297-- @since 2.10.0
298class PersistEntity record => OnlyOneUniqueKey record where
299    onlyUniqueP :: record -> Unique record
300
301-- | Given a proxy for a 'PersistEntity' record, this returns the sole
302-- 'UniqueDef' for that entity.
303--
304-- @since 2.10.0
305onlyOneUniqueDef
306    :: (OnlyOneUniqueKey record, Monad proxy)
307    => proxy record
308    -> UniqueDef
309onlyOneUniqueDef prxy =
310    case entityUniques (entityDef prxy) of
311        [uniq] -> uniq
312        _ -> error "impossible due to OnlyOneUniqueKey constraint"
313
314-- | This is an error message. It is used when writing instances of
315-- 'OnlyOneUniqueKey' for an entity that has no unique keys.
316--
317-- @since 2.10.0
318type NoUniqueKeysError ty =
319    'Text "The entity "
320        ':<>: 'ShowType ty
321        ':<>: 'Text " does not have any unique keys."
322    ':$$: 'Text "The function you are trying to call requires a unique key "
323        ':<>: 'Text "to be defined on the entity."
324
325-- | This is an error message. It is used when an entity has multiple
326-- unique keys, and the function expects a single unique key.
327--
328-- @since 2.10.0
329type MultipleUniqueKeysError ty =
330    'Text "The entity "
331        ':<>: 'ShowType ty
332        ':<>: 'Text " has multiple unique keys."
333    ':$$: 'Text "The function you are trying to call requires only a single "
334        ':<>: 'Text "unique key."
335    ':$$: 'Text "There is probably a variant of the function with 'By' "
336        ':<>: 'Text "appended that will allow you to select a unique key "
337        ':<>: 'Text "for the operation."
338
339-- | This class is used to ensure that functions requring at least one
340-- unique key are not called with records that have 0 unique keys. The
341-- quasiquoter automatically writes working instances for appropriate
342-- entities, and generates 'TypeError' instances for records that have
343-- 0 unique keys.
344--
345-- @since 2.10.0
346class PersistEntity record => AtLeastOneUniqueKey record where
347    requireUniquesP :: record -> NonEmpty (Unique record)
348
349-- | Given a proxy for a record that has an instance of
350-- 'AtLeastOneUniqueKey', this returns a 'NonEmpty' list of the
351-- 'UniqueDef's for that entity.
352--
353-- @since 2.10.0
354atLeastOneUniqueDef
355    :: (AtLeastOneUniqueKey record, Monad proxy)
356    => proxy record
357    -> NonEmpty UniqueDef
358atLeastOneUniqueDef prxy =
359    case entityUniques (entityDef prxy) of
360        (x:xs) -> x :| xs
361        _ ->
362            error "impossible due to AtLeastOneUniqueKey record constraint"
363
364-- | Insert a value, checking for conflicts with any unique constraints.  If a
365-- duplicate exists in the database, it is returned as 'Left'. Otherwise, the
366-- new 'Key is returned as 'Right'.
367--
368-- === __Example usage__
369--
370-- With <#schema-persist-unique-2 schema-2> and <#dataset-persist-unique-1 dataset-1>, we have following lines of code:
371--
372-- > l1 <- insertBy $ User "SPJ" 20
373-- > l2 <- insertBy $ User "XXX" 41
374-- > l3 <- insertBy $ User "SPJ" 40
375-- > r1 <- insertBy $ User "XXX" 100
376--
377-- First three lines return 'Left' because there're duplicates in given record's uniqueness constraints. While the last line returns a new key as 'Right'.
378insertBy
379    ::
380    ( MonadIO m
381    , PersistUniqueWrite backend
382    , PersistRecordBackend record backend
383    , AtLeastOneUniqueKey record
384    )
385    => record -> ReaderT backend m (Either (Entity record) (Key record))
386insertBy val = do
387    res <- getByValue val
388    case res of
389        Nothing -> Right `liftM` insert val
390        Just z -> return $ Left z
391
392-- | Like 'insertEntity', but returns 'Nothing' when the record
393-- couldn't be inserted because of a uniqueness constraint.
394--
395-- @since 2.7.1
396--
397-- === __Example usage__
398--
399-- We use <#schema-persist-unique-2 schema-2> and <#dataset-persist-unique-1 dataset-1> here.
400--
401-- > insertUniqueSpjEntity :: MonadIO m => ReaderT SqlBackend m (Maybe (Entity User))
402-- > insertUniqueSpjEntity = insertUniqueEntity $ User "SPJ" 50
403--
404-- > mSpjEnt <- insertUniqueSpjEntity
405--
406-- The above query results 'Nothing' as SPJ already exists.
407--
408-- > insertUniqueAlexaEntity :: MonadIO m => ReaderT SqlBackend m (Maybe (Entity User))
409-- > insertUniqueAlexaEntity = insertUniqueEntity $ User "Alexa" 3
410--
411-- > mAlexaEnt <- insertUniqueSpjEntity
412--
413-- Because there's no such unique keywords of the given record, the above query when applied on <#dataset-persist-unique-1 dataset-1>, will produce this:
414--
415-- > +----+-------+-----+
416-- > | id | name  | age |
417-- > +----+-------+-----+
418-- > |  1 | SPJ   |  40 |
419-- > +----+-------+-----+
420-- > |  2 | Simon |  41 |
421-- > +----+-------+-----+
422-- > |  3 | Alexa |   3 |
423-- > +----+-------+-----+
424
425insertUniqueEntity
426    :: (MonadIO m
427       ,PersistRecordBackend record backend
428       ,PersistUniqueWrite backend)
429    => record -> ReaderT backend m (Maybe (Entity record))
430insertUniqueEntity datum =
431  fmap (\key -> Entity key datum) `liftM` insertUnique datum
432
433-- | Return the single unique key for a record.
434--
435-- === __Example usage__
436--
437-- We use shcema-1 and <#dataset-persist-unique-1 dataset-1> here.
438--
439-- > onlySimonConst :: MonadIO m => ReaderT SqlBackend m (Unique User)
440-- > onlySimonConst = onlyUnique $ User "Simon" 999
441--
442-- > mSimonConst <- onlySimonConst
443--
444-- @mSimonConst@ would be Simon's uniqueness constraint. Note that
445-- @onlyUnique@ doesn't work if there're more than two constraints. It will
446-- fail with a type error instead.
447onlyUnique
448    ::
449    ( MonadIO m
450    , PersistUniqueWrite backend
451    , PersistRecordBackend record backend
452    , OnlyOneUniqueKey record
453    )
454    => record -> ReaderT backend m (Unique record)
455onlyUnique = pure . onlyUniqueP
456
457-- | A modification of 'getBy', which takes the 'PersistEntity' itself instead
458-- of a 'Unique' record. Returns a record matching /one/ of the unique keys. This
459-- function makes the most sense on entities with a single 'Unique'
460-- constructor.
461--
462-- === __Example usage__
463--
464-- With <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1>,
465--
466-- getBySpjValue :: MonadIO m => ReaderT SqlBackend m (Maybe (Entity User))
467-- getBySpjValue = getByValue $ User "SPJ" 999
468--
469-- > mSpjEnt <- getBySpjValue
470--
471-- The above query when applied on <#dataset-persist-unique-1 dataset-1>, will get this record:
472--
473-- > +----+------+-----+
474-- > | id | name | age |
475-- > +----+------+-----+
476-- > |  1 | SPJ  |  40 |
477-- > +----+------+-----+
478getByValue
479    :: forall record m backend.
480    ( MonadIO m
481    , PersistUniqueRead backend
482    , PersistRecordBackend record backend
483    , AtLeastOneUniqueKey record
484    )
485    => record -> ReaderT backend m (Maybe (Entity record))
486getByValue record = do
487    let uniqs = requireUniquesP record
488    getByValueUniques (NEL.toList uniqs)
489
490-- | Retrieve a record from the database using the given unique keys. It
491-- will attempt to find a matching record for each 'Unique' in the list,
492-- and returns the first one that has a match.
493--
494-- Returns 'Nothing' if you provide an empty list ('[]') or if no value
495-- matches in the database.
496--
497-- @since 2.10.0
498getByValueUniques
499    ::
500    ( MonadIO m
501    , PersistUniqueRead backend
502    , PersistRecordBackend record backend
503    )
504    => [Unique record]
505    -> ReaderT backend m (Maybe (Entity record))
506getByValueUniques uniqs =
507    checkUniques uniqs
508  where
509    checkUniques [] = return Nothing
510    checkUniques (x:xs) = do
511        y <- getBy x
512        case y of
513            Nothing -> checkUniques xs
514            Just z -> return $ Just z
515
516-- | Attempt to replace the record of the given key with the given new record.
517-- First query the unique fields to make sure the replacement maintains
518-- uniqueness constraints.
519--
520-- Return 'Nothing' if the replacement was made.
521-- If uniqueness is violated, return a 'Just' with the 'Unique' violation
522--
523-- @since 1.2.2.0
524replaceUnique
525    :: ( MonadIO m
526       , Eq (Unique record)
527       , PersistRecordBackend record backend
528       , PersistUniqueWrite backend )
529    => Key record -> record -> ReaderT backend m (Maybe (Unique record))
530replaceUnique key datumNew = getJust key >>= replaceOriginal
531  where
532    uniqueKeysNew = persistUniqueKeys datumNew
533    replaceOriginal original = do
534        conflict <- checkUniqueKeys changedKeys
535        case conflict of
536            Nothing -> replace key datumNew >> return Nothing
537            (Just conflictingKey) -> return $ Just conflictingKey
538      where
539        changedKeys = uniqueKeysNew \\ uniqueKeysOriginal
540        uniqueKeysOriginal = persistUniqueKeys original
541
542-- | Check whether there are any conflicts for unique keys with this entity and
543-- existing entities in the database.
544--
545-- Returns 'Nothing' if the entity would be unique, and could thus safely be inserted.
546-- on a conflict returns the conflicting key
547--
548-- === __Example usage__
549--
550-- We use <#schema-persist-unique-1 schema-1> and <#dataset-persist-unique-1 dataset-1> here.
551--
552-- This would be 'Nothing':
553--
554-- > mAlanConst <- checkUnique $ User "Alan" 70
555--
556-- While this would be 'Just' because SPJ already exists:
557--
558-- > mSpjConst <- checkUnique $ User "SPJ" 60
559checkUnique
560    :: (MonadIO m
561       ,PersistRecordBackend record backend
562       ,PersistUniqueRead backend)
563    => record -> ReaderT backend m (Maybe (Unique record))
564checkUnique = checkUniqueKeys . persistUniqueKeys
565
566checkUniqueKeys
567    :: (MonadIO m
568       ,PersistUniqueRead backend
569       ,PersistRecordBackend record backend)
570    => [Unique record] -> ReaderT backend m (Maybe (Unique record))
571checkUniqueKeys [] = return Nothing
572checkUniqueKeys (x:xs) = do
573    y <- getBy x
574    case y of
575        Nothing -> checkUniqueKeys xs
576        Just _ -> return (Just x)
577
578-- | The slow but generic 'putMany' implemetation for any 'PersistUniqueRead'.
579-- * Lookup corresponding entities (if any) for each record using 'getByValue'
580-- * For pre-existing records, issue a 'replace' for each old key and new record
581-- * For new records, issue a bulk 'insertMany_'
582defaultPutMany
583    ::( PersistEntityBackend record ~ BaseBackend backend
584      , PersistEntity record
585      , MonadIO m
586      , PersistStoreWrite backend
587      , PersistUniqueRead backend
588      )
589    => [record]
590    -> ReaderT backend m ()
591defaultPutMany []   = return ()
592defaultPutMany rsD@(e:_)  = do
593    case persistUniqueKeys e of
594        [] -> insertMany_ rsD
595        _ -> go
596  where
597    go = do
598        -- deduplicate the list of records in Haskell by unique key. The
599        -- previous implementation used Data.List.nubBy which is O(n^2)
600        -- complexity.
601        let rs = map snd
602                . Map.toList
603                . Map.fromList
604                . map (\r -> (persistUniqueKeyValues r, r))
605                $ rsD
606
607        -- lookup record(s) by their unique key
608        mEsOld <- mapM (getByValueUniques . persistUniqueKeys) rs
609
610        -- find pre-existing entities and corresponding (incoming) records
611        let merge (Just x) y = Just (x, y)
612            merge _        _ = Nothing
613        let mEsOldAndRs = zipWith merge mEsOld rs
614        let esOldAndRs = catMaybes mEsOldAndRs
615
616        -- determine records to insert
617        let esOld = fmap fst esOldAndRs
618        let rsOld = fmap entityVal esOld
619        let rsNew = deleteFirstsBy ((==) `on` persistUniqueKeyValues) rs rsOld
620
621        -- determine records to update
622        let rsUpd = fmap snd esOldAndRs
623        let ksOld = fmap entityKey esOld
624        let krs   = zip ksOld rsUpd
625
626        -- insert `new` records
627        insertMany_ rsNew
628        -- replace existing records
629        mapM_ (uncurry replace) krs
630
631-- | This function returns a list of 'PersistValue' that correspond to the
632-- 'Unique' keys on that record. This is useful for comparing two @record@s
633-- for equality only on the basis of their 'Unique' keys.
634persistUniqueKeyValues :: PersistEntity record => record -> [PersistValue]
635persistUniqueKeyValues = concatMap persistUniqueToValues . persistUniqueKeys
636