-
-
Notifications
You must be signed in to change notification settings - Fork 32.9k
Fixed #36481 -- Fixed QuerySet.update concrete fields check. #19595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @rpkilby for this PR, this is an interesting find! 🌟
I think this could be simplified further, since, assuming A = field.auto_created
and B = field.concrete
, using DeMorgan Law:
direct = not (A and not B) or not B
direct = not A or not (not B) or not B
direct = not A or B or not B
direct = not A or True
direct = True
So then:
if not direct or (field.is_relation and field.many_to_many):
Becomes if not True or...
which is if False or...
which is:
if field.is_relation and field.many_to_many:
Wanna push that change?
@charettes am I missing something??? 🤯 |
@nessita There is some context in the attached ticket, but I think it would be valuable to keep the |
Thank you @rpkilby! I did read the ticket, but I still think there are two things in this PR:
|
Hi @nessita. I'd argue the two items are related and that this is more of a bugfix. imo this change reflects the intent of the original code, and As to the changed test method, composite keys are a new feature as of 5.2, and Updated the test to include both saved and unsaved instances. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo this change reflects the intent of the original code, and direct being reducable is only due to the faulty logic, which this would fix.
I agree with this conclusion. The logic in add_update_values
is there to ensure that only fields backed by actual columns can be updated against as that's a requirement of UPDATE
.
Since Field.concrete
basically means column is not None
only checking against it should be enough? That fact we need the or (field.is_relation and field.many_to_many)
part in the first place is likely due to ManyToManyField
lacking a proper get_attname_column
override.
def get_attname_column(self):
attname, column = super().get_attname_column()
return attname, None
@charettes should I commit an update with the following changes: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index bad71a5fd6..e97ecf8a0d 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1844,6 +1844,10 @@ class ManyToManyField(RelatedField):
)
return name, path, args, kwargs
+ def get_attname_column(self):
+ attname, column = super().get_attname_column()
+ return attname, None
+
def _get_path_info(self, direct=False, filtered_relation=None):
"""Called by both direct and indirect m2m traversal."""
int_model = self.remote_field.through
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index d0e0f82cd5..86f12f95ea 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -91,10 +91,10 @@ class UpdateQuery(Query):
raise FieldError(
"Composite primary key fields must be updated individually."
)
- if not field.concrete or (field.is_relation and field.many_to_many):
+ if not field.concrete:
raise FieldError(
- "Cannot update model field %r (only non-relations and "
- "foreign keys permitted)." % field
+ "Cannot update model field %r (only concrete fields are permitted)."
+ % field
)
if model is not self.get_meta().concrete_model:
self.add_related_update(model, field, val)
diff --git a/tests/update/tests.py b/tests/update/tests.py
index bb83440008..e17d906c80 100644
--- a/tests/update/tests.py
+++ b/tests/update/tests.py
@@ -160,7 +160,7 @@ class AdvancedTests(TestCase):
msg = (
"Cannot update model field "
"<django.db.models.fields.related.ManyToManyField: m2m_foo> "
- "(only non-relations and foreign keys permitted)."
+ "(only concrete fields are permitted)."
)
with self.assertRaisesMessage(FieldError, msg):
Bar.objects.update(m2m_foo="whatever") |
@rpkilby no lets keep this work focused on dealing with |
Thanks - let me know if there's anything else I can to do to help wrap this up. |
I've reopened https://code.djangoproject.com/ticket/35453, which discusses this question. |
Hi @charettes @nessita - anything I can do to move this forward? Would be great to see this released as a bugfix in 5.2.x |
@rpkilby you'll want to unset patch needs improvement on the ticket to make sure it appears in the review queue. It's unlikely to be backported to 5.2.x as it doesn't qualify per our policy but it might make the cut for 6.0 feature freeze which is around the corner. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @rpkilby, this looks great.
I verified that in addition to ForeignObject
, the reverse side of a OneToOneField
behaves the same way, failing now with this better error rather than AttributeError
.
Could you add a test case for OneToOneField
? Something like:
diff --git a/tests/update/models.py b/tests/update/models.py
index d71fc887c7..83c289a5e7 100644
--- a/tests/update/models.py
+++ b/tests/update/models.py
@@ -50,4 +50,6 @@ class UniqueNumber(models.Model):
class UniqueNumberChild(UniqueNumber):
- pass
+ parent = models.OneToOneField(
+ null=True, to=UniqueNumber, on_delete=models.CASCADE, related_name="child"
+ )
diff --git a/tests/update/tests.py b/tests/update/tests.py
index bb83440008..6a85c992f0 100644
--- a/tests/update/tests.py
+++ b/tests/update/tests.py
@@ -165,6 +165,14 @@ class AdvancedTests(TestCase):
with self.assertRaisesMessage(FieldError, msg):
Bar.objects.update(m2m_foo="whatever")
+ def test_update_o2o_field(self):
+ msg = (
+ "Cannot update model field <OneToOneRel: update.uniquenumberchild>"
+ " (only non-relations and foreign keys permitted)."
+ )
+ with self.assertRaisesMessage(FieldError, msg):
+ UniqueNumber.objects.update(child="whatever")
+
def test_update_transformed_field(self):
A.objects.create(x=5)
A.objects.create(x=-6)
(I'm also happy to push this myself in the next few days, as the PR is already ready for merge otherwise.) |
Thanks for the review @jacobtylerwalls, I can add this test case later today. |
Hi @jacobtylerwalls, I added an o2o field between |
c9af049
to
96d4de2
Compare
FieldError is now emitted for invalid update calls involving reverse relations, where previously they failed with AttributeError.
96d4de2
to
a69a9f1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful, thanks for the updates!
Trac ticket number
ticket-36481
Branch description
Fixed
QuerySet.update
concrete fields check.Checklist
main
branch.