diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index cc33740195..96d555f862 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1660,7 +1660,9 @@ class BaseDatabaseSchemaEditor: return output def _field_should_be_altered(self, old_field, new_field, ignore=None): - if not old_field.concrete and not new_field.concrete: + if (not (old_field.concrete or old_field.many_to_many)) and ( + not (new_field.concrete or new_field.many_to_many) + ): return False ignore = ignore or set() _, old_path, old_args, old_kwargs = old_field.deconstruct() @@ -1692,8 +1694,10 @@ class BaseDatabaseSchemaEditor: ): old_kwargs.pop("db_default") new_kwargs.pop("db_default") - return self.quote_name(old_field.column) != self.quote_name( - new_field.column + return ( + old_field.concrete + and new_field.concrete + and (self.quote_name(old_field.column) != self.quote_name(new_field.column)) ) or (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs) def _field_should_be_indexed(self, model, field): diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index ac6ae5efbd..077a53bf55 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -144,7 +144,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): # Choose a default and insert it into the copy map if ( not create_field.has_db_default() - and not (create_field.many_to_many or create_field.generated) + and not create_field.generated and create_field.concrete ): mapping[create_field.column] = self.prepare_default( diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 9ad440128e..a71ae2f401 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1849,6 +1849,10 @@ class ManyToManyField(RelatedField): ) return name, path, args, kwargs + def get_attname_column(self): + attname, _ = 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/query.py b/django/db/models/query.py index bd79e4bf36..721bf33e57 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -725,7 +725,7 @@ class QuerySet(AltersData): "Unique fields that can trigger the upsert must be provided." ) # Updating primary keys and non-concrete fields is forbidden. - if any(not f.concrete or f.many_to_many for f in update_fields): + if any(not f.concrete for f in update_fields): raise ValueError( "bulk_create() can only be used with concrete fields in " "update_fields." @@ -736,7 +736,7 @@ class QuerySet(AltersData): "update_fields." ) if unique_fields: - if any(not f.concrete or f.many_to_many for f in unique_fields): + if any(not f.concrete for f in unique_fields): raise ValueError( "bulk_create() can only be used with concrete fields " "in unique_fields." @@ -916,7 +916,7 @@ class QuerySet(AltersData): raise ValueError("All bulk_update() objects must have a primary key set.") opts = self.model._meta fields = [opts.get_field(name) for name in fields] - if any(not f.concrete or f.many_to_many for f in fields): + if any(not f.concrete for f in fields): raise ValueError("bulk_update() can only be used with concrete fields.") all_pk_fields = set(opts.pk_fields) for parent in opts.all_parents: diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 9936f7ff42..9302247648 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -92,10 +92,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/composite_pk/test_update.py b/tests/composite_pk/test_update.py index 8d786e8afb..4f3068f228 100644 --- a/tests/composite_pk/test_update.py +++ b/tests/composite_pk/test_update.py @@ -172,7 +172,7 @@ class CompositePKUpdateTests(TestCase): def test_cant_update_relation(self): msg = ( "Cannot update model field (only non-relations and foreign keys permitted)" + "user> (only concrete fields are permitted)" ) with self.assertRaisesMessage(FieldError, msg): diff --git a/tests/model_fields/test_field_flags.py b/tests/model_fields/test_field_flags.py index 33f3334567..778a43eb63 100644 --- a/tests/model_fields/test_field_flags.py +++ b/tests/model_fields/test_field_flags.py @@ -6,6 +6,7 @@ from .models import AllFieldsModel NON_CONCRETE_FIELDS = ( models.ForeignObject, + models.ManyToManyField, GenericForeignKey, GenericRelation, ) @@ -209,7 +210,7 @@ class FieldFlagsTests(test.SimpleTestCase): def test_model_and_reverse_model_should_equal_on_relations(self): for field in AllFieldsModel._meta.get_fields(): is_concrete_forward_field = field.concrete and field.related_model - if is_concrete_forward_field: + if is_concrete_forward_field or field.many_to_many: reverse_field = field.remote_field self.assertEqual(field.model, reverse_field.related_model) self.assertEqual(field.related_model, reverse_field.model) diff --git a/tests/update/tests.py b/tests/update/tests.py index af5939a2ef..eb5d80219c 100644 --- a/tests/update/tests.py +++ b/tests/update/tests.py @@ -157,43 +157,32 @@ class AdvancedTests(TestCase): self.assertEqual(bar_qs[0].foo_id, b_foo.target) def test_update_m2m_field(self): - msg = ( - "Cannot update model field " - " " - "(only non-relations and foreign keys permitted)." - ) + rel = "" + msg = f"Cannot update model field {rel} (only concrete fields are permitted)." with self.assertRaisesMessage(FieldError, msg): Bar.objects.update(m2m_foo="whatever") def test_update_reverse_m2m_descriptor(self): - msg = ( - "Cannot update model field " - "(only non-relations and foreign keys permitted)." - ) + rel = "" + msg = f"Cannot update model field {rel} (only concrete fields are permitted)." with self.assertRaisesMessage(FieldError, msg): Foo.objects.update(m2m_foo="whatever") def test_update_reverse_fk_descriptor(self): - msg = ( - "Cannot update model field " - "(only non-relations and foreign keys permitted)." - ) + rel = "" + msg = f"Cannot update model field {rel} (only concrete fields are permitted)." with self.assertRaisesMessage(FieldError, msg): Foo.objects.update(bar="whatever") def test_update_reverse_o2o_descriptor(self): - msg = ( - "Cannot update model field " - "(only non-relations and foreign keys permitted)." - ) + rel = "" + msg = f"Cannot update model field {rel} (only concrete fields are permitted)." with self.assertRaisesMessage(FieldError, msg): Foo.objects.update(o2o_bar="whatever") def test_update_reverse_mti_parent_link_descriptor(self): - msg = ( - "Cannot update model field " - "(only non-relations and foreign keys permitted)." - ) + rel = "" + msg = f"Cannot update model field {rel} (only concrete fields are permitted)." with self.assertRaisesMessage(FieldError, msg): UniqueNumber.objects.update(uniquenumberchild="whatever")