From 43ba7e7a1ff78c11edb630e2d4b71703044da75f Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 15:47:55 +0800 Subject: [PATCH 01/17] refactor: fix the way backend pid is obtained in Gaussdb --- psycopg/psycopg/pq/pq_ctypes.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/psycopg/psycopg/pq/pq_ctypes.py b/psycopg/psycopg/pq/pq_ctypes.py index ac607de39..0c82bda80 100644 --- a/psycopg/psycopg/pq/pq_ctypes.py +++ b/psycopg/psycopg/pq/pq_ctypes.py @@ -82,6 +82,8 @@ class PGconn: "_self_ptr", "_procpid", "__weakref__", + "_backend_pid", + "_server_version", ) def __init__(self, pgconn_ptr: impl.PGconn_struct): @@ -94,6 +96,8 @@ def __init__(self, pgconn_ptr: impl.PGconn_struct): impl.PQsetNoticeReceiver(pgconn_ptr, notice_receiver, byref(self._self_ptr)) self._procpid = getpid() + self._backend_pid = None + self._server_version = None def __del__(self) -> None: # Close the connection only if it was created in this process, @@ -236,8 +240,10 @@ def protocol_version(self) -> int: @property def server_version(self) -> str: - res = self.exec_(b"select version()") - return res.get_value(0, 0).decode().split(" ")[3] + if not self._server_version: + res = self.exec_(b"select version()") + self._server_version = res.get_value(0, 0).decode().split(" ")[3] + return self._server_version @property def socket(self) -> int: @@ -248,7 +254,10 @@ def socket(self) -> int: @property def backend_pid(self) -> int: - return self._call_int(impl.PQbackendPID) + if not self._backend_pid: + res = self.exec_(b"select pg_backend_pid()") + self._backend_pid = res.get_value(0, 0).decode() + return self._backend_pid @property def needs_password(self) -> bool: From 08357766f21f5e1f38cc067770921af800e89478 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 16:13:16 +0800 Subject: [PATCH 02/17] test: obtain the backend_pid before asynchronous execution terminates for test_identify_closure --- tests/test_concurrency_async.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_concurrency_async.py b/tests/test_concurrency_async.py index 65bc2539b..7726cdae1 100644 --- a/tests/test_concurrency_async.py +++ b/tests/test_concurrency_async.py @@ -125,11 +125,12 @@ async def test_identify_closure(aconn_cls, dsn): async def closer(): await asyncio.sleep(0.2) await conn2.execute( - "select pg_terminate_backend(%s)", [aconn.pgconn.backend_pid] + "select pg_terminate_backend(%s)", [aconn_pid] ) aconn = await aconn_cls.connect(dsn) conn2 = await aconn_cls.connect(dsn) + aconn_pid = aconn.pgconn.backend_pid try: t = create_task(closer()) t0 = time.time() From 5a142d46938e2f18186feedd301e734315f0f1ef Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 17:15:20 +0800 Subject: [PATCH 03/17] test: skip the SERIALIZABLE isolation level and update the default isolation level to repeatable read --- tests/_test_connection.py | 14 +++++++------- tests/test_connection_async.py | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/_test_connection.py b/tests/_test_connection.py index d48ee5230..537a58049 100644 --- a/tests/_test_connection.py +++ b/tests/_test_connection.py @@ -40,7 +40,7 @@ class ParamDef: name="isolation_level", guc="isolation", values=list(psycopg.IsolationLevel), - non_default="serializable", + non_default="repeatable read", ) param_read_only = ParamDef( name="read_only", @@ -74,12 +74,12 @@ class ParamDef: id="isolation_level", marks=pytest.mark.crdb("skip", reason="transaction isolation"), ), - pytest.param( - param_read_only, id="read_only", marks=pytest.mark.crdb_skip("begin_read_only") - ), - pytest.param( - param_deferrable, id="deferrable", marks=pytest.mark.crdb_skip("deferrable") - ), + # pytest.param( + # param_read_only, id="read_only", marks=pytest.mark.crdb_skip("begin_read_only") + # ), + # pytest.param( + # param_deferrable, id="deferrable", marks=pytest.mark.crdb_skip("deferrable") + # ), ] diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index c8da671da..b6b099e79 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -646,6 +646,8 @@ async def test_transaction_param_readonly_property(aconn, param): async def test_set_transaction_param_implicit(aconn, param, autocommit): await aconn.set_autocommit(autocommit) for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") await getattr(aconn, f"set_{param.name}")(value) cur = await aconn.execute( "select current_setting(%s), current_setting(%s)", @@ -668,6 +670,8 @@ async def test_set_transaction_param_reset(aconn, param): await aconn.commit() for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") await getattr(aconn, f"set_{param.name}")(value) cur = await aconn.execute( "select current_setting(%s)", [f"transaction_{param.guc}"] @@ -690,6 +694,8 @@ async def test_set_transaction_param_reset(aconn, param): async def test_set_transaction_param_block(aconn, param, autocommit): await aconn.set_autocommit(autocommit) for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") await getattr(aconn, f"set_{param.name}")(value) async with aconn.transaction(): cur = await aconn.execute( From af4da99f84333b37ecfb3032a91fa3d29e205569 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 17:21:00 +0800 Subject: [PATCH 04/17] test: for the pid field type, use bigint instead of int --- tests/test_connection_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index b6b099e79..25a61c685 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -219,7 +219,7 @@ async def test_context_inerror_rollback_no_clobber(aconn_cls, conn, dsn, caplog) async with await aconn_cls.connect(dsn) as conn2: await conn2.execute("select 1") conn.execute( - "select pg_terminate_backend(%s::int)", + "select pg_terminate_backend(%s::bigint)", [conn2.pgconn.backend_pid], ) 1 / 0 From 6f401b6da17b8e0ccd433b40eaf0fb4809556ded Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 20:27:09 +0800 Subject: [PATCH 05/17] test: use the severity instead of severity_nonlocalized attribute of class Diagnostic --- tests/test_connection_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 25a61c685..68c519acc 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -499,7 +499,7 @@ def cb2(res): aconn.add_notice_handler(cb1) aconn.add_notice_handler(cb2) aconn.add_notice_handler("the wrong thing") - aconn.add_notice_handler(lambda diag: severities.append(diag.severity_nonlocalized)) + aconn.add_notice_handler(lambda diag: severities.append(diag.severity)) aconn.pgconn.exec_(b"set client_min_messages to notice") cur = aconn.cursor() From 42d90987f2e85cccaf0e9590888a6b2b3edc4349 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 20:33:04 +0800 Subject: [PATCH 06/17] refactor: remove unsupported error fiield severity_nonlocalized --- psycopg/psycopg/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psycopg/psycopg/errors.py b/psycopg/psycopg/errors.py index 5ee8239f3..8dd263911 100644 --- a/psycopg/psycopg/errors.py +++ b/psycopg/psycopg/errors.py @@ -440,7 +440,7 @@ def severity(self) -> str | None: @property def severity_nonlocalized(self) -> str | None: - return self._error_message(DiagnosticField.SEVERITY_NONLOCALIZED) + raise NotSupportedError("This is present only in reports generated by libpq versions 9.6 and later.") @property def sqlstate(self) -> str | None: From 00a9a8eac60ef18622875e13431d11a2bbf8b59d Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 21:58:31 +0800 Subject: [PATCH 07/17] test: the error type is OperationalError for GaussDB --- tests/test_connection_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 68c519acc..cdb44eb48 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -891,7 +891,7 @@ def fake_connect_gen(conninfo, **kwargs): @pytest.mark.crdb_skip("pg_terminate_backend") async def test_right_exception_on_server_disconnect(aconn): - with pytest.raises(e.AdminShutdown): + with pytest.raises(e.OperationalError): await aconn.execute( "select pg_terminate_backend(%s)", [aconn.pgconn.backend_pid] ) From 685dc27037f892053564393e54f0afc6e2cdc713 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Tue, 27 May 2025 22:00:09 +0800 Subject: [PATCH 08/17] test: use idle_in_transaction_timeout instead of idle_in_transaction_session_timeout and error result not returned in GaussDB --- tests/test_connection_async.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index cdb44eb48..1c213aef8 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -899,6 +899,7 @@ async def test_right_exception_on_server_disconnect(aconn): @pytest.mark.slow @pytest.mark.crdb("skip", reason="error result not returned") +@pytest.mark.gaussdb_skip("error result not returned") async def test_right_exception_on_session_timeout(aconn): want_ex: type[psycopg.Error] = e.IdleInTransactionSessionTimeout if sys.platform == "win32": @@ -907,7 +908,7 @@ async def test_right_exception_on_session_timeout(aconn): # with, not in the client. want_ex = psycopg.OperationalError - await aconn.execute("SET SESSION idle_in_transaction_session_timeout = 100") + await aconn.execute("SET SESSION idle_in_transaction_timeout = 100") await asleep(0.2) with pytest.raises(want_ex) as ex: await aconn.execute("SELECT * from pg_tables") From 20e05bafcf9b292729c173f0f3b84cd2df8c286d Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 10:02:25 +0800 Subject: [PATCH 09/17] test: restore the skiped parameters for tx_params_isolation --- tests/_test_connection.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/_test_connection.py b/tests/_test_connection.py index 537a58049..57257a5bc 100644 --- a/tests/_test_connection.py +++ b/tests/_test_connection.py @@ -74,12 +74,12 @@ class ParamDef: id="isolation_level", marks=pytest.mark.crdb("skip", reason="transaction isolation"), ), - # pytest.param( - # param_read_only, id="read_only", marks=pytest.mark.crdb_skip("begin_read_only") - # ), - # pytest.param( - # param_deferrable, id="deferrable", marks=pytest.mark.crdb_skip("deferrable") - # ), + pytest.param( + param_read_only, id="read_only", marks=pytest.mark.crdb_skip("begin_read_only") + ), + pytest.param( + param_deferrable, id="deferrable", marks=pytest.mark.crdb_skip("deferrable") + ), ] From 383789246d45eb6132b43e1a03e55cad5f4f5314 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 10:28:00 +0800 Subject: [PATCH 10/17] refactor: release the memory resources of server_version and backend_pid in the method finish --- psycopg/psycopg/pq/pq_ctypes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/psycopg/psycopg/pq/pq_ctypes.py b/psycopg/psycopg/pq/pq_ctypes.py index 0c82bda80..a45753e01 100644 --- a/psycopg/psycopg/pq/pq_ctypes.py +++ b/psycopg/psycopg/pq/pq_ctypes.py @@ -135,6 +135,12 @@ def connect_poll(self) -> int: def finish(self) -> None: self._pgconn_ptr, p = None, self._pgconn_ptr + self._backend_pid, pid = None, self._backend_pid + self._server_version, v = None, self._server_version + if pid: + del pid + if v: + del v if p: PQfinish(p) From 1adcc2f7e386c9667a67ec833d1e49a590781aab Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 11:55:06 +0800 Subject: [PATCH 11/17] test: add gaussdb_skip due to method PGconn.info is not implemented --- tests/test_connection_info.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_connection_info.py b/tests/test_connection_info.py index e7b45909b..01aed388e 100644 --- a/tests/test_connection_info.py +++ b/tests/test_connection_info.py @@ -44,6 +44,7 @@ def test_port(conn): conn.info.port +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_get_params(conn, dsn): info = conn.info.get_parameters() for k, v in conninfo_to_dict(dsn).items(): @@ -53,6 +54,7 @@ def test_get_params(conn, dsn): assert k not in info +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_dsn(conn, dsn): dsn = conn.info.dsn assert "password" not in dsn @@ -61,6 +63,7 @@ def test_dsn(conn, dsn): assert f"{k}=" in dsn +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_get_params_env(conn_cls, dsn, monkeypatch): dsn = conninfo_to_dict(dsn) dsn.pop("application_name", None) @@ -74,6 +77,7 @@ def test_get_params_env(conn_cls, dsn, monkeypatch): assert conn.info.get_parameters()["application_name"] == "hello test" +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_dsn_env(conn_cls, dsn, monkeypatch): dsn = conninfo_to_dict(dsn) dsn.pop("application_name", None) @@ -114,6 +118,7 @@ def test_pipeline_status_no_pipeline(conn): assert conn.info.pipeline_status.name == "OFF" +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_no_password(dsn): dsn2 = make_conninfo(dsn, password="the-pass-word") pgconn = psycopg.pq.PGconn.connect_start(dsn2.encode()) @@ -123,6 +128,7 @@ def test_no_password(dsn): assert info.get_parameters()["dbname"] == info.dbname +@pytest.mark.gaussdb_skip("This method PGconn.info is not implemented in GaussDB") def test_dsn_no_password(dsn): dsn2 = make_conninfo(dsn, password="the-pass-word") pgconn = psycopg.pq.PGconn.connect_start(dsn2.encode()) From 6da5c39456f18f3628ade1667ae00b52cbde2f7b Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 14:29:23 +0800 Subject: [PATCH 12/17] test: use bigint instead of int of backend_pid type --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 60d2083a1..249d93715 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -223,7 +223,7 @@ def test_context_inerror_rollback_no_clobber(conn_cls, conn, dsn, caplog): with conn_cls.connect(dsn) as conn2: conn2.execute("select 1") conn.execute( - "select pg_terminate_backend(%s::int)", [conn2.pgconn.backend_pid] + "select pg_terminate_backend(%s::bigint)", [conn2.pgconn.backend_pid] ) 1 / 0 From 9317c036203e45f6996a92f043d3ba55d5417166 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 14:33:22 +0800 Subject: [PATCH 13/17] test: use idle_in_transaction_timeout instead of idle_in_transaction_session_timeout and error result not returned in GaussDB --- tests/test_connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 249d93715..4a17c8814 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -887,6 +887,7 @@ def test_right_exception_on_server_disconnect(conn): @pytest.mark.slow @pytest.mark.crdb("skip", reason="error result not returned") +@pytest.mark.gaussdb_skip("error result not returned") def test_right_exception_on_session_timeout(conn): want_ex: type[psycopg.Error] = e.IdleInTransactionSessionTimeout if sys.platform == "win32": @@ -895,7 +896,7 @@ def test_right_exception_on_session_timeout(conn): # with, not in the client. want_ex = psycopg.OperationalError - conn.execute("SET SESSION idle_in_transaction_session_timeout = 100") + conn.execute("SET SESSION idle_in_transaction_timeout = 100") sleep(0.2) with pytest.raises(want_ex) as ex: conn.execute("SELECT * from pg_tables") From 86e8a902441468700d88c5a3d72b688c1fac72d6 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 14:38:30 +0800 Subject: [PATCH 14/17] test: the error type is OperationalError for GaussDB --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 4a17c8814..1646cc71a 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -881,7 +881,7 @@ def fake_connect_gen(conninfo, **kwargs): @pytest.mark.crdb_skip("pg_terminate_backend") def test_right_exception_on_server_disconnect(conn): - with pytest.raises(e.AdminShutdown): + with pytest.raises(e.OperationalError): conn.execute("select pg_terminate_backend(%s)", [conn.pgconn.backend_pid]) From 4b0773a8dcb0db99c0cd8e7c60db6e750084aca0 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 14:40:35 +0800 Subject: [PATCH 15/17] test: use the severity instead of severity_nonlocalized attribute of class Diagnostic --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 1646cc71a..6df60b280 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -502,7 +502,7 @@ def cb2(res): conn.add_notice_handler(cb1) conn.add_notice_handler(cb2) conn.add_notice_handler("the wrong thing") - conn.add_notice_handler(lambda diag: severities.append(diag.severity_nonlocalized)) + conn.add_notice_handler(lambda diag: severities.append(diag.severity)) conn.pgconn.exec_(b"set client_min_messages to notice") cur = conn.cursor() From 16c4c028fb12078b5cb45c4c06c6462f97b2fb3b Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 14:46:43 +0800 Subject: [PATCH 16/17] test: skip the SERIALIZABLE isolation level --- tests/test_connection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index 6df60b280..3a438e36b 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -648,6 +648,8 @@ def test_transaction_param_readonly_property(conn, param): def test_set_transaction_param_implicit(conn, param, autocommit): conn.set_autocommit(autocommit) for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") getattr(conn, f"set_{param.name}")(value) cur = conn.execute( "select current_setting(%s), current_setting(%s)", @@ -670,6 +672,8 @@ def test_set_transaction_param_reset(conn, param): conn.commit() for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") getattr(conn, f"set_{param.name}")(value) cur = conn.execute("select current_setting(%s)", [f"transaction_{param.guc}"]) (pgval,) = cur.fetchone() @@ -688,6 +692,8 @@ def test_set_transaction_param_reset(conn, param): def test_set_transaction_param_block(conn, param, autocommit): conn.set_autocommit(autocommit) for value in param.values: + if value == psycopg.IsolationLevel.SERIALIZABLE: + pytest.skip("GaussDB currently does not support SERIALIZABLE, which is equivalent to REPEATABLE READ") getattr(conn, f"set_{param.name}")(value) with conn.transaction(): cur = conn.execute( From 86683b9d62a465ca0d9b4d2ab3ec907384f07ea6 Mon Sep 17 00:00:00 2001 From: 5xuanwen Date: Wed, 28 May 2025 16:37:54 +0800 Subject: [PATCH 17/17] refactor: update the way to obtain the timezone to obtain it by executing a query statement --- psycopg/psycopg/_tz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psycopg/psycopg/_tz.py b/psycopg/psycopg/_tz.py index de7be44b5..a3dfbac54 100644 --- a/psycopg/psycopg/_tz.py +++ b/psycopg/psycopg/_tz.py @@ -22,8 +22,8 @@ def get_tzinfo(pgconn: PGconn | None) -> tzinfo: """Return the Python timezone info of the connection's timezone.""" - tzname = pgconn.parameter_status(b"TimeZone") if pgconn else None try: + tzname = pgconn.exec_(b"SHOW TimeZone").get_value(0, 0) if pgconn else None return _timezones[tzname] except KeyError: sname = tzname.decode() if tzname else "UTC"