Release Notes - Version 1.6.0
Enhancements
• Connection String Sanitization Migrated to Parser-Based Logic (#522)
What changed: Replaced the regex-based sanitize_connection_string() implementation with a parser-based approach using _ConnectionStringParser. The function was moved to connection_string_parser.py where it naturally belongs alongside the parser it depends on; helpers.py retains a thin delegate for backward compatibility. connection.py now imports directly from connection_string_parser, eliminating the circular import between the two modules.
Who benefits: All applications that pass connection strings to the driver — especially those using braced values, escaped braces, or other ODBC-spec value formats that the previous regex could mis-parse or mis-sanitize.
Impact: Sanitization now correctly handles all ODBC connection string value formats including {...} braced values and escaped braces per the ODBC spec; eliminates edge cases where the regex-based approach could produce incorrect results.
PR #522
Bug Fixes
• GIL Release During Blocking ODBC Connect/Disconnect (#497)
What changed: The driver now releases the Python GIL during SQLDriverConnect and SQLDisconnect calls. These are blocking I/O operations (DNS resolution, TCP handshake, TLS negotiation, authentication) that previously serialized all Python threads. The connection pool lock structure was also restructured to separate mutex-protected bookkeeping from blocking ODBC calls, preventing a mutex/GIL lock-ordering deadlock.
Who benefits: Multi-threaded applications that establish or close connections concurrently; services with high connection throughput; any application using ThreadPoolExecutor or similar constructs to parallelise database work.
Impact: 10 threads establishing connections concurrently now achieve ~5.7× speedup over serial baseline (down from fully serialized at ~1×); eliminates GIL contention stalls for all concurrent connect/disconnect workloads.
• setinputsizes() Crash When Using SQL_DECIMAL Type Hints (#519)
What changed: Fixed a runtime error in cursor.setinputsizes() when SQL_DECIMAL or SQL_NUMERIC type hints were provided. Changed the C-type mapping for these SQL types from SQL_C_NUMERIC to SQL_C_CHAR, enabling string-based decimal binding. Updated _create_parameter_types_list and executemany to convert Python Decimal objects to strings when binding to DECIMAL/NUMERIC columns.
Who benefits: Applications that use setinputsizes() to pre-declare SQL_DECIMAL or SQL_NUMERIC parameter types; workflows using executemany with Decimal values and explicit type hints.
Impact: Eliminates the crash when SQL_DECIMAL is passed to setinputsizes(); Decimal values now bind correctly in both execute() and executemany() with or without setinputsizes(); NULL decimals are handled correctly.
• ODBC Catalog Method fetchone() Returning Incorrect Data (#520)
What changed: Fixed a bug where fetchone() on ODBC catalog method result sets (tables(), columns(), primaryKeys(), foreignKeys(), statistics(), procedures(), etc.) returned incorrect data. The root cause was that the rownumber attribute was not reset or initialized when fetching catalog results, corrupting row tracking and iteration state.
Who benefits: Applications that iterate catalog result sets using fetchone() or for row in cursor; developers using introspection APIs to discover schema metadata at runtime.
Impact: fetchone() now returns correct rows for all catalog methods; rownumber increments correctly throughout iteration; no errors are raised when consuming catalog results to exhaustion.
• cursor.execute() Raises Invalid Cursor State with reset_cursor=False (#521)
What changed: Fixed cursor.execute() raising an "Invalid cursor state" ODBC error when called with reset_cursor=False on a previously executed cursor. Added a call to hstmt.close_cursor() (which issues SQLFreeStmt(SQL_CLOSE)) before re-executing when reset_cursor=False, closing only the cursor without discarding the prepared statement plan. Exposed close_cursor as a new method on the C++ SqlHandle class via pybind11.
Who benefits: Applications that use reset_cursor=False to reuse prepared statement plans across executions for reduced per-call overhead; batch-processing workloads that prepare once and execute many times.
Impact: Prepared statements with reset_cursor=False now correctly reuse the plan across multiple executions; works for queries with and without parameters and when the previous result set was not fully consumed.
• executemany Type Annotation Does Not Accept Mapping Parameters (#525)
What changed: Updated the type hint for Cursor.executemany()'s seq_of_parameters argument in both cursor.py and mssql_python.pyi to accept Sequence[Mapping[str, Any]] in addition to Sequence[Sequence[Any]]. Added Mapping to the typing imports in both files.
Who benefits: Developers passing named parameters as dictionaries to executemany(); teams using static type checkers (mypy, Pylance, Pyright) that previously reported type errors for dict-based parameter sequences.
Impact: Eliminates type-checker errors when calling executemany() with a list of dict parameters; IDE autocompletion and inline type hints now accurately reflect the accepted parameter shapes.
• setup_logging Path Traversal Guard for log_file_path (#530)
What changed: Replaced _validate_log_file_extension() with a new _validate_log_file_path() method in logging.py that canonicalizes the path, rejects relative paths that traverse outside the current working directory, and validates the file extension. _setLevel() now uses the resolved canonical path returned by the validator rather than the raw user input.
Who benefits: All applications that configure a custom log_file_path via setup_logging(); security-conscious deployments that pass user-supplied or config-driven log paths.
Impact: Relative paths containing ../ traversal sequences are rejected with a clear error; absolute paths continue to work without restriction; log files are always written to the resolved canonical path, preventing directory traversal attacks.
PR #530