-
Notifications
You must be signed in to change notification settings - Fork 302
adding upsertWhere and upsertManyWhere to persistent-postgresql
#1222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 f61f33f
whoops don't commit that
dmarticus 767ed39
don't export this
dmarticus e49750e
updating docs and changelog
dmarticus 95d4b0c
remove redundant code
dmarticus 8db67bb
omg hls led me astray
dmarticus 85a442d
refactoring the connection, updating the changelog, and running styli…
dmarticus 36c2446
continue on errors
dmarticus c7630a5
put it in the wrong place i don't actually know yaml
dmarticus 7d6af72
jeez maybe this works
dmarticus 27d9edb
maybe it should go here instead
dmarticus a7ed10f
test this on CI
dmarticus 12cecff
testing a different DB
dmarticus 1d5af15
preparing for the PR
dmarticus eed0a8a
yikes idk what i updated these haddocks incorrectly
dmarticus f9c9015
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus 495b609
moving a postgres-specific util to its own module, other code review …
dmarticus 81cf7b0
one last test is failing but I'd love external input on why
dmarticus 78f0861
last changes
dmarticus fc45c3d
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus 4ad926d
hmmmm I'm close i thinkg
dmarticus 82b56c5
generalized the postgresql changes to PersistQuery
dmarticus b9fdc8c
finished implementation, added more tests, removed all todos
dmarticus f5c40e1
haddock update
dmarticus 22cdeb6
finishing the rest of the haddocks
dmarticus bb05bfe
don't need to touch Raw
dmarticus 365b1b4
remodeled the data
dmarticus 078ae4d
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus a75421a
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus 84bcb35
Update persistent-postgresql/Database/Persist/Postgresql.hs
dmarticus ace0569
fix formatting
dmarticus de8f077
latest changes real quick
dmarticus b8676f9
this should all compile but there's no sum type yet. Just taking sto…
dmarticus b894059
remove debug logs
dmarticus 83f3648
add sum type todo
dmarticus fed25a9
this should work
dmarticus 6420304
we got the tests rocking!
dmarticus cc750ec
add an assertion to the mysql test
dmarticus ae9d88a
ooops gotta update the mysql test to make it correct
dmarticus 4d5e689
omg DERP
dmarticus b5b0c78
updating tests and also re-running CI
dmarticus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 #-} | ||
|
dmarticus marked this conversation as resolved.
|
||
| {-# LANGUAGE OverloadedStrings #-} | ||
| {-# LANGUAGE RecordWildCards #-} | ||
| {-# LANGUAGE ScopedTypeVariables #-} | ||
|
|
@@ -21,8 +25,16 @@ module Database.Persist.Postgresql | |
| , createPostgresqlPoolWithConf | ||
| , module Database.Persist.Sql | ||
| , ConnectionString | ||
| , HandleUpdateCollision | ||
|
dmarticus marked this conversation as resolved.
|
||
| , copyField | ||
| , copyUnlessNull | ||
| , copyUnlessEmpty | ||
| , copyUnlessEq | ||
| , excludeNotEqualToOriginal | ||
| , PostgresConf (..) | ||
| , PgInterval (..) | ||
| , upsertWhere | ||
|
dmarticus marked this conversation as resolved.
|
||
| , upsertManyWhere | ||
| , openSimpleConn | ||
| , openSimpleConnWithVersion | ||
| , tableName | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
@@ -397,7 +410,6 @@ insertSql' ent vals = | |
| ] | ||
| ] | ||
|
|
||
|
|
||
| upsertSql' :: EntityDef -> NonEmpty (FieldNameHS, FieldNameDB) -> Text -> Text | ||
| upsertSql' ent uniqs updateVal = | ||
| T.concat | ||
|
|
@@ -1496,6 +1508,7 @@ escapeE = escapeWith escape | |
| escapeF :: FieldNameDB -> Text | ||
| escapeF = escapeWith escape | ||
|
|
||
|
|
||
| escape :: Text -> Text | ||
| escape s = | ||
| T.pack $ '"' : go (T.unpack s) ++ "\"" | ||
|
|
@@ -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 | ||
|
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: | ||
|
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] | ||
|
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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @parsonsmatt does this TODO say enough/make sense?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
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 | ||
|
dmarticus marked this conversation as resolved.
|
||
| :: (PersistEntity record, PersistEntityBackend record ~ SqlBackend) | ||
|
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 | ||
|
dmarticus marked this conversation as resolved.
|
||
| ] | ||
|
|
||
| putManySql' :: [Text] -> [FieldDef] -> EntityDef -> Int -> Text | ||
| putManySql' conflictColumns (filter isFieldNotGenerated -> fields) ent n = q | ||
| where | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.