fix(models): apply defaults after concurrent inserts - #2276
Conversation
Retry the existing transactional update path when a concurrent caller wins creation. Re-read the row instead of saving the unlocked, possibly stale conflict lookup result. Use an explicit None check so falsy model instances still take the update path. Keep get_or_create and the existing creation transaction boundary unchanged. Cover concurrent defaults, stale/deleted rows, transaction rollback, falsy models, and unrelated integrity errors. Fixes tortoise#2060
SQL Server omits inline uniqueness for nullable fields, so the existing UniqueName fixture never entered conflict recovery there. Use a dedicated model with a non-null unique name instead of skipping the concurrency regressions on SQL Server.
|
Thanks for the update — the fix logic is sound and the test coverage remains excellent. I have one design concern about the retry loop that I'd like to discuss before merging. Concern: unbounded The new implementation wraps the entire "locked read → update / create" flow in However, Suggested alternatives:
Everything else looks good:
I'm happy to approve once the retry loop is bounded (or justified with a comment). If you'd rather keep |
Description
Retry the existing transactional update path when another caller wins the initial insert. The losing
update_or_create()call now applies its own defaults to a freshly read row rather than returning the other caller's values unchanged.The creation transaction boundary and
get_or_create()behavior stay unchanged. An explicitNonecheck also keeps falsy model instances on the update path.Motivation and Context
Closes #2060.
Re-reading the row matters: the unlocked conflict lookup may already be stale or its row may have been deleted. The retry uses the normal update path and its existing backend locking behavior, without adding backend-specific upsert SQL or changing the public API.
How Has This Been Tested?
45676b9: Python 3.10–3.14 across SQLite, PostgreSQL (asyncpg/psycopg), MySQL (InnoDB/MyISAM and asyncmy), and SQL Server, plus checks and coverage.get_or_create()semantics. Final fixtures yield six failures and three passing controls against the original implementation.Checklist: