# Fixed #36246 -- Caught `GDALException` in `BaseGeometryWidget.deserialize`.

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

#### Branch description
@sarahboyce Thank you for providing the test code for this ticket.

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

diff --git a/tests/gis_tests/test_geoforms.py b/tests/gis_tests/test_geoforms.py
index c351edaaad5a..b6068948f358 100644
--- a/tests/gis_tests/test_geoforms.py
+++ b/tests/gis_tests/test_geoforms.py
@@ -435,6 +435,19 @@ def test_get_context_attrs(self):
         context = widget.get_context("geometry", None, None)
         self.assertEqual(context["geom_type"], "Geometry")

+    def test_invalid_values(self):
+        bad_inputs = [
+            "POINT(5)",
+            "MULTI   POLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))",
+            "BLAH(0 0, 1 1)",
+            '{"type": "FeatureCollection", "features": ['
+            '{"geometry": {"type": "Point", "coordinates": [508375, 148905]}, '
+            '"type": "Feature"}]}',
+        ]
+        for input in bad_inputs:
+            with self.subTest(input=input):
+                self.assertIsNone(BaseGeometryWidget().deserialize(input))
+
     def test_subwidgets(self):
         widget = forms.BaseGeometryWidget()
         self.assertEqual(

diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py
index 1fd31530c135..bf6be709ed3a 100644
--- a/django/contrib/gis/forms/fields.py
+++ b/django/contrib/gis/forms/fields.py
@@ -1,5 +1,4 @@
 from django import forms
-from django.contrib.gis.gdal import GDALException
 from django.contrib.gis.geos import GEOSException, GEOSGeometry
 from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
@@ -41,10 +40,7 @@ def to_python(self, value):

         if not isinstance(value, GEOSGeometry):
             if hasattr(self.widget, "deserialize"):
-                try:
-                    value = self.widget.deserialize(value)
-                except GDALException:
-                    value = None
+                value = self.widget.deserialize(value)
             else:
                 try:
                     value = GEOSGeometry(value)
diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py
index 55895ae9f362..c091d3fcfc20 100644
--- a/django/contrib/gis/forms/widgets.py
+++ b/django/contrib/gis/forms/widgets.py
@@ -2,6 +2,7 @@

 from django.conf import settings
 from django.contrib.gis import gdal
+from django.contrib.gis.gdal import GDALException
 from django.contrib.gis.geometry import json_regex
 from django.contrib.gis.geos import GEOSException, GEOSGeometry
 from django.forms.widgets import Widget
@@ -36,7 +37,7 @@ def serialize(self, value):
     def deserialize(self, value):
         try:
             return GEOSGeometry(value)
-        except (GEOSException, ValueError, TypeError) as err:
+        except (GEOSException, GDALException, ValueError, TypeError) as err:
             logger.error("Error creating geometry from value '%s' (%s)", value, err)
         return None


# Fixed #36644 -- Enabled empty order_by() to avoid pk ordering by first()/last().

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

#### Branch description
Enabled calling `order_by()` without arguments to disable implicit primary key ordering in `first()`/`last()`. When a queryset is explicitly marked as unordered via `order_by()`, `first()`/`last()` now respect this and avoid adding PK ordering. Also reset default_ordering in `_combinator_query()` so that `union().first()` can still add pk ordering.

Discussion: https://github.com/django/new-features/issues/71,

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

diff --git a/tests/get_earliest_or_latest/models.py b/tests/get_earliest_or_latest/models.py
index bbf2075d368a..91725865b194 100644
--- a/tests/get_earliest_or_latest/models.py
+++ b/tests/get_earliest_or_latest/models.py
@@ -21,6 +21,14 @@ class Comment(models.Model):
     likes_count = models.PositiveIntegerField()

+class OrderedArticle(models.Model):
+    headline = models.CharField(max_length=100)
+    pub_date = models.DateField()
+
+    class Meta:
+        ordering = ["headline"]
+
+
 # Ticket #23555 - model with an intentionally broken QuerySet.__iter__ method.

diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py
index 49c803b73a0f..793fb12bb9c1 100644
--- a/tests/get_earliest_or_latest/tests.py
+++ b/tests/get_earliest_or_latest/tests.py
@@ -1,9 +1,10 @@
 from datetime import datetime
+from unittest.mock import patch

 from django.db.models import Avg
 from django.test import TestCase

-from .models import Article, Comment, IndexErrorArticle, Person
+from .models import Article, Comment, IndexErrorArticle, OrderedArticle, Person

 class EarliestOrLatestTests(TestCase):
@@ -265,3 +266,30 @@ def test_first_last_unordered_qs_aggregation_error(self):
             qs.first()
         with self.assertRaisesMessage(TypeError, msg % "last"):
             qs.last()
+
+    def test_first_last_empty_order_by_has_no_pk_ordering(self):
+        Article.objects.create(
+            headline="Article 1",
+            pub_date=datetime(2006, 9, 10),
+            expire_date=datetime(2056, 9, 11),
+        )
+
+        qs = Article.objects.order_by()
+        with patch.object(type(qs), "order_by") as mock_order_by:
+            qs.first()
+            mock_order_by.assert_not_called()
+            qs.last()
+            mock_order_by.assert_not_called()
+
+    def test_first_last_empty_order_by_clears_default_ordering(self):
+        OrderedArticle.objects.create(
+            headline="Article 1",
+            pub_date=datetime(2006, 9, 10),
+        )
+
+        qs = OrderedArticle.objects.order_by()
+        with patch.object(type(qs), "order_by") as mock_order_by:
+            qs.first()
+            mock_order_by.assert_not_called()
+            qs.last()
+            mock_order_by.assert_not_called()
diff --git a/tests/queries/test_qs_combinators.py b/tests/queries/test_qs_combinators.py
index 2b4cd2bbbdd1..7e1e01bd4be4 100644
--- a/tests/queries/test_qs_combinators.py
+++ b/tests/queries/test_qs_combinators.py
@@ -418,6 +418,15 @@ def test_union_with_first(self):
         qs2 = base_qs.filter(name="a2")
         self.assertEqual(qs1.union(qs2).first(), a1)

+    @skipUnlessDBFeature("supports_slicing_ordering_in_compound")
+    def test_union_applies_default_ordering_afterward(self):
+        c = Tag.objects.create(name="C")
+        Tag.objects.create(name="B")
+        a = Tag.objects.create(name="A")
+        qs1 = Tag.objects.filter(name__in=["A", "B"])[:1]
+        qs2 = Tag.objects.filter(name__in=["C"])[:1]
+        self.assertSequenceEqual(qs1.union(qs2), [a, c])
+
     def test_union_multiple_models_with_values_list_and_order(self):
         reserved_name = ReservedName.objects.create(name="rn1", order=0)
         qs1 = Celebrity.objects.all()

diff --git a/django/db/models/query.py b/django/db/models/query.py
index 5649a83428f7..76d0f449a67e 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -1158,7 +1158,7 @@ async def alatest(self, *fields):

     def first(self):
         """Return the first object of a query or None if no match is found."""
-        if self.ordered:
+        if self.ordered or not self.query.default_ordering:
             queryset = self
         else:
             self._check_ordering_first_last_queryset_aggregation(method="first")
@@ -1171,7 +1171,7 @@ async def afirst(self):

     def last(self):
         """Return the last object of a query or None if no match is found."""
-        if self.ordered:
+        if self.ordered or not self.query.default_ordering:
             queryset = self.reverse()
         else:
             self._check_ordering_first_last_queryset_aggregation(method="last")
@@ -1679,6 +1679,7 @@ def _combinator_query(self, combinator, *other_qs, all=False):
         clone = self._chain()
         # Clear limits and ordering so they can be reapplied
         clone.query.clear_ordering(force=True)
+        clone.query.default_ordering = True
         clone.query.clear_limits()
         clone.query.combined_queries = (self.query, *(qs.query for qs in other_qs))
         clone.query.combinator = combinator


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

diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index cdf3c00080c9..1d8a447dee8b 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -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):
diff --git a/tests/model_fields/test_decimalfield.py b/tests/model_fields/test_decimalfield.py
index 17f59674e872..bab9a39c19d7 100644
--- a/tests/model_fields/test_decimalfield.py
+++ b/tests/model_fields/test_decimalfield.py
@@ -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"))

diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py
index 23c17054d260..18ff204ae37b 100644
--- a/django/db/backends/sqlite3/operations.py
+++ b/django/db/backends/sqlite3/operations.py
@@ -300,10 +300,15 @@ def convert_timefield_value(self, value, expression, connection):
                 value = parse_time(value)
         return value

+    @staticmethod
+    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
@@ -311,7 +316,7 @@ def get_decimalfield_converter(self, expression):

             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
                     )

@@ -319,7 +324,7 @@ def converter(value, expression, connection):

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

         return converter


# Fixed #36879 -- Identified Django client in Redis client metadata.

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

#### Branch description
Redis documentation recommends that clients identify themselves via connection metadata to help operators monitor, debug, and reason about production systems (for example using CLIENT SETINFO / CLIENT INFO).

This patch allows Django to set its own driver info for redis-py connections, which will be a different `lib-name`:

 `redis-py` -> `redis-py(django_v{django_version})`.

This is achieved by subclassing the redis-py `Connection` class to include Django's custom `lib_name` or `driver_info`.

#### AI Assistance Disclosure (REQUIRED)
<!-- Please select exactly ONE of the following: -->
- [x] **No AI tools were used** in preparing this PR.
- [ ] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index db5df2070124..b36e3a6a0602 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -15,6 +15,7 @@
 from pathlib import Path
 from unittest import mock, skipIf

+import django
 from django.conf import settings
 from django.core import management, signals
 from django.core.cache import (
@@ -1986,6 +1987,24 @@ def test_redis_pool_options(self):
         self.assertEqual(pool.connection_kwargs["socket_timeout"], 0.1)
         self.assertIs(pool.connection_kwargs["retry_on_timeout"], True)

+    def test_client_driver_info(self):
+        client_info = cache._cache.get_client().client_info()
+        if {"lib-name", "lib-ver"}.issubset(client_info):
+            version = django.get_version()
+            if hasattr(self.lib, "DriverInfo"):
+                info = self._lib.DriverInfo().add_upstream_driver("django", version)
+                correct_lib_name = info.formatted_name
+            else:
+                correct_lib_name = f"redis-py(django_v{version})"
+            # Relax the assertion to allow date variance in editable installs.
+            truncated_lib_name = correct_lib_name.rsplit(".dev", maxsplit=1)[0]
+            self.assertIn(truncated_lib_name, client_info["lib-name"])
+            self.assertEqual(client_info["lib-ver"], self.lib.__version__)
+        else:
+            # Redis versions below 7.2 lack CLIENT SETINFO.
+            self.assertNotIn("lib-ver", client_info)
+            self.assertNotIn("lib-name", client_info)
+

 class FileBasedCachePathLibTests(FileBasedCacheTests):
     def mkdtemp(self):

diff --git a/django/core/cache/backends/redis.py b/django/core/cache/backends/redis.py
index bbf070a37573..727ea51f84d6 100644
--- a/django/core/cache/backends/redis.py
+++ b/django/core/cache/backends/redis.py
@@ -4,6 +4,7 @@
 import random
 import re

+import django
 from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
 from django.utils.functional import cached_property
 from django.utils.module_loading import import_string
@@ -59,7 +60,19 @@ def __init__(
             parser_class = import_string(parser_class)
         parser_class = parser_class or self._lib.connection.DefaultParser

-        self._pool_options = {"parser_class": parser_class, **options}
+        version = django.get_version()
+        if hasattr(self._lib, "DriverInfo"):
+            driver_info = self._lib.DriverInfo().add_upstream_driver("django", version)
+            driver_info_options = {"driver_info": driver_info}
+        else:
+            # DriverInfo is not available in this redis-py version.
+            driver_info_options = {"lib_name": f"redis-py(django_v{version})"}
+
+        self._pool_options = {
+            "parser_class": parser_class,
+            **driver_info_options,
+            **options,
+        }

     def _get_connection_pool_index(self, write):
         # Write to the first server. Read from other servers if there are more,


# Fixed #36893 -- Serialized elidable kwarg for RunSQL and RunPython operations.

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

#### Branch description

Fixed an issue where `elidable=True` was not being serialized for Run-SQL and Run-Python operations which caused these operations to lose their elidable status when written to a migration file (e.g., during squashing), reverting to the default `False`.

-Updated deconstruct() for both Run-SQL and Run-Python to correctly include 'elidable' keyword when set to 'True'
-Regression Tests are test_run_sql_elidable , test_run_python_elidable and test_operations.py

#### AI Assistance Disclosure (REQUIRED)
<!-- Please select exactly ONE of the following: -->
- [ ] **No AI tools were used** in preparing this PR.
- [x] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.
Assistance provided by Google DeepMind's Antigravity agent. I used the agent to understand the issue, implement the fix, and add regression tests.
#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [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.

diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index ec4b772c13ed..235804adab60 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -5536,6 +5536,10 @@ def test_run_sql(self):
         elidable_operation = migrations.RunSQL("SELECT 1 FROM void;", elidable=True)
         self.assertEqual(elidable_operation.reduce(operation, []), [operation])

+        # Test elidable deconstruction
+        definition = elidable_operation.deconstruct()
+        self.assertIs(definition[2]["elidable"], True)
+
     def test_run_sql_params(self):
         """
         #23426 - RunSQL should accept parameters.
@@ -5789,6 +5793,10 @@ def create_shetlandponies(models, schema_editor):
         elidable_operation = migrations.RunPython(inner_method, elidable=True)
         self.assertEqual(elidable_operation.reduce(operation, []), [operation])

+        # Test elidable deconstruction
+        definition = elidable_operation.deconstruct()
+        self.assertIs(definition[2]["elidable"], True)
+
     def test_run_python_invalid_reverse_code(self):
         msg = "RunPython must be supplied with callable arguments"
         with self.assertRaisesMessage(ValueError, msg):

diff --git a/django/db/migrations/operations/special.py b/django/db/migrations/operations/special.py
index 07000233253c..311271483ea9 100644
--- a/django/db/migrations/operations/special.py
+++ b/django/db/migrations/operations/special.py
@@ -93,6 +93,8 @@ def deconstruct(self):
             kwargs["state_operations"] = self.state_operations
         if self.hints:
             kwargs["hints"] = self.hints
+        if self.elidable:
+            kwargs["elidable"] = self.elidable
         return (self.__class__.__qualname__, [], kwargs)

     @property
@@ -173,6 +175,8 @@ def deconstruct(self):
             kwargs["atomic"] = self.atomic
         if self.hints:
             kwargs["hints"] = self.hints
+        if self.elidable:
+            kwargs["elidable"] = self.elidable
         return (self.__class__.__qualname__, [], kwargs)

     @property


# Fixed #36865 -- Removed casting from exact lookups in admin searches.

Fixes https://code.djangoproject.com/ticket/36865

## Description

This PR fixes a performance regression introduced in PR #17885.

The `Cast` to `CharField` for non-string field exact lookups prevents database index usage on primary key fields, generating SQL like:

```sql
WHERE ("table"."id")::varchar = '123'
```

This cannot use the primary key index and causes full table scans, resulting in timeouts on large tables (reported in [ticket #26001 comment 24](https://code.djangoproject.com/ticket/26001#comment:24)), and discovered in a project I work on at https://github.com/freelawproject/courtlistener/issues/6790.

## Solution

Drop the casting approach introduced in #17885, and replace it with field validation that uses `to_python` on the results of `smart_split` to ensure that we only apply field-term pairs that make sense. For example:

 - searching a PrimaryKey field for "foo" --> Nope!
 - searching a TextField for "foo" --> Yes!
 - searching a TextField for "123" --> Close enough. Yes!

And so forth.

This should ensure DB indexes are used and that we don't send nonsensical queries in the first place.

## Trac tickets

Introducing issue: https://code.djangoproject.com/ticket/26001
Solving it: https://code.djangoproject.com/ticket/36865

diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py
index a84c27a06662..0b594300d2a3 100644
--- a/tests/admin_changelist/models.py
+++ b/tests/admin_changelist/models.py
@@ -140,3 +140,10 @@ class CharPK(models.Model):
 class ProxyUser(User):
     class Meta:
         proxy = True
+
+
+class MixedFieldsModel(models.Model):
+    """Model with multiple field types for testing search validation."""
+
+    int_field = models.IntegerField(null=True, blank=True)
+    json_field = models.JSONField(null=True, blank=True)
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index 319d6259f6a9..e0772a3e6d4a 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -66,6 +66,7 @@
     Group,
     Invitation,
     Membership,
+    MixedFieldsModel,
     Musician,
     OrderedObject,
     Parent,
@@ -856,6 +857,89 @@ def test_custom_lookup_with_pk_shortcut(self):
         cl = m.get_changelist_instance(request)
         self.assertCountEqual(cl.queryset, [abcd])

+    def test_exact_lookup_with_invalid_value(self):
+        Child.objects.create(name="Test", age=10)
+        m = admin.ModelAdmin(Child, custom_site)
+        m.search_fields = ["pk__exact"]
+
+        request = self.factory.get("/", data={SEARCH_VAR: "foo"})
+        request.user = self.superuser
+
+        # Invalid values are gracefully ignored.
+        cl = m.get_changelist_instance(request)
+        self.assertCountEqual(cl.queryset, [])
+
+    def test_exact_lookup_mixed_terms(self):
+        """
+        Multi-term search validates each term independently.
+
+        For 'foo 123' with search_fields=['name__icontains', 'age__exact']:
+        - 'foo': age lookup skipped (invalid), name lookup used
+        - '123': both lookups used (valid for age)
+        No Cast should be used; invalid lookups are simply skipped.
+        """
+        child = Child.objects.create(name="foo123", age=123)
+        Child.objects.create(name="other", age=456)
+        m = admin.ModelAdmin(Child, custom_site)
+        m.search_fields = ["name__icontains", "age__exact"]
+
+        request = self.factory.get("/", data={SEARCH_VAR: "foo 123"})
+        request.user = self.superuser
+
+        # One result matching on foo and 123.
+        cl = m.get_changelist_instance(request)
+        self.assertCountEqual(cl.queryset, [child])
+
+        # "xyz" - invalid for age (skipped), no match for name either.
+        request = self.factory.get("/", data={SEARCH_VAR: "xyz"})
+        request.user = self.superuser
+        cl = m.get_changelist_instance(request)
+        self.assertCountEqual(cl.queryset, [])
+
+    def test_exact_lookup_with_more_lenient_formfield(self):
+        """
+        Exact lookups on BooleanField use formfield().to_python() for lenient
+        parsing. Using model field's to_python() would reject 'false' whereas
+        the form field accepts it.
+        """
+        obj = UnorderedObject.objects.create(bool=False)
+        UnorderedObject.objects.create(bool=True)
+        m = admin.ModelAdmin(UnorderedObject, custom_site)
+        m.search_fields = ["bool__exact"]
+
+        # 'false' is accepted by form field but rejected by model field.
+        request = self.factory.get("/", data={SEARCH_VAR: "false"})
+        request.user = self.superuser
+
+        cl = m.get_changelist_instance(request)
+        self.assertCountEqual(cl.queryset, [obj])
+
+    def test_exact_lookup_validates_each_field_independently(self):
+        """
+        Each field validates the search term independently without leaking
+        converted values between fields.
+
+        "3." is valid for IntegerField (converts to 3) but invalid for
+        JSONField. The converted value must not leak to the JSONField check.
+        """
+        # obj_int has int_field=3, should match "3." via IntegerField.
+        obj_int = MixedFieldsModel.objects.create(
+            int_field=3, json_field={"key": "value"}
+        )
+        # obj_json has json_field=3, should NOT match "3." because "3." is
+        # invalid JSON.
+        MixedFieldsModel.objects.create(int_field=99, json_field=3)
+        m = admin.ModelAdmin(MixedFieldsModel, custom_site)
+        m.search_fields = ["int_field__exact", "json_field__exact"]
+
+        # "3." is valid for int (becomes 3) but invalid JSON.
+        # Only obj_int should match via int_field.
+        request = self.factory.get("/", data={SEARCH_VAR: "3."})
+        request.user = self.superuser
+
+        cl = m.get_changelist_instance(request)
+        self.assertCountEqual(cl.queryset, [obj_int])
+
     def test_search_with_exact_lookup_for_non_string_field(self):
         child = Child.objects.create(name="Asher", age=11)
         model_admin = ChildAdmin(Child, custom_site)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 69b7031e8260..9c787d232912 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -41,7 +41,6 @@
 from django.core.paginator import Paginator
 from django.db import models, router, transaction
 from django.db.models.constants import LOOKUP_SEP
-from django.db.models.functions import Cast
 from django.forms.formsets import DELETION_FIELD_NAME, all_valid
 from django.forms.models import (
     BaseInlineFormSet,
@@ -1137,6 +1136,12 @@ def get_search_results(self, request, queryset, search_term):

         # Apply keyword searches.
         def construct_search(field_name):
+            """
+            Return a tuple of (lookup, field_to_validate).
+
+            field_to_validate is set for non-text exact lookups so that
+            invalid search terms can be skipped (preserving index usage).
+            """
             if field_name.startswith("^"):
                 return "%s__istartswith" % field_name.removeprefix("^"), None
             elif field_name.startswith("="):
@@ -1148,7 +1153,7 @@ def construct_search(field_name):
             lookup_fields = field_name.split(LOOKUP_SEP)
             # Go through the fields, following all relations.
             prev_field = None
-            for i, path_part in enumerate(lookup_fields):
+            for path_part in lookup_fields:
                 if path_part == "pk":
                     path_part = opts.pk.name
                 try:
@@ -1159,15 +1164,9 @@ def construct_search(field_name):
                         if path_part == "exact" and not isinstance(
                             prev_field, (models.CharField, models.TextField)
                         ):
-                            field_name_without_exact = "__".join(lookup_fields[:i])
-                            alias = Cast(
-                                field_name_without_exact,
-                                output_field=models.CharField(),
-                            )
-                            alias_name = "_".join(lookup_fields[:i])
-                            return f"{alias_name}_str", alias
-                        else:
-                            return field_name, None
+                            # Use prev_field to validate the search term.
+                            return field_name, prev_field
+                        return field_name, None
                 else:
                     prev_field = field
                     if hasattr(field, "path_infos"):
@@ -1179,30 +1178,42 @@ def construct_search(field_name):
         may_have_duplicates = False
         search_fields = self.get_search_fields(request)
         if search_fields and search_term:
-            str_aliases = {}
             orm_lookups = []
             for field in search_fields:
-                lookup, str_alias = construct_search(str(field))
-                orm_lookups.append(lookup)
-                if str_alias:
-                    str_aliases[lookup] = str_alias
-
-            if str_aliases:
-                queryset = queryset.alias(**str_aliases)
+                orm_lookups.append(construct_search(str(field)))

             term_queries = []
             for bit in smart_split(search_term):
                 if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
                     bit = unescape_string_literal(bit)
-                or_queries = models.Q.create(
-                    [(orm_lookup, bit) for orm_lookup in orm_lookups],
-                    connector=models.Q.OR,
-                )
-                term_queries.append(or_queries)
-            queryset = queryset.filter(models.Q.create(term_queries))
+                # Build term lookups, skipping values invalid for their field.
+                bit_lookups = []
+                for orm_lookup, validate_field in orm_lookups:
+                    if validate_field is not None:
+                        formfield = validate_field.formfield()
+                        try:
+                            if formfield is not None:
+                                value = formfield.to_python(bit)
+                            else:
+                                # Fields like AutoField lack a form field.
+                                value = validate_field.to_python(bit)
+                        except ValidationError:
+                            # Skip this lookup for invalid values.
+                            continue
+                    else:
+                        value = bit
+                    bit_lookups.append((orm_lookup, value))
+                if bit_lookups:
+                    or_queries = models.Q.create(bit_lookups, connector=models.Q.OR)
+                    term_queries.append(or_queries)
+                else:
+                    # No valid lookups: add a filter that returns nothing.
+                    term_queries.append(models.Q(pk__in=[]))
+            if term_queries:
+                queryset = queryset.filter(models.Q.create(term_queries))
             may_have_duplicates |= any(
                 lookup_spawns_duplicates(self.opts, search_spec)
-                for search_spec in orm_lookups
+                for search_spec, _ in orm_lookups
             )
         return queryset, may_have_duplicates


# Fixed #36847 -- Ensured auto_now_add fields are set before FileField.upload_to is called.

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

#### Branch description
In Django 5.2 and earlier, an `auto_now_add=True` `DateTimeField` could be accessed inside a `FileField`’s `upload_to` callable, because the field was populated with the current timestamp during insertion.
This is a regression introduced in 94680437a45a71c70ca8bd2e68b72aa1e2eff337 where the insert path called `field.pre_save()` with `add=False` instead of `add=True`.

This fix restores the correct behavior by changing the argument back to `add=True`.
(Used AI to help create the test coverage for the fix. I have verified that it's working correctly.)

#### AI Assistance Disclosure (REQUIRED)
<!-- Please select exactly ONE of the following: -->
- [ ] **No AI tools were used** in preparing this PR.
- [x] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [x] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 1d8a447dee8b..a594b89adb0d 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -262,10 +262,20 @@ class DataModel(models.Model):
 # FileField

+def upload_to_with_date(instance, filename):
+    return f"{instance.created_at.year}/{filename}"
+
+
 class Document(models.Model):
     myfile = models.FileField(storage=temp_storage, upload_to="unused", unique=True)

+# See ticket #36847.
+class DocumentWithTimestamp(models.Model):
+    created_at = models.DateTimeField(auto_now_add=True)
+    myfile = models.FileField(storage=temp_storage, upload_to=upload_to_with_date)
+
+
 ###############################################################################
 # ImageField

diff --git a/tests/model_fields/test_filefield.py b/tests/model_fields/test_filefield.py
index 57cc7365da15..fbf5c837ac57 100644
--- a/tests/model_fields/test_filefield.py
+++ b/tests/model_fields/test_filefield.py
@@ -13,7 +13,7 @@
 from django.test import TestCase, override_settings
 from django.test.utils import isolate_apps

-from .models import Document
+from .models import Document, DocumentWithTimestamp

 class FileFieldTests(TestCase):
@@ -209,3 +209,9 @@ class MyDocument(AbstractMyDocument):

         document = MyDocument(myfile="test_file.py")
         self.assertEqual(document.myfile.field.model, MyDocument)
+
+    def test_upload_to_callable_sees_auto_now_add_field_value(self):
+        d = DocumentWithTimestamp(myfile=ContentFile(b"content", name="foo"))
+        d.save()
+        self.assertIsNotNone(d.created_at)
+        self.assertIs(d.myfile.name.startswith(f"{d.created_at.year}/foo"), True)

diff --git a/django/db/models/base.py b/django/db/models/base.py
index ad3f0c5e23a1..d53da600d792 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -1175,7 +1175,9 @@ def _save_table(
             ].features.can_return_columns_from_insert
             for field in insert_fields:
                 value = (
-                    getattr(self, field.attname) if raw else field.pre_save(self, False)
+                    getattr(self, field.attname)
+                    if raw
+                    else field.pre_save(self, add=True)
                 )
                 if hasattr(value, "resolve_expression"):
                     if field not in returning_fields:


# Fixed #36878 -- Unified data type for *_together options in ModelState.

Ever since the beginning of Django's migration framework, there's been a bit of an inconsistency on how index_together and unique_together values have been stored on the ModelState[^1].

It's only really obvious, when looking at the current code for `from_model()`[^2] and the `rename_field()` state alteration code[^3].

The problem in the autodetector's detection of the `*_together` options as raised in the ticket, reinforces the inconsistency[^4]: the old value is being normalized to a set of tuples, whereas the new value is taken as-is.

Why this hasn't been caught before, is likely to the fact, that we never really look at a `to_state` that comes from migration operations in the autodetector. Instead, in both usages in Django[^5], [^6] the `to_state` is a `ProjectState.from_apps()`. And that state is consistently using sets of tuples and not lists of lists.

[^1]: https://github.com/django/django/commit/67dcea711e92025d0e8676b869b7ef15dbc6db73#diff-5dd147e9e978e645313dd99eab3a7bab1f1cb0a53e256843adb68aeed71e61dcR85-R87
[^2]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L842
[^3]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L340-L345
[^4]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/autodetector.py#L1757-L1771
[^5]: https://github.com/django/django/blob/2351c1b12cc9cf82d642f769c774bc3ea0cc4006/django/core/management/commands/makemigrations.py#L215-L219
[^6]: https://github.com/django/django/blob/2351c1b12cc9cf82d642f769c774bc3ea0cc4006/django/core/management/commands/migrate.py#L329-L332

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

#### Branch description
Provide a concise overview of the issue or rationale behind the proposed changes.

#### AI Assistance Disclosure (REQUIRED)
<!-- Please select exactly ONE of the following: -->
- [x] **No AI tools were used** in preparing this PR.
- [ ] **If AI tools were used**, I have disclosed which ones, and fully reviewed and verified their output.

#### Checklist
- [x] This PR follows the [contribution guidelines](https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/submitting-patches/).
- [x] This PR **does not** disclose a security vulnerability (see [vulnerability reporting](https://docs.djangoproject.com/en/stable/internals/security/)).
- [x] This PR targets the `main` branch. <!-- Backports will be evaluated and done by mergers, when necessary. -->
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [x] I have added or updated relevant tests.
- [ ] I have added or updated relevant docs, including release notes if applicable.
- [ ] I have attached screenshots in both light and dark modes for any UI changes.

diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py
index e33362185555..7a66e500cb89 100644
--- a/tests/migrations/test_autodetector.py
+++ b/tests/migrations/test_autodetector.py
@@ -5590,6 +5590,51 @@ def test_remove_composite_pk(self):
             preserve_default=True,
         )

+    def test_does_not_crash_after_rename_on_unique_together(self):
+        fields = ("first", "second")
+        before = self.make_project_state(
+            [
+                ModelState(
+                    "app",
+                    "Foo",
+                    [
+                        ("id", models.AutoField(primary_key=True)),
+                        ("first", models.IntegerField()),
+                        ("second", models.IntegerField()),
+                    ],
+                    options={"unique_together": {fields}},
+                ),
+            ]
+        )
+        after = before.clone()
+        after.rename_field("app", "foo", "first", "first_renamed")
+
+        changes = MigrationAutodetector(
+            before, after, MigrationQuestioner({"ask_rename": True})
+        )._detect_changes()
+
+        self.assertNumberMigrations(changes, "app", 1)
+        self.assertOperationTypes(
+            changes, "app", 0, ["RenameField", "AlterUniqueTogether"]
+        )
+        self.assertOperationAttributes(
+            changes,
+            "app",
+            0,
+            0,
+            model_name="foo",
+            old_name="first",
+            new_name="first_renamed",
+        )
+        self.assertOperationAttributes(
+            changes,
+            "app",
+            0,
+            1,
+            name="foo",
+            unique_together={("first_renamed", "second")},
+        )
+

 class MigrationSuggestNameTests(SimpleTestCase):
     def test_no_operations(self):
diff --git a/tests/migrations/test_base.py b/tests/migrations/test_base.py
index 24ae59ca1b9a..31967f9ed49c 100644
--- a/tests/migrations/test_base.py
+++ b/tests/migrations/test_base.py
@@ -290,14 +290,13 @@ def set_up_test_model(
     ):
         """Creates a test model state and database table."""
         # Make the "current" state.
-        model_options = {
-            "swappable": "TEST_SWAP_MODEL",
-            "unique_together": [["pink", "weight"]] if unique_together else [],
-        }
+        model_options = {"swappable": "TEST_SWAP_MODEL"}
         if options:
             model_options["permissions"] = [("can_groom", "Can groom")]
         if db_table:
             model_options["db_table"] = db_table
+        if unique_together:
+            model_options["unique_together"] = {("pink", "weight")}
         operations = [
             migrations.CreateModel(
                 "Pony",
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 00c76538e0fb..e859b62a101b 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -3336,11 +3336,11 @@ def test_rename_field_unique_together(self):
         # unique_together has the renamed column.
         self.assertIn(
             "blue",
-            new_state.models["test_rnflut", "pony"].options["unique_together"][0],
+            list(new_state.models["test_rnflut", "pony"].options["unique_together"])[0],
         )
         self.assertNotIn(
             "pink",
-            new_state.models["test_rnflut", "pony"].options["unique_together"][0],
+            list(new_state.models["test_rnflut", "pony"].options["unique_together"])[0],
         )
         # Rename field.
         self.assertColumnExists("test_rnflut_pony", "pink")
@@ -3377,7 +3377,7 @@ def test_rename_field_index_together(self):
                     ("weight", models.FloatField()),
                 ],
                 options={
-                    "index_together": [("weight", "pink")],
+                    "index_together": {("weight", "pink")},
                 },
             ),
         ]
@@ -3390,10 +3390,12 @@ def test_rename_field_index_together(self):
         self.assertNotIn("pink", new_state.models["test_rnflit", "pony"].fields)
         # index_together has the renamed column.
         self.assertIn(
-            "blue", new_state.models["test_rnflit", "pony"].options["index_together"][0]
+            "blue",
+            list(new_state.models["test_rnflit", "pony"].options["index_together"])[0],
         )
         self.assertNotIn(
-            "pink", new_state.models["test_rnflit", "pony"].options["index_together"][0]
+            "pink",
+            list(new_state.models["test_rnflit", "pony"].options["index_together"])[0],
         )

         # Rename field.
@@ -3952,7 +3954,7 @@ def test_rename_index_unnamed_index(self):
                     ("weight", models.FloatField()),
                 ],
                 options={
-                    "index_together": [("weight", "pink")],
+                    "index_together": {("weight", "pink")},
                 },
             ),
         ]
@@ -3972,6 +3974,11 @@ def test_rename_index_unnamed_index(self):
         )
         new_state = project_state.clone()
         operation.state_forwards(app_label, new_state)
+        # Ensure the model state has the correct type for the index_together
+        # option.
+        self.assertIsInstance(
+            new_state.models[app_label, "pony"].options["index_together"], set
+        )
         # Rename index.
         with connection.schema_editor() as editor:
             operation.database_forwards(app_label, editor, project_state, new_state)
@@ -4079,7 +4086,7 @@ def test_rename_index_state_forwards_unnamed_index(self):
                     ("weight", models.FloatField()),
                 ],
                 options={
-                    "index_together": [("weight", "pink")],
+                    "index_together": {("weight", "pink")},
                 },
             ),
         ]

diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index 802aeb0b5e66..9e9cc58fae13 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -192,9 +192,10 @@ def alter_model_options(self, app_label, model_name, options, option_keys=None):
     def remove_model_options(self, app_label, model_name, option_name, value_to_remove):
         model_state = self.models[app_label, model_name]
         if objs := model_state.options.get(option_name):
-            model_state.options[option_name] = [
-                obj for obj in objs if tuple(obj) != tuple(value_to_remove)
-            ]
+            new_value = [obj for obj in objs if tuple(obj) != tuple(value_to_remove)]
+            if option_name in {"index_together", "unique_together"}:
+                new_value = set(normalize_together(new_value))
+            model_state.options[option_name] = new_value
         self.reload_model(app_label, model_name, delay=True)

     def alter_model_managers(self, app_label, model_name, managers):
@@ -339,10 +340,10 @@ def rename_field(self, app_label, model_name, old_name, new_name):
         options = model_state.options
         for option in ("index_together", "unique_together"):
             if option in options:
-                options[option] = [
-                    [new_name if n == old_name else n for n in together]
+                options[option] = {
+                    tuple(new_name if n == old_name else n for n in together)
                     for together in options[option]
-                ]
+                }
         # Fix to_fields to refer to the new field.
         delay = True
         references = get_references(self, model_key, (old_name, found))


# Fixed #36812 -- Dropped support for MariaDB < 10.11.

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

#### Branch description
 Dropped the support for MariaDB 10.6-10.10.

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

diff --git a/tests/backends/mysql/tests.py b/tests/backends/mysql/tests.py
index 15228d254fc1..237e7f94472b 100644
--- a/tests/backends/mysql/tests.py
+++ b/tests/backends/mysql/tests.py
@@ -106,8 +106,8 @@ class Tests(TestCase):
     @mock.patch.object(connection, "get_database_version")
     def test_check_database_version_supported(self, mocked_get_database_version):
         if connection.mysql_is_mariadb:
-            mocked_get_database_version.return_value = (10, 5)
-            msg = "MariaDB 10.6 or later is required (found 10.5)."
+            mocked_get_database_version.return_value = (10, 10)
+            msg = "MariaDB 10.11 or later is required (found 10.10)."
         else:
             mocked_get_database_version.return_value = (8, 0, 31)
             msg = "MySQL 8.4 or later is required (found 8.0.31)."

diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index 4f61e2bdf9af..eb9601bcef44 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -64,7 +64,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     @cached_property
     def minimum_database_version(self):
         if self.connection.mysql_is_mariadb:
-            return (10, 6)
+            return (10, 11)
         else:
             return (8, 4)

@@ -207,8 +207,6 @@ def can_introspect_json_field(self):
     def supports_index_column_ordering(self):
         if self._mysql_storage_engine != "InnoDB":
             return False
-        if self.connection.mysql_is_mariadb:
-            return self.connection.mysql_version >= (10, 8)
         return True

     @cached_property
@@ -220,8 +218,7 @@ def supports_expression_indexes(self):

     @cached_property
     def has_native_uuid_field(self):
-        is_mariadb = self.connection.mysql_is_mariadb
-        return is_mariadb and self.connection.mysql_version >= (10, 7)
+        return self.connection.mysql_is_mariadb

     @cached_property
     def allows_group_by_selected_pks(self):


# Refs #26709 -- Made Index raise ValueError on non-string fields.

diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
index 69570a806233..64e0e232290a 100644
--- a/tests/admin_views/admin.py
+++ b/tests/admin_views/admin.py
@@ -18,7 +18,12 @@
 from django.utils.safestring import mark_safe
 from django.views.decorators.common import no_append_slash

-from .forms import MediaActionForm
+from .forms import (
+    MediaActionForm,
+    SectionFormWithDynamicOptgroups,
+    SectionFormWithObjectOptgroups,
+    SectionFormWithOptgroups,
+)
 from .models import (
     Actor,
     AdminOrderedAdminMethod,
@@ -1345,6 +1350,32 @@ class CourseAdmin(admin.ModelAdmin):
 site7 = admin.AdminSite(name="admin7")
 site7.register(Article, ArticleAdmin2)
 site7.register(Section)
+
+
+# Admin for testing optgroup in popup response
+class SectionAdminWithOptgroups(admin.ModelAdmin):
+    form = SectionFormWithOptgroups
+
+
+class SectionAdminWithObjectOptgroups(admin.ModelAdmin):
+    form = SectionFormWithObjectOptgroups
+
+
+class SectionAdminWithDynamicOptgroups(admin.ModelAdmin):
+    form = SectionFormWithDynamicOptgroups
+
+
+site11 = admin.AdminSite(name="admin11")
+site11.register(Article, ArticleAdmin2)
+site11.register(Section, SectionAdminWithOptgroups)
+
+site12 = admin.AdminSite(name="admin12")
+site12.register(Article, ArticleAdmin2)
+site12.register(Section, SectionAdminWithObjectOptgroups)
+
+site13 = admin.AdminSite(name="admin13")
+site13.register(Article, ArticleAdmin2)
+site13.register(Section, SectionAdminWithDynamicOptgroups)
 site7.register(PrePopulatedPost, PrePopulatedPostReadOnlyAdmin)
 site7.register(
     Pizza,
diff --git a/tests/admin_views/forms.py b/tests/admin_views/forms.py
index 3a3566c10f12..f15a5b3ac195 100644
--- a/tests/admin_views/forms.py
+++ b/tests/admin_views/forms.py
@@ -1,7 +1,10 @@
+from django import forms
 from django.contrib.admin.forms import AdminAuthenticationForm, AdminPasswordChangeForm
 from django.contrib.admin.helpers import ActionForm
 from django.core.exceptions import ValidationError

+from .models import Section
+

 class CustomAdminAuthenticationForm(AdminAuthenticationForm):
     class Media:
@@ -23,3 +26,63 @@ def __init__(self, *args, **kwargs):
 class MediaActionForm(ActionForm):
     class Media:
         js = ["path/to/media.js"]
+
+
+class SectionFormWithOptgroups(forms.ModelForm):
+    articles = forms.ChoiceField(
+        choices=[
+            ("Published", [("1", "Test Article")]),
+            ("Draft", [("2", "Other Article")]),
+        ],
+        required=False,
+    )
+
+    class Meta:
+        model = Section
+        fields = ["name", "articles"]
+
+
+class SectionFormWithObjectOptgroups(forms.ModelForm):
+    """Form with model instances as optgroup keys (tests str() conversion)."""
+
+    articles = forms.ChoiceField(required=False)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        # Use Section instances as optgroup keys
+        sections = Section.objects.all()[:2]
+        if sections:
+            self.fields["articles"].choices = [
+                (sections[0], [("1", "Article 1")]),
+                (
+                    sections[1] if len(sections) > 1 else sections[0],
+                    [("2", "Article 2")],
+                ),
+            ]
+
+    class Meta:
+        model = Section
+        fields = ["name", "articles"]
+
+
+class SectionFormWithDynamicOptgroups(forms.ModelForm):
+    """
+    Form where the field with optgroups is added dynamically in __init__.
+    This tests that the implementation doesn't rely on accessing the
+    uninstantiated form class's _meta or fields, which wouldn't work here.
+    """
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        # Dynamically add a field with optgroups after instantiation.
+        self.fields["articles"] = forms.ChoiceField(
+            choices=[
+                ("Category A", [("1", "Item 1"), ("2", "Item 2")]),
+                ("Category B", [("3", "Item 3"), ("4", "Item 4")]),
+            ],
+            required=False,
+        )
+
+    class Meta:
+        model = Section
+        fields = ["name"]
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index f7eaad659e6c..3377a6d44177 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -11,7 +11,7 @@
 from django.contrib.admin import AdminSite, ModelAdmin
 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
 from django.contrib.admin.models import ADDITION, DELETION, LogEntry
-from django.contrib.admin.options import TO_FIELD_VAR
+from django.contrib.admin.options import SOURCE_MODEL_VAR, TO_FIELD_VAR
 from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
 from django.contrib.admin.tests import AdminSeleniumTestCase
 from django.contrib.admin.utils import quote
@@ -468,6 +468,126 @@ def test_popup_add_POST(self):
         response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
         self.assertContains(response, "title with a new\\nline")

+    def test_popup_add_POST_with_valid_source_model(self):
+        """
+        Popup add with a valid source_model returns a successful response.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.section",
+            "title": "Test Article",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "data-popup-response")
+        messages = list(response.wsgi_request._messages)
+        self.assertEqual(len(messages), 0)
+
+    def test_popup_add_POST_with_optgroups(self):
+        """
+        Popup add with source_model containing optgroup choices includes
+        the optgroup in the response.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.section",
+            "title": "Test Article",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        response = self.client.post(
+            reverse("admin11:admin_views_article_add"), post_data
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, ""optgroup": "Published"")
+
+    def test_popup_add_POST_without_optgroups(self):
+        """
+        Popup add where source_model form exists but doesn't have the field
+        should work without crashing.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.section",
+            "title": "Test Article 2",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        # Use regular admin (not admin11) where Section doesn't have optgroups.
+        response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "data-popup-response")
+        self.assertNotContains(response, ""optgroup"")
+
+    def test_popup_add_POST_with_object_optgroups(self):
+        """
+        Popup add with source_model containing optgroups where the optgroup
+        keys are model instances (not strings) still serialize to strings.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.section",
+            "title": "Article 1",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        response = self.client.post(
+            reverse("admin12:admin_views_article_add"), post_data
+        )
+        self.assertEqual(response.status_code, 200)
+        # Check that optgroup is in the response with str() of Section instance
+        # The form uses Section.objects.all()[:2] which includes cls.s1
+        # ("Test section") as the first optgroup key (HTML encoded).
+        self.assertContains(response, ""optgroup": "Test section"")
+
+    def test_popup_add_POST_with_dynamic_optgroups(self):
+        """
+        Popup add with source_model where optgroup field is added dynamically
+        in __init__. This ensures the implementation doesn't rely on accessing
+        the uninstantiated form class's _meta or fields, but instead properly
+        instantiates the form with get_form(request)() to access field info.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.section",
+            "title": "Item 1",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        response = self.client.post(
+            reverse("admin13:admin_views_article_add"), post_data
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, ""optgroup": "Category A"")
+
+    def test_popup_add_POST_with_invalid_source_model(self):
+        """
+        Popup add with an invalid source_model (non-existent app/model)
+        shows an error message instead of crashing.
+        """
+        post_data = {
+            IS_POPUP_VAR: "1",
+            SOURCE_MODEL_VAR: "admin_views.nonexistent",
+            "title": "Test Article",
+            "content": "some content",
+            "date_0": "2010-09-10",
+            "date_1": "14:55:39",
+        }
+        response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, "data-popup-response")
+        messages = list(response.wsgi_request._messages)
+        self.assertEqual(len(messages), 1)
+        self.assertIn("admin_views.nonexistent", str(messages[0]))
+        self.assertIn("could not be found", str(messages[0]))
+
     def test_basic_edit_POST(self):
         """
         A smoke test to ensure POST on edit_view works.
diff --git a/tests/admin_views/urls.py b/tests/admin_views/urls.py
index c1e673d81105..3c43b8721dc4 100644
--- a/tests/admin_views/urls.py
+++ b/tests/admin_views/urls.py
@@ -32,6 +32,9 @@ def non_admin_view(request):
     ),
     path("test_admin/admin9/", admin.site9.urls),
     path("test_admin/admin10/", admin.site10.urls),
+    path("test_admin/admin11/", admin.site11.urls),
+    path("test_admin/admin12/", admin.site12.urls),
+    path("test_admin/admin13/", admin.site13.urls),
     path("test_admin/has_permission_admin/", custom_has_permission_admin.site.urls),
     path("test_admin/autocomplete_admin/", autocomplete_site.urls),
     # Shares the admin URL prefix.
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 7588c2cc32a1..e0ae5b77471c 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -971,7 +971,7 @@ def test_data_model_ref_when_model_name_is_camel_case(self):
           </select>

+             href="/admin_widgets/releaseevent/add/?_to_field=album&_popup=1&_source_model=admin_widgets.videostream">

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 2de07fde7e33..69b7031e8260 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -8,6 +8,7 @@
 from urllib.parse import urlsplit

 from django import forms
+from django.apps import apps
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.admin import helpers, widgets
@@ -71,6 +72,7 @@
 from django.views.generic import RedirectView

 IS_POPUP_VAR = "_popup"
+SOURCE_MODEL_VAR = "_source_model"
 TO_FIELD_VAR = "_to_field"
 IS_FACETS_VAR = "_facets"

@@ -1342,6 +1344,7 @@ def render_change_form(
                 "save_on_top": self.save_on_top,
                 "to_field_var": TO_FIELD_VAR,
                 "is_popup_var": IS_POPUP_VAR,
+                "source_model_var": SOURCE_MODEL_VAR,
                 "app_label": app_label,
             }
         )
@@ -1398,12 +1401,39 @@ def response_add(self, request, obj, post_url_continue=None):
             else:
                 attr = obj._meta.pk.attname
             value = obj.serializable_value(attr)
-            popup_response_data = json.dumps(
-                {
-                    "value": str(value),
-                    "obj": str(obj),
-                }
-            )
+            popup_response = {
+                "value": str(value),
+                "obj": str(obj),
+            }
+
+            # Find the optgroup for the new item, if available
+            source_model_name = request.POST.get(SOURCE_MODEL_VAR)
+
+            if source_model_name:
+                app_label, model_name = source_model_name.split(".", 1)
+                try:
+                    source_model = apps.get_model(app_label, model_name)
+                except LookupError:
+                    msg = _('The app "%s" could not be found.') % source_model_name
+                    self.message_user(request, msg, messages.ERROR)
+                else:
+                    source_admin = self.admin_site._registry[source_model]
+                    form = source_admin.get_form(request)()
+                    if self.opts.verbose_name_plural in form.fields:
+                        field = form.fields[self.opts.verbose_name_plural]
+                        for option_value, option_label in field.choices:
+                            # Check if this is an optgroup (label is a sequence
+                            # of choices rather than a single string value).
+                            if isinstance(option_label, (list, tuple)):
+                                # It's an optgroup:
+                                # (group_name, [(value, label), ...])
+                                optgroup_label = option_value
+                                for choice_value, choice_display in option_label:
+                                    if choice_display == str(obj):
+                                        popup_response["optgroup"] = str(optgroup_label)
+                                        break
+
+            popup_response_data = json.dumps(popup_response)
             return TemplateResponse(
                 request,
                 self.popup_response_template
@@ -1913,6 +1943,7 @@ def _changeform_view(self, request, object_id, form_url, extra_context):
             "object_id": object_id,
             "original": obj,
             "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
+            "source_model": request.GET.get(SOURCE_MODEL_VAR),
             "to_field": to_field,
             "media": media,
             "inline_admin_formsets": inline_formsets,
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 40a6b3bf3a86..cd40f14ce37a 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -11,6 +11,7 @@
 from django.contrib.admin.options import (
     IS_FACETS_VAR,
     IS_POPUP_VAR,
+    SOURCE_MODEL_VAR,
     TO_FIELD_VAR,
     IncorrectLookupParameters,
     ShowFacets,
@@ -49,6 +50,7 @@
     SEARCH_VAR,
     IS_FACETS_VAR,
     IS_POPUP_VAR,
+    SOURCE_MODEL_VAR,
     TO_FIELD_VAR,
 )

diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index f5c393901254..67eac083e793 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -333,16 +333,24 @@ def get_related_url(self, info, action, *args):
         )

     def get_context(self, name, value, attrs):
-        from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
+        from django.contrib.admin.views.main import (
+            IS_POPUP_VAR,
+            SOURCE_MODEL_VAR,
+            TO_FIELD_VAR,
+        )

         rel_opts = self.rel.model._meta
         info = (rel_opts.app_label, rel_opts.model_name)
         related_field_name = self.rel.get_related_field().name
+        app_label = self.rel.field.model._meta.app_label
+        model_name = self.rel.field.model._meta.model_name
+
         url_params = "&".join(
             "%s=%s" % param
             for param in [
                 (TO_FIELD_VAR, related_field_name),
                 (IS_POPUP_VAR, 1),
+                (SOURCE_MODEL_VAR, f"{app_label}.{model_name}"),
             ]
         )
         context = {


# Fixed #36030 -- Fixed precision loss in division of Decimal literals on SQLite.

#### Trac ticket number
ticket-36030

#### Branch description

On SQLite, ``SQLiteNumericMixin.as_sqlite()`` currently wraps all ``DecimalField`` expressions in ``CAST(... AS NUMERIC)``, including literal ``Value(Decimal(...))`` expressions.
For these literals, the cast is unnecessary and differs from how other backends pass Decimal parameters.

The solution decouples ``Value`` from ``SQLiteNumericMixin`` by implementing a dedicated ``as_sqlite`` method that explicitly casts ``Decimal`` literals to ``REAL``, ensuring correct floating-point arithmetic is used. To prevent regressions, the implementation retains the existing ``CAST(... AS NUMERIC)`` wrapper for ``non-Decimal`` types (such as integers) when they are output to a DecimalField.

**Tests**
A new regression test, ``test_decimal_division_literal_value``, has been added to ``BasicExpressionsTests`` to verify that division yields the correct fractional precision on all backends.

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

diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index 981d84e9e8ec..02126fa89612 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -137,6 +137,16 @@ def test_annotate_values_aggregate(self):
         )
         self.assertEqual(companies["result"], 2395)

+    def test_decimal_division_literal_value(self):
+        """
+        Division with a literal Decimal value preserves precision.
+        """
+        num = Number.objects.create(integer=2)
+        obj = Number.objects.annotate(
+            val=F("integer") / Value(Decimal("3.0"), output_field=DecimalField())
+        ).get(pk=num.pk)
+        self.assertAlmostEqual(obj.val, Decimal("0.6667"), places=4)
+
     def test_annotate_values_filter(self):
         companies = (
             Company.objects.annotate(

diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 6b90a42cf1d2..baa91cc2c173 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -1141,7 +1141,7 @@ def allowed_default(self):

 @deconstructible(path="django.db.models.Value")
-class Value(SQLiteNumericMixin, Expression):
+class Value(Expression):
     """Represent a wrapped value as a node within an expression."""

     # Provide a default value for `for_save` in order to allow unresolved
@@ -1182,6 +1182,18 @@ def as_sql(self, compiler, connection):
             return "NULL", []
         return "%s", [val]

+    def as_sqlite(self, compiler, connection, **extra_context):
+        sql, params = self.as_sql(compiler, connection, **extra_context)
+        try:
+            if self.output_field.get_internal_type() == "DecimalField":
+                if isinstance(self.value, Decimal):
+                    sql = "(CAST(%s AS REAL))" % sql
+                else:
+                    sql = "(CAST(%s AS NUMERIC))" % sql
+        except FieldError:
+            pass
+        return sql, params
+
     def resolve_expression(
         self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
     ):


# Fixed #36639 -- Added CI step to run makemigrations --check against test models.

#### Trac ticket number

ticket-36639

#### Branch description
This PR automated migration consistency checks for the test models with current migrations and added missing migration files .
I have tested this PR on my local with "act" package.

SS before fixing  migrations:

SS after fixing migrations:

#### Checklist
- [x] This PR targets the `main` branch.
- [x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
- [x] I have checked the "Has patch" ticket flag in the Trac system.
- [ ] I have added or updated relevant tests.
- [ ] 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.

diff --git a/tests/db_functions/migrations/0002_create_test_models.py b/tests/db_functions/migrations/0002_create_test_models.py
index 6c4626e7ea44..2fa924cbe005 100644
--- a/tests/db_functions/migrations/0002_create_test_models.py
+++ b/tests/db_functions/migrations/0002_create_test_models.py
@@ -10,6 +10,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="Author",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("name", models.CharField(max_length=50)),
                 ("alias", models.CharField(max_length=50, null=True, blank=True)),
                 ("goes_by", models.CharField(max_length=50, null=True, blank=True)),
@@ -19,6 +28,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="Article",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 (
                     "authors",
                     models.ManyToManyField(
@@ -37,6 +55,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="Fan",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("name", models.CharField(max_length=50)),
                 ("age", models.PositiveSmallIntegerField(default=30)),
                 (
@@ -51,6 +78,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="DTModel",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("name", models.CharField(max_length=32)),
                 ("start_datetime", models.DateTimeField(null=True, blank=True)),
                 ("end_datetime", models.DateTimeField(null=True, blank=True)),
@@ -64,6 +100,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="DecimalModel",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("n1", models.DecimalField(decimal_places=2, max_digits=6)),
                 (
                     "n2",
@@ -76,6 +121,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="IntegerModel",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("big", models.BigIntegerField(null=True, blank=True)),
                 ("normal", models.IntegerField(null=True, blank=True)),
                 ("small", models.SmallIntegerField(null=True, blank=True)),
@@ -84,6 +138,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="FloatModel",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("f1", models.FloatField(null=True, blank=True)),
                 ("f2", models.FloatField(null=True, blank=True)),
             ],
@@ -91,6 +154,15 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
             name="UUIDModel",
             fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
                 ("uuid", models.UUIDField(null=True)),
                 ("shift", models.DurationField(null=True)),
             ],
diff --git a/tests/gis_tests/rasterapp/migrations/0002_rastermodels.py b/tests/gis_tests/rasterapp/migrations/0002_rastermodels.py
index bd2a72ab45ad..c47cd044c82a 100644
--- a/tests/gis_tests/rasterapp/migrations/0002_rastermodels.py
+++ b/tests/gis_tests/rasterapp/migrations/0002_rastermodels.py
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
             fields=[
                 (
                     "id",
-                    models.AutoField(
+                    models.BigAutoField(
                         auto_created=True,
                         primary_key=True,
                         serialize=False,
@@ -49,7 +49,7 @@ class Migration(migrations.Migration):
             fields=[
                 (
                     "id",
-                    models.AutoField(
+                    models.BigAutoField(
                         auto_created=True,
                         primary_key=True,
                         serialize=False,
diff --git a/tests/migration_test_data_persistence/migrations/0002_add_book.py b/tests/migration_test_data_persistence/migrations/0002_add_book.py
index c355428f34bc..325ac6668452 100644
--- a/tests/migration_test_data_persistence/migrations/0002_add_book.py
+++ b/tests/migration_test_data_persistence/migrations/0002_add_book.py
@@ -1,4 +1,4 @@
-from django.db import migrations
+from django.db import migrations, models

 def add_book(apps, schema_editor):
@@ -16,4 +16,29 @@ class Migration(migrations.Migration):
         migrations.RunPython(
             add_book,
         ),
+        migrations.CreateModel(
+            name="Unmanaged",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("title", models.CharField(max_length=100)),
+            ],
+            options={
+                "managed": False,
+            },
+        ),
+        migrations.AlterField(
+            model_name="book",
+            name="id",
+            field=models.BigAutoField(
+                auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
+            ),
+        ),
     ]
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index d2f9eceb9931..9e1831b5e901 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -108,7 +108,7 @@ class Migration(migrations.Migration):
                 ("tags", ArrayField(TagField(), blank=True, null=True)),
                 (
                     "json",
-                    ArrayField(models.JSONField(default=dict), default=list),
+                    ArrayField(models.JSONField(default=dict), default=list, null=True),
                 ),
                 ("int_ranges", ArrayField(IntegerRangeField(), null=True, blank=True)),
                 (
@@ -179,7 +179,7 @@ class Migration(migrations.Migration):
                 ),
                 (
                     "field",
-                    ArrayField(models.FloatField(), size=2, null=True, blank=True),
+                    ArrayField(models.FloatField(), size=3),
                 ),
             ],
             options={
diff --git a/tests/sites_framework/migrations/0001_initial.py b/tests/sites_framework/migrations/0001_initial.py
index c5721ee08e38..dbd491027639 100644
--- a/tests/sites_framework/migrations/0001_initial.py
+++ b/tests/sites_framework/migrations/0001_initial.py
@@ -1,3 +1,5 @@
+import django.contrib.sites.managers
+import django.db.models.manager
 from django.db import migrations, models

@@ -12,11 +14,11 @@ class Migration(migrations.Migration):
             fields=[
                 (
                     "id",
-                    models.AutoField(
-                        verbose_name="ID",
-                        serialize=False,
+                    models.BigAutoField(
                         auto_created=True,
                         primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
                     ),
                 ),
                 ("title", models.CharField(max_length=50)),
@@ -28,6 +30,15 @@ class Migration(migrations.Migration):
             options={
                 "abstract": False,
             },
+            managers=[
+                ("objects", django.db.models.manager.Manager()),
+                (
+                    "on_site",
+                    django.contrib.sites.managers.CurrentSiteManager(
+                        "places_this_article_should_appear"
+                    ),
+                ),
+            ],
             bases=(models.Model,),
         ),
         migrations.CreateModel(
@@ -35,11 +46,11 @@ class Migration(migrations.Migration):
             fields=[
                 (
                     "id",
-                    models.AutoField(
-                        verbose_name="ID",
-                        serialize=False,
+                    models.BigAutoField(
                         auto_created=True,
                         primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
                     ),
                 ),
                 ("title", models.CharField(max_length=50)),
@@ -48,6 +59,10 @@ class Migration(migrations.Migration):
             options={
                 "abstract": False,
             },
+            managers=[
+                ("objects", django.db.models.manager.Manager()),
+                ("on_site", django.contrib.sites.managers.CurrentSiteManager()),
+            ],
             bases=(models.Model,),
         ),
         migrations.CreateModel(
@@ -55,11 +70,11 @@ class Migration(migrations.Migration):
             fields=[
                 (
                     "id",
-                    models.AutoField(
-                        verbose_name="ID",
-                        serialize=False,
+                    models.BigAutoField(
                         auto_created=True,
                         primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
                     ),
                 ),
                 ("title", models.CharField(max_length=50)),
@@ -68,6 +83,10 @@ class Migration(migrations.Migration):
             options={
                 "abstract": False,
             },
+            managers=[
+                ("objects", django.db.models.manager.Manager()),
+                ("on_site", django.contrib.sites.managers.CurrentSiteManager()),
+            ],
             bases=(models.Model,),
         ),
     ]

diff --git a/scripts/check_migrations.py b/scripts/check_migrations.py
new file mode 100644
index 000000000000..70d187e14433
--- /dev/null
+++ b/scripts/check_migrations.py
@@ -0,0 +1,31 @@
+import sys
+from pathlib import Path
+
+
+def main():
+    repo_root = Path(__file__).resolve().parent.parent
+    sys.path[:0] = [str(repo_root / "tests"), str(repo_root)]
+
+    from runtests import ALWAYS_INSTALLED_APPS, get_apps_to_install, get_test_modules
+
+    import django
+    from django.apps import apps
+    from django.core.management import call_command
+
+    django.setup()
+
+    test_modules = list(get_test_modules(gis_enabled=False))
+    installed_apps = list(ALWAYS_INSTALLED_APPS)
+    for app in get_apps_to_install(test_modules):
+        # Check against the list to prevent duplicate errors.
+        if app not in installed_apps:
+            installed_apps.append(app)
+    apps.set_installed_apps(installed_apps)
+
+    # Note: We don't use check=True here because --check calls sys.exit(1)
+    # instead of raising CommandError when migrations are missing.
+    call_command("makemigrations", "--check", verbosity=3)
+
+
+if __name__ == "__main__":
+    main()


# Fixed #36352 -- Improved error message for fields excluded by prior values()/values_list() calls.

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

Added context-aware error messages to distinguish between masked annotations and unpromoted aliases in chained values()/values_list() calls.

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

diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
index 6336cabafae9..69a2b4a7c7bc 100644
--- a/tests/annotations/tests.py
+++ b/tests/annotations/tests.py
@@ -482,6 +482,17 @@ def test_values_wrong_annotation(self):
         with self.assertRaisesMessage(FieldError, expected_message % article_fields):
             Book.objects.annotate(annotation=Value(1)).values_list("annotation_typo")

+    def test_chained_values_masked_annotation_error_message(self):
+        msg = (
+            "Cannot select the 'author_id' alias. It was excluded by a "
+            "previous values() or values_list() call. Include 'author_id' in "
+            "that call to select it."
+        )
+        with self.assertRaisesMessage(FieldError, msg):
+            Book.objects.annotate(
+                author_name=F("authors__name"), author_id=F("authors__id")
+            ).values("author_name").values("author_id")
+
     def test_decimal_annotation(self):
         salary = Decimal(10) ** -Employee._meta.get_field("salary").decimal_places
         Employee.objects.create(

diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index e3e6100f24f0..4be450167d78 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -2585,10 +2585,17 @@ def set_values(self, fields):
                         annotation_names.append(f)
                         selected[f] = f
                     elif f in self.annotations:
-                        raise FieldError(
-                            f"Cannot select the '{f}' alias. Use annotate() to "
-                            "promote it."
-                        )
+                        if self.annotation_select:
+                            raise FieldError(
+                                f"Cannot select the '{f}' alias. It was excluded "
+                                f"by a previous values() or values_list() call. "
+                                f"Include '{f}' in that call to select it."
+                            )
+                        else:
+                            raise FieldError(
+                                f"Cannot select the '{f}' alias. Use annotate() "
+                                f"to promote it."
+                            )
                     else:
                         # Call `names_to_path` to ensure a FieldError including
                         # annotations about to be masked as valid choices if