Fixed #36778 -- Extended advice to sanitize user input before constructing query expressions with it.

#### Trac ticket number
ticket-36778

#### Branch description
The subtlety around keyword arguments to `Func` providing direct control over the underlying SQL could use some clarification.

#### 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.

@@ -111,9 +111,11 @@ validated::
             return JsonResponse(status=200)
         return JsonResponse(form.errors, status=400)

-Similarly, as Django's raw SQL constructs (such as :meth:`~.QuerySet.extra` and
-:class:`.RawSQL` expression) provide developers with full control over the
-query, they are insecure if user input is not properly handled. As explained in
+Similarly, as Django's raw SQL constructs (such as :meth:`~.QuerySet.extra`,
+:class:`.RawSQL`, and :ref:`keyword arguments to database functions
+<avoiding-sql-injection-in-query-expressions>`) provide developers with full
+control over the query, they are insecure if user input is not properly
+handled. As explained in
 our :ref:`security documentation <sql-injection-protection>`, it is the
 developer's responsibility to safely process user input for these functions.

@@ -9,7 +9,8 @@ The classes documented below provide a way for users to use functions provided
 by [NAME_REDACTED] annotations, aggregations, or filters in Django.
 Functions are also :doc:`expressions </ref/models/expressions>`, so they can be
 used and combined with other expressions like :ref:`aggregate functions
-<aggregation-functions>`.
+<aggregation-functions>`. See the :class:`~django.db.models.Func` documentation
+for security considerations.

 We'll be using the following model in examples of each function::

@@ -434,6 +434,16 @@ replace the attributes of the same name without having to define your own
 class. :ref:`output_field<output-field>` can be used to define the expected
 return type.

+.. admonition:: Sanitize input used to configure a query expression
+
+    Built-in database functions (such as
+    :class:`~django.db.models.functions.Cast`) vary in whether arguments such
+    as ``output_field`` can be supplied positionally or only by keyword. For
+    ``output_field`` and several other cases, the input ultimately reaches
+    ``Func()`` as a keyword argument, so the advice to avoid constructing
+    keyword arguments from untrusted user input applies as equally to these
+    arguments as it does to ``**extra``.
+
 ``Aggregate()`` expressions
 ---------------------------

Thanks [USER_REDACTED] 👍🏾

I like the wording and the thinking about the proper placement of the docs here, didn't have much to add beyond that I think it will be useful to have a location to point at when related reports surface.

Thanks for doing that folks!

Thank you! 🏆

I could be convinced to remove this 🤔

I'm not convinced this is the best placement, as it breaks the flow of the example.

What do you think about moving it to https://docs.djangoproject.com/en/6.0/ref/models/expressions/#func-expressions, which introduces the `Func` API and offers security suggestions like

> The **extra kwargs are key=value pairs that can be interpolated into the template attribute. To avoid an SQL injection vulnerability, extra [must not contain untrusted user input](https://docs.djangoproject.com/en/6.0/ref/models/expressions/#avoiding-sql-injection-in-query-expressions) as these values are interpolated into the SQL string rather than passed as query parameters, where the database driver would escape them.

and referencing that in the introduction here with something like "See the introduction of the Func API for ... security considerations"

yeah I'm on the fence as well. I see where it's coming from as `Cast` is likely the most commonly used expression that will use `output_field` to generate SQL but given there are others that users are likely not aware I feel like this overindex on on ths particular example.


Fixed #26434 -- Removed faulty clearing of ordering field when missing from explicit grouping.

#### 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-26434

#### Branch description
This is a fix for a broken functionality directly related to the use of `queryset.count()` and `queryset.aggregate(...)`, i.e., a fix for the `QuerySet API`.

The issue itself describes the problem well, but in short: the ORM, due to overly aggressive SQL query optimization, removes parts of order_by even for columns that are not logically part of group_by.

One solution was already in master, but it was flawed from two perspectives:
1. Typically, the order_by part is just text, while the group_by part consists of column objects (Col). The previous solution did not reliably compare these two sets.
2. The fix was placed in the shared logic for aggressive SQL query optimization. This is not the right place here - we want the fix to reside in the final top-level aggregation, i.e., the method `query.get_aggregate(...)`.

Please provide feedback on:
1. The tests that have been written.
2. Whether the implementation is headed in the right direction.
3. What is missing in the implementation or what could be improved.
4. Anything else you think is worth discussing.

#### 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.

@@ -534,7 +534,8 @@ def get_aggregation(self, using, aggregate_exprs):
             # Queries with distinct_fields need ordering and when a limit is
             # applied we must take the slice from the ordered query. Otherwise
             # no need for ordering.
-            inner_query.clear_ordering(force=False)
+            if inner_query.orderby_issubset_groupby:
+                inner_query.clear_ordering(force=False)
             if not inner_query.distinct:
                 # If the inner query uses default select and it has some
                 # aggregate annotations, then we must make sure the inner
@@ -2338,6 +2339,33 @@ def add_ordering(self, *ordering):
         else:
             self.default_ordering = False

+    [USER_REDACTED]
+    def orderby_issubset_groupby(self):
+        if self.extra_order_by:
+            # Raw SQL from extra(order_by=...) can't be reliably compared
+            # against resolved OrderBy/Col expressions. Treat as not a subset.
+            return False
+        if self.group_by in (None, True):
+            # There is either no aggregation at all (None), or the group by
+            # is generated automatically from model fields (True), in which
+            # case the order by [NAME_REDACTED] a subset of them.
+            return True
+        if not self.order_by:
+            # Although an empty set is always a subset, there's no point in
+            # clearing ordering when there isn't any. Avoid the clone() below.
+            return True
+        # Don't pollute the original query (might disrupt joins).
+        q = self.clone()
+        order_by_set = {
+            (
+                order_by.resolve_expression(q)
+                if hasattr(order_by, "resolve_expression")
+                else F(order_by).resolve_expression(q)
+            )
+            for order_by in q.order_by
+        }
+        return order_by_set.issubset(self.group_by)
+
     def clear_ordering(self, force=False, clear_default=True):
         """
         Remove any ordering settings if the current query allows it without

@@ -171,6 +171,24 @@ def assertObjectAttrs(self, obj, **kwargs):
         for attr, value in kwargs.items():
             self.assertEqual(getattr(obj, attr), value)

+    def test_count_preserve_group_by(self):
+        # new release of the same book
+        Book.objects.create(
+            isbn="113235613",
+            name=self.b4.name,
+            pages=self.b4.pages,
+            rating=4.0,
+            price=Decimal("39.69"),
+            contact=self.a5,
+            publisher=self.p3,
+            pubdate=datetime.date(2018, 11, 3),
+        )
+        qs = Book.objects.values("contact__name", "publisher__name").annotate(
+            publications=Count("id")
+        )
+        self.assertEqual(qs.order_by("id").count(), len(qs.order_by("id")))
+        self.assertEqual(qs.extra(order_by=["id"]).count(), len(qs.order_by("id")))
+
     def test_annotation_with_value(self):
         values = (
             Book.objects.filter(

I really like the idea of creating a clone to resolve the `order_by` without polluting the query with then unused `JOIN`s [USER_REDACTED] thanks for that!

I left some comments that I think are aligned with the kind of feedback you were requesting, mainly around ways to avoid unnecessary cloning and resolving as these can be expensive.

Thanks for giving this second shot.

The logic seem sane to be, made very cosmetic comments that can be handled by [NAME_REDACTED] be but otherwise this looks good to go.

Thanks, last remaining question for me just relates to `extra`.

Thank you!

No need to clone immediately for these two check as it's a potentially costly operation.

```suggestion
        if self.group_by is False:
            # there is no use case for that, but we need to be sure
            return False
        if self.group_by in (None, True):
            # it seems like there is no aggregation at all (None)
            # or there are all required groupbies(True) generated automatically
            # from models fields - so, it is safe to clear ordering
            return True
        q = self.clone()
```

You can also avoid the clone if `not (q.order_by or q.extra_order_by)` I believe as an empty set is necessarily a subset of the group by.

Should we still delegate to `clear_ordering` with `force=True` instead?

```suggestion
            if (
                not inner_query.is_sliced
                and not inner_query.distinct_fields
                and inner_query.orderby_issubset_groupby
            ):
                inner_query.clear_ordering(force=True)
```

Do we have members of `group_by` which are not already resolved? I thought they must be already resolved since they are plucked from `Query.select` and `annotations`.

https://github.com/[USER_REDACTED]ngo/django/blob/020e5799ad74cfafd469f032cd05767c9d670a16/django/db/models/sql/query.py#L2383-L2391

assuming this is the case it means it could be reduced to

```suggestion
        return order_by_set.issubset(self.group_by)
```

I made changes, thank you for the catch.

I feel like the last assertion is enough as it keeps the ticket focused on the problem at play (mismatch in length between count and `len`) but mergers will

```suggestion
        self.assertEqual(qs.order_by("id").count(), len(qs.order_by("id")))
```

Similar story here and you can drop the ticket reference as it's _baked_ into the commit message and easily referenceable through blame.

```suggestion
        self.assertEqual(
            qs.order_by("id").aggregate(Avg("rating"))["rating__avg"],
            (12.5 + 4 * 4.0) / 7,
        )

```

Maybe harm is too harsh and we want to use _pollute_ but I'm not native speaker

If there is no use case it means it's dead code from its inception and thus and it can be removed.

`Query.group_by` appears to be `None | True | tuple[Compilable]`

Minor but the _it is safe to clear ordering_ assumes the sole caller will always be for order clearing purpose.

Maybe something along the lines of

```
When grouping by [NAME_REDACTED] by [NAME_REDACTED] a subset of the group by.
```

I will edit it.

Finally I returned it back, when there is not the code, it breaks a test

```
FAIL: test_crafted_xml_performance (serializers.test_deserialization.TestDeserializer.test_crafted_xml_performance) [varying depth, varying length]
The time to process invalid inputs is not quadratic.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 556, in subTest
    yield
  File "/home/mmladek/projects/django/tests/serializers/test_deserialization.py", line 182, in assertFactor
    self.assertLessEqual(sum(factors) / len(factors), factor)
    ^^^^^^^
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 1285, in assertLessEqual
    self.fail(self._formatMessage(msg, standardMsg))
    ^^^^^^^^^^^
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 732, in fail
    raise self.failureException(msg)
    ^^^^^^^^^^^^^^^
AssertionError: 2.930120819494018 not less than or equal to 2

======================================================================
FAIL: test_crafted_xml_performance (serializers.test_deserialization.TestDeserializer.test_crafted_xml_performance) [constant depth, varying length]
The time to process invalid inputs is not quadratic.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 556, in subTest
    yield
  File "/home/mmladek/projects/django/tests/serializers/test_deserialization.py", line 182, in assertFactor
    self.assertLessEqual(sum(factors) / len(factors), factor)
    ^^^^^^^
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 1285, in assertLessEqual
    self.fail(self._formatMessage(msg, standardMsg))
    ^^^^^^^^^^^
  File "/home/mmladek/.pyenv/versions/3.13.5/lib/python3.13/unittest/case.py", line 732, in fail
    raise self.failureException(msg)
    ^^^^^^^^^^^^^^^
AssertionError: 2.69819775823359 not less than or equal to 2
```
I dont have a hunch why ...

That's an [unrelated flaky test that we're addressing](https://code.djangoproject.com/ticket/36769#comment:9) as soon as Python issues a patch release of 3.12.

`extra_order_by` is composed of strings, don't we need to resolve them to `Col`s as well?

[USER_REDACTED] they can't; `extra_order_by` is this weird thing coming from `extra(order_by)` which is raw SQL.

I've been trying to kill it #16681 for reference.

Not sure if `extra(select)` ever end up in `group_by` but we are definitely more interested in `q.extra_order_by.values()` here.

Maybe we could return `False` the moment `bool(q.extra_order_by)` until we finally kill it?

Yes, makes sense. extra(order_by) is just the weird legacy case we need to account for. I'll update the patch accordingly.

The test case I just pushed gives me this:
```py
(Pdb) print(q.extra_order_by)
['id']
```

> this weird thing coming from extra(order_by) which is raw SQL.

Oh, then I guess my test isn't realistic.

You're completely right, it's a not a `dict` at all, it's a collection of raw SQL (I got confused with `extra_select`)

In this case the `.values()` doesn't make sense but unless we want to invest time in enabling this optimization in the presence of `extra(order_by)` (which is documented to be deprecated) it might be better to opt-out by returning `False` immediately in its presence.

Is there a reason you didn't opt for:
```suggestion
            if inner_query.orderby_issubset_groupby:
                inner_query.clear_ordering(force=False)
```

Github chewed up my last comment, leaving it again.

Is there reason you didn't opt for:
```suggestion
            if inner_query.orderby_issubset_groupby:
                inner_query.clear_ordering(force=False)
```

I followed blindly the comments (line: 534) hoping that is true:
```python
# Queries with distinct_fields need ordering and when a limit is
# applied we must take the slice from the ordered query. Otherwise
# no need for ordering.
```

For sure, I just mean the `clear_ordering()` actually handles that for you and also checks `select_for_update`. See the beginning of `clear_ordering()`.

Just so that we don't actually introduce any drift against that implementation, I'll roll this in for you 👍

I was about to change this to assertAlmostEqual just out of paranoia about the decimal value, when I realized I think the other test is sufficient. I think Simon suggested something along those lines. I'll remove it.

Just noticed from the coverage report that this line is not covered. When would `self.group_by` be an empty iterable?

With the current call site never as we only get in this branch `if isinstance(self.group_by, tuple)` which means the queryset was previously annotated with an aggregate expression and that there was at least one value passed to `values` to specify the group by [NAME_REDACTED] might be handy if `orderby_issubset_groupby` is eventually called from other context but in with this single call site it's not.

Shouldn't we also short-circuit if `self.order_by` is empty and `self.group_by` is non-empty?

And if `self.order_by` is non-empty but `self.group_by` is empty, we could also short-circuit but return `False`.

> Shouldn't we also short-circuit if self.order_by is empty and self.group_by is non-empty?

We could yes to be coherent with the method name but if there is no ordering there is little value in clearing it.

> And if self.order_by is non-empty but self.group_by is empty, we could also short-circuit but return False.

Same story IMO, we have to choose a side.

Do we want to make the function caller aware (make choices knowing it will only be called in the context of `get_aggregation`) or make it general purposed with all the optimization possible but harder to exercise through code coverage.

Thanks, I'm just not understanding why `group_by` is in this condition; your earlier suggestion had `extra_order_by` there instead.

> We could yes to be coherent with the method name but if there is no ordering there is little value in clearing it.

In that case, I think we should either change this to
```py
if not order_by:
    # Empty sets are always subsets, but there's no point in clearing an ordering.
    return False
```
Or just remove it altogether.

If we do that suggested tweak, then we're not doing all possible optimizations but we are doing the most likely one.

I'm good with switching it to solely checking `not order_by` 👍


Fixed #36768 -- Optimized string concatenation in File.__iter__().

#### 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-36768

#### Branch description
Addresses ticket #36768 by [NAME_REDACTED] in File._ _iter__ Chunks are now collected in a list and joined only when necessary, fixing performance degradation associated with processing split lines.

#### 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.

@@ -74,30 +74,33 @@ def multiple_chunks(self, chunk_size=None):

     def __iter__(self):
         # Iterate over this file-like object by newlines
-        buffer_ = None
+        buffer_ = []
         for chunk in self.chunks():
             for line in chunk.splitlines(True):
                 if buffer_:
-                    if endswith_cr(buffer_) and not equals_lf(line):
+                    if endswith_cr(buffer_[-1]) and not equals_lf(line):
                         # Line split after a \r newline; yield buffer_.
-                        yield buffer_
+                        yield type(buffer_[0])().join(buffer_)
                         # Continue with line.
+                        buffer_ = []
                     else:
                         # Line either split without a newline (line
                         # continues after buffer_) or with \r\n
                         # newline (line == b'\n').
-                        line = buffer_ + line
-                    # buffer_ handled, clear it.
-                    buffer_ = None
-
-                # If this is the end of a \n or \r\n line, yield.
-                if endswith_lf(line):
-                    yield line
-                else:
-                    buffer_ = line
-
-        if buffer_ is not None:
-            yield buffer_
+                        buffer_.append(line)
+
+                if not buffer_:
+                    # If this is the end of a \n or \r\n line, yield.
+                    if endswith_lf(line):
+                        yield line
+                    else:
+                        buffer_.append(line)
+                elif endswith_lf(line):
+                    yield type(buffer_[0])().join(buffer_)
+                    buffer_ = []
+
+        if buffer_:
+            yield type(buffer_[0])().join(buffer_)

     def __enter__(self):
         return self

Thanks 👍

I made a tweak to preserve the existing comments.

Let's try to keep the type consistent. Will require changes above & below.
```suggestion
                        buffer_ = []
```


Added DatabaseFeatures.prohibits_dollar_signs_in_column_aliases.

This is also applicable on CockroachDB.

Follow up to 5b90ca1e7591fa36fccf2d6dad67cf1477e6293e.

@@ -415,6 +415,10 @@ class BaseDatabaseFeatures:
     # Does the Round() database function round to even?
     rounds_to_even = False

+    # Should dollar signs be prohibited in column aliases to prevent SQL
+    # injection?
+    prohibits_dollar_signs_in_column_aliases = False
+
     # A set of dotted paths to tests in Django's test suite that are expected
     # to fail on this database.
     django_test_expected_failures = set()

@@ -1,6 +1,6 @@
 from django.db.models.sql.compiler import (  # isort:skip
     SQLAggregateCompiler,
-    SQLCompiler as BaseSQLCompiler,
+    SQLCompiler,
     SQLDeleteCompiler,
     SQLInsertCompiler as BaseSQLInsertCompiler,
     SQLUpdateCompiler,
@@ -25,15 +25,6 @@ def __str__(self):
         return "UNNEST(%s)" % ", ".join(self)

-class SQLCompiler(BaseSQLCompiler):
-    def quote_name_unless_alias(self, name):
-        if "$" in name:
-            raise ValueError(
-                "Dollar signs are not permitted in column aliases on PostgreSQL."
-            )
-        return super().quote_name_unless_alias(name)
-
-
 class SQLInsertCompiler(BaseSQLInsertCompiler):
     def assemble_as_sql(self, fields, value_rows):
         # Specialize bulk-insertion of literal values through UNNEST to

@@ -70,6 +70,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     supports_nulls_distinct_unique_constraints = True
     supports_no_precision_decimalfield = True
     can_rename_index = True
+    prohibits_dollar_signs_in_column_aliases = True
     test_collations = {
         "deterministic": "C",
         "non_default": "sv-x-icu",

@@ -553,6 +553,14 @@ def quote_name_unless_alias(self, name):
         for table names. This avoids problems with some SQL dialects that treat
         quoted strings specially (e.g. PostgreSQL).
         """
+        if (
+            self.connection.features.prohibits_dollar_signs_in_column_aliases
+            and "$" in name
+        ):
+            raise ValueError(
+                "Dollar signs are not permitted in column aliases on "
+                f"{self.connection.display_name}."
+            )
         if name in self.quote_cache:
             return self.quote_cache[name]
         if (

@@ -1545,8 +1545,11 @@ def test_alias_filtered_relation_sql_injection_dollar_sign(self):
         qs = Book.objects.alias(
             **{"crafted_alia$": FilteredRelation("authors")}
         ).values("name", "crafted_alia$")
-        if connection.vendor == "postgresql":
-            msg = "Dollar signs are not permitted in column aliases on PostgreSQL."
+        if connection.features.prohibits_dollar_signs_in_column_aliases:
+            msg = (
+                "Dollar signs are not permitted in column aliases on "
+                f"{connection.display_name}."
+            )
             with self.assertRaisesMessage(ValueError, msg):
                 list(qs)
         else:

Thanks, Tim! I assume this should be backported?

Now that we have a flag for this, I suppose we could just move this into the top of the method in the base class in `django/db/models/sql/compiler.py`?

```python
        if "$" in name and self.connection.features.prohibits_dollar_signs_in_column_aliases:
            raise ValueError(
                "Dollar signs are not permitted in column aliases on "
                f"{self.connection.display_name}."
            )

Maybe `prohibits_dollar_char_in_column_aliases` or `prohibits_dollar_in_column_aliases`?

I think it's okay. It's called a dollar sign, not a dollar character. :-D


Refs #35581 -- Fixed email image inline attachment example.

#### Trac ticket number

ticket-35581

#### Branch description

Correct this example, after testing a version of it for my Django 6.0 blog post and finding it needed fixing.

1. Added imports and setup for clarity.
2. Removed adding `<` and `>` to Content-ID, as `make_msgid()` already includes them.
3. Removed `$` from reference in HTML, and instead stripped `<>` there, as required by HTML `cid:` references.

#### 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.

@@ -471,17 +471,26 @@ email backend API :ref:`provides an alternative
           directly to the resulting message. For example, to attach an inline
           image with a :mailheader:`Content-ID`::

+              import email.utils
+              from email.message import MIMEPart
+              from django.core.mail import EmailMultiAlternatives
+
+              message = EmailMultiAlternatives(...)
+              image_data_bytes = ...  # Load image as bytes
+
+              # Create a random Content-ID, including angle brackets
               cid = email.utils.make_msgid()
               inline_image = email.message.MIMEPart()
               inline_image.set_content(
                   image_data_bytes,
                   maintype="image",
-                  subtype="png",
+                  subtype="png",  # or "jpeg", etc. depending on the image type
                   disposition="inline",
-                  cid=f"<{cid}>",
+                  cid=cid,
               )
               message.attach(inline_image)
-              message.attach_alternative(f'…  …', "text/html")
+              # Refer to Content-ID in HTML without angle brackets
+              message.attach_alternative(f'…  …', "text/html")

           Python's :meth:`email.contentmanager.set_content` documentation
           describes the supported arguments for ``MIMEPart.set_content()``.

(Embarrassed I missed this in the original PR. Hoping the correction is eligible for backport to 6.0 docs.)

I'm ambivalent on how much additional commentary to include in the example, but added some suggestions. (Long-term, I'm hoping to split the lengthy email docs between guide and reference. This example—with expanded commentary—would be better in a reference section.)

Should we add a comment explaining why the slice is here?

```suggestion
                  subtype="png",  # or "gif" or "jpeg"
```

```suggestion
              # In the html, refer to the cid (without angle brackets).
              message.attach_alternative(f'… <img src="cid:{cid[1:-1]}"> …', "text/html")
```

```suggestion
              # Create a random Content-ID (including angle brackets <…>).
              cid = email.utils.make_msgid()
```


Improved docs/releases/6.0.txt.

#### Trac ticket number

n/a

#### Branch description

Some possible improvements that I spotted:

1. Note that CSP generates a unique nonce per request.
2. Mention django-tasks as a reference implementation for task backends.
3. Move some sections from the "Miscellaneous" section to their relevant app sections.
4. Link to more mentioned classes and settings.
5. Make some wording clearer and more consistent.

#### 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,14 +1,14 @@
-============================================
-Django 6.0 release notes - UNDER DEVELOPMENT
-============================================
+========================
+Django 6.0 release notes
+========================

-*[NAME_REDACTED] 2025*
+*December 3, 2025*

 Welcome to Django 6.0!

 These release notes cover the :ref:`new features <whats-new-6.0>`, as well as
-some :ref:`backwards incompatible changes <backwards-incompatible-6.0>` you'll
-want to be aware of when upgrading from Django 5.2 or earlier. We've
+some :ref:`backwards incompatible changes <backwards-incompatible-6.0>` you
+should be aware of when upgrading from Django 5.2 or earlier. We've
 :ref:`begun the deprecation process for some features
 <deprecated-features-6.0>`.

@@ -160,6 +160,12 @@ Minor features
 * The new :attr:`.AdminSite.password_change_form` attribute allows customizing
   the form used in the admin site password change view.

+* Message levels ``messages.DEBUG`` and ``messages.INFO`` now have distinct
+  icons and CSS styling. Previously, both levels shared the same appearance as
+  ``messages.SUCCESS``. Given that :meth:`.ModelAdmin.message_user` uses
+  ``messages.INFO`` by default, set the level to ``messages.SUCCESS`` to keep
+  the previous icon and styling.
+
 :mod:`django.contrib.auth`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -189,6 +195,11 @@ Minor features
   :class:`GeometryType() <django.contrib.gis.db.models.functions.GeometryType>`
   database function allow filtering geometries by [NAME_REDACTED]

+* Widgets from :mod:`django.contrib.gis.forms.widgets` now render without
+  inline JavaScript in templates. If you have customized any geometry widgets
+  or their templates, you may need to :ref:`update them
+  <geometry-widgets-customization>` to match the new layout.
+
 :mod:`django.contrib.postgres`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -217,6 +228,10 @@ Minor features
   ensures consistent path ordering in manifest files, making them more
   reproducible and reducing unnecessary diffs.

+* The :djadmin:`collectstatic` command now reports only a summary for skipped
+  files (and for deleted files when using ``--clear``) at ``--verbosity`` 1. To
+  see per-file details for either case, set ``--verbosity`` to 2 or higher.
+
 Email
 ~~~~~

@@ -462,29 +477,9 @@ Miscellaneous
   refactored to use Python's :class:`email.message.Message` for parsing.
   Input headers exceeding 10000 characters will now raise :exc:`ValueError`.

-* Widgets from :mod:`django.contrib.gis.forms.widgets` now render without
-  inline JavaScript in templates. If you have customized any geometry widgets
-  or their templates, you may need to :ref:`update them
-  <geometry-widgets-customization>` to match the new layout.
-
-* Message levels ``messages.DEBUG`` and ``messages.INFO`` now have distinct
-  icons and CSS styling in the admin. Previously, these used the same icon and
-  styling as the ``messages.SUCCESS`` level. Since
-  :meth:`.ModelAdmin.message_user` uses the ``messages.INFO`` level by default,
-  set the level to ``messages.SUCCESS`` to retain the previous icon and
-  styling.
-
 * The minimum supported version of ``asgiref`` is increased from 3.8.1 to
   3.9.1.

-* The :djadmin:`collectstatic` command now reports only a summary of skipped
-  files due to conflicts when ``--verbosity`` is 1. To see warnings for each
-  conflicting destination path, set the ``--verbosity`` flag to 2 or higher.
-
-* The :option:`collectstatic --clear` command now reports only a summary of
-  deleted files when ``--verbosity`` is 1. To see the details for each file
-  deleted, set the ``--verbosity`` flag to 2 or higher.
-
 .. _deprecated-features-6.0:

 Features deprecated in 6.0
@@ -516,14 +511,15 @@ Miscellaneous
   available :class:`~django.db.models.StringAgg` class.

 * The PostgreSQL ``OrderableAggMixin`` is deprecated in favor of the
-  ``order_by`` attribute now available on the ``Aggregate`` class.
+  ``order_by`` attribute now available on the
+  :class:`~django.db.models.Aggregate` class.

 * The default protocol in :tfilter:`urlize` and :tfilter:`urlizetrunc` will
   change from HTTP to HTTPS in Django 7.0. Set the transitional setting
-  ``URLIZE_ASSUME_HTTPS`` to ``True`` to opt into assuming HTTPS during the
-  Django 6.x release cycle.
+  :setting:`URLIZE_ASSUME_HTTPS` to ``True`` to opt into assuming HTTPS during
+  the Django 6.x release cycle.

-* ``URLIZE_ASSUME_HTTPS`` transitional setting is deprecated.
+* The :setting:`URLIZE_ASSUME_HTTPS` transitional setting is deprecated.

 * Setting :setting:`ADMINS` or :setting:`MANAGERS` to a list of (name, address)
   tuples is deprecated. Set to a list of email address strings instead. Django

Thank you Adam! will push some changes and land 🌟

Thank you Adam for reviewing these notes!!!

Sadly, I don't think this is a good fit for the release notes. This clarification is more appropriate for the reference docs, where it is already documented.

(I'll push its removal since I'm about to do the 6.0 final release.)

This is being worked on in PR #20359.

Fair enoughi

Alrighty!


Fixed #36765 -- Added support for stored GeneratedFields on Oracle 23ai/26ai (23.7+).

@@ -373,6 +373,8 @@ class BaseDatabaseFeatures:
     supports_stored_generated_columns = False
     # Does the backend support virtual generated columns?
     supports_virtual_generated_columns = False
+    # Does the backend support altering data types of generated columns?
+    supports_alter_generated_column_data_type = True

     # Does the backend support the logical XOR operator?
     supports_logical_xor = False

@@ -452,10 +452,14 @@ def db_default_sql(self, field):
             params = []
         return sql % default_sql, params

+    def _column_generated_persistency_sql(self, field):
+        """Return the SQL to define the persistency of generated fields."""
+        return "STORED" if field.db_persist else "VIRTUAL"
+
     def _column_generated_sql(self, field):
         """Return the SQL to use in a GENERATED ALWAYS clause."""
         expression_sql, params = field.generated_sql(self.connection)
-        persistency_sql = "STORED" if field.db_persist else "VIRTUAL"
+        persistency_sql = self._column_generated_persistency_sql(field)
         if self.connection.features.requires_literal_defaults:
             expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
             params = ()
@@ -906,6 +910,15 @@ def alter_field(self, model, old_field, new_field, strict=False):
             else:
                 new_field_sql = new_field.generated_sql(self.connection)
                 modifying_generated_field = old_field_sql != new_field_sql
+                db_features = self.connection.features
+                # Some databases (e.g. Oracle) don't allow altering a data type
+                # for generated columns.
+                if (
+                    not modifying_generated_field
+                    and old_type != new_type
+                    and not db_features.supports_alter_generated_column_data_type
+                ):
+                    modifying_generated_field = True
         if modifying_generated_field:
             raise ValueError(
                 f"[NAME_REDACTED] is not supported - the field {new_field} "

@@ -69,8 +69,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     supports_ignore_conflicts = False
     max_query_params = 2**16 - 1
     supports_partial_indexes = False
-    supports_stored_generated_columns = False
     supports_virtual_generated_columns = True
+    supports_alter_generated_column_data_type = False
     can_rename_index = True
     supports_slicing_ordering_in_compound = True
     requires_compound_order_by_subquery = True
@@ -131,6 +131,11 @@ def django_test_skips(self):
             "Oracle doesn't support casting filters to NUMBER.": {
                 "lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
             },
+            "Oracle doesn't support some data types (e.g. BOOLEAN, BLOB) in "
+            "GeneratedField expressions (ORA-54003).": {
+                "schema.tests.SchemaTests.test_add_generated_field_contains",
+                "schema.tests.SchemaTests.test_add_generated_field_with_kt_model",
+            },
         }
         if self.connection.oracle_version < (23,):
             skips.update(
@@ -228,3 +233,7 @@ def supports_tuple_lookups(self):
     [USER_REDACTED]_property
     def supports_uuid4_function(self):
         return self.connection.oracle_version >= (23, 9)
+
+    [USER_REDACTED]_property
+    def supports_stored_generated_columns(self):
+        return self.connection.oracle_version >= (23, 7)

@@ -251,3 +251,6 @@ def _collate_sql(self, collation, old_collation=None, table_name=None):
         if collation is None and old_collation is not None:
             collation = self._get_default_collation(table_name)
         return super()._collate_sql(collation, old_collation, table_name)
+
+    def _column_generated_persistency_sql(self, field):
+        return "MATERIALIZED" if field.db_persist else "VIRTUAL"

@@ -1346,8 +1346,13 @@ materialized view.
     real column. If ``False``, the column acts as a virtual column and does
     not occupy database storage space.

-    PostgreSQL < 18 only supports persisted columns. Oracle only supports
-    virtual columns.
+    PostgreSQL < 18 only supports persisted columns. Oracle < 23ai/26ai (23.7)
+    only supports virtual columns.
+
+    .. versionchanged:: 6.1
+
+        Support for stored ``GeneratedField``\s was added on Oracle 23ai/26ai
+        (23.7+).

 .. admonition:: Database limitations

@@ -266,6 +266,10 @@ Models
 * The new :class:`~django.db.models.functions.UUID4` and
   :class:`~django.db.models.functions.UUID7` database functions were added.

+* :class:`~django.db.models.GeneratedField` now supports stored columns
+  (:attr:`~django.db.models.GeneratedField.db_persist` set to ``True``) on
+  Oracle 23ai/26ai (23.7+).
+
 Pagination
 ~~~~~~~~~~

@@ -1491,7 +1491,7 @@ def test_add_generated_field(self):
                             "name_and_id",
                             models.GeneratedField(
                                 expression=Concat(("name"), ("rider_id")),
-                                output_field=models.TextField(),
+                                output_field=models.CharField(max_length=60),
                                 db_persist=True,
                             ),
                         ),
@@ -6363,6 +6363,15 @@ def _test_invalid_generated_field_changes(self, db_persist):
             ("test_igfc_2", generated_1, regular),
             ("test_igfc_3", generated_1, generated_2),
         ]
+        if not connection.features.supports_alter_generated_column_data_type:
+            generated_3 = models.GeneratedField(
+                expression=F("pink") + F("pink"),
+                output_field=models.DecimalField(decimal_places=2, max_digits=16),
+                db_persist=db_persist,
+            )
+            tests.append(
+                ("test_igfc_4", generated_1, generated_3),
+            )
         for app_label, add_field, alter_field in tests:
             project_state = self.set_up_test_model(app_label)
             operations = [
@@ -6441,7 +6450,7 @@ def test_invalid_generated_field_persistency_change(self):
                 "Pony",
                 "modified_pink",
                 models.GeneratedField(
-                    expression=F("pink"),
+                    expression=F("pink") + 2,
                     output_field=models.IntegerField(),
                     db_persist=True,
                 ),
@@ -6450,7 +6459,7 @@ def test_invalid_generated_field_persistency_change(self):
                 "Pony",
                 "modified_pink",
                 models.GeneratedField(
-                    expression=F("pink"),
+                    expression=F("pink") + 2,
                     output_field=models.IntegerField(),
                     db_persist=False,
                 ),
@@ -6489,7 +6498,9 @@ def _test_add_generated_field(self, db_persist):
             operation.database_backwards(app_label, editor, new_state, project_state)
         self.assertColumnNotExists(f"{app_label}_pony", "modified_pink")

-    [USER_REDACTED]("supports_stored_generated_columns")
+    [USER_REDACTED](
+        "supports_stored_generated_columns", "supports_alter_generated_column_data_type"
+    )
     def test_generated_field_changes_output_field(self):
         app_label = "test_gfcof"
         operation = migrations.AddField(

@@ -9,7 +9,7 @@
 from django.db import connection, models
 from django.db.models import F, Value
 from django.db.models.fields.files import ImageFieldFile
-from django.db.models.functions import Lower
+from django.db.models.functions import Cast, Lower
 from django.utils.functional import SimpleLazyObject
 from django.utils.translation import gettext_lazy as _

@@ -534,7 +534,7 @@ class UUIDGrandchild(UUIDChild):
 class GeneratedModelFieldWithConverters(models.Model):
     field = models.UUIDField()
     field_copy = models.GeneratedField(
-        expression=F("field"),
+        expression=Cast("field", models.UUIDField()),
         output_field=models.UUIDField(),
         db_persist=True,
     )
@@ -561,7 +561,7 @@ class GeneratedModelNonAutoPk(models.Model):
     id = models.IntegerField(primary_key=True)
     a = models.IntegerField()
     b = models.GeneratedField(
-        expression=F("a"),
+        expression=F("a") + 1,
         output_field=models.IntegerField(),
         db_persist=True,
     )

@@ -413,7 +413,7 @@ def test_create_with_non_auto_pk(self):
         obj = GeneratedModelNonAutoPk.objects.create(id=1, a=2)
         self.assertEqual(obj.id, 1)
         self.assertEqual(obj.a, 2)
-        self.assertEqual(obj.b, 2)
+        self.assertEqual(obj.b, 3)

 [USER_REDACTED]("supports_virtual_generated_columns")

@@ -1029,7 +1029,7 @@ def test_alter_generated_field(self):
         class GeneratedFieldIndexedModel(Model):
             number = IntegerField(default=1)
             generated = GeneratedField(
-                expression=F("number"),
+                expression=F("number") + 1,
                 db_persist=True,
                 output_field=IntegerField(),
             )
@@ -1042,7 +1042,7 @@ class Meta:

         old_field = GeneratedFieldIndexedModel._meta.get_field("generated")
         new_field = GeneratedField(
-            expression=F("number"),
+            expression=F("number") + 1,
             db_persist=True,
             db_index=True,
             output_field=IntegerField(),

Nice one. Just one question about comparing types.

data type -> data types

I think we have to check for collation here also. Any other suffixes to check?

Actually, changing only `db_collation` works fine (I've checked on `VIRTUAL` generated field).

Oh good to know. Tried to test, but it wanted me to set `MAX_STRING_SIZE=EXTENDED` and that turned out to be impossibly complicated.

Yes it is complicated :disappointed:

TBH, I don't see us adding more and more conditions here. I would treat this check as "the best effort", there are too many database-specific caveats around altering `GeneratedField`. IMO, `DatabaseError` is fine here when someone hits the edge.


Refs #35859 -- Clarified the need for installing a production task backend separately.

ticket-35859

See forum:
https://forum.djangoproject.com/t/6-0-documentation-confusion-for-django-tasks/43583

@@ -7,6 +7,10 @@ Tasks
 .. module:: django.tasks
    :synopsis: Django's built-in background Task system.

+The Task framework provides the contract and plumbing for background work, not
+the engine that runs it. The Tasks API defines how work is described, queued,
+and tracked, but leaves actual execution to external infrastructure.
+
 Task definition
 ===============

@@ -274,6 +278,10 @@ Task errors
 Backends
 ========

+Backends handle how Tasks are stored and executed. All backends share a common
+interface defined by ``BaseTaskBackend``, which specifies the core methods for
+enqueueing Tasks and retrieving results.
+
 Base backend
 ------------

@@ -365,6 +373,10 @@ Feature                      :class:`.DummyBackend`  :class:`.ImmediateBackend`
 Available backends
 ------------------

+Django includes only development and testing backends. These support local
+execution and inspection, for production ready backends refer to
+:ref:`configuring-a-task-backend`.
+
 Immediate backend
 ~~~~~~~~~~~~~~~~~

@@ -102,6 +102,11 @@ Django now includes a built-in Tasks framework for running code outside the
 HTTP request–response cycle. This enables offloading work, such as sending
 emails or processing data, to background workers.

+The framework provides task definition, validation, queuing, and result
+handling. Django guarantees consistent behavior for creating and managing
+tasks, while the responsibility for running them continues to belong to
+external worker processes.
+
 Tasks are defined using the :func:`~django.tasks.task` decorator::

     from django.core.mail import send_mail

@@ -15,10 +15,11 @@ to be run elsewhere, potentially at a later date. This keeps requests fast,
 reduces latency, and improves the user experience. For example, a user
 shouldn't have to wait for an email to send before their page finishes loading.

-Django's new Tasks framework makes it easy to define and enqueue such work. It
+[NAME_REDACTED] framework makes it easy to define and enqueue such work. It
 does not provide a worker mechanism to run Tasks. The actual execution must be
 handled by [NAME_REDACTED], such as a separate process or
-service.
+service. Given that, a :ref:`task backend <configuring-a-task-backend>` capable
+of executing tasks on that service should be evaluated and configured.

 [NAME_REDACTED] fundamentals
 ============================
@@ -41,9 +42,13 @@ Configuring a Task backend
 The Task backend determines how and where Tasks are stored for execution and
 how they are executed. [NAME_REDACTED] backends have different characteristics
 and configuration options, which may impact the performance and reliability of
-your application. Django comes with a number of :ref:`built-in backends
-<task-available-backends>`. Django does not provide a generic way to execute
-Tasks, only enqueue them.
+your application. Django comes with :ref:`built-in backends
+<task-available-backends>`, but these are for development and testing only.
+
+Django handles task definition, validation, queuing, and result handling, not
+execution, so production setups need a backend or worker process that actually
+runs queued work. Relevant options are listed in the `[NAME_REDACTED]
+<https://www.djangoproject.com/community/ecosystem/>`__ page.

 Task backends are configured using the :setting:`TASKS` setting in your
 settings file. Whilst most applications will only need a single backend,
@@ -103,13 +108,14 @@ Stored results can be cleared using the
     >>> len(default_task_backend.results)
     0

-Using a custom backend
-----------------------
+Third-party backends
+--------------------

-[NAME_REDACTED] includes support for a number of Task backends out-of-the-box,
-sometimes you might want to customize the Task backend. To use an external Task
-backend with Django, use the Python import path as the :setting:`BACKEND
-<TASKS-BACKEND>` of the :setting:`TASKS` setting, like so::
+As mentioned at the beginning of this section, Django includes backends
+suitable for development and testing only. Production systems should rely on
+backends that supply a worker process and durable queue implementation. To use
+an external Task backend with Django, use the Python import path as the
+:setting:`BACKEND <TASKS-BACKEND>` of the :setting:`TASKS` setting, like so::

     TASKS = {
         "default": {

Looks good to me!

Are we ok with just saying "3rd-party packages", and leaving it to the user to go and find them?

Perhaps worth mentioning `django-tasks` in the context of a backport? Although that may be a different PR.

Looks good. (I might remove the last sentence of the new heading section, but it's okay as is.)

In my head (without me referring to the docs style guide mind you so if this breaks that then I'm fine with it), I was thinking a short info or warning call out box here stating that you will need a third-party package to execute tasks in the background.

I'll push some reworking on this since the ecosystem page, as how it is now, it's not listing the major tasks bakends. I have reached out to Tim S about this, so it'll likely get fixed soon.

In any case, I rather to link to the ecosystem page in a single place and I think the topics page is the best location. Will propose something asap!

I'll try to give this a go.

I believe we should link directly to the `django-tasks` documentation about backends here in this context rather than the ecosystem page. The reason being is that the ecosystem page is more likely to list out all background task processing libraries, including ones that don't implement the `django-tasks` API, such as Celery. It would be confusing for someone who is looking for the production ready backends to be used with the tasks part of Django to be linked to a page that lists options they can't use.

Additionally, I expect `django-tasks` to be more up to date with the "production ready" backends than the ecosystem page, no? cc: [USER_REDACTED]/steering-council

I do not think we should link directly to django-tasks here. The Tasks framework *in Django* is intentionally limited to the stable core contract for defining and queuing work. Backend specifics, including which packages implement the API, are outside that contract and change much faster than the Django docs.

The ecosystem page is the right place to endorse, list, and curate third party backends (even if that means a single entry "check django-tasks for the up to date list of production ready backends that implement the [NAME_REDACTED] API"), and it can clearly separate API compatible packages from general background processing tools. That keeps the Django docs focused on the framework itself while letting backend guidance evolve independently.

> I expect `django-tasks` to be more up to date with the "production ready" backends than the ecosystem page

As time goes on, there'll be more features in `django-tasks` than in `django`. Those features will still be usable in production, but may not come with the same rigorous testing that including in Django assumes.

I also expect more backends to exist outside of `django-tasks`, and have little intention of listing them in the README.

My 2 cents here:

* We need to be clear that django-tasks is the first production ready background task worker & backend (it's not in Django yet)
* At some point be clear about if there are things in django-tasks that won't likely appear in Django, essentially at what points does it start to diverge from the reference implementation of the DEP?
* I would expect any callout to django-tasks specifically to be time-limited to the 6.X release cycle.

All that stems from wanting to reduce the amount of questions that have and will continue to pop up on the forum and Discord.

[USER_REDACTED] [USER_REDACTED] I tried to give this a go but I reverted the box usage since the docs say all over the place (ref/tasks, topics/tasks, release notes) what tasks are and are not. At some point, we truly need users to read what's written :sob:

The additions here are good. Let's not hold up 6.0 any longer!

> I do not think we should link directly to django-tasks here. The Tasks framework in Django is intentionally limited to the stable core contract for defining and queuing work. Backend specifics, including which packages implement the API, are outside that contract and change much faster than the Django docs.

[USER_REDACTED] I don't understand this part of your statement. We're in agreement here on what the Tasks framework is in Django. We want to link to some other place because the Django docs aren't updated frequently in comparison. I get lost about how this is a reason to not link to django-tasks though.

> ... it can clearly separate API compatible packages from general background processing tools. That keeps the Django docs focused on the framework itself while letting backend guidance evolve independently.

[USER_REDACTED] Same thing here. This isn't a justification to not link directly to django-tasks. From my understanding, it's a justification that we need to link to somewhere.

> As time goes on, there'll be more features in django-tasks than in django. Those features will still be usable in production, but may not come with the same rigorous testing that including in Django assumes.

[USER_REDACTED] That's fine, but is a tangential discussion to what I understand. The question is, where are we collecting the various backends that implement [NAME_REDACTED] interface? (see below)

> I also expect more backends to exist outside of django-tasks, and have little intention of listing them in the README.

[USER_REDACTED] To align on understanding, I'm considering things for the next 2-3 years, not the permanent future. For the near future, I think we would want to be a little more interconnected to help adoption rather than leaving people to their own devices. Where would people find out about backends in the various states:

- Alpha, just created and creator is looking for feedback
- Beta, exists for up to a year, used by [NAME_REDACTED], looking for adoption
- Stable, exists a year+, is being used by [NAME_REDACTED], the Stable condition is when something would get listed on the ecosystem page on its own. Until then, it would be in the experimental section or not at all. That's why I was suggesting linking to django-tasks because I had assumed that it would act as a hub for at least a while to help gain adoption.

I suppose maybe this is where djangopackages.org comes in?

I think we should link directly to django-tasks for the database backend until we land it in Django (or decide we're no longer actively intending to land it).

For other backends I think the ecosystem page (if they're reasonably mature) is appropriate, but I think the database backend is an important exception here:

* It's the first production suitable backend to be implemented.
* It's a reference implementation for other backends.
* We're intending for it to land in Django.


Fixed #36233 -- Avoided quantizing integers stored in DecimalField on SQLite.

#### 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-36233

#### Branch description
Fixed a crash in DecimalField when using SQLite with integers exceeding 15 digits (e.g., 16-digit IDs).

The Issue: SQLite lacks a native decimal type. When retrieving large integers (e.g., 9999999999999999), the default converter treats them as floating-point numbers. Since IEEE 754 floats only support ~15 significant digits, precision is lost (e.g., becoming 10000000000000000). When Django attempts to quantize() this inaccurate value against the field's context, it raises decimal.InvalidOperation.

The Fix: Updated django.db.backends.sqlite3.operations.py to check if the underlying SQLite driver returned a native int. If so, the value is converted directly to Decimal, bypassing the lossy float conversion entirely.

Verification: Added a regression test test_sqlite_integer_precision_bypass in model_fields/test_decimalfield.py to ensure 16-digit integers are stored and retrieved correctly without crashing.
#### Checklist
- [ ] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [ ] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [ ] 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.

@@ -300,26 +300,31 @@ def convert_timefield_value(self, value, expression, connection):
                 value = parse_time(value)
         return value

+    [USER_REDACTED]
+    def _create_decimal(value):
+        if isinstance(value, (int, str)):
+            return decimal.Decimal(value)
+        return decimal.Context(prec=15).create_decimal_from_float(value)
+
     def get_decimalfield_converter(self, expression):
         # SQLite stores only 15 significant digits. Digits coming from
         # float inaccuracy must be removed.
-        create_decimal = decimal.Context(prec=15).create_decimal_from_float
         if isinstance(expression, Col):
             quantize_value = decimal.Decimal(1).scaleb(
                 -expression.output_field.decimal_places
             )

             def converter(value, expression, connection):
                 if value is not None:
-                    return create_decimal(value).quantize(
+                    return self._create_decimal(value).quantize(
                         quantize_value, context=expression.output_field.context
                     )

         else:

             def converter(value, expression, connection):
                 if value is not None:
-                    return create_decimal(value)
+                    return self._create_decimal(value)

         return converter

@@ -102,6 +102,7 @@ def get_choices():

 class BigD(models.Model):
     d = models.DecimalField(max_digits=32, decimal_places=30)
+    large_int = models.DecimalField(max_digits=16, decimal_places=0, null=True)

 class FloatModel(models.Model):

@@ -5,6 +5,7 @@
 from django.core import validators
 from django.core.exceptions import ValidationError
 from django.db import connection, models
+from django.db.models import Max
 from django.test import TestCase

 from .models import BigD, Foo
@@ -140,3 +141,20 @@ def test_roundtrip_with_trailing_zeros(self):
         obj = Foo.objects.create(a="bar", d=Decimal("8.320"))
         obj.refresh_from_db()
         self.assertEqual(obj.d.compare_total(Decimal("8.320")), Decimal("0"))
+
+    def test_large_integer_precision(self):
+        large_int_val = Decimal("9999999999999999")
+        obj = BigD.objects.create(large_int=large_int_val, d=Decimal("0"))
+        obj.refresh_from_db()
+        self.assertEqual(obj.large_int, large_int_val)
+
+    def test_large_integer_precision_aggregation(self):
+        large_int_val = Decimal("9999999999999999")
+        BigD.objects.create(large_int=large_int_val, d=Decimal("0"))
+        result = BigD.objects.aggregate(max_val=Max("large_int"))
+        self.assertEqual(result["max_val"], large_int_val)
+
+    def test_roundtrip_integer_with_trailing_zeros(self):
+        obj = Foo.objects.create(a="bar", d=Decimal("8"))
+        obj.refresh_from_db()
+        self.assertEqual(obj.d.compare_total(Decimal("8.000")), Decimal("0"))

Thanks, I have an initial comment.

Hi, good start here. A question for you about preserving trailing zeros.

(Clicked wrong review button.)

Thanks for the updates. I think I would find this more clear if you localized the updates to `create_decimal`, e.g.:

```py
[USER_REDACTED]
def _create_decimal(value):
    if isinstance(value, int):
        return decimal.Decimal(value)
    return decimal.Context(prec=15).create_decimal_from_float(value)
```

Thanks! I pushed a tiny edit.

I think we want this test to run on all backends.

Thanks for the review.
1- I removed that block so the test runs for all backends.
2- I added Simon as co-author since I took his fix from the discussions under the ticket
3- I also added another test for the else block

I don't think it's acceptable to skip the `quantize()` call. This test passes on main and fails on your branch:

```py
    def test_roundtrip_integer_with_trailing_zeros(self):
        obj = Foo.objects.create(a="bar", d=Decimal("8"))
        obj.refresh_from_db()
        self.assertEqual(obj.d.compare_total(Decimal("8.000")), Decimal("0"))
```

```py
======================================================================
FAIL: test_roundtrip_integer_with_trailing_zeros (model_fields.test_decimalfield.DecimalFieldTests.test_roundtrip_integer_with_trailing_zeros)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/jwalls/django/tests/model_fields/test_decimalfield.py", line 148, in test_roundtrip_integer_with_trailing_zeros
    self.assertEqual(obj.d.compare_total(Decimal("8.000")), Decimal("0"))
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Decimal('1') != Decimal('0')

----------------------------------------------------------------------
```

Please remove this model and reuse `BigD`. We can't add 1x model per test due to the overhead of creating tables.

Please remove "sqlite" from the test name.

We try to make surgical edits when touching longstanding code. Removing the newline and changing `elif` -> `if` will create a smaller diff.

SQL databases usually have a NUMERIC type with (max_digits, and decimal_places) which takes care of adding the missing precision to `Decimal("8")`. This isn't the case on MongoDB, and similar to SQLite, manual quantizing is necessary to make this test pass:
```diff
diff --git a/django_mongodb_backend/operations.py b/django_mongodb_backend/operations.py
index e446e2c4..f4b0857f 100644
--- a/django_mongodb_backend/operations.py
+++ b/django_mongodb_backend/operations.py
@@ -171,18 +171,20 @@ class DatabaseOperations(GISOperations, BaseDatabaseOperations):
     def convert_decimalfield_value(self, value, expression, connection):
         if value is not None:
             # from Decimal128 to decimal.Decimal()
             try:
                 value = value.to_decimal()
             except AttributeError:
                 # `value` could be an integer in the case of an annotation
                 # like ExpressionWrapper(Value(1), output_field=DecimalField().
                 return Decimal(value)
+            quantize_value = Decimal(1).scaleb(-expression.output_field.decimal_places)
+            value = value.quantize(quantize_value, context=expression.output_field.context)
         return value
```
I tend to think this is unnecessary overhead and this test (which bypasses validation anyway) should be skipped on MongoDB. Raising the issue here in case any patch authors have thoughts. Thanks!

I think the fact that `SQLiteNumericMixin` (and now `Value`) add casts based on the `output_field` is a [deeper design problem](https://github.com/[USER_REDACTED]ngo/django/pull/19459#discussion_r2135725928), given that `output_field` is supposed to be an ORM-only interoperability check, not entailing any database casts.

What you've sketched looks like a better approach than SQLite's approach, tbh.


✅ Test order for the submitted byte Files

Fixes the [14811](https://github.com/fastapi/fastapi/discussions/14811)

@@ -0,0 +1,46 @@
+"""
+Regression test: preserve order when using list[bytes] + File()
+See https://github.com/fastapi/fastapi/discussions/14811
+Related: PR #3372
+"""
+
+from typing import Annotated
+
+import anyio
+import pytest
+from fastapi import FastAPI, File
+from fastapi.testclient import TestClient
+from starlette.datastructures import UploadFile as StarletteUploadFile
+
+
+def test_list_bytes_file_preserves_order(
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    app = FastAPI()
+
+    [USER_REDACTED].post("/upload")
+    async def upload(files: Annotated[list[bytes], File()]):
+        # return something that makes order obvious
+        return [b[0] for b in files]
+
+    original_read = StarletteUploadFile.read
+
+    async def patched_read(self: StarletteUploadFile, size: int = -1) -> bytes:
+        # Make the FIRST file slower *deterministically*
+        if self.filename == "slow.txt":
+            await anyio.sleep(0.05)
+        return await original_read(self, size)
+
+    monkeypatch.setattr(StarletteUploadFile, "read", patched_read)
+
+    client = TestClient(app)
+
+    files = [
+        ("files", ("slow.txt", b"A" * 10, "text/plain")),
+        ("files", ("fast.txt", b"B" * 10, "text/plain")),
+    ]
+    r = client.post("/upload", files=files)
+    assert r.status_code == 200, r.text
+
+    # Must preserve request order: slow first, fast second
+    assert r.json() == [ord("A"), ord("B")]

LGTM!

[USER_REDACTED], thanks!

Great, thank you! 🚀

I refactored this logic in https://github.com/fastapi/fastapi/pull/14884 , that should handle the problem. But I'll still take the test here to prevent it from happening again. 🤓

```suggestion
def test_list_bytes_file_preserves_order(
```


🌐 Update translations for uk (update-outdated)

This PR was created automatically using LLMs.

It uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/uk/llm-prompt.md.

In most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR.

@@ -8,7 +8,7 @@

-    Фреймворк FastAPI: висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину
+    Фреймворк FastAPI - це висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину

@@ -33,14 +33,14 @@

 ---

-FastAPI — це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python.
+FastAPI - це сучасний, швидкий (високопродуктивний) вебфреймворк для створення API за допомогою Python, що базується на стандартних підказках типів Python.

 Ключові особливості:

 * **Швидкий**: дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших Python-фреймворків](#performance).
 * **Швидке написання коду**: пришвидшує розробку функціоналу приблизно на 200%–300%. *
 * **Менше помилок**: зменшує приблизно на 40% кількість помилок, спричинених людиною (розробником). *
-* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження.
+* **Інтуїтивний**: чудова підтримка редакторами коду. Автодоповнення всюди. Менше часу на налагодження.
 * **Простий**: спроєктований так, щоб бути простим у використанні та вивченні. Менше часу на читання документації.
 * **Короткий**: мінімізує дублювання коду. Кілька можливостей з кожного оголошення параметра. Менше помилок.
 * **Надійний**: ви отримуєте код, готовий до продакшину. З автоматичною інтерактивною документацією.
@@ -127,9 +127,9 @@ FastAPI — це сучасний, швидкий (високопродукти

-Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**.
+Якщо ви створюєте застосунок CLI для використання в терміналі замість веб-API, зверніть увагу на **Typer**.

-**Typer** — молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨ 🚀
+**Typer** - молодший брат FastAPI. І його задумано як **FastAPI для CLI**. ⌨ 🚀

 ## Вимоги { #requirements }

@@ -161,8 +161,6 @@ $ pip install "fastapi[standard]"
 Створіть файл `main.py` з:

 ```Python
-from typing import Union
-
 from fastapi import FastAPI

 app = FastAPI()
@@ -174,7 +172,7 @@ def read_root():

 [USER_REDACTED].get("/items/{item_id}")
-def read_item(item_id: int, q: Union[str, None] = None):
+def read_item(item_id: int, q: str | None = None):
     return {"item_id": item_id, "q": q}
 ```

@@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None):

 Якщо ваш код використовує `async` / `await`, скористайтеся `async def`:

-```Python hl_lines="9  14"
-from typing import Union
-
+```Python hl_lines="7  12"
 from fastapi import FastAPI

 app = FastAPI()
@@ -197,7 +193,7 @@ async def read_root():

 [USER_REDACTED].get("/items/{item_id}")
-async def read_item(item_id: int, q: Union[str, None] = None):
+async def read_item(item_id: int, q: str | None = None):
     return {"item_id": item_id, "q": q}
 ```

@@ -288,9 +284,7 @@ INFO:     Application startup complete.

 Оголосіть тіло, використовуючи стандартні типи Python, завдяки Pydantic.

-```Python hl_lines="4  9-12  25-27"
-from typing import Union
-
+```Python hl_lines="2  7-10 23-25"
 from fastapi import FastAPI
 from pydantic import BaseModel

@@ -300,7 +294,7 @@ app = FastAPI()
 class Item(BaseModel):
     name: str
     price: float
-    is_offer: Union[bool, None] = None
+    is_offer: bool | None = None

 [USER_REDACTED].get("/")
@@ -309,7 +303,7 @@ def read_root():

 [USER_REDACTED].get("/items/{item_id}")
-def read_item(item_id: int, q: Union[str, None] = None):
+def read_item(item_id: int, q: str | None = None):
     return {"item_id": item_id, "q": q}

@@ -374,15 +368,15 @@ item: Item
 * Валідацію даних:
     * Автоматичні та зрозумілі помилки, коли дані некоректні.
     * Валідацію навіть для глибоко вкладених JSON-обʼєктів.
-* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з:
+* Перетворення вхідних даних: з мережі до даних і типів Python. Читання з:
     * JSON.
     * Параметрів шляху.
     * Параметрів запиту.
     * Cookies.
     * Headers.
     * Forms.
     * Files.
-* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON):
+* Перетворення вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON):
     * Перетворення типів Python (`str`, `int`, `float`, `bool`, `list`, тощо).
     * Обʼєктів `datetime`.
     * Обʼєктів `UUID`.
@@ -439,9 +433,9 @@ item: Item

 ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png)

-Для більш повного прикладу, що включає більше можливостей, перегляньте Туторіал — Посібник користувача.
+Для більш повного прикладу, що включає більше можливостей, перегляньте Навчальний посібник - Посібник користувача.

-**Spoiler alert**: туторіал — посібник користувача містить:
+**Попередження про спойлер**: навчальний посібник - посібник користувача містить:

 * Оголошення **параметрів** з інших різних місць, як-от: **headers**, **cookies**, **form fields** та **files**.
 * Як встановлювати **обмеження валідації** як `maximum_length` або `regex`.
@@ -500,11 +494,11 @@ Deploying to [NAME_REDACTED]...

 Він забезпечує той самий **developer experience** створення застосунків на FastAPI під час їх **розгортання** у хмарі. 🎉

-[NAME_REDACTED] — основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨
+[NAME_REDACTED] - основний спонсор і джерело фінансування open source проєктів *FastAPI and friends*. ✨

 #### Розгортання в інших хмарних провайдерів { #deploy-to-other-cloud-providers }

-FastAPI — open source і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете.
+FastAPI - open source проект і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете.

 Дотримуйтеся інструкцій вашого хмарного провайдера, щоб розгорнути застосунки FastAPI у нього. 🤓

@@ -530,7 +524,7 @@ FastAPI залежить від Pydantic і Starlette.

 * httpx - потрібно, якщо ви хочете використовувати `TestClient`.
 * jinja2 - потрібно, якщо ви хочете використовувати конфігурацію шаблонів за замовчуванням.
-* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`.
+* python-multipart - потрібно, якщо ви хочете підтримувати «parsing» форм за допомогою `request.form()`.

 Використовується FastAPI:

@@ -103,15 +103,16 @@
 Оскільки за замовчуванням одиничні значення інтерпретуються як параметри запиту, вам не потрібно явно додавати `Query`, ви можете просто зробити:

 ```Python
-q: Union[str, None] = None
+q: str | None = None
 ```

-Або в Python 3.10 і вище:
+Або в Python 3.9:

 ```Python
-q: str | None = None
+q: Union[str, None] = None
 ```

+
 Наприклад:

 {* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *}
@@ -129,7 +130,7 @@ q: str | None = None

 За замовчуванням **FastAPI** очікуватиме його тіло безпосередньо.

-Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього — вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` — `embed`:
+Але якщо ви хочете, щоб він очікував JSON з ключем `item`, а всередині нього - вміст моделі, як це відбувається, коли ви оголошуєте додаткові параметри тіла, ви можете використати спеціальний параметр `Body` - `embed`:

 ```Python
 item: Item = Body(embed=True)

Changes looks good, just a small improvements :)

LLM makes mistakes when trying to translate definitions of terms

```suggestion
* **Інтуїтивний**: чудова підтримка редакторами коду. <abbr title="also known as auto-complete, autocompletion, IntelliSense - відоме як auto-complete, autocompletion, IntelliSense">Автодоповнення</abbr> всюди. Менше часу на налагодження.
```

```suggestion
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - потрібно, якщо ви хочете підтримувати <abbr title="converting the string that comes from an HTTP request into Python datatypes - перетворення строки із HTTP-запиту, у Python типи">«parsing»</abbr> форм за допомогою `request.form()`.
```

```suggestion
* <abbr title="also known as: serialization, parsing, marshalling - відоме як serialization, parsing, marshalling">Перетворення</abbr> вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON):
```

```suggestion
* <abbr title="also known as: serialization, parsing, marshalling - відоме як serialization, parsing, marshalling">Перетворення</abbr> вхідних даних: з мережі до даних і типів Python. Читання з:
```

```suggestion
    <em>Фреймворк FastAPI - це висока продуктивність, легко вивчати, швидко писати код, готовий до продакшину</em>
```

```suggestion
FastAPI - open source проект і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете.
```

```suggestion
Якщо ви створюєте застосунок <abbr title="[NAME_REDACTED]- Інтерфейс командного рядка">CLI</abbr> для використання в терміналі замість веб-API, зверніть увагу на <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>.
```

We can try solving this with https://github.com/fastapi/fastapi/pull/14744 and https://github.com/fastapi/fastapi/pull/14747

Actually, we don't need `also known as: serialization, parsing, marshalling` here - it's a mistake of LLM. It should be just translated to Ukrainian..

Should it be
`<abbr title="також відомо як serialization, parsing, marshalling">`
or
`<abbr title="відоме як serialization, parsing, marshalling">`
?

Same problem with `<abbr>` as above

```suggestion
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - потрібно, якщо ви хочете підтримувати <abbr title="перетворення строки із HTTP-запиту, у Python типи">«parsing»</abbr> форм за допомогою `request.form()`.
```

It should be just translation, without English version

any option works :)

Seems that LLM thinks that an original sentence must be leaved with a translation, but it's mistake: only the translate must be leaved. Also, LLM didn't consider a "Completion" word and, as a result, LLM thought that it must be considered as "also known [to us]" instead of "[A completion is] also known".

Therefore, it must be corrected in this way:
```suggestion
* **Інтуїтивний**: чудова підтримка редакторами коду. <abbr title="також відоме як auto-complete, autocompletion, IntelliSense">Автодоповнення</abbr> всюди. Менше часу на налагодження.
```

UPD: fixed a grammar error

It may be in this way: `<abbr title="також відоме як serialization, parsing, marshalling">`. Also, it can be `<abbr title="відоме як serialization, parsing, marshalling">`, but we're losing a word "also".

Also, there is a same problem on line 43. I explained reasons of the problem there.

UPD: added a variant and explained why is it correct too

"Spoiler alert" is not translated

Can be translated in this way:
```suggestion
**Попередження про спойлер**: навчальний посібник - посібник користувача містить:
```

In this context, it would be good to translate "open source":
```suggestion
FastAPI - відкритий код і базується на стандартах. Ви можете розгортати застосунки FastAPI в будь-якому хмарному провайдері, який ви оберете.
```

In my opinion, it's adding new text rather then a translation. We can leave or translate "open source" and it'll be looking good.

LLM makes a mistake, leaving an original sentence. Also, ["з" will be better than "із"](https://ukr-mova.in.ua/library/orfografiya/mulozvuchnist-z,-iz,-zi,-zo.-chastuna-1).
```suggestion
* <a href="https://github.com/Kludex/python-multipart" target="_blank"><code>python-multipart</code></a> - потрібно, якщо ви хочете підтримувати <abbr title="перетворення строки з HTTP-запиту, у Python типи">«parsing»</abbr> форм за допомогою `request.form()`.
```

```suggestion
* <abbr title="також відоме як serialization, parsing, marshalling">Перетворення</abbr> вхідних даних: з мережі до даних і типів Python. Читання з:
```

```suggestion
* <abbr title="також відоме як serialization, parsing, marshalling">Перетворення</abbr> вихідних даних: перетворення з даних і типів Python у мережеві дані (як JSON):
```

I personally think it's Ok to translate it as "open source проект" ("open source project"). I think it sounds clearer than "відкритий код" (more like "open source code").
I think we should go with "open source проект"

OK, no problem! We can leave "open source проект"


📝 Add banner to translated pages

@@ -0,0 +1,11 @@
+/// details | 🌐 Translation by [NAME_REDACTED]
+
+This translation was made by [NAME_REDACTED] 🤝
+
+It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
+
+You can improve this translation by [helping us guide the AI LLM better](https://fastapi.tiangolo.com/contributing/#translations).
+
+[English version](ENGLISH_VERSION_URL)
+
+///

@@ -0,0 +1,11 @@
+/// details | 🌐 Перевод выполнен с помощью ИИ и людей
+
+Этот перевод был сделан ИИ под руководством людей. 🤝
+
+В нем могут быть ошибки из-за неправильного понимания оригинального смысла или неестественности и т. д. 🤖
+
+Вы можете улучшить этот перевод, [помогая нам лучше направлять ИИ LLM](https://fastapi.tiangolo.com/ru/contributing/#translations).
+
+[Английская версия](ENGLISH_VERSION_URL)
+
+///

@@ -26,6 +26,17 @@ def get_missing_translation_content(docs_dir: str) -> str:
     return missing_translation_path.read_text(encoding="utf-8")

+[USER_REDACTED]_cache
+def get_translation_banner_content(docs_dir: str) -> str:
+    docs_dir_path = Path(docs_dir)
+    translation_banner_path = docs_dir_path / "translation-banner.md"
+    if not translation_banner_path.is_file():
+        translation_banner_path = (
+            docs_dir_path.parent.parent / "en" / "docs" / "translation-banner.md"
+        )
+    return translation_banner_path.read_text(encoding="utf-8")
+
+
 [USER_REDACTED]_cache
 def get_mkdocs_material_langs() -> list[str]:
     material_path = Path(material.__file__).parent
@@ -151,4 +162,21 @@ def on_page_markdown(
         if markdown.startswith("#"):
             header, _, body = markdown.partition("\n\n")
         return f"{header}\n\n{missing_translation_content}\n\n{body}"
-    return markdown
+
+    docs_dir_path = Path(config.docs_dir)
+    en_docs_dir_path = docs_dir_path.parent.parent / "en/docs"
+
+    if docs_dir_path == en_docs_dir_path:
+        return markdown
+
+    # For translated pages add translation banner
+    translation_banner_content = get_translation_banner_content(config.docs_dir)
+    en_url = "https://fastapi.tiangolo.com/" + page.url.lstrip("/")
+    translation_banner_content = translation_banner_content.replace(
+        "ENGLISH_VERSION_URL", en_url
+    )
+    header = ""
+    body = markdown
+    if markdown.startswith("#"):
+        header, _, body = markdown.partition("\n\n")
+    return f"{header}\n\n{translation_banner_content}\n\n{body}"

Looking great! A coupe of suggestions.

Excellent, thank you! 🚀 🎉

Is the line above valid?
I haven't found docs about it. Maybe just a typo?

Ah, yep, it's here: https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/details/

It's the same style as other blocks we use:

```Markdown
/// details | Some summary
Some content
///
```

Looking at this I'm realizing that we already have the data to put there a link to the original in English, that might help.

We could add an:

```Markdown
[English version](https://blahblah/link)
```

Thanks!
Updated

Added link


🌐 Improve LLM prompt of `uk` documentation

Most of terms are not translated by LLM, so the PR fixes this problem. Partially fixes #14700

UPD: add PR which will be partially fixed by [NAME_REDACTED]

@@ -8,6 +8,7 @@ Language code: uk.

 - Use polite/formal address consistent with existing Ukrainian docs (use “ви/ваш”).
 - Keep the tone concise and technical.
+- Use one style of dashes. For example, if text contains "-" then use only this symbol to represent a dash.

 ### Headings

@@ -32,6 +33,71 @@ Use the following preferred translations when they apply in documentation prose:
 - response (HTTP): відповідь
 - path operation: операція шляху
 - path operation function: функція операції шляху
+- prompt: підсказка
+- check: перевірка
+- [NAME_REDACTED] Interface: Інтерфейс Шлюзу Паралельного Сервера
+- [NAME_REDACTED] Network: Мережа Розробників Mozilla
+- tutorial: навчальний посібник
+- advanced user guide: просунутий посібник користувача
+- deep learning: глибоке навчання
+- machine learning: машинне навчання
+- dependency injection: впровадження залежностей
+- digest (HTTP): дайджест
+- basic authentication (HTTP): базова автентифікація
+- JSON schema: Схема JSON
+- password flow: потік паролю
+- mobile: мобільний
+- body: тіло
+- form: форма
+- path: шлях
+- query: запит
+- cookie: кукі
+- header: заголовок
+- startup: запуск
+- shutdown: вимкнення
+- lifespan: тривалість життя
+- authorization: авторизація
+- forwarded header: направлений заголовок
+- dependable: залежний
+- dependent: залежний
+- bound: межа
+- concurrency: рівночасність
+- parallelism: паралелізм
+- multiprocessing: багатопроцесорність
+- env var: змінна оточення
+- dict: словник
+- enum: перелік
+- issue: проблема
+- server worker: серверний працівник
+- worker: працівник
+- software development kit: набір для розробки програмного забезпечення
+- bearer token: токен носія
+- breaking change: несумісна зміна
+- bug: помилка
+- button: кнопка
+- callable: викликаємий
+- code: код
+- commit: фіксація
+- context manager: менеджер контексту
+- coroutine: співпрограма
+- engine: рушій
+- fake X: фальшивий X
+- item: предмет
+- lock: блокування
+- middleware: проміжне програмне забезпечення
+- mounting: монтування
+- origin: джерело
+- override: переписування
+- payload: корисне навантаження
+- processor: процесор
+- property: властивість
+- proxy: представник
+- pull request: запит на витяг
+- random-access memory: пам'ять з довільним доступом
+- status code: код статусу
+- string: строка
+- tag: мітка
+- wildcard: дика карта

 ### `///` admonitions

@@ -44,3 +110,4 @@ Use the following preferred translations when they apply in documentation prose:
 - `/// warning | Попередження`
 - `/// info | Інформація`
 - `/// danger | Обережно`
+- `/// check | Перевірте`

Awesome, thank you! 🙌

data-science - не перекладається

```suggestion
- enum: перелік
```


🐛 Fix TYPE_CHECKING annotations for Python 3.14 (PEP 649)

PR #14485 fixed support for if TYPE_CHECKING, non-evaluated stringified annotations, when using `from __future__ import annotations`.

This change adds the same support when using deferred evaluation of annotations introduced in Python 3.14.

Note: I was also able to reproduce the issue by removing `from __future__ import annotations` from `tests/test_stringified_annotation_dependency.py` (using python 3.14).

It has been [discussed here](https://github.com/fastapi/fastapi/discussions/14784)

@@ -204,7 +204,12 @@ def _get_signature(call: Callable[..., Any]) -> inspect.Signature:
         except NameError:
             # Handle type annotations with if TYPE_CHECKING, not used by FastAPI
             # e.g. dependency return types
-            signature = inspect.signature(call)
+            if sys.version_info >= (3, 14):
+                from annotationlib import Format
+
+                signature = inspect.signature(call, annotation_format=Format.FORWARDREF)
+            else:
+                signature = inspect.signature(call)
     else:
         signature = inspect.signature(call)
     return signature

@@ -0,0 +1,30 @@
+from typing import TYPE_CHECKING, Annotated
+
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+from .utils import needs_py314
+
+if TYPE_CHECKING:  # pragma: no cover
+
+    class DummyUser: ...
+
+
+[USER_REDACTED]_py314
+def test_stringified_annotation():
+    # python3.14: Use forward reference without "from __future__ import annotations"
+    async def get_current_user() -> DummyUser | None:
+        return None
+
+    app = FastAPI()
+
+    client = TestClient(app)
+
+    [USER_REDACTED].get("/")
+    async def get(
+        current_user: Annotated[DummyUser | None, Depends(get_current_user)],
+    ) -> str:
+        return "hello world"
+
+    response = client.get("/")
+    assert response.status_code == 200

@@ -1,4 +1,5 @@
 import importlib
+import sys
 from types import ModuleType
 from typing import Annotated, Any
 from unittest.mock import Mock, patch
@@ -12,8 +13,13 @@
     name="module",
     params=[
         "tutorial008_py39",
-        # Fails with `NameError: name 'DepA' is not defined`
-        pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail),
+        pytest.param(
+            "tutorial008_an_py39",
+            marks=pytest.mark.xfail(
+                sys.version_info < (3, 14),
+                reason="Fails with `NameError: name 'DepA' is not defined`",
+            ),
+        ),
     ],
 )
 def get_module(request: pytest.FixtureRequest):

@@ -6,8 +6,8 @@
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
 )
-needs_py_lt_314 = pytest.mark.skipif(
-    sys.version_info >= (3, 14), reason="requires python3.13-"
+needs_py314 = pytest.mark.skipif(
+    sys.version_info < (3, 14), reason="requires python3.14+"
 )

LGTM!
[USER_REDACTED], thank you!

Probably better title: 🐛 Fix `TYPE_CHECKING` annotations for Python 3.14 (PEP 649)

Nice, thank you! This will be available in FastAPI 0.128.1, released in the next few hours. 🚀

`needs_py_lt_314` wasn't used

I could move this import to top level, wdyt?


📝 Use `WSGIMiddleware` from `a2wsgi` instead of deprecated `fastapi.middleware.wsgi.WSGIMiddleware`

`fastapi.middleware.wsgi.WSGIMiddleware` (re-exported from Starlette) is deprecated:

https://github.com/Kludex/starlette/blob/501b86185461d107869917ced367d5624f82e3fb/starlette/middleware/wsgi.py#L15-L20

@@ -6,13 +6,29 @@ For that, you can use the `WSGIMiddleware` and use it to wrap your WSGI applicat

 ## Using `WSGIMiddleware` { #using-wsgimiddleware }

-You need to import `WSGIMiddleware`.
+/// info
+
+This requires installing `a2wsgi` for example with `pip install a2wsgi`.
+
+///
+
+You need to import `WSGIMiddleware` from `a2wsgi`.

 Then wrap the WSGI (e.g. Flask) app with the middleware.

 And then mount that under a path.

-{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
+{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *}
+
+/// note
+
+Previously, it was recommended to use `WSGIMiddleware` from `fastapi.middleware.wsgi`, but it is now deprecated.
+
+It’s advised to use the `a2wsgi` package instead. The usage remains the same.
+
+Just ensure that you have the `a2wsgi` package installed and import `WSGIMiddleware` correctly from `a2wsgi`.
+
+///

 ## Check it { #check-it }

@@ -35,11 +35,3 @@ It can be imported from `fastapi`:
 ```python
 from fastapi.middleware.trustedhost import TrustedHostMiddleware
 ```
-
-::: fastapi.middleware.wsgi.WSGIMiddleware
-
-It can be imported from `fastapi`:
-
-```python
-from fastapi.middleware.wsgi import WSGIMiddleware
-```

@@ -1,5 +1,5 @@
+from a2wsgi import WSGIMiddleware
 from fastapi import FastAPI
-from fastapi.middleware.wsgi import WSGIMiddleware
 from flask import Flask, request
 from markupsafe import escape

@@ -1 +1,3 @@
-from starlette.middleware.wsgi import WSGIMiddleware as WSGIMiddleware  # noqa
+from starlette.middleware.wsgi import (
+    WSGIMiddleware as WSGIMiddleware,
+)  # pragma: no cover # noqa

@@ -178,6 +178,7 @@ tests = [
     "strawberry-graphql>=0.200.0,<1.0.0",
     "types-orjson==3.6.2",
     "types-ujson==5.10.0.20240515",
+    "a2wsgi>=1.9.0,<=2.0.0",
 ]
 translations = [
     "gitpython==3.1.45",
@@ -231,7 +232,6 @@ xfail_strict = true
 junit_family = "xunit2"
 filterwarnings = [
     "error",
-    'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette',
     # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28
     "ignore:You seem to already have a custom.*:RuntimeWarning:trio",
     # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes

@@ -7,6 +7,18 @@ resolution-markers = [
     "python_full_version < '3.10'",
 ]

+[[package]]
+name = "a2wsgi"
+version = "1.10.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/cb/822c56fbea97e9eee201a2e434a80437f6750ebcb1ed307ee3a0a7505b14/a2wsgi-1.10.10.tar.gz", hash = "sha256:a5bcffb52081ba39df0d5e9a884fc6f819d92e3a42389343ba77cbf809fe1f45", size = 18799, upload-time = "2025-06-18T09:00:10.843Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/02/d5/349aba3dc421e73cbd4958c0ce0a4f1aa3a738bc0d7de75d2f40ed43a535/a2wsgi-1.10.10-py3-none-any.whl", hash = "sha256:d2b21379479718539dc15fce53b876251a0efe7615352dfe49f6ad1bc507848d", size = 17389, upload-time = "2025-06-18T09:00:09.676Z" },
+]
+
 [[package]]
 name = "ag-ui-protocol"
 version = "0.1.10"
@@ -1058,6 +1070,7 @@ standard-no-fastapi-cloud-cli = [

 [package.dev-dependencies]
 dev = [
+    { name = "a2wsgi" },
     { name = "anyio", extra = ["trio"] },
     { name = "black" },
     { name = "cairosvg" },
@@ -1131,6 +1144,7 @@ github-actions = [
     { name = "smokeshow" },
 ]
 tests = [
+    { name = "a2wsgi" },
     { name = "anyio", extra = ["trio"] },
     { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" },
     { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" },
@@ -1197,6 +1211,7 @@ provides-extras = ["standard", "standard-no-fastapi-cloud-cli", "all"]

 [package.metadata.requires-dev]
 dev = [
+    { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" },
     { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" },
     { name = "black", specifier = "==25.1.0" },
     { name = "cairosvg", specifier = "==2.8.2" },
@@ -1266,6 +1281,7 @@ github-actions = [
     { name = "smokeshow", specifier = ">=0.5.0" },
 ]
 tests = [
+    { name = "a2wsgi", specifier = ">=1.9.0,<=2.0.0" },
     { name = "anyio", extras = ["trio"], specifier = ">=3.2.1,<5.0.0" },
     { name = "coverage", extras = ["toml"], specifier = ">=6.5.0,<8.0" },
     { name = "dirty-equals", specifier = "==0.9.0" },

Excellent, thank you! 🚀 🙌

With versions prior to `1.9.0` it gives error `404 Not found`