Skip to content

fix(mass_model_updater): never skip a deferred tracked field as "unchanged" [sc-13375]#15112

Closed
Maffooch wants to merge 1 commit into
devfrom
fix/mass-model-updater-deferred-skip-unchanged
Closed

fix(mass_model_updater): never skip a deferred tracked field as "unchanged" [sc-13375]#15112
Maffooch wants to merge 1 commit into
devfrom
fix/mass-model-updater-deferred-skip-unchanged

Conversation

@Maffooch

Copy link
Copy Markdown
Contributor

Problem

mass_model_updater(..., skip_unchanged=True) (added in #15046) decides whether to write each row by snapshotting its tracked fields before calling function, reading model.__dict__.get(field) to avoid triggering a deferred-field query.

But 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 then recomputes that field to None, the comparison None == None marks the row "unchanged" and the write is silently dropped, leaving the stale persisted value in the DB.

This bit DefectDojo Pro's risk-mode SLA recalculation: its queryset deferred sla_expiration_date while updating it, so clearing a finding's SLA to None (enforcement disabled) never persisted — the finding kept its old SLA. (Companion fix in dojo-pro adds the field to its .only(...); this PR removes the footgun at the source.)

Fix

Guard the no-op check with model.get_deferred_fields(): if any tracked field is deferred, skip the comparison and always write that row. No extra query; non-deferred rows keep the fast skip-unchanged path.

Tests

Adds two regression tests to unittests/test_mass_model_updater.py:

  • test_writes_deferred_field_recomputed_to_none — a deferred field recomputed to None must issue an UPDATE and persist (fails without this fix: 0 UPDATEs).
  • test_writes_deferred_field_recomputed_to_value — deferred field updated to a value persists.

Full TestMassModelUpdater suite (11 tests) passes; reverting the guard makes the None test fail with 0 not greater than 0.

Scope

skip_unchanged exists only on dev (not in 3.0.200 / bugfix / master), so released versions are unaffected.

…anged"

skip_unchanged compares each row's tracked fields before/after `function` by
reading model.__dict__.get(field), to avoid a deferred-field query. But when a
tracked field is deferred (the queryset used .only()/.defer() and omitted it),
__dict__ has no entry and reads as None. A real recompute to None then looks
unchanged (None == None) and the write is silently dropped, leaving the stale
persisted value.

Guard the no-op check with model.get_deferred_fields(): if any tracked field is
deferred, skip the comparison and always write that row. Add regression tests
covering a deferred field recomputed to None (the dropped-write case) and to a
new value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Maffooch Maffooch requested a review from mtesauro as a code owner June 30, 2026 05:30
@Maffooch Maffooch changed the title fix(mass_model_updater): never skip a deferred tracked field as "unchanged" fix(mass_model_updater): never skip a deferred tracked field as "unchanged" [sc-13375] Jun 30, 2026
@Maffooch Maffooch added this to the 3.1.0 milestone Jun 30, 2026

@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

@Maffooch

Copy link
Copy Markdown
Contributor Author

Closing in favor or #15114

@Maffooch Maffooch closed this Jun 30, 2026
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.

3 participants