diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 52fcbfd2..65e04e8c 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -396,7 +396,7 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg if param is None: logger.debug("_map_sql_type: NULL parameter - index=%d", i) return ( - ddbc_sql_const.SQL_VARCHAR.value, + ddbc_sql_const.SQL_UNKNOWN_TYPE.value, ddbc_sql_const.SQL_C_DEFAULT.value, 1, 0, @@ -2208,6 +2208,16 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s min_val=min_val, max_val=max_val, ) + + # For executemany with all-NULL columns, SQL_UNKNOWN_TYPE doesn't work + # with array binding. Fall back to SQL_VARCHAR as a safe default. + if ( + sample_value is None + and paraminfo.paramSQLType == ddbc_sql_const.SQL_UNKNOWN_TYPE.value + ): + paraminfo.paramSQLType = ddbc_sql_const.SQL_VARCHAR.value + paraminfo.columnSize = 1 + # Special handling for binary data in auto-detected types if paraminfo.paramSQLType in ( ddbc_sql_const.SQL_BINARY.value, diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 0933d4fa..905b0c61 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -471,14 +471,21 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, hStmt, static_cast(paramIndex + 1), &describedType, &describedSize, &describedDigits, &nullable); if (!SQL_SUCCEEDED(rc)) { - LOG("BindParameters: SQLDescribeParam failed for " - "param[%d] (NULL parameter) - SQLRETURN=%d", - paramIndex, rc); - return rc; + // SQLDescribeParam can fail for generic SELECT statements where + // no table column is referenced. Fall back to SQL_VARCHAR as a safe + // default. + LOG_WARNING("BindParameters: SQLDescribeParam failed for " + "param[%d] (NULL parameter) - SQLRETURN=%d, falling back to " + "SQL_VARCHAR", + paramIndex, rc); + sqlType = SQL_VARCHAR; + columnSize = 1; + decimalDigits = 0; + } else { + sqlType = describedType; + columnSize = describedSize; + decimalDigits = describedDigits; } - sqlType = describedType; - columnSize = describedSize; - decimalDigits = describedDigits; } dataPtr = nullptr; strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -4048,7 +4055,8 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { break; case SQL_SS_UDT: rowSize += (static_cast(columnSize) == SQL_NO_TOTAL || columnSize == 0) - ? SQL_MAX_LOB_SIZE : columnSize; + ? SQL_MAX_LOB_SIZE + : columnSize; break; case SQL_BINARY: case SQL_VARBINARY: @@ -4112,8 +4120,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || - dataType == SQL_SS_UDT) && + dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { lobColumns.push_back(i + 1); // 1-based } @@ -4252,8 +4259,7 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || - dataType == SQL_SS_UDT) && + dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { lobColumns.push_back(i + 1); // 1-based } diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 80995b6e..fa291c64 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -658,6 +658,23 @@ def test_varbinary_full_capacity(cursor, db_connection): db_connection.commit() +def test_execute_none_into_varbinary_column(cursor, db_connection): + from mssql_python.constants import ConstantsDDBC + + drop_table_if_exists(cursor, "#test_varbinary_null") + try: + cursor.execute("CREATE TABLE #test_varbinary_null (data VARBINARY(100))") + db_connection.commit() + cursor.setinputsizes([(ConstantsDDBC.SQL_VARBINARY.value, 100, 0)]) + cursor.execute("INSERT INTO #test_varbinary_null (data) VALUES (?)", None) + db_connection.commit() + cursor.execute("SELECT data FROM #test_varbinary_null") + row = cursor.fetchone() + assert row[0] is None + finally: + drop_table_if_exists(cursor, "#test_varbinary_null") + + def test_varbinary_max(cursor, db_connection): """Test SQL_VARBINARY with MAX length""" try: