mssql-python 1.16.0__cp315-cp315-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mssql_python/row.py ADDED
@@ -0,0 +1,398 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module contains the Row class, which represents a single row of data
5
+ from a cursor fetch operation.
6
+ """
7
+
8
+ import decimal
9
+ import uuid as _uuid
10
+ from collections.abc import Mapping
11
+ from typing import Any
12
+ from mssql_python.logging import logger
13
+
14
+
15
+ class Row:
16
+ """
17
+ A row of data from a cursor fetch operation. Provides both tuple-like indexing
18
+ and attribute access to column values.
19
+
20
+ Rows support user-defined attributes, ``vars(row)``, and weak references.
21
+ Internal fields use slots; ``vars(row)`` contains user-defined attributes.
22
+
23
+ For dict-like access, use the read-only ``row._mapping`` view (a
24
+ ``collections.abc.Mapping`` of column name -> value). Iterating the Row itself
25
+ (for x in row) yields values, not keys — consistent with pyodbc.Row and
26
+ sqlite3.Row; iterate ``row._mapping`` to get column names.
27
+
28
+ Column attribute access behavior depends on the global 'lowercase' setting:
29
+ - When enabled: Case-insensitive attribute access
30
+ - When disabled (default): Case-sensitive attribute access matching original column names
31
+
32
+ Example:
33
+ row = cursor.fetchone()
34
+ print(row[0]) # Access by index
35
+ print(row.column_name) # Access by column name
36
+ print(dict(row._mapping)) # Convert to a plain dict
37
+ print(row._mapping["col"]) # Access a value by column name via the mapping
38
+ for name in row._mapping: # Iterate column names
39
+ print(name, row._mapping[name])
40
+ for value in row: # Iterating the Row yields values, not keys
41
+ print(value)
42
+ """
43
+
44
+ # Slot internal fields while preserving dynamic attributes and weak references.
45
+ __slots__ = (
46
+ "_values",
47
+ "_column_map",
48
+ "_cursor",
49
+ "_column_map_lower",
50
+ "_column_names",
51
+ "__dict__",
52
+ "__weakref__",
53
+ )
54
+
55
+ @staticmethod
56
+ def _fast_create(values, column_map, cursor, column_map_lower=None, column_names=None):
57
+ """Construct a Row bypassing __init__ — for the common fast path.
58
+
59
+ Used by fetchall/fetchmany when no output converters and no UUID
60
+ stringification are needed (the vast majority of queries). Skips
61
+ the entire if/elif/else chain and keyword argument overhead in __init__.
62
+ """
63
+ r = Row.__new__(Row)
64
+ r._values = values
65
+ r._column_map = column_map
66
+ r._cursor = cursor
67
+ r._column_map_lower = column_map_lower
68
+ r._column_names = column_names
69
+ return r
70
+
71
+ def __init__(
72
+ self,
73
+ values,
74
+ column_map,
75
+ cursor=None,
76
+ converter_map=None,
77
+ uuid_str_indices=None,
78
+ column_map_lower=None,
79
+ column_names=None,
80
+ ):
81
+ """
82
+ Initialize a Row object with values and pre-built column map.
83
+ Args:
84
+ values: List of values for this row
85
+ column_map: Pre-built column name to index mapping (shared across rows)
86
+ cursor: Optional cursor reference (for backward compatibility and lowercase access)
87
+ converter_map: Pre-computed converter map (shared across rows for performance).
88
+ An empty sequence skips converters; None enables the connection fallback.
89
+ uuid_str_indices: Tuple of column indices whose uuid.UUID values should be
90
+ converted to str. Pre-computed once per result set when native_uuid=False.
91
+ None means no conversion (native_uuid=True, the default).
92
+ column_map_lower: Pre-built lowercase column map for O(1) case-insensitive
93
+ lookups. Built once per result set in the cursor when lowercase is enabled;
94
+ None when lowercase is off (the default). Shared across all rows.
95
+ column_names: Canonical, order- and duplicate-preserving column names for
96
+ the result set, snapshotted once by the cursor and shared by reference
97
+ across all rows. Backs ``row._mapping``. None for rows built without a
98
+ cursor snapshot; ``_mapping_keys()`` then reconstructs names from
99
+ ``column_map``.
100
+ """
101
+ if converter_map:
102
+ self._values = self._apply_output_converters_optimized(values, converter_map)
103
+ elif (
104
+ converter_map is None
105
+ and cursor
106
+ and hasattr(cursor.connection, "_output_converters")
107
+ and cursor.connection._output_converters
108
+ ):
109
+ # Support direct Row construction without a pre-computed converter map.
110
+ self._values = self._apply_output_converters(values, cursor)
111
+ else:
112
+ self._values = values
113
+
114
+ if uuid_str_indices:
115
+ self._stringify_uuids(uuid_str_indices)
116
+
117
+ self._column_map = column_map
118
+ self._cursor = cursor
119
+ # Lowercase map is pre-built once per result set in the cursor and shared
120
+ # across all rows. None when lowercase is off (the default) — zero cost.
121
+ self._column_map_lower = column_map_lower
122
+ # Canonical column names for this row's result set, snapshotted once by the
123
+ # cursor (order- and duplicate-preserving) and shared by reference across every
124
+ # row. None only for rows built without a cursor snapshot (e.g. some direct or
125
+ # test constructions); _mapping_keys() then reconstructs names from _column_map.
126
+ self._column_names = column_names
127
+
128
+ def _stringify_uuids(self, indices):
129
+ """
130
+ Convert uuid.UUID values at the given column indices to uppercase str in-place.
131
+
132
+ This is only called when native_uuid=False. It operates directly on
133
+ self._values to avoid creating an extra list copy.
134
+ """
135
+ vals = self._values
136
+ # If values are still the original list (no converters), we need a mutable copy
137
+ if not isinstance(vals, list):
138
+ vals = list(vals)
139
+ self._values = vals
140
+
141
+ for i in indices:
142
+ v = vals[i]
143
+ if v is not None and isinstance(v, _uuid.UUID):
144
+ vals[i] = str(v).upper()
145
+
146
+ def _apply_output_converters(self, values, cursor):
147
+ """
148
+ Apply output converters to raw values.
149
+
150
+ Args:
151
+ values: Raw values from the database
152
+ cursor: Cursor object with connection and description
153
+
154
+ Returns:
155
+ List of converted values
156
+ """
157
+ if not cursor.description:
158
+ return values
159
+
160
+ converted_values = list(values)
161
+
162
+ for i, (value, desc) in enumerate(zip(values, cursor.description)):
163
+ if desc is None or value is None:
164
+ continue
165
+
166
+ # Get SQL type from description
167
+ sql_type = desc[1] # type_code is at index 1 in description tuple
168
+
169
+ # Try to get a converter for this type
170
+ converter = cursor.connection.get_output_converter(sql_type)
171
+
172
+ # If no converter found for the SQL type but the value is a string or bytes,
173
+ # try the WVARCHAR converter as a fallback
174
+ if converter is None and isinstance(value, (str, bytes)):
175
+ from mssql_python.constants import ConstantsDDBC
176
+
177
+ converter = cursor.connection.get_output_converter(ConstantsDDBC.SQL_WVARCHAR.value)
178
+
179
+ # If we found a converter, apply it
180
+ if converter:
181
+ try:
182
+ # If value is already a Python type (str, int, etc.),
183
+ # we need to convert it to bytes for our converters
184
+ if isinstance(value, str):
185
+ # Encode as UTF-16LE for string values (SQL_WVARCHAR format)
186
+ value_bytes = value.encode("utf-16-le")
187
+ converted_values[i] = converter(value_bytes)
188
+ else:
189
+ converted_values[i] = converter(value)
190
+ except Exception:
191
+ logger.debug("Exception occurred in output converter", exc_info=True)
192
+ # If conversion fails, keep the original value
193
+ pass
194
+
195
+ return converted_values
196
+
197
+ def _apply_output_converters_optimized(self, values, converter_map):
198
+ """
199
+ Apply output converters using pre-computed converter map for optimal performance.
200
+
201
+ Args:
202
+ values: Raw values from the database
203
+ converter_map: Pre-computed list of converters (one per column, None if no converter)
204
+
205
+ Returns:
206
+ List of converted values
207
+ """
208
+ converted_values = list(values)
209
+
210
+ for i, (value, converter) in enumerate(zip(values, converter_map)):
211
+ if converter and value is not None:
212
+ try:
213
+ if isinstance(value, str):
214
+ value_bytes = value.encode("utf-16-le")
215
+ converted_values[i] = converter(value_bytes)
216
+ else:
217
+ converted_values[i] = converter(value)
218
+ except Exception:
219
+ pass
220
+
221
+ return converted_values
222
+
223
+ def __getitem__(self, index) -> Any:
224
+ """Allow accessing by numeric index (row[0]) or column name (row["col"])."""
225
+ if type(index) is int:
226
+ return self._values[index]
227
+ if isinstance(index, str):
228
+ # A row built without a column map has no named columns, so any
229
+ # string key is simply absent (KeyError), never a TypeError.
230
+ if self._column_map is not None and index in self._column_map:
231
+ return self._values[self._column_map[index]]
232
+ # O(1) case-insensitive lookup when lowercase is enabled
233
+ if self._column_map_lower is not None:
234
+ idx = self._column_map_lower.get(index.lower())
235
+ if idx is not None:
236
+ return self._values[idx]
237
+ raise KeyError(f"Row has no column '{index}'")
238
+ if isinstance(index, (int, slice)):
239
+ return self._values[index]
240
+ raise TypeError(
241
+ f"Row indices must be integers, slices, or strings, not {type(index).__name__}"
242
+ )
243
+
244
+ def __getattr__(self, name: str) -> Any:
245
+ """
246
+ Allow accessing by column name as attribute: row.column_name
247
+
248
+ Note: Case sensitivity depends on the global 'lowercase' setting:
249
+ - When lowercase=True: Column names are stored in lowercase, enabling
250
+ case-insensitive attribute access (e.g., row.NAME, row.name, row.Name all work).
251
+ - When lowercase=False (default): Column names preserve original casing,
252
+ requiring exact case matching for attribute access.
253
+ """
254
+ # Handle lowercase attribute access - if lowercase is enabled,
255
+ # try to match attribute names case-insensitively
256
+ if name in self._column_map:
257
+ return self._values[self._column_map[name]]
258
+
259
+ # O(1) case-insensitive lookup when lowercase is enabled
260
+ if self._column_map_lower is not None:
261
+ idx = self._column_map_lower.get(name.lower())
262
+ if idx is not None:
263
+ return self._values[idx]
264
+
265
+ raise AttributeError(f"Row has no attribute '{name}'")
266
+
267
+ @property
268
+ def _mapping(self) -> "RowMapping":
269
+ """Read-only ``dict``-like view (column name -> value) over this row.
270
+
271
+ Returns a :class:`RowMapping` (a ``collections.abc.Mapping``). Typical use::
272
+
273
+ row = cursor.fetchone()
274
+ dict(row._mapping) # {'id': 1, 'name': 'Alice'}
275
+ for name, value in row._mapping.items():
276
+ ...
277
+ row._mapping["name"] # value by column name
278
+ "name" in row._mapping # membership by column name
279
+
280
+ Semantics and caveats:
281
+
282
+ - Keys are the result set's column names, order-preserving and
283
+ de-duplicated: when a name repeats, one key is kept and the last column
284
+ with that name supplies its value (matching ``row[name]`` / ``row.name``).
285
+ Every duplicate value stays reachable positionally via ``row[i]``.
286
+ - Lookup and membership use the canonical column names exactly, so the key
287
+ set, ``in`` and ``[]`` always agree. Unlike ``row[name]`` / ``row.name``,
288
+ the view does NOT resolve case-insensitive names or catalog aliases; with
289
+ ``lowercase=True`` the keys are the lowercased names.
290
+ - ``_mapping`` is a property, so a column literally named ``_mapping`` is
291
+ shadowed: read it with ``row["_mapping"]`` or ``row._mapping["_mapping"]``.
292
+ """
293
+ return RowMapping(self)
294
+
295
+ def _mapping_keys(self) -> tuple:
296
+ """Canonical, order-preserving column names backing ``_mapping``.
297
+
298
+ Prefers the names snapshotted once by the cursor for the result set, which
299
+ preserve the result set's column order. When a row was built without that
300
+ snapshot (e.g. a direct ``Row(values, column_map)`` construction), names are
301
+ reconstructed from ``_column_map`` in column-index order; that order can
302
+ differ from ``_column_map``'s insertion order, so a directly-constructed row
303
+ may key differently from an otherwise-equivalent cursor row. Returns ``()``
304
+ when neither source is available. Normal cursor fetches always supply the
305
+ snapshot.
306
+ """
307
+ if self._column_names is not None:
308
+ return self._column_names
309
+ if self._column_map:
310
+ idx_to_name: dict = {}
311
+ for name, idx in self._column_map.items():
312
+ idx_to_name.setdefault(idx, name)
313
+ return tuple(idx_to_name[i] for i in sorted(idx_to_name))
314
+ return ()
315
+
316
+ def __eq__(self, other: Any) -> bool:
317
+ """
318
+ Support comparison with lists for test compatibility.
319
+ This is the key change needed to fix the tests.
320
+ """
321
+ if isinstance(other, list):
322
+ return self._values == other
323
+ if isinstance(other, Row):
324
+ return self._values == other._values
325
+ return super().__eq__(other)
326
+
327
+ def __len__(self) -> int:
328
+ """Return the number of values in the row"""
329
+ return len(self._values)
330
+
331
+ def __iter__(self) -> Any:
332
+ """Allow iteration through values"""
333
+ return iter(self._values)
334
+
335
+ def __str__(self) -> str:
336
+ """Return string representation of the row"""
337
+ # Local import to avoid circular dependency
338
+ from mssql_python import getDecimalSeparator
339
+
340
+ parts = []
341
+ for value in self:
342
+ if isinstance(value, decimal.Decimal):
343
+ # Apply custom decimal separator for display
344
+ sep = getDecimalSeparator()
345
+ if sep != "." and value is not None:
346
+ s = str(value)
347
+ if "." in s:
348
+ s = s.replace(".", sep)
349
+ parts.append(s)
350
+ else:
351
+ parts.append(str(value))
352
+ else:
353
+ parts.append(repr(value))
354
+
355
+ return "(" + ", ".join(parts) + ")"
356
+
357
+ def __repr__(self) -> str:
358
+ """Return a detailed string representation for debugging"""
359
+ return repr(tuple(self._values))
360
+
361
+
362
+ class RowMapping(Mapping):
363
+ """Read-only ``Mapping`` view over a :class:`Row` (column name -> value).
364
+
365
+ Created via :attr:`Row._mapping`. Keys are the row's canonical column names,
366
+ order-preserving and de-duplicated (last column wins for a repeated name).
367
+ Lookup and membership use those names exactly -- no case-insensitive or catalog
368
+ alias resolution -- so iteration, ``in`` and ``[]`` always agree. The view
369
+ reflects the row it wraps and copies no values.
370
+ """
371
+
372
+ __slots__ = ("_row",)
373
+
374
+ def __init__(self, row: "Row") -> None:
375
+ self._row = row
376
+
377
+ def __getitem__(self, key: str) -> Any:
378
+ # Restrict lookups to the canonical column names yielded by __iter__ so
379
+ # membership and lookup agree with iteration (proper Mapping semantics).
380
+ # Row.__getitem__ additionally accepts case-insensitive names and catalog
381
+ # aliases, but those are not iterated keys, so the view must not resolve
382
+ # them here.
383
+ if isinstance(key, str) and key in self._row._mapping_keys():
384
+ return self._row[key]
385
+ raise KeyError(key)
386
+
387
+ def __iter__(self):
388
+ seen = set()
389
+ for name in self._row._mapping_keys():
390
+ if name not in seen:
391
+ seen.add(name)
392
+ yield name
393
+
394
+ def __len__(self) -> int:
395
+ return sum(1 for _ in self)
396
+
397
+ def __repr__(self) -> str:
398
+ return f"RowMapping({dict(self)!r})"
mssql_python/type.py ADDED
@@ -0,0 +1,160 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module contains type objects and constructors for the mssql_python package.
5
+ """
6
+
7
+ import datetime
8
+ import time
9
+ from typing import Union
10
+
11
+
12
+ # Type Objects
13
+ class STRING(str):
14
+ """
15
+ This type object is used to describe columns in a database that are string-based (e.g. CHAR).
16
+ """
17
+
18
+ def __new__(cls):
19
+ return str.__new__(cls, "")
20
+
21
+
22
+ class BINARY(bytearray):
23
+ """
24
+ This type object is used to describe (long)
25
+ binary columns in a database (e.g. LONG, RAW, BLOBs).
26
+ """
27
+
28
+ def __new__(cls):
29
+ return bytearray.__new__(cls)
30
+
31
+
32
+ class NUMBER(float):
33
+ """
34
+ This type object is used to describe numeric columns in a database.
35
+ """
36
+
37
+ def __new__(cls):
38
+ return float.__new__(cls, 0.0)
39
+
40
+
41
+ class DATETIME(datetime.datetime):
42
+ """
43
+ This type object is used to describe date/time columns in a database.
44
+ """
45
+
46
+ def __new__(
47
+ cls,
48
+ year: int = 1,
49
+ month: int = 1,
50
+ day: int = 1,
51
+ hour: int = 0,
52
+ minute: int = 0,
53
+ second: int = 0,
54
+ microsecond: int = 0,
55
+ tzinfo=None,
56
+ *,
57
+ fold: int = 0,
58
+ ):
59
+ return datetime.datetime.__new__(
60
+ cls, year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold
61
+ )
62
+
63
+
64
+ class ROWID(int):
65
+ """
66
+ This type object is used to describe the "Row ID" column in a database.
67
+ """
68
+
69
+ def __new__(cls):
70
+ return int.__new__(cls, 0)
71
+
72
+
73
+ # Type Constructors
74
+ def Date(year: int, month: int, day: int) -> datetime.date:
75
+ """
76
+ Generates a date object.
77
+ """
78
+ return datetime.date(year, month, day)
79
+
80
+
81
+ def Time(hour: int, minute: int, second: int) -> datetime.time:
82
+ """
83
+ Generates a time object.
84
+ """
85
+ return datetime.time(hour, minute, second)
86
+
87
+
88
+ def Timestamp(
89
+ year: int,
90
+ month: int,
91
+ day: int,
92
+ hour: int,
93
+ minute: int,
94
+ second: int,
95
+ microsecond: int,
96
+ ) -> datetime.datetime:
97
+ """
98
+ Generates a timestamp object.
99
+ """
100
+ return datetime.datetime(year, month, day, hour, minute, second, microsecond)
101
+
102
+
103
+ def DateFromTicks(ticks: int) -> datetime.date:
104
+ """
105
+ Generates a date object from ticks.
106
+ """
107
+ return datetime.date.fromtimestamp(ticks)
108
+
109
+
110
+ def TimeFromTicks(ticks: int) -> datetime.time:
111
+ """
112
+ Generates a time object from ticks.
113
+ """
114
+ return datetime.time(*time.localtime(ticks)[3:6])
115
+
116
+
117
+ def TimestampFromTicks(ticks: int) -> datetime.datetime:
118
+ """
119
+ Generates a timestamp object from ticks.
120
+ """
121
+ return datetime.datetime.fromtimestamp(ticks)
122
+
123
+
124
+ def Binary(value: Union[str, bytes, bytearray, memoryview]) -> bytes:
125
+ """
126
+ Converts a string or bytes-like object to bytes for binary database columns.
127
+
128
+ This function follows the DB-API 2.0 specification. It accepts str,
129
+ bytes, bytearray, and memoryview. memoryview is accepted so buffer-protocol
130
+ values (for example the memoryview Django hands a BinaryField) round-trip the
131
+ same way pyodbc handles them.
132
+
133
+ Args:
134
+ value: A string (str) or bytes-like object (bytes, bytearray, memoryview)
135
+
136
+ Returns:
137
+ bytes: The input converted to bytes
138
+
139
+ Raises:
140
+ TypeError: If the input type is not supported
141
+
142
+ Examples:
143
+ Binary("hello") # Returns b"hello"
144
+ Binary(b"hello") # Returns b"hello"
145
+ Binary(bytearray(b"hi")) # Returns b"hi"
146
+ Binary(memoryview(b"hi")) # Returns b"hi"
147
+ """
148
+ if isinstance(value, bytes):
149
+ return value
150
+ if isinstance(value, bytearray):
151
+ return bytes(value)
152
+ if isinstance(value, memoryview):
153
+ return value.tobytes()
154
+ if isinstance(value, str):
155
+ return value.encode("utf-8")
156
+ # Raise TypeError for unsupported types to improve type safety
157
+ raise TypeError(
158
+ f"Cannot convert type {type(value).__name__} to bytes. "
159
+ f"Binary() only accepts str, bytes, bytearray, or memoryview objects."
160
+ )
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: mssql-python
3
+ Version: 1.16.0
4
+ Summary: A Python library for interacting with Microsoft SQL Server
5
+ Home-page: https://github.com/microsoft/mssql-python
6
+ Author: Microsoft Corporation
7
+ Author-email: mssql-python@microsoft.com
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: azure-identity>=1.12.0
15
+ Requires-Dist: mssql-python-odbc==18.6.2.2
16
+ Requires-Dist: mssql-python-rs==0.3.0
17
+ Provides-Extra: pyarrow
18
+ Requires-Dist: pyarrow>=14.0.0; extra == "pyarrow"
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license-file
26
+ Dynamic: provides-extra
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
30
+
31
+ # General Availability Release
32
+
33
+ mssql‑python is now Generally Available (GA) as Microsoft’s official Python driver for SQL Server, Azure SQL, and SQL databases in Fabric. This release delivers a production‑ready, high‑performance, and developer‑friendly experience.
34
+
35
+ ## What makes mssql-python different?
36
+
37
+ ### Powered by DDBC – Direct Database Connectivity
38
+
39
+ Most Python SQL Server drivers, including pyodbc, route calls through the Driver Manager, which has slightly different implementations across Windows, macOS, and Linux. This results in inconsistent behavior and capabilities across platforms. Additionally, the Driver Manager must be installed separately, creating friction for both new developers and when deploying applications to servers.
40
+
41
+ At the heart of the mssql-python driver is DDBC (Direct Database Connectivity) — a lightweight, high-performance C++ layer that replaces the platform’s Driver Manager.
42
+
43
+ Key Advantages:
44
+
45
+ - Provides a consistent, cross-platform backend that handles connections, statements, and memory directly.
46
+ - Interfaces directly with the native SQL Server drivers.
47
+ - Integrates with the same TDS core library that powers the ODBC driver.
48
+
49
+ ### Why is this architecture important?
50
+
51
+ By simplifying the architecture, DDBC delivers:
52
+
53
+ - Consistency across platforms
54
+ - Lower function call overhead
55
+ - Zero external dependencies on Windows (`pip install mssql-python` is all you need)
56
+ - Full control over connections, memory, and statement handling
57
+
58
+ ### Built with PyBind11 + Modern C++ for Performance and Safety
59
+
60
+ To expose the DDBC engine to Python, mssql-python uses PyBind11 – a modern C++ binding library.
61
+
62
+ PyBind11 provides:
63
+
64
+ - Native-speed execution with automatic type conversions
65
+ - Memory-safe bindings
66
+ - Clean and Pythonic API, while performance-critical logic remains in robust, maintainable C++.
67
+
68
+ ## What's new in v1.16.0
69
+
70
+ ### Enhancements
71
+
72
+ - **Python 3.15 Support** - Release wheels and validation now cover Python 3.15 across supported platforms (#817).
73
+ - **Dictionary-Style Row Access** - `row._mapping` provides a read-only mapping view with dictionary conversion, keys, values, items, membership checks, and stable column metadata (#613).
74
+ - **External Rust Runtime Dependency** - `mssql-python` now depends on the exact compatible `mssql-python-rs` package instead of embedding its files (#768).
75
+ - **Optional `mssql-odbc` Provider** - Customers can opt into the Rust-based provider distributed with `mssql-python-rs` by setting `MSSQL_PYTHON_NATIVE_PROVIDER=mssql-odbc` or `mssql_python.native_provider = "mssql-odbc"` before the first connection; Microsoft ODBC Driver 18 (`msodbcsql18`) remains the default (#730, #804, via `mssql-python-rs`).
76
+ - **Stable-ABI Rust Runtime Wheels** - The Rust runtime now uses CPython's stable ABI and `mssql-python-rs` 0.3.0, eliminating Python-version-specific wheel variants while retaining separate wheels for each supported OS and architecture (#804, via `mssql-python-rs`).
77
+ - **Updated ODBC Runtime Package** - `mssql-python` now pins `mssql-python-odbc==18.6.2.2`, whose Linux wheels separate glibc and musl payloads while retaining the same ODBC Driver 18.6 binaries (#798, #807).
78
+ - **Faster Row Construction and Batch Fetching** - Optimized native Row construction and cursor bookkeeping reduce Python-side overhead in `fetchone()`, `fetchmany()`, and `fetchall()` (#558).
79
+ - **Faster Temporal Fetch Conversion** - DATE, TIME, and TIMESTAMP values use checked CPython constructors to reduce object-construction overhead (#795).
80
+ - **Reusable Result-Set Metadata** - Stable native column metadata is reused within a result set instead of being repeatedly described (#796).
81
+ - **Reduced Fetch Diagnostic Overhead** - Fetch diagnostics are captured once and reused instead of being redundantly rescanned after clean fetches (#809).
82
+ - **Faster Integer Row Indexing** - Exact integer indexes use a direct fast path while preserving existing behavior for booleans, subclasses, slices, and column names (#822).
83
+ - **Lower `fetchval()` Logging Overhead** - Disabled debug logging no longer dispatches unnecessary log calls from `fetchval()` (#823).
84
+
85
+ ### Bug Fixes
86
+
87
+ - **Reliable Windows Shutdown** - Late ODBC environment cleanup no longer causes process-exit access violations after Python finalization (#783).
88
+ - **Preserved Leading BOM-Like Characters** - Bounded UTF-16 text keeps leading U+FEFF and U+FFFE code points as data across fetch paths (#793).
89
+ - **Clean Pooled Connection State** - Connections are rolled back and restored to autocommit before returning to the pool, preventing retained empty transactions (#777).
90
+ - **Safe Native Logging of Dynamic Metadata** - Server-controlled Arrow column metadata can no longer be interpreted as a native format string (#791).
91
+ - **UDT Parameter Binding** - SQL Server UDT parameter identities are resolved before scalar or array binding, including typed NULLs and statement reuse (#818).
92
+ - **Visible Unsupported Encoding Requests** - `setencoding()` now warns when a valid encoding request cannot be applied instead of silently ignoring it (#828).
93
+ - **Bounded Bulk Copy Timeouts** - Positive sub-second bulk-copy timeouts no longer become infinite and are rounded to one second (#804, via `mssql-python-rs`).
94
+ - **Safe Bulk Copy Identifiers** - Bulk-copy destinations, columns, and collations are parsed, quoted, and validated consistently (#804, via `mssql-python-rs`).
95
+
96
+ For more information, please visit the project link on Github: https://github.com/microsoft/mssql-python
97
+
98
+ If you have any feedback, questions or need support please mail us at mssql-python@microsoft.com.
99
+
100
+ ## What's Next
101
+
102
+ As we continue to refine the driver and add new features, you can expect regular updates, optimizations, and bug fixes. We encourage you to contribute, provide feedback and report any issues you encounter, as this will help us improve the driver.