Opened 4 years ago
Last modified 24 hours ago
#33091 assigned Cleanup/optimization
A FieldError should be raised when trying to update MTI inherited field with F reference to child field
Description ¶
This is the converse of #30044.
In that bug, we made sure a proper error was raised when we do
ChildModel.objects.update(child_field=F('parent_field'))
We should similarly make sure a proper error is raised when we do
ChildModel.objects.update(parent_field=F('child_field'))
With current main, if we add the obvious test
-
TabularUnified tests/expressions/tests.py
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 33c5189390..0a97d537fe 100644
a b class BasicExpressionsTests(TestCase): 318 318 with self.assertRaisesMessage(FieldError, msg): 319 319 RemoteEmployee.objects.update(adjusted_salary=F('salary') * 5) 320 320 321 def test_update_set_inherited_field_from_child(self): 322 msg = 'Joined field references are not permitted in this query' 323 with self.assertRaisesMessage(FieldError, msg): 324 RemoteEmployee.objects.update(salary=F('adjusted_salary') / 5) 325 321 326 def test_object_update_unsaved_objects(self): 322 327 # F expressions cannot be used to update attributes on objects which do 323 328 # not yet exist in the database
it fails
====================================================================== FAIL: test_update_set_inherited_field_from_child (expressions.tests.BasicExpressionsTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/usr/lib/python3.9/unittest/case.py", line 59, in testPartExecutor yield File "/usr/lib/python3.9/unittest/case.py", line 592, in run self._callTestMethod(testMethod) File "/usr/lib/python3.9/unittest/case.py", line 550, in _callTestMethod method() File "/home/django/django/tests/expressions/tests.py", line 324, in test_update_set_inherited_field_from_child RemoteEmployee.objects.update(salary=F('adjusted_salary') / 5) File "/usr/lib/python3.9/contextlib.py", line 137, in __exit__ self.gen.throw(typ, value, traceback) File "/home/django/django/django/test/testcases.py", line 687, in _assert_raises_or_warns_cm self.assertIn(expected_message, str(getattr(cm, cm_attr))) File "/usr/lib/python3.9/unittest/case.py", line 1096, in assertIn self.fail(self._formatMessage(msg, standardMsg)) File "/usr/lib/python3.9/unittest/case.py", line 668, in fail raise self.failureException(msg) AssertionError: 'Joined field references are not permitted in this query' not found in "Cannot resolve keyword 'adjusted_salary' into field. Choices are: company_ceo_set, company_point_of_contact_set, firstname, id, lastname, manager, manager_id, remoteemployee, salary" ----------------------------------------------------------------------
According to the ticket's flags, the next step(s) to move this issue forward are:
- To provide a patch by sending a pull request. Claim the ticket when you start working so that someone else doesn't duplicate effort. Before sending a pull request, review your work against the patch review checklist. Check the "Has patch" flag on the ticket after sending a pull request and include a link to the pull request in the ticket comment when making that update. The usual format is:
[https://github.com/django/django/pull/#### PR]
.
Change History (13)
comment:1 by , 4 years ago
comment:2 by , 4 years ago
Triage Stage: | Unreviewed → Accepted |
---|---|
Type: | Bug → Cleanup/optimization |
Shai, thanks for the report.
Abhijith, feel-free to prepare a patch.
comment:3 by , 4 years ago
This is my first attempt of patching an actual django bug, now once i have made the patch and pushed it to my repository, I should be creating a PR. Is that all?
comment:4 by , 4 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
I am self assigning myself for this issue to avoid confusion.
comment:5 by , 4 years ago
For fixing this I should be adding a conditional statement on query.py file right? and can I get some elaboration as to which method I should be resolving, I am under the assumption the same resolve method (function) will be getting a new conditional statement
comment:6 by , 4 years ago
if annotation and not allow_joins: raise FieldError('Parent field references are not allowed in this query.')
Would this be a good fix?
This is my first fix and I am looking forward to learn, thanks in advance.
comment:7 by , 4 years ago
Hi Abhijith, sorry for failing to respond earlier.
The best answer is, try: I've included in the ticket description a failing test for it. Add this test, and check if your suggested fix makes it pass (and doesn't break other tests).
follow-up: 9 comment:8 by , 2 years ago
I think there is no direct way to get child field from opts of parent model in inheritance. For the testcase provided by shai the value of field is None at this part ,throwing different field error here. Is it the expected behaviour? or we are supposed to get the child field and later raise field error Joined field references are not permitted in this query.
comment:9 by , 2 years ago
Replying to Bhuvnesh:
I think there is no direct way to get child field from opts of parent model in inheritance.
That makes sense
For the testcase provided by shai the value of field is None at this part ,throwing different field error here. Is it the expected behaviour?
No, that's the bug...
or we are supposed to get the child field and later raise field error
Joined field references are not permitted in this query.
Yes, basically. Or, putting it another way (given your input): The update()
logic uses the parent model opts
, because the fields to be updated are all from that model; but then it uses the parent model to resolve all references, and that is wrong if there are expressions referencing the child model. Or, more succinctly: The move to the parent model is too early.
follow-up: 12 comment:10 by , 30 hours ago
Owner: | changed from | to
---|
I wonder if it would be acceptable to validate the expression references in UpdateQuery.add_update_values()
with something like:
-
TabularUnified django/db/models/options.py
diff --git a/django/db/models/options.py b/django/db/models/options.py index 11b2742f7d..8457d74ff9 100644
a b class Options: 1018 1018 names.append(field.attname) 1019 1019 return frozenset(names) 1020 1020 1021 @cached_property 1022 def _local_concrete_field_names(self): 1023 """ 1024 Return a set of the local concrete field names defined on the model. 1025 """ 1026 names = [] 1027 for f in self.local_concrete_fields: 1028 names.append(f.name) 1029 if f.name != f.attname: 1030 names.append(f.attname) 1031 return frozenset(names) 1032 1021 1033 @cached_property 1022 1034 def _reverse_one_to_one_field_names(self): 1023 1035 """ -
TabularUnified django/db/models/sql/subqueries.py
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 9cb971b38f..7f6d06ee1f 100644
a b class UpdateQuery(Query): 99 99 "Cannot update model field %r (only non-relations and " 100 100 "foreign keys permitted)." % field 101 101 ) 102 for field_name, *lookups in model._get_expr_references(val): 103 if field_name not in model._meta._local_concrete_field_names: 104 raise FieldError( 105 "Cannot update model field %r using the non-local field " 106 "reference %r." % (field, field_name) 107 ) 102 108 if model is not self.get_meta().concrete_model: 103 109 self.add_related_update(model, field, val) 104 110 continue
this would handle the case in this ticket and the one in #30044 without the need for special-casing either. This would make this ticket easier, by simplifying the logic since we don't have to check whether a descendant's reference is actually a field in the model originating the query update.
The downside is that this would also raise for invalid references, without listing the available field choices.
comment:11 by , 29 hours ago
remote_employee.salary = F("incentive") * 5 remote_employee.save()
could also raise the same error. Currently it's FieldError: Cannot resolve ... into field ... The choices are ...
comment:12 by , 24 hours ago
Replying to Clifford Gama:
TabularUnified django/db/models/sql/subqueries.py
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 9cb971b38f..7f6d06ee1f 100644
a b class UpdateQuery(Query): 99 99 "Cannot update model field %r (only non-relations and " 100 100 "foreign keys permitted)." % field 101 101 ) 102 for field_name, *lookups in model._get_expr_references(val): 103 if field_name not in model._meta._local_concrete_field_names: 104 raise FieldError( 105 "Cannot update model field %r using the non-local field " 106 "reference %r." % (field, field_name) 107 ) 102 108 if model is not self.get_meta().concrete_model: 103 109 self.add_related_update(model, field, val) 104 110 continue
I find the language "non-local field reference" a bit cryptic. It's only clear when you think in terms of _meta
properties and functions, and it's too easy to code the error when not thinking about those at all.
It makes sense to me, to use something like your suggestion to easily find if there was an error. But in the case that an error is found, I'd prefer if we start looking for the specific cases, so we can give a more friendly error message.
Was this bug patched, I would like to work on this bug and patch it. Would that be possible @Shai Berger