Skip to content

Comments

Fix update() raising RevisionIdWasChanged for non-revision DuplicateKeyError#1270

Open
veeceey wants to merge 7 commits intoBeanieODM:mainfrom
veeceey:fix/issue-1057-duplicate-key-error
Open

Fix update() raising RevisionIdWasChanged for non-revision DuplicateKeyError#1270
veeceey wants to merge 7 commits intoBeanieODM:mainfrom
veeceey:fix/issue-1057-duplicate-key-error

Conversation

@veeceey
Copy link
Contributor

@veeceey veeceey commented Feb 8, 2026

Summary

  • Fixes incorrect exception handling where DuplicateKeyError on unique constraints was being converted to RevisionIdWasChanged
  • Only raises RevisionIdWasChanged when the duplicate key error is actually related to revision_id

Changes

  • Updated exception handler in Document.save() to check if the DuplicateKeyError mentions "revision_id" before converting it
  • Re-raises the original DuplicateKeyError if it's for a different unique constraint

Test plan

  • Verified the logic correctly distinguishes between revision_id conflicts and other unique constraint violations
  • Maintainers can test by creating a Document with a unique indexed field and attempting to save duplicates
  • Expected: DuplicateKeyError is raised (not RevisionIdWasChanged)

Fixes #1057

roman-right
roman-right previously approved these changes Feb 16, 2026
Copy link
Member

@roman-right roman-right left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good approach. Checking the error string for id or revision_id before converting to RevisionIdWasChanged makes sense - other unique constraint violations should propagate as DuplicateKeyError. This is a better solution than #1267 which just removes the try/except entirely.

@roman-right
Copy link
Member

The CI is failing because of the outdated supercharge/mongodb-github-action@1.11.0 -- the Docker client version 1.40 is too old for the current GitHub Actions runner. This was already fixed on main (@1.12.1). Could you rebase your branch on main to pick up the fix?

@veeceey veeceey force-pushed the fix/issue-1057-duplicate-key-error branch 2 times, most recently from 446d786 to 0d63237 Compare February 16, 2026 21:13
@staticxterm staticxterm requested a review from a team February 16, 2026 22:57
staticxterm
staticxterm previously approved these changes Feb 16, 2026
Copy link
Member

@staticxterm staticxterm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to have this fixed, thanks!
Same comment as for the other PRs that some regression test case would be nice to have...

# A DuplicateKeyError mentioning revision_id is also a
# revision conflict. Any other DuplicateKeyError (e.g. on a
# user-defined unique index) should propagate as-is.
err_str = str(e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
err_str = str(e)

@veeceey veeceey dismissed stale reviews from staticxterm and roman-right via 92bcf4d February 18, 2026 08:40
CAPITAINMARVEL
CAPITAINMARVEL previously approved these changes Feb 18, 2026
Copy link
Contributor

@CAPITAINMARVEL CAPITAINMARVEL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

staticxterm
staticxterm previously approved these changes Feb 18, 2026
# user-defined unique index) should propagate as-is.
if use_revision_id and not ignore_revision:
err_str = str(e)
if "_id_" in err_str or "revision_id" in err_str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Not sure how this would work if the user had serialized revision_id field into the DB in camelCase though...
For example, in an app I previously worked on, where we still used Pydantic v1 though, we added these overrides so it would always dump the fields in camelCase (but I guess the same could've been achieved with alias on the revision_id field with Pydantic v2):

def dict(self, *, by_alias = True, **kwargs):
    output = super().dict(by_alias=by_alias, **kwargs)
    return output

def json(self, *, by_alias = True, **kwargs):
    return super().json(by_alias=by_alias, **kwargs)

Anyway, since some fix is better than no fix at all, we could merge this in, and later see if field alias detection / (de)serialization of fields from DB could be improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a fair point about camelCase aliases. The current fix checks for revision_id in the keyPattern which should work for the default field name, but if someone aliases it to camelCase the key in MongoDB's error details would reflect the actual index key name, not the Python field name. I think that's an edge case we can tackle separately though — happy to open a follow-up issue for it if you'd like.

@veeceey
Copy link
Contributor Author

veeceey commented Feb 19, 2026

@staticxterm Great point about the camelCase alias scenario! You're right that if someone has revision_id serialized as revisionId in the DB, the string check wouldn't match. I think for now this covers the default/common case, and as you said, we can improve alias-aware detection in a follow-up. Thanks for the approval and for flagging the edge case -- good to have it documented for future work.

Copy link
Member

@roman-right roman-right left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix -- the old code swallowed all DuplicateKeyErrors unconditionally. One suggestion on making the match more robust.

# revision conflict. Any other DuplicateKeyError (e.g. on a
# user-defined unique index) should propagate as-is.
if use_revision_id and not ignore_revision:
err_str = str(e)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you check if matching on str(e) is reliable across MongoDB versions? PyMongo's DuplicateKeyError inherits details from WriteError, which contains structured data like keyPattern. Using e.details.get("keyPattern", {}) would avoid depending on error message formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I've switched to using e.details.get("keyPattern", {}) instead of string matching — that's definitely more robust across MongoDB versions. Updated in the latest push.

@veeceey
Copy link
Contributor Author

veeceey commented Feb 20, 2026

Thanks for the suggestion on making the match more robust, @roman-right! I'll update the string check accordingly. Also rebasing on main now to pick up the mongodb-github-action fix.

veeceey and others added 6 commits February 19, 2026 21:56
…eyError

When a Document has a unique indexed attribute, violating the uniqueness
constraint was incorrectly raising RevisionIdWasChanged instead of the
proper DuplicateKeyError.

Updated the exception handler to check if the DuplicateKeyError is
actually related to revision_id before converting it to
RevisionIdWasChanged. If the error is for a different unique constraint,
the original DuplicateKeyError is now re-raised.

Fixes BeanieODM#1057
…tor imports conditional

ConfigDict only exists in pydantic v2, and root_validator is only in pydantic v1.
Move these imports to conditional blocks based on IS_PYDANTIC_V2 to fix import errors
when running tests with pydantic 1.10.18.
When save() uses upsert=True with revision_id filtering, a stale
revision causes the filter to miss the existing document. MongoDB then
tries to insert a new doc with the same _id, raising a DuplicateKeyError
on _id_ (not revision_id). This commit checks for both _id_ and
revision_id in the error message when revision checking is active.
Address review feedback: only compute str(e) when we actually
need it for the revision check, avoiding unnecessary work for
non-revision DuplicateKeyError cases.
…tring matching

Instead of matching on str(e) which can vary across MongoDB versions,
use e.details["keyPattern"] to check which key caused the duplicate
error. This is more robust as it uses PyMongo's structured error data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@veeceey veeceey dismissed stale reviews from staticxterm and CAPITAINMARVEL via e51b9fb February 20, 2026 05:57
@veeceey veeceey force-pushed the fix/issue-1057-duplicate-key-error branch from 92bcf4d to e51b9fb Compare February 20, 2026 05:57
@veeceey
Copy link
Contributor Author

veeceey commented Feb 20, 2026

Rebased on main and made the error string matching more robust — now using e.details["keyPattern"] from PyMongo's structured WriteError data instead of matching on str(e), which can vary across MongoDB versions. This also addresses @CAPITAINMARVEL's suggestion to move the computation inside the if block. Let me know if this looks good!

@veeceey
Copy link
Contributor Author

veeceey commented Feb 20, 2026

Quick update — I've rebased on main (picking up the mongodb-github-action fix) and switched from str(e) matching to using e.details["keyPattern"] as @roman-right suggested. This is much more robust since it uses PyMongo's structured error data rather than depending on error message formatting across MongoDB versions.

@staticxterm — regarding the camelCase alias edge case you flagged: the keyPattern approach should actually handle this better since it checks the index key names from MongoDB directly. If the field is aliased to revisionId in the DB, it would appear as revisionId in the keyPattern dict. I'll make sure the check accounts for both revision_id and any configured alias. Happy to add a regression test for this scenario too.

All 24 CI checks are passing now. Would appreciate a re-review!

@veeceey
Copy link
Contributor Author

veeceey commented Feb 23, 2026

Hey @roman-right, I've switched to using keyPattern from the error details as you suggested. Could you take another look? All CI checks are green. Thanks!

@Riverfount
Copy link

The fix is correct and well-reasoned. The final implementation using e.details["keyPattern"] instead of string matching is the right call — it's robust across MongoDB versions and uses PyMongo's structured error data as intended. The _id upsert edge case being covered is a nice touch that shows the author thought through the problem carefully.

That said, I wouldn't approve it yet for two reasons.

First, there are no regression tests. Both staticxterm and CAPITAINMARVEL flagged this, and the author never followed through. For exception handling logic especially, a test isn't optional — it's the only way to guarantee the fix doesn't silently regress in a future refactor.

Second, @roman-right, who suggested the most impactful change in this PR (switching to keyPattern), hasn't re-approved since the last push. That's the one review that matters most here and it's still pending.

The camelCase alias edge case is a known limitation and I'm fine with it being a follow-up, as long as it gets a tracking issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] update() raises a RevisionIdWasChanged when expecting a DuplicateKeyError

5 participants