Skip to content

Commit a5faa32

Browse files
jahnvi480Copilotbewithgaurav
authored
PERF: Optimize Row construction and repeated cursor bookkeeping (#558)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below For external contributors: Insert Github Issue number below Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#44921](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/44921) <!-- External contributors: GitHub Issue --> ### Summary Reduce Python-side overhead in `fetchone()`, `fetchmany()`, and `fetchall()` while preserving decoding, converter, diagnostic, and Row-access behavior. The branch incorporates main through `e279a4f6` and includes the review fixes in `56c3d3b5`. #### Fetch optimizations - Construct eligible `fetchmany()`/`fetchall()` rows in native `construct_rows`, bypassing the Python per-row initialization loop. Use `Row._fast_create()` for the corresponding `fetchone()` path. - Add `Row.__slots__` and retain zero-copy storage of fetched values when no conversion is required. Initialize the shared lowercase column map on both Python and native construction paths. - Cache CHAR/WCHAR encoding strings and the CHAR C type. Refresh them only when connection decoding settings change, including for existing cursors. - Reuse the SQL-to-C type mapping and use `str.isascii()` for Unicode detection. - Keep the no-conversion fast path available when converters are registered but none applies to the current result set. An explicit empty mapping avoids unnecessary row copies or per-row fallback lookups. #### Correctness and compatibility - Track converter-generation changes so adding, replacing, removing, or clearing converters after `execute()` updates subsequent fetches without rebuilding mappings on unchanged cache hits. - Preserve current-main dispatch precedence: raw ODBC SQL type, Python type, then the string/bytes-only WVARCHAR fallback. Unrelated integer and UUID columns are not passed to the string fallback. - Preserve direct `Row` construction's connection-converter fallback when no precomputed mapping is supplied, including UUID stringification. - Retain main's lowercase/string-key column access, profiling scopes, and CHAR decoding C-type forwarding. Native WCHAR decoding semantics are unchanged. - Validate `row_class` with `PyType_Check` before casting in the Python-callable native helper. Invalid arguments raise `TypeError` rather than crashing the interpreter. - Restore unconditional diagnostic retrieval after non-error fetch returns. The native wrappers can overwrite intermediate statuses, so checking only the final `SQL_SUCCESS_WITH_INFO` is unsafe. The earlier diagnostic-skip optimization has been removed; this restores prior behavior rather than claiming to repair pre-existing native diagnostic-record overwrites. - Check errors in `fetchone()` and `fetchmany()` before updating row positions or constructing rows, consistent with `fetchall()`. ### Performance: current main versus this PR | Workload (rows x columns) | API | Main median | PR median | Time reduction | |---|---|---:|---:|---:| | Narrow: 10,000 x 3 | `fetchmany(1000)` | 9.36 ms | 6.54 ms | **30.18%** | | Narrow: 10,000 x 3 | `fetchall()` | 8.67 ms | 6.99 ms | **19.30%** | | Wide: 10,000 x 24 | `fetchmany(1000)` | 27.57 ms | 24.72 ms | **10.34%** | | Wide: 10,000 x 24 | `fetchall()` | 30.59 ms | 26.07 ms | **14.79%** | Positive reduction means less time, calculated from unrounded medians. Narrow/wide batch cases improved in 10/10 and 9/10 pairs respectively. `fetchmany(1)` medians were only 1.15-1.80% lower, with narrow results inconclusive. Narrow `fetchone()` was 5.02% lower; wide `fetchone()`. These are workload-specific results, not a universal or cross-platform speedup. Full results for all 11 fetch cases, control, dispersion, paired intervals, build identities, and raw repetitions are retained locally; older pre-merge numbers are not mixed in. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Gaurav Sharma <sharmag@microsoft.com>
1 parent f3e34e6 commit a5faa32

6 files changed

Lines changed: 1188 additions & 85 deletions

File tree

‎mssql_python/connection.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ def __init__(
540540
"ctype": ConstantsDDBC.SQL_WCHAR.value,
541541
},
542542
}
543+
self._decoding_generation = 0
543544

544545
# Auth type for acquiring fresh tokens at bulk copy time.
545546
# We intentionally do NOT cache the token — a fresh one is acquired
@@ -782,6 +783,7 @@ def _token_factory():
782783

783784
# Initialize output converters dictionary and its lock for thread safety
784785
self._output_converters = {}
786+
self._converters_generation = 0
785787
self._converters_lock = threading.Lock()
786788

787789
# Initialize encoding/decoding settings lock for thread safety
@@ -1320,6 +1322,9 @@ def setdecoding(
13201322
"""
13211323
Sets the text decoding used when reading SQL_CHAR and SQL_WCHAR from the database.
13221324
1325+
Existing cursors refresh their cached SQL_CHAR/SQL_WCHAR decoding settings
1326+
before their next fetch.
1327+
13231328
This method configures how text data is decoded when reading from the database.
13241329
In Python 3, all text is Unicode (str), so this primarily affects the encoding
13251330
used to decode bytes from the database.
@@ -1443,6 +1448,7 @@ def setdecoding(
14431448
# Store the decoding settings for the specified sqltype (thread-safe with lock)
14441449
with self._encoding_lock:
14451450
self._decoding_settings[sqltype] = {"encoding": encoding, "ctype": ctype}
1451+
self._decoding_generation += 1
14461452

14471453
# Log with sanitized values for security
14481454
sqltype_name = {
@@ -1647,6 +1653,8 @@ def add_output_converter(self, sqltype: Union[int, type], func: Callable[[Any],
16471653
16481654
Thread-safe implementation that protects the converters dictionary with a lock.
16491655
1656+
Changes apply on the next fetch, including for an already executed result set.
1657+
16501658
⚠️ WARNING: Registering an output converter will cause the supplied Python function
16511659
to be executed on every matching database value. Do not register converters from
16521660
untrusted sources, as this can result in arbitrary code execution and security
@@ -1687,6 +1695,7 @@ def add_output_converter(self, sqltype: Union[int, type], func: Callable[[Any],
16871695
"""
16881696
with self._converters_lock:
16891697
self._output_converters[sqltype] = func
1698+
self._converters_generation += 1
16901699
# Pass to the underlying connection if native implementation supports it
16911700
if hasattr(self._conn, "add_output_converter"):
16921701
self._conn.add_output_converter(sqltype, func)
@@ -1717,6 +1726,8 @@ def remove_output_converter(self, sqltype: Union[int, type]) -> None:
17171726
17181727
Thread-safe implementation that protects the converters dictionary with a lock.
17191728
1729+
Existing cursors use the updated converters on their next fetch.
1730+
17201731
Args:
17211732
sqltype (int or type): The SQL type value to remove the converter for
17221733
@@ -1726,6 +1737,7 @@ def remove_output_converter(self, sqltype: Union[int, type]) -> None:
17261737
with self._converters_lock:
17271738
if sqltype in self._output_converters:
17281739
del self._output_converters[sqltype]
1740+
self._converters_generation += 1
17291741
# Pass to the underlying connection if native implementation supports it
17301742
if hasattr(self._conn, "remove_output_converter"):
17311743
self._conn.remove_output_converter(sqltype)
@@ -1737,11 +1749,14 @@ def clear_output_converters(self) -> None:
17371749
17381750
Thread-safe implementation that protects the converters dictionary with a lock.
17391751
1752+
Existing cursors stop applying converters on their next fetch.
1753+
17401754
Returns:
17411755
None
17421756
"""
17431757
with self._converters_lock:
17441758
self._output_converters.clear()
1759+
self._converters_generation += 1
17451760
# Pass to the underlying connection if native implementation supports it
17461761
if hasattr(self._conn, "clear_output_converters"):
17471762
self._conn.clear_output_converters()

0 commit comments

Comments
 (0)