# Fixed #36822 -- Added parameter limit for PostgreSQL with server-side binding.

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

#### Branch description
Added max_query_params and bulk_batch_size() to the PostgreSQL backend to handle the 65535 parameter limit when using server-side binding with psycopg3. When server_side_binding=True is configured in database OPTIONS, PostgreSQL has a hard limit of 65535 query parameters.

#### 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/backends/base/test_operations.py b/tests/backends/base/test_operations.py
index 96fadb4c7afa..8be1d3e841b1 100644
--- a/tests/backends/base/test_operations.py
+++ b/tests/backends/base/test_operations.py
@@ -1,7 +1,7 @@
 import decimal

 from django.core.management.color import no_style
-from django.db import NotSupportedError, connection, transaction
+from django.db import NotSupportedError, connection, models, transaction
 from django.db.backends.base.operations import BaseDatabaseOperations
 from django.db.models import DurationField
 from django.db.models.expressions import Col
@@ -11,10 +11,11 @@
     TransactionTestCase,
     override_settings,
     skipIfDBFeature,
+    skipUnlessDBFeature,
 )
 from django.utils import timezone

-from ..models import Author, Book
+from ..models import Author, Book, Person

 class SimpleDatabaseOperationTests(SimpleTestCase):
@@ -201,6 +202,55 @@ def test_subtract_temporals(self):
         with self.assertRaisesMessage(NotSupportedError, msg):
             self.ops.subtract_temporals(duration_field_internal_type, None, None)

+    @skipUnlessDBFeature("max_query_params")
+    def test_bulk_batch_size_limited(self):
+        max_query_params = connection.features.max_query_params
+        objects = range(max_query_params + 1)
+        first_name_field = Person._meta.get_field("first_name")
+        last_name_field = Person._meta.get_field("last_name")
+        composite_pk = models.CompositePrimaryKey("first_name", "last_name")
+        composite_pk.fields = [first_name_field, last_name_field]
+
+        self.assertEqual(connection.ops.bulk_batch_size([], objects), len(objects))
+        self.assertEqual(
+            connection.ops.bulk_batch_size([first_name_field], objects),
+            max_query_params,
+        )
+        self.assertEqual(
+            connection.ops.bulk_batch_size(
+                [first_name_field, last_name_field], objects
+            ),
+            max_query_params // 2,
+        )
+        self.assertEqual(
+            connection.ops.bulk_batch_size([composite_pk, first_name_field], objects),
+            max_query_params // 3,
+        )
+
+    @skipIfDBFeature("max_query_params")
+    def test_bulk_batch_size_unlimited(self):
+        objects = range(2**16 + 1)
+        first_name_field = Person._meta.get_field("first_name")
+        last_name_field = Person._meta.get_field("last_name")
+        composite_pk = models.CompositePrimaryKey("first_name", "last_name")
+        composite_pk.fields = [first_name_field, last_name_field]
+
+        self.assertEqual(connection.ops.bulk_batch_size([], objects), len(objects))
+        self.assertEqual(
+            connection.ops.bulk_batch_size([first_name_field], objects),
+            len(objects),
+        )
+        self.assertEqual(
+            connection.ops.bulk_batch_size(
+                [first_name_field, last_name_field], objects
+            ),
+            len(objects),
+        )
+        self.assertEqual(
+            connection.ops.bulk_batch_size([composite_pk, first_name_field], objects),
+            len(objects),
+        )
+

 class SqlFlushTests(TransactionTestCase):
     available_apps = ["backends"]
diff --git a/tests/backends/oracle/test_operations.py b/tests/backends/oracle/test_operations.py
index 1f9447bde709..197ac5a422ba 100644
--- a/tests/backends/oracle/test_operations.py
+++ b/tests/backends/oracle/test_operations.py
@@ -1,7 +1,7 @@
 import unittest

 from django.core.management.color import no_style
-from django.db import connection, models
+from django.db import connection
 from django.test import TransactionTestCase

 from ..models import Person, Tag
@@ -17,31 +17,6 @@ def test_sequence_name_truncation(self):
         )
         self.assertEqual(seq_name, "SCHEMA_AUTHORWITHEVENLOB0B8_SQ")

-    def test_bulk_batch_size(self):
-        # Oracle restricts the number of parameters in a query.
-        objects = range(2**16)
-        self.assertEqual(connection.ops.bulk_batch_size([], objects), len(objects))
-        # Each field is a parameter for each object.
-        first_name_field = Person._meta.get_field("first_name")
-        last_name_field = Person._meta.get_field("last_name")
-        self.assertEqual(
-            connection.ops.bulk_batch_size([first_name_field], objects),
-            connection.features.max_query_params,
-        )
-        self.assertEqual(
-            connection.ops.bulk_batch_size(
-                [first_name_field, last_name_field],
-                objects,
-            ),
-            connection.features.max_query_params // 2,
-        )
-        composite_pk = models.CompositePrimaryKey("first_name", "last_name")
-        composite_pk.fields = [first_name_field, last_name_field]
-        self.assertEqual(
-            connection.ops.bulk_batch_size([composite_pk, first_name_field], objects),
-            connection.features.max_query_params // 3,
-        )
-
     def test_sql_flush(self):
         statements = connection.ops.sql_flush(
             no_style(),
diff --git a/tests/backends/postgresql/test_features.py b/tests/backends/postgresql/test_features.py
new file mode 100644
index 000000000000..c63f26a7bb13
--- /dev/null
+++ b/tests/backends/postgresql/test_features.py
@@ -0,0 +1,14 @@
+import unittest
+
+from django.db import connection
+from django.test import TestCase
+
+
+@unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
+class FeaturesTests(TestCase):
+    def test_max_query_params_respects_server_side_params(self):
+        if connection.features.uses_server_side_binding:
+            limit = 2**16 - 1
+        else:
+            limit = None
+        self.assertEqual(connection.features.max_query_params, limit)
diff --git a/tests/backends/sqlite/test_operations.py b/tests/backends/sqlite/test_operations.py
index 0c2772301b17..dee55b639067 100644
--- a/tests/backends/sqlite/test_operations.py
+++ b/tests/backends/sqlite/test_operations.py
@@ -2,7 +2,7 @@
 import unittest

 from django.core.management.color import no_style
-from django.db import connection, models
+from django.db import connection
 from django.test import TestCase

 from ..models import Person, Tag
@@ -88,29 +88,6 @@ def test_sql_flush_sequences_allow_cascade(self):
             statements[-1],
         )

-    def test_bulk_batch_size(self):
-        self.assertEqual(connection.ops.bulk_batch_size([], [Person()]), 1)
-        first_name_field = Person._meta.get_field("first_name")
-        last_name_field = Person._meta.get_field("last_name")
-        self.assertEqual(
-            connection.ops.bulk_batch_size([first_name_field], [Person()]),
-            connection.features.max_query_params,
-        )
-        self.assertEqual(
-            connection.ops.bulk_batch_size(
-                [first_name_field, last_name_field], [Person()]
-            ),
-            connection.features.max_query_params // 2,
-        )
-        composite_pk = models.CompositePrimaryKey("first_name", "last_name")
-        composite_pk.fields = [first_name_field, last_name_field]
-        self.assertEqual(
-            connection.ops.bulk_batch_size(
-                [composite_pk, first_name_field], [Person()]
-            ),
-            connection.features.max_query_params // 3,
-        )
-
     def test_bulk_batch_size_respects_variable_limit(self):
         first_name_field = Person._meta.get_field("first_name")
         last_name_field = Person._meta.get_field("last_name")
diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py
index 3001847455cb..3653beceedfe 100644
--- a/tests/composite_pk/tests.py
+++ b/tests/composite_pk/tests.py
@@ -149,21 +149,18 @@ def test_in_bulk(self):

     def test_in_bulk_batching(self):
         Comment.objects.all().delete()
-        batching_required = connection.features.max_query_params is not None
-        expected_queries = 2 if batching_required else 1
+        num_objects = 10
+        connection.features.__dict__.pop("max_query_params", None)
         with unittest.mock.patch.object(
-            type(connection.features), "max_query_params", 10
+            type(connection.features), "max_query_params", num_objects
         ):
-            num_requiring_batching = (
-                connection.ops.bulk_batch_size([Comment._meta.pk], []) + 1
-            )
             comments = [
                 Comment(id=i, tenant=self.tenant, user=self.user)
-                for i in range(1, num_requiring_batching + 1)
+                for i in range(1, num_objects + 1)
             ]
             Comment.objects.bulk_create(comments)
             id_list = list(Comment.objects.values_list("pk", flat=True))
-            with self.assertNumQueries(expected_queries):
+            with self.assertNumQueries(2):
                 comment_dict = Comment.objects.in_bulk(id_list=id_list)
         self.assertQuerySetEqual(comment_dict, id_list)

diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index e34570143898..fac62158996c 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -2,12 +2,14 @@
 import decimal
 import json
 from importlib import import_module
+from itertools import chain

 import sqlparse

 from django.conf import settings
 from django.db import NotSupportedError, transaction
 from django.db.models.expressions import Col
+from django.db.models.fields.composite import CompositePrimaryKey
 from django.utils import timezone
 from django.utils.duration import duration_microseconds
 from django.utils.encoding import force_str
@@ -78,7 +80,17 @@ def bulk_batch_size(self, fields, objs):
         are the fields going to be inserted in the batch, the objs contains
         all the objects to be inserted.
         """
-        return len(objs)
+        if self.connection.features.max_query_params is None or not fields:
+            return len(objs)
+
+        return self.connection.features.max_query_params // len(
+            list(
+                chain.from_iterable(
+                    field.fields if isinstance(field, CompositePrimaryKey) else [field]
+                    for field in fields
+                )
+            )
+        )

     def format_for_duration_arithmetic(self, sql):
         raise NotImplementedError(
diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py
index 75f80941b3bb..802f27a3c6a2 100644
--- a/django/db/backends/oracle/operations.py
+++ b/django/db/backends/oracle/operations.py
@@ -1,7 +1,6 @@
 import datetime
 import uuid
 from functools import lru_cache
-from itertools import chain

 from django.conf import settings
 from django.db import NotSupportedError
@@ -9,7 +8,6 @@
 from django.db.backends.utils import split_tzname_delta, strip_quotes, truncate_name
 from django.db.models import (
     AutoField,
-    CompositePrimaryKey,
     Exists,
     ExpressionWrapper,
     Lookup,
@@ -707,18 +705,6 @@ def subtract_temporals(self, internal_type, lhs, rhs):
             )
         return super().subtract_temporals(internal_type, lhs, rhs)

-    def bulk_batch_size(self, fields, objs):
-        """Oracle restricts the number of parameters in a query."""
-        fields = list(
-            chain.from_iterable(
-                field.fields if isinstance(field, CompositePrimaryKey) else [field]
-                for field in fields
-            )
-        )
-        if fields:
-            return self.connection.features.max_query_params // len(fields)
-        return len(objs)
-
     def conditional_expression_supported_in_where_clause(self, expression):
         """
         Oracle supports only EXISTS(...) or filters in the WHERE clause, others
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 7d313467d41f..b663adc90cf2 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -151,6 +151,12 @@ def uses_server_side_binding(self):
         options = self.connection.settings_dict["OPTIONS"]
         return is_psycopg3 and options.get("server_side_binding") is True

+    @cached_property
+    def max_query_params(self):
+        if self.uses_server_side_binding:
+            return 2**16 - 1
+        return None
+
     @cached_property
     def prohibits_null_characters_in_text_exception(self):
         if is_psycopg3:
diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py
index ac98324b2e30..23c17054d260 100644
--- a/django/db/backends/sqlite3/operations.py
+++ b/django/db/backends/sqlite3/operations.py
@@ -29,26 +29,6 @@ class DatabaseOperations(BaseDatabaseOperations):
     # SQLite. Use JSON_TYPE() instead.
     jsonfield_datatype_values = frozenset(["null", "false", "true"])

-    def bulk_batch_size(self, fields, objs):
-        """
-        SQLite has a variable limit defined by SQLITE_LIMIT_VARIABLE_NUMBER
-        (reflected in max_query_params).
-        """
-        fields = list(
-            chain.from_iterable(
-                (
-                    field.fields
-                    if isinstance(field, models.CompositePrimaryKey)
-                    else [field]
-                )
-                for field in fields
-            )
-        )
-        if fields:
-            return self.connection.features.max_query_params // len(fields)
-        else:
-            return len(objs)
-
     def check_expression_support(self, expression):
         bad_fields = (models.DateField, models.DateTimeField, models.TimeField)
         bad_aggregates = (models.Sum, models.Avg, models.Variance, models.StdDev)


# Fixed #35402 -- Fixed crash in DatabaseFeatures.django_test_skips when running a subset of tests.

#### Trac ticket number
ticket-35402

#### Branch description
Continued from #18203.

Because the prior implementation of this skip machinery sent module names to `import_string()`, it was relying on a fragile behavior that depended on, in the case of submodules, the parent modules having already been imported, which isn't guaranteed.

We might want to take up Tim's suggestion in the ticket of deprecating providing module names to `import_string()` at some point.

#### 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.
- [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/base/test_creation.py b/tests/backends/base/test_creation.py
index 2542407f0bc4..8fd8f490b28f 100644
--- a/tests/backends/base/test_creation.py
+++ b/tests/backends/base/test_creation.py
@@ -1,6 +1,7 @@
 import copy
 import datetime
 import os
+import sys
 from unittest import mock

 from django.db import DEFAULT_DB_ALIAS, connection, connections
@@ -333,6 +334,10 @@ def test_mark_expected_failures_and_skips(self):
                 "backends.base.test_creation.skip_test_function",
             },
         }
+        # Emulate the scenario where the parent module for
+        # backends.base.test_creation has not been imported yet.
+        popped_module = sys.modules.pop("backends.base")
+        self.addCleanup(sys.modules.__setitem__, "backends.base", popped_module)
         creation.mark_expected_failures_and_skips()
         self.assertIs(
             expected_failure_test_function.__unittest_expecting_failure__,

diff --git a/django/db/backends/base/creation.py b/django/db/backends/base/creation.py
index ed2b0738a5a1..5087f80b7ebd 100644
--- a/django/db/backends/base/creation.py
+++ b/django/db/backends/base/creation.py
@@ -358,27 +358,39 @@ def mark_expected_failures_and_skips(self):
         Mark tests in Django's test suite which are expected failures on this
         database and test which should be skipped on this database.
         """
-        # Only load unittest if we're actually testing.
-        from unittest import expectedFailure, skip
-
         for test_name in self.connection.features.django_test_expected_failures:
-            test_case_name, _, test_method_name = test_name.rpartition(".")
-            test_app = test_name.split(".")[0]
-            # Importing a test app that isn't installed raises RuntimeError.
-            if test_app in settings.INSTALLED_APPS:
-                test_case = import_string(test_case_name)
-                test_method = getattr(test_case, test_method_name)
-                setattr(test_case, test_method_name, expectedFailure(test_method))
+            self._mark_test(test_name)
         for reason, tests in self.connection.features.django_test_skips.items():
             for test_name in tests:
-                test_case_name, _, test_method_name = test_name.rpartition(".")
-                test_app = test_name.split(".")[0]
-                # Importing a test app that isn't installed raises
-                # RuntimeError.
-                if test_app in settings.INSTALLED_APPS:
-                    test_case = import_string(test_case_name)
-                    test_method = getattr(test_case, test_method_name)
-                    setattr(test_case, test_method_name, skip(reason)(test_method))
+                self._mark_test(test_name, reason)
+
+    def _mark_test(self, test_name, skip_reason=None):
+        # Only load unittest during testing.
+        from unittest import expectedFailure, skip
+
+        module_or_class_name, _, name_to_mark = test_name.rpartition(".")
+        test_app = test_name.split(".")[0]
+        # Importing a test app that isn't installed raises RuntimeError.
+        if test_app in settings.INSTALLED_APPS:
+            try:
+                test_frame = import_string(module_or_class_name)
+            except ImportError:
+                # import_string() can raise ImportError if a submodule's parent
+                # module hasn't already been imported during test discovery.
+                # This can happen in at least two cases:
+                # 1. When running a subset of tests in a module, the test
+                #    runner won't import tests in that module's other
+                #    submodules.
+                # 2. When the parallel test runner spawns workers with an empty
+                #    import cache.
+                test_to_mark = import_string(test_name)
+                test_frame = sys.modules.get(test_to_mark.__module__)
+            else:
+                test_to_mark = getattr(test_frame, name_to_mark)
+            if skip_reason:
+                setattr(test_frame, name_to_mark, skip(skip_reason)(test_to_mark))
+            else:
+                setattr(test_frame, name_to_mark, expectedFailure(test_to_mark))

     def sql_table_creation_suffix(self):
         """


# Fixed #35442 -- Prevented N+1 queries in RelatedManager with only().

ticket-35442

#### Branch description
This PR prevents N+1 queries when iterating over a `RelatedManager` queryset that uses `.only()` to exclude the foreign key field.

Previously, `ModelIterable` would attempt to optimize the relationship population by accessing the foreign key on the child object. If that key was deferred (via .`only()`), accessing it triggered an immediate SQL query for every row.

The fix modifies `ModelIterable` to check `obj.__dict__` first. If the foreign key attributes are missing (deferred), the optimization is skipped, preserving the lazy loading behavior requested by the user.

**Tests:**
* Added `test_only_related_manager_optimization` in `defer.tests` to verify query count is 1 (previously N+1).
* Updated `defer.tests.DeferTests.test_only` to assert the correct number of deferred fields (3 instead of 2), reflecting that the foreign key is now correctly deferred.

#### 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/defer/tests.py b/tests/defer/tests.py
index c0968080b162..540eaa4a75c4 100644
--- a/tests/defer/tests.py
+++ b/tests/defer/tests.py
@@ -47,9 +47,9 @@ def test_only(self):
         # of them except the model's primary key see #15494
         self.assert_delayed(qs.only("pk")[0], 3)
         # You can use 'pk' with reverse foreign key lookups.
-        # The related_id is always set even if it's not fetched from the DB,
-        # so pk and related_id are not deferred.
-        self.assert_delayed(self.s1.primary_set.only("pk")[0], 2)
+        # The related_id is not set if it's not fetched from the DB,
+        # so pk is not deferred, but related_id is.
+        self.assert_delayed(self.s1.primary_set.only("pk")[0], 3)

     def test_defer_only_chaining(self):
         qs = Primary.objects.all()
@@ -82,6 +82,15 @@ def test_only_none_raises_error(self):
         with self.assertRaisesMessage(TypeError, msg):
             Primary.objects.only(None)

+    def test_only_related_manager_optimization(self):
+        s = Secondary.objects.create(first="one", second="two")
+        Primary.objects.bulk_create(
+            [Primary(name="p1", value="v1", related=s) for _ in range(5)]
+        )
+        with self.assertNumQueries(1):
+            for p in s.primary_set.only("pk"):
+                _ = p.pk
+
     def test_defer_extra(self):
         qs = Primary.objects.all()
         self.assert_delayed(qs.defer("name").extra(select={"a": 1})[0], 1)

diff --git a/django/db/models/query.py b/django/db/models/query.py
index 721bf33e57db..e5b3cc600bd9 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -109,16 +109,15 @@ def __iter__(self):
             (
                 field,
                 related_objs,
-                operator.attrgetter(
-                    *[
-                        (
-                            field.attname
-                            if from_field == "self"
-                            else queryset.model._meta.get_field(from_field).attname
-                        )
-                        for from_field in field.from_fields
-                    ]
-                ),
+                attnames := [
+                    (
+                        field.attname
+                        if from_field == "self"
+                        else queryset.model._meta.get_field(from_field).attname
+                    )
+                    for from_field in field.from_fields
+                ],
+                operator.attrgetter(*attnames),
             )
             for field, related_objs in queryset._known_related_objects.items()
         ]
@@ -133,10 +132,14 @@ def __iter__(self):
                     setattr(obj, attr_name, row[col_pos])

             # Add the known related objects to the model.
-            for field, rel_objs, rel_getter in known_related_objects:
+            for field, rel_objs, rel_attnames, rel_getter in known_related_objects:
                 # Avoid overwriting objects loaded by, e.g., select_related().
                 if field.is_cached(obj):
                     continue
+                # Avoid fetching potentially deferred attributes that would
+                # result in unexpected queries.
+                if any(attname not in obj.__dict__ for attname in rel_attnames):
+                    continue
                 rel_obj_id = rel_getter(obj)
                 try:
                     rel_obj = rel_objs[rel_obj_id]


# Refs #36769 -- Raised SuspiciousOperation for unexpected nested tags in XML Deserializer.

#### Trac ticket number
ticket-36769

#### Branch description
@shaib observed that raising `SuspiciousOperation` would be preferable to silently discarding data from invalid fixtures in https://github.com/django/django/pull/20377#discussion_r2616789793.

#### 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.
- [ ] 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/fixtures/tests.py b/tests/fixtures/tests.py
index 7511569d2102..dd03343027f3 100644
--- a/tests/fixtures/tests.py
+++ b/tests/fixtures/tests.py
@@ -10,6 +10,7 @@
 from django.apps import apps
 from django.contrib.sites.models import Site
 from django.core import management
+from django.core.exceptions import SuspiciousOperation
 from django.core.files.temp import NamedTemporaryFile
 from django.core.management import CommandError
 from django.core.management.commands.dumpdata import ProxyModelWarning
@@ -23,7 +24,6 @@
     CircularA,
     CircularB,
     NaturalKeyThing,
-    Person,
     PrimaryKeyUUIDModel,
     ProxySpy,
     Spy,
@@ -522,12 +522,13 @@ def test_loading_and_dumping(self):
         )

     def test_deeply_nested_elements(self):
-        """Text inside deeply-nested tags is skipped."""
-        management.call_command(
-            "loaddata", "invalid_deeply_nested_elements.xml", verbosity=0
-        )
-        person = Person.objects.get(pk=1)
-        self.assertEqual(person.name, "Django")  # not "Django pony"
+        """Text inside deeply-nested tags raises SuspiciousOperation."""
+        for file in [
+            "invalid_deeply_nested_elements.xml",
+            "invalid_deeply_nested_elements_natural_key.xml",
+        ]:
+            with self.subTest(file=file), self.assertRaises(SuspiciousOperation):
+                management.call_command("loaddata", file, verbosity=0)

     def test_dumpdata_with_excludes(self):
         # Load fixture1 which has a site, two articles, and a category
diff --git a/tests/serializers/test_deserialization.py b/tests/serializers/test_deserialization.py
index a718a990385a..b6a0818c427a 100644
--- a/tests/serializers/test_deserialization.py
+++ b/tests/serializers/test_deserialization.py
@@ -1,15 +1,14 @@
 import json
-import time
+import textwrap
 import unittest

+from django.core.exceptions import SuspiciousOperation
 from django.core.serializers.base import DeserializationError, DeserializedObject
 from django.core.serializers.json import Deserializer as JsonDeserializer
 from django.core.serializers.jsonl import Deserializer as JsonlDeserializer
 from django.core.serializers.python import Deserializer
 from django.core.serializers.xml_serializer import Deserializer as XMLDeserializer
-from django.db import models
 from django.test import SimpleTestCase
-from django.test.utils import garbage_collect

 from .models import Author

@@ -138,52 +137,23 @@ def test_yaml_bytes_input(self):
         self.assertEqual(first_item.object, self.jane)
         self.assertEqual(second_item.object, self.joe)

-    def test_crafted_xml_performance(self):
-        """The time to process invalid inputs is not quadratic."""
-
-        def build_crafted_xml(depth, leaf_text_len):
-            nested_open = "<nested>" * depth
-            nested_close = "</nested>" * depth
-            leaf = "x" * leaf_text_len
-            field_content = f"{nested_open}{leaf}{nested_close}"
-            return f"""
-                <django-objects version="1.0">
-                   <object model="contenttypes.contenttype" pk="1">
-                      <field name="app_label">{field_content}</field>
-                      <field name="model">m</field>
-                   </object>
-                </django-objects>
-            """
-
-        def deserialize(crafted_xml):
-            iterator = XMLDeserializer(crafted_xml)
-            garbage_collect()
-
-            start_time = time.perf_counter()
-            result = list(iterator)
-            end_time = time.perf_counter()
-
-            self.assertEqual(len(result), 1)
-            self.assertIsInstance(result[0].object, models.Model)
-            return end_time - start_time
-
-        def assertFactor(label, params, factor=2):
-            factors = []
-            prev_time = None
-            for depth, length in params:
-                crafted_xml = build_crafted_xml(depth, length)
-                elapsed = deserialize(crafted_xml)
-                if prev_time is not None:
-                    factors.append(elapsed / prev_time)
-                prev_time = elapsed
-
-            with self.subTest(label):
-                # Assert based on the average factor to reduce test flakiness.
-                self.assertLessEqual(sum(factors) / len(factors), factor)
-
-        assertFactor(
-            "varying depth, varying length",
-            [(50, 2000), (100, 4000), (200, 8000), (400, 16000), (800, 32000)],
-            2,
+    def test_crafted_xml_rejected(self):
+        depth = 100
+        leaf_text_len = 1000
+        nested_open = "<nested>" * depth
+        nested_close = "</nested>" * depth
+        leaf = "x" * leaf_text_len
+        field_content = f"{nested_open}{leaf}{nested_close}"
+        crafted_xml = textwrap.dedent(
+            f"""
+        <django-objects version="1.0">
+            <object model="contenttypes.contenttype" pk="1">
+                <field name="app_label">{field_content}</field>
+                <field name="model">m</field>
+            </object>
+        </django-objects>"""
         )
-        assertFactor("constant depth, varying length", [(100, 1), (100, 1000)], 2)
+
+        msg = "Unexpected element: 'nested'"
+        with self.assertRaisesMessage(SuspiciousOperation, msg):
+            list(XMLDeserializer(crafted_xml))

diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index d43865c25743..d8ffbdf00a98 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -10,7 +10,7 @@

 from django.apps import apps
 from django.conf import settings
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
 from django.core.serializers import base
 from django.db import DEFAULT_DB_ALIAS, models
 from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError
@@ -411,6 +411,8 @@ def m2m_convert(n):
         try:
             for c in node.getElementsByTagName("object"):
                 values.append(m2m_convert(c))
+        except SuspiciousOperation:
+            raise
         except Exception as e:
             if isinstance(e, ObjectDoesNotExist) and self.handle_forward_references:
                 return base.DEFER_FIELD
@@ -439,17 +441,15 @@ def _get_model_from_node(self, node, attr):
             )

+def check_element_type(element):
+    if element.childNodes:
+        raise SuspiciousOperation(f"Unexpected element: {element.tagName!r}")
+    return element.nodeType in (element.TEXT_NODE, element.CDATA_SECTION_NODE)
+
+
 def getInnerText(node):
-    """Get the inner text of a DOM node and any children one level deep."""
-    # inspired by
-    # https://mail.python.org/pipermail/xml-sig/2005-March/011022.html
     return "".join(
-        [
-            element.data
-            for child in node.childNodes
-            for element in (child, *child.childNodes)
-            if element.nodeType in (element.TEXT_NODE, element.CDATA_SECTION_NODE)
-        ]
+        [child.data for child in node.childNodes if check_element_type(child)]
     )


# Fixed #36708 -- Initialized formset to None in ChangeList.__init__().

#### Trac ticket number

ticket-36708

#### Branch description

This PR adds a default formset = None attribute to the ChangeList class.
The Ticket describes how when Django instantiates ChangeList through ModelAdmin, it assigns formset dynamically.
However, when developers create their own custom ChangeList instances, formset may not be set, leading to an AttributeError in templates.
Defining formset as a class-level field (attribute) makes sure that behaviour is a bit 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.
- [ ] 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/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index 68f6ca453eaf..319d6259f6a9 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -240,7 +240,6 @@ def test_result_list_empty_changelist_value(self):
         request.user = self.superuser
         m = ChildAdmin(Child, custom_site)
         cl = m.get_changelist_instance(request)
-        cl.formset = None
         template = Template(
             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
         )
@@ -285,7 +284,6 @@ def test_result_list_set_empty_value_display_on_admin_site(self):
         admin.site.empty_value_display = "???"
         m = ChildAdmin(Child, admin.site)
         cl = m.get_changelist_instance(request)
-        cl.formset = None
         template = Template(
             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
         )
@@ -310,7 +308,6 @@ def test_result_list_set_empty_value_display_in_model_admin(self):
         request.user = self.superuser
         m = EmptyValueChildAdmin(Child, admin.site)
         cl = m.get_changelist_instance(request)
-        cl.formset = None
         template = Template(
             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
         )
@@ -341,7 +338,6 @@ def test_result_list_html(self):
         request.user = self.superuser
         m = ChildAdmin(Child, custom_site)
         cl = m.get_changelist_instance(request)
-        cl.formset = None
         template = Template(
             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
         )
@@ -370,7 +366,6 @@ def test_action_checkbox_for_model_with_dunder_html(self):
         request = self._mocked_authenticated_request("/grandchild/", self.superuser)
         m = GrandChildAdmin(GrandChild, custom_site)
         cl = m.get_changelist_instance(request)
-        cl.formset = None
         template = Template(
             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
         )

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 6c202c8e613c..2de07fde7e33 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -2050,11 +2050,6 @@ def changelist_view(self, request, extra_context=None):
             # me back" button on the action confirmation page.
             return HttpResponseRedirect(request.get_full_path())

-        # If we're allowing changelist editing, we need to construct a formset
-        # for the changelist given all the fields to be edited. Then we'll
-        # use the formset to validate/process POSTed data.
-        formset = cl.formset = None
-
         # Handle POSTed bulk-edit data.
         if request.method == "POST" and cl.list_editable and "_save" in request.POST:
             if not self.has_change_permission(request):
@@ -2063,13 +2058,11 @@ def changelist_view(self, request, extra_context=None):
             modified_objects = self._get_list_editable_queryset(
                 request, FormSet.get_default_prefix()
             )
-            formset = cl.formset = FormSet(
-                request.POST, request.FILES, queryset=modified_objects
-            )
-            if formset.is_valid():
+            cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)
+            if cl.formset.is_valid():
                 changecount = 0
                 with transaction.atomic(using=router.db_for_write(self.model)):
-                    for form in formset.forms:
+                    for form in cl.formset.forms:
                         if form.has_changed():
                             obj = self.save_form(request, form, change=True)
                             self.save_model(request, obj, form, change=True)
@@ -2095,11 +2088,11 @@ def changelist_view(self, request, extra_context=None):
         # Handle GET -- construct a formset for display.
         elif cl.list_editable and self.has_change_permission(request):
             FormSet = self.get_changelist_formset(request)
-            formset = cl.formset = FormSet(queryset=cl.result_list)
+            cl.formset = FormSet(queryset=cl.result_list)

         # Build the list of media to be used by the formset.
-        if formset:
-            media = self.media + formset.media
+        if cl.formset:
+            media = self.media + cl.formset.media
         else:
             media = self.media

diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 8c9118808e8e..40a6b3bf3a86 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -101,7 +101,7 @@ def __init__(
         self.preserved_filters = model_admin.get_preserved_filters(request)
         self.sortable_by = sortable_by
         self.search_help_text = search_help_text
-
+        self.formset = None
         # Get search parameters from the query string.
         _search_form = self.search_form_class(request.GET)
         if not _search_form.is_valid():


# Fixed #36804 -- Fixed admin system check crash for missing models.

…f AdminSite

There was an issue while raising the `NotRegistered` exception in get_model_admin when a string was passed to it instead of a model class. Fixed it by checking with `isinstance`. Added a test for this too.

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

#### Branch description
Handles a string passed to the `get_model_admin` method of the `AdminSite` class (in cases where the `to` field of a `ForeignKey` is specified using a model name instead of a model class.

#### 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/modeladmin/test_checks.py b/tests/modeladmin/test_checks.py
index fbd63abfbf15..c493148eb9cf 100644
--- a/tests/modeladmin/test_checks.py
+++ b/tests/modeladmin/test_checks.py
@@ -1724,6 +1724,28 @@ class Admin(ModelAdmin):
             invalid_obj=Admin,
         )

+    @isolate_apps("modeladmin")
+    def test_autocomplete_e039_unresolved_model(self):
+        class UnresolvedForeignKeyModel(models.Model):
+            unresolved = models.ForeignKey("missing.Model", models.CASCADE)
+
+            class Meta:
+                app_label = "modeladmin"
+
+        class Admin(ModelAdmin):
+            autocomplete_fields = ("unresolved",)
+
+        self.assertIsInvalid(
+            Admin,
+            UnresolvedForeignKeyModel,
+            msg=(
+                'An admin for model "missing.Model" has to be registered '
+                "to be referenced by Admin.autocomplete_fields."
+            ),
+            id="admin.E039",
+            invalid_obj=Admin,
+        )
+
     def test_autocomplete_e040(self):
         class NoSearchFieldsAdmin(ModelAdmin):
             pass

diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py
index 10257a54bf89..d14515e8a424 100644
--- a/django/contrib/admin/checks.py
+++ b/django/contrib/admin/checks.py
@@ -236,14 +236,20 @@ def _check_autocomplete_fields_item(self, obj, field_name, label):
                     id="admin.E038",
                 )
             try:
+                if isinstance(field.remote_field.model, str):
+                    raise NotRegistered
                 related_admin = obj.admin_site.get_model_admin(field.remote_field.model)
             except NotRegistered:
+                # field.remote_field.model could be a string or a class.
+                remote_model = getattr(
+                    field.remote_field.model, "__name__", field.remote_field.model
+                )
                 return [
                     checks.Error(
                         'An admin for model "%s" has to be registered '
                         "to be referenced by %s.autocomplete_fields."
                         % (
-                            field.remote_field.model.__name__,
+                            remote_model,
                             type(obj).__name__,
                         ),
                         obj=obj.__class__,


# Fixed #36827 -- Added support for exclusion constraints using Hash indexes on PostgreSQL.

#### Trac ticket number

ticket-36827

#### Branch description

Add support for Hash index type for `ExclusionConstraint` in PostgreSQL.

#### 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.
- [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/postgres_tests/test_constraints.py b/tests/postgres_tests/test_constraints.py
index 331b23980d6d..39c2f9b1f160 100644
--- a/tests/postgres_tests/test_constraints.py
+++ b/tests/postgres_tests/test_constraints.py
@@ -27,7 +27,13 @@
 from django.utils import timezone

 from . import PostgreSQLTestCase
-from .models import HotelReservation, IntegerArrayModel, RangesModel, Room, Scene
+from .models import (
+    HotelReservation,
+    IntegerArrayModel,
+    RangesModel,
+    Room,
+    Scene,
+)

 try:
     from django.contrib.postgres.constraints import ExclusionConstraint
@@ -299,7 +305,7 @@ def test_invalid_condition(self):
             )

     def test_invalid_index_type(self):
-        msg = "Exclusion constraints only support GiST or SP-GiST indexes."
+        msg = "Exclusion constraints only support GiST, Hash, or SP-GiST indexes."
         with self.assertRaisesMessage(ValueError, msg):
             ExclusionConstraint(
                 index_type="gin",
@@ -480,6 +486,18 @@ def test_repr(self):
             "(F(datespan), '-|-')] name='exclude_overlapping' "
             "violation_error_code='overlapping_must_be_excluded'>",
         )
+        constraint = ExclusionConstraint(
+            name="exclude_equal_hash",
+            index_type="hash",
+            expressions=[(F("room"), RangeOperators.EQUAL)],
+            violation_error_code="room_must_be_unique",
+        )
+        self.assertEqual(
+            repr(constraint),
+            "<ExclusionConstraint: index_type='hash' expressions=["
+            "(F(room), '=')] name='exclude_equal_hash' "
+            "violation_error_code='room_must_be_unique'>",
+        )

     def test_eq(self):
         constraint_1 = ExclusionConstraint(
@@ -581,6 +599,12 @@ def test_eq(self):
             violation_error_code="custom_code",
             violation_error_message="other custom error",
         )
+        constraint_13 = ExclusionConstraint(
+            name="exclude_equal_hash",
+            index_type="hash",
+            expressions=[(F("room"), RangeOperators.EQUAL)],
+            violation_error_code="room_must_be_unique",
+        )
         self.assertEqual(constraint_1, constraint_1)
         self.assertEqual(constraint_1, mock.ANY)
         self.assertNotEqual(constraint_1, constraint_2)
@@ -597,8 +621,10 @@ def test_eq(self):
         self.assertNotEqual(constraint_1, object())
         self.assertNotEqual(constraint_10, constraint_11)
         self.assertNotEqual(constraint_11, constraint_12)
+        self.assertNotEqual(constraint_12, constraint_13)
         self.assertEqual(constraint_10, constraint_10)
         self.assertEqual(constraint_12, constraint_12)
+        self.assertEqual(constraint_13, constraint_13)

     def test_deconstruct(self):
         constraint = ExclusionConstraint(
@@ -1256,6 +1282,26 @@ def test_range_equal_cast(self):
             editor.add_constraint(Room, constraint)
         self.assertIn(constraint_name, self.get_constraints(Room._meta.db_table))

+    def test_hash_uniqueness(self):
+        constraint_name = "exclusion_equal_room_hash"
+        self.assertNotIn(constraint_name, self.get_constraints(Room._meta.db_table))
+        constraint = ExclusionConstraint(
+            name=constraint_name,
+            index_type="hash",
+            expressions=[(F("number"), RangeOperators.EQUAL)],
+        )
+        with connection.schema_editor() as editor:
+            editor.add_constraint(Room, constraint)
+        self.assertIn(constraint_name, self.get_constraints(Room._meta.db_table))
+        Room.objects.create(number=101)
+        with self.assertRaises(IntegrityError), transaction.atomic():
+            Room.objects.create(number=101)
+        Room.objects.create(number=102)
+        # Drop the constraint.
+        with connection.schema_editor() as editor:
+            editor.remove_constraint(Room, constraint)
+        self.assertNotIn(constraint.name, self.get_constraints(Room._meta.db_table))
+
     @isolate_apps("postgres_tests")
     def test_table_create(self):
         constraint_name = "exclusion_equal_number_tc"
@@ -1287,3 +1333,39 @@ def test_database_default(self):
         msg = "Constraint “ints_equal” is violated."
         with self.assertRaisesMessage(ValidationError, msg):
             constraint.validate(RangesModel, RangesModel())
+
+    def test_covering_hash_index_not_supported(self):
+        constraint_name = "covering_hash_index_not_supported"
+        msg = "Covering exclusion constraints using Hash indexes are not supported."
+        with self.assertRaisesMessage(ValueError, msg):
+            ExclusionConstraint(
+                name=constraint_name,
+                expressions=[("int1", RangeOperators.EQUAL)],
+                index_type="hash",
+                include=["int2"],
+            )
+
+    def test_composite_hash_index_not_supported(self):
+        constraint_name = "composite_hash_index_not_supported"
+        msg = "Composite exclusion constraints using Hash indexes are not supported."
+        with self.assertRaisesMessage(ValueError, msg):
+            ExclusionConstraint(
+                name=constraint_name,
+                expressions=[
+                    ("int1", RangeOperators.EQUAL),
+                    ("int2", RangeOperators.EQUAL),
+                ],
+                index_type="hash",
+            )
+
+    def test_non_equal_hash_index_not_supported(self):
+        constraint_name = "none_equal_hash_index_not_supported"
+        msg = (
+            "Exclusion constraints using Hash indexes only support the EQUAL operator."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            ExclusionConstraint(
+                name=constraint_name,
+                expressions=[("int1", RangeOperators.NOT_EQUAL)],
+                index_type="hash",
+            )

diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py
index 13a90e4b7f66..dea77c2f1458 100644
--- a/django/contrib/postgres/constraints.py
+++ b/django/contrib/postgres/constraints.py
@@ -9,6 +9,7 @@
 from django.db.models.lookups import PostgresOperatorLookup
 from django.db.models.sql import Query

+from .fields import RangeOperators
 from .utils import CheckPostgresInstalledMixin

 __all__ = ["ExclusionConstraint"]
@@ -36,9 +37,9 @@ def __init__(
         violation_error_code=None,
         violation_error_message=None,
     ):
-        if index_type and index_type.lower() not in {"gist", "spgist"}:
+        if index_type and index_type.lower() not in {"gist", "hash", "spgist"}:
             raise ValueError(
-                "Exclusion constraints only support GiST or SP-GiST indexes."
+                "Exclusion constraints only support GiST, Hash, or SP-GiST indexes."
             )
         if not expressions:
             raise ValueError(
@@ -57,6 +58,24 @@ def __init__(
             )
         if not isinstance(include, (NoneType, list, tuple)):
             raise ValueError("ExclusionConstraint.include must be a list or tuple.")
+        if index_type and index_type.lower() == "hash":
+            if include:
+                raise ValueError(
+                    "Covering exclusion constraints using Hash indexes are not "
+                    "supported."
+                )
+            if not expressions:
+                pass
+            elif len(expressions) > 1:
+                raise ValueError(
+                    "Composite exclusion constraints using Hash indexes are not "
+                    "supported."
+                )
+            elif expressions[0][1] != RangeOperators.EQUAL:
+                raise ValueError(
+                    "Exclusion constraints using Hash indexes only support the EQUAL "
+                    "operator."
+                )
         self.expressions = expressions
         self.index_type = index_type or "GIST"
         self.condition = condition


# Fixed #29257 -- Caught DatabaseError when attempting to close a possibly already-closed cursor.

#### Trac ticket number

ticket-29257

#### Branch description
When ``cursor.execute()`` raises an exception and ``cursor.close()`` also raises, Django used to surface the ``cursor.close()`` error, adding noise to the original execution failure and producing misleading messages such as: "cursor _django_curs_xxx does not exist".

This patch ensures that the original ``execute()`` exception is preserved without unnecessary context. A regression test is included to verify that ``cursor.close()`` errors do not distract from the real underlying exception.

#### 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/queries/test_sqlcompiler.py b/tests/queries/test_sqlcompiler.py
index 72a66ad07ca5..0f6f2fc10bc7 100644
--- a/tests/queries/test_sqlcompiler.py
+++ b/tests/queries/test_sqlcompiler.py
@@ -1,11 +1,14 @@
-from django.db import DEFAULT_DB_ALIAS, connection
+from unittest import mock
+
+from django.db import DEFAULT_DB_ALIAS, DatabaseError, connection
 from django.db.models.sql import Query
-from django.test import SimpleTestCase
+from django.db.models.sql.compiler import SQLCompiler
+from django.test import TestCase

 from .models import Item

-class SQLCompilerTest(SimpleTestCase):
+class SQLCompilerTest(TestCase):
     def test_repr(self):
         query = Query(Item)
         compiler = query.get_compiler(DEFAULT_DB_ALIAS, connection)
@@ -15,3 +18,24 @@ def test_repr(self):
             f"<DatabaseWrapper vendor={connection.vendor!r} alias='default'> "
             f"using='default'>",
         )
+
+    def test_execute_sql_suppresses_cursor_closing_failure_on_exception(self):
+        query = Query(Item)
+        compiler = SQLCompiler(query, connection, None)
+        cursor = mock.MagicMock()
+
+        # When execution fails, the cursor may have been closed.
+        # Django's attempt to close it again will fail, and needs catching.
+        execute_err = DatabaseError("execute failed")
+        cursor.execute.side_effect = execute_err
+        cursor.close.side_effect = DatabaseError("close failed")
+
+        with mock.patch.object(connection, "cursor", return_value=cursor):
+            with self.assertRaises(DatabaseError) as ctx:
+                compiler.execute_sql("SELECT 1", [])
+
+        # There is no irrelevant context from trying to close a closed cursor.
+        exc = ctx.exception
+        self.assertIs(exc, execute_err)
+        self.assertIsNone(exc.__cause__)
+        self.assertTrue(exc.__suppress_context__)

diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 9068d87a8974..dd957ec76595 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -1629,9 +1629,12 @@ def execute_sql(
             cursor = self.connection.cursor()
         try:
             cursor.execute(sql, params)
-        except Exception:
+        except Exception as e:
             # Might fail for server-side cursors (e.g. connection closed)
-            cursor.close()
+            try:
+                cursor.close()
+            except DatabaseError:
+                raise e from None
             raise

         if result_type == ROW_COUNT:


# Heaviside function simplification under assumptions

It seems that, under certain conditions, SymPy (version 1.0) does not apply assumptions to the Heaviside function. If I understand correctly, an appropriate handler (e.g. `refine_Heaviside`) has not been implemented for the Heaviside function.

```
import sympy as sp
sp.assumptions.assume.global_assumptions.clear()

""" The code below works as expected and applies the assumption to x"""
x = sp.Symbol('x', positive=True)
print(sp.Heaviside(x))
print(sp.simplify(sp.Heaviside(x)))

""" Every method in this section does not seem to provide the expected result."""
y = sp.Symbol('y')
print(
    sp.refine(
        sp.Heaviside(y),
        sp.assumptions.Q.real(y) & sp.assumptions.Q.positive(y)
    )
)
sp.assumptions.assume.global_assumptions.add(sp.assumptions.Q.real(y))
sp.assumptions.assume.global_assumptions.add(sp.assumptions.Q.positive(y))
print(sp.assumptions.assume.global_assumptions)
print(sp.Heaviside(y))
print(sp.simplify(sp.Heaviside(y), fu=True))
with sp.assumptions.assuming(sp.assumptions.Q.positive(y)):
    print(sp.assumptions.ask(sp.assumptions.Q.positive(sp.Heaviside(y))))

```
Result:

```
1
1
Heaviside(y)
AssumptionsContext({Q.real(y), Q.positive(y)})
Heaviside(y)
Heaviside(y)
None

```

diff --git a/sympy/assumptions/tests/test_refine.py b/sympy/assumptions/tests/test_refine.py
index 64f896e2a7b5..bfe09e5d707a 100644
--- a/sympy/assumptions/tests/test_refine.py
+++ b/sympy/assumptions/tests/test_refine.py
@@ -14,6 +14,7 @@
 from sympy.functions.elementary.piecewise import Piecewise
 from sympy.matrices.expressions.matexpr import MatrixSymbol
 from sympy.functions.elementary.integers import floor, ceiling
+from sympy.functions.special.delta_functions import Heaviside

 def test_Abs():
@@ -305,3 +306,17 @@ def test_floor_ceiling():

     assert refine(floor(floor(x)+ floor(y))) == floor(x) + floor(y)
     assert refine(ceiling(ceiling(x) - ceiling(y))) == ceiling(x) - ceiling(y)
+
+
+def test_Heaviside():
+    assert refine(Heaviside(x), Q.positive(x)) == 1
+    assert refine(Heaviside(x), Q.negative(x)) == 0
+    assert refine(Heaviside(x), Q.zero(x)) == S.Half
+    assert refine(Heaviside(x), Q.nonnegative(x)) == Heaviside(x)
+    assert refine(Heaviside(x), Q.nonpositive(x)) == Heaviside(x)
+    assert refine(Heaviside(x), True) == Heaviside(x)
+
+    # custom H0 value
+    assert refine(Heaviside(x, 1), Q.zero(x)) == 1
+    assert refine(Heaviside(x, 1), Q.positive(x)) == 1
+    assert refine(Heaviside(x, 1), Q.negative(x)) == 0

diff --git a/sympy/assumptions/refine.py b/sympy/assumptions/refine.py
index 9180131956cf..4f738a2eec34 100644
--- a/sympy/assumptions/refine.py
+++ b/sympy/assumptions/refine.py
@@ -480,6 +480,34 @@ def refine_sin_cos(expr, assumptions):
         return ((-1)**((k + 1) / 2)) * sin(rem)

+def refine_Heaviside(expr, assumptions):
+    """
+    Handler for the Heaviside step function.
+
+    Examples
+    ========
+
+    >>> from sympy.assumptions.refine import refine_Heaviside
+    >>> from sympy import Q, Heaviside
+    >>> from sympy.abc import x
+    >>> refine_Heaviside(Heaviside(x), Q.positive(x))
+    1
+    >>> refine_Heaviside(Heaviside(x), Q.negative(x))
+    0
+    >>> refine_Heaviside(Heaviside(x), Q.zero(x))
+    1/2
+
+    """
+    arg, H0 = expr.args
+    if ask(Q.positive(arg), assumptions):
+        return S.One
+    if ask(Q.negative(arg), assumptions):
+        return S.Zero
+    if ask(Q.zero(arg), assumptions):
+        return H0
+    return expr
+
+
 def refine_floor_ceiling(expr, assumptions):
     """
     Handler for the floor and ceiling functions
@@ -531,6 +559,7 @@ def refine_floor_ceiling(expr, assumptions):
     'MatrixElement': refine_matrixelement,
     'cos': refine_sin_cos,
     'sin': refine_sin_cos,
+    'Heaviside': refine_Heaviside,
     'floor': refine_floor_ceiling,
     'ceiling' : refine_floor_ceiling,
 }


# LaTeX printing of FreeGroupElements

Version: 1.15.0.dev (actually it has been a problem for quite a long time, not just 1.15)

Code: Consider calling `latex` on generators of a free group,
```
from sympy.combinatorics import free_group
from sympy.abc import a, b
from sympy import *
_, aa, bb = free_group((a,b))
print(latex(aa))
```

This will cause a RecursionError. As a side effect, displaying the generators of a free group in Jupyter notebooks will cause a RecursionError.

---

Apart from above, I tested calling `latex` over **words** instead of single generators, e.g.,
```
print(aa*bb**3*aa)
print(latex(aa*bb**3*aa))
```
This is the output:
```
a*b**3*a
\left( \left( a, \  1\right), \  \left( b, \  3\right), \  \left( a, \  1\right)\right)
```
Is it better to make `latex` print in a similar way as `str`?

diff --git a/sympy/printing/tests/test_latex.py b/sympy/printing/tests/test_latex.py
index cacc8ab6ca53..74b614e2a402 100644
--- a/sympy/printing/tests/test_latex.py
+++ b/sympy/printing/tests/test_latex.py
@@ -1,6 +1,7 @@
 from sympy import MatAdd, MatMul, Array
 from sympy.algebras.quaternion import Quaternion
 from sympy.calculus.accumulationbounds import AccumBounds
+from sympy.combinatorics.free_groups import free_group
 from sympy.combinatorics.permutations import Cycle, Permutation, AppliedPermutation
 from sympy.concrete.products import Product
 from sympy.concrete.summations import Sum
@@ -1725,6 +1726,18 @@ def test_latex_Lambda():
     assert latex(Lambda((x, y), x + 1)) == r"\left( \left( x, \  y\right) \mapsto x + 1 \right)"
     assert latex(Lambda(x, x)) == r"\left( x \mapsto x \right)"

+
+def test_latex_FreeGroupElement():
+    F, a, b = free_group("a,b")
+
+    assert latex(a**0) == "1"
+    assert latex(a) == "a"
+    assert latex(a**-1) == "a^{-1}"
+    assert latex(a**3*b**2*a*b**(-1)) == "a^{3} b^{2} a b^{-1}"
+    assert latex(b**(-2)*a*b**3, mul_symbol='dot') == \
+        r"b^{-2} \cdot a \cdot b^{3}"
+
+
 def test_latex_PolyElement():
     Ruv, u, v = ring("u,v", ZZ)
     Rxyz, x, y, z = ring("x,y,z", Ruv)

diff --git a/sympy/printing/latex.py b/sympy/printing/latex.py
index 0a818615b0db..f87e8b9d2f80 100644
--- a/sympy/printing/latex.py
+++ b/sympy/printing/latex.py
@@ -2543,6 +2543,20 @@ def _print_OmegaPower(self, expr):
     def _print_Ordinal(self, expr):
         return " + ".join([self._print(arg) for arg in expr.args])

+    def _print_FreeGroupElement(self, elm):
+        if elm.is_identity:
+            return "1"
+
+        str_form = []
+        for g, power in elm.array_form:
+            s = str(g)
+            if power == 1:
+                str_form.append(s)
+            else:
+                str_form.append(s + "^{" + str(power) + "}")
+        mul_symbol = self._settings['mul_symbol_latex'] or ""
+        return mul_symbol.join(str_form)
+
     def _print_PolyElement(self, poly):
         mul_symbol = self._settings['mul_symbol_latex']
         return poly.str(self, PRECEDENCE, "{%s}^{%d}", mul_symbol)


# deprecate degree report for numerical expression and non-polynomial expressions unless symbolic generator of interest is given

The `gen` argument of degree can be an integer when the passed `f` is a Poly, but it also allows the integer to select the generator of a numeric expression *even if the expression is not a Poly*. This looks like an oversight and it would be better to treat the numeric expression like the nonnumeric and just raise an error since, without actually computing the Poly, it is not possible to know if the expression has more than one numerical generator.

```python
>>> degree(E+pi**2, 0)
1
>>> degree(E+pi**2, 1)
2
>>> Poly(E + pi**2).gens
(E, pi)
```

The proposal is to treat any non-Number numeric expression as a potentially multivarite expression and require that the symbolic numerical generator of interest be specified.

```python
>>> degree(x + y)
...
TypeError
>>> degree(0)
-oo
>>> degree(1)
0
>>> degree(pi)
1 <--- raise TypeError instead
```
In fact, it would be smart to go one step further and disallow use of the int for univariate expressions which are not polynomial so even though this works...
```python
>>> degree(sqrt(x)+x**2)
2
>>> degree(sqrt(x)+x**2,1)
1
```
it shouldn't because...
```python
>>> (sqrt(x)+x**2).is_polynomial()
False <--- therefore, let's disable reporting of degree since without the Poly to consult the user doesn't know which generator the result is for
>>> Poly(sqrt(x)+x**2).gens
(x, sqrt(x))
```
(Actually, I think it would be better if Poly identified only sqrt(x) as the generator and viewed this as y+y**4 where y=sqrt(x). That is a different issue.)

I doubt that anyone really is depending on this behavior and would like to simply correct this oversight instead of doing a deprecation.

diff --git a/sympy/polys/tests/test_polytools.py b/sympy/polys/tests/test_polytools.py
index 46f1fc327692..431e41f2c725 100644
--- a/sympy/polys/tests/test_polytools.py
+++ b/sympy/polys/tests/test_polytools.py
@@ -72,7 +72,7 @@
 from sympy.functions.elementary.hyperbolic import tanh
 from sympy.functions.elementary.miscellaneous import sqrt
 from sympy.functions.elementary.piecewise import Piecewise
-from sympy.functions.elementary.trigonometric import sin
+from sympy.functions.elementary.trigonometric import cos, sin
 from sympy.matrices.dense import Matrix
 from sympy.matrices.expressions.matexpr import MatrixSymbol
 from sympy.polys.rootoftools import rootof
@@ -1335,10 +1335,9 @@ def test_Poly_degree():
     assert degree(x*y**2, z) == 0

     assert degree(pi) == 1
-
+    raises(TypeError, lambda: degree(I))  # Poly sees a number but no gen -> needs gen
     raises(TypeError, lambda: degree(y**2 + x**3))
     raises(TypeError, lambda: degree(y**2 + x**3, 1))
-    raises(PolynomialError, lambda: degree(x, 1.1))
     raises(PolynomialError, lambda: degree(x**2/(x**3 + 1), x))

     assert degree(Poly(0,x),z) is -oo
@@ -1348,6 +1347,40 @@ def test_Poly_degree():
     assert degree(Poly(y**2 + x**3, x), z) == 0
     assert degree(Poly(y**2 + x**3 + z**4, x), z) == 4

+
+    # optimization
+    big = 2  # make this number 1000 when full expansion can be avoided
+
+    assert degree((x + 1)**big, x) == big  # Large power (fast-path, no expansion)
+    assert degree((x + 1)**big + x**(big-1), x) == big  # Addition without cancellation
+    assert degree((x + 1)**2 - x**2, x) == 1  # Addition with cancellation (fallback required)
+    assert degree(x*(x + 1)**big, x) == big + 1  # Nested multiplication
+    assert degree(y*(x + 1)**big, x) == big  # Generator independence
+
+    # checking 0 detection
+    Z = pi*(1/pi + 1) - 1 - pi
+    assert degree(Z**big, x) is S.NegativeInfinity
+    assert degree(Z**big, pi) is S.NegativeInfinity
+    Z = Add(0, Mul(0, x, evaluate=False), evaluate=False)
+    assert degree(Z, x) is S.NegativeInfinity
+    assert degree(cos(1)**2 + sin(1)**2 - 1, x) == 0  # should be -oo
+
+    # /!\ user was warned; anything could change as Poly is improved-#
+    eq = x + 1/x**2                                                  #
+    raises(PolynomialError, lambda: degree(eq**big, x))              #
+    assert degree(eq**big, 1/x) == 2*big                             #
+                                                                     #
+    eq = exp(x) + 1/exp(2*x)                                         #
+    assert degree(eq, exp(x)) == 1                                   #
+    assert degree(eq, exp(-x)) == 2                                  #
+                                                                     #
+    one = Pow(x, 0, evaluate=False)                                  #
+    raises(PolynomialError, lambda: degree(one, one))                #
+    assert degree(x, 1.1) == degree(x, pi) == 0                      #
+    assert degree(0, 1.1) == -oo                                     #
+    # end of zoo of warnings-----------------------------------------#
+
+
 def test_Poly_degree_list():
     assert Poly(0, x).degree_list() == (-oo,)
     assert Poly(0, x, y).degree_list() == (-oo, -oo)

diff --git a/sympy/polys/polytools.py b/sympy/polys/polytools.py
index 90540c9048e2..35608f5ae8d6 100644
--- a/sympy/polys/polytools.py
+++ b/sympy/polys/polytools.py
@@ -21,7 +21,6 @@
 from sympy.core.intfunc import ilcm
 from sympy.core.numbers import I, Integer, equal_valued, NegativeInfinity
 from sympy.core.relational import Relational, Equality
-from sympy.core.sorting import ordered
 from sympy.core.symbol import Dummy, Symbol
 from sympy.core.sympify import sympify, _sympify
 from sympy.core.traversal import preorder_traversal, bottom_up
@@ -4907,8 +4906,15 @@ def _update_args(args, key, value):
 def degree(f, gen=0):
     """
     Return the degree of ``f`` in the given variable.
+    When ``f`` is a univariate polynomial, it is not
+    necessary to pass a generator, but if the generator
+    is given as an int it refers to the gen-th generator
+    of the expression which must be passed as a Poly
+    instance.

-    The degree of 0 is negative infinity.
+    The degree of 0 with respect to any variable is ``-oo``; if ``f``
+    is a numerical expression equal to 0, but not identically 0,
+    zero may be returned instead (because the variable does not appear).

     Examples
     ========
@@ -4916,6 +4922,8 @@ def degree(f, gen=0):
     >>> from sympy import degree
     >>> from sympy.abc import x, y

+    >>> degree(x**2)
+    2
     >>> degree(x**2 + y*x + 1, gen=x)
     2
     >>> degree(x**2 + y*x + 1, gen=y)
@@ -4923,6 +4931,20 @@ def degree(f, gen=0):
     >>> degree(0, x)
     -oo

+    In contrast to the strict Poly method, if the specified generator is
+    not in Poly's generators, the Poly instance is recast in terms of the
+    generator instead of raising an error.
+
+    >>> from sympy import Poly
+    >>> p = Poly(x + y, x)
+    >>> p.degree(y)
+    Traceback (most recent call last):
+    ...
+    PolynomialError: a valid generator expected, got y
+
+    >>> degree(p, y)
+    1
+
     See also
     ========

@@ -4930,36 +4952,34 @@ def degree(f, gen=0):
     degree_list
     """

-    f = sympify(f, strict=True)
-    gen_is_Num = sympify(gen, strict=True).is_Number
-    if f.is_Poly:
-        p = f
-        isNum = p.as_expr().is_Number
-    else:
-        isNum = f.is_Number
-        if not isNum:
-            if gen_is_Num:
-                p, _ = poly_from_expr(f)
-            else:
-                p, _ = poly_from_expr(f, gen)
+    _degree = lambda x: Integer(x) if type(x) is int else x

-    if isNum:
-        return S.Zero if f else S.NegativeInfinity
-
-    if not gen_is_Num:
-        if f.is_Poly and gen not in p.gens:
-            # try recast without explicit gens
-            p, _ = poly_from_expr(f.as_expr())
-        if gen not in p.gens:
-            return S.Zero
-    elif not f.is_Poly and len(f.free_symbols) > 1:
-        raise TypeError(filldedent('''
-         A symbolic generator of interest is required for a multivariate
-         expression like func = %s, e.g. degree(func, gen = %s) instead of
-         degree(func, gen = %s).
-        ''' % (f, next(ordered(f.free_symbols)), gen)))
-    result = p.degree(gen)
-    return Integer(result) if isinstance(result, int) else S.NegativeInfinity
+    f = sympify(f, strict=True)
+    if type(gen) is int:
+        if isinstance(f, Poly):
+            return _degree(f.degree(gen))
+        if f.is_Number:
+            return S.NegativeInfinity if f.is_zero else S.Zero
+        try:
+            p = Poly(f, expand=False)
+        except GeneratorsNeeded:  # e.g. f = (1+I)**2/2
+            gens = ()  # do not guess what the user intended
+        else:
+            gens = p.gens
+        free = f.free_symbols
+        if not (gen == 0 and len(gens) == 1 and len(free) < 2 and f.is_polynomial(
+                gen := next(iter(gens))) and # <-- assigns gen
+                gen.is_Atom):  # e.g. x or pi
+            raise TypeError(filldedent('''
+                To avoid ambiguity, this expression requires either
+                a symbol (not int) generator or a Poly (which identifies
+                the generators).'''))
+        return p.degree(gen)
+
+    gen = sympify(gen, strict=True)
+    if not isinstance(f, Poly) or gen not in f.gens:
+        f = poly_from_expr(f, gen)[0]
+    return _degree(f.degree(gen))

 @public


# The recursive ask() handlers are problematic

Right now all the "legacy" ask() handlers (in sympy/assumptions/handlers) work by recursively calling ask(). This is not a very good design. The `satask` approach where we instead gather a bunch of symbolic predicates and send them to a solver is better for many reasons. We should aim to remove these handlers once `satask` is able to handle everything they can handle.

Until then, there is a very simple refactor that should be made to them. Right now, they recursively call the `ask` function itself. However, instead this particular handler system should be a helper function, like `_ask_recursive()`. The handlers should all call `_ask_recursive()` rather than `ask()`.

This is important for two reasons:

- ask() does some preprocessing which would be unnecessarily duplicated with each recursive call. The intention is to increase the amount of preprocessing that is done.

- A bigger issue is that `ask` calls `satask` after this handler system fails. But the way this works, `satask` doesn't actually get the input of `ask`. This is because the recursive handlers split up logical expressions recursively! So something like `ask(Q.positive(x) | Q.negative(x), Q.real(x))` ends up in the following recursive call chain:

```
ask(Q.positive(x) | Q.negative(x) | Q.zero(x), Q.real(x))
    recursive_ask(Q.positive(x) | Q.negative(x) | Q.zero(x), Q.real(x))
        ask(Q.positive(x), Q.real(x))
            recursive_ask(Q.positive(x), Q.real(x)) -> None
            satask(Q.positive(x), Q.real(x)) -> None
        ask(Q.negative(x), Q.real(x))
            recursive_ask(Q.negative(x), Q.real(x)) -> None
            satask(Q.negative(x), Q.real(x)) -> None
        ask(Q.zero(x), Q.real(x))
            recursive_ask(Q.zero(x), Q.real(x)) -> None
            satask(Q.zero(x), Q.real(x)) -> None
    satask(Q.positive(x) | Q.negative(x) | Q.zero(x), Q.real(x)) -> True (finally)
```

(here `recursive_ask` is actually called `_eval_ask` in the code)

Each of those intermediate `satask` calls should not be there.

Note that this might actually make `ask` give less answers than previously if some query relied on both recursive and sat ask working together.

This will also make it easier to test the recursive_ask and satask "backends" separately (see the discussion at https://github.com/sympy/sympy/issues/27662#issuecomment-2945406582).

CC @TiloRC @krishnavbajoria02 this relates to what we discussed in the call today. This is another thing you can work on this week if you want.

diff --git a/sympy/assumptions/tests/test_assumptions_2.py b/sympy/assumptions/tests/test_assumptions_2.py
index 493fe4a7ed70..b157bdcf85ce 100644
--- a/sympy/assumptions/tests/test_assumptions_2.py
+++ b/sympy/assumptions/tests/test_assumptions_2.py
@@ -3,8 +3,9 @@
 """
 from sympy.abc import x, y
 from sympy.assumptions.assume import global_assumptions
-from sympy.assumptions.ask import Q
+from sympy.assumptions.ask import Q, _ask_single_fact
 from sympy.printing import pretty
+from sympy.assumptions.cnf import CNF

 def test_equal():
@@ -33,3 +34,16 @@ def test_global():
     global_assumptions.clear()
     assert not (x > 0) in global_assumptions
     assert not (y > 0) in global_assumptions
+
+
+def test_ask_single_fact():
+    assert _ask_single_fact(Q.real, CNF()) is None
+    assert _ask_single_fact(Q.even, CNF.from_prop(Q.zero)) is True
+    assert _ask_single_fact(Q.even, CNF.from_prop(Q.odd)) is False
+    assert _ask_single_fact(Q.even, CNF.from_prop(Q.real)) is None
+    assert _ask_single_fact(Q.integer, CNF.from_prop(Q.even | Q.odd)) is None
+    assert _ask_single_fact(Q.integer, CNF.from_prop(Q.prime)) is True
+    assert _ask_single_fact(Q.prime,   CNF.from_prop(Q.composite)) is False
+    assert _ask_single_fact(Q.zero, CNF.from_prop(~Q.even)) is False
+
+    assert _ask_single_fact(Q.zero, CNF.from_prop(~Q.even & Q.real)) is False

diff --git a/sympy/assumptions/ask.py b/sympy/assumptions/ask.py
index 72894df1e810..2b5f56b117c0 100644
--- a/sympy/assumptions/ask.py
+++ b/sympy/assumptions/ask.py
@@ -558,7 +558,7 @@ def ask(proposition, assumptions=True, context=global_assumptions):

 def _ask_single_fact(key, local_facts):
     """
-    Compute the truth value of single predicate using assumptions.
+    Determine whether the key is directly implied or refuted by any unit clause in local_facts.

     Parameters
     ==========
@@ -607,34 +607,34 @@ def _ask_single_fact(key, local_facts):
     >>> _ask_single_fact(key, local_facts)
     False
     """
-    if local_facts.clauses:
-
-        known_facts_dict = get_known_facts_dict()
-
-        if len(local_facts.clauses) == 1:
-            cl, = local_facts.clauses
-            if len(cl) == 1:
-                f, = cl
-                prop_facts = known_facts_dict.get(key, None)
-                prop_req = prop_facts[0] if prop_facts is not None else set()
-                if f.is_Not and f.arg in prop_req:
-                    # the prerequisite of proposition is rejected
-                    return False
-
-        for clause in local_facts.clauses:
-            if len(clause) == 1:
-                f, = clause
-                prop_facts = known_facts_dict.get(f.arg, None) if not f.is_Not else None
-                if prop_facts is None:
-                    continue
-
-                prop_req, prop_rej = prop_facts
-                if key in prop_req:
-                    # assumption implies the proposition
-                    return True
-                elif key in prop_rej:
-                    # proposition rejects the assumption
-                    return False
+    if not local_facts.clauses:
+        return None
+
+    known_facts_dict = get_known_facts_dict()
+    get_facts = lambda k: known_facts_dict.get(k, (set(), set()))
+
+    for clause in local_facts.clauses:
+        if len(clause) != 1:
+            continue
+        (f,) = clause
+        pred, negated = f.arg, f.is_Not
+
+        # Negative literal
+        key_req, _ = get_facts(key)
+        if negated and pred in key_req:
+            # If key implies pred and pred is false,
+            # then key must be false.
+            return False
+
+        # Positive literal
+        if not negated:
+            req, rej = get_facts(pred)
+            if key in req:
+                # key is implied by pred
+                return True
+            if key in rej:
+                # ~key is implied by pred
+                return False

     return None


# nonlinsolve crashes with TypeError when using sign functions

```python
nonlinsolve([sign(x) - 1, x*y - 4], [x, y]) output:- TypeError

```

`Tested On:- sympy:- 1.14.0`

diff --git a/sympy/solvers/tests/test_solveset.py b/sympy/solvers/tests/test_solveset.py
index 8547a5ddf3f1..87429fe70c49 100644
--- a/sympy/solvers/tests/test_solveset.py
+++ b/sympy/solvers/tests/test_solveset.py
@@ -1872,6 +1872,13 @@ def test_nonlinsolve_abs():
     raises(NotImplementedError, lambda: nonlinsolve([Abs(x) - 2, x + y], x, y))

+def test_nonlinsolve_sign():
+    raises(NotImplementedError, lambda: nonlinsolve([sign(x) - 1, x*y - 4], [x, y]))
+    raises(NotImplementedError, lambda: nonlinsolve([sign(x) - 1, x - y], [x, y]))
+    result = nonlinsolve([sign(x) - 1], [x])
+    assert isinstance(result, FiniteSet)
+
+
 def test_raise_exception_nonlinsolve():
     raises(IndexError, lambda: nonlinsolve([x**2 -1], []))
     raises(ValueError, lambda: nonlinsolve([x**2 -1]))

diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index a16bc73062ab..a8db21d2a53e 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -3603,7 +3603,13 @@ def _solve_using_known_values(result, solver):
                             # corresponding complex soln.
                             if not isinstance(soln, (ImageSet, ConditionSet)):
                                 soln += solveset_complex(eq2, sym)  # might give ValueError with Abs
-                    except (NotImplementedError, ValueError):
+
+                        if not isinstance(soln, (FiniteSet, ImageSet, ConditionSet, Union)) and soln is not S.EmptySet:
+                            raise NotImplementedError(
+                                f"nonlinsolve cannot handle solution of type {type(soln).__name__} "
+                                f"for symbol {sym}. Got {soln} from equation {eq2}."
+                            )
+                    except ValueError:
                         # If solveset is not able to solve equation `eq2`. Next
                         # time we may get soln using next equation `eq2`
                         continue


# `parse_latex_lark` cannot treat square brackets or curly parentheses

`sympy.parsing.latex.parse_latex` can interpret `[x+y]z` correctly, but `sympy.parsing.latex.parse_latex_lark` cannot,
and similarly `\\{x+y\\}z`.

```python
from sympy.parsing.latex import parse_latex, parse_latex_lark
parse_latex_lark('[x+y]z')
```

```
---------------------------------------------------------------------------
UnexpectedCharacters                      Traceback (most recent call last)
Cell In[7], [line 1](vscode-notebook-cell:?execution_count=7&line=1)
----> [1](vscode-notebook-cell:?execution_count=7&line=1) parse_latex_lark(latex)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/sympy/parsing/latex/lark/latex_parser.py:110, in parse_latex_lark(s)
    108 if _lark is None:
    109     raise ImportError("Lark is probably not installed")
--> [110](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/sympy/parsing/latex/lark/latex_parser.py:110) return _lark_latex_parser.doparse(s)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/sympy/parsing/latex/lark/latex_parser.py:74, in LarkLaTeXParser.doparse(self, s)
     71 if self.print_debug_output:
     72     _lark.logger.setLevel(logging.DEBUG)
---> [74](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/sympy/parsing/latex/lark/latex_parser.py:74) parse_tree = self.parser.parse(s)
     76 if not self.transform_expr:
     77     # exit early and return the parse tree
     78     _lark.logger.debug("expression = %s", s)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/lark.py:677, in Lark.parse(self, text, start, on_error)
    675 if on_error is not None and self.options.parser != 'lalr':
    676     raise NotImplementedError("The on_error option is only implemented for the LALR(1) parser.")
--> [677](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/lark.py:677) return self.parser.parse(text, start=start, on_error=on_error)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parser_frontends.py:131, in ParsingFrontend.parse(self, text, start, on_error)
    129 kw = {} if on_error is None else {'on_error': on_error}
    130 stream = self._make_lexer_thread(text)
--> [131](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parser_frontends.py:131) return self.parser.parse(stream, chosen_start, **kw)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/earley.py:280, in Parser.parse(self, lexer, start)
    277     else:
    278         columns[0].add(item)
--> [280](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/earley.py:280) to_scan = self._parse(lexer, columns, to_scan, start_symbol)
    282 # If the parse was successful, the start
    283 # symbol should have been completed in the last step of the Earley cycle, and will be in
    284 # this column. Find the item for the start_symbol, which is the root of the SPPF tree.
    285 solutions = dedup_list(n.node for n in columns[-1] if n.is_complete and n.node is not None and n.s == start_symbol and n.start == 0)

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/xearley.py:153, in Parser._parse(self, stream, columns, to_scan, start_symbol)
    150 for token in stream:
    151     self.predict_and_complete(i, to_scan, columns, transitives, node_cache)
--> [153](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/xearley.py:153)     to_scan, node_cache = scan(i, to_scan)
    155     if token == '\n':
    156         text_line += 1

File ~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/xearley.py:125, in Parser._parse.<locals>.scan(i, to_scan)
    123 if not next_set and not delayed_matches and not next_to_scan:
    124     considered_rules = list(sorted(to_scan, key=lambda key: key.rule.origin.name))
--> [125](https://vscode-remote+wsl-002bubuntu.vscode-resource.vscode-cdn.net/home/volat/parse_latex_lark/~/parse_latex_lark/.venv/lib/python3.14/site-packages/lark/parsers/xearley.py:125)     raise UnexpectedCharacters(stream, i, text_line, text_column, {item.expect.name for item in to_scan},
    126                                set(to_scan), state=frozenset(i.s for i in to_scan),
    127                                considered_rules=considered_rules
    128                                )
    130 return next_to_scan, node_cache

UnexpectedCharacters: No terminal matches '[' in the current parser context, at line 1 col 1

[x+y]z
^
Expected one of:
	* FUNC_ARCOSH
	* L_FLOOR
	* FUNC_DETERMINANT
	* CMD_INFTY
	* L_BRACE
	* FUNC_MAX
	* FUNC_ARCSIN
	* FUNC_MATRIX_ADJUGATE
	* FUNC_TANH
	* FUNC_LIM
	* FUNC_PROD
	* FUNC_ARSINH
	* CMD_DETERMINANT_BEGIN
	* SUB
	* L_PAREN
	* CMD_OVERLINE
	* GREEK_SYMBOL_WITH_LATIN_SUBSCRIPT
	* GREEK_SYMBOL_WITH_GREEK_SUBSCRIPT
	* FUNC_SQRT
	* FUNC_COSH
	* CMD_MATRIX_BEGIN
	* FUNC_ARTANH
	* BAR
	* FUNC_SEC
	* FUNC_INT
	* FUNC_ARCSEC
	* FUNC_ARCTAN
	* FUNC_LOG
	* FUNC_ARCCOT
	* __ANON_1
	* L_CEIL
	* FUNC_SIN
	* FUNC_ARCCSC
	* CMD_LANGLE
	* FUNC_COT
	* FUNC_ARCCOS
	* FUNC_SINH
	* ADD
	* FUNC_LG
	* SYMBOL
	* FUNC_EXP
	* FUNC_TAN
	* GREEK_SYMBOL_WITH_PRIMES
	* FUNC_LN
	* CMD_MATHIT
	* FUNC_MATRIX_TRACE
	* FUNC_COS
	* LATIN_SYMBOL_WITH_LATIN_SUBSCRIPT
	* CMD_FRAC
	* FUNC_CSC
	* CMD_IMAGINARY_UNIT
	* CMD_BINOM
	* FUNC_SUM
	* FUNC_MIN
	* LATIN_SYMBOL_WITH_GREEK_SUBSCRIPT
```

this is probably an error on latex.lark's part.

```
group_round_parentheses: L_PAREN _expression R_PAREN
group_square_brackets: L_BRACKET _expression R_BRACKET
group_curly_parentheses: L_BRACE _expression R_BRACE

...

adjacent_expressions: (_one_letter_symbol | number) _expression_mul
    | group_round_parentheses (group_round_parentheses | _one_letter_symbol)
    | _function _function
    | fraction _expression_mul

_expression_func: _expression_core
    | group_round_parentheses
    | fraction
    | binomial
    | _function
    | _integral// | derivative
    | limit
    | matrix
```

should be

```
group_round_parentheses: L_PAREN _expression R_PAREN
group_square_brackets: L_BRACKET _expression R_BRACKET
group_curly_parentheses: L_BRACE _expression R_BRACE
group_curly_literal_parentheses: L_BRACE_LITERAL _expression R_BRACE_LITERAL

...

adjacent_expressions: (_one_letter_symbol | number) _expression_mul
    | (group_round_parentheses | group_square_brackets | group_curly_literal_parentheses) (group_round_parentheses | group_square_brackets | group_curly_literal_parentheses | _one_letter_symbol)
    | _function _function
    | fraction _expression_mul

_expression_func: _expression_core
    | group_round_parentheses
    | group_square_brackets
    | group_curly_literal_parentheses
    | fraction
    | binomial
    | _function
    | _integral// | derivative
    | limit
    | matrix
```

and insert the following method in `transformer.py`.
```python
    def group_curly_literal_parentheses(self, tokens):
        return tokens[1]
```

diff --git a/sympy/parsing/tests/test_latex_lark.py b/sympy/parsing/tests/test_latex_lark.py
index ead2dfa26962..03b0102c9157 100644
--- a/sympy/parsing/tests/test_latex_lark.py
+++ b/sympy/parsing/tests/test_latex_lark.py
@@ -530,6 +530,38 @@ def _MatMul(a, b):
     (r"\left(  x + y\right ) z", _Mul(_Add(x, y), z)),
 ]

+SQUARE_BRACKET_EXPRESSION_PAIRS = [
+    (r"[x+y]z", _Mul(_Add(x, y), z)),
+    (r"[a+b]c", _Mul(_Add(a, b), c)),
+    (r"[x]y", _Mul(x, y)),
+    (r"[a+b][c+d]", _Mul(_Add(a, b), _Add(c, d))),
+    (r"[x][y]", _Mul(x, y)),
+    (r"[x+y]a", _Mul(_Add(x, y), a)),
+    (r"[a]b", _Mul(a, b)),
+]
+
+EVALUATED_SQUARE_BRACKET_EXPRESSION_PAIRS = [
+    (r"[x+y]z", (x + y) * z),
+    (r"[a+b]c", (a + b) * c),
+    (r"[x]y", x * y),
+    (r"[a+b][c+d]", (a + b) * (c + d)),
+    (r"[x][y]", x * y),
+    (r"[2+3]x", 5 * x),
+]
+
+LITERAL_BRACE_EXPRESSION_PAIRS = [
+    (r"\{x+y\}z", _Mul(_Add(x, y), z)),
+    (r"\{a\}b", _Mul(a, b)),
+    (r"\{x+y\}\{a+b\}", _Mul(_Add(x, y), _Add(a, b))),
+]
+
+MIXED_BRACKET_BRACE_EXPRESSION_PAIRS = [
+    (r"[x+y]\{a+b\}", _Mul(_Add(x, y), _Add(a, b))),
+    (r"\{a+b\}[x+y]", _Mul(_Add(a, b), _Add(x, y))),
+    (r"[x]\{y\}", _Mul(x, y)),
+    (r"\{a\}[b]", _Mul(a, b)),
+]
+
 UNEVALUATED_LITERAL_COMPLEX_NUMBER_EXPRESSION_PAIRS = [
     (r"\imaginaryunit^2", _Pow(I, 2)),
     (r"|\imaginaryunit|", _Abs(I)),
@@ -857,6 +889,32 @@ def test_miscellaneous_expressions():
             assert parse_latex_lark(latex_str) == sympy_expr, latex_str

+def test_square_bracket_multiplication():
+    for latex_str, sympy_expr in SQUARE_BRACKET_EXPRESSION_PAIRS:
+        with evaluate(False):
+            result = parse_latex_lark(latex_str)
+            assert result == sympy_expr, f"Failed for {latex_str}: got {result}, expected {sympy_expr}"
+
+    for latex_str, sympy_expr in EVALUATED_SQUARE_BRACKET_EXPRESSION_PAIRS:
+        result = parse_latex_lark(latex_str)
+        assert result == sympy_expr, f"Failed for {latex_str}: got {result}, expected {sympy_expr}"
+
+
+def test_literal_brace_multiplication():
+    for latex_str, sympy_expr in LITERAL_BRACE_EXPRESSION_PAIRS:
+        with evaluate(False):
+            result = parse_latex_lark(latex_str)
+            assert result == sympy_expr, f"Failed for {latex_str}"
+
+
+def test_mixed_bracket_brace_multiplication():
+    for latex_str, sympy_expr in MIXED_BRACKET_BRACE_EXPRESSION_PAIRS:
+        with evaluate(False):
+            result = parse_latex_lark(latex_str)
+            assert result == sympy_expr, \
+                f"Failed for {latex_str}: got {result}, expected {sympy_expr}"
+
+
 def test_literal_complex_number_expressions():
     for latex_str, sympy_expr in UNEVALUATED_LITERAL_COMPLEX_NUMBER_EXPRESSION_PAIRS:
         with evaluate(False):

diff --git a/sympy/parsing/latex/lark/transformer.py b/sympy/parsing/latex/lark/transformer.py
index cbd514b65173..69b3f25d9bc6 100644
--- a/sympy/parsing/latex/lark/transformer.py
+++ b/sympy/parsing/latex/lark/transformer.py
@@ -126,6 +126,9 @@ def group_square_brackets(self, tokens):
     def group_curly_parentheses(self, tokens):
         return tokens[1]

+    def group_curly_literal_parentheses(self, tokens):
+        return tokens[1]
+
     def eq(self, tokens):
         return sympy.Eq(tokens[0], tokens[2])

@@ -219,7 +222,7 @@ def iscmdstar(x):
         base = tokens[0]
         if len(tokens) == 3: # a^b OR a^\prime OR a^\ast
             sup = tokens[2]
-        if len(tokens) == 5:
+        elif len(tokens) == 5:
             # a^{'}, a^{''}, ... OR
             # a^{*}, a^{**}, ... OR
             # a^{\prime}, a^{\prime\prime}, ... OR


# `Pow.as_real_imag` is inconsistent with evaluating `Pow(0, -1)` as ComplexInfinity

> Another example of something causing this issue: `1/Sum(1, (Symbol("p"), 8, 6))`
>
> I think this boils down to the following inconsistency:
> - `Pow(0,-1).as_real_imag() == (nan, nan)`
> - `Sum(1, (Symbol("p"), 8, 6)).is_nonzero is None`
> - `Pow(Sum(1, (Symbol("p"), 8, 6)), -1).as_real_imag() == (Pow(Sum(1, (Symbol("p"), 8, 6)), -1), 0)`
>
> I guess https://github.com/sympy/sympy/blob/c0adb406785d80e9b4bbe1f0c95e7357f371bb53/sympy/core/power.py#L1130-L1137 should have a case first about what to do if `not self.base.is_nonzero` when `not exp >= 0`

 _Originally posted by @JasonGross in [#28218](https://github.com/sympy/sympy/issues/28218#issuecomment-3047313998)_

diff --git a/sympy/core/tests/test_power.py b/sympy/core/tests/test_power.py
index 80ae48c525c2..d0405e33cba5 100644
--- a/sympy/core/tests/test_power.py
+++ b/sympy/core/tests/test_power.py
@@ -668,3 +668,9 @@ def test_issue_25165():
     e1 = (1/sqrt(( - x + 1)**2 + (x - 0.23)**4)).series(x, 0, 2)
     e2 = 0.998603724830355 + 1.02004923189934*x + O(x**2)
     assert all_close(e1, e2)
+
+
+def test_issue_28219():
+    assert Pow(S.Zero, -1, evaluate=False).as_real_imag() == (S.NaN, S.NaN)
+    x = Symbol('x', real=True)
+    assert Pow(x, -1, evaluate=False).as_real_imag() == (1/x, S.Zero)

diff --git a/sympy/core/power.py b/sympy/core/power.py
index cc9e90066bc0..a29731d083a9 100644
--- a/sympy/core/power.py
+++ b/sympy/core/power.py
@@ -1132,9 +1132,14 @@ def as_real_imag(self, deep=True, **hints):
             from sympy.polys.polytools import poly

             exp = self.exp
+            if exp < 0 and self.base.is_zero:
+                return (S.NaN, S.NaN)
+
             re_e, im_e = self.base.as_real_imag(deep=deep)
+
             if not im_e:
                 return self, S.Zero
+
             a, b = symbols('a b', cls=Dummy)
             if exp >= 0:
                 if re_e.is_Number and im_e.is_Number: