From 86fd270f214cb262cb38c7b2a0b592b8c3e9e4bb Mon Sep 17 00:00:00 2001 From: Matthew Francis Brunetti Date: Fri, 13 Feb 2026 17:15:17 -0500 Subject: [PATCH] Make close() idempotent for already-closed connections Return True when ibm_db.close() or ibm_db_dbi.Connection.close() is called on an already-closed connection instead of raising. Add a DBI regression test for double-close behavior. --- ibm_db.c | 10 +++--- ibm_db_dbi.py | 22 +++++++------ ibm_db_tests/test_072_CloseTwice_DBI.py | 42 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 ibm_db_tests/test_072_CloseTwice_DBI.py diff --git a/ibm_db.c b/ibm_db.c index 3e4fcda5..8c4cd988 100644 --- a/ibm_db.c +++ b/ibm_db.c @@ -4274,7 +4274,8 @@ static PyObject *ibm_db_bind_param(PyObject *self, PyObject *args) * Specifies an active DB2 client connection. * * ===Return Values - * Returns TRUE on success or FALSE on failure. + * Returns TRUE on success. If the connection is already closed, returns TRUE. + * On failure, an exception is raised and NULL is returned. */ static PyObject *ibm_db_close(PyObject *self, PyObject *args) { @@ -4314,9 +4315,10 @@ static PyObject *ibm_db_close(PyObject *self, PyObject *args) if (!conn_res->handle_active) { - LogMsg(EXCEPTION, "Connection is not active"); - PyErr_SetString(PyExc_Exception, "Connection is not active"); - return NULL; + LogMsg(INFO, "Connection already closed; no action required"); + Py_INCREF(Py_True); + LogMsg(INFO, "exit close()"); + return Py_True; } if (conn_res->handle_active && !conn_res->flag_pconnect) diff --git a/ibm_db_dbi.py b/ibm_db_dbi.py index ece68a6d..ee7e43f6 100644 --- a/ibm_db_dbi.py +++ b/ibm_db_dbi.py @@ -923,20 +923,22 @@ def close(self): """This method closes the Database connection associated with the Connection object. It takes no arguments. + If the connection is already closed, this method is a no-op and + returns True. + """ LogMsg(INFO, "entry close()") - self.rollback() - try: - if self.conn_handler is None: - LogMsg(ERROR, "Connection cannot be closed; connection is no longer active.") - raise ProgrammingError("Connection cannot be closed; " - "connection is no longer active.") - else: + if self.conn_handler is None: + return_value = True + LogMsg(INFO, "Connection already closed; no action required.") + else: + self.rollback() + try: return_value = ibm_db.close(self.conn_handler) LogMsg(INFO, "Connection closed.") - except Exception as inst: - LogMsg(EXCEPTION, f"An exception occurred while closing connection: {inst}") - raise _get_exception(inst) + except Exception as inst: + LogMsg(EXCEPTION, f"An exception occurred while closing connection: {inst}") + raise _get_exception(inst) self.conn_handler = None for index in range(len(self._cursor_list)): if (self._cursor_list[index]() != None): diff --git a/ibm_db_tests/test_072_CloseTwice_DBI.py b/ibm_db_tests/test_072_CloseTwice_DBI.py new file mode 100644 index 00000000..0f8bc254 --- /dev/null +++ b/ibm_db_tests/test_072_CloseTwice_DBI.py @@ -0,0 +1,42 @@ +# +# Licensed Materials - Property of IBM +# +# (c) Copyright IBM Corp. 2026 +# + +from __future__ import print_function +import sys +import unittest +import ibm_db_dbi +import config +from testfunctions import IbmDbTestFunctions + + +class IbmDbTestCase(unittest.TestCase): + + def test_072_CloseTwice_DBI(self): + obj = IbmDbTestFunctions() + obj.assert_expect(self.run_test_072) + + def run_test_072(self): + conn = ibm_db_dbi.connect(config.database, config.user, config.password) + + rc1 = conn.close() + rc2 = conn.close() + + print(rc1) + print(rc2) + +#__END__ +#__LUW_EXPECTED__ +#True +#True +#__ZOS_EXPECTED__ +#True +#True +#__SYSTEMI_EXPECTED__ +#True +#True +#__IDS_EXPECTED__ +#True +#True