From 75004528102ad9a8742388602b493ca525140b98 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Thu, 20 Nov 2025 11:26:11 +0530 Subject: [PATCH 1/5] check for errors from fetch operation --- mssql_python/cursor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 1026507e1..bd31e48e8 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2156,7 +2156,10 @@ def fetchall(self) -> List[Row]: # Fetch raw data rows_data = [] try: - _ = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) + ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) + + # Check for errors from the fetch operation + check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) if self.hstmt: self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt)) From befccb81b75029ac2fded97a34859bf86f6ffde6 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 21 Nov 2025 18:19:51 +0530 Subject: [PATCH 2/5] same behavior as pyodbc --- mssql_python/cursor.py | 2 +- tests/test_003_connection.py | 3 ++- tests/test_004_cursor.py | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index bd31e48e8..9ab3180ef 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2158,7 +2158,7 @@ def fetchall(self) -> List[Row]: try: ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) - # Check for errors from the fetch operation + # Check for errors check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) if self.hstmt: diff --git a/tests/test_003_connection.py b/tests/test_003_connection.py index 8db506bc4..4237022c1 100644 --- a/tests/test_003_connection.py +++ b/tests/test_003_connection.py @@ -5340,7 +5340,8 @@ def test_timeout_long_query(db_connection): # Method 2: Try with WAITFOR start_time = time.perf_counter() cursor.execute("WAITFOR DELAY '00:00:05'") - cursor.fetchall() + # Don't call fetchall() on WAITFOR - it doesn't return results + # The execute itself should timeout elapsed_time = time.perf_counter() - start_time # If we still get here, try one more approach diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index c7d4d5bba..b3ad8e3eb 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10667,7 +10667,9 @@ def test_procedures_result_set_info(cursor, db_connection): # Test execution of the procedures to verify they work cursor.execute("EXEC pytest_proc_schema.test_no_results") - assert cursor.fetchall() == [], "test_no_results should return no results" + # Procedures with no results should have no description and calling fetchall() should raise an error + assert cursor.description is None, "test_no_results should have no description (no result set)" + # Don't call fetchall() on procedures with no results - this is invalid in ODBC cursor.execute("EXEC pytest_proc_schema.test_one_result") rows = cursor.fetchall() From 8273dccd12804e2d528bb3b71e6951c5f8bb2bd5 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Tue, 25 Nov 2025 12:49:25 +0530 Subject: [PATCH 3/5] linting --- mssql_python/cursor.py | 2 +- tests/test_004_cursor.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 9ab3180ef..8f4f01205 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2157,7 +2157,7 @@ def fetchall(self) -> List[Row]: rows_data = [] try: ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data) - + # Check for errors check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index b3ad8e3eb..b7f79db07 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10668,7 +10668,9 @@ def test_procedures_result_set_info(cursor, db_connection): # Test execution of the procedures to verify they work cursor.execute("EXEC pytest_proc_schema.test_no_results") # Procedures with no results should have no description and calling fetchall() should raise an error - assert cursor.description is None, "test_no_results should have no description (no result set)" + assert ( + cursor.description is None, + ), "test_no_results should have no description (no result set)" # Don't call fetchall() on procedures with no results - this is invalid in ODBC cursor.execute("EXEC pytest_proc_schema.test_one_result") From e91baeb22f55d0bd17ddfb454fd178f83168591c Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 28 Nov 2025 14:17:35 +0530 Subject: [PATCH 4/5] added test --- tests/test_004_cursor.py | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index b7f79db07..3ad09fd42 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -15497,6 +15497,88 @@ def test_fixed_length_binary_type(cursor, db_connection): pytest.fail(f"Fixed-length BINARY test failed: {e}") +def test_fetchall_with_integrity_constraint(cursor, db_connection): + """ + Test that UNIQUE constraint errors are appropriately triggered for multi-row INSERT + statements that use OUTPUT inserted. + + This test covers a specific case where SQL Server's protocol has error conditions + that do not become apparent until rows are fetched, requiring special handling + in fetchall(). + """ + try: + # Setup table with unique constraint + cursor.execute("DROP TABLE IF EXISTS #uniq_cons_test") + cursor.execute(""" + CREATE TABLE #uniq_cons_test ( + id INTEGER NOT NULL IDENTITY, + data VARCHAR(50) NULL, + PRIMARY KEY (id), + UNIQUE (data) + ) + """) + + # Insert initial row - should work + cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ('the data 1',)) + cursor.fetchall() # Complete the operation + + # Test single row duplicate - should raise IntegrityError + with pytest.raises(mssql_python.IntegrityError): + cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ('the data 1',)) + cursor.fetchall() # Error should be detected here + + # Insert two valid rows in one statement - should work + cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ('the data 2', 'the data 3')) + cursor.fetchall() + + # Verify current state + cursor.execute("SELECT * FROM #uniq_cons_test ORDER BY id") + rows = cursor.fetchall() + expected_before = [(1, "the data 1"), (3, "the data 2"), (4, "the data 3")] + actual_before = [tuple(row) for row in rows] + assert actual_before == expected_before + + # THE CRITICAL TEST: Multi-row INSERT with duplicate values + # This should raise IntegrityError during fetchall() + with pytest.raises(mssql_python.IntegrityError): + cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ('the data 4', 'the data 4')) # Duplicate in same statement + + # The error should be detected HERE during fetchall() + cursor.fetchall() + + # Verify table state after failed multi-row insert + cursor.execute("SELECT * FROM #uniq_cons_test ORDER BY id") + rows = cursor.fetchall() + expected_after = [(1, "the data 1"), (3, "the data 2"), (4, "the data 3")] + actual_after = [tuple(row) for row in rows] + assert actual_after == expected_after, "Table should be unchanged after failed insert" + + # Test timing: execute() should succeed, error detection happens in fetchall() + try: + cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ('the data 5', 'the data 5')) + execute_succeeded = True + except Exception: + execute_succeeded = False + + assert execute_succeeded, "execute() should succeed, error detection happens in fetchall()" + + # fetchall() should raise the IntegrityError + with pytest.raises(mssql_python.IntegrityError): + cursor.fetchall() + + except Exception as e: + pytest.fail(f"Integrity constraint multi-row test failed: {e}") + finally: + # Cleanup + try: + cursor.execute("DROP TABLE IF EXISTS #uniq_cons_test") + except: + pass + + def test_close(db_connection): """Test closing the cursor""" try: From 237d0b5f8ea94d7ab9547d83c13131d12727d25c Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 28 Nov 2025 14:34:30 +0530 Subject: [PATCH 5/5] linting --- tests/test_004_cursor.py | 62 ++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 3ad09fd42..a1c2a908f 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -15499,76 +15499,88 @@ def test_fixed_length_binary_type(cursor, db_connection): def test_fetchall_with_integrity_constraint(cursor, db_connection): """ - Test that UNIQUE constraint errors are appropriately triggered for multi-row INSERT + Test that UNIQUE constraint errors are appropriately triggered for multi-row INSERT statements that use OUTPUT inserted. - - This test covers a specific case where SQL Server's protocol has error conditions - that do not become apparent until rows are fetched, requiring special handling + + This test covers a specific case where SQL Server's protocol has error conditions + that do not become apparent until rows are fetched, requiring special handling in fetchall(). """ try: # Setup table with unique constraint cursor.execute("DROP TABLE IF EXISTS #uniq_cons_test") - cursor.execute(""" + cursor.execute( + """ CREATE TABLE #uniq_cons_test ( id INTEGER NOT NULL IDENTITY, data VARCHAR(50) NULL, PRIMARY KEY (id), UNIQUE (data) ) - """) - + """ + ) + # Insert initial row - should work - cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ('the data 1',)) + cursor.execute( + "INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ("the data 1",) + ) cursor.fetchall() # Complete the operation - + # Test single row duplicate - should raise IntegrityError with pytest.raises(mssql_python.IntegrityError): - cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ('the data 1',)) + cursor.execute( + "INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?)", ("the data 1",) + ) cursor.fetchall() # Error should be detected here - + # Insert two valid rows in one statement - should work - cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", - ('the data 2', 'the data 3')) + cursor.execute( + "INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ("the data 2", "the data 3"), + ) cursor.fetchall() - + # Verify current state cursor.execute("SELECT * FROM #uniq_cons_test ORDER BY id") rows = cursor.fetchall() expected_before = [(1, "the data 1"), (3, "the data 2"), (4, "the data 3")] actual_before = [tuple(row) for row in rows] assert actual_before == expected_before - + # THE CRITICAL TEST: Multi-row INSERT with duplicate values # This should raise IntegrityError during fetchall() with pytest.raises(mssql_python.IntegrityError): - cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", - ('the data 4', 'the data 4')) # Duplicate in same statement - + cursor.execute( + "INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ("the data 4", "the data 4"), + ) # Duplicate in same statement + # The error should be detected HERE during fetchall() cursor.fetchall() - + # Verify table state after failed multi-row insert cursor.execute("SELECT * FROM #uniq_cons_test ORDER BY id") rows = cursor.fetchall() expected_after = [(1, "the data 1"), (3, "the data 2"), (4, "the data 3")] actual_after = [tuple(row) for row in rows] assert actual_after == expected_after, "Table should be unchanged after failed insert" - + # Test timing: execute() should succeed, error detection happens in fetchall() try: - cursor.execute("INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", - ('the data 5', 'the data 5')) + cursor.execute( + "INSERT INTO #uniq_cons_test (data) OUTPUT inserted.id VALUES (?), (?)", + ("the data 5", "the data 5"), + ) execute_succeeded = True except Exception: execute_succeeded = False - + assert execute_succeeded, "execute() should succeed, error detection happens in fetchall()" - + # fetchall() should raise the IntegrityError with pytest.raises(mssql_python.IntegrityError): cursor.fetchall() - + except Exception as e: pytest.fail(f"Integrity constraint multi-row test failed: {e}") finally: