yesodweb / yesodweb/persistent

Improvements to connUpsertSql

Open
#1,160 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Haskell
Stars
486
Forks
306
PR merge metrics
No merged PRs in 30d

Description

connUpsertSql currently has the following type:

connUpsertSql :: Maybe (EntityDef -> NonEmpty (HaskellName,DBName) -> Text -> Text)
  1. I don't see why the HaskellName is passed to this function. The HaskellName of a row seems irrelevant for generating SQL. It appears unused by the Postgres code.
  2. The third argument, Text is the SQL for updating columns. In the case where there are no updates, upsertBy must fallback to defaultUpsertBy. defaultUpsertBy does more SQL queries, and is also prone to race conditions. Instead, I believe connUpsertSql should allow for no updates itself, and in that case do ON CONFLICT DO NOTHING.

Unfortunately, I think fixing this would require a breaking change.

-- | The slow but generic 'upsertBy' implementation for any 'PersistUniqueRead'.
-- * Lookup corresponding entities (if any) 'getBy'.
-- * If the record exists, update using 'updateGet'.
-- * If it does not exist, insert using 'insertEntity'.
-- @since 2.11
defaultUpsertBy
    :: ( PersistEntityBackend record ~ BaseBackend backend
       , PersistEntity record
       , MonadIO m
       , PersistStoreWrite backend
       , PersistUniqueRead backend
       )
    => Unique record   -- ^ uniqueness constraint to find by
    -> record          -- ^ new record to insert
    -> [Update record] -- ^ updates to perform if the record already exists
    -> ReaderT backend m (Entity record) -- ^ the record in the database after the operation
defaultUpsertBy uniqueKey record updates = do
    mrecord <- getBy uniqueKey
    maybe (insertEntity record) (`updateGetEntity` updates) mrecord
  where
    updateGetEntity (Entity k _) upds =
        (Entity k) `liftM` (updateGet k upds)
upsertSql' :: EntityDef -> NonEmpty (HaskellName, DBName) -> Text -> Text
upsertSql' ent uniqs updateVal =
    T.concat
        [ "INSERT INTO "
        , escape (entityDB ent)
        , "("
        , T.intercalate "," fieldNames
        , ") VALUES ("
        , T.intercalate "," placeholders
        , ") ON CONFLICT ("
        , T.intercalate "," $ map (escape . snd) (NEL.toList uniqs)
        , ") DO UPDATE SET "
        , updateVal
        , " WHERE "
        , wher
        , " RETURNING ??"
        ]
  where
    (fieldNames, placeholders) = unzip (Util.mkInsertPlaceholders ent escape)

    wher = T.intercalate " AND " $ map (singleClause . snd) $ NEL.toList uniqs

    singleClause :: DBName -> Text
    singleClause field = escape (entityDB ent) <> "." <> (escape field) <> " =?"

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing connUpsertSql and its uses, then compare the backend behavior with defaultUpsertBy and upsertSql'. Check how the PostgreSQL implementation handles HaskellName and empty update SQL. Done means defining the intended breaking API change and supporting no-update upserts without the generic fallback.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
backend, databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.