]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: retain statusmessage after executemany with returning=False
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 30 Mar 2026 16:29:54 +0000 (18:29 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Fri, 1 May 2026 00:18:55 +0000 (02:18 +0200)
docs/news.rst
psycopg/psycopg/_cursor_base.py
tests/test_cursor_common.py
tests/test_cursor_common_async.py

index 8c7a512a785a6033a3d8b915d169a95978133c93..85c5f103a27cd1f227c045ca0dc901610a182edb 100644 (file)
@@ -17,6 +17,8 @@ Psycopg 3.3.4 (unreleased)
   in C extension (:ticket:`#1280`).
 - Fix client-side adaptation of enums whose name require quotes
   (:ticket:`#1298`).
+- Consistently populate `~Cursor.statusmessage` after `~Cursor.executemany()`
+  (:ticket:`#1302`).
 
 
 Current release
index 29cf1406e2b2ad9eeb4c4e615a6d00745bde174b..4ba3065fa7f815ae990aefde1adbe75ae2386211 100644 (file)
@@ -49,7 +49,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
     __slots__ = """
         _conn format _adapters arraysize _closed _results pgresult _pos
         _iresult _rowcount _query _tx _last_query _row_factory _make_row
-        _pgconn _execmany_returning
+        _pgconn _execmany_returning _statusmessage
         __weakref__
         """.split()
 
@@ -79,6 +79,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
         self._pos = 0
         self._iresult = 0
         self._rowcount = -1
+        self._statusmessage: bytes | None = None
         self._query: PostgresQuery | None
         # None if executemany() not executing, True/False according to returning state
         self._execmany_returning: bool | None = None
@@ -178,8 +179,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
 
         `!None` if the cursor doesn't have a result available.
         """
-        msg = self.pgresult.command_status if self.pgresult else None
-        return msg.decode() if msg else None
+        return self._statusmessage.decode() if self._statusmessage else None
 
     def _make_row_maker(self) -> RowMaker[Row]:
         raise NotImplementedError
@@ -525,6 +525,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
             nrows = self.pgresult.command_tuples
             self._rowcount = nrows if nrows is not None else -1
 
+        self._statusmessage = res.command_status
         self._make_row = self._make_row_maker()
 
     def _set_results(self, results: list[PGresult]) -> None:
@@ -540,9 +541,12 @@ class BaseCursor(Generic[ConnectionType, Row]):
                 self._select_current_result(0)
         else:
             # In non-returning case, set rowcount to the cumulated number of
-            # rows of executed queries.
-            for res in results:
-                self._rowcount += res.command_tuples or 0
+            # rows of executed queries. Keep the last result's command_status
+            # so that statusmessage is available after the batch completes.
+            if results:
+                self._statusmessage = results[-1].command_status
+                for res in results:
+                    self._rowcount += res.command_tuples or 0
 
     @classmethod
     def _loaders_changed(
index 6ecd1a03ecc06c4041a259b42eb7ba99c24ee1c7..a22d205381069b52432797b5599c779a0768526d 100644 (file)
@@ -150,6 +150,9 @@ def test_statusmessage(conn):
     cur.execute("select generate_series(1, 10)")
     assert cur.statusmessage == "SELECT 10"
 
+    cur.execute("")
+    assert cur.statusmessage is None
+
     cur.execute("create table statusmessage ()")
     assert cur.statusmessage == "CREATE TABLE"
 
@@ -398,6 +401,26 @@ def test_executemany_rowcount(conn, execmany):
     assert cur.rowcount == 2
 
 
+@pytest.mark.parametrize("returning", [False, True])
+def test_executemany_statusmessage(conn, execmany, returning):
+    cur = conn.cursor()
+    cur.executemany(
+        ph(cur, "insert into execmany(num, data) values (%s, %s)"),
+        [(10, "hello"), (20, "world")],
+        returning=returning,
+    )
+    assert cur.rowcount == (1 if returning else 2)
+    assert cur.statusmessage is not None
+    assert cur.statusmessage.startswith("INSERT")
+
+    cur.executemany(
+        ph(cur, "insert into execmany(num, data) values (%s, %s)"),
+        [],
+        returning=returning,
+    )
+    assert cur.statusmessage is None
+
+
 def test_executemany_returning(conn, execmany):
     cur = conn.cursor()
     cur.executemany(
index 7a8c772557330de7a46cdf82f53312827832623b..8fcfec059ca6da6437eec63ba72b67d85e5f62cd 100644 (file)
@@ -148,6 +148,9 @@ async def test_statusmessage(aconn):
     await cur.execute("select generate_series(1, 10)")
     assert cur.statusmessage == "SELECT 10"
 
+    await cur.execute("")
+    assert cur.statusmessage is None
+
     await cur.execute("create table statusmessage ()")
     assert cur.statusmessage == "CREATE TABLE"
 
@@ -400,6 +403,26 @@ async def test_executemany_rowcount(aconn, execmany):
     assert cur.rowcount == 2
 
 
+@pytest.mark.parametrize("returning", [False, True])
+async def test_executemany_statusmessage(aconn, execmany, returning):
+    cur = aconn.cursor()
+    await cur.executemany(
+        ph(cur, "insert into execmany(num, data) values (%s, %s)"),
+        [(10, "hello"), (20, "world")],
+        returning=returning,
+    )
+    assert cur.rowcount == (1 if returning else 2)
+    assert cur.statusmessage is not None
+    assert cur.statusmessage.startswith("INSERT")
+
+    await cur.executemany(
+        ph(cur, "insert into execmany(num, data) values (%s, %s)"),
+        [],
+        returning=returning,
+    )
+    assert cur.statusmessage is None
+
+
 async def test_executemany_returning(aconn, execmany):
     cur = aconn.cursor()
     await cur.executemany(