From e54cc176fc2c1f3606f862755027cdba23dcae37 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 5 Aug 2026 13:10:52 +0530 Subject: [PATCH] FIX: Initialize numeric array indicators in executemany --- mssql_python/pybind/ddbc_bindings.cpp | 28 ++++++--------------- tests/test_004_cursor.py | 36 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index c0952f4d..b2aadbb6 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -2235,17 +2235,14 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& "param_index=%d, count=%zu", paramIndex, paramSetSize); int* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { - if (!strLenOrIndArray) - strLenOrIndArray = - AllocateParamBufferArray(tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { dataArray[i] = columnValues[i].cast(); - if (strLenOrIndArray) - strLenOrIndArray[i] = 0; + strLenOrIndArray[i] = 0; } } LOG("BindParameterArray: SQL_C_LONG bound - param_index=%d", paramIndex); @@ -2257,17 +2254,14 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& "param_index=%d, count=%zu", paramIndex, paramSetSize); double* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { - if (!strLenOrIndArray) - strLenOrIndArray = - AllocateParamBufferArray(tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { dataArray[i] = columnValues[i].cast(); - if (strLenOrIndArray) - strLenOrIndArray[i] = 0; + strLenOrIndArray[i] = 0; } } LOG("BindParameterArray: SQL_C_DOUBLE bound - " @@ -2316,11 +2310,9 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& paramIndex, paramSetSize); unsigned char* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { - if (!strLenOrIndArray) - strLenOrIndArray = - AllocateParamBufferArray(tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { @@ -2333,8 +2325,7 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& std::to_string(i)); } dataArray[i] = static_cast(intVal); - if (strLenOrIndArray) - strLenOrIndArray[i] = 0; + strLenOrIndArray[i] = 0; } } LOG("BindParameterArray: SQL_C_TINYINT bound - " @@ -2349,11 +2340,9 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& "param_index=%d, count=%zu", paramIndex, paramSetSize); short* dataArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); for (size_t i = 0; i < paramSetSize; ++i) { if (columnValues[i].is_none()) { - if (!strLenOrIndArray) - strLenOrIndArray = - AllocateParamBufferArray(tempBuffers, paramSetSize); dataArray[i] = 0; strLenOrIndArray[i] = SQL_NULL_DATA; } else { @@ -2367,8 +2356,7 @@ SQLRETURN BindParameterArray(SqlHandle& handle, SQLHANDLE hStmt, const py::list& std::to_string(i)); } dataArray[i] = static_cast(intVal); - if (strLenOrIndArray) - strLenOrIndArray[i] = 0; + strLenOrIndArray[i] = 0; } } LOG("BindParameterArray: SQL_C_SHORT bound - " diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index f756e89b..6df79cb7 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2258,6 +2258,42 @@ def test_executemany_ints_with_none(cursor, db_connection): db_connection.commit() +def test_executemany_numeric_types_with_late_none(cursor, db_connection): + """Test fixed-width numeric array indicators when NULL follows non-NULL values.""" + try: + cursor.execute("""CREATE TABLE #pytest_numeric_late_none ( + id INT NOT NULL, + tinyint_val TINYINT NULL, + smallint_val SMALLINT NULL, + int_val INT NULL, + float_val FLOAT NULL + )""") + null_rows = {80, 83} + data = [ + ( + row_id, + None if row_id in null_rows else row_id, + None if row_id in null_rows else 1000 + row_id, + None if row_id in null_rows else 100000 + row_id, + None if row_id in null_rows else 100000.5 + row_id, + ) + for row_id in range(86) + ] + + for _ in range(10): + cursor.execute("TRUNCATE TABLE #pytest_numeric_late_none") + cursor.executemany("INSERT INTO #pytest_numeric_late_none VALUES (?, ?, ?, ?, ?)", data) + assert cursor.rowcount == len(data) + db_connection.commit() + + cursor.execute("""SELECT id, tinyint_val, smallint_val, int_val, float_val + FROM #pytest_numeric_late_none ORDER BY id""") + assert [tuple(row) for row in cursor.fetchall()] == list(data) + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_numeric_late_none") + db_connection.commit() + + def test_executemany_strings_of_various_lengths(cursor, db_connection): """Test executemany with strings of different lengths.""" try: