Skip to content

Conversation

@jahnvi480
Copy link
Contributor

@jahnvi480 jahnvi480 commented Oct 23, 2025

Work Item / Issue Reference

AB#39049

GitHub Issue: #250


Summary

This pull request introduces robust and secure handling of encoding settings throughout the mssql_python codebase, with a particular focus on validating encoding names, enforcing UTF-16 restrictions for certain SQL types, and ensuring safe propagation of encoding parameters between Python and C++ layers. The changes improve both security (by preventing injection attacks) and correctness (by enforcing SQL Server encoding requirements), while also adding better error handling and logging for encoding-related issues.

Encoding validation and enforcement

  • Enhanced the _validate_encoding function in connection.py to strictly validate encoding names for security (length and allowed characters) and correctness (must be a valid Python codec), rejecting unsafe or invalid encodings.
  • Enforced that only UTF-16 encodings are allowed for SQL_WCHAR and SQL_WMETADATA types in both setencoding and setdecoding, with warnings and automatic fallback to 'utf-16le' if an invalid encoding is provided.

Propagation of encoding settings

  • Added helper methods in cursor.py (_get_encoding_settings and _get_decoding_settings) to retrieve encoding and decoding settings from the connection, with error handling and sensible defaults. These settings are now passed to statement execution and data fetching functions.

C++ layer encoding safety

  • Implemented new helper functions in ddbc_bindings.cpp for encoding/decoding strings (EncodingString, DecodingString), validating encoding names (is_valid_encoding), error modes, and extracting encoding settings safely from Python dictionaries. These changes ensure only safe and valid encodings are used in the native layer and provide detailed logging for failures.

Interface changes for parameter binding

  • Updated the parameter binding interface in the C++ layer to accept encoding settings, enabling correct handling of encoded data during SQL operations.

Error handling improvements

  • Expanded exception handling in cursor.py to include OperationalError and DatabaseError when retrieving encoding settings, ensuring that only database-related errors are caught and logged, and that safe defaults are used if retrieval fails.

Let me know if you want to walk through how encoding settings flow from Python to C++ or discuss the impact of these changes on query execution and data retrieval.

Copilot AI review requested due to automatic review settings October 23, 2025 15:45
@github-actions github-actions bot added the pr-size: large Substantial code update label Oct 23, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR strengthens encoding validation for SQL Server connections by ensuring UTF-16 encoding compliance when using wide character types. It adds validation checks with automatic fallback to safe defaults and removes duplicate test functions.

Key Changes

  • Added UTF-16 encoding enforcement for SQL_WCHAR in setencoding method
  • Enhanced setdecoding with UTF-16 validation for both SQL_WCHAR and SQL_WMETADATA types
  • Removed duplicate test functions (test_decimal_separator_*, test_lowercase_attribute, test_rowcount)

Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
mssql_python/connection.py Added encoding validation logic to enforce UTF-16 for wide character types with warning logs and safe defaults
tests/test_004_cursor.py Removed duplicate test functions for decimal separator, lowercase attribute, and rowcount functionality

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@github-actions
Copy link

github-actions bot commented Oct 23, 2025

📊 Code Coverage Report

🔥 Diff Coverage

62%


🎯 Overall Coverage

74%


📈 Total Lines Covered: 5028 out of 6781
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/connection.py (100%)
  • mssql_python/cursor.py (69.0%): Missing lines 301,303-304,310,328,330-332,334
  • mssql_python/pybind/ddbc_bindings.cpp (62.3%): Missing lines 204-207,210-212,214-216,230-231,236-239,243-247,250-252,255,257-258,260-262,264-266,268-271,274-278,282-285,287-289,294-299,301-303,305-307,310-314,316-318,320-321,323-328,330-334,336-340,375,377-378,435-437,440,444-445,449-452,455-457,460-463,465-466,468-472,474-476,478-484,486-487,490,494-497,499-504,508-515,530-533,535,537,539-541,548-549,587-588,617-618,626-627,634,638-640,642-644,647-648,665-666,672-675,684-685,688-692,694-695,702-703,709-712,721-722,727-730,732-733,738-739,741-742,747-748,758-759,763-766,785-786,805-806,811-814,838-841,871-872,897-898,925-926,933-938,963-965,996-997,1005-1007,1014-1015,1022-1024,1063-1065,1071-1072,1095-1096,1100-1101,1109,1113,1234,1237,1240,1324-1327,1421-1422,1789,1892,1901-1904,1914-1915,2105-2106,2135-2137,2143-2144,2156-2157,2159-2161,2163-2164,2186-2187,2232-2233,2245-2247,2259-2260,2264-2266,2300-2308,2319-2325,2385-2387,2403-2405,2412-2414,2445-2453,2455-2468,2470-2471,2478-2481,2490-2494,2526-2529,2545-2548,2554,2559,2602-2606,2610-2611,2614-2619,2628-2632,2636-2637,2640-2645,2654-2658,2662-2663,2666-2680,2702-2703,2707-2708,2713-2716,2761-2765,2770-2771,2775-2776,2779-2782,2788-2789,2792-2793,2823-2825,2829-2831,2841-2842,2865-2867,2932-2934,2949-2952,2955-2956,3110,3208-3210,3212,3217-3218,3250-3252,3291-3293,3295-3298,3304,3307-3312,3320-3325,3328-3331,3333-3335,3338-3341,3405-3408,3411-3414,3416-3418,3421-3423,3446-3448,3461-3463,3504-3505,3527-3528,3544-3545,3559-3560,3577-3578,3597-3598,3622-3623,3648-3649,3664-3665,3696-3700,3708-3711,3716-3718,3731-3733,3745-3747,3791-3793,3802-3804,4006-4007,4009-4011,4017-4018,4020-4021,4069-4071,4075-4077,4091-4094,4096-4097,4108-4111,4118-4123,4154-4156,4158-4163,4165-4167,4206-4207,4383-4384,4389-4390,4392-4394,4479-4480,4482-4484,4947-4948,5023-5024

Summary

  • Total: 1723 lines
  • Missing: 638 lines
  • Coverage: 62%

mssql_python/cursor.py

Lines 297-308

  297         """
  298         if hasattr(self._connection, 'getencoding'):
  299             try:
  300                 return self._connection.getencoding()
! 301             except (OperationalError, DatabaseError) as db_error:
  302                 # Only catch database-related errors, not programming errors
! 303                 log('warning', f"Failed to get encoding settings from connection due to database error: {db_error}")
! 304                 return {
  305                     'encoding': 'utf-16le', 
  306                     'ctype': ddbc_sql_const.SQL_WCHAR.value
  307                 }
  308             

Lines 306-314

  306                     'ctype': ddbc_sql_const.SQL_WCHAR.value
  307                 }
  308             
  309         # Return default encoding settings if getencoding is not available
! 310         return {
  311             'encoding': 'utf-16le',
  312             'ctype': ddbc_sql_const.SQL_WCHAR.value
  313         }

Lines 324-338

  324         """
  325         try:
  326             # Get decoding settings from connection for this SQL type
  327             return self._connection.getdecoding(sql_type)
! 328         except (OperationalError, DatabaseError) as db_error:
  329             # Only handle expected database-related errors
! 330             log('warning', f"Failed to get decoding settings for SQL type {sql_type} due to database error: {db_error}")
! 331             if sql_type == ddbc_sql_const.SQL_WCHAR.value:
! 332                 return {'encoding': 'utf-16le', 'ctype': ddbc_sql_const.SQL_WCHAR.value}
  333             else:
! 334                 return {'encoding': 'utf-8', 'ctype': ddbc_sql_const.SQL_CHAR.value}
  335 
  336     def _map_sql_type(  # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-return-statements,too-many-branches
  337         self,
  338         param: Any,

mssql_python/pybind/ddbc_bindings.cpp

Lines 200-220

  200 
  201 // Encoding function with fallback strategy
  202 static py::bytes EncodingString(const std::string& text,
  203                                 const std::string& encoding,
! 204                                 const std::string& errors = "strict") {
! 205     try {
! 206         py::gil_scoped_acquire gil;
! 207         py::str unicode_str = py::str(text);
  208 
  209         // Direct encoding - let Python handle errors strictly
! 210         py::bytes encoded = unicode_str.attr("encode")(encoding, errors);
! 211         return encoded;
! 212     } catch (const py::error_already_set& e) {
  213         // Re-raise Python exceptions (UnicodeEncodeError, etc.)
! 214         throw std::runtime_error("Encoding failed: " + std::string(e.what()));
! 215     }
! 216 }
  217 
  218 static py::str DecodingString(const char* data, size_t length,
  219                               const std::string& encoding,
  220                               const std::string& errors = "strict") {

Lines 226-344

  226         py::str decoded = byte_data.attr("decode")(encoding, errors);
  227         return decoded;
  228     } catch (const py::error_already_set& e) {
  229         // Re-raise Python exceptions (UnicodeDecodeError, etc.)
! 230         throw std::runtime_error("Decoding failed: " + std::string(e.what()));
! 231     }
  232 }
  233 
  234 // Helper function to validate that an encoding string is a legitimate Python
  235 // codec This prevents injection attacks while allowing all valid encodings
! 236 static bool is_valid_encoding(const std::string& enc) {
! 237     if (enc.empty() || enc.length() > 100) {  // Reasonable length limit
! 238         return false;
! 239     }
  240 
  241     // Check for potentially dangerous characters that shouldn't be in codec
  242     // names
! 243     for (char c : enc) {
! 244         if (!std::isalnum(c) && c != '-' && c != '_' && c != '.') {
! 245             return false;  // Reject suspicious characters
! 246         }
! 247     }
  248 
  249     // Verify it's a valid Python codec by attempting a test lookup
! 250     try {
! 251         py::gil_scoped_acquire gil;
! 252         py::module_ codecs = py::module_::import("codecs");
  253 
  254         // This will raise LookupError if the codec doesn't exist
! 255         codecs.attr("lookup")(enc);
  256 
! 257         return true;  // Codec exists and is valid
! 258     } catch (const py::error_already_set& e) {
  259         // Expected: LookupError for invalid codec names
! 260         LOG("Codec validation failed for '{}': {}", enc, e.what());
! 261         return false;  // Invalid codec name
! 262     } catch (const std::exception& e) {
  263         // Unexpected C++ exception during validation
! 264         LOG("Unexpected exception validating encoding '{}': {}", enc, e.what());
! 265         return false;
! 266     } catch (...) {
  267         // Last resort: unknown exception type
! 268         LOG("Unknown exception validating encoding '{}'", enc);
! 269         return false;
! 270     }
! 271 }
  272 
  273 // Helper function to validate error handling mode against an allowlist
! 274 static bool is_valid_error_mode(const std::string& mode) {
! 275     static const std::unordered_set<std::string> allowed = {
! 276         "strict", "ignore", "replace", "xmlcharrefreplace", "backslashreplace"};
! 277     return allowed.find(mode) != allowed.end();
! 278 }
  279 
  280 // Helper function to safely extract encoding settings from Python dict
  281 static std::pair<std::string, std::string> extract_encoding_settings(
! 282     const py::dict& settings) {
! 283     try {
! 284         std::string encoding = "utf-8";  // Default
! 285         std::string errors = "strict";   // Default
  286 
! 287         if (settings.contains("encoding") && !settings["encoding"].is_none()) {
! 288             std::string proposed_encoding =
! 289                 settings["encoding"].cast<std::string>();
  290 
  291             // SECURITY: Validate encoding to prevent injection attacks
  292             // Allows any valid Python codec (including SQL Server-supported
  293             // encodings)
! 294             if (is_valid_encoding(proposed_encoding)) {
! 295                 encoding = proposed_encoding;
! 296             } else {
! 297                 LOG("Invalid or unsafe encoding '{}' rejected, using default "
! 298                     "'utf-8'",
! 299                     proposed_encoding);
  300                 // Fall back to safe default
! 301                 encoding = "utf-8";
! 302             }
! 303         }
  304 
! 305         if (settings.contains("errors") && !settings["errors"].is_none()) {
! 306             std::string proposed_errors =
! 307                 settings["errors"].cast<std::string>();
  308 
  309             // SECURITY: Validate error mode against allowlist
! 310             if (is_valid_error_mode(proposed_errors)) {
! 311                 errors = proposed_errors;
! 312             } else {
! 313                 LOG("Invalid error mode '{}' rejected, using default 'strict'",
! 314                     proposed_errors);
  315                 // Fall back to safe default
! 316                 errors = "strict";
! 317             }
! 318         }
  319 
! 320         return std::make_pair(encoding, errors);
! 321     } catch (const py::error_already_set& e) {
  322         // Log Python exceptions (KeyError, TypeError, etc.)
! 323         LOG("Python exception while extracting encoding settings: {}. Using "
! 324             "defaults (utf-8, "
! 325             "strict)",
! 326             e.what());
! 327         return std::make_pair("utf-8", "strict");
! 328     } catch (const std::exception& e) {
  329         // Log C++ standard exceptions
! 330         LOG("Exception while extracting encoding settings: {}. Using defaults "
! 331             "(utf-8, strict)",
! 332             e.what());
! 333         return std::make_pair("utf-8", "strict");
! 334     } catch (...) {
  335         // Last resort: unknown exception type
! 336         LOG("Unknown exception while extracting encoding settings. Using "
! 337             "defaults (utf-8, strict)");
! 338         return std::make_pair("utf-8", "strict");
! 339     }
! 340 }
  341 
  342 namespace {
  343 
  344 const char* GetSqlCTypeAsString(const SQLSMALLINT cType) {

Lines 371-382

  371     }
  372 }
  373 
  374 std::string MakeParamMismatchErrorStr(const SQLSMALLINT cType,
! 375                                       const int paramIndex) {
  376     std::string errorString =
! 377         "Parameter's object type does not match parameter's C type. paramIndex "
! 378         "- " +
  379         std::to_string(paramIndex) + ", C type - " + GetSqlCTypeAsString(cType);
  380     return errorString;
  381 }

Lines 431-519

  431 
  432         // TODO: Add more data types like money, guid, interval, TVPs etc.
  433         switch (paramInfo.paramCType) {
  434             case SQL_C_CHAR: {
! 435                 if (!py::isinstance<py::str>(param)) {
! 436                     ThrowStdException(MakeParamMismatchErrorStr(
! 437                         paramInfo.paramCType, paramIndex));
  438                 }
  439 
! 440                 std::string strValue;
  441 
  442                 // Check if we have encoding settings and this is SQL_C_CHAR
  443                 // (not SQL_C_WCHAR)
! 444                 if (encoding_settings && !encoding_settings.is_none()) {
! 445                     try {
  446                         // SECURITY: Use extract_encoding_settings for full
  447                         // validation This validates encoding against allowlist
  448                         // and error mode
! 449                         py::dict settings_dict =
! 450                             encoding_settings.cast<py::dict>();
! 451                         auto [encoding, errors] =
! 452                             extract_encoding_settings(settings_dict);
  453 
  454                         // Validate ctype against allowlist
! 455                         if (settings_dict.contains("ctype")) {
! 456                             SQLSMALLINT ctype =
! 457                                 settings_dict["ctype"].cast<SQLSMALLINT>();
  458 
  459                             // Only SQL_C_CHAR and SQL_C_WCHAR are allowed
! 460                             if (ctype != SQL_C_CHAR && ctype != SQL_C_WCHAR) {
! 461                                 LOG("Invalid ctype {} for parameter {}, using "
! 462                                     "default",
! 463                                     ctype, paramIndex);
  464                                 // Fall through to default behavior
! 465                                 strValue = param.cast<std::string>();
! 466                             } else if (ctype == SQL_C_CHAR) {
  467                                 // Only use dynamic encoding for SQL_C_CHAR
! 468                                 py::bytes encoded_bytes =
! 469                                     EncodingString(param.cast<std::string>(),
! 470                                                    encoding, errors);
! 471                                 strValue = encoded_bytes.cast<std::string>();
! 472                             } else {
  473                                 // SQL_C_WCHAR - use default behavior
! 474                                 strValue = param.cast<std::string>();
! 475                             }
! 476                         } else {
  477                             // No ctype specified, use default behavior
! 478                             strValue = param.cast<std::string>();
! 479                         }
! 480                     } catch (const std::exception& e) {
! 481                         LOG("Encoding settings processing failed for parameter "
! 482                             "{}: {}. Using "
! 483                             "default.",
! 484                             paramIndex, e.what());
  485                         // Fall back to safe default behavior
! 486                         strValue = param.cast<std::string>();
! 487                     }
  488                 } else {
  489                     // No encoding settings, use default behavior
! 490                     strValue = param.cast<std::string>();
  491                 }
  492 
  493                 // Allocate buffer and copy string data
! 494                 size_t bufferSize =
! 495                     strValue.length() + 1;  // +1 for null terminator
! 496                 char* buffer =
! 497                     AllocateParamBufferArray<char>(paramBuffers, bufferSize);
  498 
! 499                 if (!buffer) {
! 500                     ThrowStdException(
! 501                         "Failed to allocate buffer for SQL_C_CHAR parameter at "
! 502                         "index " +
! 503                         std::to_string(paramIndex));
! 504                 }
  505 
  506                 // SECURITY: Validate size before copying to prevent buffer
  507                 // overflow
! 508                 size_t copyLength = strValue.length();
! 509                 if (copyLength >= bufferSize) {
! 510                     ThrowStdException(
! 511                         "Buffer overflow prevented: string length exceeds "
! 512                         "allocated buffer at "
! 513                         "index " +
! 514                         std::to_string(paramIndex));
! 515                 }
  516 
  517 // Use secure copy with bounds checking
  518 #ifdef _WIN32
  519                 // Windows: Use memcpy_s for secure copy

Lines 526-545

  526                         std::to_string(paramIndex));
  527                 }
  528 #else
  529                 // POSIX: Use std::copy_n with explicit bounds checking
! 530                 if (copyLength > 0) {
! 531                     std::copy_n(strValue.data(), copyLength, buffer);
! 532                 }
! 533 #endif
  534 
! 535                 buffer[copyLength] = '\0';  // Ensure null termination
  536 
! 537                 paramInfo.strLenOrInd = copyLength;
  538 
! 539                 LOG("Binding SQL_C_CHAR parameter at index {} with encoded "
! 540                     "length {}",
! 541                     paramIndex, strValue.length());
  542                 break;
  543             }
  544             case SQL_C_BINARY: {
  545                 if (!py::isinstance<py::str>(param) &&

Lines 544-553

  544             case SQL_C_BINARY: {
  545                 if (!py::isinstance<py::str>(param) &&
  546                     !py::isinstance<py::bytearray>(param) &&
  547                     !py::isinstance<py::bytes>(param)) {
! 548                     ThrowStdException(MakeParamMismatchErrorStr(
! 549                         paramInfo.paramCType, paramIndex));
  550                 }
  551                 if (paramInfo.isDAE) {
  552                     // Deferred execution for VARBINARY(MAX)
  553                     LOG("Parameter[{}] is marked for DAE streaming "

Lines 583-592

  583             case SQL_C_WCHAR: {
  584                 if (!py::isinstance<py::str>(param) &&
  585                     !py::isinstance<py::bytearray>(param) &&
  586                     !py::isinstance<py::bytes>(param)) {
! 587                     ThrowStdException(MakeParamMismatchErrorStr(
! 588                         paramInfo.paramCType, paramIndex));
  589                 }
  590                 if (paramInfo.isDAE) {
  591                     // deferred execution
  592                     LOG("Parameter[{}] is marked for DAE streaming",

Lines 613-622

  613                 break;
  614             }
  615             case SQL_C_BIT: {
  616                 if (!py::isinstance<py::bool_>(param)) {
! 617                     ThrowStdException(MakeParamMismatchErrorStr(
! 618                         paramInfo.paramCType, paramIndex));
  619                 }
  620                 dataPtr = static_cast<void*>(AllocateParamBuffer<bool>(
  621                     paramBuffers, param.cast<bool>()));
  622                 break;

Lines 622-631

  622                 break;
  623             }
  624             case SQL_C_DEFAULT: {
  625                 if (!py::isinstance<py::none>(param)) {
! 626                     ThrowStdException(MakeParamMismatchErrorStr(
! 627                         paramInfo.paramCType, paramIndex));
  628                 }
  629                 SQLSMALLINT sqlType = paramInfo.paramSQLType;
  630                 SQLULEN columnSize = paramInfo.columnSize;
  631                 SQLSMALLINT decimalDigits = paramInfo.decimalDigits;

Lines 630-652

  630                 SQLULEN columnSize = paramInfo.columnSize;
  631                 SQLSMALLINT decimalDigits = paramInfo.decimalDigits;
  632                 if (sqlType == SQL_UNKNOWN_TYPE) {
  633                     SQLSMALLINT describedType;
! 634                     SQLULEN describedSize;
  635                     SQLSMALLINT describedDigits;
  636                     SQLSMALLINT nullable;
  637                     RETCODE rc = SQLDescribeParam_ptr(
! 638                         hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1),
! 639                         &describedType, &describedSize, &describedDigits,
! 640                         &nullable);
  641                     if (!SQL_SUCCEEDED(rc)) {
! 642                         LOG("SQLDescribeParam failed for parameter {} with "
! 643                             "error code {}",
! 644                             paramIndex, rc);
  645                         return rc;
  646                     }
! 647                     sqlType = describedType;
! 648                     columnSize = describedSize;
  649                     decimalDigits = describedDigits;
  650                 }
  651                 dataPtr = nullptr;
  652                 strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);

Lines 661-670

  661             case SQL_C_TINYINT:
  662             case SQL_C_SSHORT:
  663             case SQL_C_SHORT: {
  664                 if (!py::isinstance<py::int_>(param)) {
! 665                     ThrowStdException(MakeParamMismatchErrorStr(
! 666                         paramInfo.paramCType, paramIndex));
  667                 }
  668                 int value = param.cast<int>();
  669                 // Range validation for signed 16-bit integer
  670                 if (value < std::numeric_limits<int16_t>::min() ||

Lines 668-679

  668                 int value = param.cast<int>();
  669                 // Range validation for signed 16-bit integer
  670                 if (value < std::numeric_limits<int16_t>::min() ||
  671                     value > std::numeric_limits<int16_t>::max()) {
! 672                     ThrowStdException(
! 673                         "Signed short integer parameter out of range at "
! 674                         "paramIndex " +
! 675                         std::to_string(paramIndex));
  676                 }
  677                 dataPtr = static_cast<void*>(
  678                     AllocateParamBuffer<int>(paramBuffers, param.cast<int>()));
  679                 break;

Lines 680-699

  680             }
  681             case SQL_C_UTINYINT:
  682             case SQL_C_USHORT: {
  683                 if (!py::isinstance<py::int_>(param)) {
! 684                     ThrowStdException(MakeParamMismatchErrorStr(
! 685                         paramInfo.paramCType, paramIndex));
  686                 }
  687                 unsigned int value = param.cast<unsigned int>();
! 688                 if (value > std::numeric_limits<uint16_t>::max()) {
! 689                     ThrowStdException(
! 690                         "Unsigned short integer parameter out of range at "
! 691                         "paramIndex " +
! 692                         std::to_string(paramIndex));
  693                 }
! 694                 dataPtr = static_cast<void*>(AllocateParamBuffer<unsigned int>(
! 695                     paramBuffers, param.cast<unsigned int>()));
  696                 break;
  697             }
  698             case SQL_C_SBIGINT:
  699             case SQL_C_SLONG:

Lines 698-707

  698             case SQL_C_SBIGINT:
  699             case SQL_C_SLONG:
  700             case SQL_C_LONG: {
  701                 if (!py::isinstance<py::int_>(param)) {
! 702                     ThrowStdException(MakeParamMismatchErrorStr(
! 703                         paramInfo.paramCType, paramIndex));
  704                 }
  705                 int64_t value = param.cast<int64_t>();
  706                 // Range validation for signed 64-bit integer
  707                 if (value < std::numeric_limits<int64_t>::min() ||

Lines 705-716

  705                 int64_t value = param.cast<int64_t>();
  706                 // Range validation for signed 64-bit integer
  707                 if (value < std::numeric_limits<int64_t>::min() ||
  708                     value > std::numeric_limits<int64_t>::max()) {
! 709                     ThrowStdException(
! 710                         "Signed 64-bit integer parameter out of range at "
! 711                         "paramIndex " +
! 712                         std::to_string(paramIndex));
  713                 }
  714                 dataPtr = static_cast<void*>(AllocateParamBuffer<int64_t>(
  715                     paramBuffers, param.cast<int64_t>()));
  716                 break;

Lines 717-752

  717             }
  718             case SQL_C_UBIGINT:
  719             case SQL_C_ULONG: {
  720                 if (!py::isinstance<py::int_>(param)) {
! 721                     ThrowStdException(MakeParamMismatchErrorStr(
! 722                         paramInfo.paramCType, paramIndex));
  723                 }
  724                 uint64_t value = param.cast<uint64_t>();
  725                 // Range validation for unsigned 64-bit integer
  726                 if (value > std::numeric_limits<uint64_t>::max()) {
! 727                     ThrowStdException(
! 728                         "Unsigned 64-bit integer parameter out of range at "
! 729                         "paramIndex " +
! 730                         std::to_string(paramIndex));
  731                 }
! 732                 dataPtr = static_cast<void*>(AllocateParamBuffer<uint64_t>(
! 733                     paramBuffers, param.cast<uint64_t>()));
  734                 break;
  735             }
  736             case SQL_C_FLOAT: {
  737                 if (!py::isinstance<py::float_>(param)) {
! 738                     ThrowStdException(MakeParamMismatchErrorStr(
! 739                         paramInfo.paramCType, paramIndex));
  740                 }
! 741                 dataPtr = static_cast<void*>(AllocateParamBuffer<float>(
! 742                     paramBuffers, param.cast<float>()));
  743                 break;
  744             }
  745             case SQL_C_DOUBLE: {
  746                 if (!py::isinstance<py::float_>(param)) {
! 747                     ThrowStdException(MakeParamMismatchErrorStr(
! 748                         paramInfo.paramCType, paramIndex));
  749                 }
  750                 dataPtr = static_cast<void*>(AllocateParamBuffer<double>(
  751                     paramBuffers, param.cast<double>()));
  752                 break;

Lines 754-770

  754             case SQL_C_TYPE_DATE: {
  755                 py::object dateType =
  756                     py::module_::import("datetime").attr("date");
  757                 if (!py::isinstance(param, dateType)) {
! 758                     ThrowStdException(MakeParamMismatchErrorStr(
! 759                         paramInfo.paramCType, paramIndex));
  760                 }
  761                 int year = param.attr("year").cast<int>();
  762                 if (year < 1753 || year > 9999) {
! 763                     ThrowStdException(
! 764                         "Date out of range for SQL Server (1753-9999) at "
! 765                         "paramIndex " +
! 766                         std::to_string(paramIndex));
  767                 }
  768                 // TODO: can be moved to python by registering SQL_DATE_STRUCT
  769                 // in pybind
  770                 SQL_DATE_STRUCT* sqlDatePtr =

Lines 781-790

  781             case SQL_C_TYPE_TIME: {
  782                 py::object timeType =
  783                     py::module_::import("datetime").attr("time");
  784                 if (!py::isinstance(param, timeType)) {
! 785                     ThrowStdException(MakeParamMismatchErrorStr(
! 786                         paramInfo.paramCType, paramIndex));
  787                 }
  788                 // TODO: can be moved to python by registering SQL_TIME_STRUCT
  789                 // in pybind
  790                 SQL_TIME_STRUCT* sqlTimePtr =

Lines 801-818

  801             case SQL_C_SS_TIMESTAMPOFFSET: {
  802                 py::object datetimeType =
  803                     py::module_::import("datetime").attr("datetime");
  804                 if (!py::isinstance(param, datetimeType)) {
! 805                     ThrowStdException(MakeParamMismatchErrorStr(
! 806                         paramInfo.paramCType, paramIndex));
  807                 }
  808                 // Checking if the object has a timezone
  809                 py::object tzinfo = param.attr("tzinfo");
  810                 if (tzinfo.is_none()) {
! 811                     ThrowStdException(
! 812                         "Datetime object must have tzinfo for "
! 813                         "SQL_C_SS_TIMESTAMPOFFSET at paramIndex " +
! 814                         std::to_string(paramIndex));
  815                 }
  816 
  817                 DateTimeOffset* dtoPtr =
  818                     AllocateParamBuffer<DateTimeOffset>(paramBuffers);

Lines 834-845

  834                     param.attr("microsecond").cast<int>() * 1000);
  835 
  836                 py::object utcoffset = tzinfo.attr("utcoffset")(param);
  837                 if (utcoffset.is_none()) {
! 838                     ThrowStdException(
! 839                         "Datetime object's tzinfo.utcoffset() returned None at "
! 840                         "paramIndex " +
! 841                         std::to_string(paramIndex));
  842                 }
  843 
  844                 int total_seconds = static_cast<int>(
  845                     utcoffset.attr("total_seconds")().cast<double>());

Lines 867-876

  867             case SQL_C_TYPE_TIMESTAMP: {
  868                 py::object datetimeType =
  869                     py::module_::import("datetime").attr("datetime");
  870                 if (!py::isinstance(param, datetimeType)) {
! 871                     ThrowStdException(MakeParamMismatchErrorStr(
! 872                         paramInfo.paramCType, paramIndex));
  873                 }
  874                 SQL_TIMESTAMP_STRUCT* sqlTimestampPtr =
  875                     AllocateParamBuffer<SQL_TIMESTAMP_STRUCT>(paramBuffers);
  876                 sqlTimestampPtr->year =

Lines 893-902

  893                 break;
  894             }
  895             case SQL_C_NUMERIC: {
  896                 if (!py::isinstance<NumericData>(param)) {
! 897                     ThrowStdException(MakeParamMismatchErrorStr(
! 898                         paramInfo.paramCType, paramIndex));
  899                 }
  900                 NumericData decimalParam = param.cast<NumericData>();
  901                 LOG("Received numeric parameter: precision - {}, scale- {}, "
  902                     "sign - {}, value - {}",

Lines 921-930

  921                 break;
  922             }
  923             case SQL_C_GUID: {
  924                 if (!py::isinstance<py::bytes>(param)) {
! 925                     ThrowStdException(MakeParamMismatchErrorStr(
! 926                         paramInfo.paramCType, paramIndex));
  927                 }
  928                 py::bytes uuid_bytes = param.cast<py::bytes>();
  929                 const unsigned char* uuid_data =
  930                     reinterpret_cast<const unsigned char*>(

Lines 929-942

  929                 const unsigned char* uuid_data =
  930                     reinterpret_cast<const unsigned char*>(
  931                         PyBytes_AS_STRING(uuid_bytes.ptr()));
  932                 if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
! 933                     LOG("Invalid UUID parameter at index {}: expected 16 "
! 934                         "bytes, got {} bytes, type {}",
! 935                         paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()),
! 936                         paramInfo.paramCType);
! 937                     ThrowStdException(
! 938                         "UUID binary data must be exactly 16 bytes long.");
  939                 }
  940                 SQLGUID* guid_data_ptr =
  941                     AllocateParamBuffer<SQLGUID>(paramBuffers);
  942                 guid_data_ptr->Data1 =

Lines 959-969

  959                 break;
  960             }
  961             default: {
  962                 std::ostringstream errorString;
! 963                 errorString << "Unsupported parameter type - "
! 964                             << paramInfo.paramCType << " for parameter - "
! 965                             << paramIndex;
  966                 ThrowStdException(errorString.str());
  967             }
  968         }
  969         assert(SQLBindParameter_ptr && SQLGetStmtAttr_ptr &&

Lines 992-1001

   992             }
   993             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE,
   994                                      (SQLPOINTER)SQL_C_NUMERIC, 0);
   995             if (!SQL_SUCCEEDED(rc)) {
!  996                 LOG("Error when setting descriptor field SQL_DESC_TYPE - {}",
!  997                     paramIndex);
   998                 return rc;
   999             }
  1000             SQL_NUMERIC_STRUCT* numericPtr =
  1001                 reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);

Lines 1001-1011

  1001                 reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
  1002             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,
  1003                                      (SQLPOINTER)numericPtr->precision, 0);
  1004             if (!SQL_SUCCEEDED(rc)) {
! 1005                 LOG("Error when setting descriptor field SQL_DESC_PRECISION - "
! 1006                     "{}",
! 1007                     paramIndex);
  1008                 return rc;
  1009             }
  1010 
  1011             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,

Lines 1010-1019

  1010 
  1011             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE,
  1012                                      (SQLPOINTER)numericPtr->scale, 0);
  1013             if (!SQL_SUCCEEDED(rc)) {
! 1014                 LOG("Error when setting descriptor field SQL_DESC_SCALE - {}",
! 1015                     paramIndex);
  1016                 return rc;
  1017             }
  1018 
  1019             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR,

Lines 1018-1028

  1018 
  1019             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR,
  1020                                      (SQLPOINTER)numericPtr, 0);
  1021             if (!SQL_SUCCEEDED(rc)) {
! 1022                 LOG("Error when setting descriptor field SQL_DESC_DATA_PTR - "
! 1023                     "{}",
! 1024                     paramIndex);
  1025                 return rc;
  1026             }
  1027         }
  1028     }

Lines 1059-1069

  1059             // Check if the attribute exists before accessing it (for Python
  1060             // version compatibility)
  1061             if (py::hasattr(sys_module, "_is_finalizing")) {
  1062                 py::object finalizing_func = sys_module.attr("_is_finalizing");
! 1063                 if (!finalizing_func.is_none() &&
! 1064                     finalizing_func().cast<bool>()) {
! 1065                     return true;  // Python is finalizing
  1066                 }
  1067             }
  1068         }
  1069         return false;

Lines 1067-1076

  1067             }
  1068         }
  1069         return false;
  1070     } catch (...) {
! 1071         std::cerr << "Error occurred while checking Python finalization state."
! 1072                   << std::endl;
  1073         // Be conservative - don't assume shutdown on any exception
  1074         // Only return true if we're absolutely certain Python is shutting down
  1075         return false;
  1076     }

Lines 1091-1105

  1091                                 .attr("get_logger")();
  1092         if (py::isinstance<py::none>(logger)) return;
  1093 
  1094         try {
! 1095             std::string ddbcFormatString =
! 1096                 "[DDBC Bindings log] " + formatString;
  1097             if constexpr (sizeof...(args) == 0) {
  1098                 logger.attr("debug")(py::str(ddbcFormatString));
  1099             } else {
! 1100                 py::str message = py::str(ddbcFormatString)
! 1101                                       .format(std::forward<Args>(args)...);
  1102                 logger.attr("debug")(message);
  1103             }
  1104         } catch (const std::exception& e) {
  1105             std::cerr << "Logging error: " << e.what() << std::endl;

Lines 1105-1117

  1105             std::cerr << "Logging error: " << e.what() << std::endl;
  1106         }
  1107     } catch (const py::error_already_set& e) {
  1108         // Python is shutting down or in an inconsistent state, silently ignore
! 1109         (void)e;  // Suppress unused variable warning
  1110         return;
  1111     } catch (const std::exception& e) {
  1112         // Any other error, ignore to prevent crash during cleanup
! 1113         (void)e;  // Suppress unused variable warning
  1114         return;
  1115     }
  1116 }

Lines 1230-1244

  1230 
  1231 // Detect platform and set path
  1232 #ifdef __linux__
  1233     if (fs::exists("/etc/alpine-release")) {
! 1234         platform = "alpine";
  1235     } else if (fs::exists("/etc/redhat-release") ||
  1236                fs::exists("/etc/centos-release")) {
! 1237         platform = "rhel";
  1238     } else if (fs::exists("/etc/SuSE-release") ||
  1239                fs::exists("/etc/SUSE-brand")) {
! 1240         platform = "suse";
  1241     } else {
  1242         platform =
  1243             "debian_ubuntu";  // Default to debian_ubuntu for other distros
  1244     }

Lines 1320-1331

  1320 
  1321     DriverHandle handle = LoadDriverLibrary(driverPath.string());
  1322     if (!handle) {
  1323         LOG("Failed to load driver: {}", GetLastErrorMessage());
! 1324         ThrowStdException(
! 1325             "Failed to load the driver. Please read the documentation "
! 1326             "(https://github.com/microsoft/mssql-python#installation) to "
! 1327             "install the required dependencies.");
  1328     }
  1329     LOG("Driver library successfully loaded.");
  1330 
  1331     // Load function pointers using helper

Lines 1417-1426

  1417         SQLForeignKeys_ptr && SQLPrimaryKeys_ptr && SQLSpecialColumns_ptr &&
  1418         SQLStatistics_ptr && SQLColumns_ptr;
  1419 
  1420     if (!success) {
! 1421         ThrowStdException(
! 1422             "Failed to load required function pointers from driver.");
  1423     }
  1424     LOG("All driver function pointers successfully loaded.");
  1425     return handle;
  1426 }

Lines 1785-1793

  1785     LOG("Checking errors for retcode - {}", retcode);
  1786     ErrorInfo errorInfo;
  1787     if (retcode == SQL_INVALID_HANDLE) {
  1788         LOG("Invalid handle received");
! 1789         errorInfo.ddbcErrorMsg = std::wstring(L"Invalid handle!");
  1790         return errorInfo;
  1791     }
  1792     assert(handle != 0);
  1793     SQLHANDLE rawHandle = handle->get();

Lines 1888-1896

  1888 }
  1889 
  1890 // Wrap SQLExecDirect
  1891 SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle,
! 1892                              const std::wstring& Query) {
  1893     LOG("Execute SQL query directly - {}", Query.c_str());
  1894     if (!SQLExecDirect_ptr) {
  1895         LOG("Function pointer not initialized. Loading the driver.");
  1896         DriverLoader::getInstance().loadDriver();  // Load the driver

Lines 1897-1908

  1897     }
  1898 
  1899     // Ensure statement is scrollable BEFORE executing
  1900     if (SQLSetStmtAttr_ptr && StatementHandle && StatementHandle->get()) {
! 1901         SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CURSOR_TYPE,
! 1902                            (SQLPOINTER)SQL_CURSOR_STATIC, 0);
! 1903         SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CONCURRENCY,
! 1904                            (SQLPOINTER)SQL_CONCUR_READ_ONLY, 0);
  1905     }
  1906 
  1907     SQLWCHAR* queryPtr;
  1908 #if defined(__APPLE__) || defined(__linux__)

Lines 1910-1919

  1910     queryPtr = queryBuffer.data();
  1911 #else
  1912     queryPtr = const_cast<SQLWCHAR*>(Query.c_str());
  1913 #endif
! 1914     SQLRETURN ret =
! 1915         SQLExecDirect_ptr(StatementHandle->get(), queryPtr, SQL_NTS);
  1916     if (!SQL_SUCCEEDED(ret)) {
  1917         LOG("Failed to execute query directly");
  1918     }
  1919     return ret;

Lines 2101-2110

  2101                         break;
  2102                     }
  2103                 }
  2104                 if (!matchedInfo) {
! 2105                     ThrowStdException(
! 2106                         "Unrecognized paramToken returned by SQLParamData");
  2107                 }
  2108                 const py::object& pyObj = matchedInfo->dataPtr;
  2109                 if (pyObj.is_none()) {
  2110                     SQLPutData_ptr(hStmt, nullptr, 0);

Lines 2131-2141

  2131                             size_t lenBytes = len * sizeof(SQLWCHAR);
  2132                             if (lenBytes >
  2133                                 static_cast<size_t>(
  2134                                     std::numeric_limits<SQLLEN>::max())) {
! 2135                                 ThrowStdException(
! 2136                                     "Chunk size exceeds maximum allowed by "
! 2137                                     "SQLLEN");
  2138                             }
  2139                             rc = SQLPutData_ptr(hStmt,
  2140                                                 (SQLPOINTER)(dataPtr + offset),
  2141                                                 static_cast<SQLLEN>(lenBytes));

Lines 2139-2148

  2139                             rc = SQLPutData_ptr(hStmt,
  2140                                                 (SQLPOINTER)(dataPtr + offset),
  2141                                                 static_cast<SQLLEN>(lenBytes));
  2142                             if (!SQL_SUCCEEDED(rc)) {
! 2143                                 LOG("SQLPutData failed at offset {} of {}",
! 2144                                     offset, totalChars);
  2145                                 return rc;
  2146                             }
  2147                             offset += len;
  2148                         }

Lines 2152-2168

  2152                         const char* dataPtr = s.data();
  2153                         size_t offset = 0;
  2154                         size_t chunkBytes = DAE_CHUNK_SIZE;
  2155                         while (offset < totalBytes) {
! 2156                             size_t len =
! 2157                                 std::min(chunkBytes, totalBytes - offset);
  2158 
! 2159                             rc = SQLPutData_ptr(hStmt,
! 2160                                                 (SQLPOINTER)(dataPtr + offset),
! 2161                                                 static_cast<SQLLEN>(len));
  2162                             if (!SQL_SUCCEEDED(rc)) {
! 2163                                 LOG("SQLPutData failed at offset {} of {}",
! 2164                                     offset, totalBytes);
  2165                                 return rc;
  2166                             }
  2167                             offset += len;
  2168                         }

Lines 2182-2191

  2182                         rc = SQLPutData_ptr(hStmt,
  2183                                             (SQLPOINTER)(dataPtr + offset),
  2184                                             static_cast<SQLLEN>(len));
  2185                         if (!SQL_SUCCEEDED(rc)) {
! 2186                             LOG("SQLPutData failed at offset {} of {}", offset,
! 2187                                 totalBytes);
  2188                             return rc;
  2189                         }
  2190                     }
  2191                 } else {

Lines 2228-2237

  2228             const py::list& columnValues =
  2229                 columnwise_params[paramIndex].cast<py::list>();
  2230             const ParamInfo& info = paramInfos[paramIndex];
  2231             if (columnValues.size() != paramSetSize) {
! 2232                 ThrowStdException("Column " + std::to_string(paramIndex) +
! 2233                                   " has mismatched size.");
  2234             }
  2235             void* dataPtr = nullptr;
  2236             SQLLEN* strLenOrIndArray = nullptr;
  2237             SQLLEN bufferLength = 0;

Lines 2241-2251

  2241                         tempBuffers, paramSetSize);
  2242                     for (size_t i = 0; i < paramSetSize; ++i) {
  2243                         if (columnValues[i].is_none()) {
  2244                             if (!strLenOrIndArray)
! 2245                                 strLenOrIndArray =
! 2246                                     AllocateParamBufferArray<SQLLEN>(
! 2247                                         tempBuffers, paramSetSize);
  2248                             dataArray[i] = 0;
  2249                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2250                         } else {
  2251                             dataArray[i] = columnValues[i].cast<int>();

Lines 2255-2270

  2255                     dataPtr = dataArray;
  2256                     break;
  2257                 }
  2258                 case SQL_C_DOUBLE: {
! 2259                     double* dataArray = AllocateParamBufferArray<double>(
! 2260                         tempBuffers, paramSetSize);
  2261                     for (size_t i = 0; i < paramSetSize; ++i) {
  2262                         if (columnValues[i].is_none()) {
  2263                             if (!strLenOrIndArray)
! 2264                                 strLenOrIndArray =
! 2265                                     AllocateParamBufferArray<SQLLEN>(
! 2266                                         tempBuffers, paramSetSize);
  2267                             dataArray[i] = 0;
  2268                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2269                         } else {
  2270                             dataArray[i] = columnValues[i].cast<double>();

Lines 2296-2312

  2296                             // against column size
  2297                             if (utf16Buf.size() > 0 &&
  2298                                 (utf16Buf.size() - 1) > info.columnSize) {
  2299                                 std::string offending = WideToUTF8(wstr);
! 2300                                 ThrowStdException(
! 2301                                     "Input string UTF-16 length exceeds "
! 2302                                     "allowed column size at parameter index " +
! 2303                                     std::to_string(paramIndex) +
! 2304                                     ". UTF-16 length: " +
! 2305                                     std::to_string(utf16Buf.size() - 1) +
! 2306                                     ", Column size: " +
! 2307                                     std::to_string(info.columnSize));
! 2308                             }
  2309                             // Secure copy: use validated bounds for
  2310                             // defense-in-depth
  2311                             size_t copyBytes =
  2312                                 utf16Buf.size() * sizeof(SQLWCHAR);

Lines 2315-2329

  2315                             SQLWCHAR* destPtr =
  2316                                 wcharArray + i * (info.columnSize + 1);
  2317 
  2318                             if (copyBytes > bufferBytes) {
! 2319                                 ThrowStdException(
! 2320                                     "Buffer overflow prevented in WCHAR array "
! 2321                                     "binding at parameter "
! 2322                                     "index " +
! 2323                                     std::to_string(paramIndex) +
! 2324                                     ", array element " + std::to_string(i));
! 2325                             }
  2326                             if (copyBytes > 0) {
  2327                                 std::copy_n(reinterpret_cast<const char*>(
  2328                                                 utf16Buf.data()),
  2329                                             copyBytes,

Lines 2381-2391

  2381                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2382                         } else {
  2383                             int intVal = columnValues[i].cast<int>();
  2384                             if (intVal < 0 || intVal > 255) {
! 2385                                 ThrowStdException(
! 2386                                     "UTINYINT value out of range at rowIndex " +
! 2387                                     std::to_string(i));
  2388                             }
  2389                             dataArray[i] = static_cast<unsigned char>(intVal);
  2390                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;
  2391                         }

Lines 2399-2409

  2399                         tempBuffers, paramSetSize);
  2400                     for (size_t i = 0; i < paramSetSize; ++i) {
  2401                         if (columnValues[i].is_none()) {
  2402                             if (!strLenOrIndArray)
! 2403                                 strLenOrIndArray =
! 2404                                     AllocateParamBufferArray<SQLLEN>(
! 2405                                         tempBuffers, paramSetSize);
  2406                             dataArray[i] = 0;
  2407                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2408                         } else {
  2409                             int intVal = columnValues[i].cast<int>();

Lines 2408-2418

  2408                         } else {
  2409                             int intVal = columnValues[i].cast<int>();
  2410                             if (intVal < std::numeric_limits<int16_t>::min() ||
  2411                                 intVal > std::numeric_limits<int16_t>::max()) {
! 2412                                 ThrowStdException(
! 2413                                     "SHORT value out of range at rowIndex " +
! 2414                                     std::to_string(i));
  2415                             }
  2416                             dataArray[i] = static_cast<int16_t>(intVal);
  2417                             if (strLenOrIndArray) strLenOrIndArray[i] = 0;
  2418                         }

Lines 2441-2475

  2441                                 encoding_settings &&
  2442                                 !encoding_settings.is_none() &&
  2443                                 encoding_settings.contains("ctype") &&
  2444                                 encoding_settings.contains("encoding")) {
! 2445                                 SQLSMALLINT ctype = encoding_settings["ctype"]
! 2446                                                         .cast<SQLSMALLINT>();
! 2447                                 if (ctype == SQL_C_CHAR) {
! 2448                                     try {
! 2449                                         py::dict settings_dict =
! 2450                                             encoding_settings.cast<py::dict>();
! 2451                                         auto [encoding, errors] =
! 2452                                             extract_encoding_settings(
! 2453                                                 settings_dict);
  2454                                         // Use our safe encoding function
! 2455                                         py::bytes encoded_bytes =
! 2456                                             EncodingString(
! 2457                                                 columnValues[i]
! 2458                                                     .cast<std::string>(),
! 2459                                                 encoding, errors);
! 2460                                         str = encoded_bytes.cast<std::string>();
! 2461                                     } catch (const std::exception& e) {
! 2462                                         ThrowStdException(
! 2463                                             "Failed to encode "
! 2464                                             "parameter array element " +
! 2465                                             std::to_string(i) + ": " +
! 2466                                             e.what());
! 2467                                     }
! 2468                                 } else {
  2469                                     // Default behavior
! 2470                                     str = columnValues[i].cast<std::string>();
! 2471                                 }
  2472                             } else {
  2473                                 // No encoding settings or SQL_C_BINARY - use
  2474                                 // default behavior
  2475                                 str = columnValues[i].cast<std::string>();

Lines 2474-2485

  2474                                 // default behavior
  2475                                 str = columnValues[i].cast<std::string>();
  2476                             }
  2477                             if (str.size() > info.columnSize) {
! 2478                                 ThrowStdException(
! 2479                                     "Input exceeds column size at index " +
! 2480                                     std::to_string(i));
! 2481                             }
  2482 
  2483                             // SECURITY: Use secure copy with bounds checking
  2484                             size_t destOffset = i * (info.columnSize + 1);
  2485                             size_t destBufferSize = info.columnSize + 1;

Lines 2486-2498

  2486                             size_t copyLength = str.size();
  2487 
  2488                             // Validate bounds to prevent buffer overflow
  2489                             if (copyLength >= destBufferSize) {
! 2490                                 ThrowStdException(
! 2491                                     "Buffer overflow prevented at parameter "
! 2492                                     "array index " +
! 2493                                     std::to_string(i));
! 2494                             }
  2495 
  2496 #ifdef _WIN32
  2497                             // Windows: Use memcpy_s for secure copy
  2498                             errno_t err =

Lines 2522-2533

  2522                     bufferLength = info.columnSize + 1;
  2523                     break;
  2524                 }
  2525                 case SQL_C_BIT: {
! 2526                     char* boolArray = AllocateParamBufferArray<char>(
! 2527                         tempBuffers, paramSetSize);
! 2528                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2529                         tempBuffers, paramSetSize);
  2530                     for (size_t i = 0; i < paramSetSize; ++i) {
  2531                         if (columnValues[i].is_none()) {
  2532                             boolArray[i] = 0;
  2533                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2541-2552

  2541                     break;
  2542                 }
  2543                 case SQL_C_STINYINT:
  2544                 case SQL_C_USHORT: {
! 2545                     uint16_t* dataArray = AllocateParamBufferArray<uint16_t>(
! 2546                         tempBuffers, paramSetSize);
! 2547                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2548                         tempBuffers, paramSetSize);
  2549                     for (size_t i = 0; i < paramSetSize; ++i) {
  2550                         if (columnValues[i].is_none()) {
  2551                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2552                             dataArray[i] = 0;

Lines 2550-2563

  2550                         if (columnValues[i].is_none()) {
  2551                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2552                             dataArray[i] = 0;
  2553                         } else {
! 2554                             dataArray[i] = columnValues[i].cast<uint16_t>();
  2555                             strLenOrIndArray[i] = 0;
  2556                         }
  2557                     }
  2558                     dataPtr = dataArray;
! 2559                     bufferLength = sizeof(uint16_t);
  2560                     break;
  2561                 }
  2562                 case SQL_C_SBIGINT:
  2563                 case SQL_C_SLONG:

Lines 2598-2623

  2598                     bufferLength = sizeof(float);
  2599                     break;
  2600                 }
  2601                 case SQL_C_TYPE_DATE: {
! 2602                     SQL_DATE_STRUCT* dateArray =
! 2603                         AllocateParamBufferArray<SQL_DATE_STRUCT>(tempBuffers,
! 2604                                                                   paramSetSize);
! 2605                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2606                         tempBuffers, paramSetSize);
  2607                     for (size_t i = 0; i < paramSetSize; ++i) {
  2608                         if (columnValues[i].is_none()) {
  2609                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 2610                             std::memset(&dateArray[i], 0,
! 2611                                         sizeof(SQL_DATE_STRUCT));
  2612                         } else {
  2613                             py::object dateObj = columnValues[i];
! 2614                             dateArray[i].year =
! 2615                                 dateObj.attr("year").cast<SQLSMALLINT>();
! 2616                             dateArray[i].month =
! 2617                                 dateObj.attr("month").cast<SQLUSMALLINT>();
! 2618                             dateArray[i].day =
! 2619                                 dateObj.attr("day").cast<SQLUSMALLINT>();
  2620                             strLenOrIndArray[i] = 0;
  2621                         }
  2622                     }
  2623                     dataPtr = dateArray;

Lines 2624-2649

  2624                     bufferLength = sizeof(SQL_DATE_STRUCT);
  2625                     break;
  2626                 }
  2627                 case SQL_C_TYPE_TIME: {
! 2628                     SQL_TIME_STRUCT* timeArray =
! 2629                         AllocateParamBufferArray<SQL_TIME_STRUCT>(tempBuffers,
! 2630                                                                   paramSetSize);
! 2631                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2632                         tempBuffers, paramSetSize);
  2633                     for (size_t i = 0; i < paramSetSize; ++i) {
  2634                         if (columnValues[i].is_none()) {
  2635                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 2636                             std::memset(&timeArray[i], 0,
! 2637                                         sizeof(SQL_TIME_STRUCT));
  2638                         } else {
  2639                             py::object timeObj = columnValues[i];
! 2640                             timeArray[i].hour =
! 2641                                 timeObj.attr("hour").cast<SQLUSMALLINT>();
! 2642                             timeArray[i].minute =
! 2643                                 timeObj.attr("minute").cast<SQLUSMALLINT>();
! 2644                             timeArray[i].second =
! 2645                                 timeObj.attr("second").cast<SQLUSMALLINT>();
  2646                             strLenOrIndArray[i] = 0;
  2647                         }
  2648                     }
  2649                     dataPtr = timeArray;

Lines 2650-2684

  2650                     bufferLength = sizeof(SQL_TIME_STRUCT);
  2651                     break;
  2652                 }
  2653                 case SQL_C_TYPE_TIMESTAMP: {
! 2654                     SQL_TIMESTAMP_STRUCT* tsArray =
! 2655                         AllocateParamBufferArray<SQL_TIMESTAMP_STRUCT>(
! 2656                             tempBuffers, paramSetSize);
! 2657                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2658                         tempBuffers, paramSetSize);
  2659                     for (size_t i = 0; i < paramSetSize; ++i) {
  2660                         if (columnValues[i].is_none()) {
  2661                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 2662                             std::memset(&tsArray[i], 0,
! 2663                                         sizeof(SQL_TIMESTAMP_STRUCT));
  2664                         } else {
  2665                             py::object dtObj = columnValues[i];
! 2666                             tsArray[i].year =
! 2667                                 dtObj.attr("year").cast<SQLSMALLINT>();
! 2668                             tsArray[i].month =
! 2669                                 dtObj.attr("month").cast<SQLUSMALLINT>();
! 2670                             tsArray[i].day =
! 2671                                 dtObj.attr("day").cast<SQLUSMALLINT>();
! 2672                             tsArray[i].hour =
! 2673                                 dtObj.attr("hour").cast<SQLUSMALLINT>();
! 2674                             tsArray[i].minute =
! 2675                                 dtObj.attr("minute").cast<SQLUSMALLINT>();
! 2676                             tsArray[i].second =
! 2677                                 dtObj.attr("second").cast<SQLUSMALLINT>();
! 2678                             tsArray[i].fraction = static_cast<SQLUINTEGER>(
! 2679                                 dtObj.attr("microsecond").cast<int>() *
! 2680                                 1000);  // µs to ns
  2681                             strLenOrIndArray[i] = 0;
  2682                         }
  2683                     }
  2684                     dataPtr = tsArray;

Lines 2698-2720

  2698                     for (size_t i = 0; i < paramSetSize; ++i) {
  2699                         const py::handle& param = columnValues[i];
  2700 
  2701                         if (param.is_none()) {
! 2702                             std::memset(&dtoArray[i], 0,
! 2703                                         sizeof(DateTimeOffset));
  2704                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2705                         } else {
  2706                             if (!py::isinstance(param, datetimeType)) {
! 2707                                 ThrowStdException(MakeParamMismatchErrorStr(
! 2708                                     info.paramCType, paramIndex));
  2709                             }
  2710 
  2711                             py::object tzinfo = param.attr("tzinfo");
  2712                             if (tzinfo.is_none()) {
! 2713                                 ThrowStdException(
! 2714                                     "Datetime object must have "
! 2715                                     "tzinfo for SQL_C_SS_TIMESTAMPOFFSET at "
! 2716                                     "paramIndex " +
  2717                                     std::to_string(paramIndex));
  2718                             }
  2719 
  2720                             // Populate the C++ struct directly from the Python

Lines 2757-2786

  2757                     bufferLength = sizeof(DateTimeOffset);
  2758                     break;
  2759                 }
  2760                 case SQL_C_NUMERIC: {
! 2761                     SQL_NUMERIC_STRUCT* numericArray =
! 2762                         AllocateParamBufferArray<SQL_NUMERIC_STRUCT>(
! 2763                             tempBuffers, paramSetSize);
! 2764                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(
! 2765                         tempBuffers, paramSetSize);
  2766                     for (size_t i = 0; i < paramSetSize; ++i) {
  2767                         const py::handle& element = columnValues[i];
  2768                         if (element.is_none()) {
  2769                             strLenOrIndArray[i] = SQL_NULL_DATA;
! 2770                             std::memset(&numericArray[i], 0,
! 2771                                         sizeof(SQL_NUMERIC_STRUCT));
  2772                             continue;
  2773                         }
  2774                         if (!py::isinstance<NumericData>(element)) {
! 2775                             throw std::runtime_error(MakeParamMismatchErrorStr(
! 2776                                 info.paramCType, paramIndex));
  2777                         }
  2778                         NumericData decimalParam = element.cast<NumericData>();
! 2779                         LOG("Received numeric parameter at [%zu]: "
! 2780                             "precision=%d, scale=%d, sign=%d, val=%s",
! 2781                             i, decimalParam.precision, decimalParam.scale,
! 2782                             decimalParam.sign, decimalParam.val.c_str());
  2783                         SQL_NUMERIC_STRUCT& target = numericArray[i];
  2784                         std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
  2785                         target.precision = decimalParam.precision;
  2786                         target.scale = decimalParam.scale;

Lines 2784-2797

  2784                         std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
  2785                         target.precision = decimalParam.precision;
  2786                         target.scale = decimalParam.scale;
  2787                         target.sign = decimalParam.sign;
! 2788                         size_t copyLen = std::min(decimalParam.val.size(),
! 2789                                                   sizeof(target.val));
  2790                         // Secure copy: bounds already validated with std::min
  2791                         if (copyLen > 0) {
! 2792                             std::copy_n(decimalParam.val.data(), copyLen,
! 2793                                         target.val);
  2794                         }
  2795                         strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT);
  2796                     }
  2797                     dataPtr = numericArray;

Lines 2819-2835

  2819                             continue;
  2820                         } else if (py::isinstance<py::bytes>(element)) {
  2821                             py::bytes b = element.cast<py::bytes>();
  2822                             if (PyBytes_GET_SIZE(b.ptr()) != 16) {
! 2823                                 ThrowStdException(
! 2824                                     "UUID binary data must be exactly "
! 2825                                     "16 bytes long.");
  2826                             }
  2827                             // Secure copy: Fixed 16-byte copy, size validated
  2828                             // above
! 2829                             std::copy_n(reinterpret_cast<const unsigned char*>(
! 2830                                             PyBytes_AS_STRING(b.ptr())),
! 2831                                         16, uuid_bytes.data());
  2832                         } else if (py::isinstance(element, uuid_class)) {
  2833                             py::bytes b =
  2834                                 element.attr("bytes_le").cast<py::bytes>();
  2835                             // Secure copy: Fixed 16-byte copy from UUID

Lines 2837-2846

  2837                             std::copy_n(reinterpret_cast<const unsigned char*>(
  2838                                             PyBytes_AS_STRING(b.ptr())),
  2839                                         16, uuid_bytes.data());
  2840                         } else {
! 2841                             ThrowStdException(MakeParamMismatchErrorStr(
! 2842                                 info.paramCType, paramIndex));
  2843                         }
  2844                         guidArray[i].Data1 =
  2845                             (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
  2846                             (static_cast<uint32_t>(uuid_bytes[2]) << 16) |

Lines 2861-2871

  2861                     bufferLength = sizeof(SQLGUID);
  2862                     break;
  2863                 }
  2864                 default: {
! 2865                     ThrowStdException(
! 2866                         "BindParameterArray: Unsupported C type: " +
! 2867                         std::to_string(info.paramCType));
  2868                 }
  2869             }
  2870             RETCODE rc = SQLBindParameter_ptr(
  2871                 hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1),

Lines 2928-2938

  2928         for (size_t rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
  2929             py::list rowParams = columnwise_params[rowIndex];
  2930 
  2931             std::vector<std::shared_ptr<void>> paramBuffers;
! 2932             rc = BindParameters(hStmt, rowParams,
! 2933                                 const_cast<std::vector<ParamInfo>&>(paramInfos),
! 2934                                 paramBuffers, encoding_settings);
  2935             if (!SQL_SUCCEEDED(rc)) return rc;
  2936 
  2937             rc = SQLExecute_ptr(hStmt);
  2938             while (rc == SQL_NEED_DATA) {

Lines 2945-2960

  2945 
  2946                 if (py::isinstance<py::str>(*py_obj_ptr)) {
  2947                     std::string data = py_obj_ptr->cast<std::string>();
  2948                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2949                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(),
! 2950                                         data_len);
! 2951                 } else if (py::isinstance<py::bytes>(*py_obj_ptr) ||
! 2952                            py::isinstance<py::bytearray>(*py_obj_ptr)) {
  2953                     std::string data = py_obj_ptr->cast<std::string>();
  2954                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2955                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(),
! 2956                                         data_len);
  2957                 } else {
  2958                     LOG("Unsupported DAE parameter type in row {}", rowIndex);
  2959                     return SQL_ERROR;
  2960                 }

Lines 3106-3114

  3106         if (ret == SQL_ERROR ||
  3107             (!SQL_SUCCEEDED(ret) && ret != SQL_SUCCESS_WITH_INFO)) {
  3108             std::ostringstream oss;
  3109             oss << "Error fetching LOB for column " << colIndex
! 3110                 << ", cType=" << cType << ", loop=" << loopCount
  3111                 << ", SQLGetData return=" << ret;
  3112             LOG(oss.str());
  3113             ThrowStdException(oss.str());
  3114         }

Lines 3204-3222

  3204                 "using encoding '{}'",
  3205                 char_encoding);
  3206             return decoded_str;
  3207         } catch (const std::exception& e) {
! 3208             LOG("FetchLobColumnData: Dynamic decoding failed: {}. "
! 3209                 "Using fallback.",
! 3210                 e.what());
  3211             // Fallback to original logic
! 3212         }
  3213     }
  3214 
  3215     // Fallback: original behavior for SQL_C_CHAR
  3216     std::string str(buffer.data(), buffer.size());
! 3217     LOG("FetchLobColumnData: Returning narrow string of length {}",
! 3218         str.length());
  3219     return py::str(str);
  3220 }
  3221 
  3222 // Helper function to retrieve column data

Lines 3246-3256

  3246         ret = SQLDescribeCol_ptr(
  3247             hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR),
  3248             &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable);
  3249         if (!SQL_SUCCEEDED(ret)) {
! 3250             LOG("Error retrieving data for column - {}, SQLDescribeCol "
! 3251                 "return code - {}",
! 3252                 i, ret);
  3253             row.append(py::none());
  3254             continue;
  3255         }

Lines 3287-3302

  3287                                     LOG("Applied dynamic decoding for CHAR "
  3288                                         "column {} using encoding '{}'",
  3289                                         i, char_encoding);
  3290                                 } catch (const std::exception& e) {
! 3291                                     LOG("Dynamic decoding failed for column "
! 3292                                         "{}: {}. Using fallback.",
! 3293                                         i, e.what());
  3294                                     // Fallback to platform-specific handling
! 3295 #if defined(__APPLE__) || defined(__linux__)
! 3296                                     std::string fullStr(reinterpret_cast<char*>(
! 3297                                         dataBuffer.data()));
! 3298                                     row.append(fullStr);
  3299 #else
  3300                                     row.append(
  3301                                         std::string(reinterpret_cast<char*>(
  3302                                             dataBuffer.data())));

Lines 3300-3316

  3300                                     row.append(
  3301                                         std::string(reinterpret_cast<char*>(
  3302                                             dataBuffer.data())));
  3303 #endif
! 3304                                 }
  3305                             } else {
  3306                                 // Buffer too small, fallback to streaming
! 3307                                 LOG("CHAR column {} data truncated, "
! 3308                                     "using streaming LOB",
! 3309                                     i);
! 3310                                 row.append(FetchLobColumnData(
! 3311                                     hStmt, i, SQL_C_CHAR, false, false,
! 3312                                     char_encoding));
  3313                             }
  3314                         } else if (dataLen == SQL_NULL_DATA) {
  3315                             LOG("Column {} is NULL (CHAR)", i);
  3316                             row.append(py::none());

Lines 3316-3345

  3316                             row.append(py::none());
  3317                         } else if (dataLen == 0) {
  3318                             row.append(py::str(""));
  3319                         } else if (dataLen == SQL_NO_TOTAL) {
! 3320                             LOG("SQLGetData couldn't determine the length of "
! 3321                                 "the "
! 3322                                 "data. Returning NULL value instead. Column ID "
! 3323                                 "- {}, "
! 3324                                 "Data Type - {}",
! 3325                                 i, dataType);
  3326                             row.append(py::none());
  3327                         } else if (dataLen < 0) {
! 3328                             LOG("SQLGetData returned an unexpected negative "
! 3329                                 "data "
! 3330                                 "length. Raising exception. Column ID - {}, "
! 3331                                 "Data Type - {}, Data Length - {}",
  3332                                 i, dataType, dataLen);
! 3333                             ThrowStdException(
! 3334                                 "SQLGetData returned an unexpected "
! 3335                                 "negative data length");
  3336                         }
  3337                     } else {
! 3338                         LOG("Error retrieving data for column - {}, data type "
! 3339                             "- "
! 3340                             "{}, SQLGetData return code - {}. Returning NULL "
! 3341                             "value instead",
  3342                             i, dataType, ret);
  3343                         row.append(py::none());
  3344                     }
  3345                 }

Lines 3401-3427

  3401                             row.append(py::none());
  3402                         } else if (dataLen == 0) {
  3403                             row.append(py::str(""));
  3404                         } else if (dataLen == SQL_NO_TOTAL) {
! 3405                             LOG("SQLGetData couldn't determine the length of "
! 3406                                 "the NVARCHAR data. Returning NULL. "
! 3407                                 "Column ID - {}",
! 3408                                 i);
  3409                             row.append(py::none());
  3410                         } else if (dataLen < 0) {
! 3411                             LOG("SQLGetData returned an unexpected negative "
! 3412                                 "data "
! 3413                                 "length. Raising exception. Column ID - {}, "
! 3414                                 "Data Type - {}, Data Length - {}",
  3415                                 i, dataType, dataLen);
! 3416                             ThrowStdException(
! 3417                                 "SQLGetData returned an unexpected "
! 3418                                 "negative data length");
  3419                         }
  3420                     } else {
! 3421                         LOG("Error retrieving data for column {} (NVARCHAR), "
! 3422                             "SQLGetData return code {}",
! 3423                             i, ret);
  3424                         row.append(py::none());
  3425                     }
  3426                 }
  3427                 break;

Lines 3442-3452

  3442                                      NULL);
  3443                 if (SQL_SUCCEEDED(ret)) {
  3444                     row.append(static_cast<int>(smallIntValue));
  3445                 } else {
! 3446                     LOG("Error retrieving data for column - {}, "
! 3447                         "data type - {}, SQLGetData return code - {}. "
! 3448                         "Returning NULL value instead",
  3449                         i, dataType, ret);
  3450                     row.append(py::none());
  3451                 }
  3452                 break;

Lines 3457-3467

  3457                     SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL);
  3458                 if (SQL_SUCCEEDED(ret)) {
  3459                     row.append(realValue);
  3460                 } else {
! 3461                     LOG("Error retrieving data for column - {}, "
! 3462                         "data type - {}, SQLGetData return code - {}. "
! 3463                         "Returning NULL value instead",
  3464                         i, dataType, ret);
  3465                     row.append(py::none());
  3466                 }
  3467                 break;

Lines 3500-3509

  3500                                 }
  3501                             }
  3502                             // if no null found, use the full buffer size as a
  3503                             // conservative fallback
! 3504                             if (safeLen == 0 && bufSize > 0 &&
! 3505                                 cnum[0] != '\0') {
  3506                                 safeLen = bufSize;
  3507                             }
  3508                         }

Lines 3523-3532

  3523                         LOG("Error converting to decimal: {}", e.what());
  3524                         row.append(py::none());
  3525                     }
  3526                 } else {
! 3527                     LOG("Error retrieving data for column - {}, data type - "
! 3528                         "{}, SQLGetData return "
  3529                         "code - {}. Returning NULL value instead",
  3530                         i, dataType, ret);
  3531                     row.append(py::none());
  3532                 }

Lines 3540-3549

  3540                                      NULL);
  3541                 if (SQL_SUCCEEDED(ret)) {
  3542                     row.append(doubleValue);
  3543                 } else {
! 3544                     LOG("Error retrieving data for column - {}, data type - "
! 3545                         "{}, SQLGetData return "
  3546                         "code - {}. Returning NULL value instead",
  3547                         i, dataType, ret);
  3548                     row.append(py::none());
  3549                 }

Lines 3555-3564

  3555                                      NULL);
  3556                 if (SQL_SUCCEEDED(ret)) {
  3557                     row.append(static_cast<int64_t>(bigintValue));
  3558                 } else {
! 3559                     LOG("Error retrieving data for column - {}, data type - "
! 3560                         "{}, SQLGetData return "
  3561                         "code - {}. Returning NULL value instead",
  3562                         i, dataType, ret);
  3563                     row.append(py::none());
  3564                 }

Lines 3573-3582

  3573                                    .attr("date")(dateValue.year,
  3574                                                  dateValue.month,
  3575                                                  dateValue.day));
  3576                 } else {
! 3577                     LOG("Error retrieving data for column - {}, data type - "
! 3578                         "{}, SQLGetData return "
  3579                         "code - {}. Returning NULL value instead",
  3580                         i, dataType, ret);
  3581                     row.append(py::none());
  3582                 }

Lines 3593-3602

  3593                                    .attr("time")(timeValue.hour,
  3594                                                  timeValue.minute,
  3595                                                  timeValue.second));
  3596                 } else {
! 3597                     LOG("Error retrieving data for column - {}, data type - "
! 3598                         "{}, SQLGetData return "
  3599                         "code - {}. Returning NULL value instead",
  3600                         i, dataType, ret);
  3601                     row.append(py::none());
  3602                 }

Lines 3618-3627

  3618                                 timestampValue.minute, timestampValue.second,
  3619                                 timestampValue.fraction /
  3620                                     1000));  // Convert back ns to µs
  3621                 } else {
! 3622                     LOG("Error retrieving data for column - {}, data type - "
! 3623                         "{}, SQLGetData return "
  3624                         "code - {}. Returning NULL value instead",
  3625                         i, dataType, ret);
  3626                     row.append(py::none());
  3627                 }

Lines 3644-3653

  3644                         dtoValue.timezone_hour * 60 + dtoValue.timezone_minute;
  3645                     // Validating offset
  3646                     if (totalMinutes < -24 * 60 || totalMinutes > 24 * 60) {
  3647                         std::ostringstream oss;
! 3648                         oss << "Invalid timezone offset from "
! 3649                                "SQL_SS_TIMESTAMPOFFSET_STRUCT: "
  3650                             << totalMinutes << " minutes for column " << i;
  3651                         ThrowStdException(oss.str());
  3652                     }
  3653                     // Convert fraction from ns to µs

Lines 3660-3669

  3660                         dtoValue.hour, dtoValue.minute, dtoValue.second,
  3661                         microseconds, tzinfo);
  3662                     row.append(py_dt);
  3663                 } else {
! 3664                     LOG("Error fetching DATETIMEOFFSET for column {}, ret={}",
! 3665                         i, ret);
  3666                     row.append(py::none());
  3667                 }
  3668                 break;
  3669             }

Lines 3692-3704

  3692                                     py::bytes(reinterpret_cast<const char*>(
  3693                                                   dataBuffer.data()),
  3694                                               dataLen));
  3695                             } else {
! 3696                                 LOG("VARBINARY column {} data truncated, "
! 3697                                     "using streaming LOB",
! 3698                                     i);
! 3699                                 row.append(FetchLobColumnData(
! 3700                                     hStmt, i, SQL_C_BINARY, false, true));
  3701                             }
  3702                         } else if (dataLen == SQL_NULL_DATA) {
  3703                             row.append(py::none());
  3704                         } else if (dataLen == 0) {

Lines 3704-3722

  3704                         } else if (dataLen == 0) {
  3705                             row.append(py::bytes(""));
  3706                         } else {
  3707                             std::ostringstream oss;
! 3708                             oss << "Unexpected negative length (" << dataLen
! 3709                                 << ") returned by SQLGetData. ColumnID=" << i
! 3710                                 << ", dataType=" << dataType
! 3711                                 << ", bufferSize=" << columnSize;
  3712                             LOG("Error: {}", oss.str());
  3713                             ThrowStdException(oss.str());
  3714                         }
  3715                     } else {
! 3716                         LOG("Error retrieving VARBINARY data for column {}. "
! 3717                             "SQLGetData rc = {}",
! 3718                             i, ret);
  3719                         row.append(py::none());
  3720                     }
  3721                 }
  3722                 break;

Lines 3727-3737

  3727                                      NULL);
  3728                 if (SQL_SUCCEEDED(ret)) {
  3729                     row.append(static_cast<int>(tinyIntValue));
  3730                 } else {
! 3731                     LOG("Error retrieving data for column - {}, data type - "
! 3732                         "{}, SQLGetData return code - {}. Returning NULL "
! 3733                         "value instead",
  3734                         i, dataType, ret);
  3735                     row.append(py::none());
  3736                 }
  3737                 break;

Lines 3741-3751

  3741                 ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL);
  3742                 if (SQL_SUCCEEDED(ret)) {
  3743                     row.append(static_cast<bool>(bitValue));
  3744                 } else {
! 3745                     LOG("Error retrieving data for column - {}, data type - "
! 3746                         "{}, SQLGetData return code - {}. Returning NULL "
! 3747                         "value instead",
  3748                         i, dataType, ret);
  3749                     row.append(py::none());
  3750                 }
  3751                 break;

Lines 3787-3797

  3787                     row.append(uuid_obj);
  3788                 } else if (indicator == SQL_NULL_DATA) {
  3789                     row.append(py::none());
  3790                 } else {
! 3791                     LOG("Error retrieving data for column - {}, data type - "
! 3792                         "{}, SQLGetData return code - {}. Returning NULL "
! 3793                         "value instead",
  3794                         i, dataType, ret);
  3795                     row.append(py::none());
  3796                 }
  3797                 break;

Lines 3798-3808

  3798             }
  3799 #endif
  3800             default:
  3801                 std::ostringstream errorString;
! 3802                 errorString << "Unsupported data type for column - "
! 3803                             << columnName << ", Type - " << dataType
! 3804                             << ", column ID - " << i;
  3805                 LOG(errorString.str());
  3806                 ThrowStdException(errorString.str());
  3807                 break;
  3808         }

Lines 4002-4015

  4002                     sizeof(DateTimeOffset) * fetchSize,
  4003                     buffers.indicators[col - 1].data());
  4004                 break;
  4005             default:
! 4006                 std::wstring columnName =
! 4007                     columnMeta["ColumnName"].cast<std::wstring>();
  4008                 std::ostringstream errorString;
! 4009                 errorString << "Unsupported data type for column - "
! 4010                             << columnName.c_str() << ", Type - " << dataType
! 4011                             << ", column ID - " << col;
  4012                 LOG(errorString.str());
  4013                 ThrowStdException(errorString.str());
  4014                 break;
  4015         }

Lines 4013-4025

  4013                 ThrowStdException(errorString.str());
  4014                 break;
  4015         }
  4016         if (!SQL_SUCCEEDED(ret)) {
! 4017             std::wstring columnName =
! 4018                 columnMeta["ColumnName"].cast<std::wstring>();
  4019             std::ostringstream errorString;
! 4020             errorString << "Failed to bind column - " << columnName.c_str()
! 4021                         << ", Type - " << dataType << ", column ID - " << col;
  4022             LOG(errorString.str());
  4023             ThrowStdException(errorString.str());
  4024             return ret;
  4025         }

Lines 4065-4081

  4065             // wont suffice
  4066             // This value indicates that the driver cannot determine the
  4067             // length of the data
  4068             if (dataLen == SQL_NO_TOTAL) {
! 4069                 LOG("Cannot determine the length of the data. Returning "
! 4070                     "NULL value instead. Column ID - {}",
! 4071                     col);
  4072                 row.append(py::none());
  4073                 continue;
  4074             } else if (dataLen == SQL_NULL_DATA) {
! 4075                 LOG("Column data is NULL. Appending None to the result "
! 4076                     "row. Column ID - {}",
! 4077                     col);
  4078                 row.append(py::none());
  4079                 continue;
  4080             } else if (dataLen == 0) {
  4081                 // Handle zero-length (non-NULL) data

Lines 4087-4101

  4087                             py::str decoded_str =
  4088                                 DecodingString("", 0, char_encoding, "strict");
  4089                             row.append(decoded_str);
  4090                         } catch (const std::exception& e) {
! 4091                             LOG("Decoding failed for empty SQL_CHAR data: {}",
! 4092                                 e.what());
! 4093                             row.append(std::string(""));
! 4094                         }
  4095                     } else {
! 4096                         row.append(std::string(""));
! 4097                     }
  4098                 } else if (dataType == SQL_WCHAR || dataType == SQL_WVARCHAR ||
  4099                            dataType == SQL_WLONGVARCHAR) {
  4100                     row.append(std::wstring(L""));
  4101                 } else if (dataType == SQL_BINARY ||

Lines 4104-4115

  4104                     row.append(py::bytes(""));
  4105                 } else {
  4106                     // For other datatypes, 0 length is unexpected. Log &
  4107                     // append None
! 4108                     LOG("Column data length is 0 for non-string/binary "
! 4109                         "datatype. Appending None to the result row. Column "
! 4110                         "ID - {}",
! 4111                         col);
  4112                     row.append(py::none());
  4113                 }
  4114                 continue;
  4115             } else if (dataLen < 0) {

Lines 4114-4127

  4114                 continue;
  4115             } else if (dataLen < 0) {
  4116                 // Negative value is unexpected, log column index, SQL type &
  4117                 // raise exception
! 4118                 LOG("Unexpected negative data length. Column ID - {}, "
! 4119                     "SQL Type - {}, Data Length - {}",
! 4120                     col, dataType, dataLen);
! 4121                 ThrowStdException(
! 4122                     "Unexpected negative data length, check "
! 4123                     "logs for details");
  4124             }
  4125             assert(dataLen > 0 && "Data length must be > 0");
  4126 
  4127             switch (dataType) {

Lines 4150-4171

  4150                             LOG("Applied dynamic decoding for batch CHAR "
  4151                                 "column {} using encoding '{}'",
  4152                                 col, char_encoding);
  4153                         } catch (const std::exception& e) {
! 4154                             LOG("Dynamic decoding failed for batch column "
! 4155                                 "{}: {}. Using fallback.",
! 4156                                 col, e.what());
  4157                             // Fallback to original logic
! 4158                             row.append(std::string(
! 4159                                 reinterpret_cast<char*>(
! 4160                                     &buffers.charBuffers[col - 1]
! 4161                                                         [i * fetchBufferSize]),
! 4162                                 numCharsInData));
! 4163                         }
  4164                     } else {
! 4165                         row.append(FetchLobColumnData(hStmt, col, SQL_C_CHAR,
! 4166                                                       false, false,
! 4167                                                       char_encoding));
  4168                     }
  4169                     break;
  4170                 }
  4171                 case SQL_WCHAR:

Lines 4202-4211

  4202                                                      [i * fetchBufferSize]),
  4203                             numCharsInData));
  4204 #endif
  4205                     } else {
! 4206                         row.append(FetchLobColumnData(hStmt, col, SQL_C_WCHAR,
! 4207                                                       true, false));
  4208                     }
  4209                     break;
  4210                 }
  4211                 case SQL_INTEGER: {

Lines 4379-4398

  4379                             reinterpret_cast<const char*>(
  4380                                 &buffers.charBuffers[col - 1][i * columnSize]),
  4381                             dataLen));
  4382                     } else {
! 4383                         row.append(FetchLobColumnData(hStmt, col, SQL_C_BINARY,
! 4384                                                       false, true));
  4385                     }
  4386                     break;
  4387                 }
  4388                 default: {
! 4389                     std::wstring columnName =
! 4390                         columnMeta["ColumnName"].cast<std::wstring>();
  4391                     std::ostringstream errorString;
! 4392                     errorString << "Unsupported data type for column - "
! 4393                                 << columnName.c_str() << ", Type - " << dataType
! 4394                                 << ", column ID - " << col;
  4395                     LOG(errorString.str());
  4396                     ThrowStdException(errorString.str());
  4397                     break;
  4398                 }

Lines 4475-4488

  4475             case SQL_SS_TIMESTAMPOFFSET:
  4476                 rowSize += sizeof(DateTimeOffset);
  4477                 break;
  4478             default:
! 4479                 std::wstring columnName =
! 4480                     columnMeta["ColumnName"].cast<std::wstring>();
  4481                 std::ostringstream errorString;
! 4482                 errorString << "Unsupported data type for column - "
! 4483                             << columnName.c_str() << ", Type - " << dataType
! 4484                             << ", column ID - " << col;
  4485                 LOG(errorString.str());
  4486                 ThrowStdException(errorString.str());
  4487                 break;
  4488         }

Lines 4943-4952

  4943           "Set the decimal separator character");
  4944     m.def(
  4945         "DDBCSQLSetStmtAttr",
  4946         [](SqlHandlePtr stmt, SQLINTEGER attr, SQLPOINTER value) {
! 4947             return SQLSetStmtAttr_ptr(stmt->get(), attr, value, 0);
! 4948         },
  4949         "Set statement attributes");
  4950     m.def("DDBCSQLGetTypeInfo", &SQLGetTypeInfo_Wrapper,
  4951           "Returns information about the data types that are supported by "
  4952           "the data source",

Lines 5019-5027

  5019         DriverLoader::getInstance().loadDriver();  // Load the driver
  5020     } catch (const std::exception& e) {
  5021         // Log the error but don't throw -
  5022         // let the error happen when functions are called
! 5023         LOG("Failed to load ODBC driver during module initialization: {}",
! 5024             e.what());
  5025     }
  5026 }


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.ddbc_bindings.cpp: 65.9%
mssql_python.row.py: 77.9%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection.cpp: 81.2%
mssql_python.cursor.py: 83.1%
mssql_python.connection.py: 83.9%
mssql_python.pybind.connection.connection_pool.cpp: 84.8%
mssql_python.auth.py: 87.1%
mssql_python.pooling.py: 87.7%
mssql_python.exceptions.py: 92.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

gargsaumya
gargsaumya previously approved these changes Oct 30, 2025
Copy link
Contributor

@sumitmsft sumitmsft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left few comments and also suggested some tests cases to Jahnvi

jahnvi480 and others added 3 commits October 31, 2025 12:34
### Work Item / Issue Reference  
<!-- 
IMPORTANT: Please follow the PR template guidelines below.
For mssql-python maintainers: Insert your ADO Work Item ID below (e.g.
AB#37452)
For external contributors: Insert Github Issue number below (e.g. #149)
Only one reference is required - either GitHub issue OR ADO Work Item.
-->

<!-- mssql-python maintainers: ADO Work Item -->
>
[AB#38478](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/38478)

-------------------------------------------------------------------
### Summary   
This pull request primarily refactors the formatting and structure of
the `mssql_python/pybind/ddbc_bindings.cpp` file, focusing on code
readability and maintainability. No functional logic changes are
introduced; instead, the changes consist of improved line wrapping,
consistent indentation, and clearer inline comments, especially in
function definitions and pybind11 module bindings.

Formatting and readability improvements:

* Reformatted function signatures and argument lists in several places
(e.g., `FetchMany_wrap`, pybind11 bindings) for better readability and
consistency.
* Improved line wrapping and indentation in conditional logic and
function calls, making code easier to follow.
* Enhanced inline comments, especially around LOB streaming and
module-level UUID caching, for clarity.
* Updated error logging during ODBC driver loading to use multi-line
comments and clearer formatting.

Header and include adjustments:

* Reordered and deduplicated header includes, grouping standard library
headers and removing redundant imports.
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.

5 participants