diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index dcf96260..fe3bdbcb 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -2924,9 +2924,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p row.append( FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false, charEncoding)); } else { - // Multiply by 4 because utf8 conversion by the driver might - // turn varchar(x) into up to 3*x (maybe 4*x?) bytes. - uint64_t fetchBufferSize = 4 * columnSize + 1 /* null-termination */; + uint64_t fetchBufferSize = columnSize + 1 /* null-termination */; std::vector dataBuffer(fetchBufferSize); SQLLEN dataLen; ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, dataBuffer.data(), dataBuffer.size(), @@ -2955,15 +2953,12 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p row.append(raw_bytes); } } else { - // Reaching this case indicates an error in mssql_python. - // Theoretically, we could still compensate by calling SQLGetData or - // FetchLobColumnData more often, but then we would still have to process - // the data we already got from the above call to SQLGetData. - // Better to throw an exception and fix the code than to risk returning corrupted data. - ThrowStdException( - "Internal error: SQLGetData returned data " - "larger than expected for CHAR column" - ); + // Buffer too small, fallback to streaming + LOG("SQLGetData: CHAR column %d data truncated " + "(buffer_size=%zu), using streaming LOB", + i, dataBuffer.size()); + row.append(FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false, + charEncoding)); } } else if (dataLen == SQL_NULL_DATA) { LOG("SQLGetData: Column %d is NULL (CHAR)", i); @@ -3000,7 +2995,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p case SQL_WCHAR: case SQL_WVARCHAR: case SQL_WLONGVARCHAR: { - if (columnSize == SQL_NO_TOTAL || columnSize == 0 || columnSize > 4000) { + if (columnSize == SQL_NO_TOTAL || columnSize > 4000) { LOG("SQLGetData: Streaming LOB for column %d (SQL_C_WCHAR) " "- columnSize=%lu", i, (unsigned long)columnSize); @@ -3029,15 +3024,12 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p "length=%lu for column %d", (unsigned long)numCharsInData, i); } else { - // Reaching this case indicates an error in mssql_python. - // Theoretically, we could still compensate by calling SQLGetData or - // FetchLobColumnData more often, but then we would still have to process - // the data we already got from the above call to SQLGetData. - // Better to throw an exception and fix the code than to risk returning corrupted data. - ThrowStdException( - "Internal error: SQLGetData returned data " - "larger than expected for WCHAR column" - ); + // Buffer too small, fallback to streaming + LOG("SQLGetData: NVARCHAR column %d data " + "truncated, using streaming LOB", + i); + row.append(FetchLobColumnData(hStmt, i, SQL_C_WCHAR, true, false, + "utf-16le")); } } else if (dataLen == SQL_NULL_DATA) { LOG("SQLGetData: Column %d is NULL (NVARCHAR)", i); @@ -3299,15 +3291,8 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p row.append(py::bytes( reinterpret_cast(dataBuffer.data()), dataLen)); } else { - // Reaching this case indicates an error in mssql_python. - // Theoretically, we could still compensate by calling SQLGetData or - // FetchLobColumnData more often, but then we would still have to process - // the data we already got from the above call to SQLGetData. - // Better to throw an exception and fix the code than to risk returning corrupted data. - ThrowStdException( - "Internal error: SQLGetData returned data " - "larger than expected for BINARY column" - ); + row.append( + FetchLobColumnData(hStmt, i, SQL_C_BINARY, false, true, "")); } } else if (dataLen == SQL_NULL_DATA) { row.append(py::none()); @@ -3449,9 +3434,7 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column // TODO: handle variable length data correctly. This logic wont // suffice HandleZeroColumnSizeAtFetch(columnSize); - // Multiply by 4 because utf8 conversion by the driver might - // turn varchar(x) into up to 3*x (maybe 4*x?) bytes. - uint64_t fetchBufferSize = 4 * columnSize + 1 /*null-terminator*/; + uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/; // TODO: For LONGVARCHAR/BINARY types, columnSize is returned as // 2GB-1 by SQLDescribeCol. So fetchBufferSize = 2GB. // fetchSize=1 if columnSize>1GB. So we'll allocate a vector of @@ -3597,7 +3580,8 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column // Fetch rows in batches // TODO: Move to anonymous namespace, since it is not used outside this file SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& columnNames, - py::list& rows, SQLUSMALLINT numCols, SQLULEN& numRowsFetched) { + py::list& rows, SQLUSMALLINT numCols, SQLULEN& numRowsFetched, + const std::vector& lobColumns) { LOG("FetchBatchData: Fetching data in batches"); SQLRETURN ret = SQLFetchScroll_ptr(hStmt, SQL_FETCH_NEXT, 0); if (ret == SQL_NO_DATA) { @@ -3616,28 +3600,19 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum SQLULEN columnSize; SQLULEN processedColumnSize; uint64_t fetchBufferSize; + bool isLob; }; std::vector columnInfos(numCols); for (SQLUSMALLINT col = 0; col < numCols; col++) { const auto& columnMeta = columnNames[col].cast(); columnInfos[col].dataType = columnMeta["DataType"].cast(); columnInfos[col].columnSize = columnMeta["ColumnSize"].cast(); + columnInfos[col].isLob = + std::find(lobColumns.begin(), lobColumns.end(), col + 1) != lobColumns.end(); columnInfos[col].processedColumnSize = columnInfos[col].columnSize; HandleZeroColumnSizeAtFetch(columnInfos[col].processedColumnSize); - switch (columnInfos[col].dataType) { - case SQL_CHAR: - case SQL_VARCHAR: - case SQL_LONGVARCHAR: - // Multiply by 4 because utf8 conversion by the driver might - // turn varchar(x) into up to 3*x (maybe 4*x?) bytes. - columnInfos[col].fetchBufferSize = - 4 * columnInfos[col].processedColumnSize + 1; // +1 for null terminator - break; - default: - columnInfos[col].fetchBufferSize = - columnInfos[col].processedColumnSize + 1; // +1 for null terminator - break; - } + columnInfos[col].fetchBufferSize = + columnInfos[col].processedColumnSize + 1; // +1 for null terminator } std::string decimalSeparator = GetDecimalSeparator(); // Cache decimal separator @@ -3655,6 +3630,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum columnInfosExt[col].columnSize = columnInfos[col].columnSize; columnInfosExt[col].processedColumnSize = columnInfos[col].processedColumnSize; columnInfosExt[col].fetchBufferSize = columnInfos[col].fetchBufferSize; + columnInfosExt[col].isLob = columnInfos[col].isLob; // Map data type to processor function (switch executed once per column, // not per cell) @@ -3763,7 +3739,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum // types) to just 10 (setup only) Note: Processor functions no // longer need to check for NULL since we do it above if (columnProcessors[col - 1] != nullptr) { - columnProcessors[col - 1](row, buffers, &columnInfosExt[col - 1], col, i); + columnProcessors[col - 1](row, buffers, &columnInfosExt[col - 1], col, i, hStmt); continue; } @@ -3940,9 +3916,7 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { case SQL_CHAR: case SQL_VARCHAR: case SQL_LONGVARCHAR: - // Multiply by 4 because utf8 conversion by the driver might - // turn varchar(x) into up to 3*x (maybe 4*x?) bytes. - rowSize += 4 * columnSize; + rowSize += columnSize; break; case SQL_SS_XML: case SQL_WCHAR: @@ -4096,7 +4070,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(intptr_t)fetchSize, 0); SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0); - ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched); + ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched, lobColumns); if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) { LOG("FetchMany_wrap: Error when fetching data - SQLRETURN=%d", ret); return ret; @@ -4229,7 +4203,7 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, while (ret != SQL_NO_DATA) { ret = - FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched); + FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched, lobColumns); if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) { LOG("FetchAll_wrap: Error when fetching data - SQLRETURN=%d", ret); return ret; diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 2b3c9e5e..391903ef 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -643,7 +643,7 @@ struct ColumnBuffers { // Performance: Column processor function type for fast type conversion // Using function pointers eliminates switch statement overhead in the hot loop typedef void (*ColumnProcessor)(PyObject* row, ColumnBuffers& buffers, const void* colInfo, - SQLUSMALLINT col, SQLULEN rowIdx); + SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt); // Extended column info struct for processor functions struct ColumnInfoExt { @@ -651,8 +651,14 @@ struct ColumnInfoExt { SQLULEN columnSize; SQLULEN processedColumnSize; uint64_t fetchBufferSize; + bool isLob; }; +// Forward declare FetchLobColumnData (defined in ddbc_bindings.cpp) - MUST be +// outside namespace +py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT col, SQLSMALLINT cType, bool isWideChar, + bool isBinary, const std::string& charEncoding = "utf-8"); + // Specialized column processors for each data type (eliminates switch in hot // loop) namespace ColumnProcessors { @@ -664,7 +670,7 @@ namespace ColumnProcessors { // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessInteger(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call (bypasses pybind11 overhead) PyObject* pyInt = PyLong_FromLong(buffers.intBuffers[col - 1][rowIdx]); if (!pyInt) { // Handle memory allocation failure @@ -679,7 +685,7 @@ inline void ProcessInteger(PyObject* row, ColumnBuffers& buffers, const void*, S // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessSmallInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call PyObject* pyInt = PyLong_FromLong(buffers.smallIntBuffers[col - 1][rowIdx]); if (!pyInt) { // Handle memory allocation failure @@ -694,7 +700,7 @@ inline void ProcessSmallInt(PyObject* row, ColumnBuffers& buffers, const void*, // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessBigInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call PyObject* pyInt = PyLong_FromLongLong(buffers.bigIntBuffers[col - 1][rowIdx]); if (!pyInt) { // Handle memory allocation failure @@ -709,7 +715,7 @@ inline void ProcessBigInt(PyObject* row, ColumnBuffers& buffers, const void*, SQ // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessTinyInt(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call PyObject* pyInt = PyLong_FromLong(buffers.charBuffers[col - 1][rowIdx]); if (!pyInt) { // Handle memory allocation failure @@ -724,7 +730,7 @@ inline void ProcessTinyInt(PyObject* row, ColumnBuffers& buffers, const void*, S // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessBit(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call (converts 0/1 to True/False) PyObject* pyBool = PyBool_FromLong(buffers.charBuffers[col - 1][rowIdx]); if (!pyBool) { // Handle memory allocation failure @@ -739,7 +745,7 @@ inline void ProcessBit(PyObject* row, ColumnBuffers& buffers, const void*, SQLUS // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessReal(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call PyObject* pyFloat = PyFloat_FromDouble(buffers.realBuffers[col - 1][rowIdx]); if (!pyFloat) { // Handle memory allocation failure @@ -754,7 +760,7 @@ inline void ProcessReal(PyObject* row, ColumnBuffers& buffers, const void*, SQLU // Performance: NULL check removed - handled centrally before processor is // called inline void ProcessDouble(PyObject* row, ColumnBuffers& buffers, const void*, SQLUSMALLINT col, - SQLULEN rowIdx) { + SQLULEN rowIdx, SQLHSTMT) { // Performance: Direct Python C API call PyObject* pyFloat = PyFloat_FromDouble(buffers.doubleBuffers[col - 1][rowIdx]); if (!pyFloat) { // Handle memory allocation failure @@ -769,7 +775,7 @@ inline void ProcessDouble(PyObject* row, ColumnBuffers& buffers, const void*, SQ // Performance: NULL/NO_TOTAL checks removed - handled centrally before // processor is called inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr, - SQLUSMALLINT col, SQLULEN rowIdx) { + SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) { const ColumnInfoExt* colInfo = static_cast(colInfoPtr); SQLLEN dataLen = buffers.indicators[col - 1][rowIdx]; @@ -789,7 +795,7 @@ inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colIn // Fast path: Data fits in buffer (not LOB or truncated) // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence // '<' - if (numCharsInData < colInfo->fetchBufferSize) { + if (!colInfo->isLob && numCharsInData < colInfo->fetchBufferSize) { // Performance: Direct Python C API call - create string from buffer PyObject* pyStr = PyUnicode_FromStringAndSize( reinterpret_cast( @@ -802,12 +808,9 @@ inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colIn PyList_SET_ITEM(row, col - 1, pyStr); } } else { - // Reaching this case indicates an error in mssql_python. - // This function is only called on columns bound by SQLBindCol. - // For such columns, the ODBC Driver does not allow us to compensate by - // fetching the remaining data using SQLGetData / FetchLobColumnData. - ThrowStdException( - "Internal error: CHAR/VARCHAR column data exceeds buffer size."); + // Slow path: LOB data requires separate fetch call + PyList_SET_ITEM(row, col - 1, + FetchLobColumnData(hStmt, col, SQL_C_CHAR, false, false).release().ptr()); } } @@ -815,7 +818,7 @@ inline void ProcessChar(PyObject* row, ColumnBuffers& buffers, const void* colIn // Performance: NULL/NO_TOTAL checks removed - handled centrally before // processor is called inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr, - SQLUSMALLINT col, SQLULEN rowIdx) { + SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) { const ColumnInfoExt* colInfo = static_cast(colInfoPtr); SQLLEN dataLen = buffers.indicators[col - 1][rowIdx]; @@ -835,7 +838,7 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI // Fast path: Data fits in buffer (not LOB or truncated) // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence // '<' - if (numCharsInData < colInfo->fetchBufferSize) { + if (!colInfo->isLob && numCharsInData < colInfo->fetchBufferSize) { #if defined(__APPLE__) || defined(__linux__) // Performance: Direct UTF-16 decode (SQLWCHAR is 2 bytes on // Linux/macOS) @@ -872,12 +875,9 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI } #endif } else { - // Reaching this case indicates an error in mssql_python. - // This function is only called on columns bound by SQLBindCol. - // For such columns, the ODBC Driver does not allow us to compensate by - // fetching the remaining data using SQLGetData / FetchLobColumnData. - ThrowStdException( - "Internal error: NCHAR/NVARCHAR column data exceeds buffer size."); + // Slow path: LOB data requires separate fetch call + PyList_SET_ITEM(row, col - 1, + FetchLobColumnData(hStmt, col, SQL_C_WCHAR, true, false).release().ptr()); } } @@ -885,7 +885,7 @@ inline void ProcessWChar(PyObject* row, ColumnBuffers& buffers, const void* colI // Performance: NULL/NO_TOTAL checks removed - handled centrally before // processor is called inline void ProcessBinary(PyObject* row, ColumnBuffers& buffers, const void* colInfoPtr, - SQLUSMALLINT col, SQLULEN rowIdx) { + SQLUSMALLINT col, SQLULEN rowIdx, SQLHSTMT hStmt) { const ColumnInfoExt* colInfo = static_cast(colInfoPtr); SQLLEN dataLen = buffers.indicators[col - 1][rowIdx]; @@ -902,7 +902,7 @@ inline void ProcessBinary(PyObject* row, ColumnBuffers& buffers, const void* col } // Fast path: Data fits in buffer (not LOB or truncated) - if (static_cast(dataLen) <= colInfo->processedColumnSize) { + if (!colInfo->isLob && static_cast(dataLen) <= colInfo->processedColumnSize) { // Performance: Direct Python C API call - create bytes from buffer PyObject* pyBytes = PyBytes_FromStringAndSize( reinterpret_cast( @@ -915,12 +915,10 @@ inline void ProcessBinary(PyObject* row, ColumnBuffers& buffers, const void* col PyList_SET_ITEM(row, col - 1, pyBytes); } } else { - // Reaching this case indicates an error in mssql_python. - // This function is only called on columns bound by SQLBindCol. - // For such columns, the ODBC Driver does not allow us to compensate by - // fetching the remaining data using SQLGetData / FetchLobColumnData. - ThrowStdException( - "Internal error: BINARY/VARBINARY column data exceeds buffer size."); + // Slow path: LOB data requires separate fetch call + PyList_SET_ITEM( + row, col - 1, + FetchLobColumnData(hStmt, col, SQL_C_BINARY, false, true, "").release().ptr()); } } diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index b975af2f..f63972f1 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -15277,122 +15277,3 @@ def test_close(db_connection): pytest.fail(f"Cursor close test failed: {e}") finally: cursor = db_connection.cursor() - - -def test_varchar_buffersize_special_character(cursor): - cursor.execute( - "drop table if exists #t1;\n" - + "create table #t1 (a varchar(2) collate SQL_Latin1_General_CP1_CI_AS)\n" - + "insert into #t1 values (N'ßl')\n" - ) - import platform - - if platform.system() != "Windows": - # works fine with default settings - cursor.connection.setdecoding(mssql_python.SQL_CHAR) - assert cursor.execute("select * from #t1").fetchone()[0] == "ßl" - assert cursor.execute("select LEFT(a, 1) from #t1").fetchone()[0] == "ß" - assert cursor.execute("select cast(a as varchar(3)) from #t1").fetchone()[0] == "ßl" - assert cursor.execute("select * from #t1").fetchmany(1)[0][0] == "ßl" - assert cursor.execute("select * from #t1").fetchall()[0][0] == "ßl" - else: - # fetchone respects setdecoding - cursor.connection.setdecoding(mssql_python.SQL_CHAR) - assert cursor.execute("select * from #t1").fetchone()[0] == b"\xdfl" - cursor.connection.setdecoding(mssql_python.SQL_CHAR, "cp1252") - assert cursor.execute("select * from #t1").fetchone()[0] == "ßl" - assert cursor.execute("select LEFT(a, 1) from #t1").fetchone()[0] == "ß" - assert cursor.execute("select cast(a as varchar(3)) from #t1").fetchone()[0] == "ßl" - - # fetchmany/fetchall do not respect setdecoding - with pytest.raises(SystemError, match=".*returned a result with an exception set"): - cursor.execute("select * from #t1").fetchmany(1) - with pytest.raises(SystemError, match=".*returned a result with an exception set"): - cursor.execute("select * from #t1").fetchall() - - -def test_varchar_latin1_fetch(cursor): - def query(): - cursor.execute( - """ - set nocount on - declare @t1 as table( - row_nr int, - latin1 varchar(1) collate SQL_Latin1_General_CP1_CI_AS, - utf8 varchar(3) collate Latin1_General_100_CI_AI_SC_UTF8 - ) - - ;with nums as ( - select 0 as n - union all - select n + 1 from nums where n < 255 - ) - insert into @t1 (row_nr, latin1) - select n, cast(n as binary(1)) - from nums - option (maxrecursion 256) - - update @t1 set utf8 = latin1 - - select * from @t1 - """ - ) - - def validate(result): - assert len(result) == 256 - for row_nr, latin1, utf8 in result: - assert utf8 == latin1 or ( - # small difference in how sql server and msodbcsql18 handle unmapped characters - row_nr in [129, 141, 143, 144, 157] - and utf8 == chr(row_nr) - and latin1 == "?" - ), (row_nr, utf8, latin1, chr(row_nr)) - - import platform - - if platform.system() != "Windows": - # works fine with defaults - cursor.connection.setdecoding(mssql_python.SQL_CHAR) - query() - validate([cursor.fetchone() for _ in range(256)]) - query() - validate(cursor.fetchall()) - query() - validate(cursor.fetchmany(500)) - else: - # works fine if correctly configured by user for fetchone (SQLGetData) - cursor.connection.setdecoding(mssql_python.SQL_CHAR, "cp1252") - query() - validate([cursor.fetchone() for _ in range(256)]) - # broken for SQLBindCol - query() - with pytest.raises(SystemError, match=".*returned a result with an exception set"): - cursor.fetchall() - query() - with pytest.raises(SystemError, match=".*returned a result with an exception set"): - cursor.fetchmany(500) - - -def test_varchar_emoji(cursor): - cursor.connection.setdecoding(mssql_python.SQL_CHAR) # default - cursor.execute( - """ - set nocount on - declare @t1 as table( - a nvarchar(20), - b varchar(20) collate Latin1_General_100_CI_AI_SC_UTF8 - ) - insert into @t1 values (N'😄', N'😄') - select a, b from @t1 - """ - ) - ret = cursor.fetchone() - - import platform - - if platform.system() == "Windows": - # impossible to fetch varchar emojis on windows currently - assert tuple(ret) == ("😄", "??") - else: - # works fine on other platforms - assert tuple(ret) == ("😄", "😄")