Skip to content

PERF: Reuse cursor-scoped state for row fetching - #858

Open
Jahnvi Thakkar (jahnvi480) wants to merge 5 commits into
mainfrom
jahnvi/source05-fetch-pr
Open

Jahnvi Thakkar (jahnvi480) wants to merge 5 commits into
mainfrom
jahnvi/source05-fetch-pr

Conversation

@jahnvi480

@jahnvi480 Jahnvi Thakkar (jahnvi480) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#48364

N/A; the work item above is the sole reference.


Summary

Reuse cursor-scoped native state for eligible one-row fetching while retaining the existing Python fallback behavior.

  • Add lazily allocated, GC-visible native State/Context/Plan ownership and direct dispatch for fetchone(), eligible fetchmany(1), fetchval(), and iteration on supported GIL-enabled CPython versions. Unsupported interpreters retain the Python methods.
  • Invalidate plans through public CPython watchers and cursor result transitions. Preserve custom methods/factories, converters, UUID handling, dictionary replacement, reentry, and completion of already-consumed rows without fetching twice.
  • Share the native single-row pipeline, distinguish complete result metadata from partial SQLGetData metadata, and reuse successful column unbinding.
  • Avoid duplicate association validation only after callback-free reuse of an existing State. Retain all five current-plan checks and cold/publication revalidation.
  • Include the existing Source05 dispatch, lifetime, mutation, parity, operation-count, and isolated failure-control regressions. Fault controls require a separate profiling-enabled build and are absent from ordinary builds.

This is the complete 10-file Source05 delta against 9424705251baf8ded41a8a4968216b1fd9b00929, without later hybrid/converter redesigns or benchmark artifacts. The recovered sources match the measured Source05 archive; no current-base semantic adaptation was needed. There is no public API change.

Performance scope

Historical matched Linux diagnostics of these Source05 sources used approximately 42.2% less elapsed time for repeated fetchone() and 74.1% less for repeated fetchmany(1) than pinned main. Converter fetching was mixed and 1.1% slower at the paired median; pyodbc remained faster in all retained core-operation pairs. These narrow historical diagnostics are not a new benchmark run or all-API qualification. Converter, pyodbc, and full performance-qualification requirements remain unmet; this PR does not claim merge readiness.

Recover the exact cumulative Source05 native-fetch implementation and its regression coverage against frozen main. Preserve watcher invalidation, ownership, fallback and failure controls without later redesigns.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 06:28
@github-actions github-actions Bot added the pr-size: large Substantial code update label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ Performance improved

2 database tasks consistently improved across 2 measured environments. No consistent slowdowns were detected.

2 IMPROVEMENTS 0 SLOWDOWNS 2/2 ENVIRONMENTS

Signal fingerprint

Database task Unix / SQL Server 2022 Unix / SQL Server 2025
Row-by-row fetching 65.7% faster 65.7% faster
10,000 scalar values / fetchval() (debug disabled) 84.3% faster 84.3% faster

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Measured timings
Environment Database task Before After Change
Unix / SQL Server 2022 Row-by-row fetching 14.421 ms 4.929 ms -65.7%
Unix / SQL Server 2022 10,000 scalar values / fetchval() (debug disabled) 106.307 ms 16.708 ms -84.3%
Unix / SQL Server 2025 Row-by-row fetching 14.698 ms 5.037 ms -65.7%
Unix / SQL Server 2025 10,000 scalar values / fetchval() (debug disabled) 107.531 ms 16.828 ms -84.3%
Performance diagnostics

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

Unix / SQL Server 2022

Row-by-row fetching: py::fetchone::cpp_call -4.722 ms; ddbc::FetchOne_wrap -3.919 ms; py::fetchone::row_wrap -0.604 ms. Call changes: ddbc::FetchOne_wrap (1001 -> 1 calls); ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
Batched row fetching: py::fetchmany::row_wrap +3.236 ms; ddbc::FetchMany_wrap +0.471 ms; ddbc::FetchBatchData +0.457 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (51 -> 1 calls).
Row fetching in batches of 100: ddbc::AppendDiagRecords::SQLGetDiagRec_call +0.003 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (501 -> 1 calls).
Row fetching in batches of 10,000: py::fetchmany::cpp_call +0.744 ms; ddbc::AppendDiagRecords::SQLGetDiagRec_call +0.005 ms; ddbc::SQLDescribeCol::driver_call +0.000 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (6 -> 1 calls).
Repeated positional queries: py::execute::post_execute +0.073 ms; py::fetchone::cpp_call +0.068 ms; ddbc::FetchOne_wrap +0.045 ms. Call changes: ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::NativeCursor::fallback (added, removed, or intermittent).
Repeated named-parameter queries: ddbc::SQLExecute_wrap +0.123 ms; py::execute::param_prep +0.122 ms; py::execute::cpp_call +0.095 ms. Call changes: ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::NativeCursor::fallback (added, removed, or intermittent).
10,000 scalar values / fetchval() (debug disabled): py::fetchone::cpp_call -16.786 ms; ddbc::FetchOne_wrap -9.728 ms; py::fetchone::row_wrap -5.432 ms. Call changes: ddbc::FetchOne_wrap (10001 -> 1 calls); ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).

Unix / SQL Server 2025

Row-by-row fetching: py::fetchone::cpp_call -4.881 ms; ddbc::FetchOne_wrap -3.950 ms; py::fetchone::row_wrap -0.605 ms. Call changes: ddbc::FetchOne_wrap (1001 -> 1 calls); ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
Batched row fetching: py::fetchmany::row_wrap +2.338 ms; ddbc::FetchBatchData::SQLFetchScroll_call +1.212 ms; ddbc::FetchMany_wrap +0.134 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (51 -> 1 calls).
Row fetching in batches of 100: ddbc::FetchBatchData::SQLFetchScroll_call +0.295 ms; ddbc::SQLDescribeCol::driver_call +0.000 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (501 -> 1 calls).
Row fetching in batches of 10,000: py::fetchmany::cpp_call +2.606 ms; ddbc::FetchMany_wrap +0.604 ms; ddbc::FetchBatchData::construct_rows +0.553 ms. Call changes: ddbc::NativeCursor::fallback (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (6 -> 1 calls).
Repeated positional queries: ddbc::SQLExecute_wrap +0.435 ms; py::execute::cpp_call +0.429 ms; py::execute::post_execute +0.102 ms. Call changes: ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::NativeCursor::fallback (added, removed, or intermittent).
Repeated named-parameter queries: py::execute::param_prep +0.059 ms; py::fetchone::cpp_call +0.042 ms; ddbc::FetchOne_wrap +0.026 ms. Call changes: ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::NativeCursor::fallback (added, removed, or intermittent).
10,000 scalar values / fetchval() (debug disabled): py::fetchone::cpp_call -16.970 ms; ddbc::FetchOne_wrap -9.780 ms; py::fetchone::row_wrap -5.363 ms. Call changes: ddbc::FetchOne_wrap (10001 -> 1 calls); ddbc::FetchSingleRow (added, removed, or intermittent); ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.208 ms 10.297 ms +1.0% no signal
SELECT queries 1.068 ms 1.062 ms +2.6% no signal
Row insertion 34.546 ms 34.718 ms +0.4% no signal
Executemany inserts 156.915 ms 162.222 ms +3.1% no signal
Fetch-all queries 118.485 ms 118.058 ms +0.4% no signal
Row-by-row fetching 14.421 ms 4.929 ms -65.7% consistent improvement
Batched row fetching 115.691 ms 116.011 ms -0.4% no signal
Transaction commit and rollback 115.512 ms 115.551 ms +0.3% no signal
Arrow row fetching 92.729 ms 93.043 ms +0.4% no signal
100,000-row insertion 433.884 ms 456.248 ms +2.3% no signal
Row fetching in batches of 100 122.280 ms 119.309 ms -2.2% no signal
Row fetching in batches of 10,000 128.834 ms 127.746 ms -1.5% no signal
Repeated positional queries 34.056 ms 35.003 ms +3.6% no signal
Repeated named-parameter queries 36.225 ms 37.966 ms +4.6% no signal
Legacy 100,000-row insertion 347.651 ms 352.872 ms +1.9% no signal
Insertion with explicit input sizes 492.117 ms 488.760 ms -0.8% no signal
Joined aggregation queries 177.610 ms 175.469 ms -1.2% no signal
Large joined-result fetching 185.979 ms 181.595 ms -0.3% no signal
1.2-million-row fetching 3370.065 ms 3384.419 ms +0.6% no signal
Common table expression queries 5.334 ms 5.375 ms +0.8% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.308 ms 1.306 ms -1.8% no signal
10,000 scalar values / fetchval() (debug disabled) 106.307 ms 16.708 ms -84.3% consistent improvement

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 97.040 ms 96.267 ms -0.5% no signal
SELECT queries 1.075 ms 1.090 ms +1.0% no signal
Row insertion 34.543 ms 35.031 ms +1.0% no signal
Executemany inserts 155.030 ms 152.972 ms -0.6% no signal
Fetch-all queries 121.168 ms 120.666 ms -0.1% no signal
Row-by-row fetching 14.698 ms 5.037 ms -65.7% consistent improvement
Batched row fetching 117.458 ms 117.241 ms -0.3% no signal
Transaction commit and rollback 115.582 ms 115.517 ms -0.2% no signal
Arrow row fetching 92.437 ms 92.921 ms +0.3% no signal
100,000-row insertion 430.714 ms 422.663 ms -1.6% no signal
Row fetching in batches of 100 122.073 ms 120.664 ms -0.8% no signal
Row fetching in batches of 10,000 122.895 ms 122.920 ms +0.5% no signal
Repeated positional queries 33.677 ms 35.300 ms +4.3% no signal
Repeated named-parameter queries 36.881 ms 37.831 ms +3.8% no signal
Legacy 100,000-row insertion 343.385 ms 350.575 ms +2.1% no signal
Insertion with explicit input sizes 484.881 ms 482.215 ms -0.3% no signal
Joined aggregation queries 162.018 ms 160.934 ms -0.6% no signal
Large joined-result fetching 176.780 ms 175.511 ms -0.7% no signal
1.2-million-row fetching 3455.557 ms 3416.004 ms -1.2% no signal
Common table expression queries 5.168 ms 5.134 ms +0.7% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.479 ms 1.472 ms -1.3% no signal
10,000 scalar values / fetchval() (debug disabled) 107.531 ms 16.828 ms -84.3% consistent improvement
Build and measurement details

ADO build 186669

PR head: 76506060948e497159da8ef979a111f8a8d5005e
Base: 2c6a268c28ace89b84dd535f14dfae5ed17d51fc
Measured merge: f6feb8bcea4852baa8250d1a6a989a25398c49b3

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4305.5; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5035.5; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Comment thread mssql_python/pybind/native_fetch.hpp Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Native ownership and watcher-based dispatch need human review, with fallback compatibility fixes, cross-platform validation, and performance qualification still outstanding.

2 open issues
What changed in this PR

This PR reduces repeated single-row fetch overhead by reusing cursor-scoped native state while retaining Python fallback paths.

Changes:

  • Adds watcher-based native dispatch and garbage-collector-visible ownership for eligible single-row operations.
  • Shares the native single-row pipeline, tracks complete metadata, and reuses successful column unbinding.
  • Adds parity, mutation, lifetime, operation-count, and isolated failure-control tests.
File Description
tests/​test_single_row_pipeline.py Tests single-row parity, ownership, and operation counts.
tests/​test_native_fetch_dispatch.py Tests dispatch, mutation, lifetime, and failure recovery.
mssql_python/​pybind/​row_factory.hpp Adds single-row construction and reusable attribute names.
mssql_python/​pybind/​result_metadata.hpp Distinguishes complete metadata from partial prefixes.
mssql_python/​pybind/​native_fetch.hpp Implements native state, watchers, dispatch, and fallback.
mssql_python/​pybind/​ddbc_bindings.h Tracks reusable column-unbinding state.
mssql_python/​pybind/​ddbc_bindings.cpp Shares single-row fetching and registers native bindings.
mssql_python/​pybind/​CMakeLists.txt Gates fault controls behind profiling-enabled builds.
mssql_python/​cursor.py Adds invalidation, completion helpers, and dispatch installation.
mssql_python/​_native_fetch.py Defines eligibility policy and installation rollback.

Review effort: Balanced

Comment thread mssql_python/_native_fetch.py Outdated
Comment thread mssql_python/pybind/native_fetch.hpp Outdated
Preserve fallback semantics for dict subclasses and keyword binding, accept CPython 3.15's ordinary dictionary descriptor, and initialize stable policy dependencies before capture. Restore logging state between tests and consolidate duplicate fetch coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 08:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Native ownership and invalidation changes need human review, with an unresolved correctness issue and incomplete cross-platform and performance qualification.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment on lines +5079 to +5081
if (fetchSize == 1 && lobColumns.empty()) {
py::list row;
ret = FetchSingleRow(StatementHandle, row, charEncoding, wcharEncoding, charCtype, messages);
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

87%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 10397 out of 12169
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/_native_fetch.py (77.7%): Missing lines 99,108,150,160,171,241-243,253,278,281,283-290,293-294,302-309
  • mssql_python/cursor.py (86.0%): Missing lines 2902-2904,2907-2908,2921
  • mssql_python/pybind/ddbc_bindings.cpp (97.9%): Missing lines 5083
  • mssql_python/pybind/native_fetch.hpp (88.7%): Missing lines 38,275,301,311,323,343,346,411,422,445,449,459,494,507,512,545,568,580,588,613,628,639,654,666,688,694,697,711,728,731,750,754,761,795,825,829,832,835,838,841,845,848,877-879,885-888,898-900,914-916,995,1007-1009,1052,1066,1070,1075,1085,1088,1107,1113-1119,1122-1124,1127-1130,1133-1137,1150,1153,1157,1161
  • mssql_python/pybind/row_factory.hpp (92.0%): Missing lines 20,26

Summary

  • Total: 1033 lines
  • Missing: 127 lines
  • Coverage: 87%

mssql_python/_native_fetch.py

Lines 95-103

   95             return None
   96         from mssql_python.connection import Connection
   97 
   98         if type(connection) is not Connection:
!  99             return None
  100         descriptor = vars(Connection).get("__dict__")
  101         if (
  102             Connection.__getattribute__ is not object.__getattribute__
  103             or type(descriptor) is not GetSetDescriptorType

Lines 104-112

  104             # CPython 3.15 shares the ordinary instance-dict descriptor on object.
  105             or descriptor.__objclass__ not in (Connection, object)
  106             or descriptor.__name__ != "__dict__"
  107         ):
! 108             return None
  109         connection_data = object.__getattribute__(connection, "__dict__")
  110         ddbc_bindings._watch_fetch_dependencies(
  111             self.observer,
  112             (data, connection_data),

Lines 146-154

  146                 for name, value in original.items()
  147             ):
  148                 return None
  149         if self.logger.is_debug_enabled:
! 150             return None
  151         if (
  152             self.row_type.__new__ is not object.__new__
  153             or self.row_type.__setattr__ is not object.__setattr__
  154             or any(

Lines 156-164

  156                 for base in Connection.__mro__
  157                 for name in ("_decoding_generation", "_converters_generation")
  158             )
  159         ):
! 160             return None
  161         if any(name in data for name in vars(self.cursor_type)):
  162             return None
  163         for cached, generation in (
  164             ("_cached_decoding_generation", "_decoding_generation"),

Lines 167-175

  167             if (
  168                 type(data.get(cached)) is not int
  169                 or type(connection_data.get(generation)) is not int
  170             ):
! 171                 return None
  172         if (
  173             data.get("closed") is not False
  174             or data.get("_has_result_set") is not True
  175             or data.get("_skip_increment_for_next_fetch") is not False

Lines 237-247

  237 
  238 
  239 class _InstallRollbackError(RuntimeError):
  240     def __init__(self, installation_error, cleanup_errors):
! 241         self.installation_error = installation_error
! 242         self.cleanup_errors = tuple(cleanup_errors)
! 243         super().__init__(
  244             "Native fetch installation and rollback both failed",
  245             installation_error,
  246             self.cleanup_errors,
  247         )

Lines 249-257

  249 
  250 def install(cursor_type, row_type, namespace):
  251     observer = ddbc_bindings._fetch_observer
  252     if observer is None or not isinstance(getattr(row_type, "_fast_create", None), FunctionType):
! 253         return None
  254     methods = ("fetchone", "fetchmany", "fetchval", "__next__")
  255     fallbacks = tuple(getattr(cursor_type, name) for name in methods)
  256     entries = ddbc_bindings._native_fetch_methods
  257     originals = {name: vars(cursor_type)[name] for name in methods}

Lines 274-298

  274             ("_next_row_index", "_rownumber", "rowcount"),
  275             context,
  276         )
  277         if any(vars(cursor_type).get(name) is not entry for name, entry in zip(methods, entries)):
! 278             raise RuntimeError("Cursor methods changed during native fetch installation")
  279         activate(runtime)
  280         return runtime
! 281     except BaseException as installation_error:
  282         # Unpublished entries can only forward to originals until activation.
! 283         cleanup_errors = []
! 284         for name, entry in zip(methods, entries):
! 285             try:
! 286                 if vars(cursor_type).get(name) is entry:
! 287                     setattr(cursor_type, name, originals[name])
! 288             except BaseException as cleanup_error:
! 289                 cleanup_errors.append((name, cleanup_error))
! 290         survivors = tuple(
  291             name for name, entry in zip(methods, entries) if vars(cursor_type).get(name) is entry
  292         )
! 293         if survivors and not cleanup_errors:
! 294             cleanup_errors.append(
  295                 (
  296                     "descriptors",
  297                     RuntimeError("Native fetch descriptors survived rollback", survivors),
  298                 )

Lines 298-310

  298                 )
  299             )
  300         # Retained descriptors must retain their forwarding functions. A partial
  301         # rollback is terminal, not a context that a later install may reuse.
! 302         terminalize = disable if cleanup_errors else abort
! 303         try:
! 304             terminalize()
! 305         except BaseException as cleanup_error:
! 306             cleanup_errors.append(("context", cleanup_error))
! 307         if cleanup_errors:
! 308             raise _InstallRollbackError(installation_error, cleanup_errors) from installation_error
! 309         raise

mssql_python/cursor.py

Lines 2898-2912

  2898     def _complete_fetchone(self, row_data, ret):
  2899         """Complete an already-consumed fetch, including mutation/error fallback."""
  2900         check_error(_SQL_HANDLE_STMT, self.hstmt, ret)
  2901         if ret == _SQL_NO_DATA:
! 2902             if self._next_row_index == 0 and self.description is not None:
! 2903                 self.rowcount = 0
! 2904             return None
  2905 
  2906         if self._skip_increment_for_next_fetch:
! 2907             self._skip_increment_for_next_fetch = False
! 2908             self._next_row_index += 1
  2909         else:
  2910             self._increment_rownumber()
  2911         self.rowcount = self._next_row_index
  2912         return self._wrap_fetchone(row_data)

Lines 2917-2925

  2917             if not converter_map and not self._uuid_str_indices:
  2918                 return Row._fast_create(
  2919                     row_data, column_map, self, column_map_lower, self._cached_result_columns
  2920                 )
! 2921             return Row(
  2922                 row_data,
  2923                 column_map,
  2924                 cursor=self,
  2925                 converter_map=converter_map,

mssql_python/pybind/ddbc_bindings.cpp

Lines 5079-5087

  5079     if (fetchSize == 1 && lobColumns.empty()) {
  5080         py::list row;
  5081         ret = FetchSingleRow(StatementHandle, row, charEncoding, wcharEncoding, charCtype, messages);
  5082         if (SQL_SUCCEEDED(ret))
! 5083             rows.append(row);
  5084         return ret;
  5085     }
  5086 
  5087     // Initialized to 0 for LOB path counter; overwritten by ODBC in non-LOB path;

mssql_python/pybind/native_fetch.hpp

Lines 34-42

  34                          PyObject* value) {
  35     if (event != PyDict_EVENT_MODIFIED || dict != internalDict ||
  36         (key != internalKey &&
  37          (!key || !internalKey || !PyUnicode_CheckExact(key) ||
! 38           PyUnicode_Compare(key, internalKey) != 0)) ||
  39         value != internalValue)
  40         mutationEpoch.fetch_add(1, std::memory_order_relaxed);
  41     return 0;
  42 }

Lines 271-279

  271     while (PyDict_Next(dict, &position, &key, &value)) {
  272         if (PyUnicode_CheckExact(key) && PyUnicode_GET_LENGTH(key) == length) {
  273             const int comparison = PyUnicode_CompareWithASCIIString(key, name);
  274             if (PyErr_Occurred())
! 275                 throw py::error_already_set();
  276             if (comparison == 0)
  277                 return value;
  278         }
  279     }

Lines 297-305

  297         } else if (PyUnicode_GET_LENGTH(key) ==
  298                    static_cast<Py_ssize_t>(sizeof("_native_fetch_state") - 1)) {
  299             const int comparison = PyUnicode_CompareWithASCIIString(key, "_native_fetch_state");
  300             if (PyErr_Occurred())
! 301                 throw py::error_already_set();
  302             if (comparison == 0)
  303                 result.value = value;
  304         }
  305     }

Lines 307-315

  307 }
  308 
  309 inline bool owned_by(State* state, Context* context, PyObject* receiver) {
  310     if (state->context != reinterpret_cast<PyObject*>(context) || !state->owner)
! 311         return false;
  312 #if PY_VERSION_HEX >= 0x030D0000
  313     PyObject* owner = nullptr;
  314     const int result = PyWeakref_GetRef(state->owner, &owner);
  315     if (result < 0)

Lines 319-327

  319 #else
  320     // The GIL and the call's strong receiver reference cover this borrowed read.
  321     PyObject* owner = PyWeakref_GetObject(state->owner);
  322     if (!owner)
! 323         throw py::error_already_set();
  324     return owner == receiver;
  325 #endif
  326 }

Lines 339-350

  339             reinterpret_cast<void*>(PyObject_GenericSetAttr))
  340         return false;
  341     auto attributes = steal(PyType_GetDict(Py_TYPE(receiver)));
  342     if (!attributes)
! 343         throw py::error_already_set();
  344     if (exact_item(attributes.ptr(), "__dict__") != context->dictDescriptor ||
  345         exact_item(attributes.ptr(), "_native_fetch_state"))
! 346         return false;
  347     auto* handle = exact_item(dict, "hstmt");
  348     auto* index = exact_item(dict, "_next_row_index");
  349     auto* messages = exact_item(dict, "messages");
  350     return exact_item(dict, "closed") == Py_False && handle && handle != Py_None &&

Lines 407-415

  407 #endif
  408         weakOwner = PyWeakref_NewRef(receiver, nullptr);
  409     auto owner = steal(weakOwner);
  410     if (!owner)
! 411         throw py::error_already_set();
  412     PyObject* allocated;
  413 #ifdef MSSQL_NATIVE_FETCH_TEST_FAULTS
  414     if (test_fault(context, 2))
  415         allocated = PyErr_NoMemory();

Lines 418-426

  418         allocated = PyType_GenericAlloc(
  419             reinterpret_cast<PyTypeObject*>(context->stateType), 0);
  420     auto result = steal(allocated);
  421     if (!result)
! 422         throw py::error_already_set();
  423     auto* state = reinterpret_cast<State*>(result.ptr());
  424 #ifdef MSSQL_NATIVE_FETCH_TEST_FAULTS
  425     state->testContext = context;
  426     ++context->faults.statesAllocated;

Lines 441-453

  441     // Allocation can run GC callbacks. Revalidate before a callback-free write,
  442     // retaining the old value so publication cannot release its last reference.
  443     auto now = instance_dict(receiver);
  444     if (!now)
! 445         throw py::error_already_set();
  446     entry = state_entry(dict.ptr());
  447     if (!now.is(dict) || entry.value != old.ptr() ||
  448         !storage_admissible(context, receiver, dict.ptr(), entry))
! 449         return {};
  450     int published;
  451 #ifdef MSSQL_NATIVE_FETCH_TEST_FAULTS
  452     if (test_fault(context, 4)) {
  453         PyErr_NoMemory();

Lines 455-463

  455     } else
  456 #endif
  457         published = PyDict_SetItem(dict.ptr(), context->stateKey, result.ptr());
  458     if (published < 0)
! 459         throw py::error_already_set();
  460 #ifdef MSSQL_NATIVE_FETCH_TEST_FAULTS
  461     ++context->faults.publications;
  462 #endif
  463     return result;

Lines 490-498

  490     auto plan = std::make_shared<Plan>(std::move(dict), epoch);
  491     py::object values = rt[2](py::handle(self), plan->cursorDict);
  492     if (!values.is_none()) {
  493         if (!PyTuple_CheckExact(values.ptr()) || PyTuple_GET_SIZE(values.ptr()) != InputCount)
! 494             throw py::type_error("Invalid native fetch plan");
  495         plan->inputs = std::move(values);
  496         if (!PyDict_CheckExact(plan->at(CursorDict).ptr()) ||
  497             !PyDict_CheckExact(plan->at(ConnectionDict).ptr()) ||
  498             !PyDict_CheckExact(plan->at(ColumnMap).ptr()) ||

Lines 503-516

  503             !PyType_Check(plan->at(ConnectionType).ptr()) ||
  504             !PyLong_CheckExact(plan->at(NextIndex).ptr()) ||
  505             !PyLong_CheckExact(plan->at(ArraySize).ptr()) ||
  506             !PyCallable_Check(plan->at(Factory).ptr()))
! 507             throw py::type_error("Invalid native fetch plan fields");
  508         // current() relies on this watch, not on the Python policy registering it.
  509         auto* observer = static_cast<Observer*>(
  510             PyCapsule_GetPointer(rt[0].ptr(), "mssql_python.native_fetch.Observer"));
  511         if (!observer || PyDict_Watch(observer->dict, plan->cursorDict.ptr()) < 0)
! 512             throw py::error_already_set();
  513         plan->handle = plan->at(Handle).cast<SqlHandlePtr>();
  514         plan->charEncoding = plan->at(CharEncoding).cast<std::string>();
  515         plan->wideEncoding = plan->at(WideEncoding).cast<std::string>();
  516         plan->charCtype = EffectiveCharCtypeForFetch(

Lines 541-549

  541         return false;
  542     auto dict = instance_dict(self);
  543     auto connectionDict = instance_dict(plan.at(Connection).ptr());
  544     if (!dict || !connectionDict)
! 545         throw py::error_already_set();
  546     // fetch() checks the key association before use. The native dictionary watch
  547     // invalidates this epoch on a later state-key change; replacement is checked here.
  548     return dict.is(plan.cursorDict) && connectionDict.is(plan.at(ConnectionDict)) &&
  549            state->dict == dict.ptr() && owned_by(state, context, self) &&

Lines 564-572

  564     internalDict = plan.cursorDict.ptr();
  565     internalKey = name.ptr();
  566     internalValue = value.ptr();
  567     if (PyDict_SetItem(plan.cursorDict.ptr(), name.ptr(), value.ptr()) < 0)
! 568         throw py::error_already_set();
  569 }
  570 
  571 inline py::object finish_python(PyObject* self, Mode mode, const py::list& values,
  572                                  SQLRETURN ret, bool positionCommitted) {

Lines 576-584

  576         py::list rows;
  577         if (SQL_SUCCEEDED(ret))
  578             rows.append(values);
  579         if (positionCommitted)
! 580             return cursor.attr("_wrap_fetchmany")(rows);
  581         return cursor.attr("_complete_fetchmany")(rows, ret);
  582     }
  583     py::object row = positionCommitted ? cursor.attr("_wrap_fetchone")(values)
  584                                       : cursor.attr("_complete_fetchone")(values, ret);

Lines 584-592

  584                                       : cursor.attr("_complete_fetchone")(values, ret);
  585     if (mode == Mode::Value && !row.is_none())
  586         return row[py::int_(0)];
  587     if (mode == Mode::Next && row.is_none())
! 588         throw py::stop_iteration();
  589     return row;
  590 }
  591 
  592 inline bool conversion_ineligible(py::handle data) {

Lines 609-617

  609         else if (length == static_cast<Py_ssize_t>(sizeof("_uuid_str_indices") - 1) &&
  610                  PyUnicode_CompareWithASCIIString(key, "_uuid_str_indices") == 0)
  611             uuidIndices = value;
  612         if (PyErr_Occurred())
! 613             throw py::error_already_set();
  614     }
  615     return !converters || !PyTuple_CheckExact(converters) ||
  616            PyTuple_GET_SIZE(converters) != 0 || uuidIndices != Py_None;
  617 }

Lines 624-632

  624         if (!PyUnicode_CheckExact(key))
  625             return false;
  626         const int comparison = PyUnicode_CompareWithASCIIString(key, "size");
  627         if (PyErr_Occurred())
! 628             throw py::error_already_set();
  629         if (comparison != 0)
  630             return false;
  631     } else if (nargs != 1 || count != 0) {
  632         return false;

Lines 635-643

  635         return false;
  636     int overflow = 0;
  637     const long size = PyLong_AsLongAndOverflow(args[0], &overflow);
  638     if (PyErr_Occurred())
! 639         throw py::error_already_set();
  640     return overflow || size != 1;
  641 }
  642 
  643 inline bool single_size(const Plan& plan, PyObject* const* args, Py_ssize_t nargs,

Lines 650-658

  650         if (!PyUnicode_CheckExact(key))
  651             return false;
  652         const int comparison = PyUnicode_CompareWithASCIIString(key, "size");
  653         if (PyErr_Occurred())
! 654             throw py::error_already_set();
  655         if (comparison != 0)
  656             return false;
  657     }
  658     PyObject* size = nargs + count ? args[0] : Py_None;

Lines 662-670

  662         return false;
  663     int overflow = 0;
  664     const long value = PyLong_AsLongAndOverflow(size, &overflow);
  665     if (PyErr_Occurred())
! 666         throw py::error_already_set();
  667     return !overflow && value == 1;
  668 }
  669 
  670 inline py::object fetch(PyObject* self, Context* context, Mode mode,

Lines 684-692

  684         return fallback(self, rt, mode, args, nargs, keywords);
  685 
  686     py::dict dict = instance_dict(object);
  687     if (!dict)
! 688         throw py::error_already_set();
  689     bool needsRevalidation;
  690     auto stateOwner = acquire_state(context, self, dict, needsRevalidation);
  691     auto* state = reinterpret_cast<State*>(stateOwner.ptr());
  692     auto admittedDict = needsRevalidation ? instance_dict(self) : dict;

Lines 690-701

  690     auto stateOwner = acquire_state(context, self, dict, needsRevalidation);
  691     auto* state = reinterpret_cast<State*>(stateOwner.ptr());
  692     auto admittedDict = needsRevalidation ? instance_dict(self) : dict;
  693     if (!admittedDict)
! 694         throw py::error_already_set();
  695     if (!admittedDict.is(dict) ||
  696         (needsRevalidation && state && !associated(state, context, self, dict.ptr())))
! 697         return fallback(self, rt, mode, args, nargs, keywords);
  698     const uint64_t epoch = mutationEpoch.load(std::memory_order_relaxed);
  699     auto plan = state && state->plan ? *state->plan : nullptr;
  700     bool stale = !plan || plan->epoch != epoch || !plan->cursorDict.is(dict);
  701     if (!stale && !plan->inputs.is_none()) {

Lines 707-715

  707     if (stale) {
  708         plan = prepare(self, rt, dict, epoch);
  709         auto preparedDict = instance_dict(self);
  710         if (!preparedDict)
! 711             throw py::error_already_set();
  712         if (state && state->plan && preparedDict.is(dict) &&
  713             associated(state, context, self, dict.ptr())) {
  714             auto old = std::move(*state->plan);
  715             *state->plan = plan;

Lines 724-735

  724             return fallback(self, rt, mode, args, nargs, keywords);
  725         auto metadata = plan->handle->resultMetadata.snapshot();
  726         if (metadata.generation != plan->metadata.generation ||
  727             metadata.metadata != plan->metadata.metadata)
! 728             return fallback(self, rt, mode, args, nargs, keywords);
  729     }
  730     if (mode == Mode::Value && PyList_GET_SIZE(plan->at(Description).ptr()) == 0)
! 731         return fallback(self, rt, mode, args, nargs, keywords);
  732 
  733     PERF_TIMER("NativeCursor::fetch");
  734     py::list values;
  735     SQLRETURN ret = FetchSingleRow(plan->handle, values, plan->charEncoding,

Lines 746-758

  746             throw py::stop_iteration();
  747         return py::none();
  748     }
  749     if (plan->nextIndex == std::numeric_limits<Py_ssize_t>::max())
! 750         return finish_python(self, mode, values, ret, false);
  751     py::int_ next(plan->nextIndex + 1);
  752     py::int_ rowNumber(plan->nextIndex);
  753     if (!current(self, context, state, *plan, rt))
! 754         return finish_python(self, mode, values, ret, false);
  755     auto counterNames = py::reinterpret_borrow<py::tuple>(rt[5]);
  756     set_counter(*plan, counterNames[0], next);
  757     set_counter(*plan, counterNames[1], rowNumber);
  758     set_counter(*plan, counterNames[2], next);

Lines 757-765

  757     set_counter(*plan, counterNames[1], rowNumber);
  758     set_counter(*plan, counterNames[2], next);
  759     ++plan->nextIndex;
  760     if (!current(self, context, state, *plan, rt))
! 761         return finish_python(self, mode, values, ret, true);
  762 
  763     if (mode == Mode::Value) {
  764         // Decode every column, not just column zero. Eligible plans have no
  765         // converters, UUID processing or customized Row/fetchone behavior.

Lines 791-799

  791                                PyObject* keywords) {
  792     try {
  793         auto* context = reinterpret_cast<Context*>(owner);
  794         if (!context->functions)
! 795             throw std::runtime_error("Native fetch context is not initialized");
  796         if (context->runtime && count > 0 &&
  797             PyObject_TypeCheck(args[0], reinterpret_cast<PyTypeObject*>(context->cursorType)) &&
  798             (mode == Mode::Many ||
  799              (count == 1 && (!keywords || PyTuple_GET_SIZE(keywords) == 0)))) {

Lines 821-852

  821         auto* context = reinterpret_cast<Context*>(owner);
  822         if (context->disabled)
  823             throw std::runtime_error("Native fetch installation is disabled after incomplete rollback");
  824         if (context->cursorType || context->active)
! 825             throw std::runtime_error("Native fetch context is already installed");
  826         PyObject* cursorType;
  827         PyObject* functions;
  828         if (!PyArg_ParseTuple(args, "OO", &cursorType, &functions))
! 829             return nullptr;
  830         if (!PyType_CheckExact(cursorType) || !PyTuple_CheckExact(functions) ||
  831             PyTuple_GET_SIZE(functions) != 4)
! 832             throw py::type_error("Invalid native fetch installation");
  833         auto bases = steal(PyObject_GetAttrString(cursorType, "__bases__"));
  834         if (!bases)
! 835             throw py::error_already_set();
  836         if (!PyTuple_CheckExact(bases.ptr()) || PyTuple_GET_SIZE(bases.ptr()) != 1 ||
  837             PyTuple_GET_ITEM(bases.ptr(), 0) != reinterpret_cast<PyObject*>(&PyBaseObject_Type))
! 838             throw py::type_error("Native fetch requires a plain Python cursor");
  839         for (Py_ssize_t i = 0; i < 4; ++i) {
  840             if (!PyFunction_Check(PyTuple_GET_ITEM(functions, i)))
! 841                 throw py::type_error("Native fetch fallbacks must be Python functions");
  842         }
  843         auto attributes = steal(PyType_GetDict(reinterpret_cast<PyTypeObject*>(cursorType)));
  844         if (!attributes)
! 845             throw py::error_already_set();
  846         auto* descriptor = exact_item(attributes.ptr(), "__dict__");
  847         if (!descriptor || Py_TYPE(descriptor) != &PyGetSetDescr_Type)
! 848             throw py::type_error("Native fetch requires an ordinary cursor dictionary");
  849         context->cursorType = Py_NewRef(cursorType);
  850         context->functions = Py_NewRef(functions);
  851         context->dictDescriptor = Py_NewRef(descriptor);
  852         Py_RETURN_NONE;

Lines 873-883

  873     for (Py_ssize_t field = 4; field <= 5; ++field) {
  874         auto* names = PyTuple_GET_ITEM(runtime, field);
  875         const Py_ssize_t count = field == 4 ? 5 : 3;
  876         if (!PyTuple_CheckExact(names) || PyTuple_GET_SIZE(names) != count) {
! 877             PyErr_SetString(PyExc_TypeError, "Invalid native fetch field names");
! 878             return nullptr;
! 879         }
  880         for (Py_ssize_t i = 0; i < count; ++i) {
  881             auto* name = PyTuple_GET_ITEM(names, i);
  882             if (!PyUnicode_CheckExact(name) ||
  883                 PyUnicode_CompareWithASCIIString(

Lines 881-892

  881             auto* name = PyTuple_GET_ITEM(names, i);
  882             if (!PyUnicode_CheckExact(name) ||
  883                 PyUnicode_CompareWithASCIIString(
  884                     name, field == 4 ? rowNames[i] : counterNames[i]) != 0) {
! 885                 if (!PyErr_Occurred())
! 886                     PyErr_SetString(PyExc_TypeError, "Invalid native fetch field name");
! 887                 return nullptr;
! 888             }
  889         }
  890     }
  891     context->runtime = Py_NewRef(runtime);
  892     Py_RETURN_NONE;

Lines 894-904

  894 
  895 inline PyObject* abort_install(PyObject* owner, PyObject*) {
  896     auto* context = reinterpret_cast<Context*>(owner);
  897     if (context->disabled || context->runtime || context->active) {
! 898         PyErr_SetString(PyExc_RuntimeError, "Cannot abort an active native fetch context");
! 899         return nullptr;
! 900     }
  901     auto* cursorType = context->cursorType;
  902     auto* functions = context->functions;
  903     auto* descriptor = context->dictDescriptor;
  904     context->cursorType = context->functions = context->dictDescriptor = nullptr;

Lines 910-920

  910 
  911 inline PyObject* disable_install(PyObject* owner, PyObject*) {
  912     auto* context = reinterpret_cast<Context*>(owner);
  913     if (context->runtime || context->active) {
! 914         PyErr_SetString(PyExc_RuntimeError, "Cannot disable an active native fetch context");
! 915         return nullptr;
! 916     }
  917     // Keep every forwarding owner: even descriptors restored on the class may
  918     // have been saved elsewhere. Unpublished entries can never create State.
  919     context->disabled = true;
  920     Py_RETURN_NONE;

Lines 991-999

  991         auto* context = reinterpret_cast<Context*>(owner);
  992         if (reinterpret_cast<PyObject*>(Py_TYPE(receiver)) == context->cursorType) {
  993             auto dict = instance_dict(receiver);
  994             if (!dict)
! 995                 throw py::error_already_set();
  996             auto* value = state_entry(dict.ptr()).value;
  997             if (value && reinterpret_cast<PyObject*>(Py_TYPE(value)) == context->stateType) {
  998                 auto stateOwner = py::reinterpret_borrow<py::object>(value);
  999                 auto* state = reinterpret_cast<State*>(value);

Lines 1003-1013

  1003             }
  1004         }
  1005         Py_RETURN_NONE;
  1006     } catch (...) {
! 1007         py::detail::try_translate_exceptions();
! 1008         return nullptr;
! 1009     }
  1010 }
  1011 
  1012 inline void bind(py::module_& m) {
  1013 #ifdef _MSC_VER

Lines 1048-1056

  1048                                  Py_TPFLAGS_IMMUTABLETYPE,
  1049                              stateSlots};
  1050     auto stateType = steal(PyType_FromSpec(&stateSpec));
  1051     if (!stateType)
! 1052         throw py::error_already_set();
  1053     static PyType_Slot contextSlots[] = {
  1054         {Py_tp_new, reinterpret_cast<void*>(internal_new)},
  1055         {Py_tp_dealloc, reinterpret_cast<void*>(dealloc<Context, context_clear>)},
  1056         {Py_tp_traverse, reinterpret_cast<void*>(context_traverse)},

Lines 1062-1079

  1062                                    Py_TPFLAGS_IMMUTABLETYPE,
  1063                                contextSlots};
  1064     auto contextType = steal(PyType_FromSpec(&contextSpec));
  1065     if (!contextType)
! 1066         throw py::error_already_set();
  1067     auto contextOwner = steal(PyType_GenericAlloc(
  1068         reinterpret_cast<PyTypeObject*>(contextType.ptr()), 0));
  1069     if (!contextOwner)
! 1070         throw py::error_already_set();
  1071     auto* context = reinterpret_cast<Context*>(contextOwner.ptr());
  1072     context->stateType = stateType.release().ptr();
  1073     context->stateKey = PyUnicode_InternFromString("_native_fetch_state");
  1074     if (!context->stateKey)
! 1075         throw py::error_already_set();
  1076     m.attr("_native_fetch_schema") = 2;
  1077 #ifdef MSSQL_NATIVE_FETCH_TEST_FAULTS
  1078     m.attr("_native_fetch_test_fault_schema") = 1;
  1079 #endif

Lines 1081-1092

  1081     py::tuple entries(4);
  1082     for (Py_ssize_t i = 0; i < 4; ++i) {
  1083         py::object function = steal(PyCFunction_NewEx(&methods[i], contextOwner.ptr(), nullptr));
  1084         if (!function)
! 1085             throw py::error_already_set();
  1086         py::object descriptor = steal(PyInstanceMethod_New(function.ptr()));
  1087         if (!descriptor)
! 1088             throw py::error_already_set();
  1089         PyTuple_SET_ITEM(entries.ptr(), i, descriptor.release().ptr());
  1090     }
  1091     m.attr("_native_fetch_methods") = std::move(entries);
  1092     static PyMethodDef controls[] = {

Lines 1103-1111

  1103     };
  1104     for (auto* method = controls; method->ml_name; ++method) {
  1105         auto function = steal(PyCFunction_NewEx(method, contextOwner.ptr(), nullptr));
  1106         if (!function)
! 1107             throw py::error_already_set();
  1108         m.attr(method->ml_name) = std::move(function);
  1109     }
  1110 
  1111     auto observer = std::make_unique<Observer>();

Lines 1109-1141

  1109     }
  1110 
  1111     auto observer = std::make_unique<Observer>();
  1112     auto unavailable = [&m]() {
! 1113         PyErr_Clear();
! 1114         if (PyErr_WarnEx(PyExc_RuntimeWarning,
! 1115                         "Native fetch prototype disabled: CPython watcher slots unavailable",
! 1116                         1) < 0)
! 1117             throw py::error_already_set();
! 1118         m.attr("_fetch_observer") = py::none();
! 1119     };
  1120     observer->dict = PyDict_AddWatcher(dict_changed);
  1121     if (observer->dict < 0) {
! 1122         unavailable();
! 1123         return;
! 1124     }
  1125     observer->type = PyType_AddWatcher(type_changed);
  1126     if (observer->type < 0) {
! 1127         PyDict_ClearWatcher(observer->dict);
! 1128         unavailable();
! 1129         return;
! 1130     }
  1131     observer->function = PyFunction_AddWatcher(function_changed);
  1132     if (observer->function < 0) {
! 1133         PyType_ClearWatcher(observer->type);
! 1134         PyDict_ClearWatcher(observer->dict);
! 1135         unavailable();
! 1136         return;
! 1137     }
  1138     // One registration per native-module lifetime. The callbacks retain no
  1139     // watched objects; CPython removes registrations at interpreter teardown.
  1140     m.attr("_fetch_observer") = py::capsule(
  1141         observer.release(), "mssql_python.native_fetch.Observer", [](void* pointer) {

Lines 1146-1165

  1146                                            const py::tuple& dicts, const py::tuple& types) {
  1147         auto* observer = static_cast<Observer*>(
  1148             PyCapsule_GetPointer(capsule.ptr(), "mssql_python.native_fetch.Observer"));
  1149         if (!observer)
! 1150             throw py::error_already_set();
  1151         for (auto dict : dicts) {
  1152             if (PyDict_Watch(observer->dict, dict.ptr()) < 0)
! 1153                 throw py::error_already_set();
  1154         }
  1155         for (auto type : types) {
  1156             if (!PyType_Check(type.ptr()))
! 1157                 throw py::type_error("Expected a type dependency");
  1158             if (!(PyType_GetFlags(reinterpret_cast<PyTypeObject*>(type.ptr())) &
  1159                   Py_TPFLAGS_IMMUTABLETYPE) &&
  1160                 PyType_Watch(observer->type, type.ptr()) < 0)
! 1161                 throw py::error_already_set();
  1162         }
  1163     });
  1164 }

mssql_python/pybind/row_factory.hpp

Lines 16-24

  16                                  const py::tuple& attribute_names) {
  17     auto* row_type = reinterpret_cast<PyTypeObject*>(row_class.ptr());
  18     py::object row = steal(row_type->tp_alloc(row_type, 0));
  19     if (!row)
! 20         throw py::error_already_set();
  21     PyObject* fields[] = {values.ptr(), column_map.ptr(), cursor.ptr(),
  22                           column_map_lower.ptr(), column_names.ptr()};
  23     for (Py_ssize_t i = 0; i < 5; ++i) {
  24         if (SetAttribute(row.ptr(), PyTuple_GET_ITEM(attribute_names.ptr(), i),

Lines 22-30

  22                           column_map_lower.ptr(), column_names.ptr()};
  23     for (Py_ssize_t i = 0; i < 5; ++i) {
  24         if (SetAttribute(row.ptr(), PyTuple_GET_ITEM(attribute_names.ptr(), i),
  25                          fields[i]) < 0)
! 26             throw py::error_already_set();
  27     }
  28     return row;
  29 }


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 62.6%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python._native_fetch.py: 77.8%
mssql_python.pybind.ddbc_bindings.cpp: 80.4%
mssql_python.pybind.connection.connection_pool.cpp: 81.3%
mssql_python.pybind.connection.connection.cpp: 84.1%
mssql_python.logging.py: 86.2%
mssql_python.pybind.native_fetch.hpp: 88.7%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI balanced review requested due to automatic review settings October 22, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Native ownership and watcher-based dispatch need final human review, with cross-platform, sanitizer, and full performance qualification still outstanding.

1 open issue

Review effort: Balanced

@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as draft October 28, 2026 05:02
Do not force eligible fetches through repeated Python-policy preparation when the profiler is enabled. Keep native fetch attribution on the actual execution path, retain all safety and debug-logging guards, and cover both profiler states in the existing operation-count regression.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 05:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because a reproduced type-watcher notification gap may allow cached fetch plans to bypass cursor overrides, while the full driver impact remains unverified.

1 open issue

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 28, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Active issues can turn nullable DECIMAL/NUMERIC results into NaN instead of None and invoke state or policy hooks before malformed fetchmany calls receive the expected TypeError.

2 open issues

Review effort: Balanced

Comment on lines +796 to +802
if (context->runtime && count > 0 &&
PyObject_TypeCheck(args[0], reinterpret_cast<PyTypeObject*>(context->cursorType)) &&
(mode == Mode::Many ||
(count == 1 && (!keywords || PyTuple_GET_SIZE(keywords) == 0)))) {
return fetch(args[0], context, mode, args + 1,
count - 1, keywords).release().ptr();
}
@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review October 28, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants