From 773cb3eb173b7c21ad055bf2f30c24486a2f8901 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 2 Jun 2026 08:44:14 +0530 Subject: [PATCH 1/7] FIX: Skip SQLDescribeParam for NULL params to avoid sp_describe_undeclared_parameters round-trips (GH-610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a parameterized query includes None values, the driver previously sent SQL_UNKNOWN_TYPE to the C++ layer, which triggered a SQLDescribeParam call (internally sp_describe_undeclared_parameters) for every NULL param on every execute(). This caused thousands of unnecessary server round-trips per minute for workloads with frequent NULLs (e.g. sp_set_session_context). The describe call fails most of the time — especially for stored procedure calls — and the C++ layer already falls back to SQL_VARCHAR, so the round-trip adds latency with no benefit. Fix: Return SQL_VARCHAR directly from _map_sql_type() for None params, matching the existing executemany() all-NULL column optimisation. Closes #610 --- mssql_python/cursor.py | 9 ++++++++- tests/test_001_globals.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 01b3f157..58441088 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -412,8 +412,15 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg logger.debug("_map_sql_type: Mapping param index=%d, type=%s", i, type(param).__name__) if param is None: logger.debug("_map_sql_type: NULL parameter - index=%d", i) + # Use SQL_VARCHAR directly instead of SQL_UNKNOWN_TYPE to avoid + # a costly SQLDescribeParam / sp_describe_undeclared_parameters + # round-trip to the server for every NULL parameter. The describe + # call fails most of the time (especially for stored-procedure + # calls) and the C++ layer already falls back to SQL_VARCHAR, so + # this is safe. Matches the executemany() all-NULL optimisation. + # See GH-610. return ( - ddbc_sql_const.SQL_UNKNOWN_TYPE.value, + ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_DEFAULT.value, 1, 0, diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 08d31b5a..e43270ed 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -1059,3 +1059,28 @@ def test_row_string_key_case_insensitive_with_lowercase(): # Non-existent attribute raises AttributeError with pytest.raises(AttributeError): row.nonexistent + + +def test_map_sql_type_none_returns_sql_varchar(): + """Test that _map_sql_type returns SQL_VARCHAR for None params (GH-610). + + Previously, None returned SQL_UNKNOWN_TYPE which triggered a costly + SQLDescribeParam / sp_describe_undeclared_parameters round-trip to the + server on every execute call with a NULL parameter. + """ + from unittest.mock import MagicMock + + from mssql_python.constants import ConstantsDDBC as ddbc_sql_const + + cursor = MagicMock(spec=mssql_python.Cursor) + # Bind the real method to the mock so we test actual logic + _map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor) + params = [None, 42, None] + + sql_type, c_type, col_size, dec_digits, is_dae = _map_sql_type(None, params, 0) + + assert sql_type == ddbc_sql_const.SQL_VARCHAR.value + assert c_type == ddbc_sql_const.SQL_C_DEFAULT.value + assert col_size == 1 + assert dec_digits == 0 + assert is_dae is False From b6f1478b8bf3077d53f87d28200c76dc5ab9108f Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 2 Jun 2026 14:03:17 +0530 Subject: [PATCH 2/7] Moving test from global and putting it in cursor --- tests/test_001_globals.py | 25 ------------------------- tests/test_004_cursor.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index e43270ed..08d31b5a 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -1059,28 +1059,3 @@ def test_row_string_key_case_insensitive_with_lowercase(): # Non-existent attribute raises AttributeError with pytest.raises(AttributeError): row.nonexistent - - -def test_map_sql_type_none_returns_sql_varchar(): - """Test that _map_sql_type returns SQL_VARCHAR for None params (GH-610). - - Previously, None returned SQL_UNKNOWN_TYPE which triggered a costly - SQLDescribeParam / sp_describe_undeclared_parameters round-trip to the - server on every execute call with a NULL parameter. - """ - from unittest.mock import MagicMock - - from mssql_python.constants import ConstantsDDBC as ddbc_sql_const - - cursor = MagicMock(spec=mssql_python.Cursor) - # Bind the real method to the mock so we test actual logic - _map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor) - params = [None, 42, None] - - sql_type, c_type, col_size, dec_digits, is_dae = _map_sql_type(None, params, 0) - - assert sql_type == ddbc_sql_const.SQL_VARCHAR.value - assert c_type == ddbc_sql_const.SQL_C_DEFAULT.value - assert col_size == 1 - assert dec_digits == 0 - assert is_dae is False diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index a63c4d8f..dc51e0c7 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2184,6 +2184,31 @@ def test_executemany_MIX_NONE_parameter_list(cursor, db_connection): db_connection.commit() +def test_map_sql_type_none_returns_sql_varchar(): + """Test that _map_sql_type returns SQL_VARCHAR for None params (GH-610). + + Previously, None returned SQL_UNKNOWN_TYPE which triggered a costly + SQLDescribeParam / sp_describe_undeclared_parameters round-trip to the + server on every execute call with a NULL parameter. + """ + from unittest.mock import MagicMock + + from mssql_python.constants import ConstantsDDBC as ddbc_sql_const + + cursor = MagicMock(spec=mssql_python.Cursor) + # Bind the real method to the mock so we test actual logic + _map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor) + params = [None, 42, None] + + sql_type, c_type, col_size, dec_digits, is_dae = _map_sql_type(None, params, 0) + + assert sql_type == ddbc_sql_const.SQL_VARCHAR.value + assert c_type == ddbc_sql_const.SQL_C_DEFAULT.value + assert col_size == 1 + assert dec_digits == 0 + assert is_dae is False + + @pytest.mark.skip(reason="Skipping due to commit reliability issues with executemany") def test_executemany_concurrent_null_parameters(db_connection): """Test executemany with NULL parameters across multiple sequential operations.""" From af90409ff76ec31979c0d10fec159f98cf141869 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 2 Jun 2026 18:15:49 +0530 Subject: [PATCH 3/7] Removed the dead executemany() SQL_UNKNOWN_TYPE check. That path is no longer reachable since _map_sql_type(None) now returns SQL_VARCHAR directly. --- mssql_python/cursor.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 58441088..70b2f061 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2342,15 +2342,6 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s 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 - # Correct column size for Decimal columns sent as SQL_VARCHAR (GH-557). # The sample value's formatted string may be shorter than another # row's (e.g. positive sample "1.0" = 3 chars vs negative "-0.1" = 4). From 00bb01703952fdb1c95516d34581510f4fd3c895 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 2 Jun 2026 18:33:03 +0530 Subject: [PATCH 4/7] Removing the dead code --- mssql_python/pybind/ddbc_bindings.cpp | 39 ++------------------------- mssql_python/pybind/ddbc_bindings.h | 5 ---- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 5ed7820f..22896c90 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -397,8 +397,6 @@ SQLParamDataFunc SQLParamData_ptr = nullptr; SQLPutDataFunc SQLPutData_ptr = nullptr; SQLTablesFunc SQLTables_ptr = nullptr; -SQLDescribeParamFunc SQLDescribeParam_ptr = nullptr; - namespace { const char* GetSqlCTypeAsString(const SQLSMALLINT cType) { @@ -627,41 +625,10 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, if (!py::isinstance(param)) { ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); } - SQLSMALLINT sqlType = paramInfo.paramSQLType; - SQLULEN columnSize = paramInfo.columnSize; - SQLSMALLINT decimalDigits = paramInfo.decimalDigits; - if (sqlType == SQL_UNKNOWN_TYPE) { - SQLSMALLINT describedType; - SQLULEN describedSize; - SQLSMALLINT describedDigits; - SQLSMALLINT nullable; - RETCODE rc = SQLDescribeParam_ptr( - hStmt, static_cast(paramIndex + 1), &describedType, - &describedSize, &describedDigits, &nullable); - if (!SQL_SUCCEEDED(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; - } - } dataPtr = nullptr; strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_NULL_DATA; bufferLength = 0; - paramInfo.paramSQLType = sqlType; - paramInfo.columnSize = columnSize; - paramInfo.decimalDigits = decimalDigits; break; } case SQL_C_STINYINT: @@ -1278,8 +1245,6 @@ DriverHandle LoadDriverOrThrowException() { SQLPutData_ptr = GetFunctionPointer(handle, "SQLPutData"); SQLTables_ptr = GetFunctionPointer(handle, "SQLTablesW"); - SQLDescribeParam_ptr = GetFunctionPointer(handle, "SQLDescribeParam"); - bool success = SQLAllocHandle_ptr && SQLSetEnvAttr_ptr && SQLSetConnectAttr_ptr && SQLSetStmtAttr_ptr && SQLGetConnectAttr_ptr && SQLDriverConnect_ptr && SQLExecDirect_ptr && SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && @@ -1288,7 +1253,7 @@ DriverHandle LoadDriverOrThrowException() { SQLDescribeCol_ptr && SQLMoreResults_ptr && SQLColAttribute_ptr && SQLEndTran_ptr && SQLDisconnect_ptr && SQLFreeHandle_ptr && SQLFreeStmt_ptr && SQLGetDiagRec_ptr && SQLGetInfo_ptr && SQLParamData_ptr && SQLPutData_ptr && - SQLTables_ptr && SQLDescribeParam_ptr && SQLGetTypeInfo_ptr && + SQLTables_ptr && SQLGetTypeInfo_ptr && SQLProcedures_ptr && SQLForeignKeys_ptr && SQLPrimaryKeys_ptr && SQLSpecialColumns_ptr && SQLStatistics_ptr && SQLColumns_ptr; @@ -1297,7 +1262,7 @@ DriverHandle LoadDriverOrThrowException() { } LOG("LoadDriverOrThrowException: All %d ODBC function pointers loaded " "successfully", - 44); + 43); return handle; } diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 0f831097..4d451fbb 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -117,9 +117,6 @@ typedef SQLRETURN(SQL_API* SQLFreeStmtFunc)(SQLHSTMT, SQLUSMALLINT); typedef SQLRETURN(SQL_API* SQLGetDiagRecFunc)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*); -typedef SQLRETURN(SQL_API* SQLDescribeParamFunc)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT*, SQLULEN*, - SQLSMALLINT*, SQLSMALLINT*); - // DAE APIs typedef SQLRETURN(SQL_API* SQLParamDataFunc)(SQLHSTMT, SQLPOINTER*); typedef SQLRETURN(SQL_API* SQLPutDataFunc)(SQLHSTMT, SQLPOINTER, SQLLEN); @@ -174,8 +171,6 @@ extern SQLFreeStmtFunc SQLFreeStmt_ptr; // Diagnostic APIs extern SQLGetDiagRecFunc SQLGetDiagRec_ptr; -extern SQLDescribeParamFunc SQLDescribeParam_ptr; - // DAE APIs extern SQLParamDataFunc SQLParamData_ptr; extern SQLPutDataFunc SQLPutData_ptr; From a9d9e584265789c3256d2b936b805f3489c9b45a Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Fri, 5 Jun 2026 09:28:28 +0530 Subject: [PATCH 5/7] FIX: Add SQLDescribeParam cache to eliminate redundant sp_describe_undeclared_parameters round-trips (GH-610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cursor.execute() includes None (NULL) params, the C++ BindParameters layer calls SQLDescribeParam for each NULL param on every execution. This triggers sp_describe_undeclared_parameters on SQL Server — a full network round-trip per NULL param per call. Fix: Add a statement-level cache in C++ that stores SQLDescribeParam results per (hStmt, paramIndex). First execution describes and caches; all subsequent executions of the same prepared statement get cache hits with zero round-trips. Cache is cleared on SQLPrepare (new SQL = new param types). Changes: - ddbc_bindings.cpp: Add DescribedParamInfo cache struct, ResolveNullParamType helper, cache invalidation on SQLPrepare in both SQLExecute_wrap and SQLExecuteMany_wrap. Both BindParameters and BindParameterArray use the cache for SQL_UNKNOWN_TYPE NULL params. - ddbc_bindings.h: Restore SQLDescribeParamFunc typedef and extern. - cursor.py: Revert _map_sql_type to return SQL_UNKNOWN_TYPE for None (let C++ cache handle resolution). Remove executemany SQL_VARCHAR hardcoded fallback (C++ BindParameterArray now resolves via cache). - test_004_cursor.py: Update test to verify SQL_UNKNOWN_TYPE for None. Closes #610 --- mssql_python/cursor.py | 24 ++--- mssql_python/pybind/ddbc_bindings.cpp | 123 +++++++++++++++++++++++++- mssql_python/pybind/ddbc_bindings.h | 5 ++ tests/test_004_cursor.py | 13 ++- 4 files changed, 138 insertions(+), 27 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 10fe4c68..b9e5257e 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -412,15 +412,11 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg logger.debug("_map_sql_type: Mapping param index=%d, type=%s", i, type(param).__name__) if param is None: logger.debug("_map_sql_type: NULL parameter - index=%d", i) - # Use SQL_VARCHAR directly instead of SQL_UNKNOWN_TYPE to avoid - # a costly SQLDescribeParam / sp_describe_undeclared_parameters - # round-trip to the server for every NULL parameter. The describe - # call fails most of the time (especially for stored-procedure - # calls) and the C++ layer already falls back to SQL_VARCHAR, so - # this is safe. Matches the executemany() all-NULL optimisation. - # See GH-610. + # GH-610: Send SQL_UNKNOWN_TYPE to C++ where the describe-cache + # in BindParameters / BindParameterArray resolves the correct + # type via SQLDescribeParam (cached after first call). return ( - ddbc_sql_const.SQL_VARCHAR.value, + ddbc_sql_const.SQL_UNKNOWN_TYPE.value, ddbc_sql_const.SQL_C_DEFAULT.value, 1, 0, @@ -2342,14 +2338,10 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s 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 + # GH-610: all-NULL columns now pass SQL_UNKNOWN_TYPE to C++, + # where BindParameterArray resolves the correct type via the + # SQLDescribeParam cache. The previous SQL_VARCHAR hardcoded + # fallback was removed because it broke VARBINARY columns. # Override DECIMAL/NUMERIC to use SQL_C_CHAR string binding. # _map_sql_type may return SQL_C_NUMERIC (expecting NumericData structs) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 22896c90..bb6115c4 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -15,6 +15,9 @@ #include // For std::memcpy #include // std::min #include +#include // std::shared_mutex, std::shared_lock, std::unique_lock +#include +#include #include // std::setw, std::setfill #include #include // std::forward @@ -397,6 +400,80 @@ SQLParamDataFunc SQLParamData_ptr = nullptr; SQLPutDataFunc SQLPutData_ptr = nullptr; SQLTablesFunc SQLTables_ptr = nullptr; +SQLDescribeParamFunc SQLDescribeParam_ptr = nullptr; + +// --- GH-610: SQLDescribeParam result cache --- +// Caches SQLDescribeParam results per (hStmt, paramIndex) to avoid +// redundant sp_describe_undeclared_parameters round-trips on repeated +// executions of the same prepared statement with NULL parameters. +struct DescribedParamInfo { + SQLSMALLINT sqlType; + SQLULEN columnSize; + SQLSMALLINT decimalDigits; + bool succeeded; +}; + +static std::shared_mutex g_describeCacheMutex; +static std::unordered_map> g_describeCache; + +static DescribedParamInfo ResolveNullParamType(SQLHANDLE hStmt, int paramIndex) { + // 1. Check cache (shared/read lock — concurrent readers allowed) + { + std::shared_lock lock(g_describeCacheMutex); + auto it = g_describeCache.find(hStmt); + if (it != g_describeCache.end()) { + auto paramIt = it->second.find(paramIndex); + if (paramIt != it->second.end()) { + LOG("ResolveNullParamType: Cache HIT for hStmt=%p param[%d] " + "-> sqlType=%d", (void*)hStmt, paramIndex, + paramIt->second.sqlType); + return paramIt->second; + } + } + } + + // 2. Cache miss — call SQLDescribeParam (NO lock held during round-trip) + SQLSMALLINT type, digits, nullable; + SQLULEN size; + LOG("ResolveNullParamType: Cache MISS for hStmt=%p param[%d], calling " + "SQLDescribeParam", (void*)hStmt, paramIndex); + RETCODE rc = SQLDescribeParam_ptr( + hStmt, static_cast(paramIndex + 1), + &type, &size, &digits, &nullable); + + DescribedParamInfo info; + if (SQL_SUCCEEDED(rc)) { + info = {type, size, digits, true}; + LOG("ResolveNullParamType: SQLDescribeParam succeeded for param[%d] " + "-> sqlType=%d, columnSize=%lu, decimalDigits=%d", + paramIndex, type, (unsigned long)size, digits); + } else { + info = {SQL_VARCHAR, 1, 0, false}; + LOG_WARNING("ResolveNullParamType: SQLDescribeParam failed for " + "param[%d] (rc=%d), falling back to SQL_VARCHAR", + paramIndex, rc); + } + + // 3. Store in cache (exclusive/write lock) + { + std::unique_lock lock(g_describeCacheMutex); + g_describeCache[hStmt][paramIndex] = info; + } + + return info; +} + +static void InvalidateDescribeCache(SQLHANDLE hStmt) { + std::unique_lock lock(g_describeCacheMutex); + auto erased = g_describeCache.erase(hStmt); + if (erased) { + LOG("InvalidateDescribeCache: Cleared cache for hStmt=%p", + (void*)hStmt); + } +} +// --- End GH-610 cache --- + namespace { const char* GetSqlCTypeAsString(const SQLSMALLINT cType) { @@ -477,7 +554,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, LOG("BindParameters: Starting parameter binding for statement handle %p " "with %zu parameters", (void*)hStmt, params.size()); - for (int paramIndex = 0; paramIndex < params.size(); paramIndex++) { + + for (int paramIndex = 0; paramIndex < static_cast(params.size()); paramIndex++) { const auto& param = params[paramIndex]; ParamInfo& paramInfo = paramInfos[paramIndex]; LOG("BindParameters: Processing param[%d] - C_Type=%d, SQL_Type=%d, " @@ -625,10 +703,23 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, if (!py::isinstance(param)) { ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); } + // GH-610: resolve SQL type for NULL params via cache + SQLSMALLINT sqlType = paramInfo.paramSQLType; + SQLULEN columnSize = paramInfo.columnSize; + SQLSMALLINT decimalDigits = paramInfo.decimalDigits; + if (sqlType == SQL_UNKNOWN_TYPE) { + auto resolved = ResolveNullParamType(hStmt, paramIndex); + sqlType = resolved.sqlType; + columnSize = resolved.columnSize; + decimalDigits = resolved.decimalDigits; + } dataPtr = nullptr; strLenOrIndPtr = AllocateParamBuffer(paramBuffers); *strLenOrIndPtr = SQL_NULL_DATA; bufferLength = 0; + paramInfo.paramSQLType = sqlType; + paramInfo.columnSize = columnSize; + paramInfo.decimalDigits = decimalDigits; break; } case SQL_C_STINYINT: @@ -1245,6 +1336,8 @@ DriverHandle LoadDriverOrThrowException() { SQLPutData_ptr = GetFunctionPointer(handle, "SQLPutData"); SQLTables_ptr = GetFunctionPointer(handle, "SQLTablesW"); + SQLDescribeParam_ptr = GetFunctionPointer(handle, "SQLDescribeParam"); + bool success = SQLAllocHandle_ptr && SQLSetEnvAttr_ptr && SQLSetConnectAttr_ptr && SQLSetStmtAttr_ptr && SQLGetConnectAttr_ptr && SQLDriverConnect_ptr && SQLExecDirect_ptr && SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && @@ -1253,7 +1346,7 @@ DriverHandle LoadDriverOrThrowException() { SQLDescribeCol_ptr && SQLMoreResults_ptr && SQLColAttribute_ptr && SQLEndTran_ptr && SQLDisconnect_ptr && SQLFreeHandle_ptr && SQLFreeStmt_ptr && SQLGetDiagRec_ptr && SQLGetInfo_ptr && SQLParamData_ptr && SQLPutData_ptr && - SQLTables_ptr && SQLGetTypeInfo_ptr && + SQLTables_ptr && SQLDescribeParam_ptr && SQLGetTypeInfo_ptr && SQLProcedures_ptr && SQLForeignKeys_ptr && SQLPrimaryKeys_ptr && SQLSpecialColumns_ptr && SQLStatistics_ptr && SQLColumns_ptr; @@ -1262,7 +1355,7 @@ DriverHandle LoadDriverOrThrowException() { } LOG("LoadDriverOrThrowException: All %d ODBC function pointers loaded " "successfully", - 43); + 43 + 1); return handle; } @@ -1752,6 +1845,8 @@ SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, rc, (void*)hStmt); return rc; } + // GH-610: Invalidate describe cache (new prepare = new param types) + InvalidateDescribeCache(hStmt); isStmtPrepared[0] = py::cast(true); } else { // Make sure the statement has been prepared earlier if we're not @@ -2514,6 +2609,17 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, const py::list& columnwise_params, "count=%zu", paramIndex, paramSetSize); + // GH-610: resolve SQL type for all-NULL columns via cache + SQLSMALLINT resolvedSqlType = info.paramSQLType; + SQLULEN resolvedColSize = info.columnSize; + SQLSMALLINT resolvedDecDigits = info.decimalDigits; + if (resolvedSqlType == SQL_UNKNOWN_TYPE) { + auto resolved = ResolveNullParamType(hStmt, paramIndex); + resolvedSqlType = resolved.sqlType; + resolvedColSize = resolved.columnSize; + resolvedDecDigits = resolved.decimalDigits; + } + // For NULL parameters, we need to allocate a minimal buffer and set all // indicators to SQL_NULL_DATA Use SQL_C_CHAR as a safe default C type for NULL // values @@ -2527,7 +2633,14 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, const py::list& columnwise_params, dataPtr = nullBuffer; bufferLength = 1; - LOG("BindParameterArray: SQL_C_DEFAULT bound - param_index=%d", paramIndex); + + // Override info fields so SQLBindParameter below uses resolved type + const_cast(info).paramSQLType = resolvedSqlType; + const_cast(info).columnSize = resolvedColSize; + const_cast(info).decimalDigits = resolvedDecDigits; + + LOG("BindParameterArray: SQL_C_DEFAULT bound - param_index=%d, " + "resolvedSqlType=%d", paramIndex, resolvedSqlType); break; } default: { @@ -2586,6 +2699,8 @@ SQLRETURN SQLExecuteMany_wrap(const SqlHandlePtr statementHandle, const std::u16 LOG("SQLExecuteMany: SQLPrepare failed - rc=%d", rc); return rc; } + // GH-610: Invalidate describe cache for this statement (new prepare = new param types) + InvalidateDescribeCache(hStmt); LOG("SQLExecuteMany: Query prepared successfully"); bool hasDAE = false; diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 4d451fbb..0f831097 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -117,6 +117,9 @@ typedef SQLRETURN(SQL_API* SQLFreeStmtFunc)(SQLHSTMT, SQLUSMALLINT); typedef SQLRETURN(SQL_API* SQLGetDiagRecFunc)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*); +typedef SQLRETURN(SQL_API* SQLDescribeParamFunc)(SQLHSTMT, SQLUSMALLINT, SQLSMALLINT*, SQLULEN*, + SQLSMALLINT*, SQLSMALLINT*); + // DAE APIs typedef SQLRETURN(SQL_API* SQLParamDataFunc)(SQLHSTMT, SQLPOINTER*); typedef SQLRETURN(SQL_API* SQLPutDataFunc)(SQLHSTMT, SQLPOINTER, SQLLEN); @@ -171,6 +174,8 @@ extern SQLFreeStmtFunc SQLFreeStmt_ptr; // Diagnostic APIs extern SQLGetDiagRecFunc SQLGetDiagRec_ptr; +extern SQLDescribeParamFunc SQLDescribeParam_ptr; + // DAE APIs extern SQLParamDataFunc SQLParamData_ptr; extern SQLPutDataFunc SQLPutData_ptr; diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 476dafe2..95ee9b57 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2184,25 +2184,24 @@ def test_executemany_MIX_NONE_parameter_list(cursor, db_connection): db_connection.commit() -def test_map_sql_type_none_returns_sql_varchar(): - """Test that _map_sql_type returns SQL_VARCHAR for None params (GH-610). +def test_map_sql_type_none_returns_sql_unknown_type(): + """Test that _map_sql_type returns SQL_UNKNOWN_TYPE for None params (GH-610). - Previously, None returned SQL_UNKNOWN_TYPE which triggered a costly - SQLDescribeParam / sp_describe_undeclared_parameters round-trip to the - server on every execute call with a NULL parameter. + None returns SQL_UNKNOWN_TYPE so the C++ BindParameters cache can resolve + the correct type via SQLDescribeParam on first call and cache it for + subsequent calls. """ from unittest.mock import MagicMock from mssql_python.constants import ConstantsDDBC as ddbc_sql_const cursor = MagicMock(spec=mssql_python.Cursor) - # Bind the real method to the mock so we test actual logic _map_sql_type = mssql_python.Cursor._map_sql_type.__get__(cursor) params = [None, 42, None] sql_type, c_type, col_size, dec_digits, is_dae = _map_sql_type(None, params, 0) - assert sql_type == ddbc_sql_const.SQL_VARCHAR.value + assert sql_type == ddbc_sql_const.SQL_UNKNOWN_TYPE.value assert c_type == ddbc_sql_const.SQL_C_DEFAULT.value assert col_size == 1 assert dec_digits == 0 From 74d0f590ccfff15809b05e1d282c57e1682c2fad Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Fri, 5 Jun 2026 11:55:28 +0530 Subject: [PATCH 6/7] TEST: Add GH-610 SQLDescribeParam cache coverage tests Add 7 integration tests covering all cache code paths: - Cache miss (first execute with NULL param) - Cache hit (repeated execute with same SQL + NULL) - Cache invalidation (different SQL triggers re-prepare) - executemany all-NULL column (BindParameterArray path) - executemany multiple all-NULL columns - All-NULL params in execute - setinputsizes bypass (explicit type skips cache) --- tests/test_004_cursor.py | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 95ee9b57..4eb7714a 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2208,6 +2208,103 @@ def test_map_sql_type_none_returns_sql_unknown_type(): assert is_dae is False +# --------------------------------------------------------- +# GH-610: SQLDescribeParam cache coverage tests +# --------------------------------------------------------- + + +def test_gh610_execute_null_param_cache_miss(cursor, db_connection): + """Cover cache MISS path: first execute with NULL triggers SQLDescribeParam.""" + cursor.execute("CREATE TABLE #gh610_cov1 (id INT, name VARCHAR(50))") + cursor.execute("INSERT INTO #gh610_cov1 VALUES (?, ?)", (1, None)) + db_connection.commit() + cursor.execute("SELECT COUNT(*) FROM #gh610_cov1") + assert cursor.fetchone()[0] == 1 + cursor.execute("DROP TABLE #gh610_cov1") + + +def test_gh610_execute_null_param_cache_hit(cursor, db_connection): + """Cover cache HIT path: repeated execute with same SQL + NULL.""" + cursor.execute("CREATE TABLE #gh610_cov2 (id INT, name VARCHAR(50))") + # First call: cache miss → SQLDescribeParam + cursor.execute("INSERT INTO #gh610_cov2 VALUES (?, ?)", (1, None)) + # Second call: cache hit → no SQLDescribeParam + cursor.execute("INSERT INTO #gh610_cov2 VALUES (?, ?)", (2, None)) + # Third call: cache hit + cursor.execute("INSERT INTO #gh610_cov2 VALUES (?, ?)", (3, None)) + db_connection.commit() + cursor.execute("SELECT COUNT(*) FROM #gh610_cov2") + assert cursor.fetchone()[0] == 3 + cursor.execute("DROP TABLE #gh610_cov2") + + +def test_gh610_cache_invalidation_on_new_sql(cursor, db_connection): + """Cover InvalidateDescribeCache path: different SQL clears cache.""" + cursor.execute("CREATE TABLE #gh610_cov3a (val INT)") + cursor.execute("CREATE TABLE #gh610_cov3b (val VARCHAR(50))") + # First query — cache populated + cursor.execute("INSERT INTO #gh610_cov3a VALUES (?)", (None,)) + # Different SQL — triggers SQLPrepare → InvalidateDescribeCache + cursor.execute("INSERT INTO #gh610_cov3b VALUES (?)", (None,)) + # Back to first — triggers SQLPrepare → InvalidateDescribeCache again + cursor.execute("INSERT INTO #gh610_cov3a VALUES (?)", (None,)) + db_connection.commit() + cursor.execute("SELECT COUNT(*) FROM #gh610_cov3a") + assert cursor.fetchone()[0] == 2 + cursor.execute("DROP TABLE #gh610_cov3a") + cursor.execute("DROP TABLE #gh610_cov3b") + + +def test_gh610_executemany_all_null_column(cursor, db_connection): + """Cover BindParameterArray SQL_C_DEFAULT + SQL_UNKNOWN_TYPE path.""" + cursor.execute("CREATE TABLE #gh610_cov4 (id INT, name VARCHAR(50))") + cursor.executemany( + "INSERT INTO #gh610_cov4 VALUES (?, ?)", + [(1, None), (2, None), (3, None)], + ) + db_connection.commit() + cursor.execute("SELECT COUNT(*) FROM #gh610_cov4 WHERE name IS NULL") + assert cursor.fetchone()[0] == 3 + cursor.execute("DROP TABLE #gh610_cov4") + + +def test_gh610_executemany_multiple_all_null_columns(cursor, db_connection): + """Cover BindParameterArray with multiple all-NULL columns.""" + cursor.execute("CREATE TABLE #gh610_cov5 (id INT, a VARCHAR(50), b INT, c VARCHAR(50))") + cursor.executemany( + "INSERT INTO #gh610_cov5 VALUES (?, ?, ?, ?)", + [(1, None, None, None), (2, None, None, None)], + ) + db_connection.commit() + cursor.execute("SELECT COUNT(*) FROM #gh610_cov5") + assert cursor.fetchone()[0] == 2 + cursor.execute("DROP TABLE #gh610_cov5") + + +def test_gh610_execute_all_null_params(cursor, db_connection): + """Cover BindParameters with all params being NULL.""" + cursor.execute("CREATE TABLE #gh610_cov6 (a INT, b VARCHAR(50))") + cursor.execute("INSERT INTO #gh610_cov6 VALUES (?, ?)", (None, None)) + db_connection.commit() + cursor.execute("SELECT * FROM #gh610_cov6") + row = cursor.fetchone() + assert row[0] is None and row[1] is None + cursor.execute("DROP TABLE #gh610_cov6") + + +def test_gh610_setinputsizes_bypasses_cache(cursor, db_connection): + """setinputsizes provides type directly — cache not used.""" + from mssql_python.constants import ConstantsDDBC as C + + cursor.execute("CREATE TABLE #gh610_cov7 (val VARCHAR(50))") + cursor.setinputsizes([(C.SQL_VARCHAR.value, 50, 0)]) + cursor.execute("INSERT INTO #gh610_cov7 VALUES (?)", (None,)) + db_connection.commit() + cursor.execute("SELECT val FROM #gh610_cov7") + assert cursor.fetchone()[0] is None + cursor.execute("DROP TABLE #gh610_cov7") + + @pytest.mark.skip(reason="Skipping due to commit reliability issues with executemany") def test_executemany_concurrent_null_parameters(db_connection): """Test executemany with NULL parameters across multiple sequential operations.""" From 61af48f67e668a48ff57747cb5548136f53dd96b Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Fri, 5 Jun 2026 13:43:40 +0530 Subject: [PATCH 7/7] Resolving issues --- mssql_python/pybind/ddbc_bindings.cpp | 149 +++++++++++++------------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index bb6115c4..a27ad440 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -5,23 +5,25 @@ // agnostic will be // taken up in beta release #include "ddbc_bindings.h" -#include "utf_utils.h" #include "connection/connection.h" #include "connection/connection_pool.h" #include "logger_bridge.hpp" +#include "utf_utils.h" + +#include // std::min #include #include #include // For std::memcpy -#include // std::min #include -#include // std::shared_mutex, std::shared_lock, std::unique_lock -#include -#include #include // std::setw, std::setfill #include +#include // std::shared_mutex, std::shared_lock, std::unique_lock +#include +#include #include // std::forward + //------------------------------------------------------------------------------------------------- // Macro definitions //------------------------------------------------------------------------------------------------- @@ -414,8 +416,7 @@ struct DescribedParamInfo { }; static std::shared_mutex g_describeCacheMutex; -static std::unordered_map> g_describeCache; +static std::unordered_map> g_describeCache; static DescribedParamInfo ResolveNullParamType(SQLHANDLE hStmt, int paramIndex) { // 1. Check cache (shared/read lock — concurrent readers allowed) @@ -426,8 +427,8 @@ static DescribedParamInfo ResolveNullParamType(SQLHANDLE hStmt, int paramIndex) auto paramIt = it->second.find(paramIndex); if (paramIt != it->second.end()) { LOG("ResolveNullParamType: Cache HIT for hStmt=%p param[%d] " - "-> sqlType=%d", (void*)hStmt, paramIndex, - paramIt->second.sqlType); + "-> sqlType=%d", + (void*)hStmt, paramIndex, paramIt->second.sqlType); return paramIt->second; } } @@ -437,10 +438,10 @@ static DescribedParamInfo ResolveNullParamType(SQLHANDLE hStmt, int paramIndex) SQLSMALLINT type, digits, nullable; SQLULEN size; LOG("ResolveNullParamType: Cache MISS for hStmt=%p param[%d], calling " - "SQLDescribeParam", (void*)hStmt, paramIndex); - RETCODE rc = SQLDescribeParam_ptr( - hStmt, static_cast(paramIndex + 1), - &type, &size, &digits, &nullable); + "SQLDescribeParam", + (void*)hStmt, paramIndex); + RETCODE rc = SQLDescribeParam_ptr(hStmt, static_cast(paramIndex + 1), &type, + &size, &digits, &nullable); DescribedParamInfo info; if (SQL_SUCCEEDED(rc)) { @@ -468,8 +469,7 @@ static void InvalidateDescribeCache(SQLHANDLE hStmt) { std::unique_lock lock(g_describeCacheMutex); auto erased = g_describeCache.erase(hStmt); if (erased) { - LOG("InvalidateDescribeCache: Cleared cache for hStmt=%p", - (void*)hStmt); + LOG("InvalidateDescribeCache: Cleared cache for hStmt=%p", (void*)hStmt); } } // --- End GH-610 cache --- @@ -683,7 +683,8 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, paramBuffers, param.cast()); LOG("BindParameters: param[%d] SQL_C_WCHAR - String " "length=%zu characters, buffer=%zu bytes", - paramIndex, sqlwcharBuffer->size(), sqlwcharBuffer->size() * sizeof(SQLWCHAR)); + paramIndex, sqlwcharBuffer->size(), + sqlwcharBuffer->size() * sizeof(SQLWCHAR)); dataPtr = sqlwcharBuffer->data(); bufferLength = sqlwcharBuffer->size() * sizeof(SQLWCHAR); strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -1355,7 +1356,7 @@ DriverHandle LoadDriverOrThrowException() { } LOG("LoadDriverOrThrowException: All %d ODBC function pointers loaded " "successfully", - 43 + 1); + 44); return handle; } @@ -1508,14 +1509,12 @@ SQLRETURN SQLProcedures_wrap(SqlHandlePtr StatementHandle, const py::object& cat std::u16string catalog = catalogObj.is_none() ? u"" : catalogObj.cast(); std::u16string schema = schemaObj.is_none() ? u"" : schemaObj.cast(); - std::u16string procedure = - procedureObj.is_none() ? u"" : procedureObj.cast(); + std::u16string procedure = procedureObj.is_none() ? u"" : procedureObj.cast(); // Release the GIL during the blocking ODBC catalog call py::gil_scoped_release release; return SQLProcedures_ptr( - StatementHandle->get(), - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), + StatementHandle->get(), catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), catalog.empty() ? 0 : SQL_NTS, schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), schema.empty() ? 0 : SQL_NTS, @@ -1567,14 +1566,13 @@ SQLRETURN SQLPrimaryKeys_wrap(SqlHandlePtr StatementHandle, const py::object& ca // Release the GIL during the blocking ODBC catalog call py::gil_scoped_release release; - return SQLPrimaryKeys_ptr( - StatementHandle->get(), - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), - catalog.empty() ? 0 : SQL_NTS, - schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), - schema.empty() ? 0 : SQL_NTS, - table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), - table.empty() ? 0 : SQL_NTS); + return SQLPrimaryKeys_ptr(StatementHandle->get(), + catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), + catalog.empty() ? 0 : SQL_NTS, + schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), + schema.empty() ? 0 : SQL_NTS, + table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), + table.empty() ? 0 : SQL_NTS); } SQLRETURN SQLStatistics_wrap(SqlHandlePtr StatementHandle, const py::object& catalogObj, @@ -1589,14 +1587,13 @@ SQLRETURN SQLStatistics_wrap(SqlHandlePtr StatementHandle, const py::object& cat // Release the GIL during the blocking ODBC catalog call py::gil_scoped_release release; - return SQLStatistics_ptr( - StatementHandle->get(), - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), - catalog.empty() ? 0 : SQL_NTS, - schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), - schema.empty() ? 0 : SQL_NTS, - table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), - table.empty() ? 0 : SQL_NTS, unique, reserved); + return SQLStatistics_ptr(StatementHandle->get(), + catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), + catalog.empty() ? 0 : SQL_NTS, + schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), + schema.empty() ? 0 : SQL_NTS, + table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), + table.empty() ? 0 : SQL_NTS, unique, reserved); } SQLRETURN SQLColumns_wrap(SqlHandlePtr StatementHandle, const py::object& catalogObj, @@ -1613,16 +1610,15 @@ SQLRETURN SQLColumns_wrap(SqlHandlePtr StatementHandle, const py::object& catalo // Release the GIL during the blocking ODBC catalog call py::gil_scoped_release release; - return SQLColumns_ptr( - StatementHandle->get(), - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), - catalog.empty() ? 0 : SQL_NTS, - schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), - schema.empty() ? 0 : SQL_NTS, - table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), - table.empty() ? 0 : SQL_NTS, - column.empty() ? nullptr : reinterpretU16stringAsSqlWChar(column), - column.empty() ? 0 : SQL_NTS); + return SQLColumns_ptr(StatementHandle->get(), + catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), + catalog.empty() ? 0 : SQL_NTS, + schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), + schema.empty() ? 0 : SQL_NTS, + table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), + table.empty() ? 0 : SQL_NTS, + column.empty() ? nullptr : reinterpretU16stringAsSqlWChar(column), + column.empty() ? 0 : SQL_NTS); } // Helper function to check for driver errors @@ -1647,8 +1643,9 @@ ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRET SQLINTEGER nativeError; SQLSMALLINT messageLen; - SQLRETURN diagReturn = SQLGetDiagRec_ptr(handleType, rawHandle, 1, sqlState, &nativeError, - message, SQL_MAX_MESSAGE_LENGTH_SQLSERVER, &messageLen); + SQLRETURN diagReturn = + SQLGetDiagRec_ptr(handleType, rawHandle, 1, sqlState, &nativeError, message, + SQL_MAX_MESSAGE_LENGTH_SQLSERVER, &messageLen); if (SQL_SUCCEEDED(diagReturn)) { std::u16string sqlStateUtf16 = dupeSqlWCharAsUtf16Le(sqlState, 5); @@ -1755,16 +1752,15 @@ SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, const std::u16string& cat { // Release the GIL during the blocking ODBC catalog call py::gil_scoped_release release; - ret = SQLTables_ptr( - StatementHandle->get(), - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), - catalog.empty() ? 0 : SQL_NTS, - schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), - schema.empty() ? 0 : SQL_NTS, - table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), - table.empty() ? 0 : SQL_NTS, - tableType.empty() ? nullptr : reinterpretU16stringAsSqlWChar(tableType), - tableType.empty() ? 0 : SQL_NTS); + ret = SQLTables_ptr(StatementHandle->get(), + catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), + catalog.empty() ? 0 : SQL_NTS, + schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), + schema.empty() ? 0 : SQL_NTS, + table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), + table.empty() ? 0 : SQL_NTS, + tableType.empty() ? nullptr : reinterpretU16stringAsSqlWChar(tableType), + tableType.empty() ? 0 : SQL_NTS); } LOG("SQLTables: Catalog metadata query %s - SQLRETURN=%d", @@ -1777,8 +1773,7 @@ SQLRETURN SQLTables_wrap(SqlHandlePtr StatementHandle, const std::u16string& cat // statement and binds the parameters. Otherwise, it executes the query // directly. 'usePrepare' parameter can be used to disable the prepare step for // queries that might already be prepared in a previous call. -SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, - const std::u16string& query, +SQLRETURN SQLExecute_wrap(const SqlHandlePtr statementHandle, const std::u16string& query, const py::list& params, std::vector& paramInfos, py::list& isStmtPrepared, const bool usePrepare, const py::dict& encodingSettings) { @@ -2640,7 +2635,8 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt, const py::list& columnwise_params, const_cast(info).decimalDigits = resolvedDecDigits; LOG("BindParameterArray: SQL_C_DEFAULT bound - param_index=%d, " - "resolvedSqlType=%d", paramIndex, resolvedSqlType); + "resolvedSqlType=%d", + paramIndex, resolvedSqlType); break; } default: { @@ -2894,9 +2890,8 @@ SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMeta // TODO: Should we define a struct for this task instead of dict? ColumnMetadata.append( py::dict("ColumnName"_a = dupeSqlWCharAsUtf16Le( - ColumnName, - std::min(static_cast(NameLength), - (sizeof(ColumnName) / sizeof(SQLWCHAR)) - 1)), + ColumnName, std::min(static_cast(NameLength), + (sizeof(ColumnName) / sizeof(SQLWCHAR)) - 1)), "DataType"_a = DataType, "ColumnSize"_a = ColumnSize, "DecimalDigits"_a = DecimalDigits, "Nullable"_a = Nullable)); } else { @@ -2918,14 +2913,14 @@ SQLRETURN SQLSpecialColumns_wrap(SqlHandlePtr StatementHandle, SQLSMALLINT ident std::u16string schema = schemaObj.is_none() ? u"" : schemaObj.cast(); py::gil_scoped_release release; - return SQLSpecialColumns_ptr( - StatementHandle->get(), identifierType, - catalog.empty() ? nullptr : reinterpretU16stringAsSqlWChar(catalog), - catalog.empty() ? 0 : SQL_NTS, - schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), - schema.empty() ? 0 : SQL_NTS, - table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), - table.empty() ? 0 : SQL_NTS, scope, nullable); + return SQLSpecialColumns_ptr(StatementHandle->get(), identifierType, + catalog.empty() ? nullptr + : reinterpretU16stringAsSqlWChar(catalog), + catalog.empty() ? 0 : SQL_NTS, + schema.empty() ? nullptr : reinterpretU16stringAsSqlWChar(schema), + schema.empty() ? 0 : SQL_NTS, + table.empty() ? nullptr : reinterpretU16stringAsSqlWChar(table), + table.empty() ? 0 : SQL_NTS, scope, nullable); } // Wrap SQLFetch to retrieve rows @@ -3252,8 +3247,8 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p // null termination. This preserves embedded NULs and avoids // any risk of reading past the valid range if the driver // omits the terminator. - row.append( - py::cast(dupeSqlWCharAsUtf16Le(dataBuffer.data(), numCharsInData))); + row.append(py::cast( + dupeSqlWCharAsUtf16Le(dataBuffer.data(), numCharsInData))); LOG("SQLGetData: CHAR column %d fetched as WCHAR, " "length=%lu", i, (unsigned long)numCharsInData); @@ -3418,8 +3413,8 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p // null termination. This preserves embedded NULs and avoids // any risk of reading past the valid range if the driver // omits the terminator. - row.append( - py::cast(dupeSqlWCharAsUtf16Le(dataBuffer.data(), numCharsInData))); + row.append(py::cast( + dupeSqlWCharAsUtf16Le(dataBuffer.data(), numCharsInData))); LOG("SQLGetData: Appended NVARCHAR string " "length=%lu for column %d", (unsigned long)numCharsInData, i);