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
39 changes: 18 additions & 21 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,27 +404,24 @@ def _map_sql_type(self, param, parameters_list, i):
False,
)

if isinstance(param, bytes):
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
return (
ddbc_sql_const.SQL_VARBINARY.value,
ddbc_sql_const.SQL_C_BINARY.value,
len(param),
0,
False,
)

if isinstance(param, bytearray):
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
return (
ddbc_sql_const.SQL_VARBINARY.value,
ddbc_sql_const.SQL_C_BINARY.value,
len(param),
0,
False,
)
if isinstance(param, (bytes, bytearray)):
length = len(param)
if length > 8000: # Use VARBINARY(MAX) for large blobs
return (
ddbc_sql_const.SQL_VARBINARY.value,
ddbc_sql_const.SQL_C_BINARY.value,
0,
0,
True
)
else: # Small blobs → direct binding
return (
ddbc_sql_const.SQL_VARBINARY.value,
ddbc_sql_const.SQL_C_BINARY.value,
max(length, 1),
0,
False
)

if isinstance(param, datetime.datetime):
return (
Expand Down
48 changes: 37 additions & 11 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,17 +254,29 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params,
!py::isinstance<py::bytes>(param)) {
ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
}
std::string* strParam =
AllocateParamBuffer<std::string>(paramBuffers, param.cast<std::string>());
if (strParam->size() > 8192 /* TODO: Fix max length */) {
ThrowStdException(
"Streaming parameters is not yet supported. Parameter size"
" must be less than 8192 bytes");
}
dataPtr = const_cast<void*>(static_cast<const void*>(strParam->c_str()));
bufferLength = strParam->size() + 1 /* null terminator */;
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
*strLenOrIndPtr = SQL_NTS;
if (paramInfo.isDAE) {
// Deferred execution for VARBINARY(MAX)
LOG("Parameter[{}] is marked for DAE streaming (VARBINARY(MAX))", paramIndex);
dataPtr = const_cast<void*>(reinterpret_cast<const void*>(&paramInfos[paramIndex]));
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
*strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0);
bufferLength = 0;
} else {
// small binary
std::string binData;
if (py::isinstance<py::bytes>(param)) {
binData = param.cast<std::string>();
} else {
// bytearray
binData = std::string(reinterpret_cast<const char*>(PyByteArray_AsString(param.ptr())),
PyByteArray_Size(param.ptr()));
}
std::string* binBuffer = AllocateParamBuffer<std::string>(paramBuffers, binData);
dataPtr = const_cast<void*>(static_cast<const void*>(binBuffer->data()));
bufferLength = static_cast<SQLLEN>(binBuffer->size());
strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
*strLenOrIndPtr = bufferLength;
}
break;
}
case SQL_C_WCHAR: {
Expand Down Expand Up @@ -1267,6 +1279,20 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle,
} else {
ThrowStdException("Unsupported C type for str in DAE");
}
} else if (py::isinstance<py::bytes>(pyObj) || py::isinstance<py::bytearray>(pyObj)) {
py::bytes b = pyObj.cast<py::bytes>();
std::string s = b;
const char* dataPtr = s.data();
size_t totalBytes = s.size();
const size_t chunkSize = DAE_CHUNK_SIZE;
for (size_t offset = 0; offset < totalBytes; offset += chunkSize) {
size_t len = std::min(chunkSize, totalBytes - offset);
rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
if (!SQL_SUCCEEDED(rc)) {
LOG("SQLPutData failed at offset {} of {}", offset, totalBytes);
return rc;
}
}
} else {
ThrowStdException("DAE only supported for str or bytes");
}
Expand Down
59 changes: 37 additions & 22 deletions tests/test_004_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6079,40 +6079,25 @@ def test_binary_data_over_8000_bytes(cursor, db_connection):
"""Test binary data larger than 8000 bytes - document current driver limitations"""
try:
# Create test table with VARBINARY(MAX) to handle large data
drop_table_if_exists(cursor, "#pytest_large_binary")
drop_table_if_exists(cursor, "#pytest_small_binary")
cursor.execute("""
CREATE TABLE #pytest_large_binary (
CREATE TABLE #pytest_small_binary (
id INT,
large_binary VARBINARY(MAX)
)
""")

# Test the current driver limitations:
# 1. Parameters cannot be > 8192 bytes
# 2. Fetch buffer is limited to 4096 bytes

large_data = b'A' * 10000 # 10,000 bytes - exceeds parameter limit

# This should fail with the current driver parameter limitation
try:
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (1, large_data))
pytest.fail("Expected streaming parameter error for data > 8192 bytes")
except RuntimeError as e:
error_msg = str(e)
assert "Streaming parameters is not yet supported" in error_msg, f"Expected streaming parameter error, got: {e}"
assert "8192 bytes" in error_msg, f"Expected 8192 bytes limit mentioned, got: {e}"

# Test data that fits within both parameter and fetch limits (< 4096 bytes)
medium_data = b'B' * 3000 # 3,000 bytes - under both limits
small_data = b'C' * 1000 # 1,000 bytes - well under limits

# These should work fine
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (1, medium_data))
cursor.execute("INSERT INTO #pytest_large_binary VALUES (?, ?)", (2, small_data))
cursor.execute("INSERT INTO #pytest_small_binary VALUES (?, ?)", (1, medium_data))
cursor.execute("INSERT INTO #pytest_small_binary VALUES (?, ?)", (2, small_data))
db_connection.commit()

# Verify the data was inserted correctly
cursor.execute("SELECT id, large_binary FROM #pytest_large_binary ORDER BY id")
cursor.execute("SELECT id, large_binary FROM #pytest_small_binary ORDER BY id")
results = cursor.fetchall()

assert len(results) == 2, f"Expected 2 rows, got {len(results)}"
Expand All @@ -6121,14 +6106,44 @@ def test_binary_data_over_8000_bytes(cursor, db_connection):
assert results[0][1] == medium_data, "Medium binary data mismatch"
assert results[1][1] == small_data, "Small binary data mismatch"

print("Note: Driver currently limits parameters to < 8192 bytes and fetch buffer to 4096 bytes.")
print("Small/medium binary data inserted and verified successfully.")
except Exception as e:
pytest.fail(f"Small binary data insertion test failed: {e}")
finally:
drop_table_if_exists(cursor, "#pytest_small_binary")
db_connection.commit()

def test_binary_data_large(cursor, db_connection):
"""Test insertion of binary data larger than 8000 bytes with streaming support."""
try:
drop_table_if_exists(cursor, "#pytest_large_binary")
cursor.execute("""
CREATE TABLE #pytest_large_binary (
id INT PRIMARY KEY,
large_binary VARBINARY(MAX)
)
""")

# Large binary data > 8000 bytes
large_data = b'A' * 10000 # 10 KB
cursor.execute("INSERT INTO #pytest_large_binary (id, large_binary) VALUES (?, ?)", (1, large_data))
db_connection.commit()
print("Inserted large binary data (>8000 bytes) successfully.")

# commented out for now
# cursor.execute("SELECT large_binary FROM #pytest_large_binary WHERE id=1")
# result = cursor.fetchone()
# assert result[0] == large_data, f"Large binary data mismatch, got {len(result[0])} bytes"

# print("Large binary data (>8000 bytes) inserted and verified successfully.")

except Exception as e:
pytest.fail(f"Binary data over 8000 bytes test failed: {e}")
pytest.fail(f"Large binary data insertion test failed: {e}")
finally:
drop_table_if_exists(cursor, "#pytest_large_binary")
db_connection.commit()


def test_all_empty_binaries(cursor, db_connection):
"""Test table with only empty binary values"""
try:
Expand Down