Skip to content

fix: Model.update() bypasses __fillable__ guard to prevent silent no-ops#68

Merged
bedus-creation merged 4 commits into
mainfrom
fix/model-update-bypasses-fillable-guard
May 28, 2026
Merged

fix: Model.update() bypasses __fillable__ guard to prevent silent no-ops#68
bedus-creation merged 4 commits into
mainfrom
fix/model-update-bypasses-fillable-guard

Conversation

@bedus-creation

@bedus-creation bedus-creation commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Model.update() previously delegated to fill(), which silently skips attributes not in __fillable__ (built from class annotations). For models with unannotated fields, every attribute was discarded, is_dirty() returned False, and save() returned True without executing any SQL — making the call appear to succeed while leaving the database unchanged.
  • Replaced fill() with a direct set_attribute() loop so update() applies every supplied attribute unconditionally. The "only update changed fields" behaviour is preserved — is_dirty() still compares against _original and excludes unchanged values from the UPDATE.
  • Added three regression tests: one confirming _exists is True for records from all(), one end-to-end update via all(), and one specifically for the unannotated-model (empty __fillable__) failure path.

Closes #67

Test plan

  • test_records_from_all_have_exists_true_exists is True for hydrated records
  • test_update_persists_change_on_record_from_allupdate() on annotated model via all() persists to DB
  • test_update_bypasses_fillable_guardupdate() on unannotated model (empty __fillable__) persists to DB (was failing before fix)
  • All existing TestModelUpdate and SqliteTestQueryBuilderModel update tests still pass

🤖 Generated with Claude Code

bedus-creation and others added 4 commits May 26, 2026 21:39
Model.update() previously delegated to fill(), which silently skips any
attribute not listed in __fillable__ (derived from class annotations).
For models with missing or unannotated fields every attribute was
discarded, is_dirty() returned False, and save() returned True without
ever executing SQL — making the call appear to succeed while leaving the
database unchanged.

Fix: replace fill() with a direct set_attribute() loop so update()
applies every supplied attribute unconditionally. The "only update
changed fields" behaviour is preserved because is_dirty() still compares
against _original and excludes unchanged values from the UPDATE statement.

Closes #67

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
__init_subclass__ previously built __fillable__ from cls.__annotations__
which only contains annotations declared directly in the class body.
Fields inherited from an intermediate base model were silently absent,
causing fill() / update() to skip them with no error.

Fix: walk the MRO with klass.__dict__.get('__annotations__', {}),
collecting annotations from every user-defined ancestor while stopping
at Model and its own bases (so framework internals like db_manager,
created_at, updated_at are excluded).

Also reverts the earlier incorrect update() change — fill(attributes).save()
is the right implementation; the problem was always in __fillable__ not
being populated correctly.

Closes #67

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…herited __fillable__

- update() now correctly routes through fill() so the __fillable__ guard
  is enforced (reverts the earlier bypass that called set_attribute() directly)
- test_update_iterating_all_persists_changes now uses a BaseUser / ConcreteUser
  pair where fields are declared on the parent — this is the exact shape that
  fails without the MRO fix: ConcreteUser.__fillable__ is empty so fill()
  silently discards every attribute and no SQL runs
- Removes stale inline comment from __init_subclass__ (logic is self-evident)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bedus-creation

Copy link
Copy Markdown
Contributor Author

close #67

@bedus-creation bedus-creation merged commit 2148e1a into main May 28, 2026
3 checks passed
@bedus-creation bedus-creation deleted the fix/model-update-bypasses-fillable-guard branch May 28, 2026 04:03
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.

Model instance update() silently fails when _exists is False on records fetched via all()

1 participant