Skip to content

Commit 9fcfe87

Browse files
authored
FIX: cursor.rowcount (#263)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- External contributors: GitHub Issue --> > GitHub Issue: #258 ------------------------------------------------------------------- ### Summary This pull request improves the accuracy and consistency of the `rowcount` attribute in the `mssql_python` cursor implementation, ensuring it reflects the correct number of rows affected or fetched after various operations. It also introduces comprehensive tests to verify `rowcount` behavior for different fetch methods and scenarios, including edge cases and specific data types. **Enhancements to `rowcount` logic:** * Updated `fetchone`, `fetchmany`, and `fetchall` methods in `mssql_python/cursor.py` to set `rowcount` correctly after each fetch operation, including for empty result sets. This ensures `rowcount` is 0 for empty results, and matches the total number of rows fetched for non-empty results. **Expanded test coverage for `rowcount`:** * Added new tests in `tests/test_004_cursor.py` to verify `rowcount` updates after `fetchone`, `fetchmany`, and `fetchall`, and to check behavior for inserts, selects, and edge cases such as empty result sets and tables with GUID columns. These tests cover typical usage patterns as well as specific scenarios reported in GitHub issues.
1 parent 7fbd220 commit 9fcfe87

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

mssql_python/cursor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,10 @@ def fetchone(self) -> Union[None, Row]:
17251725
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
17261726

17271727
if ret == ddbc_sql_const.SQL_NO_DATA.value:
1728+
# No more data available
1729+
if self._next_row_index == 0 and self.description is not None:
1730+
# This is an empty result set, set rowcount to 0
1731+
self.rowcount = 0
17281732
return None
17291733

17301734
# Update internal position after successful fetch
@@ -1733,6 +1737,8 @@ def fetchone(self) -> Union[None, Row]:
17331737
self._next_row_index += 1
17341738
else:
17351739
self._increment_rownumber()
1740+
1741+
self.rowcount = self._next_row_index
17361742

17371743
# Create and return a Row object, passing column name map if available
17381744
column_map = getattr(self, '_column_name_map', None)
@@ -1775,6 +1781,12 @@ def fetchmany(self, size: int = None) -> List[Row]:
17751781
# advance counters by number of rows actually returned
17761782
self._next_row_index += len(rows_data)
17771783
self._rownumber = self._next_row_index - 1
1784+
1785+
# Centralize rowcount assignment after fetch
1786+
if len(rows_data) == 0 and self._next_row_index == 0:
1787+
self.rowcount = 0
1788+
else:
1789+
self.rowcount = self._next_row_index
17781790

17791791
# Convert raw data to Row objects
17801792
column_map = getattr(self, '_column_name_map', None)
@@ -1807,6 +1819,12 @@ def fetchall(self) -> List[Row]:
18071819
if rows_data and self._has_result_set:
18081820
self._next_row_index += len(rows_data)
18091821
self._rownumber = self._next_row_index - 1
1822+
1823+
# Centralize rowcount assignment after fetch
1824+
if len(rows_data) == 0 and self._next_row_index == 0:
1825+
self.rowcount = 0
1826+
else:
1827+
self.rowcount = self._next_row_index
18101828

18111829
# Convert raw data to Row objects
18121830
column_map = getattr(self, '_column_name_map', None)

tests/test_004_cursor.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9647,6 +9647,190 @@ def test_primarykeys_cleanup(cursor, db_connection):
96479647
except Exception as e:
96489648
pytest.fail(f"Test cleanup failed: {e}")
96499649

9650+
def test_rowcount_after_fetch_operations(cursor, db_connection):
9651+
"""Test that rowcount is updated correctly after various fetch operations."""
9652+
try:
9653+
# Create a test table
9654+
cursor.execute("CREATE TABLE #rowcount_fetch_test (id INT PRIMARY KEY, name NVARCHAR(100))")
9655+
9656+
# Insert some test data
9657+
cursor.execute("INSERT INTO #rowcount_fetch_test VALUES (1, 'Row 1')")
9658+
cursor.execute("INSERT INTO #rowcount_fetch_test VALUES (2, 'Row 2')")
9659+
cursor.execute("INSERT INTO #rowcount_fetch_test VALUES (3, 'Row 3')")
9660+
cursor.execute("INSERT INTO #rowcount_fetch_test VALUES (4, 'Row 4')")
9661+
cursor.execute("INSERT INTO #rowcount_fetch_test VALUES (5, 'Row 5')")
9662+
db_connection.commit()
9663+
9664+
# Test fetchone
9665+
cursor.execute("SELECT * FROM #rowcount_fetch_test ORDER BY id")
9666+
# Initially, rowcount should be -1 after a SELECT statement
9667+
assert cursor.rowcount == -1, "rowcount should be -1 right after SELECT statement"
9668+
9669+
# After fetchone, rowcount should be 1
9670+
row = cursor.fetchone()
9671+
assert row is not None, "Should fetch one row"
9672+
assert cursor.rowcount == 1, "rowcount should be 1 after fetchone"
9673+
9674+
# After another fetchone, rowcount should be 2
9675+
row = cursor.fetchone()
9676+
assert row is not None, "Should fetch second row"
9677+
assert cursor.rowcount == 2, "rowcount should be 2 after second fetchone"
9678+
9679+
# Test fetchmany
9680+
cursor.execute("SELECT * FROM #rowcount_fetch_test ORDER BY id")
9681+
assert cursor.rowcount == -1, "rowcount should be -1 right after SELECT statement"
9682+
9683+
# After fetchmany(2), rowcount should be 2
9684+
rows = cursor.fetchmany(2)
9685+
assert len(rows) == 2, "Should fetch two rows"
9686+
assert cursor.rowcount == 2, "rowcount should be 2 after fetchmany(2)"
9687+
9688+
# After another fetchmany(2), rowcount should be 4
9689+
rows = cursor.fetchmany(2)
9690+
assert len(rows) == 2, "Should fetch two more rows"
9691+
assert cursor.rowcount == 4, "rowcount should be 4 after second fetchmany(2)"
9692+
9693+
# Test fetchall
9694+
cursor.execute("SELECT * FROM #rowcount_fetch_test ORDER BY id")
9695+
assert cursor.rowcount == -1, "rowcount should be -1 right after SELECT statement"
9696+
9697+
# After fetchall, rowcount should be the total number of rows fetched (5)
9698+
rows = cursor.fetchall()
9699+
assert len(rows) == 5, "Should fetch all rows"
9700+
assert cursor.rowcount == 5, "rowcount should be 5 after fetchall"
9701+
9702+
# Test mixed fetch operations
9703+
cursor.execute("SELECT * FROM #rowcount_fetch_test ORDER BY id")
9704+
9705+
# Fetch one row
9706+
row = cursor.fetchone()
9707+
assert row is not None, "Should fetch one row"
9708+
assert cursor.rowcount == 1, "rowcount should be 1 after fetchone"
9709+
9710+
# Fetch two more rows with fetchmany
9711+
rows = cursor.fetchmany(2)
9712+
assert len(rows) == 2, "Should fetch two more rows"
9713+
assert cursor.rowcount == 3, "rowcount should be 3 after fetchone + fetchmany(2)"
9714+
9715+
# Fetch remaining rows with fetchall
9716+
rows = cursor.fetchall()
9717+
assert len(rows) == 2, "Should fetch remaining two rows"
9718+
assert cursor.rowcount == 5, "rowcount should be 5 after fetchone + fetchmany(2) + fetchall"
9719+
9720+
# Test fetchall on an empty result
9721+
cursor.execute("SELECT * FROM #rowcount_fetch_test WHERE id > 100")
9722+
rows = cursor.fetchall()
9723+
assert len(rows) == 0, "Should fetch zero rows"
9724+
assert cursor.rowcount == 0, "rowcount should be 0 after fetchall on empty result"
9725+
9726+
finally:
9727+
# Clean up
9728+
try:
9729+
cursor.execute("DROP TABLE #rowcount_fetch_test")
9730+
db_connection.commit()
9731+
except:
9732+
pass
9733+
9734+
def test_rowcount_guid_table(cursor, db_connection):
9735+
"""Test rowcount with GUID/uniqueidentifier columns to match the GitHub issue scenario."""
9736+
try:
9737+
# Create a test table similar to the one in the GitHub issue
9738+
cursor.execute("CREATE TABLE #test_log (id uniqueidentifier PRIMARY KEY DEFAULT NEWID(), message VARCHAR(100))")
9739+
9740+
# Insert test data
9741+
cursor.execute("INSERT INTO #test_log (message) VALUES ('Log 1')")
9742+
cursor.execute("INSERT INTO #test_log (message) VALUES ('Log 2')")
9743+
cursor.execute("INSERT INTO #test_log (message) VALUES ('Log 3')")
9744+
db_connection.commit()
9745+
9746+
# Execute SELECT query
9747+
cursor.execute("SELECT * FROM #test_log")
9748+
assert cursor.rowcount == -1, "Rowcount should be -1 after a SELECT statement (before fetch)"
9749+
9750+
# Test fetchall
9751+
rows = cursor.fetchall()
9752+
assert len(rows) == 3, "Should fetch 3 rows"
9753+
assert cursor.rowcount == 3, "Rowcount should be 3 after fetchall"
9754+
9755+
# Execute SELECT again
9756+
cursor.execute("SELECT * FROM #test_log")
9757+
9758+
# Test fetchmany
9759+
rows = cursor.fetchmany(2)
9760+
assert len(rows) == 2, "Should fetch 2 rows"
9761+
assert cursor.rowcount == 2, "Rowcount should be 2 after fetchmany(2)"
9762+
9763+
# Fetch remaining row
9764+
rows = cursor.fetchall()
9765+
assert len(rows) == 1, "Should fetch 1 remaining row"
9766+
assert cursor.rowcount == 3, "Rowcount should be 3 after fetchmany(2) + fetchall"
9767+
9768+
# Execute SELECT again
9769+
cursor.execute("SELECT * FROM #test_log")
9770+
9771+
# Test individual fetchone calls
9772+
row1 = cursor.fetchone()
9773+
assert row1 is not None, "First row should not be None"
9774+
assert cursor.rowcount == 1, "Rowcount should be 1 after first fetchone"
9775+
9776+
row2 = cursor.fetchone()
9777+
assert row2 is not None, "Second row should not be None"
9778+
assert cursor.rowcount == 2, "Rowcount should be 2 after second fetchone"
9779+
9780+
row3 = cursor.fetchone()
9781+
assert row3 is not None, "Third row should not be None"
9782+
assert cursor.rowcount == 3, "Rowcount should be 3 after third fetchone"
9783+
9784+
row4 = cursor.fetchone()
9785+
assert row4 is None, "Fourth row should be None (no more rows)"
9786+
assert cursor.rowcount == 3, "Rowcount should remain 3 when fetchone returns None"
9787+
9788+
finally:
9789+
# Clean up
9790+
try:
9791+
cursor.execute("DROP TABLE #test_log")
9792+
db_connection.commit()
9793+
except:
9794+
pass
9795+
9796+
def test_rowcount(cursor, db_connection):
9797+
"""Test rowcount after various operations"""
9798+
try:
9799+
cursor.execute("CREATE TABLE #pytest_test_rowcount (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(100))")
9800+
db_connection.commit()
9801+
9802+
cursor.execute("INSERT INTO #pytest_test_rowcount (name) VALUES ('JohnDoe1');")
9803+
assert cursor.rowcount == 1, "Rowcount should be 1 after first insert"
9804+
9805+
cursor.execute("INSERT INTO #pytest_test_rowcount (name) VALUES ('JohnDoe2');")
9806+
assert cursor.rowcount == 1, "Rowcount should be 1 after second insert"
9807+
9808+
cursor.execute("INSERT INTO #pytest_test_rowcount (name) VALUES ('JohnDoe3');")
9809+
assert cursor.rowcount == 1, "Rowcount should be 1 after third insert"
9810+
9811+
cursor.execute("""
9812+
INSERT INTO #pytest_test_rowcount (name)
9813+
VALUES
9814+
('JohnDoe4'),
9815+
('JohnDoe5'),
9816+
('JohnDoe6');
9817+
""")
9818+
assert cursor.rowcount == 3, "Rowcount should be 3 after inserting multiple rows"
9819+
9820+
cursor.execute("SELECT * FROM #pytest_test_rowcount;")
9821+
assert cursor.rowcount == -1, "Rowcount should be -1 after a SELECT statement (before fetch)"
9822+
9823+
# After fetchall, rowcount should be updated to match the number of rows fetched
9824+
rows = cursor.fetchall()
9825+
assert len(rows) == 6, "Should have fetched 6 rows"
9826+
assert cursor.rowcount == 6, "Rowcount should be updated to 6 after fetchall"
9827+
9828+
db_connection.commit()
9829+
except Exception as e:
9830+
pytest.fail(f"Rowcount test failed: {e}")
9831+
finally:
9832+
cursor.execute("DROP TABLE #pytest_test_rowcount")
9833+
96509834
def test_specialcolumns_setup(cursor, db_connection):
96519835
"""Create test tables for testing rowIdColumns and rowVerColumns"""
96529836
try:

0 commit comments

Comments
 (0)