Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,49 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt,
bufferLength = sizeof(SQL_NUMERIC_STRUCT);
break;
}
case SQL_C_GUID: {
SQLGUID* guidArray = AllocateParamBufferArray<SQLGUID>(tempBuffers, paramSetSize);
strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);

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<unsigned char, 16> uuid_bytes;
if (element.is_none()) {
std::memset(&guidArray[i], 0, sizeof(SQLGUID));
strLenOrIndArray[i] = SQL_NULL_DATA;
continue;
}
else if (py::isinstance<py::bytes>(element)) {
py::bytes b = element.cast<py::bytes>();
if (PyBytes_GET_SIZE(b.ptr()) != 16) {
ThrowStdException("UUID binary data must be exactly 16 bytes long.");
}
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<py::bytes>();
std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
}
else {
ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
}
guidArray[i].Data1 = (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
(static_cast<uint32_t>(uuid_bytes[2]) << 16) |
(static_cast<uint32_t>(uuid_bytes[1]) << 8) |
(static_cast<uint32_t>(uuid_bytes[0]));
guidArray[i].Data2 = (static_cast<uint16_t>(uuid_bytes[5]) << 8) |
(static_cast<uint16_t>(uuid_bytes[4]));
guidArray[i].Data3 = (static_cast<uint16_t>(uuid_bytes[7]) << 8) |
(static_cast<uint16_t>(uuid_bytes[6]));
std::memcpy(guidArray[i].Data4, uuid_bytes.data() + 8, 8);
strLenOrIndArray[i] = sizeof(SQLGUID);
}
dataPtr = guidArray;
bufferLength = sizeof(SQLGUID);
break;
}
default: {
ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType));
}
Expand Down Expand Up @@ -3229,6 +3272,11 @@ 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];
uint8_t reordered[16];
reordered[0] = ((char*)&guidValue->Data1)[3];
Expand Down
144 changes: 144 additions & 0 deletions tests/test_004_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7246,6 +7246,97 @@ 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 = [(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 (?, ?)"
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} 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."

# 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 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
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()
Expand Down Expand Up @@ -10786,6 +10877,59 @@ def test_decimal_separator_calculations(cursor, db_connection):

# Cleanup
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"]
]

# 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()

# 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 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()

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