Skip to content

Commit e7e0f29

Browse files
authored
Merge branch 'main' into saumya/api-bug
2 parents d63d26c + 7598b35 commit e7e0f29

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,49 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt,
20132013
bufferLength = sizeof(SQL_NUMERIC_STRUCT);
20142014
break;
20152015
}
2016+
case SQL_C_GUID: {
2017+
SQLGUID* guidArray = AllocateParamBufferArray<SQLGUID>(tempBuffers, paramSetSize);
2018+
strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
2019+
2020+
static py::module_ uuid_mod = py::module_::import("uuid");
2021+
static py::object uuid_class = uuid_mod.attr("UUID");
2022+
for (size_t i = 0; i < paramSetSize; ++i) {
2023+
const py::handle& element = columnValues[i];
2024+
std::array<unsigned char, 16> uuid_bytes;
2025+
if (element.is_none()) {
2026+
std::memset(&guidArray[i], 0, sizeof(SQLGUID));
2027+
strLenOrIndArray[i] = SQL_NULL_DATA;
2028+
continue;
2029+
}
2030+
else if (py::isinstance<py::bytes>(element)) {
2031+
py::bytes b = element.cast<py::bytes>();
2032+
if (PyBytes_GET_SIZE(b.ptr()) != 16) {
2033+
ThrowStdException("UUID binary data must be exactly 16 bytes long.");
2034+
}
2035+
std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
2036+
}
2037+
else if (py::isinstance(element, uuid_class)) {
2038+
py::bytes b = element.attr("bytes_le").cast<py::bytes>();
2039+
std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
2040+
}
2041+
else {
2042+
ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
2043+
}
2044+
guidArray[i].Data1 = (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
2045+
(static_cast<uint32_t>(uuid_bytes[2]) << 16) |
2046+
(static_cast<uint32_t>(uuid_bytes[1]) << 8) |
2047+
(static_cast<uint32_t>(uuid_bytes[0]));
2048+
guidArray[i].Data2 = (static_cast<uint16_t>(uuid_bytes[5]) << 8) |
2049+
(static_cast<uint16_t>(uuid_bytes[4]));
2050+
guidArray[i].Data3 = (static_cast<uint16_t>(uuid_bytes[7]) << 8) |
2051+
(static_cast<uint16_t>(uuid_bytes[6]));
2052+
std::memcpy(guidArray[i].Data4, uuid_bytes.data() + 8, 8);
2053+
strLenOrIndArray[i] = sizeof(SQLGUID);
2054+
}
2055+
dataPtr = guidArray;
2056+
bufferLength = sizeof(SQLGUID);
2057+
break;
2058+
}
20162059
default: {
20172060
ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType));
20182061
}
@@ -3229,6 +3272,11 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32293272
break;
32303273
}
32313274
case SQL_GUID: {
3275+
SQLLEN indicator = buffers.indicators[col - 1][i];
3276+
if (indicator == SQL_NULL_DATA) {
3277+
row.append(py::none());
3278+
break;
3279+
}
32323280
SQLGUID* guidValue = &buffers.guidBuffers[col - 1][i];
32333281
uint8_t reordered[16];
32343282
reordered[0] = ((char*)&guidValue->Data1)[3];

tests/test_004_cursor.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7358,6 +7358,97 @@ def test_extreme_uuids(cursor, db_connection):
73587358
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
73597359
db_connection.commit()
73607360

7361+
def test_executemany_uuid_insert_and_select(cursor, db_connection):
7362+
"""Test inserting multiple UUIDs using executemany and verifying retrieval."""
7363+
table_name = "#pytest_uuid_executemany"
7364+
7365+
try:
7366+
# Drop and create a temporary table for the test
7367+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
7368+
cursor.execute(f"""
7369+
CREATE TABLE {table_name} (
7370+
id UNIQUEIDENTIFIER PRIMARY KEY,
7371+
description NVARCHAR(50)
7372+
)
7373+
""")
7374+
db_connection.commit()
7375+
7376+
# Generate data for insertion
7377+
data_to_insert = [(uuid.uuid4(), f"Item {i}") for i in range(5)]
7378+
7379+
# Insert all data with a single call to executemany
7380+
sql = f"INSERT INTO {table_name} (id, description) VALUES (?, ?)"
7381+
cursor.executemany(sql, data_to_insert)
7382+
db_connection.commit()
7383+
7384+
# Verify the number of rows inserted
7385+
assert cursor.rowcount == 5, f"Expected 5 rows inserted, but got {cursor.rowcount}"
7386+
7387+
# Fetch all data from the table
7388+
cursor.execute(f"SELECT id, description FROM {table_name} ORDER BY description")
7389+
rows = cursor.fetchall()
7390+
7391+
# Verify the number of fetched rows
7392+
assert len(rows) == len(data_to_insert), "Number of fetched rows does not match."
7393+
7394+
# Compare inserted and retrieved rows by index
7395+
for i, (retrieved_uuid, retrieved_desc) in enumerate(rows):
7396+
expected_uuid, expected_desc = data_to_insert[i]
7397+
7398+
# Assert the type is correct
7399+
if isinstance(retrieved_uuid, str):
7400+
retrieved_uuid = uuid.UUID(retrieved_uuid) # convert if driver returns str
7401+
7402+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected uuid.UUID, got {type(retrieved_uuid)}"
7403+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}': expected {expected_uuid}, got {retrieved_uuid}"
7404+
assert retrieved_desc == expected_desc, f"Description mismatch: expected {expected_desc}, got {retrieved_desc}"
7405+
7406+
finally:
7407+
# Clean up the temporary table
7408+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
7409+
db_connection.commit()
7410+
7411+
def test_executemany_uuid_roundtrip_fixed_value(cursor, db_connection):
7412+
"""Ensure a fixed canonical UUID round-trips exactly."""
7413+
table_name = "#pytest_uuid_fixed"
7414+
try:
7415+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
7416+
cursor.execute(f"""
7417+
CREATE TABLE {table_name} (
7418+
id UNIQUEIDENTIFIER,
7419+
description NVARCHAR(50)
7420+
)
7421+
""")
7422+
db_connection.commit()
7423+
7424+
fixed_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678")
7425+
description = "FixedUUID"
7426+
7427+
# Insert via executemany
7428+
cursor.executemany(
7429+
f"INSERT INTO {table_name} (id, description) VALUES (?, ?)",
7430+
[(fixed_uuid, description)]
7431+
)
7432+
db_connection.commit()
7433+
7434+
# Fetch back
7435+
cursor.execute(f"SELECT id, description FROM {table_name} WHERE description = ?", description)
7436+
row = cursor.fetchone()
7437+
retrieved_uuid, retrieved_desc = row
7438+
7439+
# Ensure type and value are correct
7440+
if isinstance(retrieved_uuid, str):
7441+
retrieved_uuid = uuid.UUID(retrieved_uuid)
7442+
7443+
assert isinstance(retrieved_uuid, uuid.UUID)
7444+
assert retrieved_uuid == fixed_uuid
7445+
assert str(retrieved_uuid) == str(fixed_uuid)
7446+
assert retrieved_desc == description
7447+
7448+
finally:
7449+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
7450+
db_connection.commit()
7451+
73617452
def test_decimal_separator_with_multiple_values(cursor, db_connection):
73627453
"""Test decimal separator with multiple different decimal values"""
73637454
original_separator = mssql_python.getDecimalSeparator()
@@ -10898,6 +10989,59 @@ def test_decimal_separator_calculations(cursor, db_connection):
1089810989

1089910990
# Cleanup
1090010991
cursor.execute("DROP TABLE IF EXISTS #pytest_decimal_calc_test")
10992+
db_connection.commit()
10993+
10994+
def test_executemany_with_uuids(cursor, db_connection):
10995+
"""Test inserting multiple rows with UUIDs and None using executemany."""
10996+
table_name = "#pytest_uuid_batch"
10997+
try:
10998+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
10999+
cursor.execute(f"""
11000+
CREATE TABLE {table_name} (
11001+
id UNIQUEIDENTIFIER,
11002+
description NVARCHAR(50)
11003+
)
11004+
""")
11005+
db_connection.commit()
11006+
11007+
# Prepare test data: mix of UUIDs and None
11008+
test_data = [
11009+
[uuid.uuid4(), "Item 1"],
11010+
[uuid.uuid4(), "Item 2"],
11011+
[None, "Item 3"],
11012+
[uuid.uuid4(), "Item 4"],
11013+
[None, "Item 5"]
11014+
]
11015+
11016+
# Map descriptions to original UUIDs for O(1) lookup
11017+
uuid_map = {desc: uid for uid, desc in test_data}
11018+
11019+
# Execute batch insert
11020+
cursor.executemany(f"INSERT INTO {table_name} (id, description) VALUES (?, ?)", test_data)
11021+
cursor.connection.commit()
11022+
11023+
# Fetch and verify
11024+
cursor.execute(f"SELECT id, description FROM {table_name}")
11025+
rows = cursor.fetchall()
11026+
11027+
assert len(rows) == len(test_data), "Number of fetched rows does not match inserted rows."
11028+
11029+
for retrieved_uuid, retrieved_desc in rows:
11030+
expected_uuid = uuid_map[retrieved_desc]
11031+
11032+
if expected_uuid is None:
11033+
assert retrieved_uuid is None, f"Expected None for '{retrieved_desc}', got {retrieved_uuid}"
11034+
else:
11035+
# Convert string to UUID if needed
11036+
if isinstance(retrieved_uuid, str):
11037+
retrieved_uuid = uuid.UUID(retrieved_uuid)
11038+
11039+
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected UUID, got {type(retrieved_uuid)}"
11040+
assert retrieved_uuid == expected_uuid, f"UUID mismatch for '{retrieved_desc}'"
11041+
11042+
finally:
11043+
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
11044+
db_connection.commit()
1090111045

1090211046
def test_nvarcharmax_executemany_streaming(cursor, db_connection):
1090311047
"""Streaming insert + fetch > 4k NVARCHAR(MAX) using executemany with all fetch modes."""

0 commit comments

Comments
 (0)