Skip to content

PERF: Optimize fetchone, fetchmany(1) and fetchval paths - #829

Open
Jahnvi Thakkar (jahnvi480) wants to merge 22 commits into
mainfrom
jahnvi/candidate-a-fetchone-column-count
Open

Jahnvi Thakkar (jahnvi480) wants to merge 22 commits into
mainfrom
jahnvi/candidate-a-fetchone-column-count

Conversation

@jahnvi480

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

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#48364

Not applicable; the work item above is the single reference.


Summary

  • Consolidate fetchone(), fetchmany(1) and inherited fetchval()/iterator optimization work in this existing PR.
  • Retain generation-scoped full column-count caching, separate from prefix SQLGetData metadata. Direct column-count calls remain uncached; all nine original count-cache regressions remain.
  • Share native single-row fetching and reuse successful unbinds only within a valid generation. Invalidate before binding, including Arrow/partial binds, and during cleanup. The marker does not certify row-array attributes.
  • Route only all-numeric fetchmany(1) results through SQLFetchScroll and SQLGetData, preserving eager count/name validation and row-array configuration/cleanup. Mixed INT/NVARCHAR and other types retain their existing native paths.
  • Use direct one-row wrapping only for exact built-in integer size 1, one returned row, canonical Row/factory, and no converter/UUID work. Preserve larger-request tails, substituted factories, integer subclasses and actual fetchone() overrides.
  • Keep full-row construction and all-column converter callbacks for fetchval(). Replace Python single-row phase context managers with equivalent paired start/stop instrumentation.
  • Add numeric parity, EOF, override/factory, diagnostics, generation-change, mixed-API and fault-recovery regressions, plus attribution documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:00
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ No regression detected

No consistent slowdowns detected across all 2 environments.

0 IMPROVEMENTS 0 SLOWDOWNS 2/2 ENVIRONMENTS

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

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: ddbc::SQLDescribeCol::driver_call +0.000 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (1000 -> 1 calls).
Repeated positional queries: ddbc::FetchOne_wrap +0.009 ms; py::execute::diag_records +0.006 ms; py::execute::param_prep +0.005 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
Repeated named-parameter queries: ddbc::FetchOne_wrap +0.025 ms; ddbc::SQLGetData_wrap +0.001 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
10,000 scalar values / fetchval() (debug disabled): ddbc::SQLGetData_wrap +0.050 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (10000 -> 1 calls).

Unix / SQL Server 2025

Row-by-row fetching: ddbc::SQLGetData_wrap +0.014 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (1000 -> 1 calls).
Repeated positional queries: ddbc::FetchOne_wrap +0.021 ms; py::execute::diag_records +0.012 ms; py::execute::param_prep +0.007 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
Repeated named-parameter queries: ddbc::FetchOne_wrap +0.035 ms; py::execute::diag_records +0.020 ms; py::fetchone::cpp_call +0.008 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent).
10,000 scalar values / fetchval() (debug disabled): ddbc::SQLGetData_wrap +0.047 ms; ddbc::AppendDiagRecords::SQLGetDiagRec_call +0.008 ms. Call changes: ddbc::FetchSingleRow::SQL_UNBIND (added, removed, or intermittent); ddbc::SQLNumResultCols_wrap (10000 -> 1 calls).

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.305 ms 10.167 ms +0.1% no signal
SELECT queries 1.062 ms 1.062 ms -0.6% no signal
Row insertion 34.689 ms 34.449 ms -1.3% no signal
Executemany inserts 159.293 ms 159.222 ms +2.8% no signal
Fetch-all queries 120.900 ms 121.133 ms +0.2% no signal
Row-by-row fetching 14.392 ms 13.142 ms -8.4% no signal
Batched row fetching 118.357 ms 118.577 ms +1.3% no signal
Transaction commit and rollback 115.079 ms 113.755 ms -1.2% no signal
Arrow row fetching 95.260 ms 95.226 ms -0.4% no signal
100,000-row insertion 441.657 ms 447.398 ms +1.4% no signal
Row fetching in batches of 100 122.260 ms 120.860 ms -0.4% no signal
Row fetching in batches of 10,000 137.417 ms 136.802 ms +0.7% no signal
Repeated positional queries 34.513 ms 33.620 ms -2.6% no signal
Repeated named-parameter queries 36.831 ms 35.485 ms -1.6% no signal
Legacy 100,000-row insertion 357.183 ms 355.291 ms -1.5% no signal
Insertion with explicit input sizes 507.765 ms 493.681 ms -4.2% no signal
Joined aggregation queries 181.516 ms 179.484 ms -0.1% no signal
Large joined-result fetching 182.135 ms 179.995 ms +0.3% no signal
1.2-million-row fetching 3467.075 ms 3474.084 ms -0.4% no signal
Common table expression queries 5.416 ms 5.441 ms +0.8% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.280 ms 1.208 ms -6.6% no signal
10,000 scalar values / fetchval() (debug disabled) 106.623 ms 95.283 ms -11.2% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 96.504 ms 96.555 ms +0.2% no signal
SELECT queries 1.057 ms 1.066 ms +2.2% no signal
Row insertion 34.179 ms 34.215 ms +0.2% no signal
Executemany inserts 153.846 ms 149.619 ms -2.4% no signal
Fetch-all queries 120.155 ms 119.411 ms -0.2% no signal
Row-by-row fetching 14.215 ms 13.041 ms -7.1% no signal
Batched row fetching 116.588 ms 117.952 ms +1.2% no signal
Transaction commit and rollback 115.468 ms 113.245 ms -2.1% no signal
Arrow row fetching 94.120 ms 94.234 ms -0.9% no signal
100,000-row insertion 437.629 ms 428.226 ms +0.7% no signal
Row fetching in batches of 100 120.805 ms 119.993 ms -0.3% no signal
Row fetching in batches of 10,000 133.146 ms 132.348 ms -1.9% no signal
Repeated positional queries 33.702 ms 33.352 ms -2.0% no signal
Repeated named-parameter queries 36.252 ms 35.606 ms -1.9% no signal
Legacy 100,000-row insertion 346.674 ms 342.243 ms -0.9% no signal
Insertion with explicit input sizes 486.182 ms 476.878 ms -2.5% no signal
Joined aggregation queries 159.051 ms 159.427 ms +0.0% no signal
Large joined-result fetching 174.360 ms 175.239 ms +1.6% no signal
1.2-million-row fetching 3448.624 ms 3416.630 ms -0.9% no signal
Common table expression queries 5.164 ms 5.084 ms -1.2% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.445 ms 1.392 ms -4.3% no signal
10,000 scalar values / fetchval() (debug disabled) 105.703 ms 95.426 ms -9.8% no signal
Build and measurement details

ADO build 182108

PR head: 0ec98941436b12367ef4b0e44844e4c88bfb588f
Base: 0facea305789bdec35d2be8e29346d20bcdb6711
Measured merge: 19f6e287177ec0cc4488119847ff0503b1073a60

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4295.3; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5005.3; 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.

This headline uses the original 22-task profiling-enabled diagnostics; separate OFF/OFF latency and ON/OFF route measurements, when available, are retained in the raw artifacts and are not headline inputs.

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

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.

Copilot review overview

🔵 Needs a closer look

The native hot-path change still requires live correctness and performance validation, as acknowledged by the draft description.

Review effort: Balanced
Findings: None

What changed in this PR

Introduces an experimental native cache to avoid repeated column-count queries during fetchone.

Changes:

  • Caches full column counts by metadata generation.
  • Reuses cached counts while preserving invalidation and error handling.
  • Adds nine subprocess-isolated integration scenarios.
File Description
mssql_python/​pybind/​ddbc_bindings.cpp Uses the cached count in FetchOne_wrap.
mssql_python/​pybind/​result_metadata.hpp Stores and invalidates full column counts.
tests/​test_fetch_settings_cache.py Tests reuse, invalidation, failures, and mixed fetching.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Use a separate connection for the cross-handle assertion without requiring MARS. Preserve all fetch and native call-count assertions.

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

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.

Copilot review overview

🔵 Needs a closer look

The native hot-path change remains an explicitly unvalidated draft with correctness tests and performance measurements still pending.

Review effort: Balanced
Findings: None

Copilot AI balanced review requested due to automatic review settings October 1, 2026 10:16

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.

Copilot review overview

🔵 Needs a closer look

The native cache behavior and performance impact remain unvalidated by runtime execution.

Review effort: Balanced
Findings: None

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

84%


🎯 Overall Coverage

84%


📈 Total Lines Covered: 9734 out of 11454
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/cursor.py (85.4%): Missing lines 2921,2939-2940,2952-2955,2979,3031,3069-3072,3113
  • mssql_python/pybind/ddbc_bindings.cpp (92.1%): Missing lines 3355,3734-3736,3753-3755,3825-3827,3847,3901,5481
  • mssql_python/pybind/ddbc_bindings.h (100%)
  • mssql_python/pybind/result_metadata.hpp (100%)
  • mssql_python/pybind/row_factory.hpp (72.2%): Missing lines 15-16,20,27,31,40,59,100,152-172,176-177,185-186,189-190
  • mssql_python/row.py (100%)

Summary

  • Total: 399 lines
  • Missing: 62 lines
  • Coverage: 84%

mssql_python/cursor.py

Lines 2917-2925

  2917                         self.messages,
  2918                     )
  2919             finally:
  2920                 if started:
! 2921                     perf_stop("py::fetchone::cpp_call", started)
  2922 
  2923             return self._finish_fetchone(ret, row_data)
  2924         except Exception:
  2925             # On error, don't increment rownumber - rethrow the error

Lines 2935-2944

  2935             return None
  2936 
  2937         # Update internal position after successful fetch
  2938         if self._skip_increment_for_next_fetch:
! 2939             self._skip_increment_for_next_fetch = False
! 2940             self._next_row_index += 1
  2941         else:
  2942             self._increment_rownumber()
  2943 
  2944         self.rowcount = self._next_row_index

Lines 2948-2959

  2948         started = perf_start()
  2949         try:
  2950             if not converter_map and not self._uuid_str_indices:
  2951                 if native and not started and _native_row_eligible(Row):
! 2952                     row_type = Row
! 2953                     factory = row_type._fast_create
! 2954                     column_names = self._cached_result_columns
! 2955                     return (
  2956                         _NATIVE_ROW_PLAN,
  2957                         row_type,
  2958                         column_map,
  2959                         self,

Lines 2975-2983

  2975                 column_names=self._cached_result_columns,
  2976             )
  2977         finally:
  2978             if started:
! 2979                 perf_stop("py::fetchone::row_wrap", started)
  2980 
  2981     def fetchmany(self, size: Optional[int] = None) -> List[Row]:
  2982         """
  2983         Fetch the next set of rows of a query result.

Lines 3027-3035

  3027                         self.messages,
  3028                     )
  3029             finally:
  3030                 if started:
! 3031                     perf_stop("py::fetchmany::cpp_call", started)
  3032 
  3033             return self._finish_fetchmany(ret, rows_data, size=size)
  3034         except Exception:
  3035             # On error, don't increment rownumber - rethrow the error

Lines 3065-3076

  3065                     and Row is _DEFAULT_ROW_TYPE
  3066                     and Row._fast_create is _DEFAULT_FAST_ROW_CREATE
  3067                 ):
  3068                     if native and not started and _native_row_eligible(Row):
! 3069                         row_type = Row
! 3070                         factory = row_type._fast_create
! 3071                         column_names = self._cached_result_columns
! 3072                         return (
  3073                             _NATIVE_ROW_PLAN,
  3074                             row_type,
  3075                             column_map,
  3076                             self,

Lines 3109-3117

  3109                 for row_data in rows_data
  3110             ]
  3111         finally:
  3112             if started:
! 3113                 perf_stop("py::fetchmany::row_wrap", started)
  3114 
  3115     def fetchall(self) -> List[Row]:
  3116         """
  3117         Fetch all (remaining) rows of a query result.

mssql_python/pybind/ddbc_bindings.cpp

Lines 3351-3359

  3351                     }
  3352                 }
  3353             }
  3354         }
! 3355         buffer.resize(offset + bytesRead);
  3356         if (bytesRead > 0) {
  3357             LOG("FetchLobColumnData: Appended %zu bytes at loop %d", bytesRead, loopCount);
  3358         }
  3359         if (ret == SQL_SUCCESS) {

Lines 3730-3740

  3730                                         PyErr_SetString(PyExc_ValueError, "embedded null character");
  3731                                         throw py::error_already_set();
  3732                                     }
  3733                                     decoded = steal(PyUnicode_Decode(
! 3734                                         reinterpret_cast<const char*>(dataBuffer.data()),
! 3735                                         static_cast<Py_ssize_t>(dataLen),
! 3736                                         decodeEncoding.c_str(), "strict"));
  3737                                     if (!decoded) throw py::error_already_set();
  3738                                     if (PyList_Append(row.ptr(), decoded.ptr()) < 0)
  3739                                         throw py::error_already_set();
  3740                                     LOG("SQLGetData: CHAR column %d decoded with '%s', %zu bytes "

Lines 3749-3759

  3749                                     // Preserve the existing codec-error bytes fallback.
  3750                                     decoded = steal(PyBytes_FromStringAndSize(
  3751                                         reinterpret_cast<const char*>(dataBuffer.data()),
  3752                                         static_cast<Py_ssize_t>(dataLen)));
! 3753                                     if (!decoded) throw py::error_already_set();
! 3754                                     if (PyList_Append(row.ptr(), decoded.ptr()) < 0)
! 3755                                         throw py::error_already_set();
  3756                                 }
  3757                             } else {
  3758                                 // Buffer too small, fallback to streaming
  3759                                 LOG("SQLGetData: CHAR column %d data truncated "

Lines 3821-3831

  3821                     SQLWCHAR* dataBuffer;
  3822                     if (bufferChars <= std::size(inlineBuffer)) {
  3823                         std::fill_n(inlineBuffer, bufferChars, SQLWCHAR{});
  3824                         dataBuffer = inlineBuffer;
! 3825                     } else {
! 3826                         heapBuffer.resize(bufferChars);
! 3827                         dataBuffer = heapBuffer.data();
  3828                     }
  3829                     SQLLEN dataLen;
  3830                     ret = SQLGetData_ptr(hStmt, i, SQL_C_WCHAR, dataBuffer, fetchBufferSize,
  3831                                          &dataLen);

Lines 3843-3851

  3843                                 // null termination. This preserves embedded NULs and avoids
  3844                                 // any risk of reading past the valid range if the driver
  3845                                 // omits the terminator.
  3846                                 row.append(FetchText::from_utf16_native(
! 3847                                     reinterpret_cast<const char*>(dataBuffer),
  3848                                     static_cast<Py_ssize_t>(numCharsInData * sizeof(SQLWCHAR))));
  3849                                 LOG("SQLGetData: Appended NVARCHAR string "
  3850                                     "length=%lu for column %d",
  3851                                     (unsigned long)numCharsInData, i);

Lines 3897-3905

  3897                 SQLLEN indicator = 0;
  3898                 ret = SQLGetData_ptr(hStmt, i, SQL_C_LONG, &intValue, 0, &indicator);
  3899                 CaptureFetchDiagnostics(hStmt, ret, messages);
  3900                 if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
! 3901                     AppendFetchedCell(row, PyLong_FromLong(intValue));
  3902                 } else {
  3903                     row.append(py::none());
  3904                 }
  3905                 break;

Lines 5477-5485

  5477     FetchStateGuard fetchStateGuard(StatementHandle, messages);
  5478 
  5479     if (!hasLobColumns && fetchSize > 0) {
  5480         ret = SQLBindColums(StatementHandle, buffers, columnNames, numCols, fetchSize, charCtype,
! 5481                             messages);
  5482         if (!SQL_SUCCEEDED(ret)) {
  5483             LOG("Error when binding columns");
  5484             return ret;
  5485         }

mssql_python/pybind/row_factory.hpp

Lines 11-24

  11 
  12 // Only the post-fetch cached-map loop: no ODBC access, Row allocation, or UUID work.
  13 inline py::list apply_output_converters(const py::object& values, const py::object& converters) {
  14     if (!PyList_CheckExact(values.ptr()) || !PyList_CheckExact(converters.ptr())) {
! 15         throw py::type_error("converter values and map must be exact lists");
! 16     }
  17     py::list result =
  18         steal<py::list>(PyList_GetSlice(values.ptr(), 0, PyList_GET_SIZE(values.ptr())));
  19     if (!result)
! 20         throw py::error_already_set();
  21 
  22     // Keep the Python iterators: their retained tuples affect finalizer timing
  23     // when a callback replaces itself or mutates the source lists.
  24     py::object pairs = steal(PyObject_CallFunctionObjArgs(reinterpret_cast<PyObject*>(&PyZip_Type),

Lines 23-35

  23     // when a callback replaces itself or mutates the source lists.
  24     py::object pairs = steal(PyObject_CallFunctionObjArgs(reinterpret_cast<PyObject*>(&PyZip_Type),
  25                                                           values.ptr(), converters.ptr(), nullptr));
  26     if (!pairs)
! 27         throw py::error_already_set();
  28     py::object items =
  29         steal(PyObject_CallOneArg(reinterpret_cast<PyObject*>(&PyEnum_Type), pairs.ptr()));
  30     if (!items)
! 31         throw py::error_already_set();
  32     pairs = py::object();
  33 
  34     // Retain the current inputs and last encoded value like the Python locals.
  35     py::object value, converter, value_bytes;

Lines 36-44

  36     for (Py_ssize_t i = 0;; ++i) {
  37         py::object item = steal(PyIter_Next(items.ptr()));
  38         if (!item) {
  39             if (PyErr_Occurred())
! 40                 throw py::error_already_set();
  41             break;
  42         }
  43         PyObject* pair = PyTuple_GET_ITEM(item.ptr(), 1);
  44         py::object next_value = borrow(PyTuple_GET_ITEM(pair, 0));

Lines 55-63

  55         try {
  56             const int is_string =
  57                 PyObject_IsInstance(value.ptr(), reinterpret_cast<PyObject*>(&PyUnicode_Type));
  58             if (is_string < 0)
! 59                 throw py::error_already_set();
  60             PyObject* argument = value.ptr();
  61             if (is_string) {
  62                 // Match str.encode's codec lookup, including the spelling.
  63                 // Subclasses and __class__ proxies still dispatch encode dynamically.

Lines 96-104

   96     const py::handle& attr_cursor, const py::handle& attr_column_map_lower,
   97     const py::handle& attr_column_names,
   98     int (*set_attr)(PyObject*, PyObject*, PyObject*) = PyObject_GenericSetAttr) {
   99     if (!row)
! 100         throw py::error_already_set();
  101 
  102     if (set_attr(row.ptr(), attr_values.ptr(), row_data) < 0 ||
  103         set_attr(row.ptr(), attr_column_map.ptr(), column_map.ptr()) < 0 ||
  104         set_attr(row.ptr(), attr_cursor.ptr(), cursor_obj.ptr()) < 0 ||

Lines 148-181

  148 }
  149 
  150 // Passive final eligibility check: do not execute newly installed class descriptors.
  151 inline bool has_default_row_allocation(PyObject* row_class, const py::str& new_name,
! 152                                        const py::str& setattr_name) {
! 153     if (!PyUnicode_CheckExact(new_name.ptr()) || !PyUnicode_CheckExact(setattr_name.ptr())) {
! 154         throw py::type_error("Row allocation guard names must be exact strings");
! 155     }
! 156     if (!row_class || Py_TYPE(row_class) != &PyType_Type) {
! 157         return false;
! 158     }
! 159     auto* row_type = reinterpret_cast<PyTypeObject*>(row_class);
! 160     if (!row_type->tp_bases || !PyTuple_CheckExact(row_type->tp_bases) ||
! 161         PyTuple_GET_SIZE(row_type->tp_bases) != 1 ||
! 162         PyTuple_GET_ITEM(row_type->tp_bases, 0) != reinterpret_cast<PyObject*>(&PyBaseObject_Type) ||
! 163         !row_type->tp_dict) {
! 164         return false;
! 165     }
! 166     PyObject* names[] = {new_name.ptr(), setattr_name.ptr()};
! 167     for (PyObject* name : names) {
! 168         PyObject* member = PyDict_GetItemWithError(row_type->tp_dict, name);
! 169         if (member) {
! 170             return false;
! 171         }
! 172         if (PyErr_Occurred()) {
  173             throw py::error_already_set();
  174         }
  175     }
! 176     return true;
! 177 }
  178 
  179 // Attribute names are prepared once as binding defaults, not allocated for each row.
  180 inline py::object construct_row(const py::object& values, const py::type& row_class,
  181                                 const py::object& column_map, const py::object& cursor_obj,

Lines 181-194

  181                                 const py::object& column_map, const py::object& cursor_obj,
  182                                 const py::object& column_map_lower, const py::object& column_names,
  183                                 const py::tuple& attributes) {
  184     if (attributes.size() != 5) {
! 185         throw py::value_error("Row construction requires five attribute names");
! 186     }
  187     for (py::handle name : attributes) {
  188         if (!PyUnicode_Check(name.ptr())) {
! 189             throw py::type_error("Row attribute names must be strings");
! 190         }
  191     }
  192     const py::object new_method = row_class.attr("__new__");
  193     py::object row = steal(PyObject_CallOneArg(new_method.ptr(), row_class.ptr()));
  194     initialize_row(row, values.ptr(), column_map, cursor_obj, column_map_lower, column_names,


📋 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.7%
mssql_python.pybind.row_factory.hpp: 74.3%
mssql_python.pybind.ddbc_bindings.cpp: 80.2%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 83.1%
mssql_python.logging.py: 86.2%
mssql_python.pybind.logger_bridge.hpp: 87.5%
mssql_python.pooling.py: 90.1%

🔗 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 5, 2026 06:03

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.

Copilot review overview

🔵 Needs a closer look

Native correctness tests and uninstrumented Release performance measurements remain unrun.

Review effort: Balanced
Findings: None

@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review October 5, 2026 07:56
Copilot AI balanced review requested due to automatic review settings October 5, 2026 07:56

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.

Copilot review overview

🔵 Needs a closer look

The native hot-path changes and fault-injection tests remain unexecuted, with no Release-OFF performance result available.

Review effort: Balanced
Findings: None

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 10:54
@github-actions github-actions Bot added pr-size: large Substantial code update and removed pr-size: medium Moderate update size labels Oct 5, 2026
@jahnvi480 Jahnvi Thakkar (jahnvi480) changed the title PERF: Cache full column counts for fetchone PERF: Optimize fetchone, fetchmany(1) and fetchval paths Oct 5, 2026
Keep logical buffer capacity, initialization and conversion unchanged,
with heap fallback for larger bounded values. Preserve existing tests
and append boundary, strict-error and isolated buffer-contract cases.

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

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.

Copilot review overview

🔵 Needs a closer look

The latency benchmark currently builds unoptimized Unix binaries instead of the documented Release configuration.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

Consolidate essential fetch regressions into existing suites and remove
PR-added benchmark-only and unified-coverage-skipped tests.

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

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

The numeric route can mask SQLGetData failures, and latency CI does not produce optimized Release builds.

3 open findings

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread eng/profiler_benchmarks/controller.py
Use existing Python Row completion while retaining native fetch behavior.
Keep route provenance accurate and update existing regression expectations.

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

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

Latency builds are not verified Release builds, route documentation conflicts with source, and new CI orchestration lacks automated coverage.

4 open findings

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread profiler/README.md Outdated
@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review October 8, 2026 09:10
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 11:45
Comment thread mssql_python/pybind/ddbc_bindings.cpp 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

Latency builds are not guaranteed to be Release builds, CI orchestration lacks tests, and route documentation contradicts the implemented policy.

4 open findings

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Preserve existing LOB semantics, require Release benchmark configurations, and cover aggregate report failures and numeric fetch errors.

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

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

The new fetchmany(1) factory check can invoke replacement descriptors after consuming a row, and the source-identity recognizer lacks direct tests.

0 open findings

4 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Add regression tests for closed source recognition

eng/​profiler_benchmarks/​controller.py:294

The new closed source recognizer has no direct regression coverage: the aggregate test monkeypatches python_source_identity, and no test invokes this function. A stale digest or an AST false rejection would make every latency/route worker fail before measurement. Add no-DB tests covering the current cursor source, a supported legacy source without the declaration, and representative duplicate/rebinding rejection cases.

Medium severity Avoid descriptor invocation when resolving _fast_create

mssql_python/​cursor.py:3066

This check resolves _fast_create through Python's descriptor protocol after the native fetch has already advanced the cursor. A replacement descriptor can therefore run side effects or raise here, making fetchmany(1) consume a row and fail even though the previous batch wrapper never consulted that factory. Inspect the raw class dictionary, as _native_row_eligible does, so substituted descriptors reliably stay on the batch path.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

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

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

The profiler documentation attributes native fusion to a nonexistent test and an inactive default route.

0 open findings

Previously missed (1)

In code that hasn't changed since last review

Low severity Update attribution docs for inactive binding and actual route checks

profiler/​README.md:248

This attribution section describes a phase-controlled fused route and test_single_row_fusion_native_counters_in_subprocess, but neither exists in the current source: ordinary fetches call the split helpers with native=False, and the test name appears only here. The documented counters therefore cannot be produced by the current default path; update this block to describe the inactive binding and the route-mode checks that actually run.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 12, 2026 06:25

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

The extensive native ownership, ODBC state, and CI reporting changes require final human review and exact-head cross-platform validation.

0 open findings

🧠 Review effort: Balanced

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