Repository: django/django

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


Implementation patch:
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:


Test patch:
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__)