Skip to content

Commit 1b059c3

Browse files
authored
Merge branch 'main' into saumya/uuid
2 parents e9aa833 + 61ed764 commit 1b059c3

File tree

3 files changed

+93
-55
lines changed

3 files changed

+93
-55
lines changed

mssql_python/cursor.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -404,28 +404,6 @@ def _map_sql_type(self, param, parameters_list, i):
404404
False,
405405
)
406406

407-
if isinstance(param, bytes):
408-
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
409-
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
410-
return (
411-
ddbc_sql_const.SQL_VARBINARY.value,
412-
ddbc_sql_const.SQL_C_BINARY.value,
413-
len(param),
414-
0,
415-
False,
416-
)
417-
418-
if isinstance(param, bytearray):
419-
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
420-
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
421-
return (
422-
ddbc_sql_const.SQL_VARBINARY.value,
423-
ddbc_sql_const.SQL_C_BINARY.value,
424-
len(param),
425-
0,
426-
False,
427-
)
428-
429407
if isinstance(param, uuid.UUID):
430408
return (
431409
ddbc_sql_const.SQL_GUID.value,
@@ -435,6 +413,25 @@ def _map_sql_type(self, param, parameters_list, i):
435413
False,
436414
)
437415

416+
if isinstance(param, (bytes, bytearray)):
417+
length = len(param)
418+
if length > 8000: # Use VARBINARY(MAX) for large blobs
419+
return (
420+
ddbc_sql_const.SQL_VARBINARY.value,
421+
ddbc_sql_const.SQL_C_BINARY.value,
422+
0,
423+
0,
424+
True
425+
)
426+
else: # Small blobs → direct binding
427+
return (
428+
ddbc_sql_const.SQL_VARBINARY.value,
429+
ddbc_sql_const.SQL_C_BINARY.value,
430+
max(length, 1),
431+
0,
432+
False
433+
)
434+
438435
if isinstance(param, datetime.datetime):
439436
return (
440437
ddbc_sql_const.SQL_TIMESTAMP.value,

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,29 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params,
254254
!py::isinstance<py::bytes>(param)) {
255255
ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
256256
}
257-
std::string* strParam =
258-
AllocateParamBuffer<std::string>(paramBuffers, param.cast<std::string>());
259-
if (strParam->size() > 8192 /* TODO: Fix max length */) {
260-
ThrowStdException(
261-
"Streaming parameters is not yet supported. Parameter size"
262-
" must be less than 8192 bytes");
263-
}
264-
dataPtr = const_cast<void*>(static_cast<const void*>(strParam->c_str()));
265-
bufferLength = strParam->size() + 1 /* null terminator */;
266-
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
267-
*strLenOrIndPtr = SQL_NTS;
257+
if (paramInfo.isDAE) {
258+
// Deferred execution for VARBINARY(MAX)
259+
LOG("Parameter[{}] is marked for DAE streaming (VARBINARY(MAX))", paramIndex);
260+
dataPtr = const_cast<void*>(reinterpret_cast<const void*>(&paramInfos[paramIndex]));
261+
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
262+
*strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0);
263+
bufferLength = 0;
264+
} else {
265+
// small binary
266+
std::string binData;
267+
if (py::isinstance<py::bytes>(param)) {
268+
binData = param.cast<std::string>();
269+
} else {
270+
// bytearray
271+
binData = std::string(reinterpret_cast<const char*>(PyByteArray_AsString(param.ptr())),
272+
PyByteArray_Size(param.ptr()));
273+
}
274+
std::string* binBuffer = AllocateParamBuffer<std::string>(paramBuffers, binData);
275+
dataPtr = const_cast<void*>(static_cast<const void*>(binBuffer->data()));
276+
bufferLength = static_cast<SQLLEN>(binBuffer->size());
277+
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
278+
*strLenOrIndPtr = bufferLength;
279+
}
268280
break;
269281
}
270282
case SQL_C_WCHAR: {
@@ -1294,6 +1306,20 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle,
12941306
} else {
12951307
ThrowStdException("Unsupported C type for str in DAE");
12961308
}
1309+
} else if (py::isinstance<py::bytes>(pyObj) || py::isinstance<py::bytearray>(pyObj)) {
1310+
py::bytes b = pyObj.cast<py::bytes>();
1311+
std::string s = b;
1312+
const char* dataPtr = s.data();
1313+
size_t totalBytes = s.size();
1314+
const size_t chunkSize = DAE_CHUNK_SIZE;
1315+
for (size_t offset = 0; offset < totalBytes; offset += chunkSize) {
1316+
size_t len = std::min(chunkSize, totalBytes - offset);
1317+
rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
1318+
if (!SQL_SUCCEEDED(rc)) {
1319+
LOG("SQLPutData failed at offset {} of {}", offset, totalBytes);
1320+
return rc;
1321+
}
1322+
}
12971323
} else {
12981324
ThrowStdException("DAE only supported for str or bytes");
12991325
}

tests/test_004_cursor.py

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6080,40 +6080,25 @@ def test_binary_data_over_8000_bytes(cursor, db_connection):
60806080
"""Test binary data larger than 8000 bytes - document current driver limitations"""
60816081
try:
60826082
# Create test table with VARBINARY(MAX) to handle large data
6083-
drop_table_if_exists(cursor, "#pytest_large_binary")
6083+
drop_table_if_exists(cursor, "#pytest_small_binary")
60846084
cursor.execute("""
6085-
CREATE TABLE #pytest_large_binary (
6085+
CREATE TABLE #pytest_small_binary (
60866086
id INT,
60876087
large_binary VARBINARY(MAX)
60886088
)
60896089
""")
60906090

6091-
# Test the current driver limitations:
6092-
# 1. Parameters cannot be > 8192 bytes
6093-
# 2. Fetch buffer is limited to 4096 bytes
6094-
6095-
large_data = b'A' * 10000 # 10,000 bytes - exceeds parameter limit
6096-
6097-
# This should fail with the current driver parameter limitation
6098-
try:
6099-
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (1, large_data))
6100-
pytest.fail("Expected streaming parameter error for data > 8192 bytes")
6101-
except RuntimeError as e:
6102-
error_msg = str(e)
6103-
assert "Streaming parameters is not yet supported" in error_msg, f"Expected streaming parameter error, got: {e}"
6104-
assert "8192 bytes" in error_msg, f"Expected 8192 bytes limit mentioned, got: {e}"
6105-
61066091
# Test data that fits within both parameter and fetch limits (< 4096 bytes)
61076092
medium_data = b'B' * 3000 # 3,000 bytes - under both limits
61086093
small_data = b'C' * 1000 # 1,000 bytes - well under limits
61096094

61106095
# These should work fine
6111-
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (1, medium_data))
6112-
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (2, small_data))
6096+
cursor.execute("INSERT INTO #pytest_small_binary VALUES (?, ?)", (1, medium_data))
6097+
cursor.execute("INSERT INTO #pytest_small_binary VALUES (?, ?)", (2, small_data))
61136098
db_connection.commit()
61146099

61156100
# Verify the data was inserted correctly
6116-
cursor.execute("SELECT id, large_binary FROM #pytest_large_binary ORDER BY id")
6101+
cursor.execute("SELECT id, large_binary FROM #pytest_small_binary ORDER BY id")
61176102
results = cursor.fetchall()
61186103

61196104
assert len(results) == 2, f"Expected 2 rows, got {len(results)}"
@@ -6122,14 +6107,44 @@ def test_binary_data_over_8000_bytes(cursor, db_connection):
61226107
assert results[0][1] == medium_data, "Medium binary data mismatch"
61236108
assert results[1][1] == small_data, "Small binary data mismatch"
61246109

6125-
print("Note: Driver currently limits parameters to < 8192 bytes and fetch buffer to 4096 bytes.")
6110+
print("Small/medium binary data inserted and verified successfully.")
6111+
except Exception as e:
6112+
pytest.fail(f"Small binary data insertion test failed: {e}")
6113+
finally:
6114+
drop_table_if_exists(cursor, "#pytest_small_binary")
6115+
db_connection.commit()
6116+
6117+
def test_binary_data_large(cursor, db_connection):
6118+
"""Test insertion of binary data larger than 8000 bytes with streaming support."""
6119+
try:
6120+
drop_table_if_exists(cursor, "#pytest_large_binary")
6121+
cursor.execute("""
6122+
CREATE TABLE #pytest_large_binary (
6123+
id INT PRIMARY KEY,
6124+
large_binary VARBINARY(MAX)
6125+
)
6126+
""")
6127+
6128+
# Large binary data > 8000 bytes
6129+
large_data = b'A' * 10000 # 10 KB
6130+
cursor.execute("INSERT INTO #pytest_large_binary (id, large_binary) VALUES (?, ?)", (1, large_data))
6131+
db_connection.commit()
6132+
print("Inserted large binary data (>8000 bytes) successfully.")
6133+
6134+
# commented out for now
6135+
# cursor.execute("SELECT large_binary FROM #pytest_large_binary WHERE id=1")
6136+
# result = cursor.fetchone()
6137+
# assert result[0] == large_data, f"Large binary data mismatch, got {len(result[0])} bytes"
6138+
6139+
# print("Large binary data (>8000 bytes) inserted and verified successfully.")
61266140

61276141
except Exception as e:
6128-
pytest.fail(f"Binary data over 8000 bytes test failed: {e}")
6142+
pytest.fail(f"Large binary data insertion test failed: {e}")
61296143
finally:
61306144
drop_table_if_exists(cursor, "#pytest_large_binary")
61316145
db_connection.commit()
61326146

6147+
61336148
def test_all_empty_binaries(cursor, db_connection):
61346149
"""Test table with only empty binary values"""
61356150
try:

0 commit comments

Comments
 (0)