Skip to content

Add partial field saves for models - #864

Open
hariomCodes wants to merge 7 commits into
redis:mainfrom
hariomCodes:codex/772-partial-save
Open

Add partial field saves for models#864
hariomCodes wants to merge 7 commits into
redis:mainfrom
hariomCodes:codex/772-partial-save

Conversation

@hariomCodes

@hariomCodes hariomCodes commented Sep 4, 2026

Copy link
Copy Markdown

Summary

  • add save(update_fields=...) support to HashModel and JsonModel
  • preserve unrelated concurrent changes with field-level writes
  • make instance update() use the same partial-write path
  • validate unknown and primary-key fields
  • document the new API and add async/sync regression coverage

Testing

  • 302 hash/JSON model tests passed across async and generated sync APIs
  • ruff check and formatting passed
  • mypy passed
  • bandit passed

Closes #772


Note

Medium Risk
Changes core save/update persistence for all Hash and JSON models; incorrect partial-write semantics could cause lost concurrent updates or unexpected None returns, though behavior is covered by extensive new tests.

Overview
Adds save(update_fields=...) on HashModel and JsonModel so callers can persist only named fields instead of rewriting the whole Redis record. update() and FindQuery.update() now go through a shared _update_with_pipeline path that applies the same partial writes (including nested address__city paths on JSON models).

Partial updates are existence-gated: Lua scripts run HSET or multiple JSON.SET only when the key still exists; otherwise save() returns None instead of creating a stub document. Hash partial saves can clear optional fields by storing explicit None as "", with from_redis aligned to decode those back like HashModel.get().

Validation is tightened: primary key updates are rejected, exclude=True fields cannot be updated, update_fields cannot be combined with nx/xx, and nested update paths are checked via model_fields (including T | None).

Reviewed by Cursor Bugbot for commit f4f0caf. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread aredis_om/model/model.py
Comment thread aredis_om/model/model.py Outdated
Comment thread aredis_om/model/model.py Outdated
Comment thread aredis_om/model/model.py Outdated
Comment thread aredis_om/model/model.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread aredis_om/model/model.py
@hariomCodes

Copy link
Copy Markdown
Author

@abrookins I've pushed f4f0caf to address the search-reader part of the latest Bugbot finding. from_redis() now applies the same empty-string-to-None conversion as HashModel.get(). I also removed a redundant validation call and consolidated the optional-field tests into a parameterized regression covering strings/integers with no default, a None default, and a non-None default, plus booleans and datetimes.

All 344 async/sync HashModel and JsonModel tests pass locally, as do Ruff lint/format, the configured mypy target, and Bandit.

The indexed numeric case is still unresolved, and I'd appreciate your guidance on the intended null-storage behavior. On Redis Stack (Redis 7.4.7), writing "" to a NUMERIC field makes the record disappear from search results; FT.INFO reports Invalid numeric value: ''. The reader fix cannot address that.

I checked replacing the field with HDEL, but the resulting missing-field behavior is different from explicitly saving None:

Declaration Value loaded after deleting the field
`value: int None`
`value: int None = None`
`value: int None = 7`

Nullable strings have the same default/missing-field distinction. Treating every absent nullable field as None would change normal defaults and expiration behavior as well.

Would you prefer this PR to reject unsupported None updates explicitly until a general null-storage design is agreed, or should it introduce separate null metadata to preserve explicit None for these fields? I'd prefer to agree on the representation before expanding #772 into a storage-format change. I have left the write representation unchanged for now, so this does not resolve the whole Bugbot comment yet.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit f4f0caf. Configure here.

Comment thread aredis_om/model/model.py
# field name) to the target value.
setattr(obj, target_field, value)
await self.save()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nested updates crash on null parents

Medium Severity

Nested JSON writes accept Optional embedded paths during validation, then fail when the parent is None. update() and FindQuery.update() hit setattr on None, and save(update_fields=...) subscripts None while walking the dumped document, so a bulk nested update aborts before the pipeline executes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f4f0caf. Configure here.

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.

add update method or implement save(update_fields) like django orm

1 participant