From 68e3296957ae45c7f2cff2a1e2acd20f7e885fef Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Thu, 10 Sep 2026 05:17:31 +0000 Subject: [PATCH 1/2] Fix mixed cursor cleanup shutdown crash --- tests/test_005_connection_cursor_lifecycle.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_005_connection_cursor_lifecycle.py b/tests/test_005_connection_cursor_lifecycle.py index 1b81d7023..2e1062a0c 100644 --- a/tests/test_005_connection_cursor_lifecycle.py +++ b/tests/test_005_connection_cursor_lifecycle.py @@ -574,8 +574,9 @@ def test_cursor_operations_after_close_raise_errors(conn_str): def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): """Test various mixed cleanup scenarios in one script""" code = f""" +import gc + from mssql_python import connect -from mssql_python.exceptions import ProgrammingError # Test 1: Normal cursor close conn1 = connect(\"\"\"{conn_str}\"\"\") @@ -592,7 +593,10 @@ def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): cursor2 = conn1.cursor() cursor2.execute("SELECT 2") cursor2.fetchall() -# Don't close cursor2, let __del__ handle it +del cursor2 +gc.collect() +assert len(conn1._cursors) == 0 +print("PASS: Cursor __del__ cleaned up cursor") # Test 4: Connection close cleans up cursors conn2 = connect(\"\"\"{conn_str}\"\"\") @@ -611,6 +615,8 @@ def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): # Clean up conn1.close() +del cursor1, cursor3, cursor4, conn1, conn2 +gc.collect() print("All tests passed") """ @@ -622,6 +628,7 @@ def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): assert result.returncode == 0, f"Script failed: {result.stderr}" assert "PASS: Double close does not raise error" in result.stdout + assert "PASS: Cursor __del__ cleaned up cursor" in result.stdout assert "PASS: Connection close cleaned up cursors" in result.stdout assert "All tests passed" in result.stdout # Should not have error logs From 5310c93695d509117db82264ed8abebcc8ed4353 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Thu, 10 Sep 2026 05:30:43 +0000 Subject: [PATCH 2/2] Verify cursor destructor invokes close --- tests/test_005_connection_cursor_lifecycle.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_005_connection_cursor_lifecycle.py b/tests/test_005_connection_cursor_lifecycle.py index 2e1062a0c..e91965dfe 100644 --- a/tests/test_005_connection_cursor_lifecycle.py +++ b/tests/test_005_connection_cursor_lifecycle.py @@ -85,7 +85,7 @@ def test_no_segfault_on_gc(conn_str): # Properly escape the connection string for embedding in code escaped_conn_str = conn_str.replace("\\", "\\\\").replace('"', '\\"') code = f""" -from mssql_python import connect +from mssql_python import Cursor, connect conn = connect("{escaped_conn_str}") cursors = [conn.cursor() for _ in range(5)] for cur in cursors: @@ -576,7 +576,7 @@ def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): code = f""" import gc -from mssql_python import connect +from mssql_python import Cursor, connect # Test 1: Normal cursor close conn1 = connect(\"\"\"{conn_str}\"\"\") @@ -593,8 +593,21 @@ def test_mixed_cursor_cleanup_scenarios(conn_str, tmp_path): cursor2 = conn1.cursor() cursor2.execute("SELECT 2") cursor2.fetchall() -del cursor2 -gc.collect() +cursor2_id = id(cursor2) +cursor2_close_calls = [] +original_close = Cursor.close + +def tracking_close(cursor): + cursor2_close_calls.append(id(cursor)) + return original_close(cursor) + +Cursor.close = tracking_close +try: + del cursor2 + gc.collect() +finally: + Cursor.close = original_close +assert cursor2_id in cursor2_close_calls assert len(conn1._cursors) == 0 print("PASS: Cursor __del__ cleaned up cursor")