diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 7a6dbfaa..048ea43a 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -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 ( diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 789d3863..464eca4b 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -254,17 +254,29 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, !py::isinstance(param)) { ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); } - std::string* strParam = - AllocateParamBuffer(paramBuffers, param.cast()); - 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(static_cast(strParam->c_str())); - bufferLength = strParam->size() + 1 /* null terminator */; - strLenOrIndPtr = AllocateParamBuffer(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(reinterpret_cast(¶mInfos[paramIndex])); + strLenOrIndPtr = AllocateParamBuffer(paramBuffers); + *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0); + bufferLength = 0; + } else { + // small binary + std::string binData; + if (py::isinstance(param)) { + binData = param.cast(); + } else { + // bytearray + binData = std::string(reinterpret_cast(PyByteArray_AsString(param.ptr())), + PyByteArray_Size(param.ptr())); + } + std::string* binBuffer = AllocateParamBuffer(paramBuffers, binData); + dataPtr = const_cast(static_cast(binBuffer->data())); + bufferLength = static_cast(binBuffer->size()); + strLenOrIndPtr = AllocateParamBuffer(paramBuffers); + *strLenOrIndPtr = bufferLength; + } break; } case SQL_C_WCHAR: { @@ -1267,6 +1279,20 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, } else { ThrowStdException("Unsupported C type for str in DAE"); } + } else if (py::isinstance(pyObj) || py::isinstance(pyObj)) { + py::bytes b = pyObj.cast(); + 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(len)); + if (!SQL_SUCCEEDED(rc)) { + LOG("SQLPutData failed at offset {} of {}", offset, totalBytes); + return rc; + } + } } else { ThrowStdException("DAE only supported for str or bytes"); } diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 1f6d6630..74150910 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -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)}" @@ -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: