Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
4e4903d
initial implementation of the upsertWhere
dmarticus Mar 31, 2021
f61f33f
whoops don't commit that
dmarticus Mar 31, 2021
767ed39
don't export this
dmarticus Mar 31, 2021
e49750e
updating docs and changelog
dmarticus Mar 31, 2021
95d4b0c
remove redundant code
dmarticus Mar 31, 2021
8db67bb
omg hls led me astray
dmarticus Mar 31, 2021
85a442d
refactoring the connection, updating the changelog, and running styli…
dmarticus Apr 1, 2021
36c2446
continue on errors
dmarticus Apr 1, 2021
c7630a5
put it in the wrong place i don't actually know yaml
dmarticus Apr 1, 2021
7d6af72
jeez maybe this works
dmarticus Apr 1, 2021
27d9edb
maybe it should go here instead
dmarticus Apr 1, 2021
a7ed10f
test this on CI
dmarticus Apr 1, 2021
12cecff
testing a different DB
dmarticus Apr 1, 2021
1d5af15
preparing for the PR
dmarticus Apr 5, 2021
eed0a8a
yikes idk what i updated these haddocks incorrectly
dmarticus Apr 5, 2021
f9c9015
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus Apr 5, 2021
495b609
moving a postgres-specific util to its own module, other code review …
dmarticus Apr 5, 2021
81cf7b0
one last test is failing but I'd love external input on why
dmarticus Apr 5, 2021
78f0861
last changes
dmarticus Apr 5, 2021
fc45c3d
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus Apr 5, 2021
4ad926d
hmmmm I'm close i thinkg
dmarticus Apr 5, 2021
82b56c5
generalized the postgresql changes to PersistQuery
dmarticus Apr 6, 2021
b9fdc8c
finished implementation, added more tests, removed all todos
dmarticus Apr 6, 2021
f5c40e1
haddock update
dmarticus Apr 6, 2021
22cdeb6
finishing the rest of the haddocks
dmarticus Apr 6, 2021
bb05bfe
don't need to touch Raw
dmarticus Apr 6, 2021
365b1b4
remodeled the data
dmarticus Apr 6, 2021
078ae4d
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus Apr 6, 2021
a75421a
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus Apr 6, 2021
84bcb35
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus Apr 7, 2021
ace0569
fix formatting
dmarticus Apr 7, 2021
de8f077
latest changes real quick
dmarticus Apr 7, 2021
b8676f9
this should all compile but there's no sum type yet. Just taking sto…
dmarticus Apr 7, 2021
b894059
remove debug logs
dmarticus Apr 7, 2021
83f3648
add sum type todo
dmarticus Apr 7, 2021
fed25a9
this should work
dmarticus Apr 7, 2021
6420304
we got the tests rocking!
dmarticus Apr 7, 2021
cc750ec
add an assertion to the mysql test
dmarticus Apr 7, 2021
ae9d88a
ooops gotta update the mysql test to make it correct
dmarticus Apr 7, 2021
4d5e689
omg DERP
dmarticus Apr 7, 2021
b5b0c78
updating tests and also re-running CI
dmarticus Apr 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/haskell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ jobs:
- run: cabal v2-build all --disable-optimization $CONFIG
- run: cabal v2-test all --disable-optimization $CONFIG
- run: cabal v2-haddock all $CONFIG
continue-on-error: true
Comment thread
dmarticus marked this conversation as resolved.
- run: cabal v2-sdist all
2 changes: 1 addition & 1 deletion persistent-mysql/Database/Persist/MySQL.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ copyField = CopyField
-- []
-- @
--
-- Once we run that code on the datahase, the new data set looks like this:
-- Once we run that code on the database, the new data set looks like this:
Comment thread
dmarticus marked this conversation as resolved.
--
-- > items:
-- > +------+-------------+-------+----------+
Expand Down
3 changes: 3 additions & 0 deletions persistent-mysql/test/InsertDuplicateUpdate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,15 @@ specs = describe "DuplicateKeyUpdate" $ do
dbItems <- map entityVal <$> selectList [] []
sort dbItems @== sort (newItem : items)
it "updates existing records" $ db $ do
let postUpdate = map (\i -> i { itemQuantity = fmap (+1) (itemQuantity i) }) items
deleteWhere ([] :: [Filter Item])
insertMany_ items
insertManyOnDuplicateKeyUpdate
items
[]
[ItemQuantity +=. Just 1]
dbItems <- sort . fmap entityVal <$> selectList [] []
dbItems @== sort postUpdate
Comment thread
dmarticus marked this conversation as resolved.
it "only copies passing values" $ db $ do
deleteWhere ([] :: [Filter Item])
insertMany_ items
Expand Down
4 changes: 4 additions & 0 deletions persistent-postgresql/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog for persistent-postgresql

## 2.12.1.0

* Added `upsertWhere` and `upsertManyWhere` to `persistent-postgresql`. [#1222](https://github.com/yesodweb/persistent/pull/1222).

## 2.12.0.0

* Decomposed `HaskellName` into `ConstraintNameHS`, `EntityNameHS`, `FieldNameHS`. Decomposed `DBName` into `ConstraintNameDB`, `EntityNameDB`, `FieldNameDB` respectively. [#1174](https://github.com/yesodweb/persistent/pull/1174)
Expand Down
227 changes: 224 additions & 3 deletions persistent-postgresql/Database/Persist/Postgresql.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
Comment thread
dmarticus marked this conversation as resolved.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
Expand All @@ -21,8 +25,16 @@ module Database.Persist.Postgresql
, createPostgresqlPoolWithConf
, module Database.Persist.Sql
, ConnectionString
, HandleUpdateCollision
Comment thread
dmarticus marked this conversation as resolved.
, copyField
, copyUnlessNull
, copyUnlessEmpty
, copyUnlessEq
, excludeNotEqualToOriginal
, PostgresConf (..)
, PgInterval (..)
, upsertWhere
Comment thread
dmarticus marked this conversation as resolved.
, upsertManyWhere
, openSimpleConn
, openSimpleConnWithVersion
, tableName
Expand Down Expand Up @@ -50,7 +62,7 @@ import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Unlift (MonadIO (..), MonadUnliftIO)
import Control.Monad.Logger (MonadLoggerIO, runNoLoggingT)
import Control.Monad.Trans.Reader (runReaderT)
import Control.Monad.Trans.Reader (ReaderT(..), runReaderT, asks)
import Control.Monad.Trans.Writer (WriterT(..), runWriterT)

import qualified Blaze.ByteString.Builder.Char8 as BBB
Expand All @@ -66,7 +78,7 @@ import qualified Data.ByteString.Char8 as B8
import Data.Char (ord)
import Data.Conduit
import qualified Data.Conduit.List as CL
import Data.Data
import Data.Data ( Data, Typeable )
import Data.Either (partitionEithers)
import Data.Fixed (Fixed(..), Pico)
import Data.Function (on)
Expand All @@ -80,6 +92,7 @@ import qualified Data.List.NonEmpty as NEL
import qualified Data.Map as Map
import Data.Maybe
import Data.Monoid ((<>))
import qualified Data.Monoid as Monoid
import Data.Pool (Pool)
import Data.String.Conversions.Monomorphic (toStrictByteString)
import Data.Text (Text)
Expand Down Expand Up @@ -397,7 +410,6 @@ insertSql' ent vals =
]
]


upsertSql' :: EntityDef -> NonEmpty (FieldNameHS, FieldNameDB) -> Text -> Text
upsertSql' ent uniqs updateVal =
T.concat
Expand Down Expand Up @@ -1496,6 +1508,7 @@ escapeE = escapeWith escape
escapeF :: FieldNameDB -> Text
escapeF = escapeWith escape


escape :: Text -> Text
escape s =
T.pack $ '"' : go (T.unpack s) ++ "\""
Expand Down Expand Up @@ -1738,6 +1751,214 @@ repsertManySql ent n = putManySql' conflictColumns fields ent n
fields = keyAndEntityFields ent
conflictColumns = escapeF . fieldDB <$> entityKeyFields ent

-- | This type is used to determine how to update rows using Postgres'
-- @INSERT ... ON CONFLICT KEY UPDATE@ functionality, exposed via
-- 'upsertWhere' and 'upsertManyWhere' in this library.
--
-- @since 2.12.1.0
data HandleUpdateCollision record where
Comment thread
dmarticus marked this conversation as resolved.
-- | Copy the field directly from the record.
CopyField :: EntityField record typ -> HandleUpdateCollision record
-- | Only copy the field if it is not equal to the provided value.
CopyUnlessEq :: PersistField typ => EntityField record typ -> typ -> HandleUpdateCollision record

-- | Copy the field into the database only if the value in the
-- corresponding record is non-@NULL@.
--
-- @since 2.12.1.0
copyUnlessNull :: PersistField typ => EntityField record (Maybe typ) -> HandleUpdateCollision record
copyUnlessNull field = CopyUnlessEq field Nothing

-- | Copy the field into the database only if the value in the
-- corresponding record is non-empty, where "empty" means the Monoid
-- definition for 'mempty'. Useful for 'Text', 'String', 'ByteString', etc.
--
-- The resulting 'HandleUpdateCollision' type is useful for the
-- 'upsertManyWhere' function.
--
-- @since 2.12.1.0
copyUnlessEmpty :: (Monoid.Monoid typ, PersistField typ) => EntityField record typ -> HandleUpdateCollision record
copyUnlessEmpty field = CopyUnlessEq field Monoid.mempty

-- | Copy the field into the database only if the field is not equal to the
-- provided value. This is useful to avoid copying weird nullary data into
-- the database.
--
-- The resulting 'HandleUpdateCollision' type is useful for the
-- 'upsertMany' function.
--
-- @since 2.12.1.0
copyUnlessEq :: PersistField typ => EntityField record typ -> typ -> HandleUpdateCollision record
copyUnlessEq = CopyUnlessEq

-- | Copy the field directly from the record.
--
-- @since 2.12.1.0
copyField :: PersistField typ => EntityField record typ -> HandleUpdateCollision record
copyField = CopyField

-- | Postgres specific 'upsertWhere'. This method does the following:
Comment thread
dmarticus marked this conversation as resolved.
-- 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:
--
-- @
-- upsertWhere record [recordField =. newValue] [recordField /= newValue]
Comment thread
dmarticus marked this conversation as resolved.
-- @
--
-- 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
upsertWhere
:: ( backend ~ PersistEntityBackend record
, PersistEntity record
, PersistEntityBackend record ~ SqlBackend
, MonadIO m
, PersistStore backend
, BackendCompatible SqlBackend backend
)
=> record
-> [Update record]
-> [Filter record]
-> ReaderT backend m ()
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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parsonsmatt does this TODO say enough/make sense?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah!

-- `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:
Comment thread
dmarticus marked this conversation as resolved.
-- 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]
--
-- 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 ()
upsertManyWhere [] _ _ _ = return ()
upsertManyWhere records fieldValues updates filters = do
conn <- asks projectBackend
uncurry rawExecute $
mkBulkUpsertQuery records conn fieldValues updates filters

-- | 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
Comment thread
dmarticus marked this conversation as resolved.
:: (PersistEntity record, PersistEntityBackend record ~ SqlBackend)
Comment thread
dmarticus marked this conversation as resolved.
=> [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
-> (Text, [PersistValue])
mkBulkUpsertQuery records conn fieldValues updates filters =
(q, recordValues <> updsValues <> copyUnlessValues <> whereVals)
where
mfieldDef x = case x of
CopyField rec -> Right (fieldDbToText (persistFieldDef rec))
CopyUnlessEq rec val -> Left (fieldDbToText (persistFieldDef rec), toPersistValue val)
(fieldsToMaybeCopy, updateFieldNames) = partitionEithers $ map mfieldDef fieldValues
fieldDbToText = escapeF . fieldDB
entityDef' = entityDef records
conflictColumns = escapeF . fieldDB <$> entityKeyFields entityDef'
firstField = case entityFieldNames of
[] -> error "The entity you're trying to insert does not have any fields."
(field:_) -> field
entityFieldNames = map fieldDbToText (entityFields entityDef')
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
mkCondFieldSet 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
q = T.concat
[ "INSERT INTO "
, nameOfTable
, Util.parenWrapped . Util.commaSeparated $ entityFieldNames
, " VALUES "
, recordPlaceholders
, " ON CONFLICT "
, Util.parenWrapped $ Util.commaSeparated $ conflictColumns
, " DO UPDATE SET "
, updateText
, wher
Comment thread
dmarticus marked this conversation as resolved.
]

putManySql' :: [Text] -> [FieldDef] -> EntityDef -> Int -> Text
putManySql' conflictColumns (filter isFieldNotGenerated -> fields) ent n = q
where
Expand Down
2 changes: 1 addition & 1 deletion persistent-postgresql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ $ createdb test

The tests do not pass a test and expect to connect with the `postgres` user.
Ensure that peer authentication is allowed for this.
An easy/insecure way to do this is to set the `METHOD` to `trust` for all the login methods in `/etc/postgresql/XX/main/pg_hba.coinf`.
An easy/insecure way to do this is to set the `METHOD` to `trust` for all the login methods in `/etc/postgresql/XX/main/pg_hba.conf`.
Comment thread
parsonsmatt marked this conversation as resolved.
(TODO: make this better?)
1 change: 1 addition & 0 deletions persistent-postgresql/persistent-postgresql.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ test-suite test
JSONTest
CustomConstraintTest
PgIntervalTest
UpsertWhere
ghc-options: -Wall

build-depends: base >= 4.9 && < 5
Expand Down
2 changes: 1 addition & 1 deletion persistent-postgresql/test/PgInit.hs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import Test.HUnit ((@?=),(@=?), Assertion, assertFailure, assertBool)
import Test.QuickCheck

import Control.Monad (unless, (>=>))
import Control.Monad.IO.Class

Comment thread
parsonsmatt marked this conversation as resolved.
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Logger
import Control.Monad.Trans.Resource (ResourceT, runResourceT)
Expand Down
Loading