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

Reported by: Shai Berger Owned by: Clifford Gama
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

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):  
    318318        with self.assertRaisesMessage(FieldError, msg):
    319319            RemoteEmployee.objects.update(adjusted_salary=F('salary') * 5)
    320320
     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
    321326    def test_object_update_unsaved_objects(self):
    322327        # F expressions cannot be used to update attributes on objects which do
    323328        # 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 Abhijith Ganesh, 4 years ago

Was this bug patched, I would like to work on this bug and patch it. Would that be possible @Shai Berger

comment:2 by Mariusz Felisiak, 4 years ago

Triage Stage: UnreviewedAccepted
Type: BugCleanup/optimization

Shai, thanks for the report.

Abhijith, feel-free to prepare a patch.

comment:3 by Abhijith Ganesh, 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 Abhijith Ganesh, 4 years ago

Owner: changed from nobody to Abhijith Ganesh
Status: newassigned

I am self assigning myself for this issue to avoid confusion.

comment:5 by Abhijith Ganesh, 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 Abhijith Ganesh, 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 Shai Berger, 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).

comment:8 by Bhuvnesh, 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.

Last edited 2 years ago by Bhuvnesh (previous) (diff)

in reply to:  8 comment:9 by Shai Berger, 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.

comment:10 by Clifford Gama, 30 hours ago

Owner: changed from Abhijith Ganesh to Clifford Gama

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:  
    10181018                    names.append(field.attname)
    10191019        return frozenset(names)
    10201020
     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
    10211033    @cached_property
    10221034    def _reverse_one_to_one_field_names(self):
    10231035        """
  • 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):  
    9999                    "Cannot update model field %r (only non-relations and "
    100100                    "foreign keys permitted)." % field
    101101                )
     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                    )
    102108            if model is not self.get_meta().concrete_model:
    103109                self.add_related_update(model, field, val)
    104110                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 Clifford Gama, 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 ...

in reply to:  10 comment:12 by Shai Berger, 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):  
    9999                    "Cannot update model field %r (only non-relations and "
    100100                    "foreign keys permitted)." % field
    101101                )
     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                    )
    102108            if model is not self.get_meta().concrete_model:
    103109                self.add_related_update(model, field, val)
    104110                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.

comment:13 by Clifford Gama, 24 hours ago

Thank you. I will prepare a patch in the following few days

Note: See TracTickets for help on using tickets.
Back to Top