Skip to content

PERF: Optimize pooled connection return while preserving transaction safety - #820

Merged
Gaurav Sharma (bewithgaurav) merged 18 commits into
mainfrom
sumitmsft/revert-777-pooling-performance-20260925
Nov 2, 2026
Merged

Gaurav Sharma (bewithgaurav) merged 18 commits into
mainfrom
sumitmsft/revert-777-pooling-performance-20260925

Conversation

@sumitmsft

@sumitmsft Sumit Sarabhai (sumitmsft) commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

AB#48076

Summary

This PR now replaces the earlier revert with a targeted pooling optimization. It retains the transaction, failure-discard, pool-capacity and native handle-lifetime protections from #777.

  • Skip pool sanitation only when a prior successful rollback/autocommit restoration established clean state and no subsequent statement allocation or uncertain operation invalidated it. A new login or deferred reset alone never establishes clean state.
  • Retained statement aliases, raw-handle exposure and unknown connection attributes remain conservative. Valid scalar login timeouts such as timeout=30 do not permanently disable the fast path.
  • Move close-time transaction handling into native code and consolidate required sanitation into one autocommit probe, metadata invalidation and GIL-release scope. Never turn autocommit on after a failed rollback.
  • Preserve cleanup gates, discard/capacity recovery, pool-generation and shutdown protections. Extend the existing explicit-transaction integration cases and add deterministic native failure/call-count coverage.

Scope and limitations

Used or uncertain connections still execute full transaction sanitation. This does not assume autocommit=True means there cannot be an explicit SQL transaction, and it does not remove safety synchronization to improve timings. End-to-end performance improvements and live SQL behavior remain subject to validation; no blanket regression-elimination claim is made.

Validation

  • Windows x64 Release extension and native test executable built successfully.
  • 43 deterministic native sanitation cases passed; CTest passed.
  • 24 targeted Python tests passed using the freshly built extension.
  • Positive-timeout coverage verifies that timeout 30 reaches physical login and that 100 subsequent unused pooled leases issue no sanitation calls.
  • Three expanded live-SQL integration cases collected successfully but were not executed locally. Normal cross-platform PR validation and performance evaluation are pending.

This reverts commit 2a86fc1 while preserving subsequent result-metadata changes. Restores the prior pooling behavior; the pooled-transaction correctness issue fixed by #777 will need a replacement fix.

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

github-actions Bot commented Sep 25, 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: ddbc::SQLExecDirect_wrap +0.015 ms; py::execute::cpp_call +0.014 ms; ddbc::SQLBindColums +0.007 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Fetch-all queries: ddbc::FetchBatchData::SQLFetchScroll_call +0.847 ms; py::fetchall::row_wrap +0.036 ms; ddbc::FetchBatchData::cache_column_metadata +0.019 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row-by-row fetching: py::fetchone::row_wrap +0.012 ms; ddbc::SQLDescribeCol::driver_call +0.000 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Batched row fetching: ddbc::FetchBatchData +1.255 ms; ddbc::FetchBatchData::SQLFetchScroll_call +1.195 ms; ddbc::FetchMany_wrap +1.188 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row fetching in batches of 100: ddbc::FetchBatchData::SQLFetchScroll_call +1.054 ms; ddbc::FetchBatchData +0.186 ms; py::fetchmany::cpp_call +0.100 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row fetching in batches of 10,000: ddbc::FetchMany_wrap +9.939 ms; py::fetchmany::cpp_call +9.621 ms; ddbc::SQLBindColums +2.733 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Joined aggregation queries: ddbc::SQLExecDirect_wrap +0.728 ms; py::execute::cpp_call +0.711 ms; py::execute::post_execute +0.022 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Large joined-result fetching: py::fetchall::cpp_call +0.349 ms; ddbc::FetchBatchData +0.241 ms; ddbc::FetchBatchData::construct_rows +0.240 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
1.2-million-row fetching: ddbc::FetchBatchData::SQLFetchScroll_call +17.856 ms; py::fetchall::cpp_call +8.007 ms; ddbc::FetchBatchData +7.369 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Common table expression queries: ddbc::SQLDescribeCol_wrap +0.000 ms; ddbc::FetchBatchData::construct_rows +0.000 ms; py::execute::param_prep +0.000 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
256 KiB VARCHAR(MAX) / fetchall(): ddbc::FetchLobColumnData +0.222 ms; ddbc::SQLGetData_wrap +0.222 ms; ddbc::FetchAll_wrap +0.096 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
10,000 scalar values / fetchval() (debug disabled): ddbc::SQLGetData_wrap +0.005 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).

Unix / SQL Server 2025

SELECT queries: ddbc::SQLBindColums +0.011 ms; ddbc::SQLExecDirect_wrap +0.008 ms; py::execute::cpp_call +0.008 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Fetch-all queries: ddbc::FetchBatchData::cache_column_metadata +0.014 ms; py::fetchall::row_wrap +0.010 ms; ddbc::SQLNumResultCols_wrap +0.000 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row-by-row fetching: py::fetchone::row_wrap +0.007 ms; ddbc::SQLNumResultCols_wrap +0.004 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Batched row fetching: ddbc::FetchBatchData::construct_rows +0.013 ms; ddbc::FetchBatchData::cache_column_metadata +0.005 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row fetching in batches of 100: ddbc::FetchBatchData::construct_rows +0.308 ms; ddbc::SQLBindColums +0.066 ms; ddbc::FetchBatchData::cache_column_metadata +0.010 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Row fetching in batches of 10,000: ddbc::SQLBindColums +16.934 ms; py::fetchmany::cpp_call +14.019 ms; ddbc::FetchMany_wrap +13.623 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Joined aggregation queries: py::execute::cpp_call +0.398 ms; ddbc::SQLExecDirect_wrap +0.396 ms; py::execute::diag_records +0.000 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).
Large joined-result fetching: ddbc::FetchBatchData::SQLFetchScroll_call +1.925 ms; py::fetchall::cpp_call +1.228 ms; ddbc::FetchBatchData +0.995 ms. Call changes: ddbc::AppendDiagRecords::SQLGetDiagRec_call (added, removed, or intermittent).

4 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.029 ms 10.419 ms +6.4% no signal
SELECT queries 1.069 ms 0.986 ms -6.6% no signal
Row insertion 31.041 ms 31.019 ms +1.0% no signal
Executemany inserts 155.436 ms 156.380 ms +0.9% no signal
Fetch-all queries 118.470 ms 118.407 ms +1.1% no signal
Row-by-row fetching 14.367 ms 14.059 ms -2.1% no signal
Batched row fetching 115.291 ms 115.970 ms +0.8% no signal
Transaction commit and rollback 100.681 ms 100.013 ms -0.6% no signal
Arrow row fetching 88.398 ms 89.735 ms -0.5% no signal
100,000-row insertion 423.790 ms 424.477 ms +0.7% no signal
Row fetching in batches of 100 119.598 ms 119.365 ms +0.6% no signal
Row fetching in batches of 10,000 123.461 ms 136.349 ms +7.2% no signal
Repeated positional queries 30.281 ms 30.075 ms -1.1% no signal
Repeated named-parameter queries 32.560 ms 32.671 ms +0.1% no signal
Legacy 100,000-row insertion 338.211 ms 343.269 ms +1.4% no signal
Insertion with explicit input sizes 474.128 ms 475.673 ms +0.4% no signal
Joined aggregation queries 174.903 ms 175.542 ms +0.1% no signal
Large joined-result fetching 188.719 ms 186.089 ms -0.6% no signal
1.2-million-row fetching 3385.788 ms 3393.658 ms +0.3% no signal
Common table expression queries 5.362 ms 5.238 ms -2.9% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.445 ms 1.513 ms -1.3% no signal
10,000 scalar values / fetchval() (debug disabled) 105.320 ms 105.454 ms -0.8% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 96.100 ms 96.024 ms -0.6% no signal
SELECT queries 1.089 ms 1.017 ms -7.1% no signal
Row insertion 31.532 ms 31.326 ms -0.7% no signal
Executemany inserts 132.979 ms 132.670 ms +0.2% no signal
Fetch-all queries 126.432 ms 122.006 ms -1.8% no signal
Row-by-row fetching 12.746 ms 12.749 ms -0.8% no signal
Batched row fetching 114.189 ms 111.599 ms -2.1% no signal
Transaction commit and rollback 101.001 ms 100.594 ms -0.8% no signal
Arrow row fetching 86.665 ms 85.917 ms -0.3% no signal
100,000-row insertion 421.664 ms 415.602 ms -1.4% no signal
Row fetching in batches of 100 109.506 ms 106.736 ms -2.6% no signal
Row fetching in batches of 10,000 130.415 ms 147.565 ms +13.2% no signal
Repeated positional queries 30.218 ms 29.892 ms -0.7% no signal
Repeated named-parameter queries 32.475 ms 32.359 ms -0.4% no signal
Legacy 100,000-row insertion 337.365 ms 340.393 ms +0.9% no signal
Insertion with explicit input sizes 481.648 ms 480.583 ms -1.0% no signal
Joined aggregation queries 165.314 ms 165.616 ms +0.7% no signal
Large joined-result fetching 201.473 ms 203.426 ms +0.6% no signal
1.2-million-row fetching 3585.553 ms 3530.965 ms -1.6% no signal
Common table expression queries 5.463 ms 5.340 ms -1.9% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.605 ms 1.416 ms -10.2% no signal
10,000 scalar values / fetchval() (debug disabled) 101.244 ms 100.496 ms -0.7% no signal
Build and measurement details

ADO build 188282

PR head: 73a0ebf300b70dcec28a002d11da8a613fa18c32
Base: d800bdc65f249f5592d28c7bf6d3c07b819a2c87
Measured merge: 26beb3e2cca06732c3c2e738c9bc67d54f872928

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

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

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

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

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

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 critical cleanup, destructor, handle-lifecycle, and disconnect-synchronization issues block approval.

Get a fresh assessment by requesting another Copilot review.

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

Open (5)
What changed in this PR

Reverts PR #777 to restore prior pooled-connection behavior while retaining result-metadata invalidation.

Changes:

  • Removes pooled transaction sanitization and related regression tests.
  • Restores previous native connection, handle cleanup, and pool-return behavior.
  • Removes the reverted changelog entry.
File Summary
tests/​test_009_pooling.py Removes pooling and native lifecycle regression tests.
tests/​test_006_exceptions.py Removes close-failure tests.
mssql_python/​pybind/​ddbc_bindings.h Removes cleanup-state APIs.
mssql_python/​pybind/​ddbc_bindings.cpp Restores earlier handle-management behavior.
mssql_python/​pybind/​connection/​connection.h Removes pool-sanitation interfaces.
mssql_python/​pybind/​connection/​connection.cpp Restores prior disconnect and pool-return behavior.
mssql_python/​pybind/​connection/​connection_pool.h Removes discard/origin-pool APIs.
mssql_python/​pybind/​connection/​connection_pool.cpp Restores key-based pool returns.
mssql_python/​connection.py Restores the previous close flow.
CHANGELOG.md Removes the reverted fix entry.

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

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

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

79%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 9624 out of 11318
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/connection.py (100%)
  • mssql_python/pybind/connection/connection.cpp (79.2%): Missing lines 291,310,331,337-339,375-376,482-486,490-491,642-644,667,712-715,750-751,762,814-819,843-844,991,1048,1050-1051
  • mssql_python/pybind/ddbc_bindings.cpp (80.0%): Missing lines 1593-1595,1678

Summary

  • Total: 204 lines
  • Missing: 42 lines
  • Coverage: 79%

mssql_python/pybind/connection/connection.cpp

Lines 287-295

  287 
  288 void Connection::commit() {
  289     PERF_TIMER("Connection::commit");
  290     _poolClean = false;
! 291     _poolSessionReset = false;
  292     if (!_dbcHandle) {
  293         ThrowStdException("Connection handle not allocated");
  294     }
  295     updateLastUsed();

Lines 306-314

  306 
  307 void Connection::rollback() {
  308     PERF_TIMER("Connection::rollback");
  309     _poolClean = false;
! 310     _poolSessionReset = false;
  311     if (!_dbcHandle) {
  312         ThrowStdException("Connection handle not allocated");
  313     }
  314     updateLastUsed();

Lines 327-335

  327     PERF_TIMER("Connection::setAutocommit");
  328     if (!enable) {
  329         _poolClean = false;
  330         _poolSessionReset = false;
! 331     }
  332     if (!_dbcHandle) {
  333         ThrowStdException("Connection handle not allocated");
  334     }
  335     clearResultMetadata();

Lines 333-343

  333         ThrowStdException("Connection handle not allocated");
  334     }
  335     clearResultMetadata();
  336     // Only successful native sanitation with no retained statement/raw handle
! 337     // aliases proves ON. A new login or _autocommit alone is not proof.
! 338     if (!force && enable && _dbcHandle->get() && _poolClean && !_poolProofDisabled) {
! 339         _autocommit = true;
  340         return;
  341     }
  342     SQLINTEGER value = enable ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF;
  343     LOG("Setting autocommit=%d", enable);

Lines 371-380

  371     SQLINTEGER value;
  372     SQLINTEGER string_length;
  373     SQLRETURN ret = SQLGetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_AUTOCOMMIT, &value,
  374                                           sizeof(value), &string_length);
! 375     if (!SQL_SUCCEEDED(ret)) {
! 376         _poolClean = false;
  377         checkError(ret);
  378     }
  379     return value == SQL_AUTOCOMMIT_ON;
  380 }

Lines 478-495

  478     }
  479 
  480     if (py::isinstance<py::int_>(value)) {
  481         // Get the integer value
! 482         int64_t longValue;
! 483         try {
! 484             longValue = value.cast<int64_t>();
! 485         } catch (const py::cast_error&) {
! 486             _poolProofDisabled = true;
  487             throw;
  488         } catch (const py::error_already_set&) {
  489             _poolProofDisabled = true;
! 490             throw;
! 491         }
  492         if (scalarLoginTimeout &&
  493             (longValue < 0 ||
  494              static_cast<uint64_t>(longValue) > std::numeric_limits<SQLUINTEGER>::max())) {
  495             _poolProofDisabled = true;

Lines 638-648

  638         const bool preservePoolProof =
  639             key == SQL_ATTR_AUTOCOMMIT && PyLong_CheckExact(item.second.ptr());
  640         const bool poolProofDisabled = _poolProofDisabled;
  641         SQLRETURN ret = setAttribute(key, py::reinterpret_borrow<py::object>(item.second));
! 642         if (preservePoolProof) {
! 643             _poolProofDisabled = poolProofDisabled;
! 644         }
  645         if (!SQL_SUCCEEDED(ret)) {
  646             std::string attrName = std::to_string(key);
  647             std::string errorMsg = "Failed to set attribute " + attrName + " before connect";
  648             ThrowStdException(errorMsg);

Lines 663-671

  663 bool Connection::reset(unsigned int timeoutSeconds, bool autocommit) {
  664     if (!_dbcHandle) {
  665         ThrowStdException("Connection handle not allocated");
  666     }
! 667     if (_cleanupState->childFreeFailed) {
  668         _poolClean = false;
  669         _poolSessionReset = false;
  670         LOG("Cannot reuse connection after a statement handle failed to free");
  671         return false;

Lines 708-719

  708         // form (UAF during silent reconnect).
  709         {
  710             // Release the GIL around the ODBC call for consistency with the
  711             // other connection-attribute paths; some attributes can block.
! 712             py::gil_scoped_release release;
! 713             ret = SQLSetConnectAttr_ptr(_dbcHandle->get(), SQL_ATTR_RESET_CONNECTION,
! 714                                         (SQLPOINTER)SQL_RESET_CONNECTION_YES, SQL_IS_INTEGER);
! 715         }
  716         if (!SQL_SUCCEEDED(ret)) {
  717             _poolClean = false;
  718             LOG("Failed to reset connection (ret=%d). Marking as dead.", ret);
  719             return false;

Lines 746-755

  746         LOG("Failed to restore pooled connection timeout (ret=%d). Marking as dead.", ret);
  747         return false;
  748     }
  749 
! 750     // A retained child can finish cleanup while either attribute call releases the GIL.
! 751     if (_cleanupState->childFreeFailed) {
  752         _poolClean = false;
  753         _poolSessionReset = false;
  754         LOG("Cannot reuse connection after a statement handle failed to free");
  755         return false;

Lines 758-766

  758     return true;
  759 }
  760 
  761 void Connection::prepareForPool() {
! 762     PERF_TIMER("Connection::prepareForPool");
  763     if (!_dbcHandle) {
  764         ThrowStdException("Connection handle not allocated");
  765     }
  766     if (_cleanupState->childFreeFailed) {

Lines 810-823

  810                                             reinterpretU16stringAsSqlWChar(rollbackQuery), SQL_NTS);
  811                     if (!SQL_SUCCEEDED(ret)) {
  812                         ErrorInfo error = SQLReadError(SQL_HANDLE_STMT, statement, ret);
  813                         statementError = error.sqlState.length() == 5
! 814                             ? "SQLSTATE:" + error.sqlState + ":" + error.ddbcErrorMsg
! 815                             : error.ddbcErrorMsg;
! 816                     }
! 817                     SQLRETURN freeRet = SQLFreeHandle_ptr(SQL_HANDLE_STMT, statement);
! 818                     if (SQL_SUCCEEDED(ret) && !SQL_SUCCEEDED(freeRet)) {
! 819                         ErrorInfo error = SQLReadError(SQL_HANDLE_STMT, statement, freeRet);
  820                         statementError = error.sqlState.length() == 5
  821                             ? "SQLSTATE:" + error.sqlState + ":" + error.ddbcErrorMsg
  822                             : error.ddbcErrorMsg;
  823                         ret = freeRet;

Lines 839-848

  839     }
  840     if (!statementError.empty()) {
  841         ThrowStdException(statementError);
  842     }
! 843     checkError(ret);
! 844     // A weak_ptr expires before its destructor finishes. Read the count before
  845     // the failure latch so a completed destructor cannot hide a failed native free.
  846     const bool noChildren = _cleanupState->childStatementCount == 0;
  847     if (_cleanupState->childFreeFailed) {
  848         ThrowStdException("Cannot pool connection after a statement handle failed to free");

Lines 987-995

  987         // SQLDisconnect failure using the existing connection and children.
  988         if (!_usePool && !rollbackBeforeDisconnect) {
  989             throw;
  990         }
! 991         // Never retain a connection whose transaction state could not be
  992         // sanitized. Release capacity and preserve the original cleanup error.
  993         try {
  994             ConnectionPoolManager::getInstance().discardConnection(_originPool, _conn);
  995         } catch (...) {

Lines 1044-1055

  1044     return conn->allocStatementHandle();
  1045 }
  1046 
  1047 py::object Connection::getInfo(SQLUSMALLINT infoType) const {
! 1048     _poolClean = false;
  1049     _poolSessionReset = false;
! 1050     if (infoType == SQL_DRIVER_HDBC || infoType == SQL_DRIVER_HENV ||
! 1051         infoType == SQL_DRIVER_HSTMT || infoType == SQL_DRIVER_HLIB) {
  1052         _poolProofDisabled = true;
  1053     }
  1054     if (!_dbcHandle) {
  1055         ThrowStdException("Connection handle not allocated");

mssql_python/pybind/ddbc_bindings.cpp

Lines 1589-1599

  1589 
  1590 // SqlHandle definition
  1591 SqlHandle::SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle,
  1592                      std::shared_ptr<ConnectionCleanupState> cleanupState)
! 1593     : _type(type), _handle(rawHandle), _cleanupState(std::move(cleanupState)) {
! 1594     if (_type == SQL_HANDLE_STMT && _cleanupState) {
! 1595         ++_cleanupState->childStatementCount;
  1596     }
  1597 }
  1598 
  1599 SqlHandle::~SqlHandle() {

Lines 1674-1682

  1674         if (_implicitly_freed || (_cleanupState && _cleanupState->disconnected)) {
  1675             _handle = nullptr;
  1676             return SQL_SUCCESS;
  1677         }
! 1678         SQLRETURN ret = SQLFreeHandle_ptr ? SQLFreeHandle_ptr(_type, _handle) : SQL_INVALID_HANDLE;
  1679         if (SQL_SUCCEEDED(ret)) {
  1680             _handle = nullptr;
  1681         } else if (_type == SQL_HANDLE_STMT && _cleanupState) {
  1682             _cleanupState->childFreeFailed = true;


📋 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.connection.connection_pool.cpp: 81.3%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.fetch_temporal.hpp: 92.1%
mssql_python.cursor.py: 92.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Replace the PR 777 revert with conservative clean-state tracking and consolidated native sanitation. Used or uncertain connections still roll back before parking; only previously sanitized unused connections can skip cleanup. Preserve scalar login timeouts without trusting arbitrary attributes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 25, 2026 19:11
@sumitmsft Sumit Sarabhai (sumitmsft) changed the title PERF: Revert pooled connection cleanup from PR #777 PERF: Optimize pooled connection return while preserving transaction safety Sep 25, 2026
Comment thread tests/native/pool_sanitation.cpp Fixed
Co-authored-by: Copilot <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

🟡 Changes recommended

Native tests are not part of standard validation, and exception tests no longer cover the failure paths their names describe.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
Resolved since last review (5)

Comment thread mssql_python/pybind/CMakeLists.txt Resolved
Comment thread tests/test_006_exceptions.py Resolved
Copilot AI review requested due to automatic review settings September 25, 2026 19: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

Enable the native sanitation regression suite in normal CI.

Review effort: Lite
Findings: 2 Medium severity

Open (2)

Replace redundant autocommit transitions with a reset-triggered rollback batch and preserve proof so checkout can skip duplicate session reset work.

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

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

Address the manual-lease state bug, restore Python-boundary failure coverage, and run native sanitation tests in CI.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
Previously missed (1)

In code that hasn't changed since last review

Low severity Autocommit read failure coverage was removed

tests/​test_006_exceptions.py:304

This replacement likewise removes the simulated get_autocommit failure, so the test named test_close_cleans_up_after_autocommit_read_failure no longer covers that error path. Because the probe now runs inside native close, add a Python-boundary test that can inject/observe a native probe failure, or rename this test so the suite does not falsely claim coverage it no longer provides.

Preserve native sanitation proof and original diagnostic enumeration on uncertain header results. Add deterministic ODBC-call and diagnostic-preservation regression coverage.

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

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 raw-handle cleanup and validation/test consistency issues must be addressed.

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

Open (3)

Teach the mixed-diagnostic interceptor the SQL_DIAG_NUMBER header contract while retaining SQLSTATE and message assertions. Run the existing native sanitation CTest target as a blocking job in standard PR validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 9, 2026 10:37

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

Moderate cleanup ownership and session-reset state issues remain unresolved.

3 open findings

🧠 Review effort: Lite

@bewithgaurav
Gaurav Sharma (bewithgaurav) dismissed stale reviews from gargsaumya and themself via 3840ee7 November 2, 2026 09:39
Copilot AI lite review requested due to automatic review settings November 2, 2026 09:39

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 changes span transaction cleanup, handle lifetime, and cross-platform native-test wiring, while live SQL Server/driver failure paths and full platform validation remain unverified and require human or CI confirmation.

No open issues 🎉

1 resolved since last review

Review effort: Lite

gargsaumya
gargsaumya previously approved these changes Nov 2, 2026
Apply the requested autocommit mode during every dirty pooled checkout so attribute failures stay bounded and trigger replacement recovery. Align the Python boundary regression with the merged native attrs contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bewithgaurav
Gaurav Sharma (bewithgaurav) dismissed stale reviews from gargsaumya and themself via 613b1e8 November 2, 2026 10:16
Copilot AI lite review requested due to automatic review settings November 2, 2026 10:17
gargsaumya
gargsaumya previously approved these changes Nov 2, 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.

🔵 Needs a closer look

Human adjudication is warranted because the high-risk native ODBC pooling and transaction-lifetime changes have only deterministic mock coverage and a Windows build, while live SQL behavior and Linux/macOS validation remain unverified.

No open issues 🎉

Review effort: Lite

Retain the clean-session sanitation fast path while forcing the requested autocommit attribute under the temporary connection timeout. This preserves stale-socket detection and replacement recovery without restoring rollback/reset work.

Co-authored-by: Copilot <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.

🔵 Needs a closer look

Native pooling/ODBC lifecycle and transaction-cleanup changes affect rollback, handle destruction, and GIL-less teardown, but live SQL and Windows/macOS validation remain unavailable despite deterministic native and targeted Python coverage.

No open issues 🎉

Review effort: Lite

Use the bounded transaction-isolation setter as the checkout round-trip for proven-clean sessions. This preserves the rollback/reset fast path while detecting unresponsive sockets before reuse.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bring PR #875 dependency changes into the pooling branch before its final validation run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings November 2, 2026 11: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.

🔵 Needs a closer look

Native pool sanitation spans high-risk transaction cleanup, handle lifetimes, discard/retry paths, and cross-platform ODBC behavior, while live SQL integration cases and the new Linux CI job were not run for validation.

No open issues 🎉

Review effort: Lite

@bewithgaurav
Gaurav Sharma (bewithgaurav) merged commit 7465c93 into main Nov 2, 2026
38 checks passed
Gaurav Sharma (bewithgaurav) added a commit that referenced this pull request Nov 2, 2026
>
[AB#49814](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/49814)

### Summary

Prepares mssql-python 1.17.0 for release and includes the pooling
optimization merged in #820.

#### Enhancements

- Reduce pooled connection return work while preserving transaction,
cleanup, capacity, and native handle-lifetime protections.

#### Bug Fixes

- Restore bulk copy after upgrades from 1.15.0.
- Bound pooled connection checkout on unresponsive sockets.
- Refresh `rowcount` when moving between results with `nextset()`.
- Make the PEP 249 `Timestamp()` microsecond argument optional.
- Update to `mssql-python-rs 0.4.0` for UTF-8 bulk-copy encoding,
identifier validation, and seven-digit datetime precision.

#### Version

- Bump `mssql_python.__version__` and `setup.py` from 1.16.0 to 1.17.0.
- Replace the PyPI 1.16.0 highlights with the 1.17.0 release inventory.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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