Opened 46 minutes ago
#37436 assigned Bug
FETCH_PEERS can overwrite deferred-field changes and raise KeyError for deleted peers
| Reported by: | Patel Namraa | Owned by: | Patel Namraa |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 6.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Patel Namraa | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
When using FETCH_PEERS with deferred fields, DeferredAttribute.fetch_many() does not correctly handle peer instances whose state has changed since the queryset was evaluated.
There are two related problems.
- Locally assigned values can be overwritten
Consider two objects loaded with a deferred field using FETCH_PEERS:
class Author(models.Model):
name = models.CharField(max_length=100)
authors = list(
Author.objects.defer("name").fetch_mode(FETCH_PEERS)
)
authors[0].name = "locally changed"
# Fetch the deferred field for another peer.
_ = authors[1].name
Fetching authors[1].name causes fetch_many() to populate the field on all peers. This also replaces authors[0].name with the value currently stored in the database.
The locally assigned value is therefore silently lost.
This does not happen with FETCH_ONE, where the locally assigned value is preserved.
- A deleted peer can cause KeyError
A second problem occurs when one of the peer rows is deleted after the queryset has been evaluated.
fetch_many() uses in_bulk() to retrieve the deferred values and then indexes the returned dictionary for every peer:
value_by_pk = (
self.field.model._base_manager.using(db)
.values_list(attname, flat=True)
.in_bulk({instance.pk for instance in instances})
)
for instance in instances:
setattr(instance, attname, value_by_pk[instance.pk])
in_bulk() does not include primary keys for rows that no longer exist. Consequently, if one peer has been deleted, accessing the deferred field on another existing peer can raise KeyError.
The missing peer should not cause an unrelated peer's deferred-field access to fail.
If the instance being accessed itself has been deleted, its behavior should remain consistent with FETCH_ONE rather than producing a KeyError caused by another peer.
Expected behavior:
FETCH_PEERS should not overwrite a peer's field if that value has already been assigned locally.
A peer whose database row no longer exists should not cause deferred-field access on other existing peers to fail.
The behavior for the instance whose own row has been deleted should remain consistent with FETCH_ONE.
I reproduced both behaviors against Django 6.1. The corresponding FETCH_ONE cases behave correctly.
This appears to be a missing state check in DeferredAttribute.fetch_many(): it unconditionally assigns the fetched value to every peer and assumes that every requested primary key is present in the result returned by in_bulk().