diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 3db3d6abd2..5ed856448f 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -223,7 +223,13 @@ class DatabaseWrapper(BaseDatabaseWrapper): conn_params.pop("assume_role", None) conn_params.pop("isolation_level", None) - conn_params.pop("server_side_binding", None) + server_side_binding = conn_params.pop("server_side_binding", None) + conn_params.setdefault( + "cursor_factory", + ServerBindingCursor + if is_psycopg3 and server_side_binding is True + else Cursor, + ) if settings_dict["USER"]: conn_params["user"] = settings_dict["USER"] if settings_dict["PASSWORD"]: @@ -269,20 +275,13 @@ class DatabaseWrapper(BaseDatabaseWrapper): connection = self.Database.connect(**conn_params) if set_isolation_level: connection.isolation_level = self.isolation_level - if is_psycopg3: - connection.cursor_factory = ( - ServerBindingCursor - if options.get("server_side_binding") is True - else Cursor - ) - else: + if not is_psycopg3: # Register dummy loads() to avoid a round trip from psycopg2's # decode to json.dumps() to json.loads(), when using a custom # decoder in JSONField. psycopg2.extras.register_default_jsonb( conn_or_curs=connection, loads=lambda x: x ) - connection.cursor_factory = Cursor return connection def ensure_timezone(self): diff --git a/docs/releases/4.2.1.txt b/docs/releases/4.2.1.txt index f8a4edf787..dd172a1a89 100644 --- a/docs/releases/4.2.1.txt +++ b/docs/releases/4.2.1.txt @@ -18,3 +18,7 @@ Bugfixes * Fixed a regression in Django 4.2 that caused aggregation over query that uses explicit grouping to group against the wrong columns (:ticket:`34464`). + +* Reallowed, following a regression in Django 4.2, setting the + ``"cursor_factory"`` option in :setting:`OPTIONS` on PostgreSQL + (:ticket:`34466`). diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index ab66e7ab0e..5589daa0c2 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -296,6 +296,24 @@ class Tests(TestCase): finally: new_connection.close() + def test_connect_custom_cursor_factory(self): + """ + A custom cursor factory can be configured with DATABASES["options"] + ["cursor_factory"]. + """ + from django.db.backends.postgresql.base import Cursor + + class MyCursor(Cursor): + pass + + new_connection = connection.copy() + new_connection.settings_dict["OPTIONS"]["cursor_factory"] = MyCursor + try: + new_connection.connect() + self.assertEqual(new_connection.connection.cursor_factory, MyCursor) + finally: + new_connection.close() + def test_connect_no_is_usable_checks(self): new_connection = connection.copy() try: