diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index a7e40703a3f8..28c0f6679e7b 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -67,7 +67,6 @@ def check(self, **kwargs): ) ) else: - # Remove the field name checks as they are not needed here. base_checks = self.base_field.check() if base_checks: error_messages = "\n ".join( diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 77605b178f69..58594fb66f80 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -106,9 +106,12 @@ def handle_inspection(self, options): connection.introspection.get_primary_key_columns( cursor, table_name ) + or [] ) primary_key_column = ( - primary_key_columns[0] if primary_key_columns else None + primary_key_columns[0] + if len(primary_key_columns) == 1 + else None ) unique_columns = [ c["columns"][0] @@ -128,6 +131,11 @@ def handle_inspection(self, options): yield "" yield "class %s(models.Model):" % model_name known_models.append(model_name) + + if len(primary_key_columns) > 1: + fields = ", ".join([f"'{col}'" for col in primary_key_columns]) + yield f" pk = models.CompositePrimaryKey({fields})" + used_column_names = [] # Holds column names used in the table so far column_to_field_name = {} # Maps column names to names of model fields used_relations = set() # Holds foreign relations used in the table. @@ -151,12 +159,6 @@ def handle_inspection(self, options): # Add primary_key and unique, if necessary. if column_name == primary_key_column: extra_params["primary_key"] = True - if len(primary_key_columns) > 1: - comment_notes.append( - "The composite primary key (%s) found, that is not " - "supported. The first column is selected." - % ", ".join(primary_key_columns) - ) elif column_name in unique_columns: extra_params["unique"] = True diff --git a/django/db/models/query.py b/django/db/models/query.py index e1f785f71456..1a4452337442 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -26,7 +26,7 @@ from django.db.models.expressions import Case, F, Value, When from django.db.models.functions import Cast, Trunc from django.db.models.query_utils import FilteredRelation, Q -from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE +from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, ROW_COUNT from django.db.models.utils import ( AltersData, create_namedtuple_class, @@ -1209,11 +1209,7 @@ def _raw_delete(self, using): """ query = self.query.clone() query.__class__ = sql.DeleteQuery - cursor = query.get_compiler(using).execute_sql(CURSOR) - if cursor: - with cursor: - return cursor.rowcount - return 0 + return query.get_compiler(using).execute_sql(ROW_COUNT) _raw_delete.alters_data = True @@ -1252,7 +1248,7 @@ def update(self, **kwargs): # Clear any annotations so that they won't be present in subqueries. query.annotations = {} with transaction.mark_for_rollback_on_error(using=self.db): - rows = query.get_compiler(self.db).execute_sql(CURSOR) + rows = query.get_compiler(self.db).execute_sql(ROW_COUNT) self._result_cache = None return rows @@ -1277,7 +1273,7 @@ def _update(self, values): # Clear any annotations so that they won't be present in subqueries. query.annotations = {} self._result_cache = None - return query.get_compiler(self.db).execute_sql(CURSOR) + return query.get_compiler(self.db).execute_sql(ROW_COUNT) _update.alters_data = True _update.queryset_only = False diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 49c5d301ccfd..5bb491d823cc 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -19,6 +19,7 @@ MULTI, NO_RESULTS, ORDER_DIR, + ROW_COUNT, SINGLE, ) from django.db.models.sql.query import Query, get_order_dir @@ -1596,15 +1597,15 @@ def execute_sql( ): """ Run the query against the database and return the result(s). The - return value is a single data item if result_type is SINGLE, or an - iterator over the results if the result_type is MULTI. - - result_type is either MULTI (use fetchmany() to retrieve all rows), - SINGLE (only retrieve a single row), or None. In this last case, the - cursor is returned if any query is executed, since it's used by - subclasses such as InsertQuery). It's possible, however, that no query - is needed, as the filters describe an empty set. In that case, None is - returned, to avoid any unnecessary database interaction. + return value depends on the value of result_type. + + When result_type is: + - MULTI: Retrieves all rows using fetchmany(). Wraps in an iterator for + chunked reads when supported. + - SINGLE: Retrieves a single row using fetchone(). + - ROW_COUNT: Retrieves the number of rows in the result. + - CURSOR: Runs the query, and returns the cursor object. It is the + caller's responsibility to close the cursor. """ result_type = result_type or NO_RESULTS try: @@ -1627,6 +1628,11 @@ def execute_sql( cursor.close() raise + if result_type == ROW_COUNT: + try: + return cursor.rowcount + finally: + cursor.close() if result_type == CURSOR: # Give the caller the cursor to process and close. return cursor @@ -2069,19 +2075,19 @@ def execute_sql(self, result_type): non-empty query that is executed. Row counts for any subsequent, related queries are not available. """ - cursor = super().execute_sql(result_type) - try: - rows = cursor.rowcount if cursor else 0 - is_empty = cursor is None - finally: - if cursor: - cursor.close() + row_count = super().execute_sql(result_type) + is_empty = row_count is None + row_count = row_count or 0 + for query in self.query.get_related_updates(): - aux_rows = query.get_compiler(self.using).execute_sql(result_type) - if is_empty and aux_rows: - rows = aux_rows + # If the result_type is NO_RESULTS then the aux_row_count is None. + aux_row_count = query.get_compiler(self.using).execute_sql(result_type) + if is_empty and aux_row_count: + # Returns the row count for any related updates as the number of + # rows updated. + row_count = aux_row_count is_empty = False - return rows + return row_count def pre_sql_setup(self): """ diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index fdfb2ea891ff..709405b0dfb8 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -11,8 +11,10 @@ # How many results to expect from a cursor.execute call MULTI = "multi" SINGLE = "single" -CURSOR = "cursor" NO_RESULTS = "no results" +# Rather than returning results, returns: +CURSOR = "cursor" +ROW_COUNT = "row count" ORDER_DIR = { "ASC": ("ASC", "DESC"), diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index cca11bfcc213..6fbf854e67f0 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1704,12 +1704,12 @@ def add_filtered_relation(self, filtered_relation, alias): "relations outside the %r (got %r)." % (filtered_relation.relation_name, lookup) ) - else: - raise ValueError( - "FilteredRelation's condition doesn't support nested " - "relations deeper than the relation_name (got %r for " - "%r)." % (lookup, filtered_relation.relation_name) - ) + if len(lookup_field_parts) > len(relation_field_parts) + 1: + raise ValueError( + "FilteredRelation's condition doesn't support nested " + "relations deeper than the relation_name (got %r for " + "%r)." % (lookup, filtered_relation.relation_name) + ) filtered_relation.condition = rename_prefix_from_q( filtered_relation.relation_name, alias, diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index f639eb8b8238..b2810c8413b5 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -3,7 +3,11 @@ """ from django.core.exceptions import FieldError -from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE, NO_RESULTS +from django.db.models.sql.constants import ( + GET_ITERATOR_CHUNK_SIZE, + NO_RESULTS, + ROW_COUNT, +) from django.db.models.sql.query import Query __all__ = ["DeleteQuery", "UpdateQuery", "InsertQuery", "AggregateQuery"] @@ -17,11 +21,7 @@ class DeleteQuery(Query): def do_query(self, table, where, using): self.alias_map = {table: self.alias_map[table]} self.where = where - cursor = self.get_compiler(using).execute_sql(CURSOR) - if cursor: - with cursor: - return cursor.rowcount - return 0 + return self.get_compiler(using).execute_sql(ROW_COUNT) def delete_batch(self, pk_list, using): """ diff --git a/django/test/client.py b/django/test/client.py index 85d91b0c448a..c7333721305d 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -752,6 +752,8 @@ def generic( "scheme": "https" if secure else "http", "headers": [(b"host", b"testserver")], } + if self.defaults: + extra = {**self.defaults, **extra} if data: s["headers"].extend( [ diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index b1260093c133..7f0212b75724 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -87,6 +87,11 @@ MRO is an acronym for Method Resolution Order. Performs key view initialization prior to :meth:`dispatch`. + Assigns the :class:`~django.http.HttpRequest` to the view's ``request`` + attribute, and any positional and/or keyword arguments + :ref:`captured from the URL pattern ` + to the ``args`` and ``kwargs`` attributes, respectively. + If overriding this method, you must call ``super()``. .. method:: dispatch(request, *args, **kwargs) diff --git a/tests/delete/tests.py b/tests/delete/tests.py index 01228631f4ba..e24c22206304 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -794,6 +794,15 @@ def test_fast_delete_aggregation(self): ) self.assertIs(Base.objects.exists(), False) + def test_fast_delete_empty_result_set(self): + user = User.objects.create() + with self.assertNumQueries(0): + self.assertEqual( + User.objects.filter(pk__in=[]).delete(), + (0, {}), + ) + self.assertSequenceEqual(User.objects.all(), [user]) + def test_fast_delete_full_match(self): avatar = Avatar.objects.create(desc="bar") User.objects.create(avatar=avatar) diff --git a/tests/file_storage/models.py b/tests/file_storage/models.py index cb0207cae909..12a54edda57b 100644 --- a/tests/file_storage/models.py +++ b/tests/file_storage/models.py @@ -80,5 +80,5 @@ def pathlib_upload_to(self, filename): storage=temp_storage, upload_to="tests", max_length=20 ) extended_length = models.FileField( - storage=temp_storage, upload_to="tests", max_length=300 + storage=temp_storage, upload_to="tests", max_length=1024 ) diff --git a/tests/filtered_relation/tests.py b/tests/filtered_relation/tests.py index 82caba866217..cbf77752dfff 100644 --- a/tests/filtered_relation/tests.py +++ b/tests/filtered_relation/tests.py @@ -668,6 +668,19 @@ def test_condition_deeper_relation_name(self): ), ) + def test_condition_deeper_relation_name_implicit_exact(self): + msg = ( + "FilteredRelation's condition doesn't support nested relations " + "deeper than the relation_name (got 'book__editor__name' for 'book')." + ) + with self.assertRaisesMessage(ValueError, msg): + Author.objects.annotate( + book_editor=FilteredRelation( + "book", + condition=Q(book__editor__name="b"), + ), + ) + def test_with_empty_relation_name_error(self): with self.assertRaisesMessage(ValueError, "relation_name cannot be empty."): FilteredRelation("", condition=Q(blank="")) diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 1be4efc43051..131bd45ce89e 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -655,11 +655,10 @@ def test_composite_primary_key(self): call_command("inspectdb", table_name, stdout=out) output = out.getvalue() self.assertIn( - f"column_1 = models.{field_type}(primary_key=True) # The composite " - f"primary key (column_1, column_2) found, that is not supported. The " - f"first column is selected.", + "pk = models.CompositePrimaryKey('column_1', 'column_2')", output, ) + self.assertIn(f"column_1 = models.{field_type}()", output) self.assertIn( "column_2 = models.%s()" % connection.features.introspected_field_types["IntegerField"], diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 18b7aaef6c0f..652563d269cd 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -1327,6 +1327,19 @@ def test_request_factory_sets_headers(self): self.assertEqual(request.headers["x-another-header"], "some other value") self.assertIn("HTTP_X_ANOTHER_HEADER", request.META) + def test_async_request_factory_default_headers(self): + request_factory_with_headers = AsyncRequestFactory( + **{ + "Authorization": "Bearer faketoken", + "X-Another-Header": "some other value", + } + ) + request = request_factory_with_headers.get("/somewhere/") + self.assertEqual(request.headers["authorization"], "Bearer faketoken") + self.assertIn("HTTP_AUTHORIZATION", request.META) + self.assertEqual(request.headers["x-another-header"], "some other value") + self.assertIn("HTTP_X_ANOTHER_HEADER", request.META) + def test_request_factory_query_string(self): request = self.request_factory.get("/somewhere/", {"example": "data"}) self.assertNotIn("Query-String", request.headers)