NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #2407 most downloaded on PyPI
A Python library for interacting with Microsoft SQL Server
Last release 23 days ago
11 Sep 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 16 of 16 stable releases
Nothing withdrawn
no release was ever pulled
10 months old
16 releases · first in 2025
One column per month.
• Faster setinputsizes() Execution
• Faster setinputsizes() Execution (#736)
What changed: Routed setinputsizes() parameter handling through the native C++ execution pipeline.
Who benefits: Applications with wide, batched, or frequently executed parameterized statements.
Impact: These workloads spend less time in Python-side parameter processing.
PR #736
• memoryview Support in Binary() (#741)
What changed: Extended Binary() to accept memoryview objects as binary inputs.
Who benefits: Applications working with zero-copy views and other buffer-protocol data sources.
Impact: Binary values can be passed without first converting a memoryview to bytes.
PR #741
• Module-Level SQL Server Type Constants (#764)
What changed: Exposed SQL Server-specific type constants directly from the mssql_python module.
Who benefits: Developers using SQL Server type metadata and parameter declarations.
Impact: Type constants are easier to discover and access through the public package API.
PR #764
• Concurrent Logging No Longer Deadlocks (#678)
What changed: Corrected GIL and mutex lock ordering in native logging paths used concurrently by multiple threads.
Who benefits: Multithreaded applications with driver logging enabled.
Impact: Concurrent logging no longer risks hanging the process.
PR #678
• Correct Rust Core in Windows ARM64 Wheels (#737)
What changed: Windows ARM64 wheels now vendor the matching ARM64 mssql_py_core binary.
Who benefits: Windows ARM64 users, especially applications using bulk copy.
Impact: Installed wheels contain architecture-compatible native components and can use bulk copy correctly.
PR #737
• Reliable Package-Local DLL Loading on Windows (#735)
What changed: Bundled Windows driver and authentication DLLs are loaded from package-local directories.
Who benefits: Windows applications deployed in environments with varying DLL search paths.
Impact: Native dependencies resolve reliably without relying on process-wide search-path configuration.
PR #735
• Consistent Decimal Parameter Binding (#742)
What changed: Decimal parameters are now bound as SQL_NUMERIC regardless of their runtime value.
Who benefits: Applications that send decimal values through parameterized queries or batches.
Impact: Decimal parameter typing remains stable across values and execution paths.
PR #742
• ODBC 3.x Parameter Types (#758)
What changed: Replaced obsolete ODBC 2.x parameter type identifiers with their ODBC 3.x equivalents.
Who benefits: Applications using typed parameters across current SQL Server driver configurations.
Impact: Parameter binding follows the current ODBC standard and avoids legacy type mismatches.
PR #758 - Thanks @Theekshna for the contribution!
• Database Name Metadata Is Decoded (#771)
What changed: Added decoding for the value returned by Connection.getinfo(SQL_DATABASE_NAME).
Who benefits: Applications that inspect the active database through connection metadata.
Impact: Database names are returned as correct Python text values.
PR #771
• Mixed Cursor Cleanup No Longer Crashes at Shutdown (#772)
What changed: Corrected native cleanup ordering for connections containing cursors in mixed lifecycle states.
Who benefits: Applications that create multiple cursors and allow some cursor cleanup to occur during shutdown.
Impact: Process shutdown completes without a native crash from mixed cursor cleanup.
PR #772
• Faster Parameter Detection and Execution
• Faster Parameter Detection and Execution (#549)
What changed: Parameter type detection and binding now run in a single native C++ pipeline, removing per-parameter Python and pybind11 overhead from the standard execute() path.
Who benefits: Applications with wide parameterized statements and batched execute workloads.
Impact: Parameter detection is approximately 60 times faster; benchmarks showed about 50% faster execution for statements with 50 or more parameters and 1.5-1.6 times higher end-to-end throughput in representative bulk insert scenarios.
• Bulk Copy Accepts timeout=0 (#698)
What changed: bulkcopy() now passes a zero timeout through as an unlimited timeout, matching the BCP API contract, while still rejecting negative, non-integer, and boolean values.
Who benefits: Users who explicitly disable bulk-copy timeouts with timeout=0.
Impact: The documented no-timeout behavior now works instead of raising a validation error.
• Arrow Reader Fetch Exceptions Are Preserved (#718)
What changed: Defensive Arrow cursor cleanup now checks cursor state before cleanup and no longer raises a secondary error that replaces the original fetch exception.
Who benefits: Applications that consume Arrow result batches and encounter a fetch failure.
Impact: Callers now receive the actual fetch error, making failures accurate and diagnosable.
• Decimal Conversion Errors No Longer Expose Parameter Values (#719)
What changed: executemany() Decimal conversion failures now report only row index, column index, and value type. Potentially value-bearing exception causes are suppressed so sensitive data cannot leak through chained tracebacks.
Who benefits: Applications that log or export database exceptions to monitoring and APM systems.
Impact: Invalid Decimal parameters remain diagnosable without exposing parameter rows, personally identifiable information, or secrets.
PR #719
• Arrow View Types Work in Bulk Copy (#729)
What changed: The bundled mssql_py_core 0.1.9 adds support for Arrow View types, with end-to-end coverage for Polars string_view columns passed directly through the Arrow C Data Interface.
Who benefits: Polars users and other Arrow producers that export variable-length View arrays to bulkcopy_arrow().
Impact: String View values and NULLs now round-trip correctly without requiring conversion through DataFrame.to_arrow().
PR #729 | GitHub Issue #708 - Thanks @gargsaumya for the contribution! (via
mssql_py_core)
• connect(timeout=) Sets the Login Timeout (#728)
What changed: The constructor timeout is now applied as SQL_ATTR_LOGIN_TIMEOUT instead of a per-statement query timeout; query timeout remains independently configurable through Connection.timeout, and both entry points validate values consistently.
Who benefits: Users who need connection attempts to fail within a predictable time while allowing long-running queries.
Impact: connect(timeout=N) now bounds login attempts as documented and no longer aborts queries after N seconds.
• Windows Extension Loading Uses Interpreter Architecture (#727)
What changed: On Windows, the native extension loader now derives architecture from the running Python interpreter instead of the host CPU and emits fallback notices as warnings rather than writing to stdout.
Who benefits: Users running x64 Python on Windows ARM64 hosts and tools that require clean stdout during import.
Impact: The loader selects the win_amd64 extension directly, avoiding repeated fallback loading and stray import output.
PR #727 | GitHub Issue #726 - Thanks @Om-singhaI for the contribution!
• ODBC Driver Now Ships Exclusively via mssql-python-odbc — Phase 2 Complete
• ODBC Driver Now Ships Exclusively via mssql-python-odbc — Phase 2 Complete (#693)
What changed: The bundled libs/ fallback introduced in v1.12.0 has been removed. mssql-python now hard-depends on mssql-python-odbc==18.6.2.1 (declared in install_requires); the native loader imports mssql_python_odbc at startup and resolves the driver / libs base directory from there. There is no in-wheel fallback anymore. pip install mssql-python continues to Just Work — the companion package is pulled transparently.
Who benefits: Users who wanted to pin or update driver binaries independently of the Python driver, redistributors who wanted a slimmer mssql-python wheel, and CI/reproducible builds that could not tolerate two distributions co-owning the libs/ directory.
Impact: Wheels are smaller and driver binaries are managed as an ordinary versioned dependency. If you install mssql-python from a private index or with --no-deps, install mssql-python-odbc==18.6.2.1 alongside it explicitly.
PR #693
• Apache Arrow Bulk Copy (#665)
What changed: New Cursor.bulkcopy_arrow(table_name, source) method for bulk loading directly from Apache Arrow sources — pyarrow.Table, pyarrow.RecordBatch, RecordBatchReader, or any object exposing the Arrow C Data Interface. The Arrow path skips the Python row materialization that classic bulkcopy() does, so ingestion of Arrow-native data is significantly faster. The classic bulkcopy() method now raises TypeError when given Arrow-shaped input and steers users to the new method. Connection context and Azure AD authentication were refactored into a shared _build_pycore_context() helper so bulkcopy and bulkcopy_arrow share identical auth handling.
Who benefits: Users with Arrow-native pipelines (Polars, DuckDB, Parquet, cross-language ETL) ingesting into SQL Server, and anyone who previously converted Arrow tables to lists of tuples just to call bulkcopy().
Impact: A fast, first-class Arrow ingestion API without leaving Arrow memory.
PR #665
• token_provider= Parameter for Azure Identity Credentials (#603)
What changed: connect() and the Connection class now accept a token_provider parameter — any object with a .get_token(scope) method. This includes every credential in azure-identity (DefaultAzureCredential, AzureCliCredential, ManagedIdentityCredential, ClientSecretCredential, …) as well as custom credential objects. Bulk copy operations re-acquire a fresh token from the provider per operation. token_provider is mutually exclusive with Authentication= in the connection string and with pre-acquired tokens in attrs_before. Only the Azure commercial cloud scope is supported via this path. A TokenProvider protocol type is exported for typing.
Who benefits: Applications that already build their credential chain with azure-identity (very common for Azure-hosted services) and want a single credential object to authenticate to SQL Server without hand-rolling ODBC SQL_COPT_SS_ACCESS_TOKEN handling.
Impact: Idiomatic Azure SDK auth for mssql-python.
• Identity-Aware Connection Pooling with Token-Expiry Refresh (#660)
What changed: The connection pool used to key only on the connection string, so two callers hitting the same server with different Entra identities collided onto one pool — a connection authenticated as user A could be handed to user B. The pool now keys on security context: for non-token auth (SQL / trusted / Service Principal / Windows-interactive) the key is unchanged; for token-based auth the key becomes connStr + "\x00" + <identity discriminator> where the discriminator is msi:<client_id> / msi:system for managed identity, acct:<home_account_id> for interactive/device-code, and tok:<sha256(token)> for DefaultAzureCredential / raw tokens. Two secondary fixes: token acquisition is now deferred to pool-misses (previously a token was acquired on every connect, even on pool hits), and pooled connections whose token is within 5 minutes of expiry are refreshed by minting a new token, byte-comparing against the pooled one, and reopening the slot when they differ. Idle identity-pools are reclaimed lazily to avoid leaks.
Who benefits: Multi-tenant services and any application that authenticates with more than one Entra identity in the same process.
Impact: Cross-identity connection leaks are prevented and token overhead on pool hits is eliminated.
• Silent Zero-Row executemany Batches on Late NULLs (#702)
What changed: Fixed the numeric array parameter binding paths (TINYINT, SMALLINT, INT, FLOAT) which allocated the ODBC indicator array only after encountering the first NULL. Earlier rows then contained uninitialized indicator slots that ODBC could interpret as data-at-execution markers, silently inserting zero rows for the entire batch without raising an exception. Indicators are now initialized for every fixed-width numeric parameter before array execution. Reproduction rate dropped from 209/300 anomalous batches to 0/300.
Who benefits: Any code path that uses executemany with mixed non-NULL / NULL numeric values.
Impact: Batches now insert every row deterministically.
• SQL_WVARCHAR Output Converter Applied as Catch-All to Non-String Columns (#692)
What changed: Cursor._build_converter_map used to fall back to the converter registered for SQL_WVARCHAR for any column that had no direct type-keyed converter, so registering a single SQL_WVARCHAR converter mangled INT / DECIMAL / DATE values as well. The optimized apply path also swallowed the resulting AttributeError, hiding the misbehavior in tests. The fallback is now gated on the column's mapped Python type being str or bytes, mirroring the guard already present in Row._apply_output_converters.
Who benefits: Users who register add_output_converter(SQL_WVARCHAR, ...) for legitimate string decoding without wanting it applied to numeric or date columns.
Impact: Output converters registered for SQL_WVARCHAR behave consistently with the documented and pyodbc-compatible semantics.
• Integer-Keyed Output Converters Silently Never Fired (#690)
What changed: Connection.add_output_converter(sqltype, func) is documented (and pyodbc accepts) to take an integer ODBC SQL type code as the key — for example SQL_DECIMAL, SQL_INTEGER. However, Cursor._build_converter_map() dispatched on the mapped Python type in cursor.description[i][1], so integer-keyed converters were stored but never invoked. The raw ODBC SQL type code is now captured in self._column_sql_types, and _build_converter_map dispatches in pyodbc-compatible order: (1) integer SQL type code, (2) Python type in description[i][1], (3) the legacy WVARCHAR catch-all. Integer keys use exact ODBC type matching, so SQL_DECIMAL and SQL_NUMERIC are distinct. Catalog metadata result sets (columns(), tables(), …) also build the converter map now.
Who benefits: Users migrating from pyodbc who register converters by SQL type code and expect them to fire, and anyone who needs distinct handling for SQL_DECIMAL vs SQL_NUMERIC or exact ODBC type dispatch.
Impact: add_output_converter now matches its documentation and pyodbc's behavior.
• RecordBatchReader.Close() for Arrow Result Sets (#644)
What changed: Cursor.arrow_reader() previously returned a raw pyarrow.RecordBatchReader, so closing the reader did not release the server-side cursor and the parent Cursor was left in an inconsistent state. It now returns an _ArrowReader wrapper whose .close() runs an 8-step cleanup: stops fetching, releases the server-side cursor, resets cursor state, and is idempotent. The parent Cursor remains usable after the reader is closed, and the wrapper supports use as a context manager.
Who benefits: Applications that fetch large result sets via Arrow and close early (streaming pipelines, request-scoped queries, long-running services that reuse cursors).
Impact: Predictable, prompt cleanup of server-side resources on Arrow reader close.
• AttributeError in Cursor.__del__ on Partially-Initialized Cursor (#646)
What changed: If Cursor.__init__ raised before self.closed and self.hstmt were set, garbage collection later called __del__ and hit an AttributeError that surfaced as an unraisable exception. __init__ now sets self.closed = False and self.hstmt = None as its first statements before any code that can raise, close() reads self.closed via getattr(self, "closed", True), and __del__ uses the correct sys.is_finalizing() (not sys._is_finalizing()) and guards its logging call so it stays safe during interpreter shutdown.
Who benefits: Anyone whose cursor construction path can fail (bad handle allocation, transient connection state) and did not want the failure to also produce noisy interpreter-shutdown errors.
Impact: Half-constructed cursors clean up quietly.
Impact: The Phase-2 split ships without a breaking change. In a future major release (v2.0.0) the bundled libs/ tree will be removed and mssql-python-…
• Standalone mssql-python-odbc Package for ODBC Driver Binaries (#663, #687)
What changed: The ODBC driver binaries that mssql-python needs at runtime are now also published as a separate, pure-data companion package called mssql-python-odbc (import name mssql_python_odbc, currently pinned to version 18.6.2). mssql-python declares mssql-python-odbc==18.6.2 in install_requires, so pip install mssql-python transparently pulls the driver package alongside it. The native loader prefers the external mssql_python_odbc package when it is present, and falls back to the ODBC driver binaries still bundled inside the mssql-python wheel when it is not — so existing installations keep working with no code changes. The fallback is GIL-safe and Alpine/musl-safe.
Who benefits: Users who want to keep driver binaries pinned or updated independently of the Python driver code, redistributors who want a slimmer mssql-python wheel over time, and anyone who was hitting duplicate-ownership issues from bundled ODBC files.
Impact: The Phase-2 split ships without a breaking change. In a future major release (v2.0.0) the bundled libs/ tree will be removed and mssql-python-odbc will become a hard dependency.
• Bulk Copy Connection Timeout Not Honored (#650)
What changed: cursor.bulkcopy() opens a separate connection through mssql_py_core, which previously defaulted to a hardcoded 15-second connect timeout with no way to override it from Python. The cursor's query timeout (set via connect(timeout=X)) is now forwarded into mssql_py_core's connect_timeout when it is set. timeout=0 is preserved as "no override" and leaves mssql_py_core on its 15s default. The cursor's timeout snapshot at the time of the bulkcopy() call is what is used — later changes to the parent connection do not affect an in-flight bulk copy.
Who benefits: Applications that call bulkcopy() against slow, throttled, or high-latency SQL Server endpoints (for example over VPN or in cross-region scenarios) and need a longer connect timeout, or that need to fail fast with a shorter one.
Impact: bulkcopy() now honors the same connect timeout as the rest of the driver.
• Bulk Copy Fails on Custom CLR UDT Columns (via mssql_py_core) (#688)
What changed: cursor.bulkcopy() into a column whose type is a custom, assembly-registered CLR UDT (any UDT other than the built-in geography / geometry / hierarchyid) previously failed with Protocol Error: Unsupported TDS type for bulk copy: 0xF0, because the wire path in the native core had no handler for the UDT (0xF0) type token when writing COLMETADATA. The Rust core now maps UDT columns to varbinary(max) on the wire and streams the supplied bytes as the UDT's serialized form (its IBinarySerialize payload), matching how pyodbc and python-tds load UDT columns. SQL Server materializes the UDT on insert. Delivered via the mssql_py_core 0.1.6 → 0.1.7 bump.
Who benefits: Users who bulk-load rows into tables with custom CLR UDT columns.
Impact: Bulk copy into custom CLR UDT columns now succeeds when the supplied values are the UDT's serialized bytes.
• SSH-tunnel / in-process forwarder deadlock
• SSH-tunnel / in-process forwarder deadlock (#604)
What changed: The driver now releases the GIL around blocking ODBC network round-trips that previously held it — the teardown path (SQLFreeHandle / SQLFreeStmt(SQL_CLOSE) when closing a statement with an open server-side cursor) and the SQLDescribeParam call issued for parametrized queries containing a None argument. The teardown release is guarded against interpreter finalization to stay safe during shutdown.
Who benefits: Users whose connection is routed through an in-process Python TCP forwarder (SSH-tunnel-style setups), where a GIL held across a blocking network syscall wedged the forwarder thread.
Impact: conn.close() / cursor.close() after a parametrized query, and parametrized queries with None values, no longer deadlock the interpreter.
• BINARY/VARBINARY NULL parameters in temp tables and table variables (#654)
What changed: Unknown NULL parameter types are now proactively resolved before any parameter is bound (for both single and batch execution paths). When SQLDescribeParam fails and the type falls back to SQL_VARCHAR, the driver emits a Python warning with concrete cursor.setinputsizes() guidance for binary columns.
Who benefits: Users inserting NULL values into BINARY / VARBINARY columns of temp tables or table variables, which previously hit driver errors or SQL Server implicit-conversion failures.
Impact: NULL binary parameters bind reliably, and when automatic resolution is not possible users get actionable guidance instead of an opaque error.
• Context manager transaction semantics (#639)
What changed: The Connection context manager (__exit__) now implements commit-on-success / rollback-on-exception semantics. With autocommit=False, a clean exit commits the transaction and an exception rolls it back; with autocommit=True behavior is unchanged. The connection is always closed on exit.
Who benefits: Users relying on with connection: blocks who previously saw every block roll back — because __exit__ only called close() regardless of how the block exited.
Impact: with blocks now persist committed work on clean exit and safely roll back on error, matching the documented behavior.
• macOS Apple Silicon import failure (#661)
What changed: configure_dylibs.sh now rewrites the bundled ODBC dylib dependencies to @loader_path for every architecture shipped in the universal2 wheel (arm64 and x86_64), not just the build host's arch. The committed arm64 and x86_64 dylibs were also re-fixed.
Who benefits: Apple Silicon users on a clean machine, where the arm64 driver previously pointed at an absolute Homebrew path (/opt/homebrew/lib/libodbcinst.2.dylib) that is absent until brew install unixodbc is run. Regressed in 1.8.0.
Impact: import mssql_python works out of the box on Apple Silicon with no separate unixODBC install.
• Service Principal bulk copy freeze (#666)
What changed: Fixed a GIL-deadlock in the Rust core that froze bulk copy when authenticating with a service principal. The fix lands in mssql_py_core and is picked up by bumping the bundled dependency from 0.1.5 to 0.1.6. (via mssql_py_core)
Who benefits: Users running bulkcopy with Authentication=ActiveDirectoryServicePrincipal, which previously froze mid-operation.
Impact: Service principal bulk copy completes without freezing.
• Active Directory Service Principal support for Bulk Copy
• Active Directory Service Principal support for Bulk Copy (#576)
What changed: bulkcopy now supports Authentication=ActiveDirectoryServicePrincipal. Because the tenant ID is only discoverable from the STS URL the server returns mid-handshake, the driver registers a token-provider callback (invoked with spn, sts_url, auth_method) that parses the tenant, builds a ClientSecretCredential, and returns a UTF-16LE encoded JWT.
Who benefits: Users who authenticate with a service principal (client ID + client secret) and need to perform bulk inserts against Azure SQL / SQL Server.
Impact: Service principal credentials now work for bulk copy operations, resolving the previous "Authentication method 'ActiveDirectoryServicePrincipal' is not supported. No token provider was registered for this method." error.
• Non-ASCII VARCHAR data in the Arrow fetch path (#575)
What changed: The Arrow fetch path now requests SQL_CHAR data as SQL_C_WCHAR (UTF-16LE) instead of relying on the narrow character path. This guarantees correct decoding regardless of encoding settings, locale, or operating system, with no significant performance impact.
Who benefits: Users fetching VARCHAR/CHAR columns containing non-ASCII data through the Arrow interface.
Impact: Arrow fetch methods now always return correct string data independent of the host environment.
PR #575 | GitHub Issue #553 - Thanks @ffelixg for the contribution!
• Bulk load connection timeouts (#641)
What changed: Fixed connection timeouts that occurred during bulk load operations. The fix lands in the Rust core and is picked up by bumping the bundled mssql_py_core dependency from 0.1.4 to 0.1.5. (via mssql_py_core)
Who benefits: Users running bulk load operations that previously hit intermittent connection timeouts.
Impact: Bulk load operations complete reliably without spurious connection timeouts.
• Accept Row objects in bulk copy
• Accept Row objects in bulk copy (#615)
What changed: bulkcopy now accepts Row objects and lists directly, converting each row to a tuple internally before passing data to the Rust backend, instead of requiring manually constructed tuples.
Who benefits: Anyone bulk-loading data that was fetched from a query (which returns Row objects) or assembled as lists.
Impact: Rows from a SELECT can be bulk-inserted straight into a target table without manual tuple conversion or type errors.
• Always statically link simdutf (#608)
What changed: Removed the find_package(simdutf) call so FetchContent is always used, building simdutf as a static library and embedding its symbols directly into the extension. Previously the macOS universal2 wheel dynamically linked simdutf against a Homebrew path baked in at CI build time.
Who benefits: macOS and Linux users installing the published wheels on machines without simdutf at the CI build path.
Impact: The driver imports successfully on a clean machine — no more missing-symbol / dlopen failures at runtime.
PR #608 | GitHub Issues #607, #628 - Thanks @edgarrmondragon for the contribution!
• Fix executemany SQL_C_NUMERIC mismatch for large decimals (#611)
What changed: executemany now overrides the C type to SQL_C_CHAR (string binding) for DECIMAL/NUMERIC parameters and adjusts column size to fit the longest string representation.
Who benefits: Users inserting Decimal values that exceed the SQL Server MONEY range via executemany.
Impact: Large decimal batch inserts (including NULLs and multi-column inserts) succeed instead of raising a runtime type-mismatch error.
• Fix incorrect type fallback for NULL parameters (#614)
What changed: Added a thread-safe per-statement cache for SQLDescribeParam results when binding None/NULL parameters, replacing the previous SQL_VARCHAR fallback that produced incorrect types. The cache is invalidated when a new statement is prepared.
Who benefits: Users binding NULL values, especially for all-NULL columns and VARBINARY types.
Impact: NULL parameters resolve to the correct type instead of a wrong SQL_VARCHAR fallback, and redundant server round-trips are eliminated.
• Fix exception pickle/unpickle round-trip (#616)
What changed: Added __reduce__ to ConnectionStringParseError and all DB-API exception subclasses so they serialize/deserialize correctly with all attributes preserved.
Who benefits: Users running across process boundaries — multiprocessing, distributed task queues, or anything that pickles exceptions.
Impact: Driver exceptions survive pickle/unpickle and deep copies without losing data or failing to reconstruct.
• Capture PRINT messages in nextset() (#618)
What changed: nextset() now captures diagnostic messages when SQL returns SQL_SUCCESS_WITH_INFO, so PRINT output from every result set is collected, not just the first.
Who benefits: Users running multi-statement batches or stored procedures that emit PRINT / informational messages across result sets.
Impact: All diagnostic messages are preserved as you advance through result sets with nextset().
• Handle Row objects in executemany DAE fallback path (#630)
What changed: executemany now converts Row objects to tuples before execution in the data-at-execution (DAE) fallback path, where _map_sql_type only recognized primitive types.
Who benefits: Users passing Row objects to executemany when writing to large columns such as varchar(max).
Impact: Writing fetched Row data to varchar(max) columns via executemany works instead of failing with a type error.
• Fix fetchone/fetchmany/fetchall type-checking under ty (#631)
What changed: Refactored catalog/metadata result-set handling to build a cached _column_map instead of dynamically reassigning fetchone, fetchmany, and fetchall as instance attributes.
Who benefits: Users running static type checkers such as ty against code using the driver.
Impact: Fetch methods remain proper class methods, so static type-checking no longer fails on cursor fetch calls.
• ActiveDirectoryMSI Support for Bulk Copy
• ActiveDirectoryMSI Support for Bulk Copy (#573)
What changed: Added Authentication=ActiveDirectoryMSI support to cursor.bulkcopy(), enabling both system-assigned (ManagedIdentityCredential()) and user-assigned (ManagedIdentityCredential(client_id=UID)) managed identity authentication. Credential kwargs are now threaded through the token acquisition path and persisted on the Connection to survive the bulk copy fresh-token flow.
Who benefits: Users running bulk copy operations from Azure-hosted services (VMs, App Service, Functions, AKS) that authenticate via Managed Identity.
Impact: Bulk copy now works with MSI authentication without workarounds. Previously only Default, DeviceCode, and Interactive auth methods were supported for bulk copy.
• Row String-Key Indexing (#589)
What changed: Row.__getitem__ now supports accessing values by column name as a string key (e.g., row["col"]), in addition to existing integer index and attribute access. Case-insensitive lookup is supported when the cursor's lowercase attribute is enabled.
Who benefits: All users who prefer dictionary-style access to result rows.
Impact: More intuitive row access patterns without needing to track column ordinal positions.
• Bundled ODBC Driver Upgrade (#569)
What changed: Updated the bundled Microsoft ODBC Driver for SQL Server from 18.5.1.1 to 18.6.2.1, picking up the latest fixes and improvements from the ODBC driver team.
Who benefits: All users — benefits from the latest ODBC driver stability and compatibility improvements.
Impact: Improved compatibility and stability from the latest ODBC driver release.
PR #569
• Deferred Connect-Attribute Use-After-Free (#596)
What changed: Fixed a use-after-free in Connection.setAttribute where deferred ODBC attributes (e.g., SQL_COPT_SS_ACCESS_TOKEN) were copied to stack-local buffers that were freed before the driver dereferenced them during SQLDriverConnect. Values are now stored in Connection-owned member buffers that remain valid for the lifetime of the connection.
Who benefits: Users authenticating with access tokens (Entra ID / AAD), especially on macOS arm64 and Azure SQL.
Impact: Eliminates SIGBUS crashes on macOS arm64, ProgrammingError: Authentication token is missing on Windows, and OperationalError: Communication link failure on Azure SQL when using token-based authentication.
• Connection String Parsed Multiple Times in Auth Path (#590)
What changed: Refactored authentication handling from string-based to dictionary-based parameter processing, eliminating redundant connection string parsing passes. _construct_connection_string now returns a normalized parameter dictionary alongside the string, removing the need for downstream re-parsing and ensuring robust sanitization of sensitive parameters before ODBC handoff.
Who benefits: All users using Entra ID / AAD authentication methods.
Impact: More reliable authentication with reduced overhead and cleaner sensitive-parameter sanitization.
• executemany Type Annotation Regression (#586)
What changed: Changed the seq_of_parameters type from invariant List[Sequence[Any]] to covariant Sequence[Sequence[Any]], fixing a mypy rejection of valid list[tuple[...]] arguments introduced in v1.6.0's dual paramstyle support. Runtime behaviour is unchanged.
Who benefits: Users running mypy or other static type checkers on code that calls Cursor.executemany.
Impact: executemany now correctly accepts list[tuple[...]], list[list[...]], and other sequence-of-sequences under strict type checking, matching PEP 249.
• RHEL 8 / glibc 2.28 Wheel Support
• RHEL 8 / glibc 2.28 Wheel Support (#548)
What changed: Added manylinux_2_28 build targets to the Linux wheel matrix, producing wheels compatible with glibc 2.28 environments. Previously only manylinux_2_34 wheels were published, which are incompatible with RHEL 8 and similar older distributions.
Who benefits: Users on Red Hat Enterprise Linux 8, CentOS Stream 8, AlmaLinux 8, Rocky Linux 8, and any other glibc 2.28 compatible distribution.
Impact: pip install mssql-python now succeeds on RHEL 8 and glibc 2.28 systems without requiring manual wheel selection.
• macOS Python 3.10 universal2 Wheel (#542)
What changed: Fixed a missing universal2 wheel for Python 3.10 on macOS. The build matrix was producing only x86_64 for Python 3.10 while all other versions received universal2 (Intel + Apple Silicon fat binary).
Who benefits: Python 3.10 users on Apple Silicon Macs (M1/M2/M3) who were previously forced to run under Rosetta 2 or install via x86_64 wheels.
Impact: Python 3.10 on macOS now installs and runs natively on Apple Silicon.
• UTF-16 String Handling via simdutf (#526)
What changed: Replaced the Python-level UTF-16 encode/decode path with simdutf and std::u16string, moving the conversion into the native layer to eliminate unnecessary Python object allocation and memory copies on every string round-trip.
Who benefits: Applications that read or write Unicode string columns (NVARCHAR, NCHAR, NTEXT) at high volume; workloads that are CPU-bound on string encoding.
Impact: Reduced per-row CPU cost for Unicode string columns; measurable throughput improvement in bulk-fetch and bulk-insert workloads with large string payloads.
PR #526
• execute() Hot Path Optimization (#528)
What changed: Optimized the execute() hot path with soft cursor reset (avoids full statement teardown when re-executing the same query), prepared statement caching (reuses previously compiled plans), and guarded diagnostics (defers ODBC diagnostic collection to failure paths only).
Who benefits: Applications that call execute() in a tight loop with the same or similar queries; ORM-heavy workloads; any high-frequency query pattern.
Impact: Reduced per-call overhead for repeat executions; lower CPU usage under high-frequency query workloads.
PR #528
• Login Failures Raised as mssql_python Exceptions (#562)
What changed: Fixed login and connection failures being surfaced as a generic RuntimeError instead of the appropriate mssql_python exception class (e.g. OperationalError). The error mapping in the connection path now correctly routes authentication and network failures through the DB-API 2.0 exception hierarchy.
Who benefits: All applications that catch specific mssql_python exceptions on connection; error-handling code that distinguishes OperationalError from other exception types.
Impact: except mssql_python.OperationalError now catches login failures as expected; no more bare RuntimeError leaking through on connection failure.
• GIL Released During All Blocking ODBC Calls (#541, #568)
What changed: Extended GIL release to cover all remaining blocking ODBC calls: SQLExecute, SQLFetch, SQLEndTran (commit/rollback), and SQLSetConnectAttr. Previously only connect/disconnect released the GIL, leaving execute, fetch, and transaction calls serializing all Python threads.
Who benefits: Multi-threaded applications using ThreadPoolExecutor, asyncio thread pools, or any concurrent query pattern; applications with long-running queries or large result sets.
Impact: All blocking ODBC I/O operations now release the GIL, enabling true Python thread concurrency during query execution and result fetching.
• executemany RuntimeError When Decimals Change Signs (#560)
What changed: Fixed a RuntimeError in executemany triggered when decimal values changed signs across rows (e.g. a positive decimal followed by a negative one in the same column). The sign change caused an incorrect C-type size calculation in the parameter binding path.
Who benefits: Applications that use executemany to insert or update rows containing Decimal columns where values span positive and negative ranges.
Impact: executemany now correctly handles sign changes in decimal columns without error.
• Inconsistent CP1252 Data in VARCHAR Columns — Windows vs Linux (#495)
What changed: Fixed a discrepancy where CP1252-encoded characters (e.g. €, ™, smart quotes) retrieved from VARCHAR columns returned correct values on Windows but garbled bytes on Linux. The Linux fetch path now applies the same CP1252 decoding as Windows.
Who benefits: Applications running on Linux that read legacy VARCHAR columns with CP1252/Windows-1252 collations; cross-platform deployments where result consistency between Windows and Linux is required.
Impact: CP1252 characters in VARCHAR columns now return identical values on Windows and Linux.
• cursor.bulkcopy() Fails on Empty String in NVARCHAR(MAX) (#559) (via mssql_py_core)
What changed: Fixed cursor.bulkcopy() raising SQL error 40197/4804 when any row in the batch contained an empty string "" destined for an NVARCHAR(MAX) or VARCHAR(MAX) column. The fix ships in mssql_py_core 0.1.4; empty strings are now treated as valid zero-length values consistent with NVARCHAR(n) behaviour.
Who benefits: Applications using cursor.bulkcopy() to insert data that may contain empty strings in MAX-length string columns.
Impact: Empty string values in NVARCHAR(MAX)/VARCHAR(MAX) columns no longer cause bulk copy failures.
• Connection String Sanitization Migrated to Parser-Based Logic
• 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
• 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
_What changed_: Added three new cursor methods — cursor.arrow(), cursor.arrow_batch(), and cursor.arrow_reader() — that convert SQL Server result sets
• Apache Arrow Fetch Support (#354)
What changed: Added three new cursor methods — cursor.arrow(), cursor.arrow_batch(), and cursor.arrow_reader() — that convert SQL Server result sets into Apache Arrow data structures using the Arrow C Data Interface. The implementation bypasses Python object creation in the hot path for improved performance.
Who benefits: Data engineers and analysts working with analytics stacks such as pandas, Polars, DuckDB, or any Arrow-native framework; developers building high-throughput ETL pipelines requiring efficient columnar data exchange.
Impact: Enables zero-copy, native-speed data transfer from SQL Server to Arrow ecosystems; provides multiple consumption patterns to suit batch workloads.
PR #354 | GitHub Issue #130 - Thanks @ffelixg for the contribution!
• sql_variant Type Support (#446)
What changed: Added native support for the SQL Server sql_variant type. The driver now detects sql_variant columns at fetch time, resolves their underlying base type, and returns correctly typed Python values. Updated constants, type validation, and C++ fetch routines to handle all valid sql_variant base types.
Who benefits: Applications querying tables that use sql_variant columns for flexible schema designs; developers migrating from other SQL Server drivers that support this type.
Impact: Eliminates unsupported type errors when querying sql_variant columns; ensures Python values match the actual stored base type. Note: sql_variant columns use a streaming fetch path, which may have a slight performance impact compared to fixed-type columns.
PR #446
• Native UUID Support (#463)
What changed: Added a native_uuid setting (configurable at module and per-connection scope) that controls whether UNIQUEIDENTIFIER columns are returned as uuid.UUID objects (default) or as pyodbc-compatible uppercase strings. The connect() API accepts a native_uuid parameter for per-connection overrides.
Who benefits: Developers who prefer working with Python's uuid.UUID type directly; teams migrating from pyodbc who need string UUID compatibility; applications requiring consistent UUID handling across connections.
Impact: Eliminates manual UUID string-to-object conversions; provides a clean migration path from pyodbc-style string UUIDs; configurable at module level or per-connection for incremental adoption.
• Row Class Public Export & Module API Improvements (#474)
What changed: The Row class is now exported at the top level of mssql_python. SQL type constants, GetInfo constants, and auth types are now dynamically exported from constants.py and importable directly from the package. Decimal separator logic was refactored into a dedicated decimal_config.py module.
Who benefits: Developers using type annotations that reference the Row class; users importing SQL type constants directly from the package; libraries building on top of mssql-python.
Impact: Enables from mssql_python import Row for type annotation use; reduces friction for users previously required to reach into internal modules; improves overall public API clarity.
• False Positive qmark Detection in SQL Parameters (#465)
What changed: Fixed a bug where ? characters inside bracketed identifiers (e.g., [column?name]), single-quoted string literals, double-quoted identifiers, single-line comments, and multi-line comments were incorrectly treated as parameter placeholders, triggering spurious parameter mismatch errors. Added context-aware SQL scanning logic that correctly skips all quoted contexts.
Who benefits: Developers writing queries with bracketed identifiers containing ?; applications using generated SQL that includes ? in comments or string literals.
Impact: Eliminates false positive parameter mismatch errors; enables use of SQL Server identifiers containing ? without workarounds.
PR # #465 | GitHub Issue #464
• NULL Parameter Type Mapping for VARBINARY Columns (#466)
What changed: Fixed a bug where inserting None (NULL) into a VARBINARY column raised an implicit conversion error from varchar to varbinary. NULL parameters were previously mapped to SQL_VARCHAR; changed to SQL_UNKNOWN_TYPE so the driver calls SQLDescribeParam to infer the correct target column type.
Who benefits: Applications that insert NULL values into VARBINARY or other binary columns; developers working with nullable binary data.
Impact: Eliminates implicit conversion errors for NULL VARBINARY parameters; ensures correct NULL binding for any column type without requiring explicit type hints.
• Stale Auth Fields in Bulk Copy EntraID Authentication (#488)
What changed: Fixed a bug in cursor.bulkcopy() where stale connection-string credential fields (authentication, user_name, password) were left in the py-core context after an Azure AD access token was acquired. py-core's validator rejected access_token when combined with those fields. The fix strips them after token acquisition. Affects ActiveDirectoryDefault, ActiveDirectoryInteractive, and ActiveDirectoryDeviceCode.
Who benefits: Applications using bulk copy with Azure AD authentication methods.
Impact: Enables bulk copy to function correctly with all Azure AD authentication flows; eliminates validation rejections from py-core when using access token authentication.
PR #488
• Credential Instance Cache for AAD Authentication (#483)
What changed: Fixed the AAD authentication flow to cache Azure Identity credential instances at module level rather than creating a new credential object on every token request. The cache is keyed by authentication type, protected by a lock for thread safety, and enables Azure Identity's built-in in-memory token cache. Who benefits: Applications making frequent connections with EntraID authentication; services with high connection throughput relying on Azure AD tokens. Impact: Reduces redundant token acquisition calls; enables token reuse through Azure Identity's in-memory cache; improves authentication performance for high-frequency connection scenarios.
• datetime.time Values Losing Microseconds (#479)
What changed: Fixed a long-standing bug where datetime.time values fetched from SQL Server TIME/TIME2 columns had their microseconds attribute incorrectly set to zero. The fix transitions TIME/TIME2 handling from native C-type structs to text-based representations with full microsecond precision for both parameter binding and result fetching.
Who benefits: Applications storing and retrieving high-precision time values; developers working with time series data or audit logs requiring sub-second precision.
Impact: datetime.time values now correctly preserve microseconds on insert and select round-trips; eliminates silent data loss for TIME(1)–TIME(7) columns.
• Arrow Fetch TIME Columns Missing Fractional Seconds (#499)
What changed: Fixed the Arrow fetch path to correctly include fractional seconds for SQL Server TIME columns. Arrow time columns are now represented as time64(ns) and include the fraction field from SQL_SS_TIME2_STRUCT in the computed nanosecond value.
Who benefits: Users of cursor.arrow(), cursor.arrow_batch(), and cursor.arrow_reader() fetching results containing TIME columns with sub-second precision.
Impact: Arrow fetches now correctly preserve TIME column fractional seconds; TIME(1)–TIME(7) precision is fully retained; the Arrow time type is pa.time64("ns").
PR #499 | GitHub Issue #498 - Thanks @ffelixg for the contribution!
• Explicit __all__ Exports from Main Module (#494)
What changed: Added an explicit __all__ list to init.py enumerating all public symbols. Any public object can now be imported directly from the root module without knowledge of the internal package structure, making the module fully mypy-compliant.
Who benefits: Developers using static type checkers (mypy, pylance, pyright); users following tutorial or documentation code snippets; libraries building on top of mssql-python.
Impact: Resolves mypy errors for direct imports from mssql_python; improves IDE autocompletion for module-level symbols; makes the public API surface explicit.
PR #494 | GitHub Issue #489 - Thanks @cepedus for the contribution!
Bulk Copy Support (#449, #430, #426, #420, #439)
Bulk Copy Support (#449, #430, #426, #420, #439)
What changed: The bulkcopy() method on the Cursor object is now a public API, providing high-performance bulk data loading into SQL Server. Added comprehensive logging support for bulk copy operations with configurable log levels. Enhanced type hints and explicit parameter definitions for better IDE support and type checking. This feature is powered by a new mssql-py-core native extension, please read here https://github.com/microsoft/mssql-rs.
Who benefits: Developers performing ETL operations, applications requiring high-throughput data imports, teams needing detailed bulk copy operation logging for debugging
Impact: Enables production use of high-performance bulk inserts, provides visibility into bulk copy operations through logging, ensures secure authentication with EntraID tokens, improves developer experience with better type hints and parameter documentation
Implements #449, #430, #426, #420, #439
Spatial Type Support (#423)
What changed: Added native support for SQL Server spatial data types including geography, geometry, and hierarchyid. These types are now automatically handled during query execution and result fetching, with proper Python type conversions. Includes comprehensive test coverage for spatial operations.
Who benefits: Applications working with geospatial data, GIS integrations, users storing location-based information, developers working with hierarchical data structures
Impact: Enables direct use of SQL Server spatial features from Python, eliminates need for manual type conversions, provides seamless integration with geospatial applications
Implements #423
Type Annotations with py.typed Marker (#367)
What changed: Added py.typed marker file to the package, enabling full type checking support for static type checkers like mypy, pylance, and pyright. This makes all type hints in the package discoverable and verifiable by type checking tools.
Who benefits: Developers using static type checkers, teams enforcing type safety in codebases, IDE users wanting better autocomplete and inline documentation
Impact: Enables compile-time type checking for mssql-python usage, improves IDE intelligence and error detection, enhances code quality through static analysis
Implements #367
VARCHAR Fetch Failure with Non-ASCII CP1252 Characters (#444)
What changed: Fixed a critical bug where fetching VARCHAR columns failed when the data length exactly equaled the column size and contained non-ASCII CP1252 characters (e.g., extended Latin characters like é, ñ, ü). The issue was caused by incorrect buffer size calculations for multi-byte character encodings. Who benefits: Applications storing international text data, databases with CP1252 collations, users in European and Latin American regions Impact: Prevents data fetch failures for international characters, ensures reliable VARCHAR handling across all character sets, eliminates data truncation issues
Fixes #444
Segmentation Fault with Interleaved Fetch Calls (#441)
What changed: Fixed a segmentation fault that occurred when interleaving calls to fetchmany() and fetchone() on the same cursor. The issue was caused by improper state management in the underlying result set iterator. Added comprehensive test coverage for various fetch operation sequences.
Who benefits: Applications using mixed fetch strategies, developers iterating through result sets with different batch sizes, tools dynamically adjusting fetch patterns
Impact: Eliminates crash risk when mixing fetch methods, ensures stable cursor iteration, improves overall driver reliability
Fixes #427, #441
Date/Time Type Code Alignment with ODBC 18 Driver (#355)
What changed: Aligned date and time type code mappings with the official ODBC 18 driver source code. This ensures consistent behavior when working with SQL Server date/time types (DATE, TIME, DATETIME2, DATETIMEOFFSET) across Python and other language drivers. Who benefits: Applications using temporal data types, developers migrating from ODBC-based solutions, cross-platform applications requiring consistent date/time handling Impact: Ensures accurate date/time type handling, eliminates subtle type conversion discrepancies, improves compatibility with other SQL Server drivers
Fixes #352, #355
Development Container Configuration (#147)
What changed: Added devcontainer configuration for VS Code, providing a complete, pre-configured development environment with all necessary dependencies, tools, and extensions. Includes Python environment setup, SQL Server instance configuration, and recommended VS Code extensions. Who benefits: New contributors setting up development environments, developers wanting consistent cross-platform setups, remote development scenarios Impact: Reduces environment setup time from hours to minutes, ensures consistent development environments across team, eliminates "works on my machine" issues
Implements #147
Segmentation Fault in libmsodbcsql-18.5 during SQLFreeHandle()
Segmentation Fault in libmsodbcsql-18.5 during SQLFreeHandle() (#415)
What changed: Fixed a segmentation fault that occurred in libmsodbcsql-18.5 when calling SQLFreeHandle() during connection cleanup. The issue was related to improper handle lifecycle management during driver shutdown. Who benefits: Applications experiencing crashes during connection cleanup, long-running applications with frequent connection cycling, multi-threaded applications managing multiple connections Impact: Eliminates driver crashes during connection teardown, improves stability for production workloads, ensures proper resource cleanup without segmentation faults
Fixes #341
_What changed_: Added a new closed property to the Connection class that returns True if close() was called, and False otherwise. This property provid
Connection.closed Property (#398)
What changed: Added a new closed property to the Connection class that returns True if close() was called, and False otherwise. This property provides a clear and explicit way to check if a connection has been closed. Comprehensive tests ensure correct behavior including idempotency when calling close() multiple times and proper state tracking with context managers.
Who benefits: Applications needing to check connection state, developers implementing connection lifecycle management, applications using context managers for database connections
Impact: Enables explicit connection state checking, improves API clarity for connection management, ensures consistent behavior with context manager usage
Fixes #394
Parameter as Dictionary - Pyformat Support (#385)
What changed: Introduced support for both qmark (?) and pyformat (%(name)s) parameter styles in SQL queries. Changed the default paramstyle from "qmark" to "pyformat". Added new parameter_helper.py module with helper functions to parse, detect, and convert parameter styles. Updated execute and executemany methods to auto-detect and convert parameter styles as needed.
Who benefits: Developers preferring named parameters, applications migrating from other database libraries, users wanting more readable SQL queries with named placeholders
Impact: Improves query readability with named parameters, provides seamless compatibility with both parameter styles, reduces migration effort from other Python database drivers
Fixes #20
Copilot Prompts for AI-Assisted Development (#393)
What changed: Added VS Code Copilot on-demand prompts in prompts to streamline developer workflows. Includes 4 prompts: setup-dev-env.prompt.md for environment setup, build-ddbc.prompt.md for rebuilding C++ extensions, run-tests.prompt.md for running pytest, and create-pr.prompt.md for creating well-structured PRs. Features agent mode, cross-referenced prompts, team-specific branch naming, and platform-specific guidance.
Who benefits: New contributors setting up the development environment, maintainers building and testing changes, developers creating pull requests
Impact: Reduces onboarding friction for new contributors, provides consistent guidance for common development tasks, improves PR quality with standardized templates
FetchMany Ignores Batch Size with LOB Columns (#346)
What changed: Fixed fetchmany(n) to respect the specified batch size when the result set contains LOB (Large Object) columns. Previously, the batch size parameter was ignored when LOB columns were present.
Who benefits: Applications fetching large result sets with LOB columns in batches, memory-constrained applications processing large text or binary data
Impact: Enables proper batch processing of LOB data, improves memory efficiency when handling large objects, ensures consistent behavior of fetchmany() across all column types
Fixes #345
Non-ASCII Path Resolution on Windows (#376)
What changed: Fixed path resolution for files with non-ASCII characters (e.g., Unicode, international characters) in Windows environments. Ensures proper handling of paths containing special characters in directory names or filenames. Who benefits: Users with non-English usernames or directory paths, applications deployed in internationalized environments, developers working with files containing Unicode characters in paths Impact: Enables driver usage in paths with international characters, resolves import and loading failures on localized Windows systems, improves cross-locale compatibility
SQL Server 2025 Test Support (#389)
What changed: Added support for testing against SQL Server 2025 across Windows, macOS, and Linux CI pipelines. Introduced new matrix configurations for SQL Server 2025 with Python 3.14 on Windows, added installation and setup scripts for SQL Server 2025 Express, and updated Docker commands to use matrix-provided SQL Server images on macOS and Linux. Enhanced test result publishing to include SQL Server version in run titles for better traceability. Who benefits: Maintainers validating compatibility with upcoming SQL Server releases, contributors ensuring cross-version compatibility, users planning to upgrade to SQL Server 2025 Impact: Ensures driver is validated against SQL Server 2025 before GA, improves release confidence for new SQL Server versions, provides early detection of compatibility issues
Forked PR Coverage Comment Workflow (#375)
What changed Implemented workflow_run solution for posting code coverage comments on pull requests from forked repositories. This enables coverage reporting for external contributors whose PRs originate from forks. Who benefits: External contributors submitting PRs from forked repositories, maintainers reviewing community contributions, open source collaboration workflows Impact: Provides visibility into code coverage for forked PRs, improves review process for external contributions, enhances community contribution experience
_What changed_: Replaced deprecated std::wstring_convert with optimized direct UTF-16 to UTF-32 conversion. Implemented explicit surrogate pair handli…
Thread-Safe Encoding/Decoding (#342)
What changed: Introduced re-entrant lock to protect encoding and decoding settings across all connection methods (setencoding, setdecoding, getencoding, getdecoding). Enforced strict validation allowing only utf-16le and utf-16be for SQL_WCHAR types, explicitly rejecting utf-16 with BOM due to byte order ambiguity. Added security validation to ensure encoding names contain only safe characters and reasonable lengths.
Who benefits: Multi-threaded applications with concurrent connections, applications processing Unicode data from SQL Server, security-conscious deployments preventing encoding-based attacks
Impact: Prevents race conditions in encoding/decoding configuration, eliminates encoding-related data corruption in concurrent scenarios, and blocks potential denial-of-service attacks through malicious encoding specifications
Fixes #250
Comprehensive Linting and Code Quality (#331)
What changed: Added GitHub Actions workflow for automated Python (flake8) and C++ (clang-format) linting. Introduced .flake8 and updated .clang-format configuration files. Applied comprehensive formatting to all Python and C++ files following project style guidelines.
Who benefits: All contributors, code reviewers, maintainers ensuring consistent code quality
Impact: Enforces consistent code style across the codebase, catches style violations early in CI, improves code readability and maintainability.
Fixes #22
Segmentation Fault on Linux During Garbage Collection (#361)
What changed: Fixed critical double-free issue in SqlHandle::free() by preventing handle cleanup during Python interpreter shutdown for both statement (SQL_HANDLE_STMT) and database connection (SQL_HANDLE_DBC) handles
Who benefits: All Linux users, long-running applications with frequent connection cycles, applications experiencing crashes during shutdown
Impact: Eliminates segmentation faults during Python garbage collection, improves application stability and reliability on Linux platforms
Fixes #341
Connection Pooling Isolation Level Leak (#343)
What changed: Transaction isolation level now explicitly reset to READ COMMITTED when pooled connections are reused. Added logic to Connection::reset() method to prevent isolation level settings from leaking between connection usages, addressing limitation of SQL_ATTR_RESET_CONNECTION which does not reset isolation level.
Who benefits: Applications using connection pooling with different isolation level requirements, multi-tenant applications sharing connection pools, systems requiring predictable transaction isolation behavior
Impact: Prevents unexpected transaction behavior from inherited isolation levels, ensures consistent database state across pooled connection reuse, eliminates hard-to-debug isolation level conflicts
Fixes #337
UTF-16 String Decoding from SQL Server (#340)
What changed: Enhanced getinfo()method to properly decode UTF-16LE strings from SQL Server with fallback to UTF-8 encoding. Added comprehensive test coverage for string encoding validation. Who benefits: Applications retrieving driver or connection metadata, systems processing non-ASCII characters in connection info, developers troubleshooting encoding issues Impact: Eliminates data corruption when retrieving string metadata from SQL Server, ensures proper character encoding across all platforms, prevents silent encoding failures
Fixes #318
Improved UTF-16/UTF-32 Conversion Performance (#365)
What changed: Replaced deprecated std::wstring_convert with optimized direct UTF-16 to UTF-32 conversion. Implemented explicit surrogate pair handling, removed intermediate buffers, and streamlined conversion logic for better performance and branch prediction. Added robust handling for invalid surrogate pairs and code points.
Who benefits: All macOS/Linux users processing Unicode data, applications handling characters outside Basic Multilingual Plane (BMP), performance-sensitive workloads
Impact: Greater than 10x performance improvement for UTF-8/16 conversions, eliminates deprecation warnings from modern compilers, improves robustness with malformed Unicode input
Connection String Escaping Rules (#364)
What changed: Fixed parser and builder to correctly handle ODBC connection string curly brace escaping rules. Only closing braces inside curlies require escaping (e.g., {pw}}d} for literal pw}d). Opening braces don't require escaping when wrapped in curlies.
Who benefits: Users with special characters in passwords or connection string values, applications migrating from other database drivers, developers troubleshooting connection string issues
Impact: Enables correct handling of passwords and values containing curly braces, aligns with official ODBC specification (MS-ODBCSTR), prevents connection failures due to incorrect escaping
Fixes #363
IntegrityError Detection with OUTPUT Clause (#338)
What changed: Fixed error handling in fetchall() method to properly check and handle errors from DDBCSQLFetchAll. Added explicit check_error call after fetch operation.
Who benefits: Applications using INSERT statements with OUTPUT clause and multiple VALUES entries, developers expecting proper IntegrityError exceptions on constraint violations
Impact: Ensures errors are properly detected and raised during batch inserts with OUTPUT clause, improves error handling reliability and debugging experience
Fixes #333
Query Timeout During Cursor Creation (#348)
What changed: Refactored timeout handling by introducing _set_timeout() method to set query timeout attribute during cursor initialization rather than on each execute() call. Centralizes timeout management in cursor lifecycle following performance best practices.
Who benefits: Applications with strict query timeout requirements, performance-sensitive workloads executing many queries, developers experiencing timeout-related issues
Impact: Improves consistency of timeout application, reduces overhead by setting timeout once during cursor creation, ensures timeout is active for entire cursor lifecycle
Fixes #291
NULL Parameter Array Handling (#332)
What changed: Added logic to BindParameterArray in ddbc_bindings.cpp to handle SQL_C_DEFAULT type for arrays containing only NULL values. Validates that all values are NULL and throws exception if any non-NULL value is detected. Added comprehensive test coverage.
Who benefits: Applications using executemany() with NULL values, batch insert operations with nullable columns, data migration scenarios
Impact: Enables correct insertion of rows with all NULL values via executemany(), prevents type inference errors, improves batch operation reliability
Sensitive Parameter Filtering (#368)
What changed: Updated remove_sensitive_params function in authentication module to exclude Trusted_Connection instead of Encrypt and TrustServerCertificate when filtering connection parameters
Who benefits: Applications using integrated authentication, security auditing systems, compliance frameworks tracking authentication methods
Impact: Correctly filters sensitive authentication parameters while preserving encryption settings, improves security parameter handling accuracy
Fixes #362
CMake Build Warnings and Errors (#353)
What changed: Enforced CMake warnings and deprecated features as errors (CMAKE_ERROR_DEPRECATED, CMAKE_WARN_DEPRECATED). Added strict compiler flags for GCC/Clang (-Werror, -Wattributes, -Wint-to-pointer-cast). Suppressed visibility attribute warnings for ParamInfo struct on Linux. Improved type casting safety in parameter binding using reinterpret_cast and static_cast.
Who benefits: Build system maintainers, developers contributing C++ code, CI/CD pipelines ensuring code quality
Impact: Catches deprecated API usage and build warnings early, improves code safety through strict type casting, ensures cross-platform build quality
We are thrilled to announce the General Availability (GA) of mssql-python, Microsoft’s official Python driver for SQL Server, Azure SQL, and SQL datab
We are thrilled to announce the General Availability (GA) of mssql-python, Microsoft’s official Python driver for SQL Server, Azure SQL, and SQL databases in Fabric. This milestone makes the driver production-ready for enterprise workloads.
Python 3.14 support Ensuring compatibility with the latest Python ecosystem so developers can confidently adopt new language features.
GA stability Hardened release engineering, expanded test coverage, and compliance checks for enterprise readiness. This includes:
Your coding agent can read these notes before it upgrades. Set up the MCP server →