Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 0 additions & 1 deletion mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,7 +1680,6 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None:
sample_value = sample_row[col_index]
else:
sample_value = self._select_best_sample_value(column)

dummy_row = list(sample_row)
paraminfo = self._create_parameter_types_list(
sample_value, param_info, dummy_row, col_index, min_val=min_val, max_val=max_val
Expand Down
50 changes: 49 additions & 1 deletion mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,49 @@
bufferLength = sizeof(SQL_NUMERIC_STRUCT);
break;
}
case SQL_C_GUID: {
SQLGUID* guidArray = AllocateParamBufferArray<SQLGUID>(tempBuffers, paramSetSize);
strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);

py::module_ uuid_mod = py::module_::import("uuid");
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 @@ -2713,7 +2756,7 @@
}
break;
}
#endif
#endif
default:
std::ostringstream errorString;
errorString << "Unsupported data type for column - " << columnName << ", Type - "
Expand Down Expand Up @@ -3095,6 +3138,11 @@
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
103 changes: 103 additions & 0 deletions tests/test_004_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7146,6 +7146,62 @@ 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 = []
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_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 @@ -10560,6 +10616,53 @@ 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"]
]

# 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 row in rows:
retrieved_uuid, retrieved_desc = row
for original_uuid, original_desc in test_data:
if original_desc == retrieved_desc:
if original_uuid is None:
assert retrieved_uuid is None, f"Expected None for '{retrieved_desc}', got {retrieved_uuid}"
else:
assert isinstance(retrieved_uuid, uuid.UUID), f"Expected UUID, got {type(retrieved_uuid)}"
assert retrieved_uuid == original_uuid, f"UUID mismatch for '{retrieved_desc}'"
break
finally:
cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
db_connection.commit()

def test_close(db_connection):
"""Test closing the cursor"""
Expand Down