Skip to content

Commit cfa04be

Browse files
committed
working uuid
1 parent bd5b323 commit cfa04be

File tree

3 files changed

+227
-19
lines changed

3 files changed

+227
-19
lines changed

mssql_python/cursor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@ def _map_sql_type(self, param, parameters_list, i, min_val=None, max_val=None):
441441
0,
442442
False
443443
)
444+
445+
if isinstance(param, uuid.UUID):
446+
return (
447+
ddbc_sql_const.SQL_GUID.value,
448+
ddbc_sql_const.SQL_C_GUID.value,
449+
16,
450+
0,
451+
False,
452+
)
444453

445454
if isinstance(param, datetime.datetime):
446455
return (
@@ -949,6 +958,8 @@ def execute(
949958

950959
if parameters:
951960
for i, param in enumerate(parameters):
961+
if isinstance(param, uuid.UUID):
962+
parameters[i] = param.bytes
952963
paraminfo = self._create_parameter_types_list(
953964
param, param_info, parameters, i
954965
)

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,34 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params,
504504
break;
505505
}
506506
case SQL_C_GUID: {
507-
// TODO
507+
if (!py::isinstance<py::bytes>(param)) {
508+
ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
509+
}
510+
py::bytes uuid_bytes = param.cast<py::bytes>();
511+
const unsigned char* uuid_data = reinterpret_cast<const unsigned char*>(PyBytes_AS_STRING(uuid_bytes.ptr()));
512+
if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
513+
ThrowStdException("UUID binary data must be exactly 16 bytes long.");
514+
}
515+
SQLGUID* guid_data_ptr = AllocateParamBuffer<SQLGUID>(paramBuffers);
516+
517+
guid_data_ptr->Data1 =
518+
(static_cast<uint32_t>(uuid_data[3]) << 24) |
519+
(static_cast<uint32_t>(uuid_data[2]) << 16) |
520+
(static_cast<uint32_t>(uuid_data[1]) << 8) |
521+
(static_cast<uint32_t>(uuid_data[0]));
522+
guid_data_ptr->Data2 =
523+
(static_cast<uint16_t>(uuid_data[5]) << 8) |
524+
(static_cast<uint16_t>(uuid_data[4]));
525+
guid_data_ptr->Data3 =
526+
(static_cast<uint16_t>(uuid_data[7]) << 8) |
527+
(static_cast<uint16_t>(uuid_data[6]));
528+
529+
std::memcpy(guid_data_ptr->Data4, &uuid_data[8], 8);
530+
dataPtr = static_cast<void*>(guid_data_ptr);
531+
bufferLength = sizeof(SQLGUID);
532+
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
533+
*strLenOrIndPtr = sizeof(SQLGUID);
534+
break;
508535
}
509536
default: {
510537
std::ostringstream errorString;
@@ -2553,20 +2580,27 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
25532580
#if (ODBCVER >= 0x0350)
25542581
case SQL_GUID: {
25552582
SQLGUID guidValue;
2556-
ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), NULL);
2557-
if (SQL_SUCCEEDED(ret)) {
2558-
std::ostringstream oss;
2559-
oss << std::hex << std::setfill('0') << std::setw(8) << guidValue.Data1 << '-'
2560-
<< std::setw(4) << guidValue.Data2 << '-' << std::setw(4) << guidValue.Data3
2561-
<< '-' << std::setw(2) << static_cast<int>(guidValue.Data4[0])
2562-
<< std::setw(2) << static_cast<int>(guidValue.Data4[1]) << '-' << std::hex
2563-
<< std::setw(2) << static_cast<int>(guidValue.Data4[2]) << std::setw(2)
2564-
<< static_cast<int>(guidValue.Data4[3]) << std::setw(2)
2565-
<< static_cast<int>(guidValue.Data4[4]) << std::setw(2)
2566-
<< static_cast<int>(guidValue.Data4[5]) << std::setw(2)
2567-
<< static_cast<int>(guidValue.Data4[6]) << std::setw(2)
2568-
<< static_cast<int>(guidValue.Data4[7]);
2569-
row.append(oss.str()); // Append GUID as a string
2583+
SQLLEN indicator;
2584+
ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), &indicator);
2585+
2586+
if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
2587+
std::vector<char> guid_bytes(16);
2588+
guid_bytes[0] = ((char*)&guidValue.Data1)[3];
2589+
guid_bytes[1] = ((char*)&guidValue.Data1)[2];
2590+
guid_bytes[2] = ((char*)&guidValue.Data1)[1];
2591+
guid_bytes[3] = ((char*)&guidValue.Data1)[0];
2592+
guid_bytes[4] = ((char*)&guidValue.Data2)[1];
2593+
guid_bytes[5] = ((char*)&guidValue.Data2)[0];
2594+
guid_bytes[6] = ((char*)&guidValue.Data3)[1];
2595+
guid_bytes[7] = ((char*)&guidValue.Data3)[0];
2596+
std::memcpy(&guid_bytes[8], guidValue.Data4, sizeof(guidValue.Data4));
2597+
2598+
py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size());
2599+
py::object uuid_module = py::module_::import("uuid");
2600+
py::object uuid_obj = uuid_module.attr("UUID")(py_guid_bytes);
2601+
row.append(uuid_obj);
2602+
} else if (indicator == SQL_NULL_DATA) {
2603+
row.append(py::none());
25702604
} else {
25712605
LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return "
25722606
"code - {}. Returning NULL value instead",
@@ -2575,7 +2609,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
25752609
}
25762610
break;
25772611
}
2578-
#endif
2612+
#endif
25792613
default:
25802614
std::ostringstream errorString;
25812615
errorString << "Unsupported data type for column - " << columnName << ", Type - "
@@ -2957,9 +2991,19 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
29572991
break;
29582992
}
29592993
case SQL_GUID: {
2960-
row.append(
2961-
py::bytes(reinterpret_cast<const char*>(&buffers.guidBuffers[col - 1][i]),
2962-
sizeof(SQLGUID)));
2994+
SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i];
2995+
// We already have the raw bytes from SQL Server in the SQLGUID struct.
2996+
// We do not need to perform any additional reordering here, as the C++
2997+
// SQLGUID struct is already laid out in the non-standard SQL Server byte order.
2998+
std::vector<char> guid_bytes(16);
2999+
std::memcpy(guid_bytes.data(), guidValue, sizeof(SQLGUID));
3000+
3001+
// Convert the raw C++ byte vector to a Python bytes object
3002+
py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size());
3003+
py::dict kwargs;
3004+
kwargs["bytes"] = py_guid_bytes;
3005+
py::object uuid_obj = py::module_::import("uuid").attr("UUID")(**kwargs);
3006+
row.append(uuid_obj);
29633007
break;
29643008
}
29653009
case SQL_BINARY:

tests/test_004_cursor.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import decimal
1515
from contextlib import closing
1616
import mssql_python
17+
import uuid
1718

1819
# Setup test table
1920
TEST_TABLE = """
@@ -10193,6 +10194,158 @@ def test_decimal_separator_calculations(cursor, db_connection):
1019310194

1019410195
# Cleanup
1019510196
cursor.execute("DROP TABLE IF EXISTS #pytest_decimal_calc_test")
10197+
10198+
def test_uuid_insert_and_select_none(cursor, db_connection):
10199+
"""Test inserting and retrieving None in a nullable UUID column."""
10200+
table_name = "#pytest_uuid_nullable"
10201+
try:
10202+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10203+
cursor.execute(f"""
10204+
CREATE TABLE {table_name} (
10205+
id UNIQUEIDENTIFIER,
10206+
name NVARCHAR(50)
10207+
)
10208+
""")
10209+
db_connection.commit()
10210+
10211+
# Insert a row with None for the UUID
10212+
cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"])
10213+
db_connection.commit()
10214+
10215+
# Fetch the row
10216+
cursor.execute(f"SELECT id, name FROM {table_name}")
10217+
row = cursor.fetchone()
10218+
retrieved_uuid, retrieved_name = row
10219+
10220+
# Assert that the retrieved UUID is None
10221+
assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}"
10222+
10223+
finally:
10224+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10225+
db_connection.commit()
10226+
10227+
def test_insert_multiple_uuids(cursor, db_connection):
10228+
"""Test inserting multiple UUIDs and verifying retrieval."""
10229+
table_name = "#pytest_uuid_multiple"
10230+
try:
10231+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10232+
cursor.execute(f"""
10233+
CREATE TABLE {table_name} (
10234+
id UNIQUEIDENTIFIER PRIMARY KEY,
10235+
description NVARCHAR(50)
10236+
)
10237+
""")
10238+
db_connection.commit()
10239+
10240+
uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)}
10241+
10242+
# Insert UUIDs and descriptions
10243+
for desc, uid in uuids_to_insert.items():
10244+
cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc])
10245+
db_connection.commit()
10246+
10247+
# Fetch all data
10248+
cursor.execute(f"SELECT id, description FROM {table_name}")
10249+
rows = cursor.fetchall()
10250+
10251+
# Verify each fetched row against the original data
10252+
assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match."
10253+
10254+
for row in rows:
10255+
retrieved_uuid, retrieved_desc = row
10256+
10257+
# Assert type is correct
10258+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}"
10259+
10260+
# Use the description to look up the original UUID
10261+
expected_uuid = uuids_to_insert.get(retrieved_desc)
10262+
10263+
assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data."
10264+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}"
10265+
finally:
10266+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10267+
db_connection.commit()
10268+
10269+
def test_uuid_insert_with_none(cursor, db_connection):
10270+
"""Test that inserting None into a UUID column raises an error (or is handled)."""
10271+
table_name = "#pytest_uuid_none"
10272+
try:
10273+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10274+
cursor.execute(f"""
10275+
CREATE TABLE {table_name} (
10276+
id UNIQUEIDENTIFIER,
10277+
name NVARCHAR(50)
10278+
)
10279+
""")
10280+
db_connection.commit()
10281+
10282+
cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"])
10283+
db_connection.commit()
10284+
10285+
cursor.execute(f"SELECT id, name FROM {table_name}")
10286+
row = cursor.fetchone()
10287+
assert row[0] is None, f"Expected NULL UUID, got {row[0]}"
10288+
assert row[1] == "Bob"
10289+
10290+
10291+
finally:
10292+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10293+
db_connection.commit()
10294+
10295+
def test_executemany_uuid_insert_and_select(cursor, db_connection):
10296+
"""Test inserting multiple UUIDs using executemany and verifying retrieval."""
10297+
table_name = "#pytest_uuid_executemany"
10298+
10299+
try:
10300+
# Drop and create a temporary table for the test
10301+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10302+
cursor.execute(f"""
10303+
CREATE TABLE {table_name} (
10304+
id UNIQUEIDENTIFIER PRIMARY KEY,
10305+
description NVARCHAR(50)
10306+
)
10307+
""")
10308+
db_connection.commit()
10309+
10310+
# Generate data for insertion
10311+
data_to_insert = []
10312+
uuids_to_check = {}
10313+
for i in range(5):
10314+
new_uuid = uuid.uuid4()
10315+
description = f"Item {i}"
10316+
data_to_insert.append((new_uuid, description))
10317+
uuids_to_check[description] = new_uuid
10318+
10319+
# Insert all data with a single call to executemany
10320+
sql = f"INSERT INTO {table_name} (id, description) VALUES (?, ?)"
10321+
cursor.executemany(sql, data_to_insert)
10322+
db_connection.commit()
10323+
10324+
# Verify the number of rows inserted
10325+
assert cursor.rowcount == 5, f"Expected 5 rows inserted, but got {cursor.rowcount}"
10326+
10327+
# Fetch all data from the table
10328+
cursor.execute(f"SELECT id, description FROM {table_name}")
10329+
rows = cursor.fetchall()
10330+
10331+
# Verify the number of fetched rows
10332+
assert len(rows) == len(data_to_insert), "Number of fetched rows does not match."
10333+
10334+
# Verify each fetched row's data and type
10335+
for row in rows:
10336+
retrieved_uuid, retrieved_desc = row
10337+
10338+
# Assert the type is correct
10339+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}"
10340+
10341+
# Assert the value matches the original data
10342+
expected_uuid = uuids_to_check.get(retrieved_desc)
10343+
assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data."
10344+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}"
10345+
10346+
finally:
10347+
# Clean up the temporary table
10348+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
1019610349
db_connection.commit()
1019710350

1019810351
def test_close(db_connection):

0 commit comments

Comments
 (0)