-
Notifications
You must be signed in to change notification settings - Fork 27
FEAT: Comprehensive Logging Framework with Python-C++ Bridge & Instrumentation #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql_python/auth.pyLines 166-174 166 try:
167 token = AADAuth.get_token(auth_type)
168 logger.info('get_auth_token: Token acquired successfully - auth_type=%s', auth_type)
169 return token
! 170 except (ValueError, RuntimeError) as e:
171 logger.warning('get_auth_token: Token acquisition failed - auth_type=%s, error=%s', auth_type, str(e))
172 return None
173 mssql_python/helpers.pyLines 39-50 39 connection_attributes = connection_str.split(";")
40 final_connection_attributes = []
41
42 # Iterate through the attributes and exclude any existing driver attribute
! 43 driver_found = False
44 for attribute in connection_attributes:
45 if attribute.lower().split("=")[0] == "driver":
! 46 driver_found = True
47 logger.debug('add_driver_to_connection_str: Existing driver attribute found, removing')
48 continue
49 final_connection_attributes.append(attribute)mssql_python/logging.pyLines 38-48 38 """Add thread_id (OS native) attribute to log record."""
39 # Use OS native thread ID for debugging compatibility
40 try:
41 thread_id = threading.get_native_id()
! 42 except AttributeError:
43 # Fallback for Python < 3.8
! 44 thread_id = threading.current_thread().ident
45 record.thread_id = thread_id
46 return True
47 Lines 128-137 128 # Flush and close each handler while holding its lock
129 for handler in old_handlers:
130 try:
131 handler.flush() # Flush BEFORE close
! 132 except:
! 133 pass # Ignore flush errors
134 handler.close()
135 self._logger.removeHandler(handler)
136 finally:
137 # Release locks on old handlersLines 137-146 137 # Release locks on old handlers
138 for handler in old_handlers:
139 try:
140 handler.release()
! 141 except:
! 142 pass # Handler might already be closed
143
144 self._file_handler = None
145 self._stdout_handler = None
146 Lines 154-163 154 end_bracket = msg.index(']')
155 source = msg[1:end_bracket]
156 message = msg[end_bracket+2:].strip() # Skip '] '
157 else:
! 158 source = 'Unknown'
! 159 message = msg
160
161 # Format timestamp with milliseconds using period separator
162 timestamp = self.formatTime(record, '%Y-%m-%d %H:%M:%S')
163 timestamp_with_ms = f"{timestamp}.{int(record.msecs):03d}"Lines 226-234 226 """
227 Reconfigure handlers when output mode changes.
228 Closes existing handlers and creates new ones based on current output mode.
229 """
! 230 self._setup_handlers()
231
232 def _cleanup_handlers(self):
233 """
234 Cleanup all handlers on process exit.Lines 250-259 250 for handler in handlers:
251 try:
252 handler.flush()
253 handler.close()
! 254 except:
! 255 pass # Ignore errors during cleanup
256 self._logger.removeHandler(handler)
257 finally:
258 for handler in handlers:
259 try:Lines 257-266 257 finally:
258 for handler in handlers:
259 try:
260 handler.release()
! 261 except:
! 262 pass
263
264 def _validate_log_file_extension(self, file_path: str) -> None:
265 """
266 Validate that the log file has an allowed extension.Lines 286-294 286 Write CSV header and metadata to the log file.
287 Called once when log file is created.
288 """
289 if not self._log_file or not self._file_handler:
! 290 return
291
292 try:
293 # Get script name from sys.argv or __main__
294 script_name = os.path.basename(sys.argv[0]) if sys.argv else '<interactive>'Lines 298-306 298
299 # Get driver version (try to import from package)
300 try:
301 from mssql_python import __version__
! 302 driver_version = __version__
303 except:
304 driver_version = 'unknown'
305
306 # Get current timeLines 322-336 322 with open(self._log_file, 'a') as f:
323 f.write(header_line)
324 f.write(csv_header)
325
! 326 except Exception as e:
327 # Notify on stderr so user knows why header is missing
! 328 try:
! 329 sys.stderr.write(f"[MSSQL-Python] Warning: Failed to write log header to {self._log_file}: {type(e).__name__}\n")
! 330 sys.stderr.flush()
! 331 except:
! 332 pass # Even stderr notification failed
333 # Don't crash - logging continues without header
334
335 def _log(self, level: int, msg: str, add_prefix: bool = True, *args, **kwargs):
336 """Lines 379-388 379 import sys
380 level_name = logging.getLevelName(level)
381 sys.stderr.write(f"[MSSQL-Python Logging Failed - {level_name}] {msg if 'msg' in locals() else 'Unable to format message'}\n")
382 sys.stderr.flush()
! 383 except:
! 384 pass # Even stderr failed - give up silently
385
386 # Convenience methods for logging
387
388 def debug(self, msg: str, *args, **kwargs):Lines 470-484 470 # Handler management
471
472 def addHandler(self, handler: logging.Handler):
473 """Add a handler to the logger (thread-safe)"""
! 474 with self._handler_lock:
! 475 self._logger.addHandler(handler)
476
477 def removeHandler(self, handler: logging.Handler):
478 """Remove a handler from the logger (thread-safe)"""
! 479 with self._handler_lock:
! 480 self._logger.removeHandler(handler)
481
482 @property
483 def handlers(self) -> list:
484 """Get list of handlers attached to the logger (thread-safe)"""Lines 489-497 489 """
490 Reset/recreate handlers.
491 Useful when log file has been deleted or needs to be recreated.
492 """
! 493 self._setup_handlers()
494
495 def _notify_cpp_level_change(self, level: int):
496 """
497 Notify C++ bridge that log level has changed.Lines 504-514 504 # Import here to avoid circular dependency
505 from . import ddbc_bindings
506 if hasattr(ddbc_bindings, 'update_log_level'):
507 ddbc_bindings.update_log_level(level)
! 508 except (ImportError, AttributeError):
509 # C++ bindings not available or not yet initialized
! 510 pass
511
512 # Properties
513
514 @propertyLines 526-543 526
527 Raises:
528 ValueError: If mode is not a valid OutputMode value
529 """
! 530 if mode not in (FILE, STDOUT, BOTH):
! 531 raise ValueError(
532 f"Invalid output mode: {mode}. "
533 f"Must be one of: {FILE}, {STDOUT}, {BOTH}"
534 )
! 535 self._output_mode = mode
536
537 # Only reconfigure if handlers were already initialized
! 538 if self._handlers_initialized:
! 539 self._reconfigure_handlers()
540
541 @property
542 def log_file(self) -> Optional[str]:
543 """Get the current log file path (None if file output is disabled)"""mssql_python/pybind/connection/connection.cppLines 214-222 214
215 // Convert to wide string
216 std::wstring wstr = Utf8ToWString(utf8_str);
217 if (wstr.empty() && !utf8_str.empty()) {
! 218 LOG("Failed to convert string value to wide string for attribute=%d", attribute);
219 return SQL_ERROR;
220 }
221 this->wstrStringBuffer.clear();
222 this->wstrStringBuffer = std::move(wstr);Lines 227-235 227 #if defined(__APPLE__) || defined(__linux__)
228 // For macOS/Linux, convert wstring to SQLWCHAR buffer
229 std::vector<SQLWCHAR> sqlwcharBuffer = WStringToSQLWCHAR(this->wstrStringBuffer);
230 if (sqlwcharBuffer.empty() && !this->wstrStringBuffer.empty()) {
! 231 LOG("Failed to convert wide string to SQLWCHAR buffer for attribute=%d", attribute);
232 return SQL_ERROR;
233 }
234
235 ptr = sqlwcharBuffer.data();Lines 243-251 243
244 SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(),
245 attribute, ptr, length);
246 if (!SQL_SUCCEEDED(ret)) {
! 247 LOG("Failed to set string attribute=%d, ret=%d", attribute, ret);
248 } else {
249 LOG("Set string attribute=%d successfully", attribute);
250 }
251 return ret;Lines 249-257 249 LOG("Set string attribute=%d successfully", attribute);
250 }
251 return ret;
252 } catch (const std::exception& e) {
! 253 LOG("Exception during string attribute=%d setting: %s", attribute, e.what());
254 return SQL_ERROR;
255 }
256 } else if (py::isinstance<py::bytes>(value) ||
257 py::isinstance<py::bytearray>(value)) {Lines 264-282 264
265 SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(),
266 attribute, ptr, length);
267 if (!SQL_SUCCEEDED(ret)) {
! 268 LOG("Failed to set binary attribute=%d, ret=%d", attribute, ret);
269 } else {
! 270 LOG("Set binary attribute=%d successfully (length=%d)", attribute, length);
271 }
272 return ret;
273 } catch (const std::exception& e) {
! 274 LOG("Exception during binary attribute=%d setting: %s", attribute, e.what());
275 return SQL_ERROR;
276 }
277 } else {
! 278 LOG("Unsupported attribute value type for attribute=%d", attribute);
279 return SQL_ERROR;
280 }
281 }Lines 322-330 322 SQL_ATTR_RESET_CONNECTION,
323 (SQLPOINTER)SQL_RESET_CONNECTION_YES,
324 SQL_IS_INTEGER);
325 if (!SQL_SUCCEEDED(ret)) {
! 326 LOG("Failed to reset connection (ret=%d). Marking as dead.", ret);
327 disconnect();
328 return false;
329 }
330 updateLastUsed();mssql_python/pybind/connection/connection_pool.cppLines 71-79 71 for (auto& conn : to_disconnect) {
72 try {
73 conn->disconnect();
74 } catch (const std::exception& ex) {
! 75 LOG("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("ConnectionPool::close: disconnect failed: %s", ex.what());
107 }
108 }
109 }mssql_python/pybind/ddbc_bindings.cppLines 293-301 293 !py::isinstance<py::bytes>(param)) {
294 ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
295 }
296 if (paramInfo.isDAE) {
! 297 LOG("BindParameters: param[%d] SQL_C_CHAR - Using DAE (Data-At-Execution) for large string streaming", paramIndex);
298 dataPtr = const_cast<void*>(reinterpret_cast<const void*>(¶mInfos[paramIndex]));
299 strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
300 *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0);
301 bufferLength = 0;Lines 395-403 395 &describedDigits,
396 &nullable
397 );
398 if (!SQL_SUCCEEDED(rc)) {
! 399 LOG("BindParameters: SQLDescribeParam failed for param[%d] (NULL parameter) - SQLRETURN=%d", paramIndex, rc);
400 return rc;
401 }
402 sqlType = describedType;
403 columnSize = describedSize;Lines 606-615 606 }
607 py::bytes uuid_bytes = param.cast<py::bytes>();
608 const unsigned char* uuid_data = reinterpret_cast<const unsigned char*>(PyBytes_AS_STRING(uuid_bytes.ptr()));
609 if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
! 610 LOG("BindParameters: param[%d] SQL_C_GUID - Invalid UUID length: expected 16 bytes, got %ld bytes",
! 611 paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()));
612 ThrowStdException("UUID binary data must be exactly 16 bytes long.");
613 }
614 SQLGUID* guid_data_ptr = AllocateParamBuffer<SQLGUID>(paramBuffers);
615 guid_data_ptr->Data1 =Lines 655-668 655 if (paramInfo.paramCType == SQL_C_NUMERIC) {
656 SQLHDESC hDesc = nullptr;
657 rc = SQLGetStmtAttr_ptr(hStmt, SQL_ATTR_APP_PARAM_DESC, &hDesc, 0, NULL);
658 if(!SQL_SUCCEEDED(rc)) {
! 659 LOG("BindParameters: SQLGetStmtAttr(SQL_ATTR_APP_PARAM_DESC) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
660 return rc;
661 }
662 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE, (SQLPOINTER) SQL_C_NUMERIC, 0);
663 if(!SQL_SUCCEEDED(rc)) {
! 664 LOG("BindParameters: SQLSetDescField(SQL_DESC_TYPE) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
665 return rc;
666 }
667 SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
668 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,Lines 667-675 667 SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
668 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,
669 (SQLPOINTER) numericPtr->precision, 0);
670 if(!SQL_SUCCEEDED(rc)) {
! 671 LOG("BindParameters: SQLSetDescField(SQL_DESC_PRECISION) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
672 return rc;
673 }
674
675 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,Lines 674-682 674
675 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,
676 (SQLPOINTER) numericPtr->scale, 0);
677 if(!SQL_SUCCEEDED(rc)) {
! 678 LOG("BindParameters: SQLSetDescField(SQL_DESC_SCALE) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
679 return rc;
680 }
681
682 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0);Lines 680-688 680 }
681
682 rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER) numericPtr, 0);
683 if(!SQL_SUCCEEDED(rc)) {
! 684 LOG("BindParameters: SQLSetDescField(SQL_DESC_DATA_PTR) failed for param[%d] - SQLRETURN=%d", paramIndex, rc);
685 return rc;
686 }
687 }
688 }Lines 760-768 760 if (pos != std::string::npos) {
761 std::string dir = module_file.substr(0, pos);
762 return dir;
763 }
! 764 LOG("GetModuleDirectory: Could not extract directory from module path - path='%s'", module_file.c_str());
765 return module_file;
766 #endif
767 }Lines 783-792 783 #else
784 // macOS/Unix: Use dlopen
785 void* handle = dlopen(driverPath.c_str(), RTLD_LAZY);
786 if (!handle) {
! 787 LOG("LoadDriverLibrary: dlopen failed for path='%s' - %s",
! 788 driverPath.c_str(), dlerror() ? dlerror() : "unknown error");
789 }
790 return handle;
791 #endif
792 }Lines 928-937 928 }
929
930 DriverHandle handle = LoadDriverLibrary(driverPath.string());
931 if (!handle) {
! 932 LOG("LoadDriverOrThrowException: Failed to load ODBC driver - path='%s', error='%s'",
! 933 driverPath.string().c_str(), GetLastErrorMessage().c_str());
934 ThrowStdException("Failed to load the driver. Please read the documentation (https://github.com/microsoft/mssql-python#installation) to install the required dependencies.");
935 }
936 LOG("LoadDriverOrThrowException: ODBC driver library loaded successfully from '%s'", driverPath.string().c_str());Lines 1311-1319 1311 ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode) {
1312 LOG("SQLCheckError: Checking ODBC errors - handleType=%d, retcode=%d", handleType, retcode);
1313 ErrorInfo errorInfo;
1314 if (retcode == SQL_INVALID_HANDLE) {
! 1315 LOG("SQLCheckError: SQL_INVALID_HANDLE detected - handle is invalid");
1316 errorInfo.ddbcErrorMsg = std::wstring( L"Invalid handle!");
1317 return errorInfo;
1318 }
1319 assert(handle != 0);Lines 1319-1327 1319 assert(handle != 0);
1320 SQLHANDLE rawHandle = handle->get();
1321 if (!SQL_SUCCEEDED(retcode)) {
1322 if (!SQLGetDiagRec_ptr) {
! 1323 LOG("SQLCheckError: SQLGetDiagRec function pointer not initialized, loading driver");
1324 DriverLoader::getInstance().loadDriver(); // Load the driver
1325 }
1326
1327 SQLWCHAR sqlState[6], message[SQL_MAX_MESSAGE_LENGTH];Lines 1350-1358 1350 py::list SQLGetAllDiagRecords(SqlHandlePtr handle) {
1351 LOG("SQLGetAllDiagRecords: Retrieving all diagnostic records for handle %p, handleType=%d",
1352 (void*)handle->get(), handle->type());
1353 if (!SQLGetDiagRec_ptr) {
! 1354 LOG("SQLGetAllDiagRecords: SQLGetDiagRec function pointer not initialized, loading driver");
1355 DriverLoader::getInstance().loadDriver();
1356 }
1357
1358 py::list records;Lines 1414-1426 1414 }
1415
1416 // Wrap SQLExecDirect
1417 SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Query) {
! 1418 std::string queryUtf8 = WideToUTF8(Query);
! 1419 LOG("SQLExecDirect: Executing query directly - statement_handle=%p, query_length=%zu chars",
! 1420 (void*)StatementHandle->get(), Query.length());
1421 if (!SQLExecDirect_ptr) {
! 1422 LOG("SQLExecDirect: Function pointer not initialized, loading driver");
1423 DriverLoader::getInstance().loadDriver(); // Load the driver
1424 }
1425
1426 // Configure forward-only cursorLines 1443-1451 1443 queryPtr = const_cast<SQLWCHAR*>(Query.c_str());
1444 #endif
1445 SQLRETURN ret = SQLExecDirect_ptr(StatementHandle->get(), queryPtr, SQL_NTS);
1446 if (!SQL_SUCCEEDED(ret)) {
! 1447 LOG("SQLExecDirect: Query execution failed - SQLRETURN=%d", ret);
1448 }
1449 return ret;
1450 }Lines 1456-1464 1456 const std::wstring& table,
1457 const std::wstring& tableType) {
1458
1459 if (!SQLTables_ptr) {
! 1460 LOG("SQLTables: Function pointer not initialized, loading driver");
1461 DriverLoader::getInstance().loadDriver();
1462 }
1463
1464 SQLWCHAR* catalogPtr = nullptr;Lines 1541-1549 1541 py::list& isStmtPrepared, const bool usePrepare = true) {
1542 LOG("SQLExecute: Executing %s query - statement_handle=%p, param_count=%zu, query_length=%zu chars",
1543 (params.size() > 0 ? "parameterized" : "direct"), (void*)statementHandle->get(), params.size(), query.length());
1544 if (!SQLPrepare_ptr) {
! 1545 LOG("SQLExecute: Function pointer not initialized, loading driver");
1546 DriverLoader::getInstance().loadDriver(); // Load the driver
1547 }
1548 assert(SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && SQLExecDirect_ptr);Lines 1554-1562 1554
1555 RETCODE rc;
1556 SQLHANDLE hStmt = statementHandle->get();
1557 if (!statementHandle || !statementHandle->get()) {
! 1558 LOG("SQLExecute: Statement handle is null or invalid");
1559 }
1560
1561 // Configure forward-only cursor
1562 if (SQLSetStmtAttr_ptr && hStmt) {Lines 1594-1602 1594 assert(isStmtPrepared.size() == 1);
1595 if (usePrepare) {
1596 rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS);
1597 if (!SQL_SUCCEEDED(rc)) {
! 1598 LOG("SQLExecute: SQLPrepare failed - SQLRETURN=%d, statement_handle=%p", rc, (void*)hStmt);
1599 return rc;
1600 }
1601 isStmtPrepared[0] = py::cast(true);
1602 } else {Lines 1659-1668 1659 ThrowStdException("Chunk size exceeds maximum allowed by SQLLEN");
1660 }
1661 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(lenBytes));
1662 if (!SQL_SUCCEEDED(rc)) {
! 1663 LOG("SQLExecute: SQLPutData failed for SQL_C_WCHAR chunk - offset=%zu",
! 1664 offset, totalChars, lenBytes, rc);
1665 return rc;
1666 }
1667 offset += len;
1668 }Lines 1676-1685 1676 size_t len = std::min(chunkBytes, totalBytes - offset);
1677
1678 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
1679 if (!SQL_SUCCEEDED(rc)) {
! 1680 LOG("SQLExecute: SQLPutData failed for SQL_C_CHAR chunk - offset=%zu",
! 1681 offset, totalBytes, len, rc);
1682 return rc;
1683 }
1684 offset += len;
1685 }Lines 1695-1704 1695 for (size_t offset = 0; offset < totalBytes; offset += chunkSize) {
1696 size_t len = std::min(chunkSize, totalBytes - offset);
1697 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset), static_cast<SQLLEN>(len));
1698 if (!SQL_SUCCEEDED(rc)) {
! 1699 LOG("SQLExecute: SQLPutData failed for binary/bytes chunk - offset=%zu",
! 1700 offset, totalBytes, len, rc);
1701 return rc;
1702 }
1703 }
1704 } else {Lines 1705-1714 1705 ThrowStdException("DAE only supported for str or bytes");
1706 }
1707 }
1708 if (!SQL_SUCCEEDED(rc)) {
! 1709 LOG("SQLExecute: SQLParamData final call %s - SQLRETURN=%d",
! 1710 (rc == SQL_NO_DATA ? "completed with no data" : "failed"), rc);
1711 return rc;
1712 }
1713 LOG("SQLExecute: DAE streaming completed successfully, SQLExecute resumed");
1714 }Lines 1740-1749 1740 const ParamInfo& info = paramInfos[paramIndex];
1741 LOG("BindParameterArray: Processing param_index=%d, C_type=%d, SQL_type=%d, column_size=%zu, decimal_digits=%d",
1742 paramIndex, info.paramCType, info.paramSQLType, info.columnSize, info.decimalDigits);
1743 if (columnValues.size() != paramSetSize) {
! 1744 LOG("BindParameterArray: Size mismatch - param_index=%d, expected=%zu, actual=%zu",
! 1745 paramIndex, paramSetSize, columnValues.size());
1746 ThrowStdException("Column " + std::to_string(paramIndex) + " has mismatched size.");
1747 }
1748 void* dataPtr = nullptr;
1749 SQLLEN* strLenOrIndArray = nullptr;Lines 1767-1775 1767 dataPtr = dataArray;
1768 break;
1769 }
1770 case SQL_C_DOUBLE: {
! 1771 LOG("BindParameterArray: Binding SQL_C_DOUBLE array - param_index=%d, count=%zu", paramIndex, paramSetSize);
1772 double* dataArray = AllocateParamBufferArray<double>(tempBuffers, paramSetSize);
1773 for (size_t i = 0; i < paramSetSize; ++i) {
1774 if (columnValues[i].is_none()) {
1775 if (!strLenOrIndArray)Lines 1780-1788 1780 dataArray[i] = columnValues[i].cast<double>();
1781 if (strLenOrIndArray) strLenOrIndArray[i] = 0;
1782 }
1783 }
! 1784 LOG("BindParameterArray: SQL_C_DOUBLE bound - param_index=%d", paramIndex);
1785 dataPtr = dataArray;
1786 break;
1787 }
1788 case SQL_C_WCHAR: {Lines 1802-1813 1802 size_t utf16_len = utf16Buf.size() > 0 ? utf16Buf.size() - 1 : 0;
1803 // Check UTF-16 length (excluding null terminator) against column size
1804 if (utf16Buf.size() > 0 && utf16_len > info.columnSize) {
1805 std::string offending = WideToUTF8(wstr);
! 1806 LOG("BindParameterArray: SQL_C_WCHAR string too long - param_index=%d, row=%zu, utf16_length=%zu, max=%zu",
! 1807 paramIndex, i, utf16_len, info.columnSize);
1808 ThrowStdException("Input string UTF-16 length exceeds allowed column size at parameter index " + std::to_string(paramIndex) +
! 1809 ". UTF-16 length: " + std::to_string(utf16_len) + ", Column size: " + std::to_string(info.columnSize));
1810 }
1811 // If we reach here, the UTF-16 string fits - copy it completely
1812 std::memcpy(wcharArray + i * (info.columnSize + 1), utf16Buf.data(), utf16Buf.size() * sizeof(SQLWCHAR));
1813 #elseLines 1838-1847 1838 strLenOrIndArray[i] = SQL_NULL_DATA;
1839 } else {
1840 int intVal = columnValues[i].cast<int>();
1841 if (intVal < 0 || intVal > 255) {
! 1842 LOG("BindParameterArray: TINYINT value out of range - param_index=%d, row=%zu, value=%d",
! 1843 paramIndex, i, intVal);
1844 ThrowStdException("UTINYINT value out of range at rowIndex " + std::to_string(i));
1845 }
1846 dataArray[i] = static_cast<unsigned char>(intVal);
1847 if (strLenOrIndArray) strLenOrIndArray[i] = 0;Lines 1864-1873 1864 } else {
1865 int intVal = columnValues[i].cast<int>();
1866 if (intVal < std::numeric_limits<short>::min() ||
1867 intVal > std::numeric_limits<short>::max()) {
! 1868 LOG("BindParameterArray: SHORT value out of range - param_index=%d, row=%zu, value=%d",
! 1869 paramIndex, i, intVal);
1870 ThrowStdException("SHORT value out of range at rowIndex " + std::to_string(i));
1871 }
1872 dataArray[i] = static_cast<short>(intVal);
1873 if (strLenOrIndArray) strLenOrIndArray[i] = 0;Lines 1890-1901 1890 std::memset(charArray + i * (info.columnSize + 1), 0, info.columnSize + 1);
1891 } else {
1892 std::string str = columnValues[i].cast<std::string>();
1893 if (str.size() > info.columnSize) {
! 1894 LOG("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 1904-1912 1904 bufferLength = info.columnSize + 1;
1905 break;
1906 }
1907 case SQL_C_BIT: {
! 1908 LOG("BindParameterArray: Binding SQL_C_BIT array - param_index=%d, count=%zu", paramIndex, paramSetSize);
1909 char* boolArray = AllocateParamBufferArray<char>(tempBuffers, paramSetSize);
1910 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
1911 for (size_t i = 0; i < paramSetSize; ++i) {
1912 if (columnValues[i].is_none()) {Lines 1912-1925 1912 if (columnValues[i].is_none()) {
1913 boolArray[i] = 0;
1914 strLenOrIndArray[i] = SQL_NULL_DATA;
1915 } else {
! 1916 bool val = columnValues[i].cast<bool>();
! 1917 boolArray[i] = val ? 1 : 0;
1918 strLenOrIndArray[i] = 0;
1919 }
1920 }
! 1921 LOG("BindParameterArray: SQL_C_BIT bound - param_index=%d", paramIndex);
1922 dataPtr = boolArray;
1923 bufferLength = sizeof(char);
1924 break;
1925 }Lines 1924-1932 1924 break;
1925 }
1926 case SQL_C_STINYINT:
1927 case SQL_C_USHORT: {
! 1928 LOG("BindParameterArray: Binding SQL_C_USHORT/STINYINT array - param_index=%d, count=%zu", paramIndex, paramSetSize);
1929 unsigned short* dataArray = AllocateParamBufferArray<unsigned short>(tempBuffers, paramSetSize);
1930 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
1931 for (size_t i = 0; i < paramSetSize; ++i) {
1932 if (columnValues[i].is_none()) {Lines 1936-1944 1936 dataArray[i] = columnValues[i].cast<unsigned short>();
1937 strLenOrIndArray[i] = 0;
1938 }
1939 }
! 1940 LOG("BindParameterArray: SQL_C_USHORT bound - param_index=%d", paramIndex);
1941 dataPtr = dataArray;
1942 bufferLength = sizeof(unsigned short);
1943 break;
1944 }Lines 1981-1989 1981 bufferLength = sizeof(float);
1982 break;
1983 }
1984 case SQL_C_TYPE_DATE: {
! 1985 LOG("BindParameterArray: Binding SQL_C_TYPE_DATE array - param_index=%d, count=%zu", paramIndex, paramSetSize);
1986 SQL_DATE_STRUCT* dateArray = AllocateParamBufferArray<SQL_DATE_STRUCT>(tempBuffers, paramSetSize);
1987 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
1988 for (size_t i = 0; i < paramSetSize; ++i) {
1989 if (columnValues[i].is_none()) {Lines 1996-2004 1996 dateArray[i].day = dateObj.attr("day").cast<SQLUSMALLINT>();
1997 strLenOrIndArray[i] = 0;
1998 }
1999 }
! 2000 LOG("BindParameterArray: SQL_C_TYPE_DATE bound - param_index=%d", paramIndex);
2001 dataPtr = dateArray;
2002 bufferLength = sizeof(SQL_DATE_STRUCT);
2003 break;
2004 }Lines 2002-2010 2002 bufferLength = sizeof(SQL_DATE_STRUCT);
2003 break;
2004 }
2005 case SQL_C_TYPE_TIME: {
! 2006 LOG("BindParameterArray: Binding SQL_C_TYPE_TIME array - param_index=%d, count=%zu", paramIndex, paramSetSize);
2007 SQL_TIME_STRUCT* timeArray = AllocateParamBufferArray<SQL_TIME_STRUCT>(tempBuffers, paramSetSize);
2008 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
2009 for (size_t i = 0; i < paramSetSize; ++i) {
2010 if (columnValues[i].is_none()) {Lines 2017-2025 2017 timeArray[i].second = timeObj.attr("second").cast<SQLUSMALLINT>();
2018 strLenOrIndArray[i] = 0;
2019 }
2020 }
! 2021 LOG("BindParameterArray: SQL_C_TYPE_TIME bound - param_index=%d", paramIndex);
2022 dataPtr = timeArray;
2023 bufferLength = sizeof(SQL_TIME_STRUCT);
2024 break;
2025 }Lines 2023-2031 2023 bufferLength = sizeof(SQL_TIME_STRUCT);
2024 break;
2025 }
2026 case SQL_C_TYPE_TIMESTAMP: {
! 2027 LOG("BindParameterArray: Binding SQL_C_TYPE_TIMESTAMP array - param_index=%d, count=%zu", paramIndex, paramSetSize);
2028 SQL_TIMESTAMP_STRUCT* tsArray = AllocateParamBufferArray<SQL_TIMESTAMP_STRUCT>(tempBuffers, paramSetSize);
2029 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
2030 for (size_t i = 0; i < paramSetSize; ++i) {
2031 if (columnValues[i].is_none()) {Lines 2042-2050 2042 tsArray[i].fraction = static_cast<SQLUINTEGER>(dtObj.attr("microsecond").cast<int>() * 1000); // µs to ns
2043 strLenOrIndArray[i] = 0;
2044 }
2045 }
! 2046 LOG("BindParameterArray: SQL_C_TYPE_TIMESTAMP bound - param_index=%d", paramIndex);
2047 dataPtr = tsArray;
2048 bufferLength = sizeof(SQL_TIMESTAMP_STRUCT);
2049 break;
2050 }Lines 2097-2105 2097 bufferLength = sizeof(DateTimeOffset);
2098 break;
2099 }
2100 case SQL_C_NUMERIC: {
! 2101 LOG("BindParameterArray: Binding SQL_C_NUMERIC array - param_index=%d, count=%zu", paramIndex, paramSetSize);
2102 SQL_NUMERIC_STRUCT* numericArray = AllocateParamBufferArray<SQL_NUMERIC_STRUCT>(tempBuffers, paramSetSize);
2103 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
2104 for (size_t i = 0; i < paramSetSize; ++i) {
2105 const py::handle& element = columnValues[i];Lines 2108-2121 2108 std::memset(&numericArray[i], 0, sizeof(SQL_NUMERIC_STRUCT));
2109 continue;
2110 }
2111 if (!py::isinstance<NumericData>(element)) {
! 2112 LOG("BindParameterArray: NUMERIC type mismatch - param_index=%d, row=%zu", paramIndex, i);
2113 throw std::runtime_error(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
2114 }
2115 NumericData decimalParam = element.cast<NumericData>();
! 2116 LOG("BindParameterArray: NUMERIC value - param_index=%d, row=%zu, precision=%d, scale=%d, sign=%d",
! 2117 paramIndex, i, decimalParam.precision, decimalParam.scale, decimalParam.sign);
2118 SQL_NUMERIC_STRUCT& target = numericArray[i];
2119 std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
2120 target.precision = decimalParam.precision;
2121 target.scale = decimalParam.scale;Lines 2125-2133 2125 std::memcpy(target.val, decimalParam.val.data(), copyLen);
2126 }
2127 strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT);
2128 }
! 2129 LOG("BindParameterArray: SQL_C_NUMERIC bound - param_index=%d", paramIndex);
2130 dataPtr = numericArray;
2131 bufferLength = sizeof(SQL_NUMERIC_STRUCT);
2132 break;
2133 }Lines 2151-2160 2151 }
2152 else if (py::isinstance<py::bytes>(element)) {
2153 py::bytes b = element.cast<py::bytes>();
2154 if (PyBytes_GET_SIZE(b.ptr()) != 16) {
! 2155 LOG("BindParameterArray: GUID bytes wrong length - param_index=%d, row=%zu, length=%d",
! 2156 paramIndex, i, PyBytes_GET_SIZE(b.ptr()));
2157 ThrowStdException("UUID binary data must be exactly 16 bytes long.");
2158 }
2159 std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
2160 }Lines 2162-2170 2162 py::bytes b = element.attr("bytes_le").cast<py::bytes>();
2163 std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
2164 }
2165 else {
! 2166 LOG("BindParameterArray: GUID type mismatch - param_index=%d, row=%zu", paramIndex, i);
2167 ThrowStdException(MakeParamMismatchErrorStr(info.paramCType, paramIndex));
2168 }
2169 guidArray[i].Data1 = (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
2170 (static_cast<uint32_t>(uuid_bytes[2]) << 16) |Lines 2183-2191 2183 bufferLength = sizeof(SQLGUID);
2184 break;
2185 }
2186 default: {
! 2187 LOG("BindParameterArray: Unsupported C type - param_index=%d, C_type=%d", paramIndex, info.paramCType);
2188 ThrowStdException("BindParameterArray: Unsupported C type: " + std::to_string(info.paramCType));
2189 }
2190 }
2191 LOG("BindParameterArray: Calling SQLBindParameter - param_index=%d, buffer_length=%lld", Lines 2202-2215 2202 bufferLength,
2203 strLenOrIndArray
2204 );
2205 if (!SQL_SUCCEEDED(rc)) {
! 2206 LOG("BindParameterArray: SQLBindParameter failed - param_index=%d, SQLRETURN=%d", paramIndex, rc);
2207 return rc;
2208 }
2209 }
2210 } catch (...) {
! 2211 LOG("BindParameterArray: Exception during binding, cleaning up buffers");
2212 throw;
2213 }
2214 paramBuffers.insert(paramBuffers.end(), tempBuffers.begin(), tempBuffers.end());
2215 LOG("BindParameterArray: Successfully bound all parameters - total_params=%zu, buffer_count=%zu",Lines 2236-2246 2236 LOG("SQLExecuteMany: Using wide string query directly");
2237 #endif
2238 RETCODE rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS);
2239 if (!SQL_SUCCEEDED(rc)) {
! 2240 LOG("SQLExecuteMany: SQLPrepare failed - rc=%d", rc);
! 2241 return rc;
! 2242 }
2243 LOG("SQLExecuteMany: Query prepared successfully");
2244
2245 bool hasDAE = false;
2246 for (const auto& p : paramInfos) {Lines 2254-2270 2254 LOG("SQLExecuteMany: Using array binding (non-DAE) - calling BindParameterArray");
2255 std::vector<std::shared_ptr<void>> paramBuffers;
2256 rc = BindParameterArray(hStmt, columnwise_params, paramInfos, paramSetSize, paramBuffers);
2257 if (!SQL_SUCCEEDED(rc)) {
! 2258 LOG("SQLExecuteMany: BindParameterArray failed - rc=%d", rc);
! 2259 return rc;
! 2260 }
2261
2262 rc = SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)paramSetSize, 0);
2263 if (!SQL_SUCCEEDED(rc)) {
! 2264 LOG("SQLExecuteMany: SQLSetStmtAttr(PARAMSET_SIZE) failed - rc=%d", rc);
! 2265 return rc;
! 2266 }
2267 LOG("SQLExecuteMany: PARAMSET_SIZE set to %zu", paramSetSize);
2268
2269 rc = SQLExecute_ptr(hStmt);
2270 LOG("SQLExecuteMany: SQLExecute completed - rc=%d", rc);Lines 2269-2342 2269 rc = SQLExecute_ptr(hStmt);
2270 LOG("SQLExecuteMany: SQLExecute completed - rc=%d", rc);
2271 return rc;
2272 } else {
! 2273 LOG("SQLExecuteMany: Using DAE (data-at-execution) - row_count=%zu", columnwise_params.size());
2274 size_t rowCount = columnwise_params.size();
2275 for (size_t rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
! 2276 LOG("SQLExecuteMany: Processing DAE row %zu of %zu", rowIndex + 1, rowCount);
2277 py::list rowParams = columnwise_params[rowIndex];
2278
2279 std::vector<std::shared_ptr<void>> paramBuffers;
2280 rc = BindParameters(hStmt, rowParams, const_cast<std::vector<ParamInfo>&>(paramInfos), paramBuffers);
! 2281 if (!SQL_SUCCEEDED(rc)) {
! 2282 LOG("SQLExecuteMany: BindParameters failed for row %zu - rc=%d", rowIndex, rc);
! 2283 return rc;
! 2284 }
! 2285 LOG("SQLExecuteMany: Parameters bound for row %zu", rowIndex);
2286
2287 rc = SQLExecute_ptr(hStmt);
! 2288 LOG("SQLExecuteMany: SQLExecute for row %zu - initial_rc=%d", rowIndex, rc);
! 2289 size_t dae_chunk_count = 0;
2290 while (rc == SQL_NEED_DATA) {
2291 SQLPOINTER token;
2292 rc = SQLParamData_ptr(hStmt, &token);
! 2293 LOG("SQLExecuteMany: SQLParamData called - chunk=%zu, rc=%d, token=%p",
! 2294 dae_chunk_count, rc, token);
! 2295 if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2296 LOG("SQLExecuteMany: SQLParamData failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2297 return rc;
! 2298 }
2299
2300 py::object* py_obj_ptr = reinterpret_cast<py::object*>(token);
! 2301 if (!py_obj_ptr) {
! 2302 LOG("SQLExecuteMany: NULL token pointer in DAE - chunk=%zu", dae_chunk_count);
! 2303 return SQL_ERROR;
! 2304 }
2305
2306 if (py::isinstance<py::str>(*py_obj_ptr)) {
2307 std::string data = py_obj_ptr->cast<std::string>();
2308 SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2309 LOG("SQLExecuteMany: Sending string DAE data - chunk=%zu, length=%lld",
! 2310 dae_chunk_count, static_cast<long long>(data_len));
2311 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
! 2312 if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2313 LOG("SQLExecuteMany: SQLPutData(string) failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2314 }
2315 } else if (py::isinstance<py::bytes>(*py_obj_ptr) || py::isinstance<py::bytearray>(*py_obj_ptr)) {
2316 std::string data = py_obj_ptr->cast<std::string>();
2317 SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2318 LOG("SQLExecuteMany: Sending bytes/bytearray DAE data - chunk=%zu, length=%lld",
! 2319 dae_chunk_count, static_cast<long long>(data_len));
2320 rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
! 2321 if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2322 LOG("SQLExecuteMany: SQLPutData(bytes) failed - chunk=%zu, rc=%d", dae_chunk_count, rc);
! 2323 }
2324 } else {
! 2325 LOG("SQLExecuteMany: Unsupported DAE data type - chunk=%zu", dae_chunk_count);
2326 return SQL_ERROR;
2327 }
! 2328 dae_chunk_count++;
2329 }
! 2330 LOG("SQLExecuteMany: DAE completed for row %zu - total_chunks=%zu, final_rc=%d",
! 2331 rowIndex, dae_chunk_count, rc);
2332
! 2333 if (!SQL_SUCCEEDED(rc)) {
! 2334 LOG("SQLExecuteMany: DAE row %zu failed - rc=%d", rowIndex, rc);
! 2335 return rc;
! 2336 }
2337 }
! 2338 LOG("SQLExecuteMany: All DAE rows processed successfully - total_rows=%zu", rowCount);
2339 return SQL_SUCCESS;
2340 }
2341 }Lines 2344-2352 2344 // Wrap SQLNumResultCols
2345 SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) {
2346 LOG("SQLNumResultCols: Getting number of columns in result set for statement_handle=%p", (void*)statementHandle->get());
2347 if (!SQLNumResultCols_ptr) {
! 2348 LOG("SQLNumResultCols: Function pointer not initialized, loading driver");
2349 DriverLoader::getInstance().loadDriver(); // Load the driver
2350 }
2351
2352 SQLSMALLINT columnCount;Lines 2358-2366 2358 // Wrap SQLDescribeCol
2359 SQLRETURN SQLDescribeCol_wrap(SqlHandlePtr StatementHandle, py::list& ColumnMetadata) {
2360 LOG("SQLDescribeCol: Getting column descriptions for statement_handle=%p", (void*)StatementHandle->get());
2361 if (!SQLDescribeCol_ptr) {
! 2362 LOG("SQLDescribeCol: Function pointer not initialized, loading driver");
2363 DriverLoader::getInstance().loadDriver(); // Load the driver
2364 }
2365
2366 SQLSMALLINT ColumnCount;Lines 2366-2374 2366 SQLSMALLINT ColumnCount;
2367 SQLRETURN retcode =
2368 SQLNumResultCols_ptr(StatementHandle->get(), &ColumnCount);
2369 if (!SQL_SUCCEEDED(retcode)) {
! 2370 LOG("SQLDescribeCol: Failed to get number of columns - SQLRETURN=%d", retcode);
2371 return retcode;
2372 }
2373
2374 for (SQLUSMALLINT i = 1; i <= ColumnCount; ++i) {Lines 2450-2460 2450 }
2451
2452 // Wrap SQLFetch to retrieve rows
2453 SQLRETURN SQLFetch_wrap(SqlHandlePtr StatementHandle) {
! 2454 LOG("SQLFetch: Fetching next row for statement_handle=%p", (void*)StatementHandle->get());
2455 if (!SQLFetch_ptr) {
! 2456 LOG("SQLFetch: Function pointer not initialized, loading driver");
2457 DriverLoader::getInstance().loadDriver(); // Load the driver
2458 }
2459
2460 return SQLFetch_ptr(StatementHandle->get());Lines 2487-2495 2487 oss << "Error fetching LOB for column " << colIndex
2488 << ", cType=" << cType
2489 << ", loop=" << loopCount
2490 << ", SQLGetData return=" << ret;
! 2491 LOG("FetchLobColumnData: %s", oss.str().c_str());
2492 ThrowStdException(oss.str());
2493 }
2494 if (actualRead == SQL_NULL_DATA) {
2495 LOG("FetchLobColumnData: Column %d is NULL at loop %d", colIndex, loopCount);Lines 2581-2589 2581 // Helper function to retrieve column data
2582 SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row) {
2583 LOG("SQLGetData: Getting data from %d columns for statement_handle=%p", colCount, (void*)StatementHandle->get());
2584 if (!SQLGetData_ptr) {
! 2585 LOG("SQLGetData: Function pointer not initialized, loading driver");
2586 DriverLoader::getInstance().loadDriver(); // Load the driver
2587 }
2588
2589 SQLRETURN ret = SQL_SUCCESS;Lines 2602-2610 2602
2603 ret = SQLDescribeCol_ptr(hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR),
2604 &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable);
2605 if (!SQL_SUCCEEDED(ret)) {
! 2606 LOG("SQLGetData: Error retrieving metadata for column %d - SQLDescribeCol SQLRETURN=%d", i, ret);
2607 row.append(py::none());
2608 continue;
2609 }Lines 2634-2642 2634 row.append(std::string(reinterpret_cast<char*>(dataBuffer.data())));
2635 #endif
2636 } else {
2637 // Buffer too small, fallback to streaming
! 2638 LOG("SQLGetData: CHAR column %d data truncated (buffer_size=%zu), using streaming LOB", i, dataBuffer.size());
2639 row.append(FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false));
2640 }
2641 } else if (dataLen == SQL_NULL_DATA) {
2642 LOG("SQLGetData: Column %d is NULL (CHAR)", i);Lines 2643-2658 2643 row.append(py::none());
2644 } else if (dataLen == 0) {
2645 row.append(py::str(""));
2646 } else if (dataLen == SQL_NO_TOTAL) {
! 2647 LOG("SQLGetData: Cannot determine data length (SQL_NO_TOTAL) for column %d (SQL_CHAR), returning NULL", i);
2648 row.append(py::none());
2649 } else if (dataLen < 0) {
! 2650 LOG("SQLGetData: Unexpected negative data length for column %d - dataType=%d, dataLen=%ld", i, dataType, (long)dataLen);
2651 ThrowStdException("SQLGetData returned an unexpected negative data length");
2652 }
2653 } else {
! 2654 LOG("SQLGetData: Error retrieving data for column %d (SQL_CHAR) - SQLRETURN=%d, returning NULL", i, ret);
2655 row.append(py::none());
2656 }
2657 }
2658 break;Lines 2697-2712 2697 row.append(py::none());
2698 } else if (dataLen == 0) {
2699 row.append(py::str(""));
2700 } else if (dataLen == SQL_NO_TOTAL) {
! 2701 LOG("SQLGetData: Cannot determine NVARCHAR data length (SQL_NO_TOTAL) for column %d, returning NULL", i);
2702 row.append(py::none());
2703 } else if (dataLen < 0) {
! 2704 LOG("SQLGetData: Unexpected negative data length for column %d (NVARCHAR) - dataLen=%ld", i, (long)dataLen);
2705 ThrowStdException("SQLGetData returned an unexpected negative data length");
2706 }
2707 } else {
! 2708 LOG("SQLGetData: Error retrieving data for column %d (NVARCHAR) - SQLRETURN=%d", i, ret);
2709 row.append(py::none());
2710 }
2711 }
2712 break;Lines 2726-2734 2726 ret = SQLGetData_ptr(hStmt, i, SQL_C_SHORT, &smallIntValue, 0, NULL);
2727 if (SQL_SUCCEEDED(ret)) {
2728 row.append(static_cast<int>(smallIntValue));
2729 } else {
! 2730 LOG("SQLGetData: Error retrieving SQL_SMALLINT for column %d - SQLRETURN=%d", i, ret);
2731 row.append(py::none());
2732 }
2733 break;
2734 }Lines 2737-2745 2737 ret = SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL);
2738 if (SQL_SUCCEEDED(ret)) {
2739 row.append(realValue);
2740 } else {
! 2741 LOG("SQLGetData: Error retrieving SQL_REAL for column %d - SQLRETURN=%d", i, ret);
2742 row.append(py::none());
2743 }
2744 break;
2745 }Lines 2780-2793 2780 py::object decimalObj = PythonObjectCache::get_decimal_class()(py::str(cnum, safeLen));
2781 row.append(decimalObj);
2782 } catch (const py::error_already_set& e) {
2783 // If conversion fails, append None
! 2784 LOG("SQLGetData: Error converting to decimal for column %d - %s", i, e.what());
2785 row.append(py::none());
2786 }
2787 }
2788 else {
! 2789 LOG("SQLGetData: Error retrieving SQL_NUMERIC/DECIMAL for column %d - SQLRETURN=%d", i, ret);
2790 row.append(py::none());
2791 }
2792 break;
2793 }Lines 2798-2806 2798 ret = SQLGetData_ptr(hStmt, i, SQL_C_DOUBLE, &doubleValue, 0, NULL);
2799 if (SQL_SUCCEEDED(ret)) {
2800 row.append(doubleValue);
2801 } else {
! 2802 LOG("SQLGetData: Error retrieving SQL_DOUBLE/FLOAT for column %d - SQLRETURN=%d", i, ret);
2803 row.append(py::none());
2804 }
2805 break;
2806 }Lines 2809-2817 2809 ret = SQLGetData_ptr(hStmt, i, SQL_C_SBIGINT, &bigintValue, 0, NULL);
2810 if (SQL_SUCCEEDED(ret)) {
2811 row.append(static_cast<long long>(bigintValue));
2812 } else {
! 2813 LOG("SQLGetData: Error retrieving SQL_BIGINT for column %d - SQLRETURN=%d", i, ret);
2814 row.append(py::none());
2815 }
2816 break;
2817 }Lines 2846-2854 2846 timeValue.second
2847 )
2848 );
2849 } else {
! 2850 LOG("SQLGetData: Error retrieving SQL_TYPE_TIME for column %d - SQLRETURN=%d", i, ret);
2851 row.append(py::none());
2852 }
2853 break;
2854 }Lines 2870-2878 2870 timestampValue.fraction / 1000 // Convert back ns to µs
2871 )
2872 );
2873 } else {
! 2874 LOG("SQLGetData: Error retrieving SQL_TYPE_TIMESTAMP for column %d - SQLRETURN=%d", i, ret);
2875 row.append(py::none());
2876 }
2877 break;
2878 }Lines 2919-2927 2919 tzinfo
2920 );
2921 row.append(py_dt);
2922 } else {
! 2923 LOG("SQLGetData: Error fetching DATETIMEOFFSET for column %d - SQLRETURN=%d, indicator=%ld", i, ret, (long)indicator);
2924 row.append(py::none());
2925 }
2926 break;
2927 }Lines 2952-2964 2952 } else {
2953 std::ostringstream oss;
2954 oss << "Unexpected negative length (" << dataLen << ") returned by SQLGetData. ColumnID="
2955 << i << ", dataType=" << dataType << ", bufferSize=" << columnSize;
! 2956 LOG("SQLGetData: %s", oss.str().c_str());
2957 ThrowStdException(oss.str());
2958 }
2959 } else {
! 2960 LOG("SQLGetData: Error retrieving VARBINARY data for column %d - SQLRETURN=%d", i, ret);
2961 row.append(py::none());
2962 }
2963 }
2964 break;Lines 2968-2976 2968 ret = SQLGetData_ptr(hStmt, i, SQL_C_TINYINT, &tinyIntValue, 0, NULL);
2969 if (SQL_SUCCEEDED(ret)) {
2970 row.append(static_cast<int>(tinyIntValue));
2971 } else {
! 2972 LOG("SQLGetData: Error retrieving SQL_TINYINT for column %d - SQLRETURN=%d", i, ret);
2973 row.append(py::none());
2974 }
2975 break;
2976 }Lines 2979-2987 2979 ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL);
2980 if (SQL_SUCCEEDED(ret)) {
2981 row.append(static_cast<bool>(bitValue));
2982 } else {
! 2983 LOG("SQLGetData: Error retrieving SQL_BIT for column %d - SQLRETURN=%d", i, ret);
2984 row.append(py::none());
2985 }
2986 break;
2987 }Lines 3008-3016 3008 row.append(uuid_obj);
3009 } else if (indicator == SQL_NULL_DATA) {
3010 row.append(py::none());
3011 } else {
! 3012 LOG("SQLGetData: Error retrieving SQL_GUID for column %d - SQLRETURN=%d, indicator=%ld", i, ret, (long)indicator);
3013 row.append(py::none());
3014 }
3015 break;
3016 }Lines 3018-3026 3018 default:
3019 std::ostringstream errorString;
3020 errorString << "Unsupported data type for column - " << columnName << ", Type - "
3021 << dataType << ", column ID - " << i;
! 3022 LOG("SQLGetData: %s", errorString.str().c_str());
3023 ThrowStdException(errorString.str());
3024 break;
3025 }
3026 }Lines 3029-3037 3029
3030 SQLRETURN SQLFetchScroll_wrap(SqlHandlePtr StatementHandle, SQLSMALLINT FetchOrientation, SQLLEN FetchOffset, py::list& row_data) {
3031 LOG("SQLFetchScroll_wrap: Fetching with scroll orientation=%d, offset=%ld", FetchOrientation, (long)FetchOffset);
3032 if (!SQLFetchScroll_ptr) {
! 3033 LOG("SQLFetchScroll_wrap: Function pointer not initialized. Loading the driver.");
3034 DriverLoader::getInstance().loadDriver(); // Load the driver
3035 }
3036
3037 // Unbind any columns from previous fetch operations to avoid memory corruptionLines 3191-3199 3191 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
3192 std::ostringstream errorString;
3193 errorString << "Unsupported data type for column - " << columnName.c_str()
3194 << ", Type - " << dataType << ", column ID - " << col;
! 3195 LOG("SQLBindColums: %s", errorString.str().c_str());
3196 ThrowStdException(errorString.str());
3197 break;
3198 }
3199 if (!SQL_SUCCEEDED(ret)) {Lines 3200-3208 3200 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
3201 std::ostringstream errorString;
3202 errorString << "Failed to bind column - " << columnName.c_str() << ", Type - "
3203 << dataType << ", column ID - " << col;
! 3204 LOG("SQLBindColums: %s", errorString.str().c_str());
3205 ThrowStdException(errorString.str());
3206 return ret;
3207 }
3208 }Lines 3370-3378 3370 PyList_SET_ITEM(row, col - 1, Py_None);
3371 continue;
3372 } else if (dataLen < 0) {
3373 // Negative value is unexpected, log column index, SQL type & raise exception
! 3374 LOG("FetchBatchData: Unexpected negative data length - column=%d, SQL_type=%d, dataLen=%ld", col, dataType, (long)dataLen);
3375 ThrowStdException("Unexpected negative data length, check logs for details");
3376 }
3377 assert(dataLen > 0 && "Data length must be > 0");Lines 3481-3489 3481 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
3482 std::ostringstream errorString;
3483 errorString << "Unsupported data type for column - " << columnName.c_str()
3484 << ", Type - " << dataType << ", column ID - " << col;
! 3485 LOG("FetchBatchData: %s", errorString.str().c_str());
3486 ThrowStdException(errorString.str());
3487 break;
3488 }
3489 }Lines 3579-3587 3579 std::wstring columnName = columnMeta["ColumnName"].cast<std::wstring>();
3580 std::ostringstream errorString;
3581 errorString << "Unsupported data type for column - " << columnName.c_str()
3582 << ", Type - " << dataType << ", column ID - " << col;
! 3583 LOG("calculateRowSize: %s", errorString.str().c_str());
3584 ThrowStdException(errorString.str());
3585 break;
3586 }
3587 }Lines 3611-3619 3611 // Retrieve column metadata
3612 py::list columnNames;
3613 ret = SQLDescribeCol_wrap(StatementHandle, columnNames);
3614 if (!SQL_SUCCEEDED(ret)) {
! 3615 LOG("FetchMany_wrap: Failed to get column descriptions - SQLRETURN=%d", ret);
3616 return ret;
3617 }
3618
3619 std::vector<SQLUSMALLINT> lobColumns;Lines 3650-3658 3650
3651 // Bind columns
3652 ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize);
3653 if (!SQL_SUCCEEDED(ret)) {
! 3654 LOG("FetchMany_wrap: Error when binding columns - SQLRETURN=%d", ret);
3655 return ret;
3656 }
3657
3658 SQLULEN numRowsFetched;Lines 3660-3668 3660 SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0);
3661
3662 ret = FetchBatchData(hStmt, buffers, columnNames, rows, numCols, numRowsFetched, lobColumns);
3663 if (!SQL_SUCCEEDED(ret) && ret != SQL_NO_DATA) {
! 3664 LOG("FetchMany_wrap: Error when fetching data - SQLRETURN=%d", ret);
3665 return ret;
3666 }
3667
3668 // Reset attributes before returning to avoid using stack pointers laterLines 3693-3701 3693 // Retrieve column metadata
3694 py::list columnNames;
3695 ret = SQLDescribeCol_wrap(StatementHandle, columnNames);
3696 if (!SQL_SUCCEEDED(ret)) {
! 3697 LOG("FetchAll_wrap: Failed to get column descriptions - SQLRETURN=%d", ret);
3698 return ret;
3699 }
3700
3701 // Define a memory limit (1 GB)Lines 3770-3778 3770
3771 // Bind columns
3772 ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize);
3773 if (!SQL_SUCCEEDED(ret)) {
! 3774 LOG("FetchAll_wrap: Error when binding columns - SQLRETURN=%d", ret);
3775 return ret;
3776 }
3777
3778 SQLULEN numRowsFetched;Lines 3826-3834 3826 // Wrap SQLMoreResults
3827 SQLRETURN SQLMoreResults_wrap(SqlHandlePtr StatementHandle) {
3828 LOG("SQLMoreResults_wrap: Check for more results");
3829 if (!SQLMoreResults_ptr) {
! 3830 LOG("SQLMoreResults_wrap: Function pointer not initialized. Loading the driver.");
3831 DriverLoader::getInstance().loadDriver(); // Load the driver
3832 }
3833
3834 return SQLMoreResults_ptr(StatementHandle->get());Lines 3835-3845 3835 }
3836
3837 // Wrap SQLFreeHandle
3838 SQLRETURN SQLFreeHandle_wrap(SQLSMALLINT HandleType, SqlHandlePtr Handle) {
! 3839 LOG("SQLFreeHandle_wrap: Free SQL handle type=%d", HandleType);
3840 if (!SQLAllocHandle_ptr) {
! 3841 LOG("SQLFreeHandle_wrap: Function pointer not initialized. Loading the driver.");
3842 DriverLoader::getInstance().loadDriver(); // Load the driver
3843 }
3844
3845 SQLRETURN ret = SQLFreeHandle_ptr(HandleType, Handle->get());Lines 3843-3851 3843 }
3844
3845 SQLRETURN ret = SQLFreeHandle_ptr(HandleType, Handle->get());
3846 if (!SQL_SUCCEEDED(ret)) {
! 3847 LOG("SQLFreeHandle_wrap: SQLFreeHandle failed with error code - %d", ret);
3848 return ret;
3849 }
3850 return ret;
3851 }Lines 3853-3861 3853 // Wrap SQLRowCount
3854 SQLLEN SQLRowCount_wrap(SqlHandlePtr StatementHandle) {
3855 LOG("SQLRowCount_wrap: Get number of rows affected by last execute");
3856 if (!SQLRowCount_ptr) {
! 3857 LOG("SQLRowCount_wrap: Function pointer not initialized. Loading the driver.");
3858 DriverLoader::getInstance().loadDriver(); // Load the driver
3859 }
3860
3861 SQLLEN rowCount;Lines 3860-3868 3860
3861 SQLLEN rowCount;
3862 SQLRETURN ret = SQLRowCount_ptr(StatementHandle->get(), &rowCount);
3863 if (!SQL_SUCCEEDED(ret)) {
! 3864 LOG("SQLRowCount_wrap: SQLRowCount failed with error code - %d", ret);
3865 return ret;
3866 }
3867 LOG("SQLRowCount_wrap: SQLRowCount returned %ld", (long)rowCount);
3868 return rowCount;Lines 4041-4050 4041 mssql_python::logging::LoggerBridge::initialize();
4042 } catch (const std::exception& e) {
4043 // Log initialization failure but don't throw
4044 // Use std::cerr instead of fprintf for type-safe output
! 4045 std::cerr << "Logger bridge initialization failed: " << e.what() << std::endl;
! 4046 }
4047
4048 try {
4049 // Try loading the ODBC driver when the module is imported
4050 LOG("Module initialization: Loading ODBC driver");Lines 4050-4057 4050 LOG("Module initialization: Loading ODBC driver");
4051 DriverLoader::getInstance().loadDriver(); // Load the driver
4052 } catch (const std::exception& e) {
4053 // Log the error but don't throw - let the error happen when functions are called
! 4054 LOG("Module initialization: Failed to load ODBC driver - %s", e.what());
4055 }
4056 }mssql_python/pybind/logger_bridge.cppLines 26-35 26 std::lock_guard<std::mutex> lock(mutex_);
27
28 // Skip if already initialized (check inside lock to prevent TOCTOU race)
29 if (initialized_) {
! 30 return;
! 31 }
32
33 try {
34 // Acquire GIL for Python API calls
35 py::gil_scoped_acquire gil;Lines 55-68 55
56 } catch (const py::error_already_set& e) {
57 // Failed to initialize - log to stderr and continue
58 // (logging will be disabled but won't crash)
! 59 std::cerr << "LoggerBridge initialization failed: " << e.what() << std::endl;
! 60 initialized_ = false;
! 61 } catch (const std::exception& e) {
! 62 std::cerr << "LoggerBridge initialization failed: " << e.what() << std::endl;
! 63 initialized_ = false;
! 64 }
65 }
66
67 void LoggerBridge::updateLevel(int level) {
68 // Update the cached level atomicallyLines 69-83 69 // This is lock-free and can be called from any thread
70 cached_level_.store(level, std::memory_order_relaxed);
71 }
72
! 73 int LoggerBridge::getLevel() {
! 74 return cached_level_.load(std::memory_order_relaxed);
! 75 }
76
! 77 bool LoggerBridge::isInitialized() {
! 78 return initialized_;
! 79 }
80
81 std::string LoggerBridge::formatMessage(const char* format, va_list args) {
82 // Use a stack buffer for most messages (4KB should be enough)
83 char buffer[4096];Lines 92-101 92 va_end(args_copy);
93
94 if (result < 0) {
95 // Error during formatting
! 96 return "[Formatting error]";
! 97 }
98
99 if (result < static_cast<int>(sizeof(buffer))) {
100 // Message fit in buffer (vsnprintf guarantees null-termination)
101 return std::string(buffer, std::min(static_cast<size_t>(result), sizeof(buffer) - 1));Lines 102-123 102 }
103
104 // Message was truncated - allocate larger buffer
105 // (This should be rare for typical log messages)
! 106 std::vector<char> large_buffer(result + 1);
! 107 va_copy(args_copy, args);
108 // Use std::vsnprintf with explicit size for safety (C++11 standard)
109 // This is the recommended safe alternative to vsprintf
110 // DevSkim: ignore DS185832 - std::vsnprintf with size is safe
! 111 int final_result = std::vsnprintf(large_buffer.data(), large_buffer.size(), format, args_copy);
! 112 va_end(args_copy);
113
114 // Ensure null termination even if formatting fails
! 115 if (final_result < 0 || final_result >= static_cast<int>(large_buffer.size())) {
! 116 large_buffer[large_buffer.size() - 1] = '\0';
! 117 }
118
! 119 return std::string(large_buffer.data());
120 }
121
122 const char* LoggerBridge::extractFilename(const char* path) {
123 // Extract just the filename from full path using safer C++ string searchLines 121-130 121
122 const char* LoggerBridge::extractFilename(const char* path) {
123 // Extract just the filename from full path using safer C++ string search
124 if (!path) {
! 125 return "";
! 126 }
127
128 // Find last occurrence of Unix path separator
129 const char* filename = std::strrchr(path, '/');
130 if (filename) {Lines 131-146 131 return filename + 1;
132 }
133
134 // Try Windows path separator
! 135 filename = std::strrchr(path, '\\');
! 136 if (filename) {
! 137 return filename + 1;
! 138 }
139
140 // No path separator found, return the whole string
! 141 return path;
! 142 }
143
144 void LoggerBridge::log(int level, const char* file, int line,
145 const char* format, ...) {
146 // Fast level check (should already be done by macro, but double-check)Lines 144-158 144 void LoggerBridge::log(int level, const char* file, int line,
145 const char* format, ...) {
146 // Fast level check (should already be done by macro, but double-check)
147 if (!isLoggable(level)) {
! 148 return;
! 149 }
150
151 // Check if initialized
152 if (!initialized_ || !cached_logger_) {
! 153 return;
! 154 }
155
156 // Format the message
157 va_list args;
158 va_start(args, format);Lines 172-184 172 // Warn if message exceeds reasonable size (critical for troubleshooting)
173 constexpr size_t MAX_LOG_SIZE = 4095; // Keep same limit for consistency
174 if (complete_message.size() > MAX_LOG_SIZE) {
175 // Use stderr to notify about truncation (logging may be the truncated call itself)
! 176 std::cerr << "[MSSQL-Python] Warning: Log message truncated from "
! 177 << complete_message.size() << " bytes to " << MAX_LOG_SIZE
! 178 << " bytes at " << file << ":" << line << std::endl;
! 179 complete_message.resize(MAX_LOG_SIZE);
! 180 }
181
182 // Lock for Python call (minimize critical section)
183 std::lock_guard<std::mutex> lock(mutex_);
184 Lines 211-226 211
212 } catch (const py::error_already_set& e) {
213 // Python error during logging - ignore to prevent cascading failures
214 // (Logging errors should not crash the application)
! 215 (void)e; // Suppress unused variable warning
! 216 } catch (const std::exception& e) {
217 // Standard C++ exception - ignore
! 218 (void)e;
! 219 } catch (...) {
220 // Catch-all for unknown exceptions (non-standard exceptions, corrupted state, etc.)
221 // Logging must NEVER crash the application
! 222 }
223 }
224
225 } // namespace logging
226 } // namespace mssql_python📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.helpers.py: 66.6%
mssql_python.row.py: 67.4%
mssql_python.pybind.ddbc_bindings.cpp: 70.4%
mssql_python.pybind.connection.connection.cpp: 76.3%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 79.6%
mssql_python.pybind.ddbc_bindings.h: 79.7%
mssql_python.connection.py: 82.5%
mssql_python.cursor.py: 83.5%🔗 Quick Links
|
…nflict and include logger_bridge.hpp in ddbc_bindings.h
…e C++11 functions
- Changed log filename format: timestamp now has no separator (YYYYMMDDHHMMSS) - Added CSV header with metadata: script name, PID, Python version, OS info - Converted log format to CSV: Timestamp, ThreadID, Level, Location, Source, Message - Replaced trace ID with OS native thread ID for debugger compatibility - Updated Python formatter to parse [Python]/[DDBC] tags into Source column - Updated C++ logger_bridge to use makeRecord() for proper file/line attribution - Logs are now easily parseable as CSV for analysis in Excel/pandas - Counter logic for connection/cursor tracking kept internally but not displayed
- Updated LOGGING.md: * Changed log format examples from trace ID to CSV format * Updated filename format (YYYYMMDDHHMMSS with no separators) * Replaced trace ID section with Thread Tracking section * Added CSV parsing examples with pandas * Updated all log output samples to show CSV columns - Updated MSSQL-Python-Logging-Design.md: * Changed file handler config to describe CSV format * Replaced Trace ID System with Thread Tracking System * Updated architecture to reflect OS native thread IDs * Added CSV formatter implementation details * Updated all code examples to use setup_logging() API * Changed log output examples to CSV format - Thread tracking now uses OS native thread IDs (threading.get_native_id()) - CSV columns: Timestamp, ThreadID, Level, Location, Source, Message - File header includes metadata (PID, script name, Python version, etc.) - Easy analysis with pandas/Excel/CSV tools
- CSV format now mentioned only once as optional import capability - Focus on log structure and content, not format - Removed repetitive CSV parsing examples - Single section 'Importing Logs as CSV (Optional)' in LOGGING.md - Brief mention in design doc that format is importable as CSV
Features: - Whitelist log file extensions: .txt, .log, .csv only - Raise ValueError for invalid extensions - Export driver_logger for use in application code - Allow apps to use mssql-python's logger: from mssql_python.logging import driver_logger - Updated documentation with usage examples - Added validation in _setLevel() method Benefits: - Prevents accidental use of wrong file types - Clear error messages for invalid extensions - Unified logging - apps can use same logger as driver - Same format and thread tracking for app logs
- Added 'Executing query:' log at start of execute() method - Removed duplicate log statement that was causing queries to appear twice - executemany() uses existing detailed log (shows parameter set count) - Each query now logged exactly once at DEBUG level - Parameters excluded from basic query log (pending PII review)
… (Linux/macOS) Problem: - Linux/macOS performed double conversion for NVARCHAR columns - SQLWCHAR → std::wstring (via SQLWCHARToWString) → Python unicode - Created unnecessary intermediate std::wstring allocation Solution: - Use PyUnicode_DecodeUTF16() to convert UTF-16 directly to Python unicode - Single-step conversion eliminates intermediate allocation - Platform-specific optimization (Linux/macOS only) Impact: - Reduces memory allocations for wide-character string columns - Eliminates one full conversion step per NVARCHAR cell - Regular VARCHAR/CHAR columns unchanged (already optimal)
… (Linux/macOS) Problem: - Linux/macOS performed double conversion for NVARCHAR columns - SQLWCHAR → std::wstring (via SQLWCHARToWString) → Python unicode - Created unnecessary intermediate std::wstring allocation Solution: - Use PyUnicode_DecodeUTF16() to convert UTF-16 directly to Python unicode - Single-step conversion eliminates intermediate allocation - Platform-specific optimization (Linux/macOS only) Impact: - Reduces memory allocations for wide-character string columns - Eliminates one full conversion step per NVARCHAR cell - Regular VARCHAR/CHAR columns unchanged (already optimal)
Problem: - All numeric conversions used pybind11 wrappers with overhead: * Type detection, wrapper object creation, bounds checking * ~20-40 CPU cycles overhead per cell Solution: - Use direct Python C API calls: * PyLong_FromLong/PyLong_FromLongLong for integers * PyFloat_FromDouble for floats * PyBool_FromLong for booleans * PyList_SET_ITEM macro (no bounds check - list pre-sized) Changes: - SQL_INTEGER, SQL_SMALLINT, SQL_BIGINT, SQL_TINYINT → PyLong_* - SQL_BIT → PyBool_FromLong - SQL_REAL, SQL_DOUBLE, SQL_FLOAT → PyFloat_FromDouble - Added explicit NULL handling for each type Impact: - Eliminates pybind11 wrapper overhead for simple numeric types - Direct array access via PyList_SET_ITEM macro - Affects 7 common numeric SQL types
Problem: -------- Column metadata (dataType, columnSize, isLob, fetchBufferSize) was accessed from the columnInfos vector inside the hot row processing loop. For a query with 1,000 rows × 10 columns, this resulted in 10,000 struct field accesses. Each access involves: - Vector bounds checking - Large struct loading (~50+ bytes per ColumnInfo) - Poor cache locality (struct fields scattered in memory) - Cost: ~10-15 CPU cycles per access (L2 cache misses likely) Solution: --------- Prefetch metadata into tightly-packed local arrays before the row loop: - std::vector<SQLSMALLINT> dataTypes (2 bytes per element) - std::vector<SQLULEN> columnSizes (8 bytes per element) - std::vector<uint64_t> fetchBufferSizes (8 bytes per element) - std::vector<bool> isLobs (1 byte per element) Total: ~190 bytes for 10 columns vs 500+ bytes with structs. These arrays stay hot in L1 cache for the entire batch processing, eliminating repeated struct access overhead. Changes: -------- - Added 4 prefetch vectors before row processing loop - Added prefetch loop to populate metadata arrays (read columnInfos once) - Replaced all columnInfos[col-1].field accesses with array lookups - Updated SQL_CHAR/SQL_VARCHAR cases - Updated SQL_WCHAR/SQL_WVARCHAR cases - Updated SQL_BINARY/SQL_VARBINARY cases Impact: ------- - Eliminates O(rows × cols) metadata lookups - 10,000 array accesses @ 3-5 cycles vs 10,000 struct accesses @ 10-15 cycles - ~70% reduction in metadata access overhead - Better L1 cache utilization (190 bytes vs 500+ bytes) - Expected 15-25% overall performance improvement on large result sets
…icrosoft/mssql-python into bewithgaurav/logging_framework
Coverage Note: LOG Statements Not CoveredCurrent coverage: ~77% excludes ~150 C++ LOG() statements Why? Tests don't initialize logging (disabled by default for performance), so LOG statements return early without executing: if (!s_pythonLogger) return; // Early exit when logging disabledAttempted solutions:
Decision: Accept the gap. LOG statements work correctly in production when enabled. Can revisit in future PRs dedicated to increase coverage. |
Work Item / Issue Reference
Summary
Logging Framework Implementation
Overview
Implements a comprehensive logging system for mssql-python with both Python and C++ components, replacing the previous fragmented approach with a unified, performance-optimized solution.
Key Changes
🎯 Core Logging System
mssql_python/logging.py): Single DEBUG-level logger with file/stdout/both output modesmssql_python/pybind/logger_bridge.cpp): High-performance bridge from C++ to Python logging with zero overhead when disabledsetup_logging(output='file'|'stdout'|'both')- all-or-nothing DEBUG logging for troubleshooting🔧 Technical Improvements
📝 Logging Coverage
warnings.warn()calls with proper logging🧪 Testing & Quality
rownumberassertion (was expectingNone, should be-1)📚 Documentation
learnings/directoryPerformance Impact
Breaking Changes
logging_config.pyandget_logger()APIsetup_logging(output='file')for DEBUG loggingTesting
Related Work