yesodweb / yesodweb/persistent
connUpsert should perform a no-op update with no updates specified
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 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"" in the backend's upsertSql functions. This is a pretty nasty hack, but we could release it as a patch version bump!
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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