Skip to content

Commit fc86366

Browse files
committed
working uuid
1 parent 1ed773c commit fc86366

File tree

3 files changed

+228
-19
lines changed

3 files changed

+228
-19
lines changed

mssql_python/cursor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,15 @@ def _map_sql_type(self, param, parameters_list, i):
425425
0,
426426
False,
427427
)
428+
429+
if isinstance(param, uuid.UUID):
430+
return (
431+
ddbc_sql_const.SQL_GUID.value,
432+
ddbc_sql_const.SQL_C_GUID.value,
433+
16,
434+
0,
435+
False,
436+
)
428437

429438
if isinstance(param, datetime.datetime):
430439
return (
@@ -765,6 +774,8 @@ def execute(
765774

766775
if parameters:
767776
for i, param in enumerate(parameters):
777+
if isinstance(param, uuid.UUID):
778+
parameters[i] = param.bytes
768779
paraminfo = self._create_parameter_types_list(
769780
param, param_info, parameters, i
770781
)

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,34 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params,
484484
break;
485485
}
486486
case SQL_C_GUID: {
487-
// TODO
487+
if (!py::isinstance<py::bytes>(param)) {
488+
ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
489+
}
490+
py::bytes uuid_bytes = param.cast<py::bytes>();
491+
const unsigned char* uuid_data = reinterpret_cast<const unsigned char*>(PyBytes_AS_STRING(uuid_bytes.ptr()));
492+
if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
493+
ThrowStdException("UUID binary data must be exactly 16 bytes long.");
494+
}
495+
SQLGUID* guid_data_ptr = AllocateParamBuffer<SQLGUID>(paramBuffers);
496+
497+
guid_data_ptr->Data1 =
498+
(static_cast<uint32_t>(uuid_data[3]) << 24) |
499+
(static_cast<uint32_t>(uuid_data[2]) << 16) |
500+
(static_cast<uint32_t>(uuid_data[1]) << 8) |
501+
(static_cast<uint32_t>(uuid_data[0]));
502+
guid_data_ptr->Data2 =
503+
(static_cast<uint16_t>(uuid_data[5]) << 8) |
504+
(static_cast<uint16_t>(uuid_data[4]));
505+
guid_data_ptr->Data3 =
506+
(static_cast<uint16_t>(uuid_data[7]) << 8) |
507+
(static_cast<uint16_t>(uuid_data[6]));
508+
509+
std::memcpy(guid_data_ptr->Data4, &uuid_data[8], 8);
510+
dataPtr = static_cast<void*>(guid_data_ptr);
511+
bufferLength = sizeof(SQLGUID);
512+
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
513+
*strLenOrIndPtr = sizeof(SQLGUID);
514+
break;
488515
}
489516
default: {
490517
std::ostringstream errorString;
@@ -2199,20 +2226,27 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
21992226
#if (ODBCVER >= 0x0350)
22002227
case SQL_GUID: {
22012228
SQLGUID guidValue;
2202-
ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), NULL);
2203-
if (SQL_SUCCEEDED(ret)) {
2204-
std::ostringstream oss;
2205-
oss << std::hex << std::setfill('0') << std::setw(8) << guidValue.Data1 << '-'
2206-
<< std::setw(4) << guidValue.Data2 << '-' << std::setw(4) << guidValue.Data3
2207-
<< '-' << std::setw(2) << static_cast<int>(guidValue.Data4[0])
2208-
<< std::setw(2) << static_cast<int>(guidValue.Data4[1]) << '-' << std::hex
2209-
<< std::setw(2) << static_cast<int>(guidValue.Data4[2]) << std::setw(2)
2210-
<< static_cast<int>(guidValue.Data4[3]) << std::setw(2)
2211-
<< static_cast<int>(guidValue.Data4[4]) << std::setw(2)
2212-
<< static_cast<int>(guidValue.Data4[5]) << std::setw(2)
2213-
<< static_cast<int>(guidValue.Data4[6]) << std::setw(2)
2214-
<< static_cast<int>(guidValue.Data4[7]);
2215-
row.append(oss.str()); // Append GUID as a string
2229+
SQLLEN indicator;
2230+
ret = SQLGetData_ptr(hStmt, i, SQL_C_GUID, &guidValue, sizeof(guidValue), &indicator);
2231+
2232+
if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
2233+
std::vector<char> guid_bytes(16);
2234+
guid_bytes[0] = ((char*)&guidValue.Data1)[3];
2235+
guid_bytes[1] = ((char*)&guidValue.Data1)[2];
2236+
guid_bytes[2] = ((char*)&guidValue.Data1)[1];
2237+
guid_bytes[3] = ((char*)&guidValue.Data1)[0];
2238+
guid_bytes[4] = ((char*)&guidValue.Data2)[1];
2239+
guid_bytes[5] = ((char*)&guidValue.Data2)[0];
2240+
guid_bytes[6] = ((char*)&guidValue.Data3)[1];
2241+
guid_bytes[7] = ((char*)&guidValue.Data3)[0];
2242+
std::memcpy(&guid_bytes[8], guidValue.Data4, sizeof(guidValue.Data4));
2243+
2244+
py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size());
2245+
py::object uuid_module = py::module_::import("uuid");
2246+
py::object uuid_obj = uuid_module.attr("UUID")(py_guid_bytes);
2247+
row.append(uuid_obj);
2248+
} else if (indicator == SQL_NULL_DATA) {
2249+
row.append(py::none());
22162250
} else {
22172251
LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return "
22182252
"code - {}. Returning NULL value instead",
@@ -2221,7 +2255,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
22212255
}
22222256
break;
22232257
}
2224-
#endif
2258+
#endif
22252259
default:
22262260
std::ostringstream errorString;
22272261
errorString << "Unsupported data type for column - " << columnName << ", Type - "
@@ -2590,9 +2624,19 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
25902624
break;
25912625
}
25922626
case SQL_GUID: {
2593-
row.append(
2594-
py::bytes(reinterpret_cast<const char*>(&buffers.guidBuffers[col - 1][i]),
2595-
sizeof(SQLGUID)));
2627+
SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i];
2628+
// We already have the raw bytes from SQL Server in the SQLGUID struct.
2629+
// We do not need to perform any additional reordering here, as the C++
2630+
// SQLGUID struct is already laid out in the non-standard SQL Server byte order.
2631+
std::vector<char> guid_bytes(16);
2632+
std::memcpy(guid_bytes.data(), guidValue, sizeof(SQLGUID));
2633+
2634+
// Convert the raw C++ byte vector to a Python bytes object
2635+
py::bytes py_guid_bytes(guid_bytes.data(), guid_bytes.size());
2636+
py::dict kwargs;
2637+
kwargs["bytes"] = py_guid_bytes;
2638+
py::object uuid_obj = py::module_::import("uuid").attr("UUID")(**kwargs);
2639+
row.append(uuid_obj);
25962640
break;
25972641
}
25982642
case SQL_BINARY:

tests/test_004_cursor.py

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

1819
# Setup test table
1920
TEST_TABLE = """
@@ -6823,6 +6824,159 @@ def test_money_smallmoney_invalid_values(cursor, db_connection):
68236824
drop_table_if_exists(cursor, "dbo.money_test")
68246825
db_connection.commit()
68256826

6827+
def test_uuid_insert_and_select_none(cursor, db_connection):
6828+
"""Test inserting and retrieving None in a nullable UUID column."""
6829+
table_name = "#pytest_uuid_nullable"
6830+
try:
6831+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6832+
cursor.execute(f"""
6833+
CREATE TABLE {table_name} (
6834+
id UNIQUEIDENTIFIER,
6835+
name NVARCHAR(50)
6836+
)
6837+
""")
6838+
db_connection.commit()
6839+
6840+
# Insert a row with None for the UUID
6841+
cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"])
6842+
db_connection.commit()
6843+
6844+
# Fetch the row
6845+
cursor.execute(f"SELECT id, name FROM {table_name}")
6846+
row = cursor.fetchone()
6847+
retrieved_uuid, retrieved_name = row
6848+
6849+
# Assert that the retrieved UUID is None
6850+
assert retrieved_uuid is None, f"Expected None, got {type(retrieved_uuid)}"
6851+
6852+
finally:
6853+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6854+
db_connection.commit()
6855+
6856+
def test_insert_multiple_uuids(cursor, db_connection):
6857+
"""Test inserting multiple UUIDs and verifying retrieval."""
6858+
table_name = "#pytest_uuid_multiple"
6859+
try:
6860+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6861+
cursor.execute(f"""
6862+
CREATE TABLE {table_name} (
6863+
id UNIQUEIDENTIFIER PRIMARY KEY,
6864+
description NVARCHAR(50)
6865+
)
6866+
""")
6867+
db_connection.commit()
6868+
6869+
uuids_to_insert = {f"Item {i}": uuid.uuid4() for i in range(5)}
6870+
6871+
# Insert UUIDs and descriptions
6872+
for desc, uid in uuids_to_insert.items():
6873+
cursor.execute(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", [uid, desc])
6874+
db_connection.commit()
6875+
6876+
# Fetch all data
6877+
cursor.execute(f"SELECT id, description FROM {table_name}")
6878+
rows = cursor.fetchall()
6879+
6880+
# Verify each fetched row against the original data
6881+
assert len(rows) == len(uuids_to_insert), "Number of fetched rows does not match."
6882+
6883+
for row in rows:
6884+
retrieved_uuid, retrieved_desc = row
6885+
6886+
# Assert type is correct
6887+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}"
6888+
6889+
# Use the description to look up the original UUID
6890+
expected_uuid = uuids_to_insert.get(retrieved_desc)
6891+
6892+
assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data."
6893+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}"
6894+
finally:
6895+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6896+
db_connection.commit()
6897+
6898+
def test_uuid_insert_with_none(cursor, db_connection):
6899+
"""Test that inserting None into a UUID column raises an error (or is handled)."""
6900+
table_name = "#pytest_uuid_none"
6901+
try:
6902+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6903+
cursor.execute(f"""
6904+
CREATE TABLE {table_name} (
6905+
id UNIQUEIDENTIFIER,
6906+
name NVARCHAR(50)
6907+
)
6908+
""")
6909+
db_connection.commit()
6910+
6911+
cursor.execute(f"INSERT INTO {table_name} (id, name) VALUES (?, ?)", [None, "Bob"])
6912+
db_connection.commit()
6913+
6914+
cursor.execute(f"SELECT id, name FROM {table_name}")
6915+
row = cursor.fetchone()
6916+
assert row[0] is None, f"Expected NULL UUID, got {row[0]}"
6917+
assert row[1] == "Bob"
6918+
6919+
6920+
finally:
6921+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6922+
db_connection.commit()
6923+
6924+
def test_executemany_uuid_insert_and_select(cursor, db_connection):
6925+
"""Test inserting multiple UUIDs using executemany and verifying retrieval."""
6926+
table_name = "#pytest_uuid_executemany"
6927+
6928+
try:
6929+
# Drop and create a temporary table for the test
6930+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6931+
cursor.execute(f"""
6932+
CREATE TABLE {table_name} (
6933+
id UNIQUEIDENTIFIER PRIMARY KEY,
6934+
description NVARCHAR(50)
6935+
)
6936+
""")
6937+
db_connection.commit()
6938+
6939+
# Generate data for insertion
6940+
data_to_insert = []
6941+
uuids_to_check = {}
6942+
for i in range(5):
6943+
new_uuid = uuid.uuid4()
6944+
description = f"Item {i}"
6945+
data_to_insert.append((new_uuid, description))
6946+
uuids_to_check[description] = new_uuid
6947+
6948+
# Insert all data with a single call to executemany
6949+
sql = f"INSERT INTO {table_name} (id, description) VALUES (?, ?)"
6950+
cursor.executemany(sql, data_to_insert)
6951+
db_connection.commit()
6952+
6953+
# Verify the number of rows inserted
6954+
assert cursor.rowcount == 5, f"Expected 5 rows inserted, but got {cursor.rowcount}"
6955+
6956+
# Fetch all data from the table
6957+
cursor.execute(f"SELECT id, description FROM {table_name}")
6958+
rows = cursor.fetchall()
6959+
6960+
# Verify the number of fetched rows
6961+
assert len(rows) == len(data_to_insert), "Number of fetched rows does not match."
6962+
6963+
# Verify each fetched row's data and type
6964+
for row in rows:
6965+
retrieved_uuid, retrieved_desc = row
6966+
6967+
# Assert the type is correct
6968+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}"
6969+
6970+
# Assert the value matches the original data
6971+
expected_uuid = uuids_to_check.get(retrieved_desc)
6972+
assert expected_uuid is not None, f"Retrieved description '{retrieved_desc}' was not in the original data."
6973+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}"
6974+
6975+
finally:
6976+
# Clean up the temporary table
6977+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
6978+
db_connection.commit()
6979+
68266980
def test_close(db_connection):
68276981
"""Test closing the cursor"""
68286982
try:

0 commit comments

Comments
 (0)