``psycopg`` release notes
=========================
+Future releases
+---------------
+
+Psycopg 3.3.5 (unreleased)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+- Discard prepared statements upon :sql:`ALTER *` or `DISCARD *`
+ (:ticket:`#1307`).
+
+
Current release
---------------
from __future__ import annotations
+import re
from enum import IntEnum, auto
from typing import TYPE_CHECKING, Any, TypeAlias
from collections import OrderedDict, deque
# The query is not to be prepared yet
return Prepare.NO, b""
- def _should_discard(self, prep: Prepare, results: Sequence[PGresult]) -> bool:
+ def _should_discard(
+ self,
+ prep: Prepare,
+ results: Sequence[PGresult],
+ __should_clear: Any = re.compile(rb"^(?:DROP|ALTER|ROLLBACK|DISCARD)\b").match,
+ ) -> bool:
"""Check if we need to discard our entire state: it should happen on
rollback or on dropping objects, because the same object may get
recreated and postgres would fail internal lookups.
for result in results:
if result.status != COMMAND_OK:
continue
- cmdstat = result.command_status
- if cmdstat and (cmdstat.startswith(b"DROP ") or cmdstat == b"ROLLBACK"):
+ if (cmdstat := result.command_status) and __should_clear(cmdstat):
return self.clear()
return False
import logging
import datetime as dt
from decimal import Decimal
+from operator import itemgetter
import pytest
raise ZeroDivisionError()
+def test_alter_table_clears_state(conn):
+ conn.prepare_threshold = 0
+ conn.execute("DROP TABLE IF EXISTS testalter")
+ conn.execute("CREATE TABLE testalter (id serial primary key, data text)")
+ conn.execute("INSERT INTO testalter (data) values (%s)", ["foo"])
+ cur = conn.execute("SELECT * from testalter ORDER BY id")
+ assert list(map(itemgetter(1), cur.fetchall())) == ["foo"]
+ conn.execute("ALTER TABLE testalter ADD data2 text")
+ conn.execute("INSERT INTO testalter (data) values (%s)", ["bar"])
+ cur = conn.execute("SELECT * from testalter ORDER BY id")
+ assert list(map(itemgetter(1), cur.fetchall())) == ["foo", "bar"]
+
+
+def test_discard_clears_state(conn):
+ conn.set_autocommit(True)
+ conn.prepare_threshold = 0
+ conn.execute("DROP TABLE IF EXISTS testdisc")
+ conn.execute("CREATE TABLE testdisc (id serial primary key, data text)")
+ conn.execute("INSERT INTO testdisc (data) values (%s)", ["foo"])
+ conn.execute("DISCARD ALL")
+ conn.execute("INSERT INTO testdisc (data) values (%s)", ["bar"])
+
+
def get_prepared_statements(conn):
cur = conn.cursor(row_factory=namedtuple_row)
# CRDB has 'PREPARE name AS' in the statement.
import logging
import datetime as dt
from decimal import Decimal
+from operator import itemgetter
import pytest
raise ZeroDivisionError()
+async def test_alter_table_clears_state(aconn):
+ aconn.prepare_threshold = 0
+ await aconn.execute("DROP TABLE IF EXISTS testalter")
+ await aconn.execute("CREATE TABLE testalter (id serial primary key, data text)")
+ await aconn.execute("INSERT INTO testalter (data) values (%s)", ["foo"])
+ cur = await aconn.execute("SELECT * from testalter ORDER BY id")
+ assert list(map(itemgetter(1), await cur.fetchall())) == ["foo"]
+ await aconn.execute("ALTER TABLE testalter ADD data2 text")
+ await aconn.execute("INSERT INTO testalter (data) values (%s)", ["bar"])
+ cur = await aconn.execute("SELECT * from testalter ORDER BY id")
+ assert list(map(itemgetter(1), await cur.fetchall())) == ["foo", "bar"]
+
+
+async def test_discard_clears_state(aconn):
+ await aconn.set_autocommit(True)
+ aconn.prepare_threshold = 0
+ await aconn.execute("DROP TABLE IF EXISTS testdisc")
+ await aconn.execute("CREATE TABLE testdisc (id serial primary key, data text)")
+ await aconn.execute("INSERT INTO testdisc (data) values (%s)", ["foo"])
+ await aconn.execute("DISCARD ALL")
+ await aconn.execute("INSERT INTO testdisc (data) values (%s)", ["bar"])
+
+
async def get_prepared_statements(aconn):
cur = aconn.cursor(row_factory=namedtuple_row)
# CRDB has 'PREPARE name AS' in the statement.