yesodweb / yesodweb/persistent

connUpsert should perform a no-op update with no updates specified

Open
#1,257 5 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

The current implementation of upsert falls back to the racey defaultUpsertBy if no updates are specified.

instance PersistUniqueWrite SqlBackend where
    upsertBy uniqueKey record updates = do
      conn <- ask
      let refCol n = T.concat [connEscapeTableName conn t, ".", n]
      let mkUpdateText = mkUpdateText' (connEscapeFieldName conn) refCol
      case connUpsertSql conn of
        Just upsertSql -> case updates of
                            [] -> defaultUpsertBy uniqueKey record updates
                            _:_ -> do
                                let upds = T.intercalate "," $ map mkUpdateText updates
                                    sql = upsertSql t (NEL.fromList $ persistUniqueToFieldNames uniqueKey) upds
                                    vals = map toPersistValue (toPersistFields record)
                                        ++ map updatePersistValue updates
                                        ++ unqs uniqueKey

                                x <- rawSql sql vals
                                return $ head x
        Nothing -> defaultUpsertBy uniqueKey record updates
        where
          t = entityDef $ Just record
          unqs uniqueKey' = concatMap persistUniqueToValues [uniqueKey']

In part, this is caused by the connUpsertSql function using a Text for the upds, instead of the list of updates itself.

Postgres supports this with ON CONFLICT DO NOTHING.

MySQL supports this horrifyingly with UPDATE id=id which apparently doesn't trigger any triggers (source).

Transparently, we can fix this by checking if the update text is "" in the backend's upsertSql functions. This is a pretty nasty hack, but we could release it as a patch version bump! no, no, absolutely not, this is absolutely a breaking change in the contract of the function, it would need to be a major version bump, even though the type isn't changing

It's probably better to have the backends render the [Update rec] themselves, but that complicates the signature of connUpsertSql a bit:

connUpsertSql :: Maybe (forall e. PersistEntity e => EntityDef -> ... [Update e] -> Text)

The EntityDef becomes redundant, since we can call entityDef (Proxy @e) and summon the canonical one.

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 with the PersistUniqueWrite upsertBy implementation and the connUpsertSql type, then inspect how the PostgreSQL and MySQL backends render upserts. Decide how an empty update list should be represented without relying on the race-prone defaultUpsertBy path. Done means empty updates perform a backend-supported no-op while preserving the intended contract for non-empty updates.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell, mysql, postgresql
Domain
backend-api-design, databases
Issue type
Bug
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.