fix(patch): leave unset fields untouched in update payloads - #561
Conversation
Code ReviewIssues Found
Summary1 issue found. The incomplete guard in the shared |
f380097 to
3dd1711
Compare
fixed this issue -- re-review the PR |
@claude re-review this PR, addressed your comments |
|
Claude finished @prasad-albert's task in 1m 20s —— View job Code Review
FindingsPrevious issue — FIXED. if attribute not in updated.model_fields_set:
continueThe All four guard sites
All sites are consistent. Unset fields are skipped unconditionally, regardless of their default value. Tests
Minor nit (non-blocking)
SummaryNo issues found. The fix is correct, all guard sites are consistent, and the new tests validate the key scenario. Good to merge. |
sumedhb1995
left a comment
There was a problem hiding this comment.
Awesome!! This changes update semantics for anyone who relied on "construct a fresh partial object, call update, and have omitted fields be deleted." Could you please ensure that, at-least internally, no-one was relying on this paradigm? That pattern is unusual (the documented flow is fetch-modify-save), but it is a behavior change nonetheless. I would consider at the very least including this in the some breaking changes or similar change notes to let any external consumers of the SDK know
sumedhb1995
left a comment
There was a problem hiding this comment.
Just a heads up, I think smart_datasets.py defines its own _generate_patch_payload that does not delegate to base and has no model_fields_set guard. That will need its own separate fix
It is a behavior change -- I will be explicitly adding this in the release notes and communicate to internal users. |
added a fix for this in the same PR |
Patch-payload generators decided delete ops from `value is None`, so a field the caller never set was read as a deletion. Partial update objects emitted bad `delete` ops that the API rejects or that silently wiped existing data (inventory rows, tags). Skip any attribute not in `updated.model_fields_set`: an unset field is a no-op, while an explicit `None`/`[]` still deletes/clears. Covers all collections using `_generate_patch_payload` plus task special attrs in `generate_adv_patch_payload`. Adds integration coverage for lots (base path) and tasks (special-attr path).
inventory._generate_inventory_patch_payload iterated tags/acls/cas/company without checking model_fields_set, coercing unset attrs to [] and emitting delete ops for all existing values. attachments._generate_attachment_patch_payload constructed an empty AttachmentMetadata() when metadata was unset, diffing it against the server state and emitting deletes for every existing metadata field. Same model_fields_set guard applied as in base._generate_patch_payload.
…e_patch_payload SmartDataset overrides _generate_patch_payload without delegating to base, so the model_fields_set guard added in base.py did not cover it. Unset fields emitted spurious UPDATE ops with null new_value. Same fix applied.
The API rejects multiple operations on the same list attribute (e.g. Symbols) in a single request with '1 entities duplicated'. Batch scalar ops together and send each list-attribute op (delete/add) individually, matching the pattern used for Units/Synonyms in PR #559.
45dd230 to
48a87bb
Compare
Problem
Patch-payload generators decided
deleteops fromvalue is Nonealone. A field the caller never set defaults toNone(or is coerced to[]bygetattr(updated, attr) or []in the task special-attr loop), so omitted fields were read as deletions.For partial update objects (the dominant Ask Albert agent pattern), this emitted bad
deleteops that either:"Delete operation not allowed for attribute-name"), orFix
Gate the diff on
attribute in updated.model_fields_set:not in model_fields_set) -> no-op, leave server valueNone-> delete (unchanged)[]/{}-> clear to empty (unchanged)Non-breaking: the fetch-mutate-update pattern is unchanged (fetched objects have all server fields in
model_fields_set); only erroneous deletes of omitted fields stop. Explicit clears still work.