Skip to content

Conversation

@bewithgaurav
Copy link
Collaborator

@bewithgaurav bewithgaurav commented Oct 31, 2025

Work Item / Issue Reference

AB#39180
AB#40126


Summary

This pull request introduces a new, comprehensive logging system to the mssql_python driver, replacing the previous logging approach. The update provides JDBC-style log levels (FINEST, FINER, FINE), enhanced security through automatic sanitization of sensitive data, trace IDs, file rotation, and thread safety. The logging system is now used throughout the codebase, improving diagnostics and maintainability.

Key changes include:

Logging System Enhancements:

  • Introduced a new logging module with JDBC-style log levels (FINEST, FINER, FINE), automatic sanitization, trace IDs, file rotation, and thread safety. The logging API is now documented in the README.md with usage examples and a link to detailed documentation. [1] [2] [3]

Codebase Refactoring for Logging:

These changes make logging more robust, secure, and easier to use, while also improving the developer experience with better diagnostics and documentation.

@github-actions github-actions bot added the pr-size: large Substantial code update label Oct 31, 2025
logger.setLevel(FINEST)

# Log a message with a password
test_message = "Connection string: Server=localhost;PWD=secret123;Database=test"

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling.

Do not leave debug code in production
@github-actions
Copy link

github-actions bot commented Oct 31, 2025

📊 Code Coverage Report

🔥 Diff Coverage

57%


🎯 Overall Coverage

76%


📈 Total Lines Covered: 4830 out of 6322
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/init.py (100%)
  • mssql_python/auth.py (100%)
  • mssql_python/connection.py (88.4%): Missing lines 735,1029,1177,1367,1371
  • mssql_python/constants.py (100%)
  • mssql_python/cursor.py (79.6%): Missing lines 394,469,1072,1184,1224,1227,1754,1756,2261,2470
  • mssql_python/db_connection.py (100%)
  • mssql_python/ddbc_bindings.py (100%)
  • mssql_python/exceptions.py (66.7%): Missing lines 527
  • mssql_python/helpers.py (90.3%): Missing lines 61,115,160
  • mssql_python/logging.py (92.1%): Missing lines 59,84,219,274,302,304,337,340-341,343
  • mssql_python/pooling.py (100%)
  • mssql_python/pybind/connection/connection.cpp (75.8%): Missing lines 24,221,242,265,292,296,300,348
  • mssql_python/pybind/connection/connection_pool.cpp (33.3%): Missing lines 75,106
  • mssql_python/pybind/ddbc_bindings.cpp (40.8%): Missing lines 282,384,595-596,644,649,656,663,669,749,772-773,917-918,1300,1308,1339,1403-1405,1407,1432,1445,1530,1543,1583,1648-1649,1665-1666,1684-1685,1694-1695,1729-1730,1747,1758,1760,1767,1773,1798-1799,1801,1837-1838,1860,1865-1866,1894-1895,1897,1909,1912,1917,1919-1921,1925-1926,1933,1936,1941,1947,1964,1984,1996,1999,2004,2013,2019,2022,2027,2036,2042,2045,2050,2063,2082,2120,2123,2129,2133,2137-2138,2150,2177-2178,2182,2190,2211,2230,2235,2264-2266,2282-2284,2288-2290,2297,2300,2305-2309,2312-2313,2317-2322,2325-2328,2333-2334,2336-2338,2342-2343,2345-2347,2349,2352,2354-2355,2357-2360,2362,2372,2386,2394,2478,2480,2514,2603,2620,2652,2661,2664,2668,2716,2719,2723,2745,2756,2804,2809,2822,2833,2870,2894,2943,2976,2980,2992,3003,3033,3043,3054,3216,3225,3263,3267,3280,3286,3496,3584,3616,3655,3665,3699,3776,3832,3841,3843,3849,3859,3866,4053-4054,4062
  • mssql_python/pybind/logger_bridge.cpp (19.1%): Missing lines 29-30,56-61,70-72,74-76,78,80,85-88,90,92-93,95,97-98,102-103,105-106,108-109,111,113-115,118-121,124-127,130-131,134,136-138,141-143,146-149,152,157-159,162-164,167,169,171,175-176,178,180,183-184,186-188
  • mssql_python/pybind/logger_bridge.hpp (57.1%): Missing lines 150-152,158-160,166-168
  • mssql_python/row.py (68.2%): Missing lines 142,144,149,161,163,165,190
  • mssql_python/type.py (100%)

Summary

  • Total: 759 lines
  • Missing: 324 lines
  • Coverage: 57%

mssql_python/connection.py

Lines 731-739

  731                     escape_char = "\\"
  732                 self._searchescape = escape_char
  733             except Exception as e:
  734                 # Log the exception for debugging, but do not expose sensitive info
! 735                 logger.debug(
  736                     "warning",
  737                     "Failed to retrieve search escape character, using default '\\'. "
  738                     "Exception: %s",
  739                     type(e).__name__,

Lines 1025-1033

  1025                         "debug",
  1026                         "Automatically closed cursor after batch execution error",
  1027                     )
  1028                 except Exception as close_err:
! 1029                     logger.debug(
  1030                         "warning",
  1031                         f"Error closing cursor after execution failure: {close_err}",
  1032                     )
  1033             # Re-raise the original exception

Lines 1173-1181

  1173                     except UnicodeDecodeError:
  1174                         try:
  1175                             return actual_data.decode("latin1").rstrip("\0")
  1176                         except Exception as e:
! 1177                             logger.debug(
  1178                                 "error",
  1179                                 "Failed to decode string in getinfo: %s. "
  1180                                 "Returning None to avoid silent corruption.",
  1181                                 e,

Lines 1363-1375

  1363                         cursor.close()
  1364                 except Exception as e:  # pylint: disable=broad-exception-caught
  1365                     # Collect errors but continue closing other cursors
  1366                     close_errors.append(f"Error closing cursor: {e}")
! 1367                     logger.warning( f"Error closing cursor: {e}")
  1368 
  1369             # If there were errors closing cursors, log them but continue
  1370             if close_errors:
! 1371                 logger.debug(
  1372                     "warning",
  1373                     "Encountered %d errors while closing cursors",
  1374                     len(close_errors),
  1375                 )

mssql_python/cursor.py

Lines 390-398

  390             exponent = decimal_as_tuple.exponent
  391 
  392             # Handle special values (NaN, Infinity, etc.)
  393             if isinstance(exponent, str):
! 394                 logger.finer('_map_sql_type: DECIMAL special value - index=%d, exponent=%s', i, exponent)
  395                 # For special values like 'n' (NaN), 'N' (sNaN), 'F' (Infinity)
  396                 # Return default precision and scale
  397                 precision = 38  # SQL Server default max precision
  398             else:

Lines 465-473

  465                 param.startswith("POINT")
  466                 or param.startswith("LINESTRING")
  467                 or param.startswith("POLYGON")
  468             ):
! 469                 logger.finest('_map_sql_type: STR is geometry type - index=%d', i)
  470                 return (
  471                     ddbc_sql_const.SQL_WVARCHAR.value,
  472                     ddbc_sql_const.SQL_C_WCHAR.value,
  473                     len(param),

Lines 1068-1076

  1068                     ddbc_sql_const.SQL_ATTR_QUERY_TIMEOUT.value,
  1069                     timeout_value,
  1070                 )
  1071                 check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
! 1072                 logger.debug("Set query timeout to %d seconds", timeout_value)
  1073             except Exception as e:  # pylint: disable=broad-exception-caught
  1074                 logger.warning("Failed to set query timeout: %s", str(e))
  1075 
  1076         logger.finest('execute: Creating parameter type list')

Lines 1180-1188

  1180                 if desc and desc[1] == uuid.UUID:  # Column type code at index 1
  1181                     self._uuid_indices.append(i)
  1182                 # Verify we have complete description tuples (7 items per PEP-249)
  1183                 elif desc and len(desc) != 7:
! 1184                     logger.debug(
  1185                         "warning",
  1186                         f"Column description at index {i} has incorrect tuple length: {len(desc)}",
  1187                     )
  1188             self.rowcount = -1

Lines 1220-1231

  1220             column_metadata = []
  1221             try:
  1222                 ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
  1223             except InterfaceError as e:
! 1224                 logger.error( f"Driver interface error during metadata retrieval: {e}")
  1225             except Exception as e:  # pylint: disable=broad-exception-caught
  1226                 # Log the exception with appropriate context
! 1227                 logger.debug(
  1228                     "error",
  1229                     f"Failed to retrieve column metadata: {e}. "
  1230                     f"Using standard ODBC column definitions instead.",
  1231                 )

Lines 1750-1760

  1750                     ddbc_sql_const.SQL_ATTR_QUERY_TIMEOUT.value,
  1751                     timeout_value,
  1752                 )
  1753                 check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
! 1754                 logger.debug( f"Set query timeout to {self._timeout} seconds")
  1755             except Exception as e:  # pylint: disable=broad-exception-caught
! 1756                 logger.warning( f"Failed to set query timeout: {e}")
  1757 
  1758         # Get sample row for parameter type detection and validation
  1759         sample_row = (
  1760             seq_of_parameters[0]

Lines 2257-2265

  2257 
  2258                 if sys and sys._is_finalizing():
  2259                     # Suppress logging during interpreter shutdown
  2260                     return
! 2261                 logger.debug( "Exception during cursor cleanup in __del__: %s", e)
  2262 
  2263     def scroll(self, value: int, mode: str = "relative") -> None:  # pylint: disable=too-many-branches
  2264         """
  2265         Scroll using SQLFetchScroll only, matching test semantics:

Lines 2466-2474

  2466             )
  2467 
  2468         except Exception as e:  # pylint: disable=broad-exception-caught
  2469             # Log the error and re-raise
! 2470             logger.error( f"Error executing tables query: {e}")
  2471             raise
  2472 
  2473     def callproc(
  2474         self, procname: str, parameters: Optional[Sequence[Any]] = None

mssql_python/exceptions.py

Lines 523-531

  523         string_second = error_message[error_message.index("]") + 1 :]
  524         string_third = string_second[string_second.index("]") + 1 :]
  525         return string_first + string_third
  526     except Exception as e:
! 527         logger.error("Error while truncating error message: %s", e)
  528         return error_message
  529 
  530 
  531 def raise_exception(sqlstate: str, ddbc_error: str) -> None:

mssql_python/helpers.py

Lines 57-65

  57         logger.finest('add_driver_to_connection_str: Driver added (had_existing=%s, attr_count=%d)', 
  58             str(driver_found), len(final_connection_attributes))
  59 
  60     except Exception as e:
! 61         logger.finer('add_driver_to_connection_str: Failed to process connection string - %s', str(e))
  62         raise ValueError(
  63             "Invalid connection string, Please follow the format: "
  64             "Server=server_name;Database=database_name;UID=user_name;PWD=password"
  65         ) from e

Lines 111-119

  111             # Overwrite the value with 'MSSQL-Python'
  112             app_found = True
  113             key, _ = param.split("=", 1)
  114             modified_parameters.append(f"{key}=MSSQL-Python")
! 115             logger.finest('add_driver_name_to_app_parameter: Existing APP parameter overwritten')
  116         else:
  117             # Keep other parameters as is
  118             modified_parameters.append(param)

Lines 156-164

  156     """
  157     logger.finest('sanitize_user_input: Sanitizing input (type=%s, length=%d)', 
  158         type(user_input).__name__, len(user_input) if isinstance(user_input, str) else 0)
  159     if not isinstance(user_input, str):
! 160         logger.finest('sanitize_user_input: Non-string input detected')
  161         return "<non-string>"
  162 
  163     # Remove control characters and non-printable characters
  164     # Allow alphanumeric, dash, underscore, and dot (common in encoding names)

mssql_python/logging.py

Lines 55-63

  55     def __init__(self):
  56         """Initialize the logger (only once)"""
  57         # Skip if already initialized
  58         if hasattr(self, '_initialized'):
! 59             return
  60         
  61         self._initialized = True
  62         
  63         # Create the underlying Python logger

Lines 80-88

  80             str: Path to the log file
  81         """
  82         # Clear any existing handlers
  83         if self._logger.handlers:
! 84             self._logger.handlers.clear()
  85         
  86         # Create log file in current working directory (not package directory)
  87         timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  88         pid = os.getpid()

Lines 215-223

  215         self._log(logging.ERROR, f"[Python] {msg}", *args, **kwargs)
  216     
  217     def critical(self, msg: str, *args, **kwargs):
  218         """Log at CRITICAL level"""
! 219         self._log(logging.CRITICAL, f"[Python] {msg}", *args, **kwargs)
  220     
  221     def log(self, level: int, msg: str, *args, **kwargs):
  222         """Log a message at the specified level"""
  223         self._log(level, f"[Python] {msg}", *args, **kwargs)

Lines 270-278

  270     
  271     @property
  272     def handlers(self) -> list:
  273         """Get list of handlers attached to the logger"""
! 274         return self._logger.handlers
  275     
  276     def reset_handlers(self):
  277         """
  278         Reset/recreate file handler.

Lines 298-308

  298             # Import here to avoid circular dependency
  299             from . import ddbc_bindings
  300             if hasattr(ddbc_bindings, 'update_log_level'):
  301                 ddbc_bindings.update_log_level(level)
! 302         except (ImportError, AttributeError):
  303             # C++ bindings not available or not yet initialized
! 304             pass
  305     
  306     # Properties
  307     
  308     @property

Lines 333-347

  333         log_level: Logging level (maps to closest FINE/FINER/FINEST)
  334     """
  335     # Map old levels to new levels
  336     if log_level <= FINEST:
! 337         logger.setLevel(FINEST)
  338     elif log_level <= FINER:
  339         logger.setLevel(FINER)
! 340     elif log_level <= FINE:
! 341         logger.setLevel(FINE)
  342     else:
! 343         logger.setLevel(log_level)
  344     
  345     return logger
  346 

mssql_python/pybind/connection/connection.cpp

Lines 20-28

  20 static SqlHandlePtr getEnvHandle() {
  21     static SqlHandlePtr envHandle = []() -> SqlHandlePtr {
  22         LOG_FINER("Allocating ODBC environment handle");
  23         if (!SQLAllocHandle_ptr) {
! 24             LOG_FINER("Function pointers not initialized, loading driver");
  25             DriverLoader::getInstance().loadDriver();
  26         }
  27         SQLHANDLE env = nullptr;
  28         SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_ENV, SQL_NULL_HANDLE,

Lines 217-225

  217 
  218             // Convert to wide string
  219             std::wstring wstr = Utf8ToWString(utf8_str);
  220             if (wstr.empty() && !utf8_str.empty()) {
! 221                 LOG_FINER("Failed to convert string value to wide string for attribute=%d", attribute);
  222                 return SQL_ERROR;
  223             }
  224 
  225             // Limit static buffer growth for memory safety

Lines 238-246

  238 #if defined(__APPLE__) || defined(__linux__)
  239             // For macOS/Linux, convert wstring to SQLWCHAR buffer
  240             std::vector<SQLWCHAR> sqlwcharBuffer = WStringToSQLWCHAR(wstr);
  241             if (sqlwcharBuffer.empty() && !wstr.empty()) {
! 242                 LOG_FINER("Failed to convert wide string to SQLWCHAR buffer for attribute=%d", attribute);
  243                 return SQL_ERROR;
  244             }
  245 
  246             ptr = sqlwcharBuffer.data();

Lines 261-269

  261                 LOG_FINER("Set string attribute=%d successfully", attribute);
  262             }
  263             return ret;
  264         } catch (const std::exception& e) {
! 265             LOG_FINER("Exception during string attribute=%d setting: %s", attribute, e.what());
  266             return SQL_ERROR;
  267         }
  268     } else if (py::isinstance<py::bytes>(value) ||
  269                py::isinstance<py::bytearray>(value)) {

Lines 288-304

  288                                                   attribute, ptr, length);
  289             if (!SQL_SUCCEEDED(ret)) {
  290                 LOG_FINER("Failed to set binary attribute=%d, ret=%d", attribute, ret);
  291             } else {
! 292                 LOG_FINER("Set binary attribute=%d successfully (length=%d)", attribute, length);
  293             }
  294             return ret;
  295         } catch (const std::exception& e) {
! 296             LOG_FINER("Exception during binary attribute=%d setting: %s", attribute, e.what());
  297             return SQL_ERROR;
  298         }
  299     } else {
! 300         LOG_FINER("Unsupported attribute value type for attribute=%d", attribute);
  301         return SQL_ERROR;
  302     }
  303 }

Lines 344-352

  344         SQL_ATTR_RESET_CONNECTION,
  345         (SQLPOINTER)SQL_RESET_CONNECTION_YES,
  346         SQL_IS_INTEGER);
  347     if (!SQL_SUCCEEDED(ret)) {
! 348         LOG_FINER("Failed to reset connection (ret=%d). Marking as dead.", ret);
  349         disconnect();
  350         return false;
  351     }
  352     updateLastUsed();

mssql_python/pybind/connection/connection_pool.cpp

Lines 71-79

  71     for (auto& conn : to_disconnect) {
  72         try {
  73             conn->disconnect();
  74         } catch (const std::exception& ex) {
! 75             LOG_FINER("Disconnect bad/expired connections failed: %s", ex.what());
  76         }
  77     }
  78     return valid_conn;
  79 }

Lines 102-110

  102     for (auto& conn : to_close) {
  103         try {
  104             conn->disconnect();
  105         } catch (const std::exception& ex) {
! 106             LOG_FINER("ConnectionPool::close: disconnect failed: %s", ex.what());
  107         }
  108     }
  109 }

mssql_python/pybind/ddbc_bindings.cpp

Lines 278-286

  278                     !py::isinstance<py::bytes>(param)) {
  279                     ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
  280                 }
  281                 if (paramInfo.isDAE) {
! 282                     LOG_FINER("BindParameters: param[%d] SQL_C_CHAR - Using DAE (Data-At-Execution) for large string streaming", paramIndex);
  283                     dataPtr = const_cast<void*>(reinterpret_cast<const void*>(&paramInfos[paramIndex]));
  284                     strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
  285                     *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0);
  286                     bufferLength = 0;

Lines 380-388

  380                         &describedDigits,
  381                         &nullable
  382                     );
  383                     if (!SQL_SUCCEEDED(rc)) {
! 384                         LOG_FINER("BindParameters: SQLDescribeParam failed for param[%d] (NULL parameter) - SQLRETURN=%d", paramIndex, rc);
  385                         return rc;
  386                     }
  387                     sqlType       = describedType;
  388                     columnSize    = describedSize;

Lines 591-600

  591                 }
  592                 py::bytes uuid_bytes = param.cast<py::bytes>();
  593                 const unsigned char* uuid_data = reinterpret_cast<const unsigned char*>(PyBytes_AS_STRING(uuid_bytes.ptr()));
  594                 if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
! 595                     LOG_FINER("BindParameters: param[%d] SQL_C_GUID - Invalid UUID length: expected 16 bytes, got %ld bytes", 
! 596                               paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()));
  597                     ThrowStdException("UUID binary data must be exactly 16 bytes long.");
  598                 }
  599                 SQLGUID* guid_data_ptr = AllocateParamBuffer<SQLGUID>(paramBuffers);
  600                 guid_data_ptr->Data1 =

Lines 640-653

  640         if (paramInfo.paramCType == SQL_C_NUMERIC) {
  641             SQLHDESC hDesc = nullptr;
  642             rc = SQLGetStmtAttr_ptr(hStmt, SQL_ATTR_APP_PARAM_DESC, &hDesc, 0, NULL);
  643             if(!SQL_SUCCEEDED(rc)) {
! 644                 LOG_FINER("BindParameters: SQLGetStmtAttr(SQL_ATTR_APP_PARAM_DESC) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
  645                 return rc;
  646             }
  647             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE, (SQLPOINTER) SQL_C_NUMERIC, 0);
  648             if(!SQL_SUCCEEDED(rc)) {
! 649                 LOG_FINER("BindParameters: SQLSetDescField(SQL_DESC_TYPE) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
  650                 return rc;
  651             }
  652             SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
  653             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,

Lines 652-660

  652             SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
  653             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,
  654 			             (SQLPOINTER) numericPtr->precision, 0);
  655             if(!SQL_SUCCEEDED(rc)) {
! 656                 LOG_FINER("BindParameters: SQLSetDescField(SQL_DESC_PRECISION) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
  657                 return rc;
  658             }
  659 
  660             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,

Lines 659-667

  659 
  660             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,
  661 			             (SQLPOINTER) numericPtr->scale, 0);
  662             if(!SQL_SUCCEEDED(rc)) {
! 663                 LOG_FINER("BindParameters: SQLSetDescField(SQL_DESC_SCALE) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
  664                 return rc;
  665             }
  666 
  667             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0);

Lines 665-673

  665             }
  666 
  667             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0);
  668             if(!SQL_SUCCEEDED(rc)) {
! 669                 LOG_FINER("BindParameters: SQLSetDescField(SQL_DESC_DATA_PTR) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
  670                 return rc;
  671             }
  672         }
  673     }

Lines 745-753

  745     if (pos != std::string::npos) {
  746         std::string dir = module_file.substr(0, pos);
  747         return dir;
  748     }
! 749     LOG_FINEST("GetModuleDirectory: Could not extract directory from module path - path='%s'", module_file.c_str());
  750     return module_file;
  751 #endif
  752 }

Lines 768-777

  768 #else
  769     // macOS/Unix: Use dlopen
  770     void* handle = dlopen(driverPath.c_str(), RTLD_LAZY);
  771     if (!handle) {
! 772         LOG_FINER("LoadDriverLibrary: dlopen failed for path='%s' - %s", 
! 773                   driverPath.c_str(), dlerror() ? dlerror() : "unknown error");
  774     }
  775     return handle;
  776 #endif
  777 }

Lines 913-922

  913     }
  914 
  915     DriverHandle handle = LoadDriverLibrary(driverPath.string());
  916     if (!handle) {
! 917         LOG_FINER("LoadDriverOrThrowException: Failed to load ODBC driver - path='%s', error='%s'", 
! 918                   driverPath.string().c_str(), GetLastErrorMessage().c_str());
  919         ThrowStdException("Failed to load the driver. Please read the documentation (https://github.com/microsoft/mssql-python#installation) to install the required dependencies.");
  920     }
  921     LOG_FINER("LoadDriverOrThrowException: ODBC driver library loaded successfully from '%s'", driverPath.string().c_str());

Lines 1296-1304

  1296 ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode) {
  1297     LOG_FINER("SQLCheckError: Checking ODBC errors - handleType=%d, retcode=%d", handleType, retcode);
  1298     ErrorInfo errorInfo;
  1299     if (retcode == SQL_INVALID_HANDLE) {
! 1300         LOG_FINER("SQLCheckError: SQL_INVALID_HANDLE detected - handle is invalid");
  1301         errorInfo.ddbcErrorMsg = std::wstring( L"Invalid handle!");
  1302         return errorInfo;
  1303     }
  1304     assert(handle != 0);

Lines 1304-1312

  1304     assert(handle != 0);
  1305     SQLHANDLE rawHandle = handle->get();
  1306     if (!SQL_SUCCEEDED(retcode)) {
  1307         if (!SQLGetDiagRec_ptr) {
! 1308             LOG_FINER("SQLCheckError: SQLGetDiagRec function pointer not initialized, loading driver");
  1309             DriverLoader::getInstance().loadDriver();  // Load the driver
  1310         }
  1311 
  1312         SQLWCHAR sqlState[6], message[SQL_MAX_MESSAGE_LENGTH];

Lines 1335-1343

  1335 py::list SQLGetAllDiagRecords(SqlHandlePtr handle) {
  1336     LOG_FINER("SQLGetAllDiagRecords: Retrieving all diagnostic records for handle %p, handleType=%d", 
  1337               (void*)handle->get(), handle->type());
  1338     if (!SQLGetDiagRec_ptr) {
! 1339         LOG_FINER("SQLGetAllDiagRecords: SQLGetDiagRec function pointer not initialized, loading driver");
  1340         DriverLoader::getInstance().loadDriver();
  1341     }
  1342     
  1343     py::list records;

Lines 1399-1411

  1399 }
  1400 
  1401 // Wrap SQLExecDirect
  1402 SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Query) {
! 1403     std::string queryUtf8 = WideToUTF8(Query);
! 1404     LOG_FINE("SQLExecDirect: Executing query directly - statement_handle=%p, query_length=%zu chars", 
! 1405              (void*)StatementHandle->get(), Query.length());
  1406     if (!SQLExecDirect_ptr) {
! 1407         LOG_FINER("SQLExecDirect: Function pointer not initialized, loading driver");
  1408         DriverLoader::getInstance().loadDriver();  // Load the driver
  1409     }
  1410 
  1411     // Ensure statement is scrollable BEFORE executing

Lines 1428-1436

  1428     queryPtr = const_cast<SQLWCHAR*>(Query.c_str());
  1429 #endif
  1430     SQLRETURN ret = SQLExecDirect_ptr(StatementHandle->get(), queryPtr, SQL_NTS);
  1431     if (!SQL_SUCCEEDED(ret)) {
! 1432         LOG_FINER("SQLExecDirect: Query execution failed - SQLRETURN=%d", ret);
  1433     }
  1434     return ret;
  1435 }

Lines 1441-1449

  1441                          const std::wstring& table,
  1442                          const std::wstring& tableType) {
  1443     
  1444     if (!SQLTables_ptr) {
! 1445         LOG_FINER("SQLTables: Function pointer not initialized, loading driver");
  1446         DriverLoader::getInstance().loadDriver();
  1447     }
  1448 
  1449     SQLWCHAR* catalogPtr = nullptr;

Lines 1526-1534

  1526                           py::list& isStmtPrepared, const bool usePrepare = true) {
  1527     LOG_FINE("SQLExecute: Executing %s query - statement_handle=%p, param_count=%zu, query_length=%zu chars", 
  1528              (params.size() > 0 ? "parameterized" : "direct"), (void*)statementHandle->get(), params.size(), query.length());
  1529     if (!SQLPrepare_ptr) {
! 1530         LOG_FINER("SQLExecute: Function pointer not initialized, loading driver");
  1531         DriverLoader::getInstance().loadDriver();  // Load the driver
  1532     }
  1533     assert(SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && SQLExecDirect_ptr);

Lines 1539-1547

  1539 
  1540     RETCODE rc;
  1541     SQLHANDLE hStmt = statementHandle->get();
  1542     if (!statementHandle || !statementHandle->get()) {
! 1543         LOG_FINER("SQLExecute: Statement handle is null or invalid");
  1544     }
  1545 
  1546     // Ensure statement is scrollable BEFORE executing
  1547     if (SQLSetStmtAttr_ptr && hStmt) {

Lines 1579-1587

  1579         assert(isStmtPrepared.size() == 1);
  1580         if (usePrepare) {
  1581             rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS);
  1582             if (!SQL_SUCCEEDED(rc)) {
! 1583                 LOG_FINER("SQLExecute: SQLPrepare failed - SQLRETURN=%d, statement_handle=%p", rc, (void*)hStmt);
  1584                 return rc;
  1585             }
  1586             isStmtPrepared[0] = py::cast(true);
  1587         } else {

Lines 1644-1653

  1644                                 ThrowStdException("Chunk size exceeds maximum allowed by SQLLEN");
  1645                             }
  1646                             rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(lenBytes));
  1647                             if (!SQL_SUCCEEDED(rc)) {
! 1648                                 LOG_FINEST("SQLExecute: SQLPutData failed for SQL_C_WCHAR chunk - offset=%zu, total_chars=%zu, chunk_bytes=%zu, SQLRETURN=%d", 
! 1649                                            offset, totalChars, lenBytes, rc);
  1650                                 return rc;
  1651                             }
  1652                             offset += len;
  1653                         }

Lines 1661-1670

  1661                             size_t len = std::min(chunkBytes, totalBytes - offset);
  1662 
  1663                             rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
  1664                             if (!SQL_SUCCEEDED(rc)) {
! 1665                                 LOG_FINEST("SQLExecute: SQLPutData failed for SQL_C_CHAR chunk - offset=%zu, total_bytes=%zu, chunk_bytes=%zu, SQLRETURN=%d", 
! 1666                                            offset, totalBytes, len, rc);
  1667                                 return rc;
  1668                             }
  1669                             offset += len;
  1670                         }

Lines 1680-1689

  1680                     for (size_t offset = 0; offset < totalBytes; offset += chunkSize) {
  1681                         size_t len = std::min(chunkSize, totalBytes - offset);
  1682                         rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
  1683                         if (!SQL_SUCCEEDED(rc)) {
! 1684                             LOG_FINEST("SQLExecute: SQLPutData failed for binary/bytes chunk - offset=%zu, total_bytes=%zu, chunk_bytes=%zu, SQLRETURN=%d", 
! 1685                                        offset, totalBytes, len, rc);
  1686                             return rc;
  1687                         }
  1688                     }
  1689                 } else {

Lines 1690-1699

  1690                     ThrowStdException("DAE only supported for str or bytes");
  1691                 }
  1692             }
  1693             if (!SQL_SUCCEEDED(rc)) {
! 1694                 LOG_FINER("SQLExecute: SQLParamData final call %s - SQLRETURN=%d", 
! 1695                           (rc == SQL_NO_DATA ? "completed with no data" : "failed"), rc);
  1696                 return rc;
  1697             }
  1698             LOG_FINER("SQLExecute: DAE streaming completed successfully, SQLExecute resumed");
  1699         }

Lines 1725-1734

  1725             const ParamInfo& info = paramInfos[paramIndex];
  1726             LOG_FINEST("BindParameterArray: Processing param_index=%d, C_type=%d, SQL_type=%d, column_size=%zu, decimal_digits=%d", 
  1727                        paramIndex, info.paramCType, info.paramSQLType, info.columnSize, info.decimalDigits);
  1728             if (columnValues.size() != paramSetSize) {
! 1729                 LOG_FINER("BindParameterArray: Size mismatch - param_index=%d, expected=%zu, actual=%zu", 
! 1730                           paramIndex, paramSetSize, columnValues.size());
  1731                 ThrowStdException("Column " + std::to_string(paramIndex) + " has mismatched size.");
  1732             }
  1733             void* dataPtr = nullptr;
  1734             SQLLEN* strLenOrIndArray = nullptr;

Lines 1743-1751

  1743                             if (!strLenOrIndArray)
  1744                                 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1745                             dataArray[i] = 0;
  1746                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 1747                             null_count++;
  1748                         } else {
  1749                             dataArray[i] = columnValues[i].cast<int>();
  1750                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;
  1751                         }

Lines 1754-1764

  1754                     dataPtr = dataArray;
  1755                     break;
  1756                 }
  1757                 case SQL_C_DOUBLE: {
! 1758                     LOG_FINEST("BindParameterArray: Binding SQL_C_DOUBLE array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  1759                     double* dataArray = AllocateParamBufferArray<double>(tempBuffers, paramSetSize);
! 1760                     size_t null_count = 0;
  1761                     for (size_t i = 0; i < paramSetSize; ++i) {
  1762                         if (columnValues[i].is_none()) {
  1763                             if (!strLenOrIndArray)
  1764                                 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);

Lines 1763-1771

  1763                             if (!strLenOrIndArray)
  1764                                 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1765                             dataArray[i] = 0;
  1766                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 1767                             null_count++;
  1768                         } else {
  1769                             dataArray[i] = columnValues[i].cast<double>();
  1770                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;
  1771                         }

Lines 1769-1777

  1769                             dataArray[i] = columnValues[i].cast<double>();
  1770                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;
  1771                         }
  1772                     }
! 1773                     LOG_FINEST("BindParameterArray: SQL_C_DOUBLE bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  1774                     dataPtr = dataArray;
  1775                     break;
  1776                 }
  1777                 case SQL_C_WCHAR: {

Lines 1794-1805

  1794                             total_chars += utf16_len;
  1795                             // Check UTF-16 length (excluding null terminator) against column size
  1796                             if (utf16Buf.size() > 0 && utf16_len > info.columnSize) {
  1797                                 std::string offending = WideToUTF8(wstr);
! 1798                                 LOG_FINER("BindParameterArray: SQL_C_WCHAR string too long - param_index=%d, row=%zu, utf16_length=%zu, max=%zu",
! 1799                                          paramIndex, i, utf16_len, info.columnSize);
  1800                                 ThrowStdException("Input string UTF-16 length exceeds allowed column size at parameter index " + std::to_string(paramIndex) + 
! 1801                                     ". UTF-16 length: " + std::to_string(utf16_len) + ", Column size: " + std::to_string(info.columnSize));
  1802                             }
  1803                             // If we reach here, the UTF-16 string fits - copy it completely
  1804                             std::memcpy(wcharArray + i * (info.columnSize + 1), utf16Buf.data(), utf16Buf.size() * sizeof(SQLWCHAR));
  1805 #else

Lines 1833-1842

  1833                             null_count++;
  1834                         } else {
  1835                             int intVal = columnValues[i].cast<int>();
  1836                             if (intVal < 0 || intVal > 255) {
! 1837                                 LOG_FINER("BindParameterArray: TINYINT value out of range - param_index=%d, row=%zu, value=%d",
! 1838                                          paramIndex, i, intVal);
  1839                                 ThrowStdException("UTINYINT value out of range at rowIndex " + std::to_string(i));
  1840                             }
  1841                             dataArray[i] = static_cast<unsigned char>(intVal);
  1842                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;

Lines 1856-1870

  1856                             if (!strLenOrIndArray)
  1857                                 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1858                             dataArray[i] = 0;
  1859                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 1860                             null_count++;
  1861                         } else {
  1862                             int intVal = columnValues[i].cast<int>();
  1863                             if (intVal < std::numeric_limits<short>::min() ||
  1864                                 intVal > std::numeric_limits<short>::max()) {
! 1865                                 LOG_FINER("BindParameterArray: SHORT value out of range - param_index=%d, row=%zu, value=%d",
! 1866                                          paramIndex, i, intVal);
  1867                                 ThrowStdException("SHORT value out of range at rowIndex " + std::to_string(i));
  1868                             }
  1869                             dataArray[i] = static_cast<short>(intVal);
  1870                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;

Lines 1890-1901

  1890                         } else {
  1891                             std::string str = columnValues[i].cast<std::string>();
  1892                             total_bytes += str.size();
  1893                             if (str.size() > info.columnSize) {
! 1894                                 LOG_FINER("BindParameterArray: String/binary too long - param_index=%d, row=%zu, size=%zu, max=%zu",
! 1895                                          paramIndex, i, str.size(), info.columnSize);
  1896                                 ThrowStdException("Input exceeds column size at index " + std::to_string(i));
! 1897                             }
  1898                             std::memcpy(charArray + i * (info.columnSize + 1), str.c_str(), str.size());
  1899                             strLenOrIndArray[i] = static_cast<SQLLEN>(str.size());
  1900                         }
  1901                     }

Lines 1905-1930

  1905                     bufferLength = info.columnSize + 1;
  1906                     break;
  1907                 }
  1908                 case SQL_C_BIT: {
! 1909                     LOG_FINEST("BindParameterArray: Binding SQL_C_BIT array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  1910                     char* boolArray = AllocateParamBufferArray<char>(tempBuffers, paramSetSize);
  1911                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 1912                     size_t null_count = 0, true_count = 0;
  1913                     for (size_t i = 0; i < paramSetSize; ++i) {
  1914                         if (columnValues[i].is_none()) {
  1915                             boolArray[i] = 0;
  1916                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 1917                             null_count++;
  1918                         } else {
! 1919                             bool val = columnValues[i].cast<bool>();
! 1920                             boolArray[i] = val ? 1 : 0;
! 1921                             if (val) true_count++;
  1922                             strLenOrIndArray[i] = 0;
  1923                         }
  1924                     }
! 1925                     LOG_FINEST("BindParameterArray: SQL_C_BIT bound - param_index=%d, null_values=%zu, true_values=%zu",
! 1926                                paramIndex, null_count, true_count);
  1927                     dataPtr = boolArray;
  1928                     bufferLength = sizeof(char);
  1929                     break;
  1930                 }

Lines 1929-1945

  1929                     break;
  1930                 }
  1931                 case SQL_C_STINYINT:
  1932                 case SQL_C_USHORT: {
! 1933                     LOG_FINEST("BindParameterArray: Binding SQL_C_USHORT/STINYINT array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  1934                     unsigned short* dataArray = AllocateParamBufferArray<unsigned short>(tempBuffers, paramSetSize);
  1935                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 1936                     size_t null_count = 0;
  1937                     for (size_t i = 0; i < paramSetSize; ++i) {
  1938                         if (columnValues[i].is_none()) {
  1939                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1940                             dataArray[i] = 0;
! 1941                             null_count++;
  1942                         } else {
  1943                             dataArray[i] = columnValues[i].cast<unsigned short>();
  1944                             strLenOrIndArray[i] = 0;
  1945                         }

Lines 1943-1951

  1943                             dataArray[i] = columnValues[i].cast<unsigned short>();
  1944                             strLenOrIndArray[i] = 0;
  1945                         }
  1946                     }
! 1947                     LOG_FINEST("BindParameterArray: SQL_C_USHORT bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  1948                     dataPtr = dataArray;
  1949                     bufferLength = sizeof(unsigned short);
  1950                     break;
  1951                 }

Lines 1960-1968

  1960                     for (size_t i = 0; i < paramSetSize; ++i) {
  1961                         if (columnValues[i].is_none()) {
  1962                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1963                             dataArray[i] = 0;
! 1964                             null_count++;
  1965                         } else {
  1966                             dataArray[i] = columnValues[i].cast<int64_t>();
  1967                             strLenOrIndArray[i] = 0;
  1968                         }

Lines 1980-1988

  1980                     for (size_t i = 0; i < paramSetSize; ++i) {
  1981                         if (columnValues[i].is_none()) {
  1982                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1983                             dataArray[i] = 0.0f;
! 1984                             null_count++;
  1985                         } else {
  1986                             dataArray[i] = columnValues[i].cast<float>();
  1987                             strLenOrIndArray[i] = 0;
  1988                         }

Lines 1992-2008

  1992                     bufferLength = sizeof(float);
  1993                     break;
  1994                 }
  1995                 case SQL_C_TYPE_DATE: {
! 1996                     LOG_FINEST("BindParameterArray: Binding SQL_C_TYPE_DATE array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  1997                     SQL_DATE_STRUCT* dateArray = AllocateParamBufferArray<SQL_DATE_STRUCT>(tempBuffers, paramSetSize);
  1998                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 1999                     size_t null_count = 0;
  2000                     for (size_t i = 0; i < paramSetSize; ++i) {
  2001                         if (columnValues[i].is_none()) {
  2002                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2003                             std::memset(&dateArray[i], 0, sizeof(SQL_DATE_STRUCT));
! 2004                             null_count++;
  2005                         } else {
  2006                             py::object dateObj = columnValues[i];
  2007                             dateArray[i].year = dateObj.attr("year").cast<SQLSMALLINT>();
  2008                             dateArray[i].month = dateObj.attr("month").cast<SQLUSMALLINT>();

Lines 2009-2017

  2009                             dateArray[i].day = dateObj.attr("day").cast<SQLUSMALLINT>();
  2010                             strLenOrIndArray[i] = 0;
  2011                         }
  2012                     }
! 2013                     LOG_FINEST("BindParameterArray: SQL_C_TYPE_DATE bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  2014                     dataPtr = dateArray;
  2015                     bufferLength = sizeof(SQL_DATE_STRUCT);
  2016                     break;
  2017                 }

Lines 2015-2031

  2015                     bufferLength = sizeof(SQL_DATE_STRUCT);
  2016                     break;
  2017                 }
  2018                 case SQL_C_TYPE_TIME: {
! 2019                     LOG_FINEST("BindParameterArray: Binding SQL_C_TYPE_TIME array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  2020                     SQL_TIME_STRUCT* timeArray = AllocateParamBufferArray<SQL_TIME_STRUCT>(tempBuffers, paramSetSize);
  2021                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 2022                     size_t null_count = 0;
  2023                     for (size_t i = 0; i < paramSetSize; ++i) {
  2024                         if (columnValues[i].is_none()) {
  2025                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2026                             std::memset(&timeArray[i], 0, sizeof(SQL_TIME_STRUCT));
! 2027                             null_count++;
  2028                         } else {
  2029                             py::object timeObj = columnValues[i];
  2030                             timeArray[i].hour = timeObj.attr("hour").cast<SQLUSMALLINT>();
  2031                             timeArray[i].minute = timeObj.attr("minute").cast<SQLUSMALLINT>();

Lines 2032-2040

  2032                             timeArray[i].second = timeObj.attr("second").cast<SQLUSMALLINT>();
  2033                             strLenOrIndArray[i] = 0;
  2034                         }
  2035                     }
! 2036                     LOG_FINEST("BindParameterArray: SQL_C_TYPE_TIME bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  2037                     dataPtr = timeArray;
  2038                     bufferLength = sizeof(SQL_TIME_STRUCT);
  2039                     break;
  2040                 }

Lines 2038-2054

  2038                     bufferLength = sizeof(SQL_TIME_STRUCT);
  2039                     break;
  2040                 }
  2041                 case SQL_C_TYPE_TIMESTAMP: {
! 2042                     LOG_FINEST("BindParameterArray: Binding SQL_C_TYPE_TIMESTAMP array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  2043                     SQL_TIMESTAMP_STRUCT* tsArray = AllocateParamBufferArray<SQL_TIMESTAMP_STRUCT>(tempBuffers, paramSetSize);
  2044                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 2045                     size_t null_count = 0;
  2046                     for (size_t i = 0; i < paramSetSize; ++i) {
  2047                         if (columnValues[i].is_none()) {
  2048                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2049                             std::memset(&tsArray[i], 0, sizeof(SQL_TIMESTAMP_STRUCT));
! 2050                             null_count++;
  2051                         } else {
  2052                             py::object dtObj = columnValues[i];
  2053                             tsArray[i].year = dtObj.attr("year").cast<SQLSMALLINT>();
  2054                             tsArray[i].month = dtObj.attr("month").cast<SQLUSMALLINT>();

Lines 2059-2067

  2059                             tsArray[i].fraction = static_cast<SQLUINTEGER>(dtObj.attr("microsecond").cast<int>() * 1000);  // µs to ns
  2060                             strLenOrIndArray[i] = 0;
  2061                         }
  2062                     }
! 2063                     LOG_FINEST("BindParameterArray: SQL_C_TYPE_TIMESTAMP bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  2064                     dataPtr = tsArray;
  2065                     bufferLength = sizeof(SQL_TIMESTAMP_STRUCT);
  2066                     break;
  2067                 }

Lines 2078-2086

  2078 
  2079                         if (param.is_none()) {
  2080                             std::memset(&dtoArray[i], 0, sizeof(DateTimeOffset));
  2081                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 2082                             null_count++;
  2083                         } else {
  2084                             if (!py::isinstance(param, datetimeType)) {
  2085                                 ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2086                             }

Lines 2116-2127

  2116                     bufferLength = sizeof(DateTimeOffset);
  2117                     break;
  2118                 }
  2119                 case SQL_C_NUMERIC: {
! 2120                     LOG_FINEST("BindParameterArray: Binding SQL_C_NUMERIC array - param_index=%d, count=%zu", paramIndex, paramSetSize);
  2121                     SQL_NUMERIC_STRUCT* numericArray = AllocateParamBufferArray<SQL_NUMERIC_STRUCT>(tempBuffers, paramSetSize);
  2122                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
! 2123                     size_t null_count = 0;
  2124                     for (size_t i = 0; i < paramSetSize; ++i) {
  2125                         const py::handle& element = columnValues[i];
  2126                         if (element.is_none()) {
  2127                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2125-2142

  2125                         const py::handle& element = columnValues[i];
  2126                         if (element.is_none()) {
  2127                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2128                             std::memset(&numericArray[i], 0, sizeof(SQL_NUMERIC_STRUCT));
! 2129                             null_count++;
  2130                             continue;
  2131                         }
  2132                         if (!py::isinstance<NumericData>(element)) {
! 2133                             LOG_FINER("BindParameterArray: NUMERIC type mismatch - param_index=%d, row=%zu", paramIndex, i);
  2134                             throw std::runtime_error(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2135                         }
  2136                         NumericData decimalParam = element.cast<NumericData>();
! 2137                         LOG_FINEST("BindParameterArray: NUMERIC value - param_index=%d, row=%zu, precision=%d, scale=%d, sign=%d",
! 2138                                   paramIndex, i, decimalParam.precision, decimalParam.scale, decimalParam.sign);
  2139                         SQL_NUMERIC_STRUCT& target = numericArray[i];
  2140                         std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
  2141                         target.precision = decimalParam.precision;
  2142                         target.scale = decimalParam.scale;

Lines 2146-2154

  2146                             std::memcpy(target.val, decimalParam.val.data(), copyLen);
  2147                         }
  2148                         strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT);
  2149                     }
! 2150                     LOG_FINEST("BindParameterArray: SQL_C_NUMERIC bound - param_index=%d, null_values=%zu", paramIndex, null_count);
  2151                     dataPtr = numericArray;
  2152                     bufferLength = sizeof(SQL_NUMERIC_STRUCT);
  2153                     break;
  2154                 }

Lines 2173-2186

  2173                         }
  2174                         else if (py::isinstance<py::bytes>(element)) {
  2175                             py::bytes b = element.cast<py::bytes>();
  2176                             if (PyBytes_GET_SIZE(b.ptr()) != 16) {
! 2177                                 LOG_FINER("BindParameterArray: GUID bytes wrong length - param_index=%d, row=%zu, length=%d",
! 2178                                          paramIndex, i, PyBytes_GET_SIZE(b.ptr()));
  2179                                 ThrowStdException("UUID binary data must be exactly 16 bytes long.");
  2180                             }
  2181                             std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
! 2182                             bytes_count++;
  2183                         }
  2184                         else if (py::isinstance(element, uuid_class)) {
  2185                             py::bytes b = element.attr("bytes_le").cast<py::bytes>();
  2186                             std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);

Lines 2186-2194

  2186                             std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
  2187                             uuid_count++;
  2188                         }
  2189                         else {
! 2190                             LOG_FINER("BindParameterArray: GUID type mismatch - param_index=%d, row=%zu", paramIndex, i);
  2191                             ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2192                         }
  2193                         guidArray[i].Data1 = (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
  2194                                             (static_cast<uint32_t>(uuid_bytes[2]) << 16) |

Lines 2207-2215

  2207                     bufferLength = sizeof(SQLGUID);
  2208                     break;
  2209                 }
  2210                 default: {
! 2211                     LOG_FINER("BindParameterArray: Unsupported C type - param_index=%d, C_type=%d", paramIndex, info.paramCType);
  2212                     ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType));
  2213                 }
  2214             }
  2215             LOG_FINEST("BindParameterArray: Calling SQLBindParameter - param_index=%d, buffer_length=%lld", 

Lines 2226-2239

  2226                 bufferLength,
  2227                 strLenOrIndArray
  2228             );
  2229             if (!SQL_SUCCEEDED(rc)) {
! 2230                 LOG_FINER("BindParameterArray: SQLBindParameter failed - param_index=%d, SQLRETURN=%d", paramIndex, rc);
  2231                 return rc;
  2232             }
  2233         }
  2234     } catch (...) {
! 2235         LOG_FINER("BindParameterArray: Exception during binding, cleaning up buffers");
  2236         throw;
  2237     }
  2238     paramBuffers.insert(paramBuffers.end(), tempBuffers.begin(), tempBuffers.end());
  2239     LOG_FINER("BindParameterArray: Successfully bound all parameters - total_params=%zu, buffer_count=%zu",

Lines 2260-2270

  2260     LOG_FINEST("SQLExecuteMany: Using wide string query directly");
  2261 #endif
  2262     RETCODE rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS);
  2263     if (!SQL_SUCCEEDED(rc)) {
! 2264         LOG_FINER("SQLExecuteMany: SQLPrepare failed - rc=%d", rc);
! 2265         return rc;
! 2266     }
  2267     LOG_FINEST("SQLExecuteMany: Query prepared successfully");
  2268 
  2269     bool hasDAE = false;
  2270     for (const auto& p : paramInfos) {

Lines 2278-2294

  2278         LOG_FINER("SQLExecuteMany: Using array binding (non-DAE) - calling BindParameterArray");
  2279         std::vector<std::shared_ptr<void>> paramBuffers;
  2280         rc = BindParameterArray(hStmt, columnwise_params, paramInfos, paramSetSize, paramBuffers);
  2281         if (!SQL_SUCCEEDED(rc)) {
! 2282             LOG_FINER("SQLExecuteMany: BindParameterArray failed - rc=%d", rc);
! 2283             return rc;
! 2284         }
  2285 
  2286         rc = SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)paramSetSize, 0);
  2287         if (!SQL_SUCCEEDED(rc)) {
! 2288             LOG_FINER("SQLExecuteMany: SQLSetStmtAttr(PARAMSET_SIZE) failed - rc=%d", rc);
! 2289             return rc;
! 2290         }
  2291         LOG_FINEST("SQLExecuteMany: PARAMSET_SIZE set to %zu", paramSetSize);
  2292 
  2293         rc = SQLExecute_ptr(hStmt);
  2294         LOG_FINER("SQLExecuteMany: SQLExecute completed - rc=%d", rc);

Lines 2293-2366

  2293         rc = SQLExecute_ptr(hStmt);
  2294         LOG_FINER("SQLExecuteMany: SQLExecute completed - rc=%d", rc);
  2295         return rc;
  2296     } else {
! 2297         LOG_FINER("SQLExecuteMany: Using DAE (data-at-execution) - row_count=%zu", columnwise_params.size());
  2298         size_t rowCount = columnwise_params.size();
  2299         for (size_t rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
! 2300             LOG_FINEST("SQLExecuteMany: Processing DAE row %zu of %zu", rowIndex + 1, rowCount);
  2301             py::list rowParams = columnwise_params[rowIndex];
  2302 
  2303             std::vector<std::shared_ptr<void>> paramBuffers;
  2304             rc = BindParameters(hStmt, rowParams, const_cast<std::vector<ParamInfo>&>(paramInfos), paramBuffers);
! 2305             if (!SQL_SUCCEEDED(rc)) {
! 2306                 LOG_FINER("SQLExecuteMany: BindParameters failed for row %zu - rc=%d", rowIndex, rc);
! 2307                 return rc;
! 2308             }
! 2309             LOG_FINEST("SQLExecuteMany: Parameters bound for row %zu", rowIndex);
  2310 
  2311             rc = SQLExecute_ptr(hStmt);
! 2312             LOG_FINEST("SQLExecuteMany: SQLExecute for row %zu - initial_rc=%d", rowIndex, rc);
! 2313             size_t dae_chunk_count = 0;
  2314             while (rc == SQL_NEED_DATA) {
  2315                 SQLPOINTER token;
  2316                 rc = SQLParamData_ptr(hStmt, &token);
! 2317                 LOG_FINEST("SQLExecuteMany: SQLParamData called - chunk=%zu, rc=%d, token=%p", 
! 2318                           dae_chunk_count, rc, token);
! 2319                 if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2320                     LOG_FINER("SQLExecuteMany: SQLParamData failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2321                     return rc;
! 2322                 }
  2323 
  2324                 py::object* py_obj_ptr = reinterpret_cast<py::object*>(token);
! 2325                 if (!py_obj_ptr) {
! 2326                     LOG_FINER("SQLExecuteMany: NULL token pointer in DAE - chunk=%zu", dae_chunk_count);
! 2327                     return SQL_ERROR;
! 2328                 }
  2329 
  2330                 if (py::isinstance<py::str>(*py_obj_ptr)) {
  2331                     std::string data = py_obj_ptr->cast<std::string>();
  2332                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2333                     LOG_FINEST("SQLExecuteMany: Sending string DAE data - chunk=%zu, length=%lld", 
! 2334                               dae_chunk_count, static_cast<long long>(data_len));
  2335                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
! 2336                     if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2337                         LOG_FINER("SQLExecuteMany: SQLPutData(string) failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2338                     }
  2339                 } else if (py::isinstance<py::bytes>(*py_obj_ptr) || py::isinstance<py::bytearray>(*py_obj_ptr)) {
  2340                     std::string data = py_obj_ptr->cast<std::string>();
  2341                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2342                     LOG_FINEST("SQLExecuteMany: Sending bytes/bytearray DAE data - chunk=%zu, length=%lld", 
! 2343                               dae_chunk_count, static_cast<long long>(data_len));
  2344                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
! 2345                     if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2346                         LOG_FINER("SQLExecuteMany: SQLPutData(bytes) failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2347                     }
  2348                 } else {
! 2349                     LOG_FINER("SQLExecuteMany: Unsupported DAE data type - chunk=%zu", dae_chunk_count);
  2350                     return SQL_ERROR;
  2351                 }
! 2352                 dae_chunk_count++;
  2353             }
! 2354             LOG_FINEST("SQLExecuteMany: DAE completed for row %zu - total_chunks=%zu, final_rc=%d", 
! 2355                       rowIndex, dae_chunk_count, rc);
  2356 
! 2357             if (!SQL_SUCCEEDED(rc)) {
! 2358                 LOG_FINER("SQLExecuteMany: DAE row %zu failed - rc=%d", rowIndex, rc);
! 2359                 return rc;
! 2360             }
  2361         }
! 2362         LOG_FINER("SQLExecuteMany: All DAE rows processed successfully - total_rows=%zu", rowCount);
  2363         return SQL_SUCCESS;
  2364     }
  2365 }

Lines 2368-2376

  2368 // Wrap SQLNumResultCols
  2369 SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) {
  2370     LOG_FINER("SQLNumResultCols: Getting number of columns in result set for statement_handle=%p", (void*)statementHandle->get());
  2371     if (!SQLNumResultCols_ptr) {
! 2372         LOG_FINER("SQLNumResultCols: Function pointer not initialized, loading driver");
  2373         DriverLoader::getInstance().loadDriver();  // Load the driver
  2374     }
  2375 
  2376     SQLSMALLINT columnCount;

Lines 2382-2390

  2382 // Wrap SQLDescribeCol
  2383 SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMetadata) {
  2384     LOG_FINER("SQLDescribeCol: Getting column descriptions for statement_handle=%p", (void*)StatementHandle->get());
  2385     if (!SQLDescribeCol_ptr) {
! 2386         LOG_FINER("SQLDescribeCol: Function pointer not initialized, loading driver");
  2387         DriverLoader::getInstance().loadDriver();  // Load the driver
  2388     }
  2389 
  2390     SQLSMALLINT ColumnCount;

Lines 2390-2398

  2390     SQLSMALLINT ColumnCount;
  2391     SQLRETURN retcode =
  2392         SQLNumResultCols_ptr(StatementHandle->get(), &ColumnCount);
  2393     if (!SQL_SUCCEEDED(retcode)) {
! 2394         LOG_FINER("SQLDescribeCol: Failed to get number of columns - SQLRETURN=%d", retcode);
  2395         return retcode;
  2396     }
  2397 
  2398     for (SQLUSMALLINT i = 1; i <= ColumnCount; ++i) {

Lines 2474-2484

  2474 }
  2475 
  2476 // Wrap SQLFetch to retrieve rows
  2477 SQLRETURN SQLFetch_wrap(SqlHandlePtr StatementHandle) {
! 2478     LOG_FINER("SQLFetch: Fetching next row for statement_handle=%p", (void*)StatementHandle->get());
  2479     if (!SQLFetch_ptr) {
! 2480         LOG_FINER("SQLFetch: Function pointer not initialized, loading driver");
  2481         DriverLoader::getInstance().loadDriver();  // Load the driver
  2482     }
  2483 
  2484     return SQLFetch_ptr(StatementHandle->get());

Lines 2510-2518

  2510             oss << "Error fetching LOB for column " << colIndex
  2511                 << ", cType=" << cType
  2512                 << ", loop=" << loopCount
  2513                 << ", SQLGetData return=" << ret;
! 2514             LOG_FINER("FetchLobColumnData: %s", oss.str().c_str());
  2515             ThrowStdException(oss.str());
  2516         }
  2517         if (actualRead == SQL_NULL_DATA) {
  2518             LOG_FINEST("FetchLobColumnData: Column %d is NULL at loop %d", colIndex, loopCount);

Lines 2599-2607

  2599 // Helper function to retrieve column data
  2600 SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row) {
  2601     LOG_FINER("SQLGetData: Getting data from %d columns for statement_handle=%p", colCount, (void*)StatementHandle->get());
  2602     if (!SQLGetData_ptr) {
! 2603         LOG_FINER("SQLGetData: Function pointer not initialized, loading driver");
  2604         DriverLoader::getInstance().loadDriver();  // Load the driver
  2605     }
  2606 
  2607     SQLRETURN ret = SQL_SUCCESS;

Lines 2616-2624

  2616 
  2617         ret = SQLDescribeCol_ptr(hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR),
  2618                                  &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable);
  2619         if (!SQL_SUCCEEDED(ret)) {
! 2620             LOG_FINER("SQLGetData: Error retrieving metadata for column %d - SQLDescribeCol SQLRETURN=%d", i, ret);
  2621             row.append(py::none());
  2622             continue;
  2623         }

Lines 2648-2656

  2648                                 row.append(std::string(reinterpret_cast<char*>(dataBuffer.data())));
  2649     #endif
  2650                             } else {
  2651                                 // Buffer too small, fallback to streaming
! 2652                                 LOG_FINER("SQLGetData: CHAR column %d data truncated (buffer_size=%zu), using streaming LOB", i, dataBuffer.size());
  2653                                 row.append(FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false));
  2654                             }
  2655                         } else if (dataLen == SQL_NULL_DATA) {
  2656                             LOG_FINEST("SQLGetData: Column %d is NULL (CHAR)", i);

Lines 2657-2672

  2657                             row.append(py::none());
  2658                         } else if (dataLen == 0) {
  2659                             row.append(py::str(""));
  2660                         } else if (dataLen == SQL_NO_TOTAL) {
! 2661                             LOG_FINER("SQLGetData: Cannot determine data length (SQL_NO_TOTAL) for column %d (SQL_CHAR), returning NULL", i);
  2662                             row.append(py::none());
  2663                         } else if (dataLen < 0) {
! 2664                             LOG_FINER("SQLGetData: Unexpected negative data length for column %d - dataType=%d, dataLen=%ld", i, dataType, (long)dataLen);
  2665                             ThrowStdException("SQLGetData returned an unexpected negative data length");
  2666                         }
  2667                     } else {
! 2668                         LOG_FINER("SQLGetData: Error retrieving data for column %d (SQL_CHAR) - SQLRETURN=%d, returning NULL", i, ret);
  2669                         row.append(py::none());
  2670                     }
  2671 				}
  2672                 break;

Lines 2712-2727

  2712                             row.append(py::none());
  2713                         } else if (dataLen == 0) {
  2714                             row.append(py::str(""));
  2715                         } else if (dataLen == SQL_NO_TOTAL) {
! 2716                             LOG_FINER("SQLGetData: Cannot determine NVARCHAR data length (SQL_NO_TOTAL) for column %d, returning NULL", i);
  2717                             row.append(py::none());
  2718                         } else if (dataLen < 0) {
! 2719                             LOG_FINER("SQLGetData: Unexpected negative data length for column %d (NVARCHAR) - dataLen=%ld", i, (long)dataLen);
  2720                             ThrowStdException("SQLGetData returned an unexpected negative data length");
  2721                         }
  2722                     } else {
! 2723                         LOG_FINER("SQLGetData: Error retrieving data for column %d (NVARCHAR) - SQLRETURN=%d", i, ret);
  2724                         row.append(py::none());
  2725                     }
  2726                 }
  2727                 break;

Lines 2741-2749

  2741                 ret = SQLGetData_ptr(hStmt, i, SQL_C_SHORT, &smallIntValue, 0, NULL);
  2742                 if (SQL_SUCCEEDED(ret)) {
  2743                     row.append(static_cast<int>(smallIntValue));
  2744                 } else {
! 2745                     LOG_FINER("SQLGetData: Error retrieving SQL_SMALLINT for column %d - SQLRETURN=%d", i, ret);
  2746                     row.append(py::none());
  2747                 }
  2748                 break;
  2749             }

Lines 2752-2760

  2752                 ret = SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL);
  2753                 if (SQL_SUCCEEDED(ret)) {
  2754                     row.append(realValue);
  2755                 } else {
! 2756                     LOG_FINER("SQLGetData: Error retrieving SQL_REAL for column %d - SQLRETURN=%d", i, ret);
  2757                     row.append(py::none());
  2758                 }
  2759                 break;
  2760             }

Lines 2800-2813

  2800                         // Add to row
  2801                         row.append(decimalObj);
  2802                     } catch (const py::error_already_set& e) {
  2803                         // If conversion fails, append None
! 2804                         LOG_FINER("SQLGetData: Error converting to decimal for column %d - %s", i, e.what());
  2805                         row.append(py::none());
  2806                     }
  2807                 }
  2808                 else {
! 2809                     LOG_FINER("SQLGetData: Error retrieving SQL_NUMERIC/DECIMAL for column %d - SQLRETURN=%d", i, ret);
  2810                     row.append(py::none());
  2811                 }
  2812                 break;
  2813             }

Lines 2818-2826

  2818                 ret = SQLGetData_ptr(hStmt, i, SQL_C_DOUBLE, &doubleValue, 0, NULL);
  2819                 if (SQL_SUCCEEDED(ret)) {
  2820                     row.append(doubleValue);
  2821                 } else {
! 2822                     LOG_FINER("SQLGetData: Error retrieving SQL_DOUBLE/FLOAT for column %d - SQLRETURN=%d", i, ret);
  2823                     row.append(py::none());
  2824                 }
  2825                 break;
  2826             }

Lines 2829-2837

  2829                 ret = SQLGetData_ptr(hStmt, i, SQL_C_SBIGINT, &bigintValue, 0, NULL);
  2830                 if (SQL_SUCCEEDED(ret)) {
  2831                     row.append(static_cast<long long>(bigintValue));
  2832                 } else {
! 2833                     LOG_FINER("SQLGetData: Error retrieving SQL_BIGINT for column %d - SQLRETURN=%d", i, ret);
  2834                     row.append(py::none());
  2835                 }
  2836                 break;
  2837             }

Lines 2866-2874

  2866                             timeValue.second
  2867                         )
  2868                     );
  2869                 } else {
! 2870                     LOG_FINER("SQLGetData: Error retrieving SQL_TYPE_TIME for column %d - SQLRETURN=%d", i, ret);
  2871                     row.append(py::none());
  2872                 }
  2873                 break;
  2874             }

Lines 2890-2898

  2890                             timestampValue.fraction / 1000  // Convert back ns to µs
  2891                         )
  2892                     );
  2893                 } else {
! 2894                     LOG_FINER("SQLGetData: Error retrieving SQL_TYPE_TIMESTAMP for column %d - SQLRETURN=%d", i, ret);
  2895                     row.append(py::none());
  2896                 }
  2897                 break;
  2898             }

Lines 2939-2947

  2939                         tzinfo
  2940                     );
  2941                     row.append(py_dt);
  2942                 } else {
! 2943                     LOG_FINER("SQLGetData: Error fetching DATETIMEOFFSET for column %d - SQLRETURN=%d, indicator=%ld", i, ret, (long)indicator);
  2944                     row.append(py::none());
  2945                 }
  2946                 break;
  2947             }

Lines 2972-2984

  2972                         } else {
  2973                             std::ostringstream oss;
  2974                             oss << "Unexpected negative length (" << dataLen << ") returned by SQLGetData. ColumnID=" 
  2975                                 << i << ", dataType=" << dataType << ", bufferSize=" << columnSize;
! 2976                             LOG_FINER("SQLGetData: %s", oss.str().c_str());
  2977                             ThrowStdException(oss.str());
  2978                         }
  2979                     } else {
! 2980                         LOG_FINER("SQLGetData: Error retrieving VARBINARY data for column %d - SQLRETURN=%d", i, ret);
  2981                         row.append(py::none());
  2982                     }
  2983                 }
  2984                 break;

Lines 2988-2996

  2988                 ret = SQLGetData_ptr(hStmt, i, SQL_C_TINYINT, &tinyIntValue, 0, NULL);
  2989                 if (SQL_SUCCEEDED(ret)) {
  2990                     row.append(static_cast<int>(tinyIntValue));
  2991                 } else {
! 2992                     LOG_FINER("SQLGetData: Error retrieving SQL_TINYINT for column %d - SQLRETURN=%d", i, ret);
  2993                     row.append(py::none());
  2994                 }
  2995                 break;
  2996             }

Lines 2999-3007

  2999                 ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL);
  3000                 if (SQL_SUCCEEDED(ret)) {
  3001                     row.append(static_cast<bool>(bitValue));
  3002                 } else {
! 3003                     LOG_FINER("SQLGetData: Error retrieving SQL_BIT for column %d - SQLRETURN=%d", i, ret);
  3004                     row.append(py::none());
  3005                 }
  3006                 break;
  3007             }

Lines 3029-3037

  3029                     row.append(uuid_obj);
  3030                 } else if (indicator == SQL_NULL_DATA) {
  3031                     row.append(py::none());
  3032                 } else {
! 3033                     LOG_FINER("SQLGetData: Error retrieving SQL_GUID for column %d - SQLRETURN=%d, indicator=%ld", i, ret, (long)indicator);
  3034                     row.append(py::none());
  3035                 }
  3036                 break;
  3037             }

Lines 3039-3047

  3039             default:
  3040                 std::ostringstream errorString;
  3041                 errorString << "Unsupported data type for column - " << columnName << ", Type - "
  3042                             << dataType << ", column ID - " << i;
! 3043                 LOG_FINER("SQLGetData: %s", errorString.str().c_str());
  3044                 ThrowStdException(errorString.str());
  3045                 break;
  3046         }
  3047     }

Lines 3050-3058

  3050 
  3051 SQLRETURN SQLFetchScroll_wrap(SqlHandlePtr StatementHandle, SQLSMALLINT FetchOrientation, SQLLEN FetchOffset, py::list& row_data) {
  3052     LOG_FINE("SQLFetchScroll_wrap: Fetching with scroll orientation=%d, offset=%ld", FetchOrientation, (long)FetchOffset);
  3053     if (!SQLFetchScroll_ptr) {
! 3054         LOG_FINER("SQLFetchScroll_wrap: Function pointer not initialized. Loading the driver.");
  3055         DriverLoader::getInstance().loadDriver();  // Load the driver
  3056     }
  3057 
  3058     // Unbind any columns from previous fetch operations to avoid memory corruption

Lines 3212-3220

  3212                 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
  3213                 std::ostringstream errorString;
  3214                 errorString << "Unsupported data type for column - " << columnName.c_str()
  3215                             << ", Type - " << dataType << ", column ID - " << col;
! 3216                 LOG_FINER("SQLBindColums: %s", errorString.str().c_str());
  3217                 ThrowStdException(errorString.str());
  3218                 break;
  3219         }
  3220         if (!SQL_SUCCEEDED(ret)) {

Lines 3221-3229

  3221             std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
  3222             std::ostringstream errorString;
  3223             errorString << "Failed to bind column - " << columnName.c_str() << ", Type - "
  3224                         << dataType << ", column ID - " << col;
! 3225             LOG_FINER("SQLBindColums: %s", errorString.str().c_str());
  3226             ThrowStdException(errorString.str());
  3227             return ret;
  3228         }
  3229     }

Lines 3259-3271

  3259             }
  3260             // TODO: variable length data needs special handling, this logic wont suffice
  3261             // This value indicates that the driver cannot determine the length of the data
  3262             if (dataLen == SQL_NO_TOTAL) {
! 3263                 LOG_FINER("FetchBatchData: Cannot determine data length for column %d - returning NULL", col);
  3264                 row.append(py::none());
  3265                 continue;
  3266             } else if (dataLen == SQL_NULL_DATA) {
! 3267                 LOG_FINEST("FetchBatchData: Column %d data is NULL", col);
  3268                 row.append(py::none());
  3269                 continue;
  3270             } else if (dataLen == 0) {
  3271                 // Handle zero-length (non-NULL) data

Lines 3276-3284

  3276                 } else if (dataType == SQL_BINARY || dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY) {
  3277                     row.append(py::bytes(""));
  3278                 } else {
  3279                     // For other datatypes, 0 length is unexpected. Log & append None
! 3280                     LOG_FINER("FetchBatchData: Unexpected 0-length data for column %d (type=%d) - returning NULL", col, dataType);
  3281                     row.append(py::none());
  3282                 }
  3283                 continue;
  3284             } else if (dataLen < 0) {

Lines 3282-3290

  3282                 }
  3283                 continue;
  3284             } else if (dataLen < 0) {
  3285                 // Negative value is unexpected, log column index, SQL type & raise exception
! 3286                 LOG_FINER("FetchBatchData: Unexpected negative data length - column=%d, SQL_type=%d, dataLen=%ld", col, dataType, (long)dataLen);
  3287                 ThrowStdException("Unexpected negative data length, check logs for details");
  3288             }
  3289             assert(dataLen > 0 && "Data length must be > 0");

Lines 3492-3500

  3492                     std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
  3493                     std::ostringstream errorString;
  3494                     errorString << "Unsupported data type for column - " << columnName.c_str()
  3495                                 << ", Type - " << dataType << ", column ID - " << col;
! 3496                     LOG_FINER("FetchBatchData: %s", errorString.str().c_str());
  3497                     ThrowStdException(errorString.str());
  3498                     break;
  3499                 }
  3500             }

Lines 3580-3588

  3580                 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
  3581                 std::ostringstream errorString;
  3582                 errorString << "Unsupported data type for column - " << columnName.c_str()
  3583                             << ", Type - " << dataType << ", column ID - " << col;
! 3584                 LOG_FINER("calculateRowSize: %s", errorString.str().c_str());
  3585                 ThrowStdException(errorString.str());
  3586                 break;
  3587         }
  3588     }

Lines 3612-3620

  3612     // Retrieve column metadata
  3613     py::list columnNames;
  3614     ret = SQLDescribeCol_wrap(StatementHandle, columnNames);
  3615     if (!SQL_SUCCEEDED(ret)) {
! 3616         LOG_FINER("FetchMany_wrap: Failed to get column descriptions - SQLRETURN=%d", ret);
  3617         return ret;
  3618     }
  3619 
  3620     std::vector<SQLUSMALLINT> lobColumns;

Lines 3651-3659

  3651 
  3652     // Bind columns
  3653     ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize);
  3654     if (!SQL_SUCCEEDED(ret)) {
! 3655         LOG_FINER("FetchMany_wrap: Error when binding columns - SQLRETURN=%d", ret);
  3656         return ret;
  3657     }
  3658 
  3659     SQLULEN numRowsFetched;

Lines 3661-3669

  3661     SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0);
  3662 
  3663     ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched, lobColumns);
  3664     if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) {
! 3665         LOG_FINER("FetchMany_wrap: Error when fetching data - SQLRETURN=%d", ret);
  3666         return ret;
  3667     }
  3668 
  3669     // Reset attributes before returning to avoid using stack pointers later

Lines 3695-3703

  3695     // Retrieve column metadata
  3696     py::list columnNames;
  3697     ret = SQLDescribeCol_wrap(StatementHandle, columnNames);
  3698     if (!SQL_SUCCEEDED(ret)) {
! 3699         LOG_FINER("FetchAll_wrap: Failed to get column descriptions - SQLRETURN=%d", ret);
  3700         return ret;
  3701     }
  3702 
  3703     // Define a memory limit (1 GB)

Lines 3772-3780

  3772 
  3773     // Bind columns
  3774     ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize);
  3775     if (!SQL_SUCCEEDED(ret)) {
! 3776         LOG_FINER("FetchAll_wrap: Error when binding columns - SQLRETURN=%d", ret);
  3777         return ret;
  3778     }
  3779 
  3780     SQLULEN numRowsFetched;

Lines 3828-3836

  3828 // Wrap SQLMoreResults
  3829 SQLRETURN SQLMoreResults_wrap(SqlHandlePtr StatementHandle) {
  3830     LOG_FINE("SQLMoreResults_wrap: Check for more results");
  3831     if (!SQLMoreResults_ptr) {
! 3832         LOG_FINER("SQLMoreResults_wrap: Function pointer not initialized. Loading the driver.");
  3833         DriverLoader::getInstance().loadDriver();  // Load the driver
  3834     }
  3835 
  3836     return SQLMoreResults_ptr(StatementHandle->get());

Lines 3837-3847

  3837 }
  3838 
  3839 // Wrap SQLFreeHandle
  3840 SQLRETURN SQLFreeHandle_wrap(SQLSMALLINT HandleType, SqlHandlePtr Handle) {
! 3841     LOG_FINE("SQLFreeHandle_wrap: Free SQL handle type=%d", HandleType);
  3842     if (!SQLAllocHandle_ptr) {
! 3843         LOG_FINER("SQLFreeHandle_wrap: Function pointer not initialized. Loading the driver.");
  3844         DriverLoader::getInstance().loadDriver();  // Load the driver
  3845     }
  3846 
  3847     SQLRETURN ret = SQLFreeHandle_ptr(HandleType, Handle->get());

Lines 3845-3853

  3845     }
  3846 
  3847     SQLRETURN ret = SQLFreeHandle_ptr(HandleType, Handle->get());
  3848     if (!SQL_SUCCEEDED(ret)) {
! 3849         LOG_FINER("SQLFreeHandle_wrap: SQLFreeHandle failed with error code - %d", ret);
  3850         return ret;
  3851     }
  3852     return ret;
  3853 }

Lines 3855-3863

  3855 // Wrap SQLRowCount
  3856 SQLLEN SQLRowCount_wrap(SqlHandlePtr StatementHandle) {
  3857     LOG_FINE("SQLRowCount_wrap: Get number of rows affected by last execute");
  3858     if (!SQLRowCount_ptr) {
! 3859         LOG_FINER("SQLRowCount_wrap: Function pointer not initialized. Loading the driver.");
  3860         DriverLoader::getInstance().loadDriver();  // Load the driver
  3861     }
  3862 
  3863     SQLLEN rowCount;

Lines 3862-3870

  3862 
  3863     SQLLEN rowCount;
  3864     SQLRETURN ret = SQLRowCount_ptr(StatementHandle->get(), &rowCount);
  3865     if (!SQL_SUCCEEDED(ret)) {
! 3866         LOG_FINER("SQLRowCount_wrap: SQLRowCount failed with error code - %d", ret);
  3867         return ret;
  3868     }
  3869     LOG_FINER("SQLRowCount_wrap: SQLRowCount returned %ld", (long)rowCount);
  3870     return rowCount;

Lines 4049-4058

  4049     try {
  4050         mssql_python::logging::LoggerBridge::initialize();
  4051     } catch (const std::exception& e) {
  4052         // Log initialization failure but don't throw
! 4053         fprintf(stderr, "Logger bridge initialization failed: %s\n", e.what());
! 4054     }
  4055     
  4056     try {
  4057         // Try loading the ODBC driver when the module is imported
  4058         LOG_FINE("Module initialization: Loading ODBC driver");

Lines 4058-4065

  4058         LOG_FINE("Module initialization: Loading ODBC driver");
  4059         DriverLoader::getInstance().loadDriver();  // Load the driver
  4060     } catch (const std::exception& e) {
  4061         // Log the error but don't throw - let the error happen when functions are called
! 4062         LOG_FINER("Module initialization: Failed to load ODBC driver - %s", e.what());
  4063     }
  4064 }

mssql_python/pybind/logger_bridge.cpp

Lines 25-34

  25     std::lock_guard<std::mutex> lock(mutex_);
  26     
  27     // Skip if already initialized
  28     if (initialized_) {
! 29         return;
! 30     }
  31     
  32     try {
  33         // Acquire GIL for Python API calls
  34         py::gil_scoped_acquire gil;

Lines 52-65

  52         
  53     } catch (const py::error_already_set& e) {
  54         // Failed to initialize - log to stderr and continue
  55         // (logging will be disabled but won't crash)
! 56         std::cerr << "LoggerBridge initialization failed: " << e.what() << std::endl;
! 57         initialized_ = false;
! 58     } catch (const std::exception& e) {
! 59         std::cerr << "LoggerBridge initialization failed: " << e.what() << std::endl;
! 60         initialized_ = false;
! 61     }
  62 }
  63 
  64 void LoggerBridge::updateLevel(int level) {
  65     // Update the cached level atomically

Lines 66-192

   66     // This is lock-free and can be called from any thread
   67     cached_level_.store(level, std::memory_order_relaxed);
   68 }
   69 
!  70 int LoggerBridge::getLevel() {
!  71     return cached_level_.load(std::memory_order_relaxed);
!  72 }
   73 
!  74 bool LoggerBridge::isInitialized() {
!  75     return initialized_;
!  76 }
   77 
!  78 std::string LoggerBridge::formatMessage(const char* format, va_list args) {
   79     // Use a stack buffer for most messages (4KB should be enough)
!  80     char buffer[4096];
   81     
   82     // Format the message using safe std::vsnprintf (C++11 standard)
   83     // std::vsnprintf is safe: always null-terminates, never overflows buffer
   84     // DevSkim warning is false positive - this is the recommended safe alternative
!  85     va_list args_copy;
!  86     va_copy(args_copy, args);
!  87     int result = std::vsnprintf(buffer, sizeof(buffer), format, args_copy);
!  88     va_end(args_copy);
   89     
!  90     if (result < 0) {
   91         // Error during formatting
!  92         return "[Formatting error]";
!  93     }
   94     
!  95     if (result < static_cast<int>(sizeof(buffer))) {
   96         // Message fit in buffer (vsnprintf guarantees null-termination)
!  97         return std::string(buffer, std::min(static_cast<size_t>(result), sizeof(buffer) - 1));
!  98     }
   99     
  100     // Message was truncated - allocate larger buffer
  101     // (This should be rare for typical log messages)
! 102     std::vector<char> large_buffer(result + 1);
! 103     va_copy(args_copy, args);
  104     // std::vsnprintf is safe here too - proper bounds checking with buffer size
! 105     std::vsnprintf(large_buffer.data(), large_buffer.size(), format, args_copy);
! 106     va_end(args_copy);
  107     
! 108     return std::string(large_buffer.data());
! 109 }
  110 
! 111 const char* LoggerBridge::extractFilename(const char* path) {
  112     // Extract just the filename from full path using safer C++ string search
! 113     if (!path) {
! 114         return "";
! 115     }
  116     
  117     // Find last occurrence of Unix path separator
! 118     const char* filename = std::strrchr(path, '/');
! 119     if (filename) {
! 120         return filename + 1;
! 121     }
  122     
  123     // Try Windows path separator
! 124     filename = std::strrchr(path, '\\');
! 125     if (filename) {
! 126         return filename + 1;
! 127     }
  128     
  129     // No path separator found, return the whole string
! 130     return path;
! 131 }
  132 
  133 void LoggerBridge::log(int level, const char* file, int line, 
! 134                       const char* format, ...) {
  135     // Fast level check (should already be done by macro, but double-check)
! 136     if (!isLoggable(level)) {
! 137         return;
! 138     }
  139     
  140     // Check if initialized
! 141     if (!initialized_ || !cached_logger_) {
! 142         return;
! 143     }
  144     
  145     // Format the message
! 146     va_list args;
! 147     va_start(args, format);
! 148     std::string message = formatMessage(format, args);
! 149     va_end(args);
  150     
  151     // Extract filename from path
! 152     const char* filename = extractFilename(file);
  153     
  154     // Format the complete log message with file:line prefix using safe std::snprintf
  155     // std::snprintf is safe: always null-terminates, never overflows buffer
  156     // DevSkim warning is false positive - this is the recommended safe alternative
! 157     char complete_message[4096];
! 158     int written = std::snprintf(complete_message, sizeof(complete_message), 
! 159                                "[DDBC] %s [%s:%d]", message.c_str(), filename, line);
  160     
  161     // Ensure null-termination (snprintf guarantees this, but be explicit)
! 162     if (written >= static_cast<int>(sizeof(complete_message))) {
! 163         complete_message[sizeof(complete_message) - 1] = '\0';
! 164     }
  165     
  166     // Lock for Python call (minimize critical section)
! 167     std::lock_guard<std::mutex> lock(mutex_);
  168     
! 169     try {
  170         // Acquire GIL for Python API call
! 171         py::gil_scoped_acquire gil;
  172         
  173         // Call Python logger's log method
  174         // logger.log(level, message)
! 175         py::handle logger_handle(cached_logger_);
! 176         py::object logger_obj = py::reinterpret_borrow<py::object>(logger_handle);
  177         
! 178         logger_obj.attr("_log")(level, complete_message);
  179         
! 180     } catch (const py::error_already_set& e) {
  181         // Python error during logging - ignore to prevent cascading failures
  182         // (Logging errors should not crash the application)
! 183         (void)e;  // Suppress unused variable warning
! 184     } catch (const std::exception& e) {
  185         // Other error - ignore
! 186         (void)e;
! 187     }
! 188 }
  189 
  190 } // namespace logging
  191 } // namespace mssql_python

mssql_python/pybind/logger_bridge.hpp

Lines 146-156

  146 
  147 #define LOG_FINEST(fmt, ...) \
  148     do { \
  149         if (mssql_python::logging::LoggerBridge::isLoggable(mssql_python::logging::LOG_LEVEL_FINEST)) { \
! 150             mssql_python::logging::LoggerBridge::log( \
! 151                 mssql_python::logging::LOG_LEVEL_FINEST, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
! 152         } \
  153     } while(0)
  154 
  155 #define LOG_FINER(fmt, ...) \
  156     do { \

Lines 154-164

  154 
  155 #define LOG_FINER(fmt, ...) \
  156     do { \
  157         if (mssql_python::logging::LoggerBridge::isLoggable(mssql_python::logging::LOG_LEVEL_FINER)) { \
! 158             mssql_python::logging::LoggerBridge::log( \
! 159                 mssql_python::logging::LOG_LEVEL_FINER, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
! 160         } \
  161     } while(0)
  162 
  163 #define LOG_FINE(fmt, ...) \
  164     do { \

Lines 162-172

  162 
  163 #define LOG_FINE(fmt, ...) \
  164     do { \
  165         if (mssql_python::logging::LoggerBridge::isLoggable(mssql_python::logging::LOG_LEVEL_FINE)) { \
! 166             mssql_python::logging::LoggerBridge::log( \
! 167                 mssql_python::logging::LOG_LEVEL_FINE, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
! 168         } \
  169     } while(0)
  170 
  171 #define LOG_INFO(fmt, ...) \
  172     do { \

mssql_python/row.py

Lines 138-153

  138                             try:
  139                                 # Remove braces if present
  140                                 clean_value = value.strip("{}")
  141                                 processed_values[i] = uuid.UUID(clean_value)
! 142                                 conversion_count += 1
  143                             except (ValueError, AttributeError):
! 144                                 logger.finer( '_process_uuid_values: Conversion failed for index=%d', i)
  145                                 pass  # Keep original if conversion fails
  146                 logger.finest( '_process_uuid_values: Converted %d UUID strings to UUID objects', conversion_count)
  147             # Fallback to scanning all columns if indices weren't pre-identified
  148             else:
! 149                 logger.finest( '_process_uuid_values: Scanning all columns for GUID type')
  150                 for i, value in enumerate(processed_values):
  151                     if value is None:
  152                         continue

Lines 157-169

  157                         if sql_type == -11:  # SQL_GUID
  158                             if isinstance(value, str):
  159                                 try:
  160                                     processed_values[i] = uuid.UUID(value.strip("{}"))
! 161                                     conversion_count += 1
  162                                 except (ValueError, AttributeError):
! 163                                     logger.finer( '_process_uuid_values: Scan conversion failed for index=%d', i)
  164                                     pass
! 165                 logger.finest( '_process_uuid_values: Scan converted %d UUID strings', conversion_count)
  166         # When native_uuid is False, convert UUID objects to strings
  167         else:
  168             string_conversion_count = 0
  169             for i, value in enumerate(processed_values):

Lines 186-194

  186         """
  187         from mssql_python.logging import logger
  188         
  189         if not self._description:
! 190             logger.finest( '_apply_output_converters: No description - returning values as-is')
  191             return values
  192 
  193         logger.finest( '_apply_output_converters: Applying converters - value_count=%d', len(values))


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 19.1%
mssql_python.pybind.logger_bridge.hpp: 57.1%
mssql_python.pybind.ddbc_bindings.cpp: 69.9%
mssql_python.row.py: 76.3%
mssql_python.pybind.connection.connection.cpp: 81.6%
mssql_python.connection.py: 83.2%
mssql_python.pybind.connection.connection_pool.cpp: 85.5%
mssql_python.auth.py: 87.3%
mssql_python.helpers.py: 90.1%
mssql_python.logging.py: 92.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

// Format the message using safe vsnprintf (always null-terminates)
va_list args_copy;
va_copy(args_copy, args);
int result = std::vsnprintf(buffer, sizeof(buffer), format, args_copy);

Check warning

Code scanning / devskim

These functions are historically error-prone and have been associated with a significant number of vulnerabilities. Most of these functions have safer alternatives, such as replacing 'strcpy' with 'strlcpy' or 'strcpy_s'.

Banned C function detected
// (This should be rare for typical log messages)
std::vector<char> large_buffer(result + 1);
va_copy(args_copy, args);
std::vsnprintf(large_buffer.data(), large_buffer.size(), format, args_copy);

Check warning

Code scanning / devskim

These functions are historically error-prone and have been associated with a significant number of vulnerabilities. Most of these functions have safer alternatives, such as replacing 'strcpy' with 'strlcpy' or 'strcpy_s'.

Banned C function detected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants