Skip to content

PERF: Avoid redundant fetch diagnostic scans - #809

Merged
Jahnvi Thakkar (jahnvi480) merged 12 commits into
mainfrom
jahnvi/fix-fetch-diagnostics
Sep 24, 2026
Merged

Jahnvi Thakkar (jahnvi480) merged 12 commits into
mainfrom
jahnvi/fix-fetch-diagnostics

Conversation

@jahnvi480

@jahnvi480 Jahnvi Thakkar (jahnvi480) commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#48347
AB#48364


Summary

Capture fetch diagnostics immediately while the ODBC records are valid, then remove redundant Python-side scans on clean fetch calls.

  • Pass the existing cursor message list through the native fetch path; preserve warning/EOF diagnostics and propagate data-read errors before later ODBC calls overwrite them.
  • Avoid redundant diagnostic replay on natural Arrow reader exhaustion while retaining conservative close/cancel/error handling.
  • Update existing regression tests only. No new test functions, parameterized cases, build/CI/dependency changes, public API changes, hidden prefetch, or new caches.

Validation

Linux x64, Python 3.13.15, Release builds with profiling compiled out/on separately. The final selected 274-case group, 3 isolated cases, real EOF-warning contract, 3 additional Arrow lifecycle cases and explicitly selected cancellation case passed. Nine diagnostic mocks are included in the 274-case group. Full Black check passed.

Each of 12 primary profiling observations recorded one actual helper SQLGetDiagRec call per clean 10,000-row drain; three Arrow observations recorded two calls each.

Performance

Uninstrumented 10,000-row workload medians; 20 matched pairs per primary and 10 for the Arrow-equivalent control:

Workload Candidate Paired pyodbc
fetchone 26.449 ms 5.845 ms
iteration 27.143 ms 5.220 ms
fetchval 36.988 ms 3.305 ms
fetchmany(1) 97.624 ms 8.087 ms
Arrow-equivalent control 5.241 ms 12.235 ms

The four primary medians were approximately 94–96% lower than the separately measured main baseline. This is descriptive, not a direct paired main/candidate confidence interval. All four primary workloads still lose to pyodbc. No isolated allocation or latency benefit is claimed for the positional-argument cleanup; native functions use FASTCALL|KEYWORDS.

Draft limitations

The combined performance goal is not complete, and neither ADO task is being declared closed. Perf Police scoped source review and the measured diagnostic-call budget passed; overall qualification remains HOLD. Real intermediate native-warning/mixed-record preservation, wider platform/failure-path coverage, and the full planned benchmark/noninferiority matrix remain unqualified. The scalar/small-batch warning arrived at EOF; fetchall native warning origin remains unlocalized. Intermediate native warning preservation is not established. Earlier fetchall/fetchmany(1000)/nextset observations are not final-revision measurements.

Capture fetch diagnostics before subsequent ODBC calls replace them, preserve data-read errors and Arrow cleanup behavior, and update existing regression coverage only. The tree is identical to tested revision c1808128312c956dbf108028a9c6fb750d209a51; broader warning/platform coverage and the faster-than-pyodbc goal remain unqualified.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 23, 2026 09:24
@github-actions github-actions Bot added the pr-size: large Substantial code update label Sep 23, 2026

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

🟡 Changes recommended

Unresolved native error-propagation issues can overwrite diagnostics or expose invalid metadata.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Optimizes fetch diagnostics by capturing ODBC records during native fetches and avoiding redundant Python-side scans.

Changes:

  • Threads cursor messages through native fetch and Arrow paths.
  • Preserves warning, EOF, and cleanup diagnostics.
  • Updates regression tests for diagnostic forwarding and call counts.
File Summary
tests/​test_fetch_settings_cache.py Updates fetch bridge diagnostic assertions.
tests/​test_004_cursor.py Strengthens EOF-warning coverage.
tests/​test_004_cursor_arrow.py Verifies Arrow exhaustion diagnostics.
mssql_python/​pybind/​ddbc_bindings.h Propagates message storage through native helpers.
mssql_python/​pybind/​ddbc_bindings.cpp Implements native capture and cleanup; includes one critical, two moderate, and one nit finding regarding error propagation.
mssql_python/​cursor.py Passes message lists through fetch paths and removes redundant scans.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mssql_python/pybind/ddbc_bindings.cpp
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

86%


🎯 Overall Coverage

84%


📈 Total Lines Covered: 9358 out of 11042
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/cursor.py (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (91.9%): Missing lines 1931-1933,1966-1967,3586,3643,3696,3806,4407-4408,4864-4866,5017,5341,5553
  • mssql_python/pybind/ddbc_bindings.h (0.0%): Missing lines 643-644,694-698,762-765,804-808

Summary

  • Total: 237 lines
  • Missing: 33 lines
  • Coverage: 86%

mssql_python/pybind/ddbc_bindings.cpp

Lines 1927-1937

  1927             }
  1928             if (stateReturn == SQL_NO_DATA)
  1929                 break;
  1930             // Skip only this continuation record, without retrieving its message text.
! 1931             if (stateReturn == SQL_SUCCESS && std::equal(sqlState, sqlState + 6, u"01004"))
! 1932                 continue;
! 1933             if (stateReturn != SQL_SUCCESS)
  1934                 LOG("AppendDiagRecords: SQLSTATE lookup returned %d; reading full record %d",
  1935                     stateReturn, recNumber);
  1936         }
  1937         SQLWCHAR message[SQL_MAX_MESSAGE_LENGTH_SQLSERVER] = {0};

Lines 1962-1971

  1962         // Format the state string
  1963         std::string stateWithError = "[" + stateStr + "] (" + std::to_string(nativeError) + ")";
  1964 
  1965         // Create the tuple with converted strings
! 1966         py::tuple record = py::make_tuple(py::str(stateWithError), py::str(msgStr));
! 1967         if (PyList_Append(records.ptr(), record.ptr()) < 0)
  1968             throw py::error_already_set();
  1969     }
  1970 }

Lines 3582-3590

  3582                                 LOG("SQLGetData: CHAR column %d (WCHAR path) data "
  3583                                     "truncated, using streaming LOB",
  3584                                     i);
  3585                                 row.append(FetchLobColumnData(hStmt, i, SQL_C_WCHAR, true, false,
! 3586                                                               "utf-16le", messages));
  3587                             }
  3588                         } else if (dataLen == SQL_NULL_DATA) {
  3589                             LOG("SQLGetData: Column %d is NULL (CHAR via WCHAR)", i);
  3590                             row.append(py::none());

Lines 3639-3647

  3639                     std::vector<SQLCHAR> dataBuffer(fetchBufferSize);
  3640                     SQLLEN dataLen;
  3641                     ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, dataBuffer.data(), dataBuffer.size(),
  3642                                          &dataLen);
! 3643                     CaptureFetchDiagnostics(
  3644                         hStmt, ret, messages,
  3645                         ret == SQL_SUCCESS_WITH_INFO &&
  3646                             (dataLen == SQL_NO_TOTAL ||
  3647                              dataLen >= static_cast<SQLLEN>(dataBuffer.size())));

Lines 3692-3700

  3692                             LOG("SQLGetData: SQL_NO_TOTAL for column %d (SQL_CHAR), "
  3693                                 "streaming via FetchLobColumnData",
  3694                                 i);
  3695                             row.append(FetchLobColumnData(hStmt, i, SQL_C_CHAR, false, false,
! 3696                                                           effectiveCharEnc, messages));
  3697                         } else if (dataLen < 0) {
  3698                             LOG("SQLGetData: Unexpected negative data length "
  3699                                 "for column %d - dataType=%d, dataLen=%ld",
  3700                                 i, dataType, (long)dataLen);

Lines 3802-3810

  3802             case SQL_INTEGER: {
  3803                 SQLINTEGER intValue;
  3804                 SQLLEN indicator = 0;
  3805                 ret = SQLGetData_ptr(hStmt, i, SQL_C_LONG, &intValue, 0, &indicator);
! 3806                 CaptureFetchDiagnostics(hStmt, ret, messages);
  3807                 if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
  3808                     row.append(static_cast<int>(intValue));
  3809                 } else {
  3810                     row.append(py::none());

Lines 4403-4412

  4403 template <typename Metadata>
  4404 SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, const Metadata& columnNames,
  4405                          py::list& rows, SQLUSMALLINT numCols, SQLULEN& numRowsFetched,
  4406                          const std::vector<SQLUSMALLINT>& lobColumns,
! 4407                          const std::string& charEncoding = "utf-16le", int charCtype = SQL_C_WCHAR,
! 4408                          py::handle messages = {}) {
  4409     PERF_TIMER("FetchBatchData");
  4410     LOG("FetchBatchData: Fetching data in batches");
  4411     SQLRETURN ret;
  4412     {

Lines 4860-4870

  4860 
  4861     void observe(SQLRETURN ret) const {
  4862         CaptureFetchDiagnostics(handle->get(), ret, py::handle(messages));
  4863         CheckFetchError(handle, ret);
! 4864     }
! 4865 
! 4866     void setRowArraySize(SQLULEN rowArraySize) const {
  4867         observe(SQLSetStmtAttr_ptr(handle->get(), SQL_ATTR_ROW_ARRAY_SIZE,
  4868                                    (SQLPOINTER)(intptr_t)rowArraySize, 0));
  4869     }

Lines 5013-5021

  5013     }
  5014 
  5015     // Initialize column buffers
  5016     ColumnBuffers buffers(numCols, fetchSize);
! 5017     FetchStateGuard fetchStateGuard(StatementHandle, messages);
  5018 
  5019     // Bind columns
  5020     ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize, charCtype, messages);
  5021     if (!SQL_SUCCEEDED(ret)) {

Lines 5337-5345

  5337     SQLULEN numRowsFetched = 0;
  5338     FetchStateGuard fetchStateGuard(StatementHandle, messages);
  5339 
  5340     if (!hasLobColumns && fetchSize > 0) {
! 5341         ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize, charCtype, messages);
  5342         if (!SQL_SUCCEEDED(ret)) {
  5343             LOG("Error when binding columns");
  5344             return ret;
  5345         }

Lines 5549-5557

  5549                             ret = SQLGetData_ptr(hStmt, idxCol + 1, SQL_C_TYPE_DATE,
  5550                                                  buffers.dateBuffers[idxCol].data(),
  5551                                                  sizeof(SQL_DATE_STRUCT),
  5552                                                  buffers.indicators[idxCol].data());
! 5553                             fetchStateGuard.observe(ret);
  5554                             if (!SQL_SUCCEEDED(ret)) {
  5555                                 LOG("Error fetching TYPE_DATE data for column %d", idxCol + 1);
  5556                                 return ret;
  5557                             }

mssql_python/pybind/ddbc_bindings.h

  639             }
  640         } else {
  641             // LOB / truncated: stream with SQL_C_WCHAR
  642             PyList_SET_ITEM(row, col - 1,
! 643                             FetchLobColumnData(hStmt, col, SQL_C_WCHAR, true, false, "utf-16le",
! 644                                                py::handle(colInfo->messages))
  645                                 .release()
  646                                 .ptr());
  647         }
  648         return;

  690             PyList_SET_ITEM(row, col - 1, pyStr);
  691         }
  692     } else {
  693         // Slow path: LOB data requires separate fetch call
! 694         PyList_SET_ITEM(row, col - 1,
! 695                         FetchLobColumnData(hStmt, col, SQL_C_CHAR, false, false,
! 696                                            colInfo->charEncoding, py::handle(colInfo->messages))
! 697                             .release()
! 698                             .ptr());
  699     }
  700 }
  701 
  702 // Process SQL NCHAR/NVARCHAR (wide/Unicode string) column into Python str

  758 #endif
  759     } else {
  760         // Slow path: LOB data requires separate fetch call
  761         PyList_SET_ITEM(row, col - 1,
! 762                         FetchLobColumnData(hStmt, col, SQL_C_WCHAR, true, false, "utf-8",
! 763                                            py::handle(colInfo->messages))
! 764                             .release()
! 765                             .ptr());
  766     }
  767 }
  768 
  769 // Process SQL BINARY/VARBINARY (binary data) column into Python bytes

  800             PyList_SET_ITEM(row, col - 1, pyBytes);
  801         }
  802     } else {
  803         // Slow path: LOB data requires separate fetch call
! 804         PyList_SET_ITEM(row, col - 1,
! 805                         FetchLobColumnData(hStmt, col, SQL_C_BINARY, false, true, "",
! 806                                            py::handle(colInfo->messages))
! 807                             .release()
! 808                             .ptr());
  809     }
  810 }
  811 
  812 }  // namespace ColumnProcessors


📋 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.pybind.ddbc_bindings.cpp: 79.1%
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.pooling.py: 90.1%
mssql_python.pybind.fetch_temporal.hpp: 92.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI review requested due to automatic review settings September 23, 2026 11:54

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

🟡 Changes recommended

Unresolved native fetch error-propagation issues include one critical and two moderate findings.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as ready for review September 24, 2026 03:20
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

PR Performance Report

✅ Performance improved

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

3 IMPROVEMENTS 0 SLOWDOWNS 2/2 ENVIRONMENTS

Signal fingerprint

Database task Unix / SQL Server 2022 Unix / SQL Server 2025
Row-by-row fetching 74.0% faster 74.2% faster
Row fetching in batches of 100 24.4% faster 21.9% faster
Repeated positional queries 21.4% faster No signal

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 49.497 ms 12.882 ms -74.0%
Unix / SQL Server 2022 Row fetching in batches of 100 144.904 ms 108.820 ms -24.4%
Unix / SQL Server 2022 Repeated positional queries 38.662 ms 30.413 ms -21.4%
Unix / SQL Server 2025 Row-by-row fetching 55.884 ms 14.496 ms -74.2%
Unix / SQL Server 2025 Row fetching in batches of 100 158.177 ms 123.386 ms -21.9%
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

SELECT queries: py::fetchall::cpp_call +0.216 ms; ddbc::FetchAll_wrap +0.215 ms; ddbc::FetchBatchData +0.091 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Executemany inserts: ddbc::BindParameterArray +0.394 ms; py::executemany::param_type_detection +0.346 ms; py::executemany::param_processing +0.013 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Fetch-all queries: ddbc::SQLDescribeCol::driver_call +0.000 ms; ddbc::SQLNumResultCols_wrap +0.000 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Row-by-row fetching: ddbc::SQLGetData_wrap -0.178 ms; py::fetchone::cpp_call -0.131 ms; ddbc::FetchOne_wrap -0.114 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchone::diag_records (added, removed, or intermittent).
Batched row fetching: py::fetchmany::cpp_call +0.875 ms; ddbc::FetchBatchData +0.452 ms; ddbc::FetchBatchData::construct_rows +0.408 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchmany::diag_records (added, removed, or intermittent).
Arrow row fetching: ddbc::FetchArrowBatch_wrap +2.556 ms; ddbc::SQLBindColums +0.006 ms; ddbc::SQLDescribeCol::driver_call +0.001 ms. Call changes: ddbc::SQLGetAllDiagRecords (added, removed, or intermittent).
Row fetching in batches of 100: ddbc::SQLBindColums -0.226 ms; ddbc::SQLNumResultCols_wrap -0.035 ms; ddbc::FetchBatchData::cache_column_metadata -0.030 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchmany::diag_records (added, removed, or intermittent).
Row fetching in batches of 10,000: ddbc::FetchMany_wrap +16.529 ms; py::fetchmany::cpp_call +16.467 ms; ddbc::SQLBindColums +15.071 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchmany::diag_records (added, removed, or intermittent).
Repeated positional queries: py::execute::cpp_call -0.442 ms; ddbc::SQLExecute_wrap -0.431 ms; ddbc::BindParameters -0.018 ms. Call changes: ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchone::diag_records (added, removed, or intermittent).
Repeated named-parameter queries: py::execute::diag_records +0.011 ms; ddbc::BindParameters +0.009 ms; ddbc::SQLNumResultCols_wrap +0.004 ms. Call changes: ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchone::diag_records (added, removed, or intermittent).
Joined aggregation queries: py::execute::cpp_call +7.685 ms; ddbc::SQLExecDirect_wrap +7.672 ms; ddbc::FetchBatchData +0.186 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Large joined-result fetching: py::fetchall::cpp_call +2.368 ms; ddbc::FetchAll_wrap +1.982 ms; ddbc::FetchBatchData +1.951 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
1.2-million-row fetching: ddbc::FetchBatchData::SQLFetchScroll_call +0.269 ms; ddbc::FetchBatchData::cache_column_metadata +0.114 ms; ddbc::SQLDescribeCol_wrap +0.003 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Common table expression queries: py::fetchall::cpp_call +0.132 ms; ddbc::FetchAll_wrap +0.131 ms; ddbc::FetchBatchData +0.124 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).

Unix / SQL Server 2025

SELECT queries: py::fetchall::cpp_call +0.076 ms; ddbc::FetchAll_wrap +0.076 ms; ddbc::FetchBatchData +0.069 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Executemany inserts: py::executemany::cpp_call +0.823 ms; ddbc::SQLExecuteMany_wrap +0.821 ms; ddbc::BindParameterArray +0.018 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Fetch-all queries: ddbc::FetchBatchData::SQLFetchScroll_call +1.049 ms; py::fetchall::cpp_call +0.340 ms; ddbc::FetchAll_wrap +0.251 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchall::diag_records (added, removed, or intermittent).
Row-by-row fetching: py::fetchone::cpp_call -0.858 ms; ddbc::FetchOne_wrap -0.779 ms; ddbc::SQLGetData_wrap -0.726 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchone::diag_records (added, removed, or intermittent).
Batched row fetching: ddbc::FetchBatchData::SQLFetchScroll_call +0.937 ms; ddbc::FetchBatchData +0.762 ms; ddbc::FetchMany_wrap +0.669 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent); ddbc::SQLGetAllDiagRecords (added, removed, or intermittent); py::fetchmany::diag_records (added, removed, or intermittent).
Arrow row fetching: ddbc::FetchArrowBatch_wrap +0.979 ms. Call changes: ddbc::SQLGetAllDiagRecords (added, removed, or intermittent).

8 additional diagnostic rows are available in the raw ADO artifacts.

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 11.727 ms 11.518 ms +0.8% no signal
SELECT queries 1.100 ms 1.242 ms +13.0% no signal
Row insertion 31.613 ms 31.957 ms +0.4% no signal
Executemany inserts 145.101 ms 140.432 ms -0.9% no signal
Fetch-all queries 124.538 ms 120.267 ms -2.9% no signal
Row-by-row fetching 49.497 ms 12.882 ms -74.0% consistent improvement
Batched row fetching 115.816 ms 113.346 ms -2.0% no signal
Transaction commit and rollback 102.115 ms 100.631 ms -1.1% no signal
Arrow row fetching 90.322 ms 91.858 ms +1.7% no signal
100,000-row insertion 426.100 ms 427.728 ms -1.2% no signal
Row fetching in batches of 100 144.904 ms 108.820 ms -24.4% consistent improvement
Row fetching in batches of 10,000 128.527 ms 145.829 ms +14.1% no signal
Repeated positional queries 38.662 ms 30.413 ms -21.4% consistent improvement
Repeated named-parameter queries 40.783 ms 32.798 ms -19.3% no signal
Legacy 100,000-row insertion 321.674 ms 324.394 ms +0.9% no signal
Insertion with explicit input sizes 462.761 ms 461.803 ms +1.7% no signal
Joined aggregation queries 185.890 ms 193.628 ms +4.0% no signal
Large joined-result fetching 193.299 ms 195.422 ms +0.6% no signal
1.2-million-row fetching 3544.321 ms 3481.260 ms -0.4% no signal
Common table expression queries 5.753 ms 5.873 ms +1.5% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 97.330 ms 97.033 ms -0.3% no signal
SELECT queries 1.092 ms 1.074 ms +2.4% no signal
Row insertion 34.555 ms 34.333 ms -0.8% no signal
Executemany inserts 153.729 ms 153.163 ms -0.2% no signal
Fetch-all queries 121.799 ms 122.131 ms +0.5% no signal
Row-by-row fetching 55.884 ms 14.496 ms -74.2% consistent improvement
Batched row fetching 121.244 ms 117.319 ms -3.0% no signal
Transaction commit and rollback 116.978 ms 116.817 ms -0.2% no signal
Arrow row fetching 96.044 ms 96.010 ms +0.9% no signal
100,000-row insertion 435.236 ms 437.792 ms -0.4% no signal
Row fetching in batches of 100 158.177 ms 123.386 ms -21.9% consistent improvement
Row fetching in batches of 10,000 140.931 ms 137.470 ms -1.7% no signal
Repeated positional queries 41.912 ms 34.420 ms -17.8% no signal
Repeated named-parameter queries 44.885 ms 36.773 ms -17.9% no signal
Legacy 100,000-row insertion 349.983 ms 351.754 ms +0.5% no signal
Insertion with explicit input sizes 483.583 ms 487.451 ms +1.0% no signal
Joined aggregation queries 158.031 ms 157.538 ms -0.4% no signal
Large joined-result fetching 184.874 ms 185.801 ms +0.9% no signal
1.2-million-row fetching 3474.413 ms 3504.277 ms +0.4% no signal
Common table expression queries 5.241 ms 5.261 ms +1.6% no signal
Build and measurement details

ADO build 177935

PR head: d268f978ba1588de1d60f6e3bc21cba731ef1cba
Base: 9048f557893d4514035904549755abc0ef076808
Measured merge: 06d399b250fe0f0857f2cbc382704feadfb9bd52

  • 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.

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

Raise through the existing DB-API translator before returning a failed SQLNumResultCols output. Return a failed sql_variant probe immediately, before a later column can replace its diagnostics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Compare ODBC SQLLEN indicators against bounded buffer sizes converted to SQLLEN. This fixes the four MSVC C4018 errors promoted by /WX without weakening compiler settings, and preserves SQL_NO_TOTAL handling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@bewithgaurav Gaurav Sharma (bewithgaurav) left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the row-level fetch improvement is real, but clean large-value fetches now incur substantially more diagnostic overhead, and windows builds fail.
requesting changes before merging.

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Copilot AI review requested due to automatic review settings September 24, 2026 08:18
Preserve the later upstream merge while retaining the locally validated native review fixes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Read SQLSTATE with SQLGetDiagFieldW before materializing internal continuation diagnostics. Skip only confirmed 01004 records, retrieve every unrelated record unchanged, and retain full-record fallback if the field API is unavailable or fails. Track actual field calls with the existing profiler.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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

Diagnostic and failure-path behavior remains insufficiently qualified across platforms and intermediate warning scenarios.

Review effort: Lite
Findings: 2 High severity

Open (2)

Copilot AI review requested due to automatic review settings September 24, 2026 08:29

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

An earlier column-read failure can be overwritten by a later success in the native fetch path.

Review effort: Lite
Findings: None

Resolved since last review (2)

Comment thread tests/test_fetch_settings_cache.py
Exercise the compiled diagnostic collector in isolated subprocesses with deterministic ODBC diagnostic callbacks and a real LOB continuation. Verify warning order, internal truncation filtering, SQLSTATE fallback, full-record call counts, and the unfiltered diagnostic path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contain callback exceptions and assert them outside the C ABI. Learn the target statement through an unchanged diagnostic call, forward unrelated handles and EOF to saved driver addresses, and restore pointers before cursor cleanup.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 09:27

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

🟡 Changes recommended

Native fetch and metadata error paths can lose diagnostics or return success before the original errors are propagated.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 10: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

🟡 Changes recommended

Critical diagnostic buffer sizing and multiple bind-error diagnostic preservation issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Low severity

Open (3)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread tests/test_fetch_settings_cache.py
Resolve the #796 metadata-cache integration while retaining immediate diagnostic capture, mixed-record filtering, and error propagation from #809. Forward the message sink through the new shared metadata-description helper and templated binding path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 13:31

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

🟡 Changes recommended

Native cleanup failure handling and intermediate diagnostic preservation remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Comment thread mssql_python/pybind/ddbc_bindings.cpp

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

fixes lgtm, approving

@gargsaumya gargsaumya 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.

lgtm

@jahnvi480
Jahnvi Thakkar (jahnvi480) merged commit 29fa554 into main Sep 24, 2026
32 checks passed
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.

5 participants