From fc863667664544cc01171a779997aa850f6ea906 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 12 Sep 2025 11:06:43 +0530 Subject: [PATCH 1/7] working uuid --- mssql_python/cursor.py | 11 ++ mssql_python/pybind/ddbc_bindings.cpp | 82 ++++++++++---- tests/test_004_cursor.py | 154 ++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 19 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 7a6dbfaa9..1e9c3b8be 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -425,6 +425,15 @@ def _map_sql_type(self, param, parameters_list, i): 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): return ( @@ -765,6 +774,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 789d3863a..33006889c 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -484,7 +484,34 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, break; } case SQL_C_GUID: { - // TODO + if (!py::isinstance(param)) { + ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); + } + py::bytes uuid_bytes = param.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."); + } + SQLGUID* guid_data_ptr = AllocateParamBuffer(paramBuffers); + + guid_data_ptr->Data1 = + (static_cast(uuid_data[3]) << 24) | + (static_cast(uuid_data[2]) << 16) | + (static_cast(uuid_data[1]) << 8) | + (static_cast(uuid_data[0])); + guid_data_ptr->Data2 = + (static_cast(uuid_data[5]) << 8) | + (static_cast(uuid_data[4])); + guid_data_ptr->Data3 = + (static_cast(uuid_data[7]) << 8) | + (static_cast(uuid_data[6])); + + std::memcpy(guid_data_ptr->Data4, &uuid_data[8], 8); + dataPtr = static_cast(guid_data_ptr); + bufferLength = sizeof(SQLGUID); + strLenOrIndPtr = AllocateParamBuffer(paramBuffers); + *strLenOrIndPtr = sizeof(SQLGUID); + break; } default: { std::ostringstream errorString; @@ -2199,20 +2226,27 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p #if (ODBCVER >= 0x0350) case SQL_GUID: { SQLGUID guidValue; - ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), NULL); - if (SQL_SUCCEEDED(ret)) { - std::ostringstream oss; - oss << std::hex << std::setfill('0') << std::setw(8) << guidValue.Data1 << '-' - << std::setw(4) << guidValue.Data2 << '-' << std::setw(4) << guidValue.Data3 - << '-' << std::setw(2) << static_cast(guidValue.Data4[0]) - << std::setw(2) << static_cast(guidValue.Data4[1]) << '-' << std::hex - << std::setw(2) << static_cast(guidValue.Data4[2]) << std::setw(2) - << static_cast(guidValue.Data4[3]) << std::setw(2) - << static_cast(guidValue.Data4[4]) << std::setw(2) - << static_cast(guidValue.Data4[5]) << std::setw(2) - << static_cast(guidValue.Data4[6]) << std::setw(2) - << static_cast(guidValue.Data4[7]); - row.append(oss.str()); // Append GUID as a string + SQLLEN indicator; + ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), &indicator); + + if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) { + std::vector guid_bytes(16); + guid_bytes[0] = ((char*)&guidValue.Data1)[3]; + guid_bytes[1] = ((char*)&guidValue.Data1)[2]; + guid_bytes[2] = ((char*)&guidValue.Data1)[1]; + guid_bytes[3] = ((char*)&guidValue.Data1)[0]; + guid_bytes[4] = ((char*)&guidValue.Data2)[1]; + guid_bytes[5] = ((char*)&guidValue.Data2)[0]; + guid_bytes[6] = ((char*)&guidValue.Data3)[1]; + guid_bytes[7] = ((char*)&guidValue.Data3)[0]; + std::memcpy(&guid_bytes[8], guidValue.Data4, sizeof(guidValue.Data4)); + + py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size()); + py::object uuid_module = py::module_::import("uuid"); + py::object uuid_obj = uuid_module.attr("UUID")(py_guid_bytes); + row.append(uuid_obj); + } else if (indicator == SQL_NULL_DATA) { + row.append(py::none()); } else { LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " "code - {}. Returning NULL value instead", @@ -2221,7 +2255,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } -#endif + #endif default: std::ostringstream errorString; errorString << "Unsupported data type for column - " << columnName << ", Type - " @@ -2590,9 +2624,19 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum break; } case SQL_GUID: { - row.append( - py::bytes(reinterpret_cast(&buffers.guidBuffers[col - 1][i]), - sizeof(SQLGUID))); + 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()); + py::dict kwargs; + kwargs["bytes"] = py_guid_bytes; + py::object uuid_obj = py::module_::import("uuid").attr("UUID")(**kwargs); + row.append(uuid_obj); break; } case SQL_BINARY: diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 1f6d66303..764cebe60 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -14,6 +14,7 @@ import decimal from contextlib import closing from mssql_python import Connection, row +import uuid # Setup test table TEST_TABLE = """ @@ -6823,6 +6824,159 @@ def test_money_smallmoney_invalid_values(cursor, db_connection): drop_table_if_exists(cursor, "dbo.money_test") db_connection.commit() +def test_uuid_insert_and_select_none(cursor, db_connection): + """Test inserting and retrieving None in a nullable UUID column.""" + table_name = "#pytest_uuid_nullable" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + name NVARCHAR(50) + ) + """) + db_connection.commit() + + # Insert a row with None for the UUID + cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + db_connection.commit() + + # Fetch the row + cursor.execute(f"SELECT id, name FROM {table_name}") + row = cursor.fetchone() + retrieved_uuid, retrieved_name = row + + # Assert that the retrieved UUID is None + assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}" + + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + +def test_insert_multiple_uuids(cursor, db_connection): + """Test inserting multiple UUIDs and verifying retrieval.""" + table_name = "#pytest_uuid_multiple" + try: + 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() + + uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)} + + # Insert UUIDs and descriptions + for desc, uid in uuids_to_insert.items(): + cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc]) + db_connection.commit() + + # Fetch all data + cursor.execute(f"SELECT id, description FROM {table_name}") + rows = cursor.fetchall() + + # Verify each fetched row against the original data + assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match." + + for row in rows: + retrieved_uuid, retrieved_desc = row + + # Assert type is correct + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" + + # Use the description to look up the original UUID + expected_uuid = uuids_to_insert.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: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + +def test_uuid_insert_with_none(cursor, db_connection): + """Test that inserting None into a UUID column raises an error (or is handled).""" + table_name = "#pytest_uuid_none" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + name NVARCHAR(50) + ) + """) + db_connection.commit() + + cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + db_connection.commit() + + cursor.execute(f"SELECT id, name FROM {table_name}") + row = cursor.fetchone() + assert row[0] is None, f"Expected NULL UUID, got {row[0]}" + assert row[1] == "Bob" + + + finally: + 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_close(db_connection): """Test closing the cursor""" try: From a7c83f8cf0eafeaf0c56f80f61f2cf20a93ad892 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 15 Sep 2025 14:29:50 +0530 Subject: [PATCH 2/7] tested --- tests/test_004_cursor.py | 56 ---------------------------------------- 1 file changed, 56 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 764cebe60..c9d547e53 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6921,62 +6921,6 @@ def test_uuid_insert_with_none(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_close(db_connection): """Test closing the cursor""" try: From e9aa8339652b83ab3a47049ba9a27e165f344901 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 15 Sep 2025 15:39:11 +0530 Subject: [PATCH 3/7] resolving copilot comments --- mssql_python/pybind/ddbc_bindings.cpp | 3 --- tests/test_004_cursor.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 33006889c..11a460dbe 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -2625,9 +2625,6 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum } case SQL_GUID: { 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)); diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index c9d547e53..dbd992b1b 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6896,7 +6896,7 @@ def test_insert_multiple_uuids(cursor, db_connection): db_connection.commit() def test_uuid_insert_with_none(cursor, db_connection): - """Test that inserting None into a UUID column raises an error (or is handled).""" + """Test that inserting None into a UUID column results in a NULL value and is handled correctly.""" table_name = "#pytest_uuid_none" try: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") From c03b72b875368e9a27f9b13493537dc5aeec380a Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 12:10:28 +0530 Subject: [PATCH 4/7] cleanup --- mssql_python/cursor.py | 2 - mssql_python/pybind/ddbc_bindings.cpp | 36 +++++----- tests/test_004_cursor.py | 96 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 1e9c3b8be..1ad530c7b 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -774,8 +774,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 ) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 11a460dbe..b6c5a387b 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -484,29 +484,27 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, break; } case SQL_C_GUID: { - if (!py::isinstance(param)) { + py::object uuid_obj = param; + py::object uuid_cls = py::module_::import("uuid").attr("UUID"); + if (!py::isinstance(uuid_obj, uuid_cls)) { ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); } - py::bytes uuid_bytes = param.cast(); - const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); + py::bytes uuid_bytes = uuid_obj.attr("bytes").cast(); if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { ThrowStdException("UUID binary data must be exactly 16 bytes long."); } + const unsigned char* b = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); SQLGUID* guid_data_ptr = AllocateParamBuffer(paramBuffers); + guid_data_ptr->Data1 = (static_cast(b[3]) << 24) | + (static_cast(b[2]) << 16) | + (static_cast(b[1]) << 8) | + static_cast(b[0]); + guid_data_ptr->Data2 = (static_cast(b[5]) << 8) | + static_cast(b[4]); + guid_data_ptr->Data3 = (static_cast(b[7]) << 8) | + static_cast(b[6]); + std::memcpy(guid_data_ptr->Data4, &b[8], 8); - guid_data_ptr->Data1 = - (static_cast(uuid_data[3]) << 24) | - (static_cast(uuid_data[2]) << 16) | - (static_cast(uuid_data[1]) << 8) | - (static_cast(uuid_data[0])); - guid_data_ptr->Data2 = - (static_cast(uuid_data[5]) << 8) | - (static_cast(uuid_data[4])); - guid_data_ptr->Data3 = - (static_cast(uuid_data[7]) << 8) | - (static_cast(uuid_data[6])); - - std::memcpy(guid_data_ptr->Data4, &uuid_data[8], 8); dataPtr = static_cast(guid_data_ptr); bufferLength = sizeof(SQLGUID); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -2242,8 +2240,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p std::memcpy(&guid_bytes[8], guidValue.Data4, sizeof(guidValue.Data4)); py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size()); - py::object uuid_module = py::module_::import("uuid"); - py::object uuid_obj = uuid_module.attr("UUID")(py_guid_bytes); + py::object uuid_obj = py::module_::import("uuid").attr("UUID")(py::arg("bytes") = py_guid_bytes); row.append(uuid_obj); } else if (indicator == SQL_NULL_DATA) { row.append(py::none()); @@ -2255,7 +2252,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } - #endif +#endif default: std::ostringstream errorString; errorString << "Unsupported data type for column - " << columnName << ", Type - " @@ -2628,7 +2625,6 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum 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; diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index dbd992b1b..ef4ac2d74 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6921,6 +6921,102 @@ def test_uuid_insert_with_none(cursor, db_connection): cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() + +def test_uuid_insert_and_select_none(cursor, db_connection): + """Test inserting and retrieving None in a nullable UUID column.""" + table_name = "#pytest_uuid_nullable" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + name NVARCHAR(50) + ) + """) + db_connection.commit() + + # Insert a row with None for the UUID + cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + db_connection.commit() + + # Fetch the row + cursor.execute(f"SELECT id, name FROM {table_name}") + retrieved_uuid, retrieved_name = cursor.fetchone() + + # Assert that the retrieved UUID is None + assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}" + assert retrieved_name == "Bob" + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + + +def test_insert_multiple_uuids(cursor, db_connection): + """Test inserting multiple UUIDs and verifying retrieval.""" + table_name = "#pytest_uuid_multiple" + try: + 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() + + uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)} + + # Insert UUIDs and descriptions directly + for desc, uid in uuids_to_insert.items(): + cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc]) + db_connection.commit() + + # Fetch all data + cursor.execute(f"SELECT id, description FROM {table_name}") + rows = cursor.fetchall() + + # Verify each fetched row against the original data + assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match." + + for retrieved_uuid, retrieved_desc in rows: + # Assert type is correct + assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" + + # Use the description to look up the original UUID + expected_uuid = uuids_to_insert.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: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + + +def test_uuid_insert_with_none(cursor, db_connection): + """Test inserting None into a UUID column results in a NULL value.""" + table_name = "#pytest_uuid_none" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f""" + CREATE TABLE {table_name} ( + id UNIQUEIDENTIFIER, + name NVARCHAR(50) + ) + """) + db_connection.commit() + + cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + db_connection.commit() + + cursor.execute(f"SELECT id, name FROM {table_name}") + retrieved_uuid, retrieved_name = cursor.fetchone() + + assert retrieved_uuid is None, f"Expected NULL UUID, got {retrieved_uuid}" + assert retrieved_name == "Bob" + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + def test_close(db_connection): """Test closing the cursor""" try: From b7fceaae8509f155087c0de232a4d2b4df70673b Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Fri, 19 Sep 2025 12:24:20 +0530 Subject: [PATCH 5/7] merge conflict --- tests/test_004_cursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index ef4ac2d74..4fe976bc1 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -13,7 +13,7 @@ from datetime import datetime, date, time import decimal from contextlib import closing -from mssql_python import Connection, row +import mssql_python import uuid # Setup test table From 32e7d049445cdb338489bff1f5e8413edd8a6870 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 22 Sep 2025 14:34:39 +0530 Subject: [PATCH 6/7] also fixes the github issue #241 --- mssql_python/cursor.py | 33 +++-- mssql_python/pybind/ddbc_bindings.cpp | 50 ++++--- tests/test_004_cursor.py | 180 ++++++++++++++------------ 3 files changed, 150 insertions(+), 113 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 1ad530c7b..c6747cdd6 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -319,6 +319,16 @@ def _map_sql_type(self, param, parameters_list, i): parameters_list[i].scale, False, ) + + if isinstance(param, uuid.UUID): + parameters_list[i] = param.bytes_le + return ( + ddbc_sql_const.SQL_GUID.value, + ddbc_sql_const.SQL_C_GUID.value, + 16, + 0, + False, + ) if isinstance(param, str): if ( @@ -333,6 +343,20 @@ def _map_sql_type(self, param, parameters_list, i): 0, False, ) + + try: + val = uuid.UUID(param) + parameters_list[i] = val.bytes_le + return ( + ddbc_sql_const.SQL_GUID.value, + ddbc_sql_const.SQL_C_GUID.value, + 16, + 0, + False + ) + except ValueError: + pass + # Attempt to parse as date, datetime, datetime2, timestamp, smalldatetime or time if self._parse_date(param): @@ -426,15 +450,6 @@ def _map_sql_type(self, param, parameters_list, i): 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): return ( ddbc_sql_const.SQL_TIMESTAMP.value, diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index b6c5a387b..23a43dce8 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -484,27 +484,28 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, break; } case SQL_C_GUID: { - py::object uuid_obj = param; - py::object uuid_cls = py::module_::import("uuid").attr("UUID"); - if (!py::isinstance(uuid_obj, uuid_cls)) { + if (!py::isinstance(param)) { ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); } - py::bytes uuid_bytes = uuid_obj.attr("bytes").cast(); + py::bytes uuid_bytes = param.cast(); + const unsigned char* uuid_data = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) { + LOG("Invalid UUID parameter at index {}: expected 16 bytes, got {} bytes, type {}", paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()), paramInfo.paramCType); ThrowStdException("UUID binary data must be exactly 16 bytes long."); } - const unsigned char* b = reinterpret_cast(PyBytes_AS_STRING(uuid_bytes.ptr())); SQLGUID* guid_data_ptr = AllocateParamBuffer(paramBuffers); - guid_data_ptr->Data1 = (static_cast(b[3]) << 24) | - (static_cast(b[2]) << 16) | - (static_cast(b[1]) << 8) | - static_cast(b[0]); - guid_data_ptr->Data2 = (static_cast(b[5]) << 8) | - static_cast(b[4]); - guid_data_ptr->Data3 = (static_cast(b[7]) << 8) | - static_cast(b[6]); - std::memcpy(guid_data_ptr->Data4, &b[8], 8); - + guid_data_ptr->Data1 = + (static_cast(uuid_data[3]) << 24) | + (static_cast(uuid_data[2]) << 16) | + (static_cast(uuid_data[1]) << 8) | + (static_cast(uuid_data[0])); + guid_data_ptr->Data2 = + (static_cast(uuid_data[5]) << 8) | + (static_cast(uuid_data[4])); + guid_data_ptr->Data3 = + (static_cast(uuid_data[7]) << 8) | + (static_cast(uuid_data[6])); + std::memcpy(guid_data_ptr->Data4, &uuid_data[8], 8); dataPtr = static_cast(guid_data_ptr); bufferLength = sizeof(SQLGUID); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -2240,7 +2241,8 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p std::memcpy(&guid_bytes[8], guidValue.Data4, sizeof(guidValue.Data4)); py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size()); - py::object uuid_obj = py::module_::import("uuid").attr("UUID")(py::arg("bytes") = py_guid_bytes); + py::object uuid_module = py::module_::import("uuid"); + py::object uuid_obj = uuid_module.attr("UUID")(py::arg("bytes")=py_guid_bytes); row.append(uuid_obj); } else if (indicator == SQL_NULL_DATA) { row.append(py::none()); @@ -2622,10 +2624,18 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum } case SQL_GUID: { SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i]; - std::vector guid_bytes(16); - std::memcpy(guid_bytes.data(), guidValue, sizeof(SQLGUID)); - - 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); diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 4fe976bc1..df849e81e 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6824,6 +6824,9 @@ def test_money_smallmoney_invalid_values(cursor, db_connection): drop_table_if_exists(cursor, "dbo.money_test") db_connection.commit() +import uuid +import pytest + def test_uuid_insert_and_select_none(cursor, db_connection): """Test inserting and retrieving None in a nullable UUID column.""" table_name = "#pytest_uuid_nullable" @@ -6843,16 +6846,16 @@ def test_uuid_insert_and_select_none(cursor, db_connection): # Fetch the row cursor.execute(f"SELECT id, name FROM {table_name}") - row = cursor.fetchone() - retrieved_uuid, retrieved_name = row - - # Assert that the retrieved UUID is None - assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}" + retrieved_uuid, retrieved_name = cursor.fetchone() + # Assert correct results + assert retrieved_uuid is None, f"Expected None, got {retrieved_uuid}" + assert retrieved_name == "Bob" finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() + def test_insert_multiple_uuids(cursor, db_connection): """Test inserting multiple UUIDs and verifying retrieval.""" table_name = "#pytest_uuid_multiple" @@ -6866,65 +6869,74 @@ def test_insert_multiple_uuids(cursor, db_connection): """) db_connection.commit() + # Prepare test data uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)} - + # Insert UUIDs and descriptions for desc, uid in uuids_to_insert.items(): cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc]) db_connection.commit() - # Fetch all data + # Fetch all rows cursor.execute(f"SELECT id, description FROM {table_name}") rows = cursor.fetchall() - - # Verify each fetched row against the original data - assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match." - - for row in rows: - retrieved_uuid, retrieved_desc = row - - # Assert type is correct + + # Verify each fetched row + assert len(rows) == len(uuids_to_insert), "Fetched row count mismatch" + + for retrieved_uuid, retrieved_desc in rows: assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" - - # Use the description to look up the original UUID - expected_uuid = uuids_to_insert.get(retrieved_desc) - - assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data." + expected_uuid = uuids_to_insert[retrieved_desc] assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}" finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() -def test_uuid_insert_with_none(cursor, db_connection): - """Test that inserting None into a UUID column results in a NULL value and is handled correctly.""" - table_name = "#pytest_uuid_none" + +def test_fetchmany_uuids(cursor, db_connection): + """Test fetching multiple UUID rows with fetchmany().""" + table_name = "#pytest_uuid_fetchmany" try: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") cursor.execute(f""" CREATE TABLE {table_name} ( - id UNIQUEIDENTIFIER, - name NVARCHAR(50) + id UNIQUEIDENTIFIER PRIMARY KEY, + description NVARCHAR(50) ) """) db_connection.commit() - cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(10)} + + for desc, uid in uuids_to_insert.items(): + cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc]) db_connection.commit() - cursor.execute(f"SELECT id, name FROM {table_name}") - row = cursor.fetchone() - assert row[0] is None, f"Expected NULL UUID, got {row[0]}" - assert row[1] == "Bob" + cursor.execute(f"SELECT id, description FROM {table_name}") + # Fetch in batches of 3 + batch_size = 3 + fetched_rows = [] + while True: + batch = cursor.fetchmany(batch_size) + if not batch: + break + fetched_rows.extend(batch) + # Verify all rows + assert len(fetched_rows) == len(uuids_to_insert), "Fetched row count mismatch" + for retrieved_uuid, retrieved_desc in fetched_rows: + assert isinstance(retrieved_uuid, uuid.UUID) + expected_uuid = uuids_to_insert[retrieved_desc] + assert retrieved_uuid == expected_uuid finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() -def test_uuid_insert_and_select_none(cursor, db_connection): - """Test inserting and retrieving None in a nullable UUID column.""" - table_name = "#pytest_uuid_nullable" +def test_uuid_insert_with_none(cursor, db_connection): + """Test inserting None into a UUID column results in a NULL value.""" + table_name = "#pytest_uuid_none" try: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") cursor.execute(f""" @@ -6935,84 +6947,84 @@ def test_uuid_insert_and_select_none(cursor, db_connection): """) db_connection.commit() - # Insert a row with None for the UUID - cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Alice"]) db_connection.commit() - # Fetch the row cursor.execute(f"SELECT id, name FROM {table_name}") retrieved_uuid, retrieved_name = cursor.fetchone() - # Assert that the retrieved UUID is None - assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}" - assert retrieved_name == "Bob" + assert retrieved_uuid is None, f"Expected NULL UUID, got {retrieved_uuid}" + assert retrieved_name == "Alice" finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() +def test_invalid_uuid_inserts(cursor, db_connection): + """Test inserting invalid UUID values raises appropriate errors.""" + table_name = "#pytest_uuid_invalid" + try: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + cursor.execute(f"CREATE TABLE {table_name} (id UNIQUEIDENTIFIER)") + db_connection.commit() -def test_insert_multiple_uuids(cursor, db_connection): - """Test inserting multiple UUIDs and verifying retrieval.""" - table_name = "#pytest_uuid_multiple" + invalid_values = [ + "12345", # Too short + "not-a-uuid", # Not a UUID string + 123456789, # Integer + 12.34, # Float + object() # Arbitrary object + ] + + for val in invalid_values: + with pytest.raises(Exception): + cursor.execute(f"INSERT INTO {table_name} (id) VALUES (?)", [val]) + db_connection.commit() + finally: + cursor.execute(f"DROP TABLE IF EXISTS {table_name}") + db_connection.commit() + +def test_duplicate_uuid_inserts(cursor, db_connection): + """Test that inserting duplicate UUIDs into a PK column raises an error.""" + table_name = "#pytest_uuid_duplicate" try: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") - cursor.execute(f""" - CREATE TABLE {table_name} ( - id UNIQUEIDENTIFIER PRIMARY KEY, - description NVARCHAR(50) - ) - """) + cursor.execute(f"CREATE TABLE {table_name} (id UNIQUEIDENTIFIER PRIMARY KEY)") db_connection.commit() - uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)} - - # Insert UUIDs and descriptions directly - for desc, uid in uuids_to_insert.items(): - cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc]) + uid = uuid.uuid4() + cursor.execute(f"INSERT INTO {table_name} (id) VALUES (?)", [uid]) db_connection.commit() - # Fetch all data - cursor.execute(f"SELECT id, description FROM {table_name}") - rows = cursor.fetchall() - - # Verify each fetched row against the original data - assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match." - - for retrieved_uuid, retrieved_desc in rows: - # Assert type is correct - assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}" - - # Use the description to look up the original UUID - expected_uuid = uuids_to_insert.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}" + with pytest.raises(Exception): + cursor.execute(f"INSERT INTO {table_name} (id) VALUES (?)", [uid]) + db_connection.commit() finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() - -def test_uuid_insert_with_none(cursor, db_connection): - """Test inserting None into a UUID column results in a NULL value.""" - table_name = "#pytest_uuid_none" +def test_extreme_uuids(cursor, db_connection): + """Test inserting extreme but valid UUIDs.""" + table_name = "#pytest_uuid_extreme" try: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") - cursor.execute(f""" - CREATE TABLE {table_name} ( - id UNIQUEIDENTIFIER, - name NVARCHAR(50) - ) - """) + cursor.execute(f"CREATE TABLE {table_name} (id UNIQUEIDENTIFIER)") db_connection.commit() - cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"]) + extreme_uuids = [ + uuid.UUID(int=0), # All zeros + uuid.UUID(int=(1 << 128) - 1), # All ones + ] + + for uid in extreme_uuids: + cursor.execute(f"INSERT INTO {table_name} (id) VALUES (?)", [uid]) db_connection.commit() - cursor.execute(f"SELECT id, name FROM {table_name}") - retrieved_uuid, retrieved_name = cursor.fetchone() + cursor.execute(f"SELECT id FROM {table_name}") + rows = cursor.fetchall() + fetched_uuids = [row[0] for row in rows] - assert retrieved_uuid is None, f"Expected NULL UUID, got {retrieved_uuid}" - assert retrieved_name == "Bob" + for uid in extreme_uuids: + assert uid in fetched_uuids, f"Extreme UUID {uid} not retrieved correctly" finally: cursor.execute(f"DROP TABLE IF EXISTS {table_name}") db_connection.commit() From 3529b8839e43341625e965382fafb17d7445a2bd Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 22 Sep 2025 14:44:26 +0530 Subject: [PATCH 7/7] cleanup --- tests/test_004_cursor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index df849e81e..ebe497f70 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6824,9 +6824,6 @@ def test_money_smallmoney_invalid_values(cursor, db_connection): drop_table_if_exists(cursor, "dbo.money_test") db_connection.commit() -import uuid -import pytest - def test_uuid_insert_and_select_none(cursor, db_connection): """Test inserting and retrieving None in a nullable UUID column.""" table_name = "#pytest_uuid_nullable"