Skip to content

ODBC connection pooling (ADO #47317) - #308

Draft
Saurabh Singh (saurabh500) wants to merge 12 commits into
mainfrom
saurabh500-odbc-connection-pooling-combined
Draft

ODBC connection pooling (ADO #47317)#308
Saurabh Singh (saurabh500) wants to merge 12 commits into
mainfrom
saurabh500-odbc-connection-pooling-combined

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Implements Connection Pool constructs in mssql-odbc for ADO User Story #47317, so mssql-python's client-side connection pool can safely reuse a physical Rust ODBC connection across borrowers.

This is the single, consolidated PR for the feature (the earlier stacked stage PRs #293/#294/#296/#301/#302 have been closed in favor of this one). All four stages' work is included here against main.

What this delivers

  • mssql-tds reset correctness (AB#47337). All ResetConnection ENVCHANGE sites route through TdsClient::on_reset_connection_ack(), which resets the session-state table, clears session-bound caches (prepared handles, param-encryption, pending captures) via clear_session_bound_caches(), and restores negotiated DATABASE/LANGUAGE/COLLATION to login values. Fatal error tokens (severity >= 20) mark the transport dead via mark_known_dead() so a poisoned connection is discarded by the pool.
  • ODBC pooling primitives (AB#47338). SQL_ATTR_CONNECTION_DEAD (cached, never-probe liveness read, DEAD-until-proven) and the pooling reset attribute (value validation → HY024; 08003 when disconnected; busy rejection via claim-idle; rollback of a live local txn; arm the reset bit). The reset is accepted under both spellings — the ODBC 3.8 SQL_ATTR_RESET_CONNECTION (116) and the msodbcsql vendor attribute SQL_COPT_SS_RESET_CONNECTION (1246) — so both the direct-loading mssql-python consumer and Driver-Manager-mediated callers can request a check-in reset (see below).
  • Self-acking reset at checkout (AB#47339). TdsClient::reset_connection() arms RESETCONNECTION and drives a round trip that forces the ENVCHANGE ack (clearing session-bound caches); on failure the client is poisoned and 08S01 is surfaced so mssql-python discards it. The SQL_ATTR_TXN_ISOLATION handler emits a real SET TRANSACTION ISOLATION LEVEL batch, because sp_reset_connection does not reset isolation (D9) — matching mssql-python PR #343, which re-applies READ COMMITTED on every checkout.
  • Auth-reuse confirmation, tests, and docs (AB#47340). Confirmed a reset reuses the same physical login (no re-auth; a rotated token post-connect is rejected), with a unit test. C++ live e2e connection_pool_test.cpp covers same-SPID reuse, clean state for the next borrower, per-checkout isolation reset, connection-liveness, and prepared-statement behavior across reset. Shared behavior is verified on both drivers; the one intended mssql-odbc-specific divergence (transparent re-prepare of a dropped handle, which msodbcsql fails with native 8179) is isolated behind SKIP_IF_COMPARING_MSODBCSQL().

Design doc

  • docs/odbc-connection-pooling-plan.md — full design: consumer analysis, discoveries D1–D10, workstreams, DoD, and non-goals.
  • docs/odbc-connection-pooling-python-e2e.md — mssql-python pool integration verification plan (scenarios a–f) via the existing run_e2e.ps1/run_e2e.sh driver-registration hook.

Work items

Fixes AB#47317. Implements AB#47337, AB#47338, AB#47339, AB#47340.

Why the reset attribute is accepted under two identifiers (D10)

SQL_ATTR_RESET_CONNECTION (116) is an ODBC 3.8 attribute reserved for Driver Manager → driver communication: the DM sets it itself when returning a connection to its own pool, and per the 3.8 driver guidance "an application cannot set this attribute directly." The Windows DM enforces that — a DM-mediated application that sets it gets HY092 ("Option type out of range") from the DM before the call reaches any driver. This is not driver-specific: the installed msodbcsql18 fails identically. Such callers use msodbcsql's vendor attribute SQL_COPT_SS_RESET_CONNECTION (SQL_COPT_SS_BASE_EX+6 = 1246, value SQL_RESET_YES, odbcss.h) instead, since vendor-range attributes are passed straight through. unixODBC applies no such gate, so 116 reaches the driver on Linux/macOS.

mssql-python — the consumer this feature exists for — is not subject to that gate, because it does not use a Driver Manager. It loads the driver library directly (LoadDriverLibrary() in mssql_python/pybind/ddbc_bindings.cpp: LoadLibraryW on Windows, dlopen elsewhere), binds the exports via GetProcAddress/dlsym (SQLSetConnectAttr_ptr et al.), and calls them itself. Its SQL_ATTR_RESET_CONNECTION = 116 therefore arrives at our exported SQLSetConnectAttrW unchanged on every platform, Windows included — no upstream change is required.

So the driver handles both identifiers on every platform: 116 for the direct-loading consumer, and 1246 for DM-mediated callers on Windows — which includes this repo's own C++ e2e suite, since it links against the Driver Manager. connection_pool_test.cpp selects the spelling its transport requires and passes against both mssql-odbc and msodbcsql.

An earlier commit in this PR attempted to fix the Windows e2e HY092 by advertising DriverODBCVer = "03.80" in the test registry registration. That was the wrong diagnosis — the DM's rejection is about who may set attribute 116, not about the driver's advertised ODBC version — and it did not fix the failure. That change has been reverted; no DriverODBCVer change is needed, and this imposes no new requirement on a real install of the driver.

Notes

Draft. Merge is author-owned.

Saurabh Singh (SQL Drivers) and others added 7 commits August 14, 2026 14:53
Design plan for enabling connection pool constructs in mssql-odbc so
mssql-python's client-side pool can safely reuse a physical Rust ODBC
connection. Covers reset/liveness/isolation semantics, msodbcsql
cross-reference, and a 4-stage implementation breakdown.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Route every ResetConnection ENVCHANGE through a single
on_reset_connection_ack that resets session state, clears session-bound
prepared caches (shared with the reconnect path), and restores the
negotiated database/language/collation to their login values.

Add TdsTransport::mark_known_dead and flag the transport dead when a
server error token has severity/class >= 20, so a pool checkout discards
a fatally errored connection even on a still-open socket.

Add unit tests for the reset transition and the fatal-error path, plus a
live test proving a prepared handle is invalidated across a reset.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Add SQL_ATTR_CONNECTION_DEAD and SQL_ATTR_RESET_CONNECTION support for
connection pooling (Stage 2, ADO #47338).

- B1: add SQL_ATTR_CONNECTION_DEAD/SQL_CD_TRUE/SQL_CD_FALSE and
  SQL_ATTR_RESET_CONNECTION/SQL_RESET_CONNECTION_YES constants.
- B2: SQLGetConnectAttr(SQL_ATTR_CONNECTION_DEAD) returns a cached,
  never-probe liveness read; disconnected/never-connected reads DEAD.
- B3: SQLSetConnectAttr(SQL_ATTR_RESET_CONNECTION) validates the value
  (HY024 otherwise), rolls back a live local transaction, arms the full
  RESETCONNECTION bit via prepare_reset_connection(false), and clears
  local_tran_started; disconnected surfaces 08003, busy is rejected.
- Unit tests cover both attributes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Drive the RESETCONNECTION round trip eagerly inside
SQLSetConnectAttr(SQL_ATTR_RESET_CONNECTION) so the reset is processed
and acknowledged before pool checkout returns, rather than riding a later
request that may short-circuit. A failed reset poisons the client and
surfaces 08S01 so mssql-python discards the connection.

Add TdsClient::reset_connection() (arms the bit and forces the ack round
trip) and mark_connection_dead(); wire the ODBC handler to them, map
failures to 08S01, and document the D9 raw-T-SQL isolation caveat at the
same-value short-circuit. Cover the eager ack, poison, reset+isolation
re-apply, and full checkout-cycle lifecycle with unit/mock tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
…2e plan

Confirm SQLSetConnectAttr(SQL_ATTR_RESET_CONNECTION) and
TdsClient::reset_connection() preserve the auth/recovery context (same
physical login, no re-auth), documented on reset_connection and locked in
by a test rejecting a post-connect access token (B6).

Fill remaining ODBC unit gaps: CONNECTION_DEAD reports dead for a
client marked known-dead. Add a live C++ e2e (connection_pool_test.cpp)
mirroring transaction_test.cpp: same-physical-connection reuse, clean
state for the next borrower, isolation reset each checkout, prepared
statement survival, and value validation.

Document the mssql-python end-to-end verification plan
(odbc-connection-pooling-python-e2e.md) since no in-repo Python pool
harness exists; reference it from the main plan.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
The reprepare-after-reset test asserts mssql-odbc-specific behavior: our
driver transparently re-prepares after sp_reset_connection drops the
server-side handle, while msodbcsql blindly reuses the dropped handle and
fails (native 8179). Add SKIP_IF_COMPARING_MSODBCSQL() so the parity
comparison leg skips this intended divergence, mirroring transaction_test.cpp.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Add PreparedStatementUsableAcrossReset, which runs on both drivers and
verifies the shared contract: a prepared statement works before the reset
and a freshly prepared statement works after it, so the connection stays
usable across a pool reset on both mssql-odbc and msodbcsql.

Narrow PreparedStatementSurvivesResetViaReprepare to just the mssql-odbc
divergence: re-executing the same already-prepared handle after the reset
must transparently re-prepare (msodbcsql fails native 8179). Keep
SKIP_IF_COMPARING_MSODBCSQL() only on this test so the shared behavior is
still compared on both parity legs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Saurabh Singh (SQL Drivers) and others added 2 commits August 15, 2026 19:22
…n Windows

The Windows ODBC Driver Manager decides whether to forward ODBC 3.8-only
connection attributes (SQL_ATTR_RESET_CONNECTION) based on the driver's
registry DriverODBCVer, read at load time, not the runtime
SQLGetInfo(SQL_DRIVER_ODBC_VER). Without it the DM rejects the attribute
with HY092 before reaching the driver, failing every connection_pool_test
reset case on the Windows e2e legs. Register DriverODBCVer=03.80 (mirroring
msodbcsql's sqlnclidrv.rgs) with matching snapshot/restore so cleanup
unwinds it. Linux/unixODBC has no equivalent gate, so run_e2e.sh is
unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63e24621-7665-4572-a5b8-e5dfa55060a0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

97%

🎯 Overall Coverage

91.6%

📦 Project: mssql-tds + mssql-odbc + mssql-py-core
ℹ️ Note: diff coverage is reported, not enforced.


Diff Coverage

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

  • mssql-odbc/src/api/get_connect_attr.rs (100%)
  • mssql-odbc/src/api/set_connect_attr.rs (100%)
  • mssql-odbc/src/api/txn.rs (94.7%): Missing lines 382-383,414-415,419-421,439-440
  • mssql-tds/src/connection/tds_client.rs (98.5%): Missing lines 1321,4064
  • mssql-tds/src/connection/transport/tds_transport.rs (0.0%): Missing lines 93
  • mssql-tds/src/test_client_support.rs (100%)

Summary

  • Total: 417 lines
  • Missing: 12 lines
  • Coverage: 97%

mssql-odbc/src/api/txn.rs

  378     const OP: &str = "SQLSetConnectAttrW(SQL_ATTR_RESET_CONNECTION)";
  379 
  380     {
  381         let Ok(mut state) = dbc.inner.lock() else {
! 382             error!("{OP}: dbc mutex poisoned");
! 383             return SQL_ERROR;
  384         };
  385         free_errors(&mut state);
  386         if value != u64::from(SQL_RESET_CONNECTION_YES) {
  387             error!(value, "{OP}: invalid value");

  410             // A poisoned mutex leaves the transaction flag unreadable, so a
  411             // required rollback could be skipped. Fail deterministically like
  412             // every other DBC lock site rather than resetting on a DBC whose
  413             // state is already compromised.
! 414             error!("{OP}: dbc mutex poisoned");
! 415             return SQL_ERROR;
  416         }
  417     };
  418     let result = if started && client.has_active_transaction() {
! 419         debug!("{OP}: rolling back live local transaction before reset");
! 420         dbc.runtime
! 421             .block_on(client.rollback_transaction(None, None))
  422     } else {
  423         Ok(())
  424     };

  435     }
  436     release_dbc_client(dbc, client);
  437 
  438     let Ok(mut state) = dbc.inner.lock() else {
! 439         error!("{OP}: dbc mutex poisoned");
! 440         return SQL_ERROR;
  441     };
  442     state.local_tran_started = false;
  443     if let Err(e) = result {
  444         error!(%e, "{OP}: connection reset failed");

mssql-tds/src/connection/tds_client.rs

  1317                 }
  1318                 Tokens::EnvChange(env_change) => {
  1319                     info!(?env_change);
  1320                     if env_change.sub_type == EnvChangeTokenSubType::ResetConnection {
! 1321                         self.on_reset_connection_ack();
  1322                     }
  1323                     self.execution_context
  1324                         .capture_change_property(&env_change, &mut self.negotiated_settings)?;
  1325                     continue;

  4060             }
  4061             Tokens::EnvChange(env_change) => {
  4062                 info!(?env_change);
  4063                 if env_change.sub_type == EnvChangeTokenSubType::ResetConnection {
! 4064                     self.on_reset_connection_ack();
  4065                 }
  4066                 self.execution_context
  4067                     .capture_change_property(&env_change, &mut self.negotiated_settings)?;
  4068                 Ok(None)

mssql-tds/src/connection/transport/tds_transport.rs

  89     /// has not seen a socket-level failure, so a later pool checkout must
  90     /// discard the connection.
  91     ///
  92     /// [`connection_known_dead`]: TdsTransport::connection_known_dead
! 93     fn mark_known_dead(&mut self) {}
  94 }


🔗 Quick Links

View Azure DevOps Build · Coverage Report

Saurabh Singh (SQL Drivers) and others added 2 commits August 16, 2026 10:11
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as ready for review August 16, 2026 17:43
@saurabh500
Saurabh Singh (saurabh500) requested a review from a team as a code owner August 16, 2026 17:43
Copilot AI balanced review requested due to automatic review settings August 16, 2026 17: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.

Pull request overview

Adds TDS and ODBC primitives required for safe physical-connection reuse by mssql-python’s client-side pool.

Changes:

  • Centralizes TDS reset acknowledgement and fatal-error liveness handling.
  • Adds ODBC reset, connection-dead, and isolation behavior.
  • Adds unit, live E2E, and integration-verification coverage.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
mssql-tds/tests/test_reset_connection.rs Tests prepared-handle invalidation.
mssql-tds/src/test_client_support.rs Adds reset and liveness test support.
mssql-tds/src/connection/transport/tds_transport.rs Adds transport death marking.
mssql-tds/src/connection/transport/network_transport.rs Implements death marking.
mssql-tds/src/connection/tds_client.rs Handles reset acknowledgements and fatal errors.
mssql-tds/src/connection/execution_context.rs Delegates reset state handling.
mssql-odbc/tests/e2e/tests/connection_pool_test.cpp Adds live pooling tests.
mssql-odbc/tests/e2e/run_e2e.ps1 Removes formatting noise.
mssql-odbc/tests/e2e/CMakeLists.txt Registers pooling tests.
mssql-odbc/src/api/txn.rs Implements eager reset and isolation flow.
mssql-odbc/src/api/sqlstate.rs Adds communication-failure SQLSTATE.
mssql-odbc/src/api/set_connect_attr.rs Routes reset attributes.
mssql-odbc/src/api/odbc_types.rs Defines pooling constants.
mssql-odbc/src/api/get_connect_attr.rs Reports cached connection liveness.
docs/odbc-connection-pooling-python-e2e.md Documents Python integration verification.
docs/odbc-connection-pooling-plan.md Describes pooling design and delivery plan.

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

Comment thread mssql-odbc/src/api/get_connect_attr.rs Outdated
Comment thread mssql-odbc/src/api/txn.rs
Comment thread mssql-odbc/tests/e2e/tests/connection_pool_test.cpp Outdated
Comment thread docs/odbc-connection-pooling-python-e2e.md Outdated
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as draft August 16, 2026 17:50
auto-merge was automatically disabled August 16, 2026 17:50

Pull request was converted to draft

…rigor

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants