Skip to content

Commit c403899

Browse files
Fix for FetchMany(number of rows) ignores batch size when table contains an LOB
1 parent 4d2634a commit c403899

File tree

2 files changed

+43
-16
lines changed

2 files changed

+43
-16
lines changed

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3870,22 +3870,23 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
38703870
}
38713871
}
38723872

3873+
SQLULEN numRowsFetched = 0;
38733874
// If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap
38743875
if (!lobColumns.empty()) {
38753876
LOG("FetchMany_wrap: LOB columns detected (%zu columns), using per-row "
38763877
"SQLGetData path",
38773878
lobColumns.size());
3878-
while (true) {
3879+
while (numRowsFetched < (SQLULEN)fetchSize) {
38793880
ret = SQLFetch_ptr(hStmt);
38803881
if (ret == SQL_NO_DATA)
38813882
break;
38823883
if (!SQL_SUCCEEDED(ret))
38833884
return ret;
38843885

38853886
py::list row;
3886-
SQLGetData_wrap(StatementHandle, numCols,
3887-
row); // <-- streams LOBs correctly
3887+
SQLGetData_wrap(StatementHandle, numCols, row); // <-- streams LOBs correctly
38883888
rows.append(row);
3889+
numRowsFetched++;
38893890
}
38903891
return SQL_SUCCESS;
38913892
}
@@ -3900,7 +3901,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
39003901
return ret;
39013902
}
39023903

3903-
SQLULEN numRowsFetched;
3904+
39043905
SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(intptr_t)fetchSize, 0);
39053906
SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0);
39063907

tests/test_004_cursor.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
integer_column INTEGER,
3131
float_column FLOAT,
3232
wvarchar_column NVARCHAR(255),
33+
lob_wvarchar_column NVARCHAR(MAX),
3334
time_column TIME,
3435
datetime_column DATETIME,
3536
date_column DATE,
@@ -47,6 +48,7 @@
4748
2147483647,
4849
1.23456789,
4950
"nvarchar data",
51+
b"nvarchar data",
5052
time(12, 34, 56),
5153
datetime(2024, 5, 20, 12, 34, 56, 123000),
5254
date(2024, 5, 20),
@@ -65,6 +67,7 @@
6567
0,
6668
0.0,
6769
"test1",
70+
b"nvarchar data",
6871
time(0, 0, 0),
6972
datetime(2024, 1, 1, 0, 0, 0),
7073
date(2024, 1, 1),
@@ -79,6 +82,7 @@
7982
1,
8083
1.1,
8184
"test2",
85+
b"test2",
8286
time(1, 1, 1),
8387
datetime(2024, 2, 2, 1, 1, 1),
8488
date(2024, 2, 2),
@@ -93,6 +97,7 @@
9397
2147483647,
9498
1.23456789,
9599
"test3",
100+
b"test3",
96101
time(12, 34, 56),
97102
datetime(2024, 5, 20, 12, 34, 56, 123000),
98103
date(2024, 5, 20),
@@ -821,7 +826,7 @@ def test_insert_args(cursor, db_connection):
821826
cursor.execute(
822827
"""
823828
INSERT INTO #pytest_all_data_types VALUES (
824-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
829+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
825830
)
826831
""",
827832
TEST_DATA[0],
@@ -836,6 +841,7 @@ def test_insert_args(cursor, db_connection):
836841
TEST_DATA[9],
837842
TEST_DATA[10],
838843
TEST_DATA[11],
844+
TEST_DATA[12],
839845
)
840846
db_connection.commit()
841847
cursor.execute("SELECT * FROM #pytest_all_data_types WHERE id = 1")
@@ -855,7 +861,7 @@ def test_parametrized_insert(cursor, db_connection, data):
855861
cursor.execute(
856862
"""
857863
INSERT INTO #pytest_all_data_types VALUES (
858-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
864+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
859865
)
860866
""",
861867
[None if v is None else v for v in data],
@@ -930,29 +936,49 @@ def test_rowcount_executemany(cursor, db_connection):
930936

931937
def test_fetchone(cursor):
932938
"""Test fetching a single row"""
933-
cursor.execute("SELECT * FROM #pytest_all_data_types WHERE id = 1")
939+
cursor.execute("SELECT * FROM #pytest_all_data_types")
934940
row = cursor.fetchone()
935941
assert row is not None, "No row returned"
936-
assert len(row) == 12, "Incorrect number of columns"
942+
assert len(row) == 13, "Incorrect number of columns"
937943

938944

939945
def test_fetchmany(cursor):
940946
"""Test fetching multiple rows"""
941-
cursor.execute("SELECT * FROM #pytest_all_data_types")
947+
cursor.execute("SELECT id, bit_column, tinyint_column, smallint_column, bigint_column, integer_column, float_column, wvarchar_column, time_column, datetime_column, date_column, real_column FROM #pytest_all_data_types")
942948
rows = cursor.fetchmany(2)
943949
assert isinstance(rows, list), "fetchmany should return a list"
944950
assert len(rows) == 2, "Incorrect number of rows returned"
945951

952+
def test_fetchmany_lob(cursor):
953+
"""Test fetching multiple rows"""
954+
cursor.execute("SELECT * FROM #pytest_all_data_types")
955+
rows = cursor.fetchmany(2)
956+
assert isinstance(rows, list), "fetchmany should return a list"
957+
assert len(rows) == 2, "Incorrect number of rows returned"
946958

947959
def test_fetchmany_with_arraysize(cursor, db_connection):
948960
"""Test fetchmany with arraysize"""
949961
cursor.arraysize = 3
950-
cursor.execute("SELECT * FROM #pytest_all_data_types")
962+
cursor.execute("SELECT id, bit_column, tinyint_column, smallint_column, bigint_column, integer_column, float_column, wvarchar_column, time_column, datetime_column, date_column, real_column FROM #pytest_all_data_types")
951963
rows = cursor.fetchmany()
952964
assert len(rows) == 3, "fetchmany with arraysize returned incorrect number of rows"
953965

966+
def test_fetchmany_lob_with_arraysize(cursor, db_connection):
967+
"""Test fetchmany with arraysize"""
968+
cursor.arraysize = 3
969+
cursor.execute("SELECT * FROM #pytest_all_data_types")
970+
rows = cursor.fetchmany()
971+
assert len(rows) == 3, "fetchmany_lob with arraysize returned incorrect number of rows"
972+
954973

955974
def test_fetchall(cursor):
975+
"""Test fetching all rows"""
976+
cursor.execute("SELECT id, bit_column, tinyint_column, smallint_column, bigint_column, integer_column, float_column, wvarchar_column, time_column, datetime_column, date_column, real_column FROM #pytest_all_data_types")
977+
rows = cursor.fetchall()
978+
assert isinstance(rows, list), "fetchall should return a list"
979+
assert len(rows) == len(PARAM_TEST_DATA), "Incorrect number of rows returned"
980+
981+
def test_fetchall_lob(cursor):
956982
"""Test fetching all rows"""
957983
cursor.execute("SELECT * FROM #pytest_all_data_types")
958984
rows = cursor.fetchall()
@@ -980,11 +1006,11 @@ def test_execute_invalid_query(cursor):
9801006
# assert row[5] == TEST_DATA[5], "Integer mismatch"
9811007
# assert round(row[6], 5) == round(TEST_DATA[6], 5), "Float mismatch"
9821008
# assert row[7] == TEST_DATA[7], "Nvarchar mismatch"
983-
# assert row[8] == TEST_DATA[8], "Time mismatch"
984-
# assert row[9] == TEST_DATA[9], "Datetime mismatch"
985-
# assert row[10] == TEST_DATA[10], "Date mismatch"
986-
# assert round(row[11], 5) == round(TEST_DATA[11], 5), "Real mismatch"
987-
1009+
# assert row[8] == TEST_DATA[8], "Nvarchar max mismatch"
1010+
# assert row[9] == TEST_DATA[9], "Time mismatch"
1011+
# assert row[10] == TEST_DATA[10], "Datetime mismatch"
1012+
# assert row[11] == TEST_DATA[11], "Date mismatch"
1013+
# assert round(row[12], 5) == round(TEST_DATA[12], 5), "Real mismatch"
9881014

9891015
def test_arraysize(cursor):
9901016
"""Test arraysize"""
@@ -998,7 +1024,7 @@ def test_description(cursor):
9981024
"""Test description"""
9991025
cursor.execute("SELECT * FROM #pytest_all_data_types WHERE id = 1")
10001026
desc = cursor.description
1001-
assert len(desc) == 12, "Description length mismatch"
1027+
assert len(desc) == 13, "Description length mismatch"
10021028
assert desc[0][0] == "id", "Description column name mismatch"
10031029

10041030

0 commit comments

Comments
 (0)