Skip to content

Conversation

@jahnvi480
Copy link
Contributor

@jahnvi480 jahnvi480 commented Nov 17, 2025

Work Item / Issue Reference

AB#33454
AB#40493

GitHub Issue: #22


Summary

This pull request introduces a comprehensive linting workflow for both Python and C++ code, updates code style configuration files, and refactors the benchmarks/bench_mssql.py script for improved readability and consistency. The most significant changes are grouped below.

Linting Workflow & Configuration

  • Added a new GitHub Actions workflow (.github/workflows/lint-check.yml) to automate linting and formatting checks for Python and C++ files, including job summaries and failure handling.
  • Introduced .flake8 for Python linting configuration, specifying line length, ignored warnings, excluded directories, and per-file ignores.
  • Updated .clang-format to define C++ code style, switching to LLVM style with Microsoft modifications, reducing column limit, and adding detailed alignment and spacing rules.

Benchmark Script Refactoring (benchmarks/bench_mssql.py)

  • Reformatted all multi-line SQL queries and data lists to use consistent indentation and triple-quoted strings, improving readability and maintainability.
  • Standardized string quoting to double quotes for data values and SQL queries.
  • Added blank lines between function definitions and logical code blocks to enhance code structure and clarity throughout the benchmark script.
  • Improved environment variable usage for the connection string by splitting the assignment across multiple lines for better readability.

These changes collectively improve code quality, maintainability, and ensure consistent style enforcement across the project.

Copilot AI review requested due to automatic review settings November 17, 2025 10:43
@github-actions github-actions bot added the pr-size: large Substantial code update label Nov 17, 2025
Copy link
Contributor

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

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

devskim found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copilot finished reviewing on behalf of jahnvi480 November 17, 2025 10:44
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 pull request introduces comprehensive linting and code formatting infrastructure for both Python and C++ code, along with automated workflow checks. The changes consist primarily of automated formatting applied consistently across the codebase.

Key Changes:

  • Added GitHub Actions workflow for automated linting checks (.github/workflows/lint-check.yml)
  • Introduced linting configuration files (.flake8, .clang-format, pyproject.toml)
  • Applied consistent formatting to all Python test files and source modules
  • Applied clang-format to C++ pybind11 files with LLVM style conventions
  • Updated requirements.txt to include linting tools

Reviewed Changes

Copilot reviewed 47 out of 51 changed files in this pull request and generated 47 comments.

Show a summary per file
File Description
.flake8, pyproject.toml Added Python linting configuration with 100-character line limit
.clang-format Added C++ formatting rules using LLVM style with Microsoft modifications
requirements.txt Added linting dependencies (black, flake8, pylint, cpplint, mypy)
tests/test_*.py Applied consistent Python formatting (quotes, spacing, line breaks)
mssql_python/*.py Applied formatting to source modules
mssql_python/pybind/*.h, *.cpp Applied clang-format to C++ headers and implementations

The formatting changes are consistent and maintain code functionality. All modifications appear to be purely stylistic with no semantic changes to the codebase. The linting infrastructure will help maintain code quality going forward.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -756,16 +715,12 @@ def test_longvarchar(cursor, db_connection):
assert (
cursor.fetchone() == None
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Testing for None should use the 'is' operator.

Copilot uses AI. Check for mistakes.
@@ -795,16 +748,12 @@ def test_longwvarchar(cursor, db_connection):
assert (
cursor.fetchone() == None
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Testing for None should use the 'is' operator.

Copilot uses AI. Check for mistakes.
@@ -2554,9 +2444,7 @@ def test_cursor_context_manager_exception_handling(db_connection):
with pytest.raises(ValueError):
with db_connection.cursor() as cursor:
cursor_ref = cursor
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Variable cursor_ref is not used.

Copilot uses AI. Check for mistakes.
assert (
cursor in db_connection._cursors
), "Cursor from execute() not tracked by connection"
assert cursor in db_connection._cursors, "Cursor from execute() not tracked by connection"

# Test with data modification and verify it requires commit
if not db_connection.autocommit:
drop_table_if_exists(db_connection.cursor(), "#pytest_test_execute")
cursor1 = db_connection.execute(
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Variable cursor1 is not used.

Copilot uses AI. Check for mistakes.
cursor2 = db_connection.execute(
"INSERT INTO #pytest_test_execute VALUES (1, 'test_value')"
)
cursor2 = db_connection.execute("INSERT INTO #pytest_test_execute VALUES (1, 'test_value')")
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Variable cursor2 is not used.

Copilot uses AI. Check for mistakes.
db_connection.set_attr(
mssql_python.SQL_ATTR_CONNECTION_TIMEOUT, 2147483647
) # Max int32
db_connection.set_attr(mssql_python.SQL_ATTR_CONNECTION_TIMEOUT, 2147483647) # Max int32
pass
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Unnecessary 'pass' statement.

Copilot uses AI. Check for mistakes.
f"Warning: Using fallback module file {module_files[0]} instead of "
f"{expected_module}"
)
print(f"Warning: Using fallback module file {module_files[0]} instead of " f"{expected_module}")
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Print statement may execute during import.

Copilot uses AI. Check for mistakes.
settings["ctype"] == ctype
), f"Failed to set ctype for sqltype {sqltype}"
assert settings["encoding"] == encoding, f"Failed to set encoding for sqltype {sqltype}"
assert settings["ctype"] == ctype, f"Failed to set ctype for sqltype {sqltype}"

finally:
conn.close()
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Instance of context-manager class Connection is closed in a finally block. Consider using 'with' statement.

Copilot uses AI. Check for mistakes.
settings["ctype"] == ctype
), f"Failed to set ctype for sqltype {sqltype}"
assert settings["encoding"] == encoding, f"Failed to set encoding for sqltype {sqltype}"
assert settings["ctype"] == ctype, f"Failed to set ctype for sqltype {sqltype}"

finally:
conn.close()
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Instance of context-manager class Connection is closed in a finally block. Consider using 'with' statement.

Copilot uses AI. Check for mistakes.
settings["ctype"] == ctype
), f"Failed to set ctype for sqltype {sqltype}"
assert settings["encoding"] == encoding, f"Failed to set encoding for sqltype {sqltype}"
assert settings["ctype"] == ctype, f"Failed to set ctype for sqltype {sqltype}"

finally:
conn.close()
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

Instance of context-manager class Connection is closed in a finally block. Consider using 'with' statement.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Nov 17, 2025

📊 Code Coverage Report

🔥 Diff Coverage

65%


🎯 Overall Coverage

75%


📈 Total Lines Covered: 5089 out of 6747
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/init.py (0.0%): Missing lines 190
  • mssql_python/auth.py (100%)
  • mssql_python/connection.py (92.9%): Missing lines 1267
  • mssql_python/connection_string_builder.py (100%)
  • mssql_python/connection_string_parser.py (100%)
  • mssql_python/constants.py (100%)
  • mssql_python/cursor.py (87.8%): Missing lines 814,1342,1345,1348,1911
  • mssql_python/ddbc_bindings.py (0.0%): Missing lines 123
  • mssql_python/exceptions.py (100%)
  • mssql_python/helpers.py (100%)
  • mssql_python/logging.py (92.6%): Missing lines 156,322
  • mssql_python/pybind/connection/connection.cpp (79.5%): Missing lines 205-207,220-222,253,280,283
  • mssql_python/pybind/connection/connection_pool.cpp (71.4%): Missing lines 32-34,66,88-89
  • mssql_python/pybind/ddbc_bindings.cpp (60.7%): Missing lines 32,79,86,93,100,107,240-243,306-310,408,412-413,415-417,420-421,444-446,459-461,477-479,493-495,524-526,559-561,578-580,655-657,702-704,709-711,718-720,726-728,734-736,778,822-824,847-848,908,910,912,995-1000,1339,1346-1347,1379-1380,1440-1442,1450-1453,1608-1610,1674-1675,1680-1682,1696-1697,1699-1701,1721-1723,1732-1733,1772-1774,1789-1790,1796,1804-1806,1811-1812,1817-1818,1821-1823,1850-1858,1903-1907,1929-1930,1937-1941,1945,1971-1976,1991-1993,2013-2017,2028-2030,2084-2088,2102-2104,2110-2114,2128-2130,2136-2140,2154-2155,2159-2161,2184-2185,2190-2192,2234-2238,2248-2252,2255-2259,2271-2273,2302-2307,2314-2318,2339-2343,2356-2358,2363-2364,2427-2428,2435-2436,2449-2451,2453-2455,2468-2470,2473-2475,2477-2478,2481-2483,2486-2488,2496-2498,2505-2507,2518-2519,2635-2636,2761-2763,2798-2800,2809-2812,2815-2819,2822-2824,2879-2882,2885-2889,2892-2894,2916-2918,2929-2931,2981-2983,2987-2989,3002-3004,3015-3017,3044-3046,3064-3066,3088-3089,3103-3105,3141-3143,3148-3150,3162-3164,3175-3177,3208-3210,3233-3234,3536,3566-3568,3592-3594,3601-3603,3732-3733,3883,4013,4088-4089,4100-4101,4117-4118,4241-4242
  • mssql_python/pybind/ddbc_bindings.h (76.9%): Missing lines 81,398,446,721-722,788-789,828-829
  • mssql_python/pybind/logger_bridge.cpp (80.0%): Missing lines 178-180
  • mssql_python/pybind/logger_bridge.hpp (100%)
  • mssql_python/row.py (100%)
  • mssql_python/type.py (100%)

Summary

  • Total: 1171 lines
  • Missing: 401 lines
  • Coverage: 65%

mssql_python/init.py

Lines 186-194

  186 _original_module_setattr = sys.modules[__name__].__setattr__
  187 
  188 
  189 def _custom_setattr(name, value):
! 190     if name == "lowercase":
  191         with _settings_lock:
  192             _settings.lowercase = bool(value)
  193             # Update the module's lowercase variable
  194             _original_module_setattr(name, _settings.lowercase)

mssql_python/connection.py

Lines 1263-1271

  1263                         pass
  1264 
  1265                     # Last resort: return as integer if all else fails
  1266                     try:
! 1267                         return int.from_bytes(data[: min(length, 8)], "little", signed=True)
  1268                     except Exception:
  1269                         return 0
  1270                 elif isinstance(data, (int, float)):
  1271                     # Already numeric

mssql_python/cursor.py

Lines 810-818

  810             if sql_type in (
  811                 ddbc_sql_const.SQL_DECIMAL.value,
  812                 ddbc_sql_const.SQL_NUMERIC.value,
  813             ):
! 814                 column_size = max(1, min(int(column_size) if column_size > 0 else 18, 38))
  815                 decimal_digits = min(max(0, decimal_digits), column_size)
  816 
  817         else:
  818             # Fall back to automatic type inference

Lines 1338-1352

  1338                 return rows
  1339 
  1340             # Save original fetch methods
  1341             if not hasattr(self, "_original_fetchone"):
! 1342                 self._original_fetchone = (
  1343                     self.fetchone
  1344                 )  # pylint: disable=attribute-defined-outside-init
! 1345                 self._original_fetchmany = (
  1346                     self.fetchmany
  1347                 )  # pylint: disable=attribute-defined-outside-init
! 1348                 self._original_fetchall = (
  1349                     self.fetchall
  1350                 )  # pylint: disable=attribute-defined-outside-init
  1351 
  1352             # Use specialized mapping methods

Lines 1907-1915

  1907                 if sql_type in (
  1908                     ddbc_sql_const.SQL_DECIMAL.value,
  1909                     ddbc_sql_const.SQL_NUMERIC.value,
  1910                 ):
! 1911                     column_size = max(1, min(int(column_size) if column_size > 0 else 18, 38))
  1912                     decimal_digits = min(max(0, decimal_digits), column_size)
  1913 
  1914                 # For binary data columns with mixed content, we need to find max size
  1915                 if sql_type in (

mssql_python/ddbc_bindings.py

Lines 119-127

  119             f"No ddbc_bindings module found for {python_version}-{architecture} "
  120             f"with extension {extension}"
  121         )
  122     module_path = os.path.join(module_dir, module_files[0])
! 123     print(f"Warning: Using fallback module file {module_files[0]} instead of " f"{expected_module}")
  124 
  125 
  126 # Use the original module name 'ddbc_bindings' that the C extension was compiled with
  127 module_name = "ddbc_bindings"

mssql_python/logging.py

Lines 152-160

  152                     end_bracket = msg.index("]")
  153                     source = msg[1:end_bracket]
  154                     message = msg[end_bracket + 2 :].strip()  # Skip '] '
  155                 else:
! 156                     source = "Unknown"
  157                     message = msg
  158 
  159                 # Format timestamp with milliseconds using period separator
  160                 timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S")

Lines 318-326

  318 
  319         except Exception as e:
  320             # Notify on stderr so user knows why header is missing
  321             try:
! 322                 sys.stderr.write(
  323                     f"[MSSQL-Python] Warning: Failed to write log header to {self._log_file}: {type(e).__name__}\n"
  324                 )
  325                 sys.stderr.flush()
  326             except:

mssql_python/pybind/connection/connection.cpp

Lines 201-211

  201 
  202             // Convert to wide string
  203             std::wstring wstr = Utf8ToWString(utf8_str);
  204             if (wstr.empty() && !utf8_str.empty()) {
! 205                 LOG("Failed to convert string value to wide string for "
! 206                     "attribute=%d",
! 207                     attribute);
  208                 return SQL_ERROR;
  209             }
  210             this->wstrStringBuffer.clear();
  211             this->wstrStringBuffer = std::move(wstr);

Lines 216-226

  216 #if defined(__APPLE__) || defined(__linux__)
  217             // For macOS/Linux, convert wstring to SQLWCHAR buffer
  218             std::vector<SQLWCHAR> sqlwcharBuffer = WStringToSQLWCHAR(this->wstrStringBuffer);
  219             if (sqlwcharBuffer.empty() && !this->wstrStringBuffer.empty()) {
! 220                 LOG("Failed to convert wide string to SQLWCHAR buffer for "
! 221                     "attribute=%d",
! 222                     attribute);
  223                 return SQL_ERROR;
  224             }
  225 
  226             ptr = sqlwcharBuffer.data();

Lines 249-257

  249             this->strBytesBuffer = std::move(binary_data);
  250             SQLPOINTER ptr = const_cast<char*>(this->strBytesBuffer.c_str());
  251             SQLINTEGER length = static_cast<SQLINTEGER>(this->strBytesBuffer.size());
  252 
! 253             SQLRETURN ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), attribute, ptr, length);
  254             if (!SQL_SUCCEEDED(ret)) {
  255                 LOG("Failed to set binary attribute=%d, ret=%d", attribute, ret);
  256             } else {
  257                 LOG("Set binary attribute=%d successfully (length=%d)", attribute, length);

Lines 276-287

  276             continue;
  277         }
  278 
  279         // Apply all supported attributes
! 280         SQLRETURN ret = setAttribute(key, py::reinterpret_borrow<py::object>(item.second));
  281         if (!SQL_SUCCEEDED(ret)) {
  282             std::string attrName = std::to_string(key);
! 283             std::string errorMsg = "Failed to set attribute " + attrName + " before connect";
  284             ThrowStdException(errorMsg);
  285         }
  286     }
  287 }

mssql_python/pybind/connection/connection_pool.cpp

Lines 28-38

  28                                            std::chrono::duration_cast<std::chrono::seconds>(
  29                                                now - conn->lastUsed())
  30                                                .count();
  31                                        if (idle_time > _idle_timeout_secs) {
! 32                                            to_disconnect.push_back(conn);
! 33                                            return true;
! 34                                        }
  35                                        return false;
  36                                    }),
  37                     _pool.end());

Lines 62-70

  62             valid_conn = std::make_shared<Connection>(connStr, true);
  63             valid_conn->connect(attrs_before);
  64             ++_current_size;
  65         } else if (!valid_conn) {
! 66             throw std::runtime_error("ConnectionPool::acquire: pool size limit reached");
  67         }
  68     }
  69 
  70     // Phase 3: Disconnect expired/bad connections outside lock

Lines 84-93

  84         conn->updateLastUsed();
  85         _pool.push_back(conn);
  86     } else {
  87         conn->disconnect();
! 88         if (_current_size > 0)
! 89             --_current_size;
  90     }
  91 }
  92 
  93 void ConnectionPool::close() {

mssql_python/pybind/ddbc_bindings.cpp

Lines 28-36

  28 #define SQL_MAX_NUMERIC_LEN 16
  29 #define SQL_SS_XML (-152)
  30 
  31 #define STRINGIFY_FOR_CASE(x)                                                                      \
! 32     case x:                                                                                        \
  33         return #x
  34 
  35 // Architecture-specific defines
  36 #ifndef ARCHITECTURE

Lines 75-83

  75 py::object get_datetime_class() {
  76     if (cache_initialized && datetime_class) {
  77         return datetime_class;
  78     }
! 79     return py::module_::import("datetime").attr("datetime");
  80 }
  81 
  82 py::object get_date_class() {
  83     if (cache_initialized && date_class) {

Lines 82-90

  82 py::object get_date_class() {
  83     if (cache_initialized && date_class) {
  84         return date_class;
  85     }
! 86     return py::module_::import("datetime").attr("date");
  87 }
  88 
  89 py::object get_time_class() {
  90     if (cache_initialized && time_class) {

Lines 89-97

  89 py::object get_time_class() {
  90     if (cache_initialized && time_class) {
  91         return time_class;
  92     }
! 93     return py::module_::import("datetime").attr("time");
  94 }
  95 
  96 py::object get_decimal_class() {
  97     if (cache_initialized && decimal_class) {

Lines 96-104

   96 py::object get_decimal_class() {
   97     if (cache_initialized && decimal_class) {
   98         return decimal_class;
   99     }
! 100     return py::module_::import("decimal").attr("Decimal");
  101 }
  102 
  103 py::object get_uuid_class() {
  104     if (cache_initialized && uuid_class) {

Lines 103-111

  103 py::object get_uuid_class() {
  104     if (cache_initialized && uuid_class) {
  105         return uuid_class;
  106     }
! 107     return py::module_::import("uuid").attr("UUID");
  108 }
  109 }  // namespace PythonObjectCache
  110 
  111 //-------------------------------------------------------------------------------------------------

Lines 236-247

  236     }
  237 }
  238 
  239 std::string MakeParamMismatchErrorStr(const SQLSMALLINT cType, const int paramIndex) {
! 240     std::string errorString = "Parameter's object type does not match "
! 241                               "parameter's C type. paramIndex - " +
! 242                               std::to_string(paramIndex) + ", C type - " +
! 243                               GetSqlCTypeAsString(cType);
  244     return errorString;
  245 }
  246 
  247 // This function allocates a buffer of ParamType, stores it as a void* in

Lines 302-314

  302                     !py::isinstance<py::bytes>(param)) {
  303                     ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
  304                 }
  305                 if (paramInfo.isDAE) {
! 306                     LOG("BindParameters: param[%d] SQL_C_CHAR - Using DAE "
! 307                         "(Data-At-Execution) for large string streaming",
! 308                         paramIndex);
! 309                     dataPtr =
! 310                         const_cast<void*>(reinterpret_cast<const void*>(&paramInfos[paramIndex]));
  311                     strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
  312                     *strLenOrIndPtr = SQL_LEN_DATA_AT_EXEC(0);
  313                     bufferLength = 0;
  314                 } else {

Lines 404-425

  404                 SQLULEN columnSize = paramInfo.columnSize;
  405                 SQLSMALLINT decimalDigits = paramInfo.decimalDigits;
  406                 if (sqlType == SQL_UNKNOWN_TYPE) {
  407                     SQLSMALLINT describedType;
! 408                     SQLULEN describedSize;
  409                     SQLSMALLINT describedDigits;
  410                     SQLSMALLINT nullable;
  411                     RETCODE rc = SQLDescribeParam_ptr(
! 412                         hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1), &describedType,
! 413                         &describedSize, &describedDigits, &nullable);
  414                     if (!SQL_SUCCEEDED(rc)) {
! 415                         LOG("BindParameters: SQLDescribeParam failed for "
! 416                             "param[%d] (NULL parameter) - SQLRETURN=%d",
! 417                             paramIndex, rc);
  418                         return rc;
  419                     }
! 420                     sqlType = describedType;
! 421                     columnSize = describedSize;
  422                     decimalDigits = describedDigits;
  423                 }
  424                 dataPtr = nullptr;
  425                 strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);

Lines 440-450

  440                 int value = param.cast<int>();
  441                 // Range validation for signed 16-bit integer
  442                 if (value < std::numeric_limits<short>::min() ||
  443                     value > std::numeric_limits<short>::max()) {
! 444                     ThrowStdException("Signed short integer parameter out of "
! 445                                       "range at paramIndex " +
! 446                                       std::to_string(paramIndex));
  447                 }
  448                 dataPtr =
  449                     static_cast<void*>(AllocateParamBuffer<int>(paramBuffers, param.cast<int>()));
  450                 break;

Lines 455-465

  455                     ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
  456                 }
  457                 unsigned int value = param.cast<unsigned int>();
  458                 if (value > std::numeric_limits<unsigned short>::max()) {
! 459                     ThrowStdException("Unsigned short integer parameter out of "
! 460                                       "range at paramIndex " +
! 461                                       std::to_string(paramIndex));
  462                 }
  463                 dataPtr = static_cast<void*>(
  464                     AllocateParamBuffer<unsigned int>(paramBuffers, param.cast<unsigned int>()));
  465                 break;

Lines 473-483

  473                 int64_t value = param.cast<int64_t>();
  474                 // Range validation for signed 64-bit integer
  475                 if (value < std::numeric_limits<int64_t>::min() ||
  476                     value > std::numeric_limits<int64_t>::max()) {
! 477                     ThrowStdException("Signed 64-bit integer parameter out of "
! 478                                       "range at paramIndex " +
! 479                                       std::to_string(paramIndex));
  480                 }
  481                 dataPtr = static_cast<void*>(
  482                     AllocateParamBuffer<int64_t>(paramBuffers, param.cast<int64_t>()));
  483                 break;

Lines 489-499

  489                 }
  490                 uint64_t value = param.cast<uint64_t>();
  491                 // Range validation for unsigned 64-bit integer
  492                 if (value > std::numeric_limits<uint64_t>::max()) {
! 493                     ThrowStdException("Unsigned 64-bit integer parameter out "
! 494                                       "of range at paramIndex " +
! 495                                       std::to_string(paramIndex));
  496                 }
  497                 dataPtr = static_cast<void*>(
  498                     AllocateParamBuffer<uint64_t>(paramBuffers, param.cast<uint64_t>()));
  499                 break;

Lines 520-530

  520                     ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
  521                 }
  522                 int year = param.attr("year").cast<int>();
  523                 if (year < 1753 || year > 9999) {
! 524                     ThrowStdException("Date out of range for SQL Server "
! 525                                       "(1753-9999) at paramIndex " +
! 526                                       std::to_string(paramIndex));
  527                 }
  528                 // TODO: can be moved to python by registering SQL_DATE_STRUCT
  529                 // in pybind
  530                 SQL_DATE_STRUCT* sqlDatePtr = AllocateParamBuffer<SQL_DATE_STRUCT>(paramBuffers);

Lines 555-565

  555                 }
  556                 // Checking if the object has a timezone
  557                 py::object tzinfo = param.attr("tzinfo");
  558                 if (tzinfo.is_none()) {
! 559                     ThrowStdException("Datetime object must have tzinfo for "
! 560                                       "SQL_C_SS_TIMESTAMPOFFSET at paramIndex " +
! 561                                       std::to_string(paramIndex));
  562                 }
  563 
  564                 DateTimeOffset* dtoPtr = AllocateParamBuffer<DateTimeOffset>(paramBuffers);

Lines 574-584

  574                     static_cast<SQLUINTEGER>(param.attr("microsecond").cast<int>() * 1000);
  575 
  576                 py::object utcoffset = tzinfo.attr("utcoffset")(param);
  577                 if (utcoffset.is_none()) {
! 578                     ThrowStdException("Datetime object's tzinfo.utcoffset() "
! 579                                       "returned None at paramIndex " +
! 580                                       std::to_string(paramIndex));
  581                 }
  582 
  583                 int total_seconds =
  584                     static_cast<int>(utcoffset.attr("total_seconds")().cast<double>());

Lines 651-661

  651                 py::bytes uuid_bytes = param.cast<py::bytes>();
  652                 const unsigned char* uuid_data =
  653                     reinterpret_cast<const unsigned char*>(PyBytes_AS_STRING(uuid_bytes.ptr()));
  654                 if (PyBytes_GET_SIZE(uuid_bytes.ptr()) != 16) {
! 655                     LOG("BindParameters: param[%d] SQL_C_GUID - Invalid UUID "
! 656                         "length: expected 16 bytes, got %ld bytes",
! 657                         paramIndex, PyBytes_GET_SIZE(uuid_bytes.ptr()));
  658                     ThrowStdException("UUID binary data must be exactly 16 bytes long.");
  659                 }
  660                 SQLGUID* guid_data_ptr = AllocateParamBuffer<SQLGUID>(paramBuffers);
  661                 guid_data_ptr->Data1 = (static_cast<uint32_t>(uuid_data[3]) << 24) |

Lines 698-715

  698         if (paramInfo.paramCType == SQL_C_NUMERIC) {
  699             SQLHDESC hDesc = nullptr;
  700             rc = SQLGetStmtAttr_ptr(hStmt, SQL_ATTR_APP_PARAM_DESC, &hDesc, 0, NULL);
  701             if (!SQL_SUCCEEDED(rc)) {
! 702                 LOG("BindParameters: SQLGetStmtAttr(SQL_ATTR_APP_PARAM_DESC) "
! 703                     "failed for param[%d] - SQLRETURN=%d",
! 704                     paramIndex, rc);
  705                 return rc;
  706             }
  707             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_TYPE, (SQLPOINTER)SQL_C_NUMERIC, 0);
  708             if (!SQL_SUCCEEDED(rc)) {
! 709                 LOG("BindParameters: SQLSetDescField(SQL_DESC_TYPE) failed for "
! 710                     "param[%d] - SQLRETURN=%d",
! 711                     paramIndex, rc);
  712                 return rc;
  713             }
  714             SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
  715             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,

Lines 714-724

  714             SQL_NUMERIC_STRUCT* numericPtr = reinterpret_cast<SQL_NUMERIC_STRUCT*>(dataPtr);
  715             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_PRECISION,
  716                                      (SQLPOINTER)numericPtr->precision, 0);
  717             if (!SQL_SUCCEEDED(rc)) {
! 718                 LOG("BindParameters: SQLSetDescField(SQL_DESC_PRECISION) "
! 719                     "failed for param[%d] - SQLRETURN=%d",
! 720                     paramIndex, rc);
  721                 return rc;
  722             }
  723 
  724             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE, (SQLPOINTER)numericPtr->scale, 0);

Lines 722-732

  722             }
  723 
  724             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_SCALE, (SQLPOINTER)numericPtr->scale, 0);
  725             if (!SQL_SUCCEEDED(rc)) {
! 726                 LOG("BindParameters: SQLSetDescField(SQL_DESC_SCALE) failed "
! 727                     "for param[%d] - SQLRETURN=%d",
! 728                     paramIndex, rc);
  729                 return rc;
  730             }
  731 
  732             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER)numericPtr, 0);

Lines 730-740

  730             }
  731 
  732             rc = SQLSetDescField_ptr(hDesc, 1, SQL_DESC_DATA_PTR, (SQLPOINTER)numericPtr, 0);
  733             if (!SQL_SUCCEEDED(rc)) {
! 734                 LOG("BindParameters: SQLSetDescField(SQL_DESC_DATA_PTR) failed "
! 735                     "for param[%d] - SQLRETURN=%d",
! 736                     paramIndex, rc);
  737                 return rc;
  738             }
  739         }
  740     }

Lines 774-782

  774             // version compatibility)
  775             if (py::hasattr(sys_module, "_is_finalizing")) {
  776                 py::object finalizing_func = sys_module.attr("_is_finalizing");
  777                 if (!finalizing_func.is_none() && finalizing_func().cast<bool>()) {
! 778                     return true;  // Python is finalizing
  779                 }
  780             }
  781         }
  782         return false;

Lines 818-828

  818     if (pos != std::string::npos) {
  819         std::string dir = module_file.substr(0, pos);
  820         return dir;
  821     }
! 822     LOG("GetModuleDirectory: Could not extract directory from module path - "
! 823         "path='%s'",
! 824         module_file.c_str());
  825     return module_file;
  826 #endif
  827 }

Lines 843-852

  843 #else
  844     // macOS/Unix: Use dlopen
  845     void* handle = dlopen(driverPath.c_str(), RTLD_LAZY);
  846     if (!handle) {
! 847         LOG("LoadDriverLibrary: dlopen failed for path='%s' - %s", driverPath.c_str(),
! 848             dlerror() ? dlerror() : "unknown error");
  849     }
  850     return handle;
  851 #endif
  852 }

Lines 904-916

  904 
  905 // Detect platform and set path
  906 #ifdef __linux__
  907     if (fs::exists("/etc/alpine-release")) {
! 908         platform = "alpine";
  909     } else if (fs::exists("/etc/redhat-release") || fs::exists("/etc/centos-release")) {
! 910         platform = "rhel";
  911     } else if (fs::exists("/etc/SuSE-release") || fs::exists("/etc/SUSE-brand")) {
! 912         platform = "suse";
  913     } else {
  914         platform = "debian_ubuntu";  // Default to debian_ubuntu for other distros
  915     }

Lines 991-1004

   991     }
   992 
   993     DriverHandle handle = LoadDriverLibrary(driverPath.string());
   994     if (!handle) {
!  995         LOG("LoadDriverOrThrowException: Failed to load ODBC driver - "
!  996             "path='%s', error='%s'",
!  997             driverPath.string().c_str(), GetLastErrorMessage().c_str());
!  998         ThrowStdException("Failed to load the driver. Please read the documentation "
!  999                           "(https://github.com/microsoft/mssql-python#installation) to "
! 1000                           "install the required dependencies.");
  1001     }
  1002     LOG("LoadDriverOrThrowException: ODBC driver library loaded successfully "
  1003         "from '%s'",
  1004         driverPath.string().c_str());

Lines 1335-1343

  1335     LOG("SQLCheckError: Checking ODBC errors - handleType=%d, retcode=%d", handleType, retcode);
  1336     ErrorInfo errorInfo;
  1337     if (retcode == SQL_INVALID_HANDLE) {
  1338         LOG("SQLCheckError: SQL_INVALID_HANDLE detected - handle is invalid");
! 1339         errorInfo.ddbcErrorMsg = std::wstring(L"Invalid handle!");
  1340         return errorInfo;
  1341     }
  1342     assert(handle != 0);
  1343     SQLHANDLE rawHandle = handle->get();

Lines 1342-1351

  1342     assert(handle != 0);
  1343     SQLHANDLE rawHandle = handle->get();
  1344     if (!SQL_SUCCEEDED(retcode)) {
  1345         if (!SQLGetDiagRec_ptr) {
! 1346             LOG("SQLCheckError: SQLGetDiagRec function pointer not "
! 1347                 "initialized, loading driver");
  1348             DriverLoader::getInstance().loadDriver();  // Load the driver
  1349         }
  1350 
  1351         SQLWCHAR sqlState[6], message[SQL_MAX_MESSAGE_LENGTH];

Lines 1375-1384

  1375     LOG("SQLGetAllDiagRecords: Retrieving all diagnostic records for handle "
  1376         "%p, handleType=%d",
  1377         (void*)handle->get(), handle->type());
  1378     if (!SQLGetDiagRec_ptr) {
! 1379         LOG("SQLGetAllDiagRecords: SQLGetDiagRec function pointer not "
! 1380             "initialized, loading driver");
  1381         DriverLoader::getInstance().loadDriver();
  1382     }
  1383 
  1384     py::list records;

Lines 1436-1446

  1436 
  1437 // Wrap SQLExecDirect
  1438 SQLRETURN SQLExecDirect_wrap(SqlHandlePtr StatementHandle, const std::wstring& Query) {
  1439     std::string queryUtf8 = WideToUTF8(Query);
! 1440     LOG("SQLExecDirect: Executing query directly - statement_handle=%p, "
! 1441         "query_length=%zu chars",
! 1442         (void*)StatementHandle->get(), Query.length());
  1443     if (!SQLExecDirect_ptr) {
  1444         LOG("SQLExecDirect: Function pointer not initialized, loading driver");
  1445         DriverLoader::getInstance().loadDriver();  // Load the driver
  1446     }

Lines 1446-1457

  1446     }
  1447 
  1448     // Configure forward-only cursor
  1449     if (SQLSetStmtAttr_ptr && StatementHandle && StatementHandle->get()) {
! 1450         SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CURSOR_TYPE,
! 1451                            (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, 0);
! 1452         SQLSetStmtAttr_ptr(StatementHandle->get(), SQL_ATTR_CONCURRENCY,
! 1453                            (SQLPOINTER)SQL_CONCUR_READ_ONLY, 0);
  1454     }
  1455 
  1456     SQLWCHAR* queryPtr;
  1457 #if defined(__APPLE__) || defined(__linux__)

Lines 1604-1614

  1604         assert(isStmtPrepared.size() == 1);
  1605         if (usePrepare) {
  1606             rc = SQLPrepare_ptr(hStmt, queryPtr, SQL_NTS);
  1607             if (!SQL_SUCCEEDED(rc)) {
! 1608                 LOG("SQLExecute: SQLPrepare failed - SQLRETURN=%d, "
! 1609                     "statement_handle=%p",
! 1610                     rc, (void*)hStmt);
  1611                 return rc;
  1612             }
  1613             isStmtPrepared[0] = py::cast(true);
  1614         } else {

Lines 1670-1686

  1670                             size_t len = std::min(chunkChars, totalChars - offset);
  1671                             size_t lenBytes = len * sizeof(SQLWCHAR);
  1672                             if (lenBytes >
  1673                                 static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
! 1674                                 ThrowStdException("Chunk size exceeds maximum "
! 1675                                                   "allowed by SQLLEN");
  1676                             }
  1677                             rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset),
  1678                                                 static_cast<SQLLEN>(lenBytes));
  1679                             if (!SQL_SUCCEEDED(rc)) {
! 1680                                 LOG("SQLExecute: SQLPutData failed for "
! 1681                                     "SQL_C_WCHAR chunk - offset=%zu",
! 1682                                     offset, totalChars, lenBytes, rc);
  1683                                 return rc;
  1684                             }
  1685                             offset += len;
  1686                         }

Lines 1692-1705

  1692                         size_t chunkBytes = DAE_CHUNK_SIZE;
  1693                         while (offset < totalBytes) {
  1694                             size_t len = std::min(chunkBytes, totalBytes - offset);
  1695 
! 1696                             rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset),
! 1697                                                 static_cast<SQLLEN>(len));
  1698                             if (!SQL_SUCCEEDED(rc)) {
! 1699                                 LOG("SQLExecute: SQLPutData failed for "
! 1700                                     "SQL_C_CHAR chunk - offset=%zu",
! 1701                                     offset, totalBytes, len, rc);
  1702                                 return rc;
  1703                             }
  1704                             offset += len;
  1705                         }

Lines 1717-1727

  1717                         size_t len = std::min(chunkSize, totalBytes - offset);
  1718                         rc = SQLPutData_ptr(hStmt, (SQLPOINTER)(dataPtr + offset),
  1719                                             static_cast<SQLLEN>(len));
  1720                         if (!SQL_SUCCEEDED(rc)) {
! 1721                             LOG("SQLExecute: SQLPutData failed for "
! 1722                                 "binary/bytes chunk - offset=%zu",
! 1723                                 offset, totalBytes, len, rc);
  1724                             return rc;
  1725                         }
  1726                     }
  1727                 } else {

Lines 1728-1737

  1728                     ThrowStdException("DAE only supported for str or bytes");
  1729                 }
  1730             }
  1731             if (!SQL_SUCCEEDED(rc)) {
! 1732                 LOG("SQLExecute: SQLParamData final call %s - SQLRETURN=%d",
! 1733                     (rc == SQL_NO_DATA ? "completed with no data" : "failed"), rc);
  1734                 return rc;
  1735             }
  1736             LOG("SQLExecute: DAE streaming completed successfully, SQLExecute "
  1737                 "resumed");

Lines 1768-1778

  1768                 "SQL_type=%d, column_size=%zu, decimal_digits=%d",
  1769                 paramIndex, info.paramCType, info.paramSQLType, info.columnSize,
  1770                 info.decimalDigits);
  1771             if (columnValues.size() != paramSetSize) {
! 1772                 LOG("BindParameterArray: Size mismatch - param_index=%d, "
! 1773                     "expected=%zu, actual=%zu",
! 1774                     paramIndex, paramSetSize, columnValues.size());
  1775                 ThrowStdException("Column " + std::to_string(paramIndex) + " has mismatched size.");
  1776             }
  1777             void* dataPtr = nullptr;
  1778             SQLLEN* strLenOrIndArray = nullptr;

Lines 1785-1794

  1785                     int* dataArray = AllocateParamBufferArray<int>(tempBuffers, paramSetSize);
  1786                     for (size_t i = 0; i < paramSetSize; ++i) {
  1787                         if (columnValues[i].is_none()) {
  1788                             if (!strLenOrIndArray)
! 1789                                 strLenOrIndArray =
! 1790                                     AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1791                             dataArray[i] = 0;
  1792                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1793                         } else {
  1794                             dataArray[i] = columnValues[i].cast<int>();

Lines 1792-1800

  1792                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1793                         } else {
  1794                             dataArray[i] = columnValues[i].cast<int>();
  1795                             if (strLenOrIndArray)
! 1796                                 strLenOrIndArray[i] = 0;
  1797                         }
  1798                     }
  1799                     LOG("BindParameterArray: SQL_C_LONG bound - param_index=%d", paramIndex);
  1800                     dataPtr = dataArray;

Lines 1800-1827

  1800                     dataPtr = dataArray;
  1801                     break;
  1802                 }
  1803                 case SQL_C_DOUBLE: {
! 1804                     LOG("BindParameterArray: Binding SQL_C_DOUBLE array - "
! 1805                         "param_index=%d, count=%zu",
! 1806                         paramIndex, paramSetSize);
  1807                     double* dataArray = AllocateParamBufferArray<double>(tempBuffers, paramSetSize);
  1808                     for (size_t i = 0; i < paramSetSize; ++i) {
  1809                         if (columnValues[i].is_none()) {
  1810                             if (!strLenOrIndArray)
! 1811                                 strLenOrIndArray =
! 1812                                     AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1813                             dataArray[i] = 0;
  1814                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1815                         } else {
  1816                             dataArray[i] = columnValues[i].cast<double>();
! 1817                             if (strLenOrIndArray)
! 1818                                 strLenOrIndArray[i] = 0;
  1819                         }
  1820                     }
! 1821                     LOG("BindParameterArray: SQL_C_DOUBLE bound - "
! 1822                         "param_index=%d",
! 1823                         paramIndex);
  1824                     dataPtr = dataArray;
  1825                     break;
  1826                 }
  1827                 case SQL_C_WCHAR: {

Lines 1846-1862

  1846                             // Check UTF-16 length (excluding null terminator)
  1847                             // against column size
  1848                             if (utf16Buf.size() > 0 && utf16_len > info.columnSize) {
  1849                                 std::string offending = WideToUTF8(wstr);
! 1850                                 LOG("BindParameterArray: SQL_C_WCHAR string "
! 1851                                     "too long - param_index=%d, row=%zu, "
! 1852                                     "utf16_length=%zu, max=%zu",
! 1853                                     paramIndex, i, utf16_len, info.columnSize);
! 1854                                 ThrowStdException("Input string UTF-16 length exceeds "
! 1855                                                   "allowed column size at parameter index " +
! 1856                                                   std::to_string(paramIndex) + ". UTF-16 length: " +
! 1857                                                   std::to_string(utf16_len) + ", Column size: " +
! 1858                                                   std::to_string(info.columnSize));
  1859                             }
  1860                             // If we reach here, the UTF-16 string fits - copy
  1861                             // it completely
  1862                             std::memcpy(wcharArray + i * (info.columnSize + 1), utf16Buf.data(),

Lines 1899-1911

  1899                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1900                         } else {
  1901                             int intVal = columnValues[i].cast<int>();
  1902                             if (intVal < 0 || intVal > 255) {
! 1903                                 LOG("BindParameterArray: TINYINT value out of "
! 1904                                     "range - param_index=%d, row=%zu, value=%d",
! 1905                                     paramIndex, i, intVal);
! 1906                                 ThrowStdException("UTINYINT value out of range at rowIndex " +
! 1907                                                   std::to_string(i));
  1908                             }
  1909                             dataArray[i] = static_cast<unsigned char>(intVal);
  1910                             if (strLenOrIndArray)
  1911                                 strLenOrIndArray[i] = 0;

Lines 1925-1934

  1925                     short* dataArray = AllocateParamBufferArray<short>(tempBuffers, paramSetSize);
  1926                     for (size_t i = 0; i < paramSetSize; ++i) {
  1927                         if (columnValues[i].is_none()) {
  1928                             if (!strLenOrIndArray)
! 1929                                 strLenOrIndArray =
! 1930                                     AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1931                             dataArray[i] = 0;
  1932                             strLenOrIndArray[i] = SQL_NULL_DATA;
  1933                         } else {
  1934                             int intVal = columnValues[i].cast<int>();

Lines 1933-1949

  1933                         } else {
  1934                             int intVal = columnValues[i].cast<int>();
  1935                             if (intVal < std::numeric_limits<short>::min() ||
  1936                                 intVal > std::numeric_limits<short>::max()) {
! 1937                                 LOG("BindParameterArray: SHORT value out of "
! 1938                                     "range - param_index=%d, row=%zu, value=%d",
! 1939                                     paramIndex, i, intVal);
! 1940                                 ThrowStdException("SHORT value out of range at rowIndex " +
! 1941                                                   std::to_string(i));
  1942                             }
  1943                             dataArray[i] = static_cast<short>(intVal);
  1944                             if (strLenOrIndArray)
! 1945                                 strLenOrIndArray[i] = 0;
  1946                         }
  1947                     }
  1948                     LOG("BindParameterArray: SQL_C_SHORT bound - "
  1949                         "param_index=%d",

Lines 1967-1980

  1967                                         info.columnSize + 1);
  1968                         } else {
  1969                             std::string str = columnValues[i].cast<std::string>();
  1970                             if (str.size() > info.columnSize) {
! 1971                                 LOG("BindParameterArray: String/binary too "
! 1972                                     "long - param_index=%d, row=%zu, size=%zu, "
! 1973                                     "max=%zu",
! 1974                                     paramIndex, i, str.size(), info.columnSize);
! 1975                                 ThrowStdException("Input exceeds column size at index " +
! 1976                                                   std::to_string(i));
  1977                             }
  1978                             std::memcpy(charArray + i * (info.columnSize + 1), str.c_str(),
  1979                                         str.size());
  1980                             strLenOrIndArray[i] = static_cast<SQLLEN>(str.size());

Lines 1987-1997

  1987                     bufferLength = info.columnSize + 1;
  1988                     break;
  1989                 }
  1990                 case SQL_C_BIT: {
! 1991                     LOG("BindParameterArray: Binding SQL_C_BIT array - "
! 1992                         "param_index=%d, count=%zu",
! 1993                         paramIndex, paramSetSize);
  1994                     char* boolArray = AllocateParamBufferArray<char>(tempBuffers, paramSetSize);
  1995                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  1996                     for (size_t i = 0; i < paramSetSize; ++i) {
  1997                         if (columnValues[i].is_none()) {

Lines 2009-2021

  2009                     break;
  2010                 }
  2011                 case SQL_C_STINYINT:
  2012                 case SQL_C_USHORT: {
! 2013                     LOG("BindParameterArray: Binding SQL_C_USHORT/STINYINT "
! 2014                         "array - param_index=%d, count=%zu",
! 2015                         paramIndex, paramSetSize);
! 2016                     unsigned short* dataArray =
! 2017                         AllocateParamBufferArray<unsigned short>(tempBuffers, paramSetSize);
  2018                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2019                     for (size_t i = 0; i < paramSetSize; ++i) {
  2020                         if (columnValues[i].is_none()) {
  2021                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2024-2034

  2024                             dataArray[i] = columnValues[i].cast<unsigned short>();
  2025                             strLenOrIndArray[i] = 0;
  2026                         }
  2027                     }
! 2028                     LOG("BindParameterArray: SQL_C_USHORT bound - "
! 2029                         "param_index=%d",
! 2030                         paramIndex);
  2031                     dataPtr = dataArray;
  2032                     bufferLength = sizeof(unsigned short);
  2033                     break;
  2034                 }

Lines 2080-2092

  2080                     bufferLength = sizeof(float);
  2081                     break;
  2082                 }
  2083                 case SQL_C_TYPE_DATE: {
! 2084                     LOG("BindParameterArray: Binding SQL_C_TYPE_DATE array - "
! 2085                         "param_index=%d, count=%zu",
! 2086                         paramIndex, paramSetSize);
! 2087                     SQL_DATE_STRUCT* dateArray =
! 2088                         AllocateParamBufferArray<SQL_DATE_STRUCT>(tempBuffers, paramSetSize);
  2089                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2090                     for (size_t i = 0; i < paramSetSize; ++i) {
  2091                         if (columnValues[i].is_none()) {
  2092                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2098-2108

  2098                             dateArray[i].day = dateObj.attr("day").cast<SQLUSMALLINT>();
  2099                             strLenOrIndArray[i] = 0;
  2100                         }
  2101                     }
! 2102                     LOG("BindParameterArray: SQL_C_TYPE_DATE bound - "
! 2103                         "param_index=%d",
! 2104                         paramIndex);
  2105                     dataPtr = dateArray;
  2106                     bufferLength = sizeof(SQL_DATE_STRUCT);
  2107                     break;
  2108                 }

Lines 2106-2118

  2106                     bufferLength = sizeof(SQL_DATE_STRUCT);
  2107                     break;
  2108                 }
  2109                 case SQL_C_TYPE_TIME: {
! 2110                     LOG("BindParameterArray: Binding SQL_C_TYPE_TIME array - "
! 2111                         "param_index=%d, count=%zu",
! 2112                         paramIndex, paramSetSize);
! 2113                     SQL_TIME_STRUCT* timeArray =
! 2114                         AllocateParamBufferArray<SQL_TIME_STRUCT>(tempBuffers, paramSetSize);
  2115                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2116                     for (size_t i = 0; i < paramSetSize; ++i) {
  2117                         if (columnValues[i].is_none()) {
  2118                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2124-2134

  2124                             timeArray[i].second = timeObj.attr("second").cast<SQLUSMALLINT>();
  2125                             strLenOrIndArray[i] = 0;
  2126                         }
  2127                     }
! 2128                     LOG("BindParameterArray: SQL_C_TYPE_TIME bound - "
! 2129                         "param_index=%d",
! 2130                         paramIndex);
  2131                     dataPtr = timeArray;
  2132                     bufferLength = sizeof(SQL_TIME_STRUCT);
  2133                     break;
  2134                 }

Lines 2132-2144

  2132                     bufferLength = sizeof(SQL_TIME_STRUCT);
  2133                     break;
  2134                 }
  2135                 case SQL_C_TYPE_TIMESTAMP: {
! 2136                     LOG("BindParameterArray: Binding SQL_C_TYPE_TIMESTAMP "
! 2137                         "array - param_index=%d, count=%zu",
! 2138                         paramIndex, paramSetSize);
! 2139                     SQL_TIMESTAMP_STRUCT* tsArray =
! 2140                         AllocateParamBufferArray<SQL_TIMESTAMP_STRUCT>(tempBuffers, paramSetSize);
  2141                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2142                     for (size_t i = 0; i < paramSetSize; ++i) {
  2143                         if (columnValues[i].is_none()) {
  2144                             strLenOrIndArray[i] = SQL_NULL_DATA;

Lines 2150-2165

  2150                             tsArray[i].day = dtObj.attr("day").cast<SQLUSMALLINT>();
  2151                             tsArray[i].hour = dtObj.attr("hour").cast<SQLUSMALLINT>();
  2152                             tsArray[i].minute = dtObj.attr("minute").cast<SQLUSMALLINT>();
  2153                             tsArray[i].second = dtObj.attr("second").cast<SQLUSMALLINT>();
! 2154                             tsArray[i].fraction = static_cast<SQLUINTEGER>(
! 2155                                 dtObj.attr("microsecond").cast<int>() * 1000);  // µs to ns
  2156                             strLenOrIndArray[i] = 0;
  2157                         }
  2158                     }
! 2159                     LOG("BindParameterArray: SQL_C_TYPE_TIMESTAMP bound - "
! 2160                         "param_index=%d",
! 2161                         paramIndex);
  2162                     dataPtr = tsArray;
  2163                     bufferLength = sizeof(SQL_TIMESTAMP_STRUCT);
  2164                     break;
  2165                 }

Lines 2180-2196

  2180                             std::memset(&dtoArray[i], 0, sizeof(DateTimeOffset));
  2181                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2182                         } else {
  2183                             if (!py::isinstance(param, datetimeType)) {
! 2184                                 ThrowStdException(
! 2185                                     MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2186                             }
  2187 
  2188                             py::object tzinfo = param.attr("tzinfo");
  2189                             if (tzinfo.is_none()) {
! 2190                                 ThrowStdException("Datetime object must have tzinfo for "
! 2191                                                   "SQL_C_SS_TIMESTAMPOFFSET at paramIndex " +
! 2192                                                   std::to_string(paramIndex));
  2193                             }
  2194 
  2195                             // Populate the C++ struct directly from the Python
  2196                             // datetime object.

Lines 2230-2242

  2230                     bufferLength = sizeof(DateTimeOffset);
  2231                     break;
  2232                 }
  2233                 case SQL_C_NUMERIC: {
! 2234                     LOG("BindParameterArray: Binding SQL_C_NUMERIC array - "
! 2235                         "param_index=%d, count=%zu",
! 2236                         paramIndex, paramSetSize);
! 2237                     SQL_NUMERIC_STRUCT* numericArray =
! 2238                         AllocateParamBufferArray<SQL_NUMERIC_STRUCT>(tempBuffers, paramSetSize);
  2239                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2240                     for (size_t i = 0; i < paramSetSize; ++i) {
  2241                         const py::handle& element = columnValues[i];
  2242                         if (element.is_none()) {

Lines 2244-2263

  2244                             std::memset(&numericArray[i], 0, sizeof(SQL_NUMERIC_STRUCT));
  2245                             continue;
  2246                         }
  2247                         if (!py::isinstance<NumericData>(element)) {
! 2248                             LOG("BindParameterArray: NUMERIC type mismatch - "
! 2249                                 "param_index=%d, row=%zu",
! 2250                                 paramIndex, i);
! 2251                             throw std::runtime_error(
! 2252                                 MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2253                         }
  2254                         NumericData decimalParam = element.cast<NumericData>();
! 2255                         LOG("BindParameterArray: NUMERIC value - "
! 2256                             "param_index=%d, row=%zu, precision=%d, scale=%d, "
! 2257                             "sign=%d",
! 2258                             paramIndex, i, decimalParam.precision, decimalParam.scale,
! 2259                             decimalParam.sign);
  2260                         SQL_NUMERIC_STRUCT& target = numericArray[i];
  2261                         std::memset(&target, 0, sizeof(SQL_NUMERIC_STRUCT));
  2262                         target.precision = decimalParam.precision;
  2263                         target.scale = decimalParam.scale;

Lines 2267-2277

  2267                             std::memcpy(target.val, decimalParam.val.data(), copyLen);
  2268                         }
  2269                         strLenOrIndArray[i] = sizeof(SQL_NUMERIC_STRUCT);
  2270                     }
! 2271                     LOG("BindParameterArray: SQL_C_NUMERIC bound - "
! 2272                         "param_index=%d",
! 2273                         paramIndex);
  2274                     dataPtr = numericArray;
  2275                     bufferLength = sizeof(SQL_NUMERIC_STRUCT);
  2276                     break;
  2277                 }

Lines 2298-2311

  2298                             continue;
  2299                         } else if (py::isinstance<py::bytes>(element)) {
  2300                             py::bytes b = element.cast<py::bytes>();
  2301                             if (PyBytes_GET_SIZE(b.ptr()) != 16) {
! 2302                                 LOG("BindParameterArray: GUID bytes wrong "
! 2303                                     "length - param_index=%d, row=%zu, "
! 2304                                     "length=%d",
! 2305                                     paramIndex, i, PyBytes_GET_SIZE(b.ptr()));
! 2306                                 ThrowStdException("UUID binary data must be "
! 2307                                                   "exactly 16 bytes long.");
  2308                             }
  2309                             std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
  2310                         } else if (py::isinstance(element, uuid_class)) {
  2311                             py::bytes b = element.attr("bytes_le").cast<py::bytes>();

Lines 2310-2322

  2310                         } else if (py::isinstance(element, uuid_class)) {
  2311                             py::bytes b = element.attr("bytes_le").cast<py::bytes>();
  2312                             std::memcpy(uuid_bytes.data(), PyBytes_AS_STRING(b.ptr()), 16);
  2313                         } else {
! 2314                             LOG("BindParameterArray: GUID type mismatch - "
! 2315                                 "param_index=%d, row=%zu",
! 2316                                 paramIndex, i);
! 2317                             ThrowStdException(
! 2318                                 MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2319                         }
  2320                         guidArray[i].Data1 = (static_cast<uint32_t>(uuid_bytes[3]) << 24) |
  2321                                              (static_cast<uint32_t>(uuid_bytes[2]) << 16) |
  2322                                              (static_cast<uint32_t>(uuid_bytes[1]) << 8) |

Lines 2335-2347

  2335                     bufferLength = sizeof(SQLGUID);
  2336                     break;
  2337                 }
  2338                 default: {
! 2339                     LOG("BindParameterArray: Unsupported C type - "
! 2340                         "param_index=%d, C_type=%d",
! 2341                         paramIndex, info.paramCType);
! 2342                     ThrowStdException("BindParameterArray: Unsupported C type: " +
! 2343                                       std::to_string(info.paramCType));
  2344                 }
  2345             }
  2346             LOG("BindParameterArray: Calling SQLBindParameter - "
  2347                 "param_index=%d, buffer_length=%lld",

Lines 2352-2368

  2352                                      static_cast<SQLSMALLINT>(info.paramCType),
  2353                                      static_cast<SQLSMALLINT>(info.paramSQLType), info.columnSize,
  2354                                      info.decimalDigits, dataPtr, bufferLength, strLenOrIndArray);
  2355             if (!SQL_SUCCEEDED(rc)) {
! 2356                 LOG("BindParameterArray: SQLBindParameter failed - "
! 2357                     "param_index=%d, SQLRETURN=%d",
! 2358                     paramIndex, rc);
  2359                 return rc;
  2360             }
  2361         }
  2362     } catch (...) {
! 2363         LOG("BindParameterArray: Exception during binding, cleaning up "
! 2364             "buffers");
  2365         throw;
  2366     }
  2367     paramBuffers.insert(paramBuffers.end(), tempBuffers.begin(), tempBuffers.end());
  2368     LOG("BindParameterArray: Successfully bound all parameters - "

Lines 2423-2432

  2423         rc = SQLExecute_ptr(hStmt);
  2424         LOG("SQLExecuteMany: SQLExecute completed - rc=%d", rc);
  2425         return rc;
  2426     } else {
! 2427         LOG("SQLExecuteMany: Using DAE (data-at-execution) - row_count=%zu",
! 2428             columnwise_params.size());
  2429         size_t rowCount = columnwise_params.size();
  2430         for (size_t rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
  2431             LOG("SQLExecuteMany: Processing DAE row %zu of %zu", rowIndex + 1, rowCount);
  2432             py::list rowParams = columnwise_params[rowIndex];

Lines 2431-2440

  2431             LOG("SQLExecuteMany: Processing DAE row %zu of %zu", rowIndex + 1, rowCount);
  2432             py::list rowParams = columnwise_params[rowIndex];
  2433 
  2434             std::vector<std::shared_ptr<void>> paramBuffers;
! 2435             rc = BindParameters(hStmt, rowParams, const_cast<std::vector<ParamInfo>&>(paramInfos),
! 2436                                 paramBuffers);
  2437             if (!SQL_SUCCEEDED(rc)) {
  2438                 LOG("SQLExecuteMany: BindParameters failed for row %zu - rc=%d", rowIndex, rc);
  2439                 return rc;
  2440             }

Lines 2445-2459

  2445             size_t dae_chunk_count = 0;
  2446             while (rc == SQL_NEED_DATA) {
  2447                 SQLPOINTER token;
  2448                 rc = SQLParamData_ptr(hStmt, &token);
! 2449                 LOG("SQLExecuteMany: SQLParamData called - chunk=%zu, rc=%d, "
! 2450                     "token=%p",
! 2451                     dae_chunk_count, rc, token);
  2452                 if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2453                     LOG("SQLExecuteMany: SQLParamData failed - chunk=%zu, "
! 2454                         "rc=%d",
! 2455                         dae_chunk_count, rc);
  2456                     return rc;
  2457                 }
  2458 
  2459                 py::object* py_obj_ptr = reinterpret_cast<py::object*>(token);

Lines 2464-2492

  2464 
  2465                 if (py::isinstance<py::str>(*py_obj_ptr)) {
  2466                     std::string data = py_obj_ptr->cast<std::string>();
  2467                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2468                     LOG("SQLExecuteMany: Sending string DAE data - chunk=%zu, "
! 2469                         "length=%lld",
! 2470                         dae_chunk_count, static_cast<long long>(data_len));
  2471                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
  2472                     if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2473                         LOG("SQLExecuteMany: SQLPutData(string) failed - "
! 2474                             "chunk=%zu, rc=%d",
! 2475                             dae_chunk_count, rc);
  2476                     }
! 2477                 } else if (py::isinstance<py::bytes>(*py_obj_ptr) ||
! 2478                            py::isinstance<py::bytearray>(*py_obj_ptr)) {
  2479                     std::string data = py_obj_ptr->cast<std::string>();
  2480                     SQLLEN data_len = static_cast<SQLLEN>(data.size());
! 2481                     LOG("SQLExecuteMany: Sending bytes/bytearray DAE data - "
! 2482                         "chunk=%zu, length=%lld",
! 2483                         dae_chunk_count, static_cast<long long>(data_len));
  2484                     rc = SQLPutData_ptr(hStmt, (SQLPOINTER)data.c_str(), data_len);
  2485                     if (!SQL_SUCCEEDED(rc) && rc != SQL_NEED_DATA) {
! 2486                         LOG("SQLExecuteMany: SQLPutData(bytes) failed - "
! 2487                             "chunk=%zu, rc=%d",
! 2488                             dae_chunk_count, rc);
  2489                     }
  2490                 } else {
  2491                     LOG("SQLExecuteMany: Unsupported DAE data type - chunk=%zu", dae_chunk_count);
  2492                     return SQL_ERROR;

Lines 2492-2502

  2492                     return SQL_ERROR;
  2493                 }
  2494                 dae_chunk_count++;
  2495             }
! 2496             LOG("SQLExecuteMany: DAE completed for row %zu - total_chunks=%zu, "
! 2497                 "final_rc=%d",
! 2498                 rowIndex, dae_chunk_count, rc);
  2499 
  2500             if (!SQL_SUCCEEDED(rc)) {
  2501                 LOG("SQLExecuteMany: DAE row %zu failed - rc=%d", rowIndex, rc);
  2502                 return rc;

Lines 2501-2511

  2501                 LOG("SQLExecuteMany: DAE row %zu failed - rc=%d", rowIndex, rc);
  2502                 return rc;
  2503             }
  2504         }
! 2505         LOG("SQLExecuteMany: All DAE rows processed successfully - "
! 2506             "total_rows=%zu",
! 2507             rowCount);
  2508         return SQL_SUCCESS;
  2509     }
  2510 }

Lines 2514-2523

  2514     LOG("SQLNumResultCols: Getting number of columns in result set for "
  2515         "statement_handle=%p",
  2516         (void*)statementHandle->get());
  2517     if (!SQLNumResultCols_ptr) {
! 2518         LOG("SQLNumResultCols: Function pointer not initialized, loading "
! 2519             "driver");
  2520         DriverLoader::getInstance().loadDriver();  // Load the driver
  2521     }
  2522 
  2523     SQLSMALLINT columnCount;

Lines 2631-2640

  2631         ret = SQLGetData_ptr(hStmt, colIndex, cType, chunk.data(), DAE_CHUNK_SIZE, &actualRead);
  2632 
  2633         if (ret == SQL_ERROR || !SQL_SUCCEEDED(ret) && ret != SQL_SUCCESS_WITH_INFO) {
  2634             std::ostringstream oss;
! 2635             oss << "Error fetching LOB for column " << colIndex << ", cType=" << cType
! 2636                 << ", loop=" << loopCount << ", SQLGetData return=" << ret;
  2637             LOG("FetchLobColumnData: %s", oss.str().c_str());
  2638             ThrowStdException(oss.str());
  2639         }
  2640         if (actualRead == SQL_NULL_DATA) {

Lines 2757-2767

  2757 
  2758         ret = SQLDescribeCol_ptr(hStmt, i, columnName, sizeof(columnName) / sizeof(SQLWCHAR),
  2759                                  &columnNameLen, &dataType, &columnSize, &decimalDigits, &nullable);
  2760         if (!SQL_SUCCEEDED(ret)) {
! 2761             LOG("SQLGetData: Error retrieving metadata for column %d - "
! 2762                 "SQLDescribeCol SQLRETURN=%d",
! 2763                 i, ret);
  2764             row.append(py::none());
  2765             continue;
  2766         }

Lines 2794-2804

  2794                                 row.append(std::string(reinterpret_cast<char*>(dataBuffer.data())));
  2795 #endif
  2796                             } else {
  2797                                 // Buffer too small, fallback to streaming
! 2798                                 LOG("SQLGetData: CHAR column %d data truncated "
! 2799                                     "(buffer_size=%zu), using streaming LOB",
! 2800                                     i, dataBuffer.size());
  2801                                 row.append(FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false));
  2802                             }
  2803                         } else if (dataLen == SQL_NULL_DATA) {
  2804                             LOG("SQLGetData: Column %d is NULL (CHAR)", i);

Lines 2805-2828

  2805                             row.append(py::none());
  2806                         } else if (dataLen == 0) {
  2807                             row.append(py::str(""));
  2808                         } else if (dataLen == SQL_NO_TOTAL) {
! 2809                             LOG("SQLGetData: Cannot determine data length "
! 2810                                 "(SQL_NO_TOTAL) for column %d (SQL_CHAR), "
! 2811                                 "returning NULL",
! 2812                                 i);
  2813                             row.append(py::none());
  2814                         } else if (dataLen < 0) {
! 2815                             LOG("SQLGetData: Unexpected negative data length "
! 2816                                 "for column %d - dataType=%d, dataLen=%ld",
! 2817                                 i, dataType, (long)dataLen);
! 2818                             ThrowStdException("SQLGetData returned an unexpected negative "
! 2819                                               "data length");
  2820                         }
  2821                     } else {
! 2822                         LOG("SQLGetData: Error retrieving data for column %d "
! 2823                             "(SQL_CHAR) - SQLRETURN=%d, returning NULL",
! 2824                             i, ret);
  2825                         row.append(py::none());
  2826                     }
  2827                 }
  2828                 break;

Lines 2875-2898

  2875                             row.append(py::none());
  2876                         } else if (dataLen == 0) {
  2877                             row.append(py::str(""));
  2878                         } else if (dataLen == SQL_NO_TOTAL) {
! 2879                             LOG("SQLGetData: Cannot determine NVARCHAR data "
! 2880                                 "length (SQL_NO_TOTAL) for column %d, "
! 2881                                 "returning NULL",
! 2882                                 i);
  2883                             row.append(py::none());
  2884                         } else if (dataLen < 0) {
! 2885                             LOG("SQLGetData: Unexpected negative data length "
! 2886                                 "for column %d (NVARCHAR) - dataLen=%ld",
! 2887                                 i, (long)dataLen);
! 2888                             ThrowStdException("SQLGetData returned an unexpected negative "
! 2889                                               "data length");
  2890                         }
  2891                     } else {
! 2892                         LOG("SQLGetData: Error retrieving data for column %d "
! 2893                             "(NVARCHAR) - SQLRETURN=%d",
! 2894                             i, ret);
  2895                         row.append(py::none());
  2896                     }
  2897                 }
  2898                 break;

Lines 2912-2922

  2912                 ret = SQLGetData_ptr(hStmt, i, SQL_C_SHORT, &smallIntValue, 0, NULL);
  2913                 if (SQL_SUCCEEDED(ret)) {
  2914                     row.append(static_cast<int>(smallIntValue));
  2915                 } else {
! 2916                     LOG("SQLGetData: Error retrieving SQL_SMALLINT for column "
! 2917                         "%d - SQLRETURN=%d",
! 2918                         i, ret);
  2919                     row.append(py::none());
  2920                 }
  2921                 break;
  2922             }

Lines 2925-2935

  2925                 ret = SQLGetData_ptr(hStmt, i, SQL_C_FLOAT, &realValue, 0, NULL);
  2926                 if (SQL_SUCCEEDED(ret)) {
  2927                     row.append(realValue);
  2928                 } else {
! 2929                     LOG("SQLGetData: Error retrieving SQL_REAL for column %d - "
! 2930                         "SQLRETURN=%d",
! 2931                         i, ret);
  2932                     row.append(py::none());
  2933                 }
  2934                 break;
  2935             }

Lines 2977-2993

  2977                             PythonObjectCache::get_decimal_class()(py::str(cnum, safeLen));
  2978                         row.append(decimalObj);
  2979                     } catch (const py::error_already_set& e) {
  2980                         // If conversion fails, append None
! 2981                         LOG("SQLGetData: Error converting to decimal for "
! 2982                             "column %d - %s",
! 2983                             i, e.what());
  2984                         row.append(py::none());
  2985                     }
  2986                 } else {
! 2987                     LOG("SQLGetData: Error retrieving SQL_NUMERIC/DECIMAL for "
! 2988                         "column %d - SQLRETURN=%d",
! 2989                         i, ret);
  2990                     row.append(py::none());
  2991                 }
  2992                 break;
  2993             }

Lines 2998-3008

  2998                 ret = SQLGetData_ptr(hStmt, i, SQL_C_DOUBLE, &doubleValue, 0, NULL);
  2999                 if (SQL_SUCCEEDED(ret)) {
  3000                     row.append(doubleValue);
  3001                 } else {
! 3002                     LOG("SQLGetData: Error retrieving SQL_DOUBLE/FLOAT for "
! 3003                         "column %d - SQLRETURN=%d",
! 3004                         i, ret);
  3005                     row.append(py::none());
  3006                 }
  3007                 break;
  3008             }

Lines 3011-3021

  3011                 ret = SQLGetData_ptr(hStmt, i, SQL_C_SBIGINT, &bigintValue, 0, NULL);
  3012                 if (SQL_SUCCEEDED(ret)) {
  3013                     row.append(static_cast<long long>(bigintValue));
  3014                 } else {
! 3015                     LOG("SQLGetData: Error retrieving SQL_BIGINT for column %d "
! 3016                         "- SQLRETURN=%d",
! 3017                         i, ret);
  3018                     row.append(py::none());
  3019                 }
  3020                 break;
  3021             }

Lines 3040-3050

  3040                 if (SQL_SUCCEEDED(ret)) {
  3041                     row.append(PythonObjectCache::get_time_class()(timeValue.hour, timeValue.minute,
  3042                                                                    timeValue.second));
  3043                 } else {
! 3044                     LOG("SQLGetData: Error retrieving SQL_TYPE_TIME for column "
! 3045                         "%d - SQLRETURN=%d",
! 3046                         i, ret);
  3047                     row.append(py::none());
  3048                 }
  3049                 break;
  3050             }

Lines 3060-3070

  3060                         timestampValue.hour, timestampValue.minute, timestampValue.second,
  3061                         timestampValue.fraction / 1000  // Convert back ns to µs
  3062                         ));
  3063                 } else {
! 3064                     LOG("SQLGetData: Error retrieving SQL_TYPE_TIMESTAMP for "
! 3065                         "column %d - SQLRETURN=%d",
! 3066                         i, ret);
  3067                     row.append(py::none());
  3068                 }
  3069                 break;
  3070             }

Lines 3084-3093

  3084                     int totalMinutes = dtoValue.timezone_hour * 60 + dtoValue.timezone_minute;
  3085                     // Validating offset
  3086                     if (totalMinutes < -24 * 60 || totalMinutes > 24 * 60) {
  3087                         std::ostringstream oss;
! 3088                         oss << "Invalid timezone offset from "
! 3089                                "SQL_SS_TIMESTAMPOFFSET_STRUCT: "
  3090                             << totalMinutes << " minutes for column " << i;
  3091                         ThrowStdException(oss.str());
  3092                     }
  3093                     // Convert fraction from ns to µs

Lines 3099-3109

  3099                         dtoValue.year, dtoValue.month, dtoValue.day, dtoValue.hour, dtoValue.minute,
  3100                         dtoValue.second, microseconds, tzinfo);
  3101                     row.append(py_dt);
  3102                 } else {
! 3103                     LOG("SQLGetData: Error fetching DATETIMEOFFSET for column "
! 3104                         "%d - SQLRETURN=%d, indicator=%ld",
! 3105                         i, ret, (long)indicator);
  3106                     row.append(py::none());
  3107                 }
  3108                 break;
  3109             }

Lines 3137-3154

  3137                         } else if (dataLen == 0) {
  3138                             row.append(py::bytes(""));
  3139                         } else {
  3140                             std::ostringstream oss;
! 3141                             oss << "Unexpected negative length (" << dataLen
! 3142                                 << ") returned by SQLGetData. ColumnID=" << i
! 3143                                 << ", dataType=" << dataType << ", bufferSize=" << columnSize;
  3144                             LOG("SQLGetData: %s", oss.str().c_str());
  3145                             ThrowStdException(oss.str());
  3146                         }
  3147                     } else {
! 3148                         LOG("SQLGetData: Error retrieving VARBINARY data for "
! 3149                             "column %d - SQLRETURN=%d",
! 3150                             i, ret);
  3151                         row.append(py::none());
  3152                     }
  3153                 }
  3154                 break;

Lines 3158-3168

  3158                 ret = SQLGetData_ptr(hStmt, i, SQL_C_TINYINT, &tinyIntValue, 0, NULL);
  3159                 if (SQL_SUCCEEDED(ret)) {
  3160                     row.append(static_cast<int>(tinyIntValue));
  3161                 } else {
! 3162                     LOG("SQLGetData: Error retrieving SQL_TINYINT for column "
! 3163                         "%d - SQLRETURN=%d",
! 3164                         i, ret);
  3165                     row.append(py::none());
  3166                 }
  3167                 break;
  3168             }

Lines 3171-3181

  3171                 ret = SQLGetData_ptr(hStmt, i, SQL_C_BIT, &bitValue, 0, NULL);
  3172                 if (SQL_SUCCEEDED(ret)) {
  3173                     row.append(static_cast<bool>(bitValue));
  3174                 } else {
! 3175                     LOG("SQLGetData: Error retrieving SQL_BIT for column %d - "
! 3176                         "SQLRETURN=%d",
! 3177                         i, ret);
  3178                     row.append(py::none());
  3179                 }
  3180                 break;
  3181             }

Lines 3204-3214

  3204                     row.append(uuid_obj);
  3205                 } else if (indicator == SQL_NULL_DATA) {
  3206                     row.append(py::none());
  3207                 } else {
! 3208                     LOG("SQLGetData: Error retrieving SQL_GUID for column %d - "
! 3209                         "SQLRETURN=%d, indicator=%ld",
! 3210                         i, ret, (long)indicator);
  3211                     row.append(py::none());
  3212                 }
  3213                 break;
  3214             }

Lines 3229-3238

  3229                               SQLLEN FetchOffset, py::list& row_data) {
  3230     LOG("SQLFetchScroll_wrap: Fetching with scroll orientation=%d, offset=%ld", FetchOrientation,
  3231         (long)FetchOffset);
  3232     if (!SQLFetchScroll_ptr) {
! 3233         LOG("SQLFetchScroll_wrap: Function pointer not initialized. Loading "
! 3234             "the driver.");
  3235         DriverLoader::getInstance().loadDriver();  // Load the driver
  3236     }
  3237 
  3238     // Unbind any columns from previous fetch operations to avoid memory

Lines 3532-3540

  3532         bool released;
  3533         RowGuard() : row(nullptr), released(false) {}
  3534         ~RowGuard() {
  3535             if (row && !released)
! 3536                 Py_DECREF(row);
  3537         }
  3538         void release() { released = true; }
  3539     };

Lines 3562-3572

  3562                 PyList_SET_ITEM(row, col - 1, Py_None);
  3563                 continue;
  3564             }
  3565             if (dataLen == SQL_NO_TOTAL) {
! 3566                 LOG("Cannot determine the length of the data. Returning NULL "
! 3567                     "value instead. Column ID - {}",
! 3568                     col);
  3569                 Py_INCREF(Py_None);
  3570                 PyList_SET_ITEM(row, col - 1, Py_None);
  3571                 continue;
  3572             }

Lines 3588-3598

  3588 
  3589             // Additional validation for complex types
  3590             if (dataLen == 0) {
  3591                 // Handle zero-length (non-NULL) data for complex types
! 3592                 LOG("Column data length is 0 for complex datatype. Setting "
! 3593                     "None to the result row. Column ID - {}",
! 3594                     col);
  3595                 Py_INCREF(Py_None);
  3596                 PyList_SET_ITEM(row, col - 1, Py_None);
  3597                 continue;
  3598             } else if (dataLen < 0) {

Lines 3597-3607

  3597                 continue;
  3598             } else if (dataLen < 0) {
  3599                 // Negative value is unexpected, log column index, SQL type &
  3600                 // raise exception
! 3601                 LOG("FetchBatchData: Unexpected negative data length - "
! 3602                     "column=%d, SQL_type=%d, dataLen=%ld",
! 3603                     col, dataType, (long)dataLen);
  3604                 ThrowStdException("Unexpected negative data length, check logs for details");
  3605             }
  3606             assert(dataLen > 0 && "Data length must be > 0");

Lines 3728-3737

  3728         // Row is now fully populated - add it to results list atomically
  3729         // This ensures no partially-filled rows exist in the list on exception
  3730         if (PyList_Append(rowsList, row) < 0) {
  3731             // RowGuard will clean up row automatically
! 3732             throw std::runtime_error("Failed to append row to results list - "
! 3733                                      "memory allocation failure");
  3734         }
  3735         // PyList_Append increments refcount, so we can release our reference
  3736         // Mark guard as released so destructor doesn't double-free
  3737         guard.release();

Lines 3879-3887

  3879             ret = SQLFetch_ptr(hStmt);
  3880             if (ret == SQL_NO_DATA)
  3881                 break;
  3882             if (!SQL_SUCCEEDED(ret))
! 3883                 return ret;
  3884 
  3885             py::list row;
  3886             SQLGetData_wrap(StatementHandle, numCols,
  3887                             row);  // <-- streams LOBs correctly

Lines 4009-4017

  4009             ret = SQLFetch_ptr(hStmt);
  4010             if (ret == SQL_NO_DATA)
  4011                 break;
  4012             if (!SQL_SUCCEEDED(ret))
! 4013                 return ret;
  4014 
  4015             py::list row;
  4016             SQLGetData_wrap(StatementHandle, numCols,
  4017                             row);  // <-- streams LOBs correctly

Lines 4084-4093

  4084 // Wrap SQLMoreResults
  4085 SQLRETURN SQLMoreResults_wrap(SqlHandlePtr StatementHandle) {
  4086     LOG("SQLMoreResults_wrap: Check for more results");
  4087     if (!SQLMoreResults_ptr) {
! 4088         LOG("SQLMoreResults_wrap: Function pointer not initialized. Loading "
! 4089             "the driver.");
  4090         DriverLoader::getInstance().loadDriver();  // Load the driver
  4091     }
  4092 
  4093     return SQLMoreResults_ptr(StatementHandle->get());

Lines 4096-4105

  4096 // Wrap SQLFreeHandle
  4097 SQLRETURN SQLFreeHandle_wrap(SQLSMALLINT HandleType, SqlHandlePtr Handle) {
  4098     LOG("SQLFreeHandle_wrap: Free SQL handle type=%d", HandleType);
  4099     if (!SQLAllocHandle_ptr) {
! 4100         LOG("SQLFreeHandle_wrap: Function pointer not initialized. Loading the "
! 4101             "driver.");
  4102         DriverLoader::getInstance().loadDriver();  // Load the driver
  4103     }
  4104 
  4105     SQLRETURN ret = SQLFreeHandle_ptr(HandleType, Handle->get());

Lines 4113-4122

  4113 // Wrap SQLRowCount
  4114 SQLLEN SQLRowCount_wrap(SqlHandlePtr StatementHandle) {
  4115     LOG("SQLRowCount_wrap: Get number of rows affected by last execute");
  4116     if (!SQLRowCount_ptr) {
! 4117         LOG("SQLRowCount_wrap: Function pointer not initialized. Loading the "
! 4118             "driver.");
  4119         DriverLoader::getInstance().loadDriver();  // Load the driver
  4120     }
  4121 
  4122     SQLLEN rowCount;

Lines 4237-4246

  4237           "Set the decimal separator character");
  4238     m.def(
  4239         "DDBCSQLSetStmtAttr",
  4240         [](SqlHandlePtr stmt, SQLINTEGER attr, SQLPOINTER value) {
! 4241             return SQLSetStmtAttr_ptr(stmt->get(), attr, value, 0);
! 4242         },
  4243         "Set statement attributes");
  4244     m.def("DDBCSQLGetTypeInfo", &SQLGetTypeInfo_Wrapper,
  4245           "Returns information about the data types that are supported by the "
  4246           "data source",

mssql_python/pybind/ddbc_bindings.h

  77 }
  78 
  79 inline std::wstring SQLWCHARToWString(const SQLWCHAR* sqlwStr, size_t length = SQL_NTS) {
  80     if (!sqlwStr)
! 81         return std::wstring();
  82     if (length == SQL_NTS) {
  83         size_t i = 0;
  84         while (sqlwStr[i] != 0)
  85             ++i;

  394 ErrorInfo SQLCheckError_Wrap(SQLSMALLINT handleType, SqlHandlePtr handle, SQLRETURN retcode);
  395 
  396 inline std::string WideToUTF8(const std::wstring& wstr) {
  397     if (wstr.empty())
! 398         return {};
  399 
  400 #if defined(_WIN32)
  401     int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast<int>(wstr.size()),
  402                                           nullptr, 0, nullptr, nullptr);

  442 }
  443 
  444 inline std::wstring Utf8ToWString(const std::string& str) {
  445     if (str.empty())
! 446         return {};
  447 #if defined(_WIN32)
  448     int size_needed =
  449         MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.size()), nullptr, 0);
  450     if (size_needed == 0) {

  717             PyList_SET_ITEM(row, col - 1, pyStr);
  718         }
  719     } else {
  720         // Slow path: LOB data requires separate fetch call
! 721         PyList_SET_ITEM(row, col - 1,
! 722                         FetchLobColumnData(hStmt, col, SQL_C_CHAR, false, false).release().ptr());
  723     }
  724 }
  725 
  726 // Process SQL NCHAR/NVARCHAR (wide/Unicode string) column into Python str

  784         }
  785 #endif
  786     } else {
  787         // Slow path: LOB data requires separate fetch call
! 788         PyList_SET_ITEM(row, col - 1,
! 789                         FetchLobColumnData(hStmt, col, SQL_C_WCHAR, true, false).release().ptr());
  790     }
  791 }
  792 
  793 // Process SQL BINARY/VARBINARY (binary data) column into Python bytes

  824             PyList_SET_ITEM(row, col - 1, pyBytes);
  825         }
  826     } else {
  827         // Slow path: LOB data requires separate fetch call
! 828         PyList_SET_ITEM(row, col - 1,
! 829                         FetchLobColumnData(hStmt, col, SQL_C_BINARY, false, true).release().ptr());
  830     }
  831 }
  832 
  833 }  // namespace ColumnProcessors

mssql_python/pybind/logger_bridge.cpp

Lines 174-184

  174     constexpr size_t MAX_LOG_SIZE = 4095;  // Keep same limit for consistency
  175     if (complete_message.size() > MAX_LOG_SIZE) {
  176         // Use stderr to notify about truncation (logging may be the truncated
  177         // call itself)
! 178         std::cerr << "[MSSQL-Python] Warning: Log message truncated from "
! 179                   << complete_message.size() << " bytes to " << MAX_LOG_SIZE << " bytes at " << file
! 180                   << ":" << line << std::endl;
  181         complete_message.resize(MAX_LOG_SIZE);
  182     }
  183 
  184     // Lock for Python call (minimize critical section)


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.row.py: 66.2%
mssql_python.pybind.ddbc_bindings.cpp: 67.1%
mssql_python.helpers.py: 67.5%
mssql_python.pybind.connection.connection.cpp: 74.7%
mssql_python.pybind.ddbc_bindings.h: 76.9%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 79.6%
mssql_python.connection.py: 82.5%
mssql_python.cursor.py: 83.6%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

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

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants