Fixed #36661 -- Added introspection of database-level delete options.

@@ -4,6 +4,7 @@
 from django.core.management.base import BaseCommand, CommandError
 from django.db import DEFAULT_DB_ALIAS, connections
 from django.db.models.constants import LOOKUP_SEP
+from django.db.models.deletion import DatabaseOnDelete

 class Command(BaseCommand):
@@ -163,7 +164,9 @@ def handle_inspection(self, options):
                         extra_params["unique"] = True

                     if is_relation:
-                        ref_db_column, ref_db_table = relations[column_name]
+                        ref_db_column, ref_db_table, db_on_delete = relations[
+                            column_name
+                        ]
                         if extra_params.pop("unique", False) or extra_params.get(
                             "primary_key"
                         ):
@@ -191,6 +194,8 @@ def handle_inspection(self, options):
                                 model_name.lower(),
                                 att_name,
                             )
+                        if db_on_delete and isinstance(db_on_delete, DatabaseOnDelete):
+                            extra_params["on_delete"] = f"models.{db_on_delete}"
                         used_relations.add(rel_to)
                     else:
                         # Calling `get_field_type` to get the field type string
@@ -227,8 +232,12 @@ def handle_inspection(self, options):
                         "" if "." in field_type else "models.",
                         field_type,
                     )
+                    on_delete_qualname = extra_params.pop("on_delete", None)
                     if field_type.startswith(("ForeignKey(", "OneToOneField(")):
-                        field_desc += ", models.DO_NOTHING"
+                        if on_delete_qualname:
+                            field_desc += f", {on_delete_qualname}"
+                        else:
+                            field_desc += ", models.DO_NOTHING"

                     # Add comment.
                     if connection.features.supports_comments and row.comment:

@@ -1,5 +1,7 @@
 from collections import namedtuple

+from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING
+
 # Structure returned by DatabaseIntrospection.get_table_list()
 TableInfo = namedtuple("TableInfo", ["name", "type"])

@@ -15,6 +17,13 @@ class BaseDatabaseIntrospection:
     """Encapsulate backend-specific introspection utilities."""

     data_types_reverse = {}
+    on_delete_types = {
+        "CASCADE": DB_CASCADE,
+        "NO ACTION": DO_NOTHING,
+        "SET DEFAULT": DB_SET_DEFAULT,
+        "SET NULL": DB_SET_NULL,
+        # DB_RESTRICT - "RESTRICT" is not supported.
+    }

     def __init__(self, connection):
         self.connection = connection
@@ -169,8 +178,11 @@ def get_sequences(self, cursor, table_name, table_fields=()):

     def get_relations(self, cursor, table_name):
         """
-        Return a dictionary of {field_name: (field_name_other_table,
-        other_table)} representing all foreign keys in the given table.
+        Return a dictionary of
+            {
+                field_name: (field_name_other_table, other_table, db_on_delete)
+            }
+        representing all foreign keys in the given table.
         """
         raise NotImplementedError(
             "subclasses of BaseDatabaseIntrospection may require a "

@@ -334,6 +334,7 @@ def check_constraints(self, table_names=None):
                 for column_name, (
                     referenced_column_name,
                     referenced_table_name,
+                    _,
                 ) in relations.items():
                     cursor.execute(
                         """

@@ -196,24 +196,36 @@ def get_sequences(self, cursor, table_name, table_fields=()):

     def get_relations(self, cursor, table_name):
         """
-        Return a dictionary of {field_name: (field_name_other_table,
-        other_table)} representing all foreign keys in the given table.
+        Return a dictionary of
+            {
+                field_name: (field_name_other_table, other_table, db_on_delete)
+            }
+        representing all foreign keys in the given table.
         """
         cursor.execute(
             """
-            SELECT column_name, referenced_column_name, referenced_table_name
-            FROM information_schema.key_column_usage
-            WHERE table_name = %s
-                AND table_schema = DATABASE()
-                AND referenced_table_schema = DATABASE()
-                AND referenced_table_name IS NOT NULL
-                AND referenced_column_name IS NOT NULL
+            SELECT
+                kcu.column_name,
+                kcu.referenced_column_name,
+                kcu.referenced_table_name,
+                rc.delete_rule
+            FROM
+                information_schema.key_column_usage kcu
+            JOIN
+                information_schema.referential_constraints rc
+                ON rc.constraint_name = kcu.constraint_name
+                AND rc.constraint_schema = kcu.constraint_schema
+            WHERE kcu.table_name = %s
+                AND kcu.table_schema = DATABASE()
+                AND kcu.referenced_table_schema = DATABASE()
+                AND kcu.referenced_table_name IS NOT NULL
+                AND kcu.referenced_column_name IS NOT NULL
             """,
             [table_name],
         )
         return {
-            field_name: (other_field, other_table)
-            for field_name, other_field, other_table in cursor.fetchall()
+            field_name: (other_field, other_table, self.on_delete_types.get(on_delete))
+            for field_name, other_field, other_table, on_delete in cursor.fetchall()
         }

     def get_storage_engine(self, cursor, table_name):

@@ -254,13 +254,16 @@ def get_sequences(self, cursor, table_name, table_fields=()):

     def get_relations(self, cursor, table_name):
         """
-        Return a dictionary of {field_name: (field_name_other_table,
-        other_table)} representing all foreign keys in the given table.
+        Return a dictionary of
+            {
+                field_name: (field_name_other_table, other_table, db_on_delete)
+            }
+        representing all foreign keys in the given table.
         """
         table_name = table_name.upper()
         cursor.execute(
             """
-    SELECT ca.column_name, cb.table_name, cb.column_name
+    SELECT ca.column_name, cb.table_name, cb.column_name, user_constraints.delete_rule
     FROM   user_constraints, USER_CONS_COLUMNS ca, USER_CONS_COLUMNS cb
     WHERE  user_constraints.table_name = %s AND
            user_constraints.constraint_name = ca.constraint_name AND
@@ -273,8 +276,14 @@ def get_relations(self, cursor, table_name):
             self.identifier_converter(field_name): (
                 self.identifier_converter(rel_field_name),
                 self.identifier_converter(rel_table_name),
+                self.on_delete_types.get(on_delete),
             )
-            for field_name, rel_table_name, rel_field_name in cursor.fetchall()
+            for (
+                field_name,
+                rel_table_name,
+                rel_field_name,
+                on_delete,
+            ) in cursor.fetchall()
         }

     def get_primary_key_columns(self, cursor, table_name):

@@ -3,7 +3,7 @@
 from django.db.backends.base.introspection import BaseDatabaseIntrospection
 from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
 from django.db.backends.base.introspection import TableInfo as BaseTableInfo
-from django.db.models import Index
+from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING, [NAME_REDACTED] = namedtuple("FieldInfo", [*BaseFieldInfo._fields, "is_autofield", "comment"])
 TableInfo = namedtuple("TableInfo", [*BaseTableInfo._fields, "comment"])
@@ -38,6 +38,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):

     ignored_tables = []

+    on_delete_types = {
+        "a": DO_NOTHING,
+        "c": DB_CASCADE,
+        "d": DB_SET_DEFAULT,
+        "n": DB_SET_NULL,
+        # DB_RESTRICT - "r" is not supported.
+    }
+
     def get_field_type(self, data_type, description):
         field_type = super().get_field_type(data_type, description)
         if description.is_autofield or (
@@ -154,12 +162,15 @@ def get_sequences(self, cursor, table_name, table_fields=()):

     def get_relations(self, cursor, table_name):
         """
-        Return a dictionary of {field_name: (field_name_other_table,
-        other_table)} representing all foreign keys in the given table.
+        Return a dictionary of
+            {
+                field_name: (field_name_other_table, other_table, db_on_delete)
+            }
+        representing all foreign keys in the given table.
         """
         cursor.execute(
             """
-            SELECT a1.attname, c2.relname, a2.attname
+            SELECT a1.attname, c2.relname, a2.attname, con.confdeltype
             FROM pg_constraint con
             LEFT JOIN pg_class c1 ON con.conrelid = c1.oid
             LEFT JOIN pg_class c2 ON con.confrelid = c2.oid
@@ -175,7 +186,10 @@ def get_relations(self, cursor, table_name):
         """,
             [table_name],
         )
-        return {row[0]: (row[2], row[1]) for row in cursor.fetchall()}
+        return {
+            row[0]: (row[2], row[1], self.on_delete_types.get(row[3]))
+            for row in cursor.fetchall()
+        }

     def get_constraints(self, cursor, table_name):
         """

@@ -153,20 +153,27 @@ def get_sequences(self, cursor, table_name, table_fields=()):

     def get_relations(self, cursor, table_name):
         """
-        Return a dictionary of {column_name: (ref_column_name, ref_table_name)}
+        Return a dictionary of
+            {column_name: (ref_column_name, ref_table_name, db_on_delete)}
         representing all foreign keys in the given table.
         """
         cursor.execute(
             "PRAGMA foreign_key_list(%s)" % self.connection.ops.quote_name(table_name)
         )
         return {
-            column_name: (ref_column_name, ref_table_name)
+            column_name: (
+                ref_column_name,
+                ref_table_name,
+                self.on_delete_types.get(on_delete),
+            )
             for (
                 _,
                 _,
                 ref_table_name,
                 column_name,
                 ref_column_name,
+                _,
+                on_delete,
                 *_,
             ) in cursor.fetchall()
         }
@@ -407,7 +414,10 @@ def get_constraints(self, cursor, table_name):
                     "check": False,
                     "index": False,
                 }
-                for index, (column_name, (ref_column_name, ref_table_name)) in relations
+                for index, (
+                    column_name,
+                    (ref_column_name, ref_table_name, _),
+                ) in relations
             }
         )
         return constraints

@@ -314,6 +314,11 @@ backends.
   database has native support for ``DurationField``, override this method to
   simply return the value.

+* The ``DatabaseIntrospection.get_relations()`` should now return a dictionary
+  with 3-tuples containing (``field_name_other_table``, ``other_table``,
+  ``db_on_delete``) as values. ``db_on_delete`` is one of the database-level
+  delete options e.g. :attr:`~django.db.models.DB_CASCADE`.
+
 :mod:`django.contrib.gis`
 -------------------------

@@ -161,3 +161,11 @@ class CompositePKModel(models.Model):
     pk = models.CompositePrimaryKey("column_1", "column_2")
     column_1 = models.IntegerField()
     column_2 = models.IntegerField()
+
+
+class DbOnDeleteModel(models.Model):
+    fk_do_nothing = models.ForeignKey(UniqueTogether, on_delete=models.DO_NOTHING)
+    fk_db_cascade = models.ForeignKey(ColumnTypes, on_delete=models.DB_CASCADE)
+    fk_set_null = models.ForeignKey(
+        DigitsInColumnName, on_delete=models.DB_SET_NULL, null=True
+    )

@@ -299,6 +299,27 @@ def test_foreign_key_to_field(self):
             out.getvalue(),
         )

+    [USER_REDACTED]("can_introspect_foreign_keys")
+    def test_foreign_key_db_on_delete(self):
+        out = StringIO()
+        call_command("inspectdb", "inspectdb_dbondeletemodel", stdout=out)
+        output = out.getvalue()
+        self.assertIn(
+            "fk_do_nothing = models.ForeignKey('InspectdbUniquetogether', "
+            "models.DO_NOTHING)",
+            output,
+        )
+        self.assertIn(
+            "fk_db_cascade = models.ForeignKey('InspectdbColumntypes', "
+            "models.DB_CASCADE)",
+            output,
+        )
+        self.assertIn(
+            "fk_set_null = models.ForeignKey('InspectdbDigitsincolumnname', "
+            "models.DB_SET_NULL, blank=True, null=True)",
+            output,
+        )
+
     def test_digits_column_name_introspection(self):
         """
         Introspection of column names consist/start with digits (#16536/#17676)

@@ -110,3 +110,18 @@ class DbCommentModel(models.Model):
     class Meta:
         db_table_comment = "Custom table comment"
         required_db_features = {"supports_comments"}
+
+
+class DbOnDeleteModel(models.Model):
+    fk_do_nothing = models.ForeignKey(Country, on_delete=models.DO_NOTHING)
+    fk_db_cascade = models.ForeignKey(City, on_delete=models.DB_CASCADE)
+    fk_set_null = models.ForeignKey(Reporter, on_delete=models.DB_SET_NULL, null=True)
+
+
+class DbOnDeleteSetDefaultModel(models.Model):
+    fk_db_set_default = models.ForeignKey(
+        Country, on_delete=models.DB_SET_DEFAULT, db_default=models.Value(1)
+    )
+
+    class Meta:
+        required_db_features = {"supports_on_delete_db_default"}

@@ -1,5 +1,5 @@
 from django.db import DatabaseError, connection
-from django.db.models import Index
+from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING, Index
 from django.test import TransactionTestCase, skipUnlessDBFeature

 from .models import (
@@ -10,6 +10,8 @@
     Comment,
     Country,
     DbCommentModel,
+    DbOnDeleteModel,
+    DbOnDeleteSetDefaultModel,
     District,
     Reporter,
     UniqueConstraintConditionModel,
@@ -219,10 +221,14 @@ def test_get_relations(self):
                 cursor, Article._meta.db_table
             )

-        # That's {field_name: (field_name_other_table, other_table)}
+        if connection.vendor == "mysql" and connection.mysql_is_mariadb:
+            no_db_on_delete = None
+        else:
+            no_db_on_delete = DO_NOTHING
+        # {field_name: (field_name_other_table, other_table, db_on_delete)}
         expected_relations = {
-            "reporter_id": ("id", Reporter._meta.db_table),
-            "response_to_id": ("id", Article._meta.db_table),
+            "reporter_id": ("id", Reporter._meta.db_table, no_db_on_delete),
+            "response_to_id": ("id", Article._meta.db_table, no_db_on_delete),
         }
         self.assertEqual(relations, expected_relations)

@@ -238,6 +244,38 @@ def test_get_relations(self):
             editor.add_field(Article, body)
         self.assertEqual(relations, expected_relations)

+    [USER_REDACTED]("can_introspect_foreign_keys")
+    def test_get_relations_db_on_delete(self):
+        with connection.cursor() as cursor:
+            relations = connection.introspection.get_relations(
+                cursor, DbOnDeleteModel._meta.db_table
+            )
+
+        if connection.vendor == "mysql" and connection.mysql_is_mariadb:
+            no_db_on_delete = None
+        else:
+            no_db_on_delete = DO_NOTHING
+        # {field_name: (field_name_other_table, other_table, db_on_delete)}
+        expected_relations = {
+            "fk_db_cascade_id": ("id", City._meta.db_table, DB_CASCADE),
+            "fk_do_nothing_id": ("id", Country._meta.db_table, no_db_on_delete),
+            "fk_set_null_id": ("id", Reporter._meta.db_table, DB_SET_NULL),
+        }
+        self.assertEqual(relations, expected_relations)
+
+    [USER_REDACTED]("can_introspect_foreign_keys", "supports_on_delete_db_default")
+    def test_get_relations_db_on_delete_default(self):
+        with connection.cursor() as cursor:
+            relations = connection.introspection.get_relations(
+                cursor, DbOnDeleteSetDefaultModel._meta.db_table
+            )
+
+        # {field_name: (field_name_other_table, other_table, db_on_delete)}
+        expected_relations = {
+            "fk_db_set_default_id": ("id", Country._meta.db_table, DB_SET_DEFAULT),
+        }
+        self.assertEqual(relations, expected_relations)
+
     def test_get_primary_key_column(self):
         with connection.cursor() as cursor:
             primary_key_column = connection.introspection.get_primary_key_column(

Looks great, thanks 👍

Thanks [USER_REDACTED]

I suspect it's the same on MySQL as neither support deferrable foreign key constraints which means that both `RETRICT` and `DO_NOTHING` are equivalent.

Not sure we should translate it to `DO_NOTHING` though as when we do add support do `DB_RESTRICT` the value will be swapped back to `DO_NOTHING` on introspection.

Maybe this is something we can revisit when we do add support for `DB_RESTRICT`.

> I suspect it's the same on MySQL as neither support deferrable foreign key constraints which means that both `RETRICT` and `DO_NOTHING` are equivalent.

At least on MySQL, `delete_rule` returns `NO ACTION` by default :)

> Not sure we should translate it to `DO_NOTHING` though as when we do add support do `DB_RESTRICT` the value will be swapped back to `DO_NOTHING` on introspection.

Previously we translated all options to `DO_NOTHING`.

No tests fail when I alter this to assign an empty tuple, but maybe that's fine.

```suggestion
                    on_delete_qualname = extra_params.pop("on_delete", None)
```

Do we need the late import?

True, but if we have a choice between synonyms, why wouldn't we leave it as is and avoid swapping it out for `DO_NOTHING`? Maybe a project starts by introspecting a mariadb and plans to migrate to postgres later.

If we did that, couldn't we remove the property and just make this a class constant?

> I suspect it's the same on MySQL as neither support deferrable foreign key constraints which means that both RETRICT and DO_NOTHING are equivalent.

A subtle difference is [mentioned](https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html#foreign-key-referential-actions) between InnoDB and NDB:

> [NDB](https://dev.mysql.com/doc/refman/8.0/en/mysql-cluster.html) supports deferred checks, and NO ACTION specifies a deferred check;

Do we need to mention this in the "changes that may be needed in third-party database
backends"?

Thanks for that, TIL about NDB.

I don't think we support the NDB storage engine though, at least we don't test against it and [we explicitly mark the MySQL backend as not supporting deferred constraint checks](https://github.com/search?q=repo%3Adjango%2Fdjango%20can_defer_constraint_checks&type=code).

Right, currently it's unnecessary because the only `db_on_delete` that is not `DatabaseOnDelete` is actually `DO_NOTHING`.

Nope, `django.db` could be an issue but `django.db.models` is fine.

We can remove `on_delete_types` in MySQL backend, but this requires minor adjustments in introspection tests.

Updated.

"one of" -> "one of the"


Fixed #36674 -- Prevented memory leak in select_related().

#### Trac ticket number

ticket-36674

#### Branch description
Fixed memory leak in select_related() by [NAME_REDACTED]
function get_select_from_parent to a class method.

The nested function created closure cells that caused circular references,
preventing Python's garbage collector from properly cleaning up memory.

Implemented classmethod approach as suggested by [NAME_REDACTED]
recursion handling with cls.get_select_from_parent().

#### Checklist
- [x] Commit message follows Django conventions
- [x] No regressions in select_related tests
- [ ] "Has patch" flag will be set after PR creation

@@ -228,6 +228,13 @@ def collapse_group_by(self, expressions, having):
             ]
         return expressions

+    [USER_REDACTED]
+    def get_select_from_parent(cls, klass_info):
+        for ki in klass_info["related_klass_infos"]:
+            if ki["from_parent"]:
+                ki["select_fields"] = klass_info["select_fields"] + ki["select_fields"]
+            cls.get_select_from_parent(ki)
+
     def get_select(self, with_col_aliases=False):
         """
         Return three values:
@@ -300,15 +307,7 @@ def get_select(self, with_col_aliases=False):
             related_klass_infos = self.get_related_selections(select, select_mask)
             klass_info["related_klass_infos"] = related_klass_infos

-            def get_select_from_parent(klass_info):
-                for ki in klass_info["related_klass_infos"]:
-                    if ki["from_parent"]:
-                        ki["select_fields"] = (
-                            klass_info["select_fields"] + ki["select_fields"]
-                        )
-                    get_select_from_parent(ki)
-
-            get_select_from_parent(klass_info)
+            self.get_select_from_parent(klass_info)

         ret = []
         col_idx = 1

@@ -1,6 +1,9 @@
+import gc
+
 from django.core.exceptions import FieldError
 from django.db.models import FETCH_PEERS
 from django.test import SimpleTestCase, TestCase
+from django.test.utils import garbage_collect

 from .models import (
     Bookmark,
@@ -57,6 +60,17 @@ def setUpTestData(cls):
             "Amanita muscaria"
         )

+    def setup_gc_debug(self):
+        self.addCleanup(gc.set_debug, 0)
+        self.addCleanup(gc.enable)
+        gc.disable()
+        garbage_collect()
+        gc.set_debug(gc.DEBUG_SAVEALL)
+
+    def assert_no_memory_leaks(self):
+        garbage_collect()
+        self.assertEqual(gc.garbage, [])
+
     def test_access_fks_without_select_related(self):
         """
         Normally, accessing FKs doesn't fill in related objects
@@ -128,6 +142,11 @@ def test_select_related_with_extra(self):
         )
         self.assertEqual(s.id + 10, s.a)

+    def test_select_related_memory_leak(self):
+        self.setup_gc_debug()
+        list(Species.objects.select_related("genus"))
+        self.assert_no_memory_leaks()
+
     def test_certain_fields(self):
         """
         The optional fields passed to select_related() control which related

Looking good, just want to ensure we leverage the existing approach that asserts against `gc.garbage`.

Thanks for the updates. I pushed small edits to test code.

If and when we reuse the util we can move it to django/test/utils.py. 👍

A context manager is the right approach, but I was expecting to factor out the guts of `test_release_memory_without_garbage_collection` and use it both here and there.


Fixed #36678 -- Propagated errors from _init_worker in ParallelTestRunner.

#### Trac ticket number
ticket-36678

#### Branch description
Before, errors during `_init_worker` were swallowed, leading to infinite retries via the multiprocessing pool. See ticket-36677 for an example of a failure that caused infinite retries.

Now, errors during `_init_worker` are raised in a way that causes the ParallelTestRunner to exit.

Errors during `_init_worker` would have been largely theoretical before ticket-36083 (6.0), but with that change, we run system checks during `_init_worker`, which can fail. The checks run once before workers are forked, so for deterministic checks, you wouldn't reach this failure point, but with flaky checks, a user might end up in an infinite retry scenario similar to ticket-36677.

#### Checklist
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [ ] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

@@ -1,6 +1,7 @@
 import argparse
 import ctypes
 import faulthandler
+import functools
 import hashlib
 import io
 import itertools
@@ -480,6 +481,16 @@ def _init_worker(
         )

+def _safe_init_worker(init_worker, counter, *args, **kwargs):
+    try:
+        init_worker(counter, *args, **kwargs)
+    except Exception:
+        with counter.get_lock():
+            # Set a value that will not increment above zero any time soon.
+            counter.value = -1000
+        raise
+
+
 def _run_subsuite(args):
     """
     Run a suite of tests with a RemoteTestRunner and return a RemoteTestResult.
@@ -553,7 +564,7 @@ def run(self, result):
         counter = multiprocessing.Value(ctypes.c_int, 0)
         pool = multiprocessing.Pool(
             processes=self.processes,
-            initializer=self.init_worker.__func__,
+            initializer=functools.partial(_safe_init_worker, self.init_worker.__func__),
             initargs=[
                 counter,
                 self.initial_settings,
@@ -580,7 +591,11 @@ def run(self, result):

             try:
                 subsuite_index, events = test_results.next(timeout=0.1)
-            except multiprocessing.TimeoutError:
+            except multiprocessing.TimeoutError as err:
+                if counter.value < 0:
+                    err.add_note("ERROR: _init_worker failed, see prior traceback")
+                    pool.close()
+                    raise
                 continue
             except StopIteration:
                 pool.close()

@@ -3,6 +3,7 @@
 """

 import collections.abc
+import functools
 import multiprocessing
 import os
 import sys
@@ -738,8 +739,10 @@ def run_suite(self, suite, **kwargs):
                     "test_runner_apps.simple.tests",
                 ]
             )
-        # Initializer must be a function.
-        self.assertIs(mocked_pool.call_args.kwargs["initializer"], _init_worker)
+        # Initializer must be a partial function binding _init_worker.
+        initializer = mocked_pool.call_args.kwargs["initializer"]
+        self.assertIsInstance(initializer, functools.partial)
+        self.assertIs(initializer.args[0], _init_worker)
         initargs = mocked_pool.call_args.kwargs["initargs"]
         self.assertEqual(len(initargs), 7)
         self.assertEqual(initargs[5], True)  # debug_mode

Looks good, though I have some concerns/questions. I may be threading the needle too finely 😛

Looks great, thank you! 🌟

I experiemented with another approach where I wrapped the body of `_init_worker` in a `try/except`, and set a flag (`init_failed`) in the except, and accessed it in the workers via a lock (shared state), but the bloating of the diff told me I was going in the wrong direction (?)

Actually, scratch that, I'll have to go back in that direction, in case user tests exceed this time limit. Bah. (EDIT: done ✔ )

I assumed this was added to avoid this error, but I'm still getting it if I force an error inside `init_worker`:
```
ResourceWarning: unclosed running multiprocessing pool <multiprocessing.pool.Pool state=RUN pool_size=16>
```

Could we add a more prominent and explanatory note here?

Is the goal of this new method to avoid indenting the original `_init_worker` one level in to add the try-except there?

Also, question: what happens if we create a `multiprocessing.Pool` of `N` threads: `t1`, `t2`, ..., `tN`, and (for example) `t2` executes super fast and crashes inside `_init_worker` before `t3` even starts initializing?

Since `_safe_init_worker` sets `counter.value = -1` on failure, any thread still initializing could then read that shared value and end up with `_worker_id = 0`. That seems unintended and could lead to duplicate or invalid worker IDs. Furthermore, it could still raise but the code handling the exception does not see the counter in `-1`.

Natalia suggested something like "see traceback above"

> Is the goal of this new method to avoid indenting the original _init_worker one level in to add the try-except there?

Exactly, keep the diff smaller and (more importantly) reduce the number of statements under a try.

> That seems unintended and could lead to duplicate or invalid worker IDs.

Thanks 🎯


Bumped isort to 7.0.0.

Alternative to #19109

Given that https://github.com/PyCQA/isort/issues/2352, is unlikely to be fixed anytime soon, I think a pragmatic approach is to just add these two formatting disables. We've done that in a few cases. Then we don't end up in the "unstable" situation described in the issue.

@@ -45,7 +45,7 @@ jobs:
         uses: actions/setup-python@v6
         with:
           python-version: '3.13'
-      - run: python -m pip install "isort<6"
+      - run: python -m pip install isort
       - name: isort
         # Pinned to v3.0.0.
         uses: liskin/gh-problem-matcher-wrap@e7b7beaaafa52524748b31a381160759d68d61fb

@@ -13,7 +13,7 @@ repos:
         files: 'docs/.*\.txt$'
         args: ["--rst-literal-block"]
   - repo: https://github.com/PyCQA/isort
-    rev: 5.13.2
+    rev: 7.0.0
     hooks:
       - id: isort
   - repo: https://github.com/PyCQA/flake8

@@ -1,10 +1,10 @@
-from django.db.models.sql.compiler import (
+from django.db.models.sql.compiler import (  # isort:skip
     SQLAggregateCompiler,
     SQLCompiler,
     SQLDeleteCompiler,
+    SQLInsertCompiler as BaseSQLInsertCompiler,
+    SQLUpdateCompiler,
 )
-from django.db.models.sql.compiler import SQLInsertCompiler as BaseSQLInsertCompiler
-from django.db.models.sql.compiler import SQLUpdateCompiler

 __all__ = [
     "SQLAggregateCompiler",

@@ -137,7 +137,7 @@ Imports

   .. console::

-      $ python -m pip install "isort >= 5.1.0"
+      $ python -m pip install "isort >= 7.0.0"
       $ isort .

   This runs ``isort`` recursively from your current directory, modifying any

@@ -83,7 +83,7 @@ environments can be seen as follows:
     blacken-docs
     flake8>=3.7.0
     docs
-    isort>=5.1.0
+    isort>=7.0.0
     lint-docs

 Testing other Python versions and database backends

@@ -8,7 +8,7 @@
 from django.http import Http404, HttpRequest
 from django.test import TestCase, override_settings

-from .models import (
+from .models import (  # isort:skip
     Article,
     Author,
     FooWithBrokenAbsoluteUrl,
@@ -17,9 +17,9 @@
     ModelWithM2MToSite,
     ModelWithNullFKToSite,
     SchemeIncludedURL,
+    Site as MockSite,
+    UUIDModel,
 )
-from .models import Site as MockSite
-from .models import UUIDModel

 [USER_REDACTED]_settings(ROOT_URLCONF="contenttypes_tests.urls")

@@ -75,7 +75,7 @@ commands =
 [testenv:isort]
 basepython = python3
 usedevelop = false
-deps = isort >= 5.1.0
+deps = isort >= 7.0.0
 changedir = {toxinidir}
 commands = isort --check-only --diff django tests scripts

Added some thoughts, thank you!

LGTM 🌟

(I imagine this is where this import first was before isort tried to alphabetize it.)

If we are going down this path (isort skip), I would rather we do:

```python
from django.db.models.sql.compiler import (
    SQLAggregateCompiler,
    SQLCompiler,
    SQLDeleteCompiler,
    SQLInsertCompiler as BaseSQLInsertCompiler,
    SQLUpdateCompiler,
)
```

As before, if we are skipping, I would rather:
```python
from .models import (
    Article,
    Author,
    FooWithBrokenAbsoluteUrl,
    FooWithoutUrl,
    FooWithUrl,
    ModelWithM2MToSite,
    ModelWithNullFKToSite,
    SchemeIncludedURL,
    Site as MockSite,
    UUIDModel,
)
```

No problem, I prefer that also. 👍


Refs #35844 -- [NAME_REDACTED] 3.14 compatibility.

@@ -29,7 +29,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'docs/requirements.txt'
       - run: python -m pip install -r docs/requirements.txt
@@ -47,7 +47,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
       - run: python -m pip install blacken-docs
       - name: Build docs
         run: |
@@ -68,7 +68,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
       - run: python -m pip install sphinx-lint
       - name: Build docs
         run: |

@@ -27,7 +27,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
       - run: python -m pip install flake8
       - name: flake8
         # Pinned to v3.0.0.
@@ -44,7 +44,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
       - run: python -m pip install isort
       - name: isort
         # Pinned to v3.0.0.

@@ -42,7 +42,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Update apt repo

@@ -18,7 +18,7 @@ jobs:
         python-version:
           - '3.12'
           - '3.13'
-          - '3.14-dev'
+          - '3.14'
     name: Windows, SQLite, Python ${{ matrix.python-version }}
     continue-on-error: true
     steps:
@@ -45,7 +45,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
       - name: Install libmemcached-dev for pylibmc
         run: sudo apt-get install libmemcached-dev
@@ -86,7 +86,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install libmemcached-dev for pylibmc
@@ -122,7 +122,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install libmemcached-dev for pylibmc
@@ -167,7 +167,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install libmemcached-dev for pylibmc

@@ -24,7 +24,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install and upgrade packaging tools

@@ -24,7 +24,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install libmemcached-dev for pylibmc
@@ -61,7 +61,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v6
         with:
-          python-version: '3.13'
+          python-version: '3.14'
           cache: 'pip'
           cache-dependency-path: 'tests/requirements/py3.txt'
       - name: Install libmemcached-dev for pylibmc

@@ -23,7 +23,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.13'
+          - '3.14'
     name: Windows, SQLite, Python ${{ matrix.python-version }}
     steps:
       - name: Checkout

@@ -2,7 +2,7 @@
 How to install Django on Windows
 ================================

-This document will guide you through installing Python 3.13 and Django on
+This document will guide you through installing Python 3.14 and Django on
 Windows. It also provides instructions for setting up a virtual environment,
 which makes it easier to work on Python projects. This is meant as a beginner's
 guide for users working on Django projects and does not reflect how Django
@@ -18,7 +18,7 @@ [NAME_REDACTED]
 ==============

 Django is a Python web framework, thus requiring Python to be installed on your
-machine. At the time of writing, Python 3.13 is the latest version.
+machine. At the time of writing, Python 3.14 is the latest version.

 To install Python on your machine go to https://www.python.org/downloads/. The
 website should offer you a download button for the latest Python version.

@@ -235,6 +235,7 @@ this. For a small app like polls, this process isn't too difficult.
            "[NAME_REDACTED] :: Python :: 3 :: Only",
            "[NAME_REDACTED] :: Python :: 3.12",
            "[NAME_REDACTED] :: Python :: 3.13",
+           "[NAME_REDACTED] :: Python :: 3.14",
            "Topic :: Internet :: WWW/HTTP",
            "Topic :: Internet :: WWW/HTTP :: [NAME_REDACTED]",
        ]

@@ -4,7 +4,8 @@ Django 5.2.8 release notes

 *[NAME_REDACTED] 5, 2025*

-Django 5.2.8 fixes several bugs in 5.2.7.
+Django 5.2.8 fixes several bugs in 5.2.7 and adds compatibility with Python
+3.14.

 Bugfixes
 ========

@@ -23,8 +23,9 @@ end in April 2026.
 Python compatibility
 ====================

-Django 5.2 supports Python 3.10, 3.11, 3.12, and 3.13. We **highly recommend**
-and only officially support the latest release of each series.
+Django 5.2 supports Python 3.10, 3.11, 3.12, 3.13, and 3.14 (as of 5.2.8). We
+**highly recommend** and only officially support the latest release of each
+series.

 .. _whats-new-5.2:

@@ -18,8 +18,8 @@ project.
 Python compatibility
 ====================

-Django 6.0 supports Python 3.12 and 3.13. We **highly recommend**, and only
-officially support, the latest release of each series.
+Django 6.0 supports Python 3.12, 3.13, and 3.14. We **highly recommend**, and
+only officially support, the latest release of each series.

 The Django 5.2.x series is the last to support Python 3.10 and 3.11.

@@ -29,6 +29,7 @@ classifiers = [
     "[NAME_REDACTED] :: Python :: 3 :: Only",
     "[NAME_REDACTED] :: Python :: 3.12",
     "[NAME_REDACTED] :: Python :: 3.13",
+    "[NAME_REDACTED] :: Python :: 3.14",
     "Topic :: Internet :: WWW/HTTP",
     "Topic :: Internet :: WWW/HTTP :: [NAME_REDACTED]",
     "Topic :: Internet :: WWW/HTTP :: WSGI",

@@ -41,10 +41,7 @@ def test01_wktreader(self):
     def test02_wktwriter(self):
         # Creating a WKTWriter instance, testing its ptr property.
         wkt_w = WKTWriter()
-        msg = (
-            "Incompatible pointer type: "
-            "<class 'django.contrib.gis.geos.prototypes.io.LP_WKTReader_st'>."
-        )
+        msg = "Incompatible pointer type: "
         with self.assertRaisesMessage(TypeError, msg):
             wkt_w.ptr = WKTReader.ptr_type()

@@ -258,7 +258,7 @@ def test_nonascii_as_string_with_ascii_charset(self, mock_set_payload):
         `surrogateescape`.

         Following https://github.com/[USER_REDACTED]n/cpython/issues/76511, newer
-        versions of Python (3.12.3 and 3.13) ensure that a message's
+        versions of Python (3.12.3 and 3.13+) ensure that a message's
         payload is encoded with the provided charset and `surrogateescape` is
         used as the error handling strategy.

@@ -7,7 +7,7 @@ docutils >= 0.19
 geoip2 >= 4.8.0
 jinja2 >= 2.11.0
 numpy >= 1.26.0
-Pillow >= 10.1.0; sys.platform != 'win32' or python_version < '3.14'
+Pillow >= 10.1.0
 # pylibmc/libmemcached can't be built on Windows.
 pylibmc; sys_platform != 'win32'
 pymemcache >= 3.4.0

@@ -27,7 +27,7 @@ setenv =
     PYTHONDONTWRITEBYTECODE=1
 deps =
     -e .
-    py{3,312,313}: -rtests/requirements/py3.txt
+    py{3,312,313,314}: -rtests/requirements/py3.txt
     postgres: -rtests/requirements/postgres.txt
     mysql: -rtests/requirements/mysql.txt
     oracle: -rtests/requirements/oracle.txt

Terrific.

Is this working fine with `win32` now? I couldn't catch why that condition was removed.

Yeah, this looks like a mistake.

All, PRs are tested with Windows, e.g.
https://github.com/[USER_REDACTED]ngo/django/actions/runs/18599008742/job/53032590585?pr=19964

I think the restriction was added defensively during a very early phase of testing against 3.14 alphas, and wheels for windows may not have been available yet. (48fa531) Removing it if possible was always the plan.

Thank you for the info. I think this deserves its own commit and/or a mention in the commit message, but I guess it's too late now.


Skipped GISFunctionsTests.test_geometry_type() test for MultiPoint on MariaDB and GEOS 3.12+.

`GEOSWKTWriter_write()` behavior was changed in GEOS 3.12+ to include parentheses for sub-members (https://github.com/libgeos/geos/pull/903).

MariaDB doesn't accept WKT representations with additional parentheses for `MultiPoint`. This is an accepted bug ([MDEV-36166](https://jira.mariadb.org/browse/MDEV-36166)) in MariaDB that should be fixed in the future.

@@ -14,6 +14,7 @@
     Polygon,
     fromstr,
 )
+from django.contrib.gis.geos.libgeos import geos_version_tuple
 from django.contrib.gis.measure import Area
 from django.db import NotSupportedError, connection
 from django.db.models import IntegerField, Sum, Value
@@ -916,39 +917,46 @@ def test_argument_validation(self):

     [USER_REDACTED]("has_GeometryType_function")
     def test_geometry_type(self):
-        Feature.objects.bulk_create(
-            [
-                Feature(name="Point", geom=Point(0, 0)),
-                Feature(name="LineString", geom=LineString((0, 0), (1, 1))),
-                Feature(name="Polygon", geom=Polygon(((0, 0), (1, 0), (1, 1), (0, 0)))),
-                Feature(name="MultiPoint", geom=MultiPoint(Point(0, 0), Point(1, 1))),
-                Feature(
-                    name="MultiLineString",
-                    geom=MultiLineString(
-                        LineString((0, 0), (1, 1)), LineString((1, 1), (2, 2))
-                    ),
+        test_features = [
+            Feature(name="Point", geom=Point(0, 0)),
+            Feature(name="LineString", geom=LineString((0, 0), (1, 1))),
+            Feature(name="Polygon", geom=Polygon(((0, 0), (1, 0), (1, 1), (0, 0)))),
+            Feature(
+                name="MultiLineString",
+                geom=MultiLineString(
+                    LineString((0, 0), (1, 1)), LineString((1, 1), (2, 2))
                 ),
-                Feature(
-                    name="MultiPolygon",
-                    geom=MultiPolygon(
-                        Polygon(((0, 0), (1, 0), (1, 1), (0, 0))),
-                        Polygon(((1, 1), (2, 1), (2, 2), (1, 1))),
-                    ),
+            ),
+            Feature(
+                name="MultiPolygon",
+                geom=MultiPolygon(
+                    Polygon(((0, 0), (1, 0), (1, 1), (0, 0))),
+                    Polygon(((1, 1), (2, 1), (2, 2), (1, 1))),
                 ),
-            ]
-        )
-
-        expected_results = {
+            ),
+        ]
+        expected_results = [
             ("POINT", Point),
             ("LINESTRING", LineString),
             ("POLYGON", Polygon),
-            ("MULTIPOINT", MultiPoint),
             ("MULTILINESTRING", MultiLineString),
             ("MULTIPOLYGON", MultiPolygon),
-        }
-
-        for geom_type, geom_class in expected_results:
-            with self.subTest(geom_type=geom_type):
+        ]
+        # GEOSWKTWriter_write() behavior was changed in GEOS 3.12+ to include
+        # parentheses for sub-members. MariaDB doesn't accept WKT
+        # representations with additional parentheses for MultiPoint. This is
+        # an accepted bug (MDEV-36166) in MariaDB that should be fixed in the
+        # future.
+        if not connection.ops.mariadb or geos_version_tuple() < (3, 12):
+            test_features.append(
+                Feature(name="MultiPoint", geom=MultiPoint(Point(0, 0), Point(1, 1)))
+            )
+            expected_results.append(("MULTIPOINT", MultiPoint))
+        for test_feature, (geom_type, geom_class) in zip(
+            test_features, expected_results, strict=True
+        ):
+            with self.subTest(geom_type=geom_type, geom=test_feature.geom.wkt):
+                test_feature.save()
                 obj = (
                     Feature.objects.annotate(
                         geometry_type=functions.GeometryType("geom")

Thanks for the sleuthing and debugging session [USER_REDACTED] 🙇

TIL that we set vendor name as booleans on `BaseSpatialOperations` instead of using features per-backend instead (I was expecting this to raise `AttributeError` or non-MySQL based backends).


Fixed #36648, Refs #33772 -- Accounted for composite pks in first()/last() when aggregating.

#### Trac ticket number
ticket-36648

#### Branch description
Before, `.first()` and `.last()` checked if the primary key was already in the group by [NAME_REDACTED], otherwise it threw a `TypeError`. (See ticket-33772).

Now, if you have a composite primary key, you can also provide each column individually in a `values()` call instead of needing to use the `"pk"` syntax.

#### Checklist
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [x] I have added or updated relevant docs, including release notes if applicable.
- [x] I have attached screenshots in both light and dark modes for any UI changes.

@@ -2141,8 +2141,14 @@ def _check_operator_queryset(self, other, operator_):
             raise TypeError(f"Cannot use {operator_} operator with combined queryset.")

     def _check_ordering_first_last_queryset_aggregation(self, method):
-        if isinstance(self.query.group_by, tuple) and not any(
-            col.output_field is self.model._meta.pk for col in self.query.group_by
+        if (
+            isinstance(self.query.group_by, tuple)
+            # Raise if the pk fields are not in the group_by.
+            and self.model._meta.pk
+            not in {col.output_field for col in self.query.group_by}
+            and set(self.model._meta.pk_fields).difference(
+                {col.target for col in self.query.group_by}
+            )
         ):
             raise TypeError(
                 f"Cannot use QuerySet.{method}() on an unordered queryset performing "

@@ -10,3 +10,7 @@ Bugfixes
 ========

 * Added compatibility for ``oracledb`` 3.4.0 (:ticket:`36646`).
+
+* Fixed a bug in Django 5.2 where ``QuerySet.first()`` and ``QuerySet.last()``
+  raised an error on querysets performing aggregation that selected all fields
+  of a composite primary key.

@@ -141,3 +141,23 @@ def test_max_pk(self):
         msg = "Max expression does not support composite primary keys."
         with self.assertRaisesMessage(ValueError, msg):
             Comment.objects.aggregate(Max("pk"))
+
+    def test_first_from_unordered_queryset_aggregation_pk_selected(self):
+        self.assertEqual(
+            Comment.objects.values("pk").annotate(max=Max("id")).first(),
+            {"pk": (1, 1), "max": 1},
+        )
+
+    def test_first_from_unordered_queryset_aggregation_pk_selected_separately(self):
+        self.assertEqual(
+            Comment.objects.values("tenant", "id").annotate(max=Max("id")).first(),
+            {"tenant": 1, "id": 1, "max": 1},
+        )
+
+    def test_first_from_unordered_queryset_aggregation_pk_incomplete(self):
+        msg = (
+            "Cannot use QuerySet.first() on an unordered queryset performing "
+            "aggregation. Add an ordering with order_by()."
+        )
+        with self.assertRaisesMessage(TypeError, msg):
+            Comment.objects.values("tenant").annotate(max=Max("id")).first()

Looks great, thank you! 🌟

I added some optional suggestions for your evaluation.

Could we avoid the extra indentation level and nested `if`, since all the checks need to hold as an `and`?

```suggestion
        if (
            isinstance(self.query.group_by, tuple)
            # Raise if the pk fields are not in the group_by.
            and self.model._meta.pk
            not in {col.output_field for col in self.query.group_by}
            and set(self.model._meta.pk_fields).difference(
                {col.target for col in self.query.group_by}
            )
        ):
            raise TypeError(
                f"Cannot use QuerySet.{method}() on an unordered queryset performing "
                "aggregation. Add an ordering with order_by()."
            )
```

This was a little hard for me to understand, what do you think of something along these lines:

```suggestion
* Fixed a bug in Django 5.2 where ``QuerySet.first()`` and ``QuerySet.last()``
  raised an error on unordered aggregated querysets that selected all fields of
  a composite primary key.
```

I like it -- I might just keep 'querysets performing aggregation' rather than 'aggregated querysets' which isn't clearer for me.


Added notes about automatic roadmap generation for next version in docs/internals/howto-release-django.txt.

This is a follow up of work done in https://github.com/[USER_REDACTED]ngo/djangoproject.com/pull/2233.

@@ -355,11 +355,15 @@ Feature freeze tasks
    ``django_next_version`` to ``'6.0'`` on the new stable branch
    (:commit:`example commit <1eb62e5b622ef7fd6e0123d8bbf6662d893d5d08>`).

-#. Go to the `Add release page in the admin`__, create a ``Release`` object for
-   the *final* release, ensuring that the *is active* field is unset, thus
-   marking it as *unreleased*. For example, when creating ``stable/5.2.x``,
-   create ``5.2`` with the active field unset. If the release is part of
-   an LTS branch, mark it so.
+#. Create ``Release`` entries for the next version in the `admin`__ on
+   ``djangoproject.com``. Add one for each milestone (alpha, beta, rc, and
+   final), leaving *is active* unset to mark them as unreleased. Set target
+   dates per the agreed schedule, and set the LTS flag if applicable. The
+   ``X.Y`` roadmap page will be available at ``/download/X.Y/roadmap/``.
+
+   For example, when creating ``stable/5.2.x``, add ``Release`` entries for
+   ``6.0a1``, ``6.0b1``, ``6.0rc1``, and ``6.0``. The ``6.0`` roadmap can be
+   then reviewed at https://www.djangoproject.com/download/6.0/roadmap/.

    __ https://www.djangoproject.com/admin/releases/release/add/

@@ -380,12 +384,6 @@ Feature freeze tasks
    <https://github.com/pypa/trove-classifiers/pulls?q=is%3Apr+django+trove+classifier>`_.
    For example ``Framework :: Django :: 5.2``.

-#. Create a `roadmap page
-   <https://code.djangoproject.com/wiki/Version6.0Roadmap>`_ for the next
-   release on Trac. To create a new page on the Wiki, navigate to the URL of
-   where you wish to create the page and a "Create this page" button will be
-   available.
-
 #. Update the current branch under active development and add pre-release
    branch in the `Django release process
    <https://code.djangoproject.com/#Djangoreleaseprocess>`_ on Trac.

Great, just some teeny copyedits to consider.

```suggestion
   dates per the agreed schedule, and set the LTS flag if applicable. The
```

```suggestion
   ``X.Y`` roadmap page will be available at ``/download/X.Y/roadmap/``.
```

Most of this section uses the 5.2 freeze as an example, would be nice to avoid jumping to another example.

Applying, thanks!

Good point, will do that, thanks!


Fixed #31506 -- [NAME_REDACTED] and timedelta behavior with ExpressionWrapper.

Add warning in ExpressionWrapper documentation about date/datetime type differences when using timedelta on PostgreSQL and MySQL. [NAME_REDACTED]() and integer arithmetic solutions.

#### Trac ticket number
<!-- Replace XXXXX with the corresponding Trac ticket number, or delete the line and write "N/A" if this is a trivial PR. -->

ticket-31506

#### Branch description
It documents the behavioral differences across database backends when using DateField with timedelta inside an ExpressionWrapper.

To address the issue where arithmetic operations on date/time fields in PostgreSQL and MySQL can return a datetime type instead of a date, a warning has been added to the ExpressionWrapper documentation. It also provides solutions, such as using the Cast() function, to help developers avoid unintended type mismatches.

#### Checklist
- [X] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [X] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [X] I have checked the "Has patch" ticket flag in the Trac system.
- [X] I have added or updated relevant tests.
- [X] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

@@ -37,6 +37,12 @@ determine the result's field type, such as complex expressions that mix field
 types. For example, adding a ``DecimalField()`` and a ``FloatField()`` requires
 an output field, like ``output_field=FloatField()``.

+``output_field`` also allows using custom fields that perform type conversions
+outside a specific model field context. For example, if you frequently need to
+perform date arithmetic with ``timedelta``, you can create a custom field that
+handles the conversion, ensuring consistent results across databases. See
+:doc:`/howto/custom-model-fields`.
+
 Some examples
 =============

@@ -575,6 +581,12 @@ available on other expressions. ``ExpressionWrapper`` is necessary when using
 arithmetic on ``F()`` expressions with different types as described in
 :ref:`using-f-with-annotations`.

+.. admonition:: Database casting not performed
+
+    ``ExpressionWrapper`` only sets the output field for the ORM and does not
+    perform any database-level casting. To ensure a specific type is returned
+    from the database, use :class:`~django.db.models.functions.Cast` instead.
+
 ``JSONNull()`` expression
 -------------------------

@@ -831,6 +831,39 @@ exclusive. Any combination of these options will result in an error.
     instance, removing its time component. This is true for both storage and
     comparison.

+.. warning::
+
+    On PostgreSQL and MySQL, arithmetic operations on a ``DateField`` with a
+    :class:`~datetime.timedelta` return a ``datetime`` instead of a ``date``.
+    This occurs because Python's ``timedelta`` is converted to SQL
+    ``INTERVAL``, and the SQL operation ``date +/- interval`` returns a
+    ``timestamp`` on these databases.
+
+    To ensure a ``date`` result, use one of the following approaches. Either
+    explicitly cast the result to a date::
+
+        import datetime
+        from django.db.models import DateField, F
+        from django.db.models.functions import Cast
+
+        qs = MyModel.objects.annotate(
+            previous_day=Cast(
+                F("date_field") - datetime.timedelta(days=1),
+                output_field=DateField(),
+            )
+        )
+
+    Or on PostgreSQL only, use integer arithmetic to represent days::
+
+        from django.db.models import DateField, ExpressionWrapper, F
+
+        qs = MyModel.objects.annotate(
+            previous_day=ExpressionWrapper(
+                F("date_field") - 1,  # Subtract 1 day as integer
+                output_field=DateField(),
+            )
+        )
+
 ``DateTimeField``
 -----------------

Thanks for the PR! Will be great to add help here to prevent paper cuts. 👍

A concern I have is that adding this level of detail to the `ExpressionWrapper` section could reinforce a misunderstanding about what it does.

The problem surfaces when using simpler expressions like `F()`, as in your example. The reporter expected this problem to be solved via `ExpressionWrapper`, but that was a misunderstanding. `ExpressionWrapper` only helps you set `output_field`, which is an ORM concept[0], not a database concept, on expressions that don't have `output_field`. If we are going to add admonitions to a tool's docs about when *not* to use that tool, I'd prefer the advice be general instead of highly specialized to date fields.

The advice you present here to use `Cast` or integer arithmetic or custom fields is correct, but we could present that in the `DateField` docs instead, where there are similar warnings about other paper cuts, rather than detouring into `ExpressionWrapper`.

That said, I do think we should clarify in the `ExpressionWrapper` docs that it doesn't do any casting in the database.

So, to pull it all together, my recommendation is to:
- clarify in `ExpressionWrapper` how it differs from `Cast`
- move the advice about creating a custom model field to the docs for `output_field`, where it suggests `output_field` is "only required when Django is unable to automatically determine the result’s field type". That's true, but it is *also helpful* in cases where you want to encapsulate the python-side casting behavior, which you can do with custom fields, which you point out here very well.
- move the admonition about postgres/mysql datefield arithmetic behavior to the `DateField` admonitions, keeping your integer arithmetic alternative example

Thanks again, and welcome aboard! ⛵

***
[0] - The fact that in at least one case `output_field` actually [entails a database cast](https://github.com/[USER_REDACTED]ngo/django/pull/19459#discussion_r2135725928) is a bug that people are likely relying on!

Thank you, updates look great, just some last edits. Also mind fixing the merge conflicts?

Thank you [USER_REDACTED]! Should be good to go once the missing backticks and line lengths lints are fixed 👍

Yay, checks are passing now. I gave this a final read and noticed one slight inconsistency, can you let me know if you agree with this suggestion?

Great, the latest rounds of changes read perfectly. Thanks for being so responsive. Welcome aboard! ⛵

I think this can just be a documentation PR. The tests are date field specific, but this file tests the general behavior of expressions.

In general we don't add tests that memorialize database quirks. (If we did, we'd want to include mysql also.)

I generated the docs locally and I can't see this header nor the other one below.

This is one of the errors marked [in CI](https://github.com/[USER_REDACTED]ngo/django/actions/runs/19323334567/job/55268931679?pr=19947#step:5:15)

I think it's not possible to include subheaders inside admonitions, [per Sphinx](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#admonitions-messages-and-warnings).

Lines must not be more than 79 characters long. I believe you have to fix this manually.

[CI error](https://github.com/[USER_REDACTED]ngo/django/actions/runs/19323334567/job/55268931723?pr=19947#step:5:14)

I understood! Thanks:)

We avoid bare `.. note::` directives, preferring instead something like `.. admonition:: Database casting not performed`.

"Cast" might sound like database cast(), which we should avoid implying.
```suggestion
with ``timedelta``, you can create a custom field that handles the conversion.
```

There is probably a simpler wording here than "encapsulating". Maybe:

``output_field`` also allows using custom fields that perform type conversions
outside a specific model field context.

or similar

```suggestion
See :doc:`/howto/custom-model-fields`.
```

Order this import first.

Unlike admonition, this renders in the body of the directive, so let's remove it.
```suggestion
.. warning::
```

You could now rephrase this sentence to be tighter like the heading that's now gone. "On PostgreSQL and MySQL, arithmetic operations on :class:`DateField` return a :class:`~datetime ... rather than ..."

This says "all databases", but the second option applies to PostgreSQL only.

Maybe we can just cut down the headers.

Something like the following

```suggestion
    Either explicitly cast the result to a date::
```

```suggestion
    Or on PostgreSQL only, use integer arithmetic to represent days::
```

Oh, you're right:) I think I missed the details.

[USER_REDACTED] Thanks for the suggestion! I agree "all databases" should be removed
to avoid the contradiction.

However, I think we should keep the
"To ensure a ``date`` result, use one of these approaches:" line.

Keeping this sentence would explicitly connect two solutions to the problem described above.

I like it.

I’ve applied the changes. Thanks for the review! 😊


Fixed #36660 -- Fixed a regression in descending Index local field checks.

#### Trac ticket number
<!-- Replace XXXXX with the corresponding Trac ticket number, or delete the line and write "N/A" if this is a trivial PR. -->

ticket-36660

#### Branch description

Regression in 8638d8bf74c1a58302c97d4436ad2eb08438145b.

Refs ticket-36273.

Thanks [USER_REDACTED] for the report.

@@ -114,7 +114,12 @@ def check(self, model, connection):
                 )
         errors.extend(
             model._check_local_fields(
-                {*self.fields, *self.include, *references}, "indexes"
+                {
+                    *[field for field, _ in self.fields_orders],
+                    *self.include,
+                    *references,
+                },
+                "indexes",
             )
         )
         # Database-feature checks:

@@ -185,6 +185,15 @@ class Meta:
             ],
         )

+    def test_pointing_to_desc_field(self):
+        class Model(models.Model):
+            name = models.CharField(max_length=100)
+
+            class Meta:
+                indexes = [models.Index(fields=["-name"], name="index_name")]
+
+        self.assertEqual(Model.check(databases=self.databases), [])
+
     def test_pointing_to_m2m_field(self):
         class Model(models.Model):
             m2m = models.ManyToManyField("self")

single line?


Fixed #36656 -- Avoided truncating async streaming responses in GZipMiddleware.

#### Trac ticket number

ticket-36656

#### Branch description

Fix the issue reported in the ticket. The test adjustment makes `test_compress_async_streaming_response` fail before the fix, and pass after.

#### Checklist
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [x] I have added or updated relevant docs, including release notes if applicable.
- [x] I have attached screenshots in both light and dark modes for any UI changes.

@@ -1,7 +1,7 @@
 from django.utils.cache import patch_vary_headers
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.regex_helper import _lazy_re_compile
-from django.utils.text import compress_sequence, compress_string
+from django.utils.text import acompress_sequence, compress_sequence, compress_string

 re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")

@@ -32,18 +32,10 @@ def process_response(self, request, response):

         if response.streaming:
             if response.is_async:
-                # pull to lexical scope to capture fixed reference in case
-                # streaming_content is set again later.
-                original_iterator = response.streaming_content
-
-                async def gzip_wrapper():
-                    async for chunk in original_iterator:
-                        yield compress_string(
-                            chunk,
-                            max_random_bytes=self.max_random_bytes,
-                        )
-
-                response.streaming_content = gzip_wrapper()
+                response.streaming_content = acompress_sequence(
+                    response.streaming_content,
+                    max_random_bytes=self.max_random_bytes,
+                )
             else:
                 response.streaming_content = compress_sequence(
                     response.streaming_content,

@@ -393,6 +393,22 @@ def compress_sequence(sequence, *, max_random_bytes=None):
     yield buf.read()

+async def acompress_sequence(sequence, *, max_random_bytes=None):
+    buf = StreamingBuffer()
+    filename = _get_random_filename(max_random_bytes) if max_random_bytes else None
+    with GzipFile(
+        filename=filename, mode="wb", compresslevel=6, fileobj=buf, mtime=0
+    ) as zfile:
+        # Output headers...
+        yield buf.read()
+        async for item in sequence:
+            zfile.write(item)
+            data = buf.read()
+            if data:
+                yield data
+    yield buf.read()
+
+
 # Expression to match some_token and some_token="with spaces" (and similarly
 # for single-quoted strings).
 smart_split_re = _lazy_re_compile(

@@ -2,6 +2,7 @@
 import random
 import re
 import struct
+import zlib
 from io import BytesIO
 from unittest import mock
 from urllib.parse import quote
@@ -880,8 +881,8 @@ def get_response(self, request):

     [USER_REDACTED]
     def decompress(gzipped_string):
-        with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f:
-            return f.read()
+        # Use zlib to ensure gzipped_string contains exactly one gzip stream.
+        return zlib.decompress(gzipped_string, zlib.MAX_WBITS | 16)

     [USER_REDACTED]
     def get_mtime(gzipped_string):

Is this blocked on ticket-36293? Without flushing here, your sample app from the ticket blocks. Or does this have independent value?

This change has independent value because it fixes response correctness, even if they are buffered.

Great, thought so, just checking 👍


Fixed #36622 -- [NAME_REDACTED] storages from evaluating at boot time.

#### Trac ticket number
<!-- Replace XXXXX with the corresponding Trac ticket number, or delete the line and write "N/A" if this is a trivial PR. -->

ticket-36622

#### Checklist
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

@@ -248,7 +248,7 @@ def __init__(
     ):
         self._primary_key_set_explicitly = "primary_key" in kwargs

-        self.storage = storage or default_storage
+        self.storage = storage if storage is not None else default_storage
         if callable(self.storage):
             # Hold a reference to the callable for deconstruct().
             self._storage_callable = self.storage

@@ -11,6 +11,7 @@

 from django.core.files.storage import FileSystemStorage, default_storage
 from django.db import models
+from django.utils.functional import LazyObject

 class CustomValidNameStorage(FileSystemStorage):
@@ -37,6 +38,11 @@ def __call__(self):
         return self

+class LazyTempStorage(LazyObject):
+    def _setup(self):
+        self._wrapped = temp_storage
+
+
 class Storage(models.Model):
     def custom_upload_to(self, filename):
         return "foo"
@@ -82,3 +88,4 @@ def pathlib_upload_to(self, filename):
     extended_length = models.FileField(
         storage=temp_storage, upload_to="tests", max_length=1024
     )
+    lazy_storage = models.FileField(storage=LazyTempStorage(), upload_to="tests")

@@ -29,6 +29,7 @@
 from django.urls import NoReverseMatch, reverse_lazy
 from django.utils import timezone
 from django.utils._os import symlinks_supported
+from django.utils.functional import empty

 from .models import (
     Storage,
@@ -1267,3 +1268,11 @@ def test_nonexistent_backend(self):
         )
         with self.assertRaisesMessage(InvalidStorageError, msg):
             test_storages["invalid_backend"]
+
+
+class StorageLazyObjectTests(SimpleTestCase):
+    def test_lazy_object_is_not_evaluated_before_manual_access(self):
+        obj = Storage()
+        self.assertIs(obj.lazy_storage.storage._wrapped, empty)
+        # assertEqual triggers resolution.
+        self.assertEqual(obj.lazy_storage.storage, temp_storage)

Looks great, thank you [USER_REDACTED]! 🌟

Added two small comments, LMKWYT.

Thank you much!

Do we need this module level definition? Could we instead just instantiate in the usage below?
```python
lazy_storage = models.FileField(storage=LazyTempStorage(), upload_to="tests")
```

Small nitpick:
```suggestion
class StorageLazyObjectTests(SimpleTestCase):
```


Fixed #35095 --  Cleaned up discrepancy in Swiss number formatting documentation.

#### Trac ticket number

ticket-35095

#### Branch description

Changes are based on previous PR https://github.com/[USER_REDACTED]ngo/django/pull/19396.

Clean up discrepancy between Swiss number formatting documentation and code comments.

Document context-specific formatting limitations:

    Monetary: Fr. 23.50
    Measurements: 22,5 m

Add "French" to the documentation heading.

#### Checklist
- [x] This PR targets the `main` branch.
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [ ] I have added or updated relevant tests.
- [x] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

@@ -97,6 +97,7 @@ answer newbie questions, and generally made Django that much better:
     [NAME_EMAIL_REDACTED]
     [NAME_EMAIL_REDACTED]
     [EMAIL_REDACTED]
+    [NAME_REDACTED] Kääriäinen <[EMAIL_REDACTED]>
     [EMAIL_REDACTED]
     [NAME_EMAIL_REDACTED]

@@ -25,11 +25,9 @@
     "%d.%m.%Y %H:%M",  # '25.10.2006 14:30'
 ]

-# these are the separators for non-monetary numbers. For monetary numbers,
-# the DECIMAL_SEPARATOR is a . (decimal point) and the THOUSAND_SEPARATOR is a
-# ' (single quote).
-# For details, please refer to the documentation and the following link:
-# https://www.bk.admin.ch/bk/de/home/dokumentation/sprachen/hilfsmittel-textredaktion/schreibweisungen.html
+# Swiss number formatting can vary based on context (e.g. Fr. 23.50 vs 22,5 m).
+# Django does not support context-specific formatting and uses generic
+# separators.
 DECIMAL_SEPARATOR = ","
 THOUSAND_SEPARATOR = "\xa0"  # non-breaking space
 NUMBER_GROUPING = 3

@@ -27,6 +27,10 @@
     "%d/%m/%Y %H:%M:%S.%f",  # '25/10/2006 14:30:59.000200'
     "%d/%m/%Y %H:%M",  # '25/10/2006 14:30'
 ]
+
+# Swiss number formatting can vary based on context (e.g. Fr. 23.50 vs 22,5 m).
+# Django does not support context-specific formatting and uses generic
+# separators.
 DECIMAL_SEPARATOR = ","
 THOUSAND_SEPARATOR = "\xa0"  # non-breaking space
 NUMBER_GROUPING = 3

@@ -189,12 +189,13 @@ Limitations of the provided locale formats
 Some locales use context-sensitive formats for numbers, which Django's
 localization system cannot handle automatically.

-Switzerland (German)
---------------------
-
-The Swiss number formatting depends on the type of number that is being
-formatted. For monetary values, a comma is used as the thousand separator and
-a decimal point for the decimal separator. For all other numbers, a comma is
-used as decimal separator and a space as thousand separator. The locale format
-provided by [NAME_REDACTED] separators, a comma for decimal and a space
-for thousand separators.
+Switzerland (German, French)
+----------------------------
+
+The Swiss number formatting traditionally varies depending on context. For
+example, monetary values may use a dot as decimal separator (``Fr. 23.50``),
+while measurements often use a comma (``22,5 m``). Django’s localization system
+does not support such context-specific variations automatically.
+
+The locale format provided by [NAME_REDACTED] separators, a comma for
+decimal and a space for thousand separators.

Nice one [USER_REDACTED], your help here is much appreciated.

```suggestion
The locale format provided by [NAME_REDACTED] separators: a comma for
```

Did you come to a conclusion about the usefulness of that link?

I removed the link because of your [comment:29](https://code.djangoproject.com/ticket/35095#comment:29) suggesting it, which convinced me. Also, the document is in German. In English, a [styleguide for English-language translators](https://www.bk.admin.ch/bk/en/home/dokumentation/languages/hilfsmittel-textredaktion.html) is the only similar document available.

(closing the loop, happy to leave this as it was, I see this was preexisting 👍 )


Added missing backticks in docs/ref/models/fields.txt.

@@ -1720,8 +1720,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in

 * .. attribute:: CASCADE

-    Cascade deletes. Django emulates the behavior of the SQL constraint ON
-    DELETE CASCADE and also deletes the object containing the ForeignKey.
+    Cascade deletes. Django emulates the behavior of the SQL constraint ``ON
+    DELETE CASCADE`` and also deletes the object containing the
+    :class:`ForeignKey`.

     :meth:`.Model.delete` isn't called on related models, but the
     :data:`~django.db.models.signals.pre_delete` and
@@ -1940,9 +1941,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in

     Setting it to ``False`` does not mean you can reference a swappable model
     even if it is swapped out - ``False`` means that the migrations made
-    with this ForeignKey will always reference the exact model you specify
-    (so it will fail hard if the user tries to run with a User model you don't
-    support, for example).
+    with this :class:`ForeignKey` will always reference the exact model you
+    specify (so it will fail hard if the user tries to run with a ``User``
+    model you don't support, for example).

     If in doubt, leave it to its default of ``True``.

```suggestion
    DELETE CASCADE`` and also deletes the object containing the :class:`ForeignKey`.
```

```suggestion
    with this :class:`ForeignKey` will always reference the exact model you specify
```


🌐 Add Russian translation for `docs/ru/docs/fastapi-cli.md`

New translation added: docs/ru/docs/fastapi-cli.md
[USER_REDACTED], [USER_REDACTED], [USER_REDACTED] please leave a look if you have time.

Thanks! :blush:

@@ -0,0 +1,75 @@
+# FastAPI CLI
+
+**FastAPI CLI** это программа командной строки, которую вы можете использовать для запуска вашего FastAPI приложения, для управления FastAPI-проектом, а также для многих других вещей.
+
+`fastapi-cli` устанавливается вместе со стандартным пакетом FastAPI (при запуске команды `pip install "fastapi[standard]"`). Данный пакет предоставляет доступ к программе `fastapi` через терминал.
+
+Чтобы запустить приложение FastAPI в режиме разработки, вы можете использовать команду `fastapi dev`:
+
+

+
+```console
+$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u>
+
+  <span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span>  Starting development server 🚀
+
+             Searching for package file structure from directories with
+             <font color="#3465A4">__init__.py</font> files
+             Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font>
+
+   <span style="background-color:#007166"><font color="#D3D7CF"> module </font></span>  🐍 main.py
+
+     <span style="background-color:#007166"><font color="#D3D7CF"> code </font></span>  Importing the FastAPI app object from the module with the
+             following code:
+
+             <u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u>
+
+      <span style="background-color:#007166"><font color="#D3D7CF"> app </font></span>  Using import string: <font color="#3465A4">main:app</font>
+
+   <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span>  Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font>
+   <span style="background-color:#007166"><font color="#D3D7CF"> server </font></span>  Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font>
+
+      <span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span>  Running in development mode, for production use:
+             <b>fastapi run</b>
+
+             Logs:
+
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Will watch for changes in these directories:
+             <b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b>
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to
+             quit<b>)</b>
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b>
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Waiting for application startup.
+     <span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span>  Application startup complete.
+```
+
+

+
+Приложение командной строки `fastapi` это и есть **FastAPI CLI**.
+
+FastAPI CLI берет путь к вашей Python-программе (напр. `main.py`) и автоматически находит объект `FastAPI` (обычно это `app`), затем определяет правильный процесс импорта и запускает сервер приложения.
+
+Для работы в production окружении вместо `fastapi dev` нужно использовать `fastapi run`. 🚀
+
+Внутри **FastAPI CLI** используется Uvicorn, высокопроизводительный, готовый к работе в production сервер ASGI. 😎
+
+## `fastapi dev`
+
+Вызов `fastapi dev` запускает режим разработки.
+
+По умолчанию включена автоматическая перезагрузка (**auto-reload**), благодаря этому при изменении кода происходит перезагрузка сервера приложения. Эта установка требует значительных ресурсов и делает систему менее стабильной. Используйте её только при разработке. Приложение слушает входящие подключения на IP `127.0.0.1`. Это IP адрес вашей машины, предназначенный для внутренних коммуникаций (`localhost`).
+
+## `fastapi run`
+
+Вызов `fastapi run` по умолчанию запускает FastAPI в режиме production.
+
+По умолчанию функция перезагрузки **auto-reload** отключена. Приложение слушает входящие подключения на IP `0.0.0.0`, т.е. на всех доступных адресах компьютера. Таким образом, приложение будет находиться в публичном доступе для любого, кто может подсоединиться к вашей машине. Продуктовые приложения запускаются именно так, например, с помощью контейнеров.
+
+В большинстве случаев вы будете (и должны) использовать прокси-сервер ("termination proxy"), который будет поддерживать HTTPS поверх вашего приложения. Всё будет зависеть от того, как вы развертываете приложение: за вас это либо сделает ваш провайдер, либо вам придется сделать настройки самостоятельно.
+
+/// tip | Подсказка
+
+Вы можете больше узнать об этом в документации по развертыванию приложений [deployment documentation](deployment/index.md){.internal-link target=_blank}.
+
+///

Thanks!

ок

мне чаще попадалось - "утилита командной строки"

```suggestion
Для работы в production окружении вместо `fastapi dev` нужно использовать `fastapi run`. 🚀
```

Поправка внесена :smiley:


🌐 Add Portuguese translation for `docs/pt/docs/advanced/generate-clients.md`

I had some difficulty translating sentences that used the term "payload", especially on line 252.

@@ -0,0 +1,261 @@
+# [NAME_REDACTED]
+
+Como o **FastAPI** é baseado na especificação **OpenAPI**, você obtém compatibilidade automática com muitas ferramentas, incluindo a documentação automática da API (fornecida pelo Swagger UI).
+
+Uma vantagem particular que nem sempre é óbvia é que você pode **gerar clientes** (às vezes chamados de **SDKs**) para a sua API, para muitas **linguagens de programação** diferentes.
+
+## Geradores de [NAME_REDACTED]
+
+Existem muitas ferramentas para gerar clientes a partir do **OpenAPI**.
+
+Uma ferramenta comum é o [NAME_REDACTED].
+
+Se voce está construindo um **frontend**, uma alternativa muito interessante é o openapi-ts.
+
+## Geradores de Clientes e SDKs - Patrocinadores
+
+Existem também alguns geradores de clientes e SDKs baseados na OpenAPI (FastAPI) **patrocinados por empresas**, em alguns casos eles podem oferecer **recursos adicionais** além de SDKs/clientes gerados de alta qualidade.
+
+Alguns deles também ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, isso garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**.
+
+E isso mostra o verdadeiro compromisso deles com o FastAPI e sua **comunidade** (você), pois eles não apenas querem fornecer um **bom serviço**, mas também querem garantir que você tenha um **framework bom e saudável**, o FastAPI. 🙇
+
+Por exemplo, você pode querer experimentar:
+
+* Speakeasy
+* Stainless
+* liblab
+
+Existem também várias outras empresas que oferecem serviços semelhantes que você pode pesquisar e encontrar online. 🤓
+
+## Gerar um [NAME_REDACTED]
+
+Vamos começar com um aplicativo **FastAPI** simples:
+
+{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *}
+
+Note que as *operações de rota* definem os modelos que usam para o corpo da requisição e o corpo da resposta, usando os modelos `Item` e `ResponseMessage`.
+
+### Documentação da API
+
+Se você acessar a documentação da API, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas:
+
+
+
+Você pode ver esses schemas porque eles foram declarados com os modelos no app.
+
+Essas informações estão disponíveis no **OpenAPI schema** do app e são mostradas na documentação da API (pelo Swagger UI).
+
+E essas mesmas informações dos modelos que estão incluídas no OpenAPI são o que pode ser usado para **gerar o código do cliente**.
+
+### Gerar um [NAME_REDACTED]
+
+Agora que temos o app com os modelos, podemos gerar o código do cliente para o frontend.
+
+#### Instalar o `openapi-ts`
+
+Você pode instalar o `openapi-ts` no seu código frontend com:
+
+

+
+```console
+$ npm install [USER_REDACTED]/openapi-ts --save-dev
+
+---> 100%
+```
+
+

+
+#### Gerar o Código do Cliente
+
+Para gerar o código do cliente, você pode usar a aplicação de linha de comando `openapi-ts` que agora está instalada.
+
+Como ela está instalada no projeto local, você provavelmente não conseguiria chamar esse comando diretamente, mas você o colocaria no seu arquivo `package.json`.
+
+Poderia ser assim:
+
+```JSON  hl_lines="7"
+{
+  "name": "frontend-app",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios"
+  },
+  "author": "",
+  "license": "",
+  "devDependencies": {
+    "[USER_REDACTED]/openapi-ts": "^0.27.38",
+    "typescript": "^4.6.2"
+  }
+}
+```
+
+Depois de ter esse script NPM `generate-client` lá, você pode executá-lo com:
+
+

+
+```console
+$ npm run generate-client
+
+frontend-app@1.0.0 generate-client /home/user/code/frontend-app
+> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios
+```
+
+

+
+Esse comando gerará o código em `./src/client` e usará o `axios` (a biblioteca HTTP frontend) internamente.
+
+### Experimente o Código do Cliente
+
+Agora você pode importar e usar o código do cliente, ele poderia ser assim, observe que você obtém preenchimento automático para os métodos:
+
+
+
+Você também obterá preenchimento automático para o corpo a ser enviado:
+
+
+
+/// tip | Dica
+
+Observe o preenchimento automático para `name` e `price`, que foi definido no aplicativo FastAPI, no modelo `Item`.
+
+///
+
+Você terá erros em linha para os dados que você envia:
+
+
+
+O objeto de resposta também terá preenchimento automático:
+
+
+
+## [NAME_REDACTED] com Tags
+
+Em muitos casos seu app FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*.
+
+Por exemplo, você poderia ter uma seção para **items** e outra seção para **users**, e elas poderiam ser separadas por tags:
+
+{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *}
+
+### Gerar um [NAME_REDACTED] com Tags
+
+Se você gerar um cliente para um app FastAPI usando tags, normalmente também separará o código do cliente com base nas tags.
+
+Dessa forma, você poderá ter as coisas ordenadas e agrupadas corretamente para o código do cliente:
+
+
+
+Nesse caso você tem:
+
+* `ItemsService`
+* `UsersService`
+
+### Nomes dos Métodos do Cliente
+
+Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito "limpos":
+
+```TypeScript
+ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
+```
+
+...isto ocorre porque o gerador de clientes usa o **operation ID** interno do OpenAPI para cada *operação de rota*.
+
+O OpenAPI exige que cada operation ID seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **caminho** e o **método/operacao HTTP** para gerar esse operation ID, porque dessa forma ele pode garantir que os operation IDs sejam únicos.
+
+Mas eu vou te mostrar como melhorar isso a seguir. 🤓
+
+### IDs de Operação Personalizados e [NAME_REDACTED] de Método
+
+Você pode **modificar** a maneira como esses IDs de operação são **gerados** para torná-los mais simples e ter **nomes de método mais simples** nos clientes.
+
+Neste caso, você terá que garantir que cada ID de operação seja **único** de alguma outra maneira.
+
+Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID da operação com base na **tag** e no **nome** da *operação de rota* (o nome da função).
+
+### Função Personalizada para Gerar IDs de Operação Únicos
+
+O FastAPI usa um **ID único** para cada *operação de rota*, ele é usado para o **ID da operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas.
+
+Você pode personalizar essa função. Ela recebe uma `APIRoute` e gera uma string.
+
+Por exemplo, aqui está usando a primeira tag (você provavelmente terá apenas uma tag) e o nome da *operação de rota* (o nome da função).
+
+Você pode então passar essa função personalizada para o **FastAPI** como o parâmetro `generate_unique_id_function`:
+
+{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *}
+
+### Gerar um [NAME_REDACTED] com IDs de Operação Personalizados
+
+Agora, se você gerar o cliente novamente, verá que ele tem os nomes dos métodos melhorados:
+
+
+
+Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do caminho da URL e da operação HTTP.
+
+### Pré-processar a Especificação OpenAPI para o Gerador de Clientes
+
+O código gerado ainda tem algumas **informações duplicadas**.
+
+Nós já sabemos que esse método está relacionado aos **items** porque essa palavra está no `ItemsService` (retirada da tag), mas ainda temos o nome da tag prefixado no nome do método também. 😕
+
+Provavelmente ainda queremos mantê-lo para o OpenAPI em geral, pois isso garantirá que os IDs de operação sejam **únicos**.
+
+Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais **simples**.
+
+Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então poderíamos **remover essa tag prefixada** com um script como este:
+
+{* ../../docs_src/generate_clients/tutorial004.py *}
+
+//// tab | Node.js
+
+```Javascript
+{!> ../../docs_src/generate_clients/tutorial004.js!}
+```
+
+////
+
+Com isso, os IDs de operação seriam renomeados de coisas como `items-get_items` para apenas `get_items`, dessa forma o gerador de clientes pode gerar nomes de métodos mais simples.
+
+### Gerar um [NAME_REDACTED] com o OpenAPI Pré-processado
+
+Agora, como o resultado final está em um arquivo `openapi.json`, você modificaria o `package.json` para usar esse arquivo local, por exemplo:
+
+```JSON  hl_lines="7"
+{
+  "name": "frontend-app",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios"
+  },
+  "author": "",
+  "license": "",
+  "devDependencies": {
+    "[USER_REDACTED]/openapi-ts": "^0.27.38",
+    "typescript": "^4.6.2"
+  }
+}
+```
+
+Depois de gerar o novo cliente, você teria agora **nomes de métodos "limpos"**, com todo o **preenchimento automático**, **erros em linha**, etc:
+
+
+
+## Benefícios
+
+Ao usar os clientes gerados automaticamente, você teria **preenchimento automático** para:
+
+* Métodos.
+* Corpo de requisições, parâmetros da query, etc.
+* Corpo de respostas.
+
+Você também teria **erros em linha** para tudo.
+
+E sempre que você atualizar o código do backend, e **regenerar** o frontend, ele teria quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração seria refletida no código gerado. 🤓
+
+Isso também significa que se algo mudar, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele dará erro se houver alguma **incompatibilidade** nos dados usados.
+
+Então, você **detectaria vários erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨

It is good enough to be considered.

Great work [USER_REDACTED] ✨

Just a minor problem in the translation of the admonition.

🚀

Perfect [USER_REDACTED] ✨

you don't translate the admonitions directly.

The right way to translate it is to keep the admonition, and then add a pipe + the translation.

You can check the docs for this at: [Translation specific tips and guidelines](https://fastapi.tiangolo.com/contributing/#translation-specific-tips-and-guidelines)

So, it would be:

/// tip | Dica
...
///

Perfect! I'll fix it. Thanks 🫂

I'm not with my pc to test it right now, but the t in tip is lowercase. I don't know if it will work with a capital letter

eeeerrr, lack of attention 🤦🏼. will fix it!