From 0f672b998740a9791e5dda9074ed9f8b08bcbb41 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 12 Sep 2025 11:06:43 +0530 Subject: [PATCH 1/9] working uuid --- mssql_python/cursor.py | 11 ++++++ mssql_python/pybind/ddbc_bindings.cpp | 2 +- tests/test_004_cursor.py | 57 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 6365d559..190a5bbd 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -465,6 +465,15 @@ def _map_sql_type(self, param, parameters_list, i, min_val=None, max_val=None): 0, False ) + + if isinstance(param, uuid.UUID): + return ( + ddbc_sql_const.SQL_GUID.value, + ddbc_sql_const.SQL_C_GUID.value, + 16, + 0, + False, + ) if isinstance(param, datetime.datetime): if param.tzinfo is not None: @@ -984,6 +993,8 @@ def execute( if parameters: for i, param in enumerate(parameters): + if isinstance(param, uuid.UUID): + parameters[i] = param.bytes paraminfo = self._create_parameter_types_list( param, param_info, parameters, i ) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index e5c979b7..fb676a6d 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -2713,7 +2713,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } -#endif + #endif default: std::ostringstream errorString; errorString << "Unsupported data type for column - " << columnName << ", Type - " diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 0b53d449..dc0aee7f 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -7146,6 +7146,62 @@ def test_extreme_uuids(cursor, db_connection): cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() +def test_executemany_uuid_insert_and_select(cursor, db_connection): + """Test inserting multiple UUIDs using executemany and verifying retrieval.""" + table_name = "#pytest_uuid_executemany" + + try: + # Drop and create a temporary table for the test + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER PRIMARY KEY, + description NVARCHAR(50) + ) + """) + db_connection.commit() + + # Generate data for insertion + data_to_insert = [] + uuids_to_check = {} + for i in range(5): + new_uuid = uuid.uuid4() + description = f"Item {i}" + data_to_insert.append((new_uuid, description)) + uuids_to_check[description] = new_uuid + + # Insert all data with a single call to executemany + sql = f"INSERT INTO {table_name} (id, description) VALUES (?, ?)" + cursor.executemany(sql, data_to_insert) + db_connection.commit() + + # Verify the number of rows inserted + assert cursor.rowcount == 5, f"Expected 5 rows inserted, but got {cursor.rowcount}" + + # Fetch all data from the table + cursor.execute(f"SELECT id, description FROM {table_name}") + rows = cursor.fetchall() + + # Verify the number of fetched rows + assert len(rows) == len(data_to_insert), "Number of fetched rows does not match." + + # Verify each fetched row's data and type + for row in rows: + retrieved_uuid, retrieved_desc = row + + # Assert the type is correct + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" + + # Assert the value matches the original data + expected_uuid = uuids_to_check.get(retrieved_desc) + assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data." + assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}" + + finally: + # Clean up the temporary table + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + def test_decimal_separator_with_multiple_values(cursor, db_connection): """Test decimal separator with multiple different decimal values""" original_separator = mssql_python.getDecimalSeparator() @@ -10560,6 +10616,7 @@ def test_decimal_separator_calculations(cursor, db_connection): # Cleanup cursor.execute("DROP TABLE IF EXISTS #pytest_decimal_calc_test") + db_connection.commit() def test_close(db_connection): """Test closing the cursor""" From e6a9bbc011d3fd72d82d04c4f792229f97348c26 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 15 Sep 2025 15:39:11 +0530 Subject: [PATCH 2/9] resolving copilot comments --- mssql_python/pybind/ddbc_bindings.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index fb676a6d..317e9671 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -3096,18 +3096,14 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum } case SQL_GUID: { SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i]; - uint8_t reordered[16]; - reordered[0] = ((char*)&guidValue->Data1)[3]; - reordered[1] = ((char*)&guidValue->Data1)[2]; - reordered[2] = ((char*)&guidValue->Data1)[1]; - reordered[3] = ((char*)&guidValue->Data1)[0]; - reordered[4] = ((char*)&guidValue->Data2)[1]; - reordered[5] = ((char*)&guidValue->Data2)[0]; - reordered[6] = ((char*)&guidValue->Data3)[1]; - reordered[7] = ((char*)&guidValue->Data3)[0]; - std::memcpy(reordered + 8, guidValue->Data4, 8); - - py::bytes py_guid_bytes(reinterpret_cast(reordered), 16); + // We already have the raw bytes from SQL Server in the SQLGUID struct. + // We do not need to perform any additional reordering here, as the C++ + // SQLGUID struct is already laid out in the non-standard SQL Server byte order. + std::vector guid_bytes(16); + std::memcpy(guid_bytes.data(), guidValue, sizeof(SQLGUID)); + + // Convert the raw C++ byte vector to a Python bytes object + py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size()); py::dict kwargs; kwargs["bytes"] = py_guid_bytes; py::object uuid_obj = py::module_::import("uuid").attr("UUID")(**kwargs); From 5077ec570d0ed8cbbadc020ba588b391489d61ba Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 13:05:44 +0530 Subject: [PATCH 3/9] working executemany-uuid --- mssql_python/pybind/ddbc_bindings.cpp | 76 +++++++++++++++++++++++++++ tests/test_004_cursor.py | 46 ++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 317e9671..47bc0aa1 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -1963,6 +1963,82 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, bufferLength = sizeof(SQL_NUMERIC_STRUCT); break; } +// case SQL_C_GUID: { +// SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); +// strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + +// for (size_t i = 0; i < paramSetSize; ++i) { +// if (columnValues[i].is_none()) { +// std::memset(&guidArray[i], 0, sizeof(SQLGUID)); +// strLenOrIndArray[i] = SQL_NULL_DATA; +// } else { +// if (!py::isinstance(columnValues[i])) { +// ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); +// } +// py::bytes uuid_bytes = columnValues[i].cast(); +// const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); +// if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { +// ThrowStdException("UUID binary data must be exactly 16 bytes long."); +// } + +// // Map bytes to SQLGUID fields +// guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | +// (static_cast(uuid_data[2]) << 16) | +// (static_cast(uuid_data[1]) << 8) | +// (static_cast(uuid_data[0])); +// guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | +// (static_cast(uuid_data[4])); +// guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | +// (static_cast(uuid_data[6])); +// std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); + +// strLenOrIndArray[i] = sizeof(SQLGUID); +// } +// } + +// dataPtr = guidArray; +// bufferLength = sizeof(SQLGUID); +// break; +// } +case SQL_C_GUID: { + SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + + py::object uuid_type = py::module_::import("uuid").attr("UUID"); + + for (size_t i = 0; i < paramSetSize; ++i) { + if (columnValues[i].is_none()) { + std::memset(&guidArray[i], 0, sizeof(SQLGUID)); + strLenOrIndArray[i] = SQL_NULL_DATA; + } else if (py::isinstance(columnValues[i], uuid_type)) { + py::bytes uuid_bytes = columnValues[i].attr("bytes"); + const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); + + if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { + ThrowStdException("UUID binary data must be exactly 16 bytes long."); + } + + guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | + (static_cast(uuid_data[2]) << 16) | + (static_cast(uuid_data[1]) << 8) | + (static_cast(uuid_data[0])); + guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | + (static_cast(uuid_data[4])); + guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | + (static_cast(uuid_data[6])); + std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); + + strLenOrIndArray[i] = sizeof(SQLGUID); + } else { + ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); + } + } + + dataPtr = guidArray; + bufferLength = sizeof(SQLGUID); + break; +} + default: { ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType)); } diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index dc0aee7f..c833921b 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -10618,6 +10618,52 @@ def test_decimal_separator_calculations(cursor, db_connection): cursor.execute("DROP TABLE IF EXISTS #pytest_decimal_calc_test") db_connection.commit() +def test_executemany_with_uuids(cursor, db_connection): + """Test inserting multiple rows with UUIDs and None using executemany.""" + table_name = "#pytest_uuid_batch" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + description NVARCHAR(50) + ) + """) + db_connection.commit() + + # Prepare test data: mix of UUIDs and None + test_data = [ + [uuid.uuid4(), "Item 1"], + [uuid.uuid4(), "Item 2"], + [None, "Item 3"], + [uuid.uuid4(), "Item 4"], + [None, "Item 5"] + ] + + # Execute batch insert + cursor.executemany(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", test_data) + cursor.connection.commit() + + # Fetch and verify + cursor.execute(f"SELECT id, description FROM {table_name}") + rows = cursor.fetchall() + + assert len(rows) == len(test_data), "Number of fetched rows does not match inserted rows." + + for row in rows: + retrieved_uuid, retrieved_desc = row + for original_uuid, original_desc in test_data: + if original_desc == retrieved_desc: + if original_uuid is None: + assert retrieved_uuid is None, f"Expected None for '{retrieved_desc}', got {retrieved_uuid}" + else: + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected UUID, got {type(retrieved_uuid)}" + assert retrieved_uuid == original_uuid, f"UUID mismatch for '{retrieved_desc}'" + break + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + def test_close(db_connection): """Test closing the cursor""" try: From 21739f0391fa43da6b239dc90b95ed77fbe420fb Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 13:32:49 +0530 Subject: [PATCH 4/9] cleanup --- mssql_python/pybind/ddbc_bindings.cpp | 108 +++++++++----------------- 1 file changed, 35 insertions(+), 73 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 47bc0aa1..1b6e29eb 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -1963,82 +1963,44 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, bufferLength = sizeof(SQL_NUMERIC_STRUCT); break; } -// case SQL_C_GUID: { -// SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); -// strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - -// for (size_t i = 0; i < paramSetSize; ++i) { -// if (columnValues[i].is_none()) { -// std::memset(&guidArray[i], 0, sizeof(SQLGUID)); -// strLenOrIndArray[i] = SQL_NULL_DATA; -// } else { -// if (!py::isinstance(columnValues[i])) { -// ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); -// } -// py::bytes uuid_bytes = columnValues[i].cast(); -// const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); -// if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { -// ThrowStdException("UUID binary data must be exactly 16 bytes long."); -// } - -// // Map bytes to SQLGUID fields -// guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | -// (static_cast(uuid_data[2]) << 16) | -// (static_cast(uuid_data[1]) << 8) | -// (static_cast(uuid_data[0])); -// guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | -// (static_cast(uuid_data[4])); -// guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | -// (static_cast(uuid_data[6])); -// std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); - -// strLenOrIndArray[i] = sizeof(SQLGUID); -// } -// } - -// dataPtr = guidArray; -// bufferLength = sizeof(SQLGUID); -// break; -// } -case SQL_C_GUID: { - SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - - py::object uuid_type = py::module_::import("uuid").attr("UUID"); - - for (size_t i = 0; i < paramSetSize; ++i) { - if (columnValues[i].is_none()) { - std::memset(&guidArray[i], 0, sizeof(SQLGUID)); - strLenOrIndArray[i] = SQL_NULL_DATA; - } else if (py::isinstance(columnValues[i], uuid_type)) { - py::bytes uuid_bytes = columnValues[i].attr("bytes"); - const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); - - if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { - ThrowStdException("UUID binary data must be exactly 16 bytes long."); - } + case SQL_C_GUID: { + SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); + strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | - (static_cast(uuid_data[2]) << 16) | - (static_cast(uuid_data[1]) << 8) | - (static_cast(uuid_data[0])); - guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | - (static_cast(uuid_data[4])); - guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | - (static_cast(uuid_data[6])); - std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); - - strLenOrIndArray[i] = sizeof(SQLGUID); - } else { - ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); - } - } + py::object uuid_type = py::module_::import("uuid").attr("UUID"); - dataPtr = guidArray; - bufferLength = sizeof(SQLGUID); - break; -} + for (size_t i = 0; i < paramSetSize; ++i) { + if (columnValues[i].is_none()) { + std::memset(&guidArray[i], 0, sizeof(SQLGUID)); + strLenOrIndArray[i] = SQL_NULL_DATA; + } else if (py::isinstance(columnValues[i], uuid_type)) { + py::bytes uuid_bytes = columnValues[i].attr("bytes"); + const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); + if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { + ThrowStdException("UUID binary data must be exactly 16 bytes long."); + } + + guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | + (static_cast(uuid_data[2]) << 16) | + (static_cast(uuid_data[1]) << 8) | + (static_cast(uuid_data[0])); + guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | + (static_cast(uuid_data[4])); + guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | + (static_cast(uuid_data[6])); + std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); + + strLenOrIndArray[i] = sizeof(SQLGUID); + } else { + ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); + } + } + + dataPtr = guidArray; + bufferLength = sizeof(SQLGUID); + break; + } default: { ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType)); } From 74c6296c25938022f139bfe10164d156783d826a Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 13:50:47 +0530 Subject: [PATCH 5/9] unnecessary --- mssql_python/cursor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 190a5bbd..76cc2002 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -993,8 +993,6 @@ def execute( if parameters: for i, param in enumerate(parameters): - if isinstance(param, uuid.UUID): - parameters[i] = param.bytes paraminfo = self._create_parameter_types_list( param, param_info, parameters, i ) From 763f32fd919553e8dbd9ffa8314944961bae98f2 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 24 Sep 2025 17:05:48 +0530 Subject: [PATCH 6/9] pipeline fix --- mssql_python/cursor.py | 10 ---- mssql_python/pybind/ddbc_bindings.cpp | 74 ++++++++++++++++----------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 76cc2002..5c578073 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -465,15 +465,6 @@ def _map_sql_type(self, param, parameters_list, i, min_val=None, max_val=None): 0, False ) - - if isinstance(param, uuid.UUID): - return ( - ddbc_sql_const.SQL_GUID.value, - ddbc_sql_const.SQL_C_GUID.value, - 16, - 0, - False, - ) if isinstance(param, datetime.datetime): if param.tzinfo is not None: @@ -1689,7 +1680,6 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: sample_value = sample_row[col_index] else: sample_value = self._select_best_sample_value(column) - dummy_row = list(sample_row) paraminfo = self._create_parameter_types_list( sample_value, param_info, dummy_row, col_index, min_val=min_val, max_val=max_val diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 1b6e29eb..077a6113 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -1967,36 +1967,41 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - py::object uuid_type = py::module_::import("uuid").attr("UUID"); - + py::module_ uuid_mod = py::module_::import("uuid"); + py::object uuid_class = uuid_mod.attr("UUID"); for (size_t i = 0; i < paramSetSize; ++i) { - if (columnValues[i].is_none()) { + const py::handle& element = columnValues[i]; + std::array uuid_bytes; + if (element.is_none()) { std::memset(&guidArray[i], 0, sizeof(SQLGUID)); strLenOrIndArray[i] = SQL_NULL_DATA; - } else if (py::isinstance(columnValues[i], uuid_type)) { - py::bytes uuid_bytes = columnValues[i].attr("bytes"); - const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); - - if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { + continue; + } + else if (py::isinstance(element)) { + py::bytes b = element.cast(); + if (PyBytes_GET_SIZE(b.ptr()) != 16) { ThrowStdException("UUID binary data must be exactly 16 bytes long."); } - - guidArray[i].Data1 = (static_cast(uuid_data[3]) << 24) | - (static_cast(uuid_data[2]) << 16) | - (static_cast(uuid_data[1]) << 8) | - (static_cast(uuid_data[0])); - guidArray[i].Data2 = (static_cast(uuid_data[5]) << 8) | - (static_cast(uuid_data[4])); - guidArray[i].Data3 = (static_cast(uuid_data[7]) << 8) | - (static_cast(uuid_data[6])); - std::memcpy(guidArray[i].Data4, &uuid_data[8], 8); - - strLenOrIndArray[i] = sizeof(SQLGUID); - } else { + std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16); + } + else if (py::isinstance(element, uuid_class)) { + py::bytes b = element.attr("bytes_le").cast(); + std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16); + } + else { ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex)); } + guidArray[i].Data1 = (static_cast(uuid_bytes[3]) << 24) | + (static_cast(uuid_bytes[2]) << 16) | + (static_cast(uuid_bytes[1]) << 8) | + (static_cast(uuid_bytes[0])); + guidArray[i].Data2 = (static_cast(uuid_bytes[5]) << 8) | + (static_cast(uuid_bytes[4])); + guidArray[i].Data3 = (static_cast(uuid_bytes[7]) << 8) | + (static_cast(uuid_bytes[6])); + std::memcpy(guidArray[i].Data4, uuid_bytes.data() + 8, 8); + strLenOrIndArray[i] = sizeof(SQLGUID); } - dataPtr = guidArray; bufferLength = sizeof(SQLGUID); break; @@ -3133,15 +3138,24 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum break; } case SQL_GUID: { + SQLLEN indicator = buffers.indicators[col - 1][i]; + if (indicator == SQL_NULL_DATA) { + row.append(py::none()); + break; + } SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i]; - // We already have the raw bytes from SQL Server in the SQLGUID struct. - // We do not need to perform any additional reordering here, as the C++ - // SQLGUID struct is already laid out in the non-standard SQL Server byte order. - std::vector guid_bytes(16); - std::memcpy(guid_bytes.data(), guidValue, sizeof(SQLGUID)); - - // Convert the raw C++ byte vector to a Python bytes object - py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size()); + uint8_t reordered[16]; + reordered[0] = ((char*)&guidValue->Data1)[3]; + reordered[1] = ((char*)&guidValue->Data1)[2]; + reordered[2] = ((char*)&guidValue->Data1)[1]; + reordered[3] = ((char*)&guidValue->Data1)[0]; + reordered[4] = ((char*)&guidValue->Data2)[1]; + reordered[5] = ((char*)&guidValue->Data2)[0]; + reordered[6] = ((char*)&guidValue->Data3)[1]; + reordered[7] = ((char*)&guidValue->Data3)[0]; + std::memcpy(reordered + 8, guidValue->Data4, 8); + + py::bytes py_guid_bytes(reinterpret_cast(reordered), 16); py::dict kwargs; kwargs["bytes"] = py_guid_bytes; py::object uuid_obj = py::module_::import("uuid").attr("UUID")(**kwargs); From e7177327f2a93e32f07010333f99187a033d8678 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 24 Sep 2025 17:07:35 +0530 Subject: [PATCH 7/9] cleanup --- mssql_python/cursor.py | 1 + mssql_python/pybind/ddbc_bindings.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 5c578073..eb8c893b 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1680,6 +1680,7 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: sample_value = sample_row[col_index] else: sample_value = self._select_best_sample_value(column) + dummy_row = list(sample_row) paraminfo = self._create_parameter_types_list( sample_value, param_info, dummy_row, col_index, min_val=min_val, max_val=max_val diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 077a6113..4f64c0ad 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -2756,7 +2756,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } - #endif +#endif default: std::ostringstream errorString; errorString << "Unsupported data type for column - " << columnName << ", Type - " From 9b64b9f893500ef27cc82ea66b090b3c0bd38e93 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Wed, 24 Sep 2025 17:15:29 +0530 Subject: [PATCH 8/9] resolved copilot comments --- mssql_python/cursor.py | 2 +- mssql_python/pybind/ddbc_bindings.cpp | 4 +- tests/test_004_cursor.py | 54 +++++++++++++-------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index eb8c893b..6365d559 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1680,7 +1680,7 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None: sample_value = sample_row[col_index] else: sample_value = self._select_best_sample_value(column) - + dummy_row = list(sample_row) paraminfo = self._create_parameter_types_list( sample_value, param_info, dummy_row, col_index, min_val=min_val, max_val=max_val diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 4f64c0ad..6bd4b4b8 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -1967,8 +1967,8 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, SQLGUID* guidArray = AllocateParamBufferArray(tempBuffers, paramSetSize); strLenOrIndArray = AllocateParamBufferArray(tempBuffers, paramSetSize); - py::module_ uuid_mod = py::module_::import("uuid"); - py::object uuid_class = uuid_mod.attr("UUID"); + static py::module_ uuid_mod = py::module_::import("uuid"); + static py::object uuid_class = uuid_mod.attr("UUID"); for (size_t i = 0; i < paramSetSize; ++i) { const py::handle& element = columnValues[i]; std::array uuid_bytes; diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index c833921b..e0915344 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -7162,13 +7162,7 @@ def test_executemany_uuid_insert_and_select(cursor, db_connection): db_connection.commit() # Generate data for insertion - data_to_insert = [] - uuids_to_check = {} - for i in range(5): - new_uuid = uuid.uuid4() - description = f"Item {i}" - data_to_insert.append((new_uuid, description)) - uuids_to_check[description] = new_uuid + data_to_insert = [(uuid.uuid4(), f"Item {i}") for i in range(5)] # Insert all data with a single call to executemany sql = f"INSERT INTO {table_name} (id, description) VALUES (?, ?)" @@ -7179,23 +7173,23 @@ def test_executemany_uuid_insert_and_select(cursor, db_connection): assert cursor.rowcount == 5, f"Expected 5 rows inserted, but got {cursor.rowcount}" # Fetch all data from the table - cursor.execute(f"SELECT id, description FROM {table_name}") + cursor.execute(f"SELECT id, description FROM {table_name} ORDER BY description") rows = cursor.fetchall() # Verify the number of fetched rows assert len(rows) == len(data_to_insert), "Number of fetched rows does not match." - - # Verify each fetched row's data and type - for row in rows: - retrieved_uuid, retrieved_desc = row - + + # Compare inserted and retrieved rows by index + for i, (retrieved_uuid, retrieved_desc) in enumerate(rows): + expected_uuid, expected_desc = data_to_insert[i] + # Assert the type is correct + if isinstance(retrieved_uuid, str): + retrieved_uuid = uuid.UUID(retrieved_uuid) # convert if driver returns str + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" - - # Assert the value matches the original data - expected_uuid = uuids_to_check.get(retrieved_desc) - assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data." assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}" + assert retrieved_desc == expected_desc, f"Description mismatch: expected {expected_desc}, got {retrieved_desc}" finally: # Clean up the temporary table @@ -10640,6 +10634,9 @@ def test_executemany_with_uuids(cursor, db_connection): [None, "Item 5"] ] + # Map descriptions to original UUIDs for O(1) lookup + uuid_map = {desc: uid for uid, desc in test_data} + # Execute batch insert cursor.executemany(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", test_data) cursor.connection.commit() @@ -10650,16 +10647,19 @@ def test_executemany_with_uuids(cursor, db_connection): assert len(rows) == len(test_data), "Number of fetched rows does not match inserted rows." - for row in rows: - retrieved_uuid, retrieved_desc = row - for original_uuid, original_desc in test_data: - if original_desc == retrieved_desc: - if original_uuid is None: - assert retrieved_uuid is None, f"Expected None for '{retrieved_desc}', got {retrieved_uuid}" - else: - assert isinstance(retrieved_uuid, uuid.UUID), f"Expected UUID, got {type(retrieved_uuid)}" - assert retrieved_uuid == original_uuid, f"UUID mismatch for '{retrieved_desc}'" - break + for retrieved_uuid, retrieved_desc in rows: + expected_uuid = uuid_map[retrieved_desc] + + if expected_uuid is None: + assert retrieved_uuid is None, f"Expected None for '{retrieved_desc}', got {retrieved_uuid}" + else: + # Convert string to UUID if needed + if isinstance(retrieved_uuid, str): + retrieved_uuid = uuid.UUID(retrieved_uuid) + + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected UUID, got {type(retrieved_uuid)}" + assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}'" + finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() From eebd6de0eddf383f37629b0e9e817d1c84cc9ee1 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 29 Sep 2025 18:58:44 +0530 Subject: [PATCH 9/9] addressed review comments --- tests/test_004_cursor.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index e0915344..b2e7ee66 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -7196,6 +7196,47 @@ def test_executemany_uuid_insert_and_select(cursor, db_connection): cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() +def test_executemany_uuid_roundtrip_fixed_value(cursor, db_connection): + """Ensure a fixed canonical UUID round-trips exactly.""" + table_name = "#pytest_uuid_fixed" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + description NVARCHAR(50) + ) + """) + db_connection.commit() + + fixed_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") + description = "FixedUUID" + + # Insert via executemany + cursor.executemany( + f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", + [(fixed_uuid, description)] + ) + db_connection.commit() + + # Fetch back + cursor.execute(f"SELECT id, description FROM {table_name} WHERE description = ?", description) + row = cursor.fetchone() + retrieved_uuid, retrieved_desc = row + + # Ensure type and value are correct + if isinstance(retrieved_uuid, str): + retrieved_uuid = uuid.UUID(retrieved_uuid) + + assert isinstance(retrieved_uuid, uuid.UUID) + assert retrieved_uuid == fixed_uuid + assert str(retrieved_uuid) == str(fixed_uuid) + assert retrieved_desc == description + + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + def test_decimal_separator_with_multiple_values(cursor, db_connection): """Test decimal separator with multiple different decimal values""" original_separator = mssql_python.getDecimalSeparator()