Skip to content

PERF: Reuse native buffers and bindings across fetchmany calls - #806

Open
Jahnvi Thakkar (jahnvi480) wants to merge 27 commits into
mainfrom
jahnvi/perf-fetch-buffer-reuse
Open

Jahnvi Thakkar (jahnvi480) wants to merge 27 commits into
mainfrom
jahnvi/perf-fetch-buffer-reuse

Conversation

@jahnvi480

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

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

ADO Task: AB#48352


Summary

Follow-up to #796, now merged into main. #796 reuses result-set metadata; this change reuses one statement-owned native buffer and ODBC binding plan across compatible, bounded-column fetchmany() calls.

Reuse requires the same result generation, metadata, batch size, and decoding settings. Checked cleanup protects driver-held pointers across incompatible transitions, errors, and close. Public APIs/defaults, Row/converter semantics, and MAX/LOB/sql_variant fallback routes remain unchanged; there is no hidden prefetch.

Repository scope (30893611 → 39e22098): five runtime files plus regressions in the existing tests/test_025_profiler.py. There is no effective build/CI or documentation diff. Three isolated cases use the existing profiler to assert actual bind/unbind counts for a compatible hit, a size/encoding/result-set miss, resumed reuse, and EOF. One additional focused test, parameterized for failed unbinding and failed rows-fetched-pointer cleanup, covers failure propagation, blocked reuse/rebinding/fetching, and recovery. These cases require a profiling-enabled native build and live SQL; unavailable profiling is explicitly skipped, not reported as zero calls. The failure-injection cases also skip Windows because its ODBC function-pointer globals are not exported there.

Scope correction: ad0a8550 reverted the four-file, 308-line cleanup-test increment 1c05d11e after the user rejected its size. The native test target, CMake/CI wiring, and related documentation remain removed. ad0a8550 restored the complete be219eb3 source tree; the follow-up through db19e637 added only one 93-line test in the existing test module. 39e22098 parameterizes that same test for the second cleanup-failure boundary (+26/-10 in that file only). There are no additional production, build, CI, or documentation changes. The earlier DevSkim fixes and three profiler regressions remain; history was preserved through a normal revert, not rewritten.

flowchart LR
    subgraph "Before (#796)"
        B["Each fetchmany"] --> S["Allocate + bind"] --> F["Fetch + unbind"]
    end
    subgraph After
        A["First compatible call"] --> P["Owned buffers + bindings"]
        P --> R["Reuse on subsequent fetchmany calls"]
        R --> D["Detach at EOF / transition"]
    end
Loading

Mechanism and correctness

Actual bind/unbind/attribute-call timers and plan-allocation profiling are preserved. The auxiliary value-buffer-growth timer was removed; its absence is not zero allocation work.

The source preserves the main merges through 30893611 and the two DevSkim fixes in 9eb586a6, which replace formatted C calls with native-only output. The reported cache/cleanup lock inversion was checked against the function-local mutex lifetimes: cache clearing returns and releases its lock before the cleanup gate is acquired; no teardown reordering was needed.

Prior SQL counter validation and source equivalence: Azure DevOps build 178157 checked out merge e3dc03c0, whose complete tree e29a5927 matches both be219eb3 and ad0a8550. Both Ubuntu x86_64 / Python 3.12.3 SQL Server 2022 and SQL Server 2025 legs completed fresh Release ON native builds and the existing checkout/profiling import guard. Each leg passed all three size/encoding/result regression cases, with zero selected skips. Each full pytest task reported 5,557 passed, 132 skipped, 42 deselected. These results remain attributed to that CI run and do not qualify the subsequently added cleanup-failure cases. Successful raw counters and native-binary hashes were not independently exported; this is not full-matrix qualification.

The original 9eb586a6 CI run exposed an unsupported UTF-16 alias in the encoding test before its transition fetch. be219eb3 corrected only two test-input lines to supported ASCII and Latin-1 settings with SQL_CHAR fixed; the original failure is retained in the evidence. This tests decoding-configuration invalidation, not a change in Linux's effective UTF-8 decoder.

Windows OFF: 9eb586a6 passed a fresh Windows x64 / Python 3.13.15 Release OFF build with /W4 /WX, followed by source-package/fresh-native import with profiling absence asserted. The current source has identical native and build inputs; subsequent changes here are test-only. This is explicit source equivalence, not a newly rebuilt Windows binary or execution of the cleanup-failure cases.

Focused cleanup-failure regression — both cases passed in current-source CI: In response to the requested detach/unbind failure-injection coverage, test_fetchmany_failed_cleanup_blocks_reuse_until_cleanup_succeeds runs isolated [unbind] and [rows_fetched_ptr] cases. The first injects SQL_ERROR through the existing SQLFreeStmt_ptr. The second leaves real unbinding untouched and injects SQL_ERROR only when SQLSetStmtAttr_ptr clears SQL_ATTR_ROWS_FETCHED_PTR, exercising failure after successful unbinding. In each case, a resized fetch and a retry at the original size must return failure with no rows, retry cleanup, and leave actual plan-allocation, bind, and fetch counts unchanged. Restoring the pointer must allow recovery with the unconsumed rows and then EOF. The test also rejects terminal-retention diagnostics. This covers the observable native-return/non-reuse/recovery contract, not direct private C++ allocation/destructor proof or public exception translation.

Azure DevOps build 178187 checked out merge 21797ffd, whose complete tree c23883f3 exactly matches published head 39e22098. Both Ubuntu x86_64 / Python 3.12.3 SQL Server 2022 and SQL Server 2025 legs completed fresh Release ON native builds and the existing checkout/profiling import guard. Both exact cleanup-failure cases passed once in each leg, with zero selected skips; the three existing size/encoding/result cases also passed. Each full pytest task reported 5,559 passed, 132 skipped, 42 deselected, 2 warnings. Raw checkout/build/test logs are retained. Successful child counters and native-binary hashes were not independently exported; these results do not claim Windows execution, full-matrix completion, performance gains, or direct destructor proof. Earlier build 178178 qualified only the original single failed-unbind case at db19e637; its evidence remains separate. The withdrawn native target's 1c05d11e Windows results remain historical only and are not evidence for these cases.

The #809 integration captures intermediate retained-binding warnings into native-only per-call storage, then appends them to cursor messages outside the cleanup gate with the GIL held. Checked unbinding and original failing statuses are preserved; SQL_SUCCESS does not trigger diagnostic scans. Runtime warning/error ordering and broad fault-path qualification remain outside these focused tests.

Prior scoped correctness evidence: Linux Release OFF checks of frozen #796 revision 65081f06 and 13ad1165 passed 339 ordinary + 3 isolated tests per arm, zero skips. The same 65081f06 arm was reused for this incremental comparison; removed tests/native fixtures and ON runs were not included. Source-hash differences were confirmed as archive CRLF/LF differences, and the scoped correctness/minimality review closed for those revisions. These results do not qualify the current head.

Historical mechanism evidence only: earlier 416e54d6 / tree 99926c46 qualification included native fixtures, standalone 6 baseline / 9 candidate cases, and fetchmany(1) counts of one plan, 24 actual binds, one unbind, and 10,001 native fetches for 10,000 rows × 24 INT columns. This does not qualify the current head or establish elapsed-time gains. Baseline retained-binding counters were unavailable, not zero.

Original failures, skips, and inapplicable baseline-counter oracles remain in the qualification ledger. This is not a clean full-suite claim; mock ODBC/GIL fault tests are not live-driver concurrency proof.

Latency, no-regression, current-main, and pyodbc acceptance remain open. This is not performance/readiness signoff.

Remove native metadata dictionary roundtrips while preserving eager Unicode names and fresh per-call descriptions. Add behavior and profiling regression coverage. Performance acceptance remains unresolved after the bounded local study.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve reserve-before-locking-weak-handles and release retained handles outside the child-list mutex. Expose the unchanged native-only algorithm for direct invariant coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Require successful scalar NULL handling and exact describe counts. Add production-header cache, failure, concurrency, allocation and lifetime tests with active Release assertions, plus Windows/Linux/macOS CTest CI and test guidance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use C++ streams for test-runner output, document the line-scoped allocator rule exception, and trigger native invariant tests for production integration header and binding changes. Keep production fetch code and the allocation-failure probes unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve the bounded fetch-buffer follow-up on the original #796 dependency.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Align only the reviewed fetch-buffer follow-up with dependency416.
The exact tree matches the qualified99926c46 snapshot; the allocator
translation-unit split is test-only and leaves production unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the pr-size: large Substantial code update label Sep 22, 2026
Remove PR-only tests, native test build and CI wiring, and restore the base test guide. Inline the test-only invalidation helper and single-caller metadata setup; stop retaining unused precision/nullability fields while preserving the cache lifetime, generation, Unicode, variant and failure contracts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Merge the frozen metadata dependency and remove PR-only test/build scaffolding.
Use direct profiled ODBC calls and the dependency child snapshot loop while
preserving retained-buffer ownership and cleanup guards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve the existing fetch-buffer reuse changes and integrate the merged
metadata optimization plus current main without rewriting branch history.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@jahnvi480
Jahnvi Thakkar (jahnvi480) changed the base branch from jahnvi/perf-small-fetch-native-metadata to main September 24, 2026 13:00
Preserve native fetch diagnostics across retained binding setup and cleanup, without Python work under the cleanup gate.

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

github-actions Bot commented Sep 24, 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

SELECT queries: py::execute::cpp_call +0.028 ms; ddbc::SQLExecDirect_wrap +0.028 ms; ddbc::FetchBatchData +0.011 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Fetch-all queries: ddbc::FetchBatchData::SQLFetchScroll_call +0.242 ms; py::fetchall::row_wrap +0.230 ms; ddbc::SQLBindColums +0.001 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Row-by-row fetching: py::fetchone::cpp_call +0.168 ms; ddbc::FetchOne_wrap +0.165 ms; ddbc::SQLGetData_wrap +0.085 ms. Call changes: ddbc::fetch_bindings::SQL_UNBIND (added, removed, or intermittent).
Batched row fetching: ddbc::FetchBatchData +1.590 ms; ddbc::FetchBatchData::SQLFetchScroll_call +1.051 ms; ddbc::FetchBatchData::construct_rows +0.598 ms. Call changes: ddbc::SQLBindColums (51 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).
Arrow row fetching: ddbc::FetchArrowBatch_wrap +1.774 ms; ddbc::SQLBindColums +0.003 ms; ddbc::SQLDescribeCol_wrap +0.002 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Row fetching in batches of 100: ddbc::AppendDiagRecords::SQLGetDiagRec_call +0.011 ms; ddbc::FetchBatchData::cache_column_metadata +0.009 ms; ddbc::SQLDescribeCol::driver_call +0.000 ms. Call changes: ddbc::SQLBindColums (501 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).
Row fetching in batches of 10,000: ddbc::SQLBindColums +0.600 ms; ddbc::FetchBatchData::SQLFetchScroll_call +0.047 ms; ddbc::AppendDiagRecords::SQLGetDiagRec_call +0.002 ms. Call changes: ddbc::SQLBindColums (6 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).
Repeated positional queries: py::execute::cpp_call +0.163 ms; ddbc::SQLExecute_wrap +0.160 ms; py::fetchone::cpp_call +0.048 ms. Call changes: ddbc::fetch_bindings::SQL_UNBIND (added, removed, or intermittent).
Repeated named-parameter queries: py::execute::cpp_call +0.069 ms; py::execute::param_prep +0.068 ms; ddbc::SQLExecute_wrap +0.050 ms. Call changes: ddbc::fetch_bindings::SQL_UNBIND (added, removed, or intermittent).
Joined aggregation queries: py::fetchall::cpp_call +0.042 ms; ddbc::FetchAll_wrap +0.041 ms; ddbc::SQLBindColums +0.009 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Large joined-result fetching: py::execute::cpp_call +1.342 ms; ddbc::SQLExecDirect_wrap +1.336 ms; ddbc::SQLBindColums +0.005 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
1.2-million-row fetching: ddbc::FetchBatchData::SQLFetchScroll_call +5.994 ms; ddbc::FetchBatchData::cache_column_metadata +0.313 ms; ddbc::SQLExecDirect_wrap +0.055 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Common table expression queries: py::fetchall::cpp_call +0.041 ms; py::execute::cpp_call +0.040 ms; ddbc::FetchAll_wrap +0.040 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).

Unix / SQL Server 2025

SELECT queries: ddbc::FetchBatchData +0.017 ms; ddbc::SQLExecDirect_wrap +0.017 ms; py::execute::cpp_call +0.016 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Fetch-all queries: ddbc::FetchBatchData::SQLFetchScroll_call +0.362 ms; ddbc::FetchBatchData::cache_column_metadata +0.009 ms; ddbc::SQLBindColums +0.002 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Row-by-row fetching: ddbc::FetchOne_wrap +0.050 ms; ddbc::SQLGetData_wrap +0.011 ms; py::fetchone::cpp_call +0.003 ms. Call changes: ddbc::fetch_bindings::SQL_UNBIND (added, removed, or intermittent).
Batched row fetching: ddbc::SQLDescribeCol::driver_call +0.000 ms; ddbc::SQLNumResultCols_wrap +0.000 ms. Call changes: ddbc::SQLBindColums (51 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).
Arrow row fetching: ddbc::SQLBindColums +0.002 ms; ddbc::SQLDescribeCol_wrap +0.002 ms; ddbc::SQLNumResultCols_wrap +0.000 ms. Call changes: ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent).
Row fetching in batches of 100: ddbc::FetchBatchData::SQLFetchScroll_call +0.362 ms; ddbc::FetchBatchData +0.331 ms; ddbc::SQLNumResultCols_wrap +0.004 ms. Call changes: ddbc::SQLBindColums (501 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).
Row fetching in batches of 10,000: py::fetchmany::row_wrap +0.099 ms; ddbc::FetchBatchData::SQLFetchScroll_call +0.073 ms; ddbc::SQLNumResultCols_wrap +0.003 ms. Call changes: ddbc::SQLBindColums (6 -> 1 calls); ddbc::fetch_bindings::SQLBindCol (added, removed, or intermittent); ddbc::fetch_bindings::SQLGetStmtAttr (added, removed, or intermittent).

6 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 10.490 ms 10.617 ms +0.0% no signal
SELECT queries 1.118 ms 1.090 ms -0.6% no signal
Row insertion 35.442 ms 35.199 ms -1.0% no signal
Executemany inserts 158.149 ms 156.975 ms +1.4% no signal
Fetch-all queries 121.906 ms 121.451 ms -0.4% no signal
Row-by-row fetching 14.276 ms 14.367 ms +1.5% no signal
Batched row fetching 117.883 ms 117.980 ms +1.3% no signal
Transaction commit and rollback 118.723 ms 117.192 ms -3.0% no signal
Arrow row fetching 93.949 ms 96.155 ms +2.8% no signal
100,000-row insertion 474.826 ms 463.718 ms -5.0% no signal
Row fetching in batches of 100 125.532 ms 118.507 ms -5.7% no signal
Row fetching in batches of 10,000 133.201 ms 127.152 ms -6.0% no signal
Repeated positional queries 34.177 ms 34.278 ms +1.1% no signal
Repeated named-parameter queries 36.610 ms 36.979 ms +0.9% no signal
Legacy 100,000-row insertion 354.195 ms 354.370 ms +0.3% no signal
Insertion with explicit input sizes 502.580 ms 503.979 ms +0.8% no signal
Joined aggregation queries 178.072 ms 178.106 ms -0.1% no signal
Large joined-result fetching 188.113 ms 187.478 ms -0.3% no signal
1.2-million-row fetching 3478.020 ms 3477.445 ms -0.0% no signal
Common table expression queries 5.317 ms 5.388 ms -0.5% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.279 ms 1.289 ms -6.6% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 97.767 ms 97.855 ms +0.1% no signal
SELECT queries 1.111 ms 1.111 ms -0.3% no signal
Row insertion 34.218 ms 34.292 ms +2.3% no signal
Executemany inserts 149.271 ms 157.497 ms +6.4% no signal
Fetch-all queries 124.340 ms 120.785 ms -3.6% no signal
Row-by-row fetching 14.723 ms 14.351 ms -3.3% no signal
Batched row fetching 124.122 ms 117.315 ms -5.5% no signal
Transaction commit and rollback 115.328 ms 119.328 ms -0.4% no signal
Arrow row fetching 94.399 ms 94.769 ms -3.0% no signal
100,000-row insertion 471.331 ms 444.343 ms -8.3% no signal
Row fetching in batches of 100 121.621 ms 117.695 ms -3.3% no signal
Row fetching in batches of 10,000 128.800 ms 126.365 ms -0.1% no signal
Repeated positional queries 33.616 ms 33.758 ms -0.1% no signal
Repeated named-parameter queries 36.511 ms 35.365 ms -3.7% no signal
Legacy 100,000-row insertion 353.950 ms 358.754 ms -1.3% no signal
Insertion with explicit input sizes 489.987 ms 503.304 ms -1.8% no signal
Joined aggregation queries 161.470 ms 160.937 ms -0.2% no signal
Large joined-result fetching 186.692 ms 185.633 ms +0.3% no signal
1.2-million-row fetching 3538.321 ms 3573.126 ms +1.3% no signal
Common table expression queries 5.069 ms 5.195 ms +0.1% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.496 ms 1.513 ms -1.4% no signal
Build and measurement details

ADO build 178754

PR head: 4d12d4f4a90dee64acd43f3ab81e714116e58be3
Base: fead15c30e49172bab643bc9cc5504936e86459e
Measured merge: d24b913850c704b3f05b34f694dad13dc590f7cb

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

Comment thread mssql_python/pybind/ddbc_bindings.cpp Fixed
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.

Copilot review overview

🟡 Changes recommended

A potential teardown deadlock, unresolved cleanup-leak behavior, and missing regression coverage block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds statement-owned native buffers and ODBC binding-plan reuse for compatible fetchmany() calls, with lifecycle cleanup and invalidation.

Changes:

  • Reuses buffers and bindings across compatible batches.
  • Handles transitions, fallback paths, and connection cleanup.
  • Adds binding lifecycle and ownership management.
File Summary
mssql_python/​pybind/​fetch_bindings.hpp Defines reusable buffers, binding plans, and ownership logic; terminal cleanup leaks remain unresolved.
mssql_python/​pybind/​ddbc_bindings.h Integrates fetch-binding state into statement handles.
mssql_python/​pybind/​ddbc_bindings.cpp Implements reuse and cleanup; contains a potential teardown deadlock and lacks regression coverage for reuse transitions.
mssql_python/​pybind/​connection/​connection.h Extends metadata cleanup configuration.
mssql_python/​pybind/​connection/​connection.cpp Coordinates fetch-binding cleanup with connection lifecycle operations.

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

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

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

83%


🎯 Overall Coverage

83%


📈 Total Lines Covered: 9659 out of 11525
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/pybind/connection/connection.cpp (83.3%): Missing lines 284
  • mssql_python/pybind/ddbc_bindings.cpp (81.1%): Missing lines 1590-1591,1598-1599,1620-1627,1643-1649,1652-1653,1678-1680,1697-1698,1705,1707-1709,1711-1712,1741,1775-1777,1809-1810,1815-1816,1818-1819,1821-1825,1947,2099,2171,2174-2175,2251-2252,3495,4557,4633,4689,5265-5267,5383,5411
  • mssql_python/pybind/ddbc_bindings.h (100%)
  • mssql_python/pybind/fetch_bindings.hpp (90.5%): Missing lines 113-116,152-153,186

Summary

  • Total: 421 lines
  • Missing: 71 lines
  • Coverage: 83%

mssql_python/pybind/connection/connection.cpp

Lines 280-288

  280     // Releasing the last handle can acquire the connection cleanup gate.
  281     // Keep that destruction outside the child-list lock.
  282     for (const auto& handle : handles) {
  283         handle->resultMetadata.clear();
! 284         if (detachFetchBindings && handle->fetchBindings.hasPlan()) {
  285             handle->requireDetachedFetchBindings();
  286         }
  287     }
  288 }

mssql_python/pybind/ddbc_bindings.cpp

Lines 1586-1595

  1586         std::rethrow_exception(m_loadError);
  1587     }
  1588 }
  1589 
! 1590 static void CaptureFetchBindingDiagnostics(SQLHSTMT stmt, SQLRETURN ret,
! 1591                                           FetchBindingDiagnostics* diagnostics);
  1592 static void AppendFetchBindingDiagnostics(py::handle messages,
  1593                                          const FetchBindingDiagnostics& diagnostics,
  1594                                          bool preserveFailure = false);

Lines 1594-1603

  1594                                          bool preserveFailure = false);
  1595 
  1596 SQLRETURN FetchBindingPlan::attach(SQLHSTMT stmt, FetchBindingDiagnostics* diagnostics) {
  1597     reusable = false;
! 1598     needsReset = true;
! 1599     SQLRETURN ret;
  1600     {
  1601         PERF_TIMER("fetch_bindings::SQLSetStmtAttr::ROW_ARRAY_SIZE");
  1602         ret = SQLSetStmtAttr_ptr(
  1603             stmt, SQL_ATTR_ROW_ARRAY_SIZE,

Lines 1616-1631

  1616     if (!SQL_SUCCEEDED(ret)) {
  1617         return ret;
  1618     }
  1619     if (activeSize != static_cast<SQLULEN>(fetchSize)) {
! 1620         throw std::runtime_error("ODBC changed the requested fetch row-array size");
! 1621     }
! 1622     driverMayReference = true;
! 1623     {
! 1624         PERF_TIMER("fetch_bindings::SQLSetStmtAttr::ROWS_FETCHED_PTR");
! 1625         ret = SQLSetStmtAttr_ptr(stmt, SQL_ATTR_ROWS_FETCHED_PTR, &rowsFetched, 0);
! 1626     }
! 1627     CaptureFetchBindingDiagnostics(stmt, ret, diagnostics);
  1628     if (!SQL_SUCCEEDED(ret)) {
  1629         return ret;
  1630     }
  1631     for (const auto& column : bindings) {

Lines 1639-1657

  1639             return ret;
  1640         }
  1641     }
  1642     reusable = true;
! 1643     return ret;
! 1644 }
! 1645 
! 1646 SQLRETURN FetchBindingPlan::detach(SQLHSTMT stmt, FetchBindingDiagnostics* diagnostics) {
! 1647     reusable = false;
! 1648     if (!needsReset) {
! 1649         return SQL_SUCCESS;
  1650     }
  1651     SQLRETURN ret;
! 1652     {
! 1653         PERF_TIMER("fetch_bindings::SQL_UNBIND");
  1654         ret = SQLFreeStmt_ptr(stmt, SQL_UNBIND);
  1655     }
  1656     CaptureFetchBindingDiagnostics(stmt, ret, diagnostics);
  1657     if (!SQL_SUCCEEDED(ret)) {

Lines 1674-1684

  1674     if (SQL_SUCCEEDED(ret)) {
  1675         needsReset = false;
  1676     }
  1677     return ret;
! 1678 }
! 1679 
! 1680 namespace {
  1681 
  1682 inline SQLRETURN BeginResultTransition(const SqlHandlePtr& stmt) {
  1683     stmt->resultMetadata.clear();
  1684     return stmt->detachFetchBindings();

Lines 1693-1702

  1693     }
  1694     ThrowStdException(message);
  1695 }
  1696 
! 1697 }  // namespace
! 1698 
  1699 // SqlHandle definition
  1700 SqlHandle::SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle,
  1701                      std::shared_ptr<ConnectionCleanupState> cleanupState)
  1702     : _type(type), _handle(rawHandle), _cleanupState(std::move(cleanupState)) {}

Lines 1701-1716

  1701                      std::shared_ptr<ConnectionCleanupState> cleanupState)
  1702     : _type(type), _handle(rawHandle), _cleanupState(std::move(cleanupState)) {}
  1703 
  1704 SqlHandle::~SqlHandle() {
! 1705     try {
  1706         if (_handle) {
! 1707             SQLRETURN ret = freeHandle();
! 1708             if (!SQL_SUCCEEDED(ret)) {
! 1709                 // A failed free leaves a live handle. Detach if possible before
  1710                 // the last plan owner applies its native-only emergency policy.
! 1711                 SQLRETURN detached = detachFetchBindings();
! 1712                 std::fputs(
  1713                     SQL_SUCCEEDED(detached)
  1714                         ? "mssql-python: native handle cleanup failed; fetch buffer detach succeeded\n"
  1715                         : "mssql-python: native handle cleanup failed; fetch buffer detach failed\n",
  1716                     stderr);

Lines 1737-1745

  1737 }
  1738 
  1739 void SqlHandle::markImplicitlyFreed() {
  1740     // SAFETY: Only STMT handles should be marked as implicitly freed.
! 1741     // Successful SQLDisconnect frees the connection's child statements.
  1742     // Other handle types (ENV, DBC, DESC) are NOT automatically freed by parents.
  1743     // Calling this on wrong handle types will cause silent handle leaks.
  1744     if (_type != SQL_HANDLE_STMT) {
  1745         // Log error but don't throw - we're likely in cleanup/destructor path

Lines 1771-1781

  1771 
  1772 SQLRETURN SqlHandle::detachFetchBindingsNative(FetchBindingDiagnostics* diagnostics) {
  1773     auto plan = fetchBindings.snapshot();
  1774     if (!plan) {
! 1775         return SQL_SUCCESS;
! 1776     }
! 1777     if (_implicitly_freed || (_cleanupState && _cleanupState->disconnected)) {
  1778         fetchBindings.nativeReleased();
  1779         return SQL_SUCCESS;
  1780     }
  1781     if (!_handle || !SQLFreeStmt_ptr || !SQLSetStmtAttr_ptr) {

Lines 1805-1829

  1805     SQLRETURN ret;
  1806     try {
  1807         if (PyGILState_Check()) {
  1808             py::gil_scoped_release release;
! 1809             ret = detachNative();
! 1810         } else {
  1811             ret = detachNative();
  1812         }
  1813     } catch (...) {
  1814         resultMetadata.clear();
! 1815         AppendFetchBindingDiagnostics(messages, diagnostics, true);
! 1816         throw;
  1817     }
! 1818     if (!SQL_SUCCEEDED(ret)) {
! 1819         resultMetadata.clear();
  1820     }
! 1821     // No Python objects or callbacks while the native cleanup gate is held.
! 1822     AppendFetchBindingDiagnostics(messages, diagnostics, !SQL_SUCCEEDED(ret));
! 1823     return ret;
! 1824 }
! 1825 
  1826 void SqlHandle::requireDetachedFetchBindings() {
  1827     SQLRETURN ret = detachFetchBindings();
  1828     if (!SQL_SUCCEEDED(ret)) {
  1829         ThrowFetchCleanupError(_type, _handle, ret, "Detaching retained fetch buffers");

Lines 1943-1951

  1943     }
  1944     if (statementHandle->isImplicitlyFreed()) {
  1945         return SQL_INVALID_HANDLE;
  1946     }
! 1947     if (SQLRETURN ret = BeginResultTransition(statementHandle); !SQL_SUCCEEDED(ret)) {
  1948         return ret;
  1949     }
  1950     if (!SQLFreeStmt_ptr) {
  1951         DriverLoader::getInstance().loadDriver();

Lines 2095-2103

  2095                           const py::object& schemaObj, const py::object& tableObj,
  2096                           const py::object& columnObj) {
  2097     PERF_TIMER("SQLColumns_wrap");
  2098     if (SQLRETURN ret = BeginResultTransition(StatementHandle); !SQL_SUCCEEDED(ret)) {
! 2099         return ret;
  2100     }
  2101     if (!SQLColumns_ptr) {
  2102         ThrowStdException("SQLColumns function not loaded");
  2103     }

Lines 2167-2179

  2167                              const std::string& message) {
  2168     py::tuple record = py::make_tuple(py::str(state), py::str(message));
  2169     if (PyList_Append(records.ptr(), record.ptr()) < 0)
  2170         throw py::error_already_set();
! 2171 }
  2172 
  2173 static void AppendDiagRecords(SQLHANDLE rawHandle, SQLSMALLINT handleType, py::handle records,
! 2174                               bool internalTruncation = false,
! 2175                               FetchBindingDiagnostics* nativeRecords = nullptr) {
  2176     // Iterate through all available diagnostic records
  2177     for (SQLSMALLINT recNumber = 1;; recNumber++) {
  2178         SQLWCHAR sqlState[6] = {0};
  2179         if (internalTruncation && SQLGetDiagField_ptr) {

Lines 2247-2256

  2247         error.discard_as_unraisable("fetch binding diagnostics");
  2248     } catch (const std::exception& error) {
  2249         if (!preserveFailure)
  2250             throw;
! 2251         std::fputs("mssql-python: failed to append fetch binding diagnostics: ", stderr);
! 2252         std::fputs(error.what(), stderr);
  2253         std::fputc('\n', stderr);
  2254     }
  2255 }

Lines 3491-3499

  3491 SQLRETURN SQLFetch_wrap(SqlHandlePtr StatementHandle) {
  3492     PERF_TIMER("SQLFetch_wrap");
  3493     if (SQLRETURN ret = StatementHandle->detachFetchBindings(); !SQL_SUCCEEDED(ret)) {
  3494         return ret;
! 3495     }
  3496     LOG("SQLFetch: Fetching next row for statement_handle=%p", (void*)StatementHandle->get());
  3497     if (!SQLFetch_ptr) {
  3498         LOG("SQLFetch: Function pointer not initialized, loading driver");
  3499         DriverLoader::getInstance().loadDriver();  // Load the driver

Lines 4553-4561

  4553     SQLRETURN ret = SQL_SUCCESS;
  4554     const bool useWideChar = (charCtype == SQL_C_WCHAR);
  4555     auto bindColumn = [bindings](SQLHSTMT stmt, SQLUSMALLINT column, SQLSMALLINT cType,
  4556                                  SQLPOINTER data, SQLLEN length, SQLLEN* indicators) -> SQLRETURN {
! 4557         if (bindings) {
  4558             bindings->push_back({column, cType, data, length, indicators});
  4559             return SQL_SUCCESS;
  4560         }
  4561         PERF_TIMER("fetch_bindings::SQLBindCol");

Lines 4629-4637

  4629                                      sizeof(SQLCHAR), buffers.indicators[col - 1].data());
  4630                 break;
  4631             case SQL_REAL:
  4632                 buffers.realBuffers[col - 1].resize(fetchSize);
! 4633                 ret = bindColumn(hStmt, col, SQL_C_FLOAT, buffers.realBuffers[col - 1].data(),
  4634                                      sizeof(SQLREAL), buffers.indicators[col - 1].data());
  4635                 break;
  4636             case SQL_DECIMAL:
  4637             case SQL_NUMERIC:

Lines 4685-4693

  4685                 // TODO: handle variable length data correctly. This logic wont
  4686                 // suffice
  4687                 HandleZeroColumnSizeAtFetch(columnSize);
  4688                 buffers.charBuffers[col - 1].resize(fetchSize * columnSize);
! 4689                 ret = bindColumn(hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(),
  4690                                      columnSize, buffers.indicators[col - 1].data());
  4691                 break;
  4692             case SQL_SS_TIMESTAMPOFFSET:
  4693                 buffers.datetimeoffsetBuffers[col - 1].resize(fetchSize);

Lines 5261-5271

  5261     ResultMetadataFailureGuard metadataFailure(StatementHandle->resultMetadata, ret);
  5262     if (fetchSize <= 0) {
  5263         ThrowStdException("Native fetchmany requires a positive fetch size");
  5264     }
! 5265     auto plan = StatementHandle->fetchBindings.snapshot();
! 5266     const auto metadataSnapshot = StatementHandle->resultMetadata.snapshot();
! 5267     if (plan && !plan->matches(metadataSnapshot, fetchSize, charEncoding, wcharEncoding, charCtype)) {
  5268         ret = StatementHandle->detachFetchBindings(nullptr, messages);
  5269         if (!SQL_SUCCEEDED(ret)) {
  5270             return ret;
  5271         }

Lines 5379-5387

  5379             StatementHandle->fetchBindings.install(plan);
  5380             FetchBindingDiagnostics diagnostics;
  5381             try {
  5382                 ret = plan->attach(hStmt, messages && !messages.is_none() ? &diagnostics : nullptr);
! 5383             } catch (...) {
  5384                 AppendFetchBindingDiagnostics(messages, diagnostics, true);
  5385                 throw;
  5386             }
  5387             AppendFetchBindingDiagnostics(messages, diagnostics, !SQL_SUCCEEDED(ret));

Lines 5407-5415

  5407         ret = StatementHandle->detachFetchBindings(nullptr, messages);
  5408         if (!SQL_SUCCEEDED(ret)) {
  5409             return ret;
  5410         }
! 5411     }
  5412     // Initialize column buffers
  5413     ColumnBuffers buffers(numCols, fetchSize);
  5414     FetchStateGuard fetchStateGuard(StatementHandle, messages);

mssql_python/pybind/fetch_bindings.hpp

Lines 109-120

  109         void operator()(FetchBindingPlan* plan) const noexcept {
  110             if (plan->driverMayReference.load()) {
  111                 // Final owner only: freeing this allocation could leave driver
  112                 // pointers dangling after failed native cleanup or finalization.
! 113                 std::fputs("mssql-python: retaining fetch buffers after unconfirmed native "
! 114                            "cleanup until process exit\n", stderr);
! 115                 return;
! 116             }
  117             delete plan;
  118         }
  119     };

Lines 148-157

  148 
  149     void install(const std::shared_ptr<FetchBindingPlan>& plan) {
  150         std::lock_guard<std::mutex> lock(mutex_);
  151         if (plan_) {
! 152             throw std::logic_error("Fetch bindings must be detached before replacement");
! 153         }
  154         plan_ = plan;
  155         hasPlan_.store(true, std::memory_order_release);
  156     }

Lines 182-190

  182     }
  183 
  184     bool eligible() const { return eligible_.load(); }
  185 
! 186     void disableReuse() { eligible_ = false; }
  187 
  188   private:
  189     mutable std::mutex mutex_;
  190     std::shared_ptr<FetchBindingPlan> plan_;


📋 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.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 77.3%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 82.9%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.fetch_bindings.hpp: 90.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI review requested due to automatic review settings September 25, 2026 06: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.

Copilot review overview

🟡 Changes recommended

Fix the Ubuntu Release configuration and avoid std::cerr in the post-shutdown destructor path.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Avoid std::cerr in destructor after Python shutdown

mssql_python/​pybind/​ddbc_bindings.cpp:1672

This destructor is explicitly documented below as a path that must not log because it can run after Python shutdown, but the new failure path writes through std::cerr. C++ iostream teardown is not guaranteed to be usable from such a destructor; use the existing non-throwing native fputs/stderr style or suppress this diagnostic.

Comment thread eng/pipelines/pr-validation-pipeline.yml Outdated
Comment thread mssql_python/pybind/CMakeLists.txt Outdated
Revert 1c05d11 after the user rejected the expanded scope. Restore the exact be219eb source tree, preserving the earlier review fixes and profiler regressions.

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

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

Teardown-safe diagnostics and fault-injection coverage for failed cleanup are still needed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

Comment thread mssql_python/pybind/fetch_bindings.hpp
Exercise an injected SQL_UNBIND failure in the existing isolated profiler test suite. Assert that failed cleanup blocks reuse and rebinding, then verify recovery and EOF without changing production or build configuration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Check the existing SQLFetchScroll call counter alongside plan allocation and binding counts in the single cleanup regression.

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

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

Cleanup failure paths remain insufficiently covered, and the test scope conflicts with the stated validation evidence.

Get a fresh assessment by requesting another Copilot review.

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

Open (3)
Resolved since last review (1)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread tests/test_025_profiler.py Outdated
Parameterize the existing isolated cleanup regression to fail either SQL_UNBIND or clearing SQL_ATTR_ROWS_FETCHED_PTR. Preserve no-allocation, no-bind, no-fetch, retry, and recovery assertions without changing production or build configuration.

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

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

Two moderate cleanup and idempotency issues remain unresolved.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Avoid std::cerr in destructor during shutdown

mssql_python/​pybind/​ddbc_bindings.cpp:1672

This destructor can run during interpreter/process teardown, but the new std::cerr use depends on the C++ iostream runtime and conflicts with the nearby rule that destructors must not log (ddbc_bindings.cpp:1713-1718). If cleanup fails late in shutdown, this can access torn-down iostream state and turn an already-handled cleanup failure into a crash. Use the existing C-style stderr path (or omit this diagnostic) instead of std::cerr.

Copilot AI review requested due to automatic review settings September 28, 2026 03:52

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

Replace unsafe std::cerr logging in the destructor with teardown-safe native reporting or remove the diagnostic.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Copilot AI review requested due to automatic review settings September 28, 2026 06: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.

Copilot review overview

🔵 Needs a closer look

Replace or suppress the destructor-time std::cerr logging to avoid unsafe behavior during runtime shutdown.

Review effort: Lite
Findings: 1 High severity

Open (1)

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Use fixed native stderr messages for failed handle cleanup and detach status without relying on C++ iostream teardown ordering. Cleanup and retained-buffer ownership remain unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 28, 2026 10: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.

Copilot review overview

🔵 Needs a closer look

Native lifecycle changes are complex, and latency and no-regression validation remain open.

Review effort: Lite
Findings: None

Resolved since last review (1)

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.

6 participants