Skip to content

fix(mass_model_updater): read tracked fields via attribute access for skip_unchanged#15114

Open
valentijnscholten wants to merge 1 commit into
DefectDojo:devfrom
valentijnscholten:fix/mass-model-updater-getattr
Open

fix(mass_model_updater): read tracked fields via attribute access for skip_unchanged#15114
valentijnscholten wants to merge 1 commit into
DefectDojo:devfrom
valentijnscholten:fix/mass-model-updater-getattr

Conversation

@valentijnscholten

Copy link
Copy Markdown
Member

Summary

  • mass_model_updater(..., skip_unchanged=True) (added in perf(dedupe): skip unchanged rows, VALUES fast-write, prefetch vulnerability_ids #15046) snapshotted each tracked field with model.__dict__.get(f) before calling function, to avoid triggering a deferred-field load when deciding whether a row changed.
  • When a tracked field is deferred — the caller's queryset used .only()/.defer() and omitted it — __dict__ has no entry, so the snapshot reads None. If function recomputes that field to None, the comparison None == None marks the row "unchanged" and the write is silently dropped, leaving a stale persisted value.
  • Root cause is a caller that defers a field it then updates (e.g. a .only(...) that omits the very field passed in fields=). The previous __dict__ approach turned that caller mistake into silent data loss instead of a visible cost.
  • Fix: read the pre-function value of each tracked field with normal attribute access (getattr) instead of __dict__. A deferred tracked field is loaded and compared against its real persisted value, so a real change can never be mistaken for a no-op. The skip-unchanged optimization keeps working for correctly-loaded querysets; a deferred tracked field now degrades to a visible per-row load rather than silent incorrectness. Documented that callers needing the fast path must load the tracked fields.
  • Adds two regression tests to unittests/test_mass_model_updater.py: a deferred tracked field recomputed to None must issue an UPDATE and persist; a deferred tracked field left unchanged must still skip the write.
  • skip_unchanged exists only on dev (not in released versions), so released versions are unaffected.

Alternative to #15112, which guards the no-op check with get_deferred_fields() and always writes deferred rows. This PR instead removes the __dict__ indirection that caused the bug, so the no-op check stays correct (and the optimization keeps working) for deferred-but-loaded rows.

… skip_unchanged

The skip-unchanged no-op check snapshotted each tracked field with
model.__dict__.get(f) to avoid triggering a deferred-field load. But when a
tracked field is deferred (the caller's queryset used .only()/.defer() and
omitted it), __dict__ has no entry and .get() returns None. If function then
recomputes that field to None, None == None marks the row unchanged and the
write is silently dropped, persisting a stale value.

Read the pre-function value with normal attribute access (getattr) instead.
Deferred tracked fields are loaded and compared against their real persisted
value, so a real change can never be mistaken for a no-op. The cost of a
deferred tracked field is now a visible per-row load rather than silent data
loss; callers needing the fast path must load the tracked fields in their
queryset (documented).

Adds regression tests: a deferred tracked field recomputed to None must issue
an UPDATE and persist; one left unchanged must still skip the write.
@Maffooch Maffooch added this to the 3.1.0 milestone Jun 30, 2026
@valentijnscholten valentijnscholten marked this pull request as ready for review June 30, 2026 18:27
@valentijnscholten valentijnscholten marked this pull request as draft June 30, 2026 18:27
@valentijnscholten valentijnscholten marked this pull request as ready for review June 30, 2026 18:45

@mtesauro mtesauro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants