Add ODBC transaction support - #227
Conversation
Implement SQLEndTran, SQL_ATTR_AUTOCOMMIT and SQL_ATTR_TXN_ISOLATION for mssql-odbc, matching msodbcsql 18 semantics. - SQLEndTran on DBC and ENV handles, with worst-code-wins fan-out - Autocommit switching, including the implicit commit and 01000 record when returning to autocommit with open work - Isolation levels mapped to SET TRANSACTION ISOLATION LEVEL batches - Auto-begin before each statement while autocommit is off, so a server-side ROLLBACK or XACT_ABORT does not strand the connection - 25000 on disconnect with an open user transaction - Transaction capability values from SQLGetInfo - SQL_API_SQLENDTRAN and SQL_API_SQLGETCONNECTATTR advertised through SQLGetFunctions, without which the Driver Manager answers IM001 AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds ODBC transaction support to mssql-odbc, including transaction control, connection attributes, capability reporting, and lifecycle integration.
Changes:
- Implements
SQLEndTran, autocommit, and isolation-level handling. - Integrates transactions with execution, connection, cursor, and disconnect paths.
- Adds unit/E2E coverage and design documentation.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
mssql-odbc/src/api/disconnect.rs |
Adds transaction-safe disconnect behavior. |
mssql-odbc/src/api/driver_connect.rs |
Applies deferred transaction settings. |
mssql-odbc/src/api/end_tran.rs |
Implements SQLEndTran. |
mssql-odbc/src/api/exec_common.rs |
Adds shared transaction initialization. |
mssql-odbc/src/api/exec_direct.rs |
Starts transactions before direct execution. |
mssql-odbc/src/api/execute.rs |
Starts transactions before prepared execution. |
mssql-odbc/src/api/exports.rs |
Exports SQLEndTran. |
mssql-odbc/src/api/get_connect_attr.rs |
Returns transaction attributes. |
mssql-odbc/src/api/get_functions.rs |
Advertises transaction APIs. |
mssql-odbc/src/api/get_info.rs |
Reports transaction capabilities. |
mssql-odbc/src/api/get_type_info.rs |
Integrates catalog execution with transactions. |
mssql-odbc/src/api/mod.rs |
Registers transaction modules. |
mssql-odbc/src/api/odbc_types.rs |
Defines transaction constants. |
mssql-odbc/src/api/set_connect_attr.rs |
Handles transaction attributes. |
mssql-odbc/src/api/sqlstate.rs |
Adds transaction diagnostics. |
mssql-odbc/src/api/txn.rs |
Provides shared transaction state and operations. |
mssql-odbc/src/handles/dbc.rs |
Stores per-connection transaction state. |
mssql-odbc/docs/transactions_plan.md |
Documents design, scope, and tests. |
mssql-odbc/tests/e2e/CMakeLists.txt |
Registers transaction E2E tests. |
mssql-odbc/tests/e2e/tests/transaction_test.cpp |
Exercises transaction behavior against SQL Server. |
Suppressed comments (2)
mssql-odbc/docs/transactions_plan.md:264
- The test matrix says invalid handle type produces HY092, but
end_tran_bad_handle_type_returns_invalid_handleassertsSQL_INVALID_HANDLE. Keep this entry consistent with the implemented API contract.
| 10 | `SQLEndTran` invalid handle type → `HY092` | unit |
mssql-odbc/src/api/txn.rs:223
- If this lock is poisoned after the transaction begins, the function still returns success without setting
local_tran_started. The statement then executes, butSQLEndTrantreats it as having no user transaction and skips commit/rollback. Fail the execution when the state cannot be recorded.
if let Ok(mut state) = dbc.inner.lock() {
state.local_tran_started = true;
}
Ok(())
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Fail instead of silently degrading to autocommit when the DBC mutex is poisoned in begin_transaction_if_manual. - Surface post-connect isolation/autocommit failures as SQL_SUCCESS_WITH_INFO with a diagnostic instead of only logging them. - Propagate cursor-close failures out of close_all_cursors so a transaction request is never sent on a connection stuck mid-batch. - Align the SAFETY comments on cloned child-handle traversal with the existing handle-lifetime TODO in disconnect.rs. - Correct the plan doc: SQLEndTran with a bad handle type returns SQL_INVALID_HANDLE, not HY092. - Add stored-procedure rollback recovery coverage and Benefits-from-mock-tds annotations. AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-odbc/src/api/close_cursor.rsmssql-odbc/src/api/disconnect.rsmssql-odbc/src/api/driver_connect.rsmssql-odbc/src/api/end_tran.rsmssql-odbc/src/api/exec_direct.rsmssql-odbc/src/api/get_type_info.rsmssql-odbc/src/api/txn.rsmssql-odbc/src/handles/dbc.rs🔗 Quick Links |
Saurabh Singh (saurabh500)
left a comment
There was a problem hiding this comment.
Self-review pass focused on ODBC spec conformance and hot-path cost. Overall the design is sound — the lock discipline is genuinely careful (client is take()n out of the mutex rather than held across I/O, locks consistently released before block_on), and the local_tran_started vs TdsClient::has_active_transaction() split is the right call: it's what makes recovery from a server-side rollback work.
Two blocking issues, both verified against the spec and the code:
SQLEndTran(SQL_HANDLE_ENV)fails when any DBC on the environment is not connected. The spec says inactive connections must not affect the transaction. Currently one spare or already-disconnected DBC fails an environment-wide commit — and a unit test asserts the wrong behavior.- Connection-scoped transaction ops wipe the diagnostics of every statement on the connection.
close_all_cursorsroutes throughSQLFreeStmt(SQL_CLOSE), which callsfree_errorsbefore its no-cursor early return.SQLEndTran(SQL_ROLLBACK)after a failed statement erases the error the app was about to report.
Plus a return-code ranking bug that's latent but one line from being live, a capability we advertise but no app can reach (SQL_TXN_SS_SNAPSHOT), and several hot-path costs — the biggest being that close_all_cursors is O(N) full FFI entries per commit when the common case is zero open cursors.
Things I checked that are correct and easy to get wrong, so nobody re-litigates them:
- No lock held across network I/O anywhere in
txn.rs; no self-deadlock in the cursor sweep (statementsis cloned in alet, so the guard drops before re-locking). - Poison handling is consistent —
let Ok(..) = .. elsethroughout, nointo_inner()recovery, nounwrap()outside tests. - Auto-begin recovery is real:
has_active_transaction()is driven byENVCHANGEBegin/Commit/Rollback/Defect tokens, soXACT_ABORTand raw T-SQLROLLBACKgenuinely clear it. - Every SQL-executing entry point is covered by auto-begin (
SQLExecDirect,SQLExecute,SQLGetTypeInfo);SQLPrepareonly stages, andfree_handle's best-effortsp_unpreparecorrectly must not open one. - Suspended-state interaction is right —
local_tran_startedis cleared unconditionally so the mandatory follow-upSQLDisconnectisn't blocked by its own 25000, and 1205/1211 map to40001, which the DM treats as confirmed non-completion. SQL_CB_CLOSEsemantics hold (prepared handle survives viacapture_prepared_handle),HY012/SQL_INVALID_HANDLEvalidation matches the spec table, no-active-transaction returnsSQL_SUCCESS, and all the newSQLGetInfoconstants and return widths are correct.HYC00for0x10is right — that'sSQL_TXN_VERSIONING, a real ODBC value we don't implement, soHYC00beatsHY024.
Also worth flagging in the PR body before this leaves draft: SQLDisconnect now returns 25000 with an open transaction, and since auto-begin runs before every statement in manual-commit mode (including read-only SELECTs and SQLGetTypeInfo), a manual-commit app that only ever read data must now call SQLEndTran before disconnecting. Matches msodbcsql, but it's user-visible.
Fix two correctness bugs and make SNAPSHOT reachable. SQLEndTran on SQL_HANDLE_ENV now skips connections that are not connected, per "Connections that are not active do not affect the transaction". A single allocated-but-unconnected DBC previously posted 08003 and failed the whole environment-wide commit. Return-code promotion now ranks worst-wins instead of last-non-success-wins. Connection-scoped cursor sweeps no longer clear child statement diagnostics. SQLFreeStmt(SQL_CLOSE) calls free_errors before its no-cursor early return, so every commit, rollback, autocommit switch and isolation change wiped the diagnostics of every statement on the connection. Applications that rolled back after a failure could no longer read why the statement failed. Sweeps now route through close_cursor_for_connection_op, which leaves diagnostics alone and returns after one lock when no cursor is open. msodbcsql clears them; this is a documented, deliberate divergence (plan section 7.2). Implement SQL_COPT_SS_TXN_ISOLATION (1227). The driver advertised SQL_TXN_SS_SNAPSHOT in SQL_TXN_ISOLATION_OPTION, but no application could select it: the Driver Manager screens SQL_ATTR_TXN_ISOLATION down to the four standard bits before the driver is called. The vendor attribute passes through untouched and drives the same code path. Because SNAPSHOT is now reachable, set_txn_isolation handles the empty driver-begun transaction that switch_to_manual_commit leaves open: SQL Server rejects SET TRANSACTION ISOLATION LEVEL SNAPSHOT inside an active transaction, so it rolls the empty one back, applies the change, and reopens it. Also: short-circuit setting the isolation level already in effect, matching the same-value no-op autocommit uses; collapse begin_transaction_if_manual to one DBC lock in both steady states; map transaction-manager transport failures to 08007 rather than HY000; and document that the eager begin at the autocommit switch is msodbcsql parity, not an optimization. Validated: 533 unit tests, clippy and fmt clean, and 18/18 e2e on both mssql-odbc and msodbcsql 18 against a live server. AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c08c4ad-e404-402e-b1d0-7a7ec80755a3
Remove the deliberate divergence where connection-scoped cursor sweeps preserved child statement diagnostics. msodbcsql's CommitAbortTran calls SQLFreeStmt(lpstmt, SQL_CLOSE) on every statement it visits (sqlctran.cpp:302-323), and that entry point calls FreeErrors(lpstmt) before it inspects either the option or the cursor state (sqlccmd.cpp:379-380). Diagnostics are discarded on every child statement, including one that failed and so never opened a cursor. close_cursor_for_connection_op now clears diagnostics unconditionally, ahead of its cursor-open check. It stays a separate internal path only for cost: statements with no open cursor still return straight after the reset, so the per-statement FFI saving is unaffected. EndTranPreservesStatementDiagnostics becomes EndTranClearsStatementDiagnostics and drops SKIP_IF_COMPARING_MSODBCSQL, so it now runs on both driver legs. AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c08c4ad-e404-402e-b1d0-7a7ec80755a3
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Summary
Solid, well-researched implementation of ODBC transactions in mssql-odbc — SQLEndTran, SQL_ATTR_AUTOCOMMIT, SQL_ATTR_TXN_ISOLATION / SQL_COPT_SS_TXN_ISOLATION, the SQLGetInfo transaction types, and the SQLGetFunctions entries. The msodbcsql source citations throughout, the plan doc, and the 36-test C++ e2e suite that runs against both drivers are exactly the right way to build this. I verified locally: cargo clippy -p mssql-odbc --all-targets is clean and 514 lib unit tests pass.
One correctness issue I'd want resolved before merge (cursor sweep skipped in autocommit mode), plus a few smaller items. Details inline.
Two repo-level items that aren't attached to a diff line
Diff coverage. The repo targets 85% diff coverage in CI (.github/copilot-instructions.md), and the C++ e2e suite doesn't feed cargo-llvm-cov. Measured on this branch:
mssql-odbc/src/api/end_tran.rs 150 lines, 5 missed, 96.67%
mssql-odbc/src/api/txn.rs 380 lines, 254 missed, 33.16%
end_tran.rs is in great shape. txn.rs is the risk: everything that needs a TdsClient (end_transaction's network path, switch_to_autocommit / switch_to_manual_commit, the connected branch of set_txn_isolation, apply_post_connect_txn_settings) is only exercised by e2e. mssql-odbc doesn't currently depend on mssql-mock-tds, so the cheapest path is probably to lift the pure decision logic out — e.g. a function that maps (autocommit, local_tran_started, has_active_transaction, requested_mode) to an action enum — and unit-test that, leaving the thin I/O shells uncovered. Worth checking what the CI gate actually reports before merge.
CHANGELOG. No CHANGELOG.md entry. This PR adds a new exported entry point (SQLEndTran), three connection attributes, five SQLGetInfo types, and two SQLGetFunctions advertisements — the ## [Unreleased] / ### Added section already carries mssql-odbc: entries at this level of granularity (the INFO-message work, for instance).
Sweep cursors in SQLEndTran even when no transaction was started. A cursor opened in autocommit mode survives a switch to manual commit, which starts no transaction, so the next SQLEndTran found local_tran_started false with the row stream still open and returned without closing it. This driver advertises SQL_CB_CLOSE, so the Driver Manager acts on that return and the connection was left claimed, locking out every other statement. Also: - free stale errors when a post-connect settings claim fails, so a successful connect cannot leave a stray record behind - short-circuit an unchanged SQL_ATTR_TXN_ISOLATION before the HY011 open-transaction rejection, matching SQL_ATTR_AUTOCOMMIT - use the validated isolation level instead of unwrap_or and a raw cast - route a poisoned connection mutex in the SQLEndTran env fan-out to SQL_ERROR instead of skipping it, and post a summary HY000 on the ENV handle so SQLGetDiagRec is not SQL_NO_DATA after a failure - drop the ensure_transaction pass-through wrapper - correct doc comments that described behavior the code does not have AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c08c4ad-e404-402e-b1d0-7a7ec80755a3
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Re-review of 651c36e
All nine points from the previous round are addressed, and the fixes are the right ones. Verified locally on this head: cargo clippy -p mssql-odbc --all-targets clean, 516 lib unit tests pass (up from 514).
| Previous finding | Status |
|---|---|
Blocking — cursor sweep skipped when local_tran_started is false |
Fixed. close_all_cursors moved ahead of the early return, SQL_SUCCESS semantics preserved, new unit test end_tran_sweeps_cursors_even_with_no_transaction_started reproduces it correctly |
SQLEndTran(ENV) returned SQL_ERROR with no ENV diagnostic |
Fixed. HY000 summary with the failure count, detail left on each DBC, plus a negative test asserting no record on the all-success path |
| Poisoned DBC mutex silently skipped in the ENV fan-out | Fixed — now contributes SQL_ERROR and increments failed |
apply_post_connect_txn_settings returned SQL_SUCCESS over a stale error record |
Fixed |
ensure_transaction pass-through wrapper |
Removed, three call sites now use begin_transaction_if_manual directly |
value as u32 truncating cast / dead unwrap_or |
Fixed — (level, tsql) carried out of the locked block |
HY011 ordered before the same-value no-op |
Fixed, with a good comment on why the order matters |
switch_to_manual_commit doc drift |
Fixed |
Two items from the previous round are still open, and there's one new issue with the regression guard for the blocking fix. Details inline.
Still open
CHANGELOG. No CHANGELOG.md entry yet (the diff is 20 files, none of them the changelog). New exported entry point, three connection attributes, five SQLGetInfo types.
Diff coverage. txn.rs is at 36.07% line coverage (257/402 missed), up from 33.16%; end_tran.rs is at 95.00%. The two new unit tests helped, but the CI 85% diff-coverage gate is still the thing to watch — everything in txn.rs that needs a live TdsClient remains e2e-only.
The e2e test asserted the no-transaction-started sweep by switching to manual commit first, but switch_to_manual_commit sweeps cursors as its own first statement, so the cursor was already closed before SQLEndTran ran and the test passed with or without the sweep reordering. That combination turns out to be unreachable from the public ODBC surface: in manual-commit mode every cursor-opening entry point starts a transaction first, and in autocommit mode the Driver Manager answers SQLEndTran without calling the driver at all. Verified against a live server -- the plain-autocommit form fails with "Connection is busy with results for another command" because the driver is never invoked. The unit test that calls end_transaction directly remains the guard for that path. Replace the e2e test with the reachable half of the same contract: a successful SQLEndTran leaves the connection free for other statements. It now runs on both drivers instead of being skipped on msodbcsql. Also truncate the connect-path diagnostics back to their pre-claim length instead of clearing them, so a SQL_SUCCESS_WITH_INFO login keeps the server's INFO records, and drop two stale comment paragraphs describing the old unreachable justification. AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c08c4ad-e404-402e-b1d0-7a7ec80755a3
Two conflicts, both from main's prepared-execute rework (#148) landing alongside the transaction work: - `execute.rs`: main replaced the inline sp_execute / sp_prepexec branch with `client.execute_prepared`, which now owns reconnect and re-prepare. Took main's body and re-applied the `begin_transaction_if_manual` hook ahead of it. The new staging struct moves the prepared plan out of the statement before execution, so the transaction-begin failure path now writes it back before reporting, matching the failed-claim arm. Dropped the top-level `rewrite_param_markers` import, which main's body no longer uses; the test module imports it separately. - `tests/e2e/CMakeLists.txt`: both `transaction_test` and main's `session_recovery_test` registrations kept. Validated: clippy and fmt clean, 565/565 unit tests, e2e 19/19 on both `mssql-odbc` and `ODBC Driver 18 for SQL Server`. AB#46379 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c08c4ad-e404-402e-b1d0-7a7ec80755a3
Description
Implements ODBC transaction support (spec §4.9) in
mssql-odbc: manual-commitby default,
SQLEndTrancommit/rollback, isolation levels including SQL ServerSNAPSHOT, implicit rollback on close, and the four transactionSQLGetInfocapabilities.
The design, the full goal/non-goal list and the divergences from msodbcsql are
in
mssql-odbc/docs/transactions_plan.md,added by this PR.
Goals implemented
SQL_AUTOCOMMIT_OFF), with the implicit transaction opened lazily on first executeSQLEndTran(SQL_HANDLE_DBC, …)commit and rollback, plus theSQL_HANDLE_ENVfan-outSQL_ATTR_TXN_ISOLATIONfor the four standard levels, andSQL_COPT_SS_TXN_ISOLATIONforSQL_TXN_SS_SNAPSHOTSQLDisconnectrolls back an open transaction instead of stranding itSQL_TXN_CAPABLE,SQL_DEFAULT_TXN_ISOLATION,SQL_CURSOR_COMMIT_BEHAVIOR,SQL_MULTIPLE_ACTIVE_TXNSQL_ATTR_AUTOCOMMITis settable before connect and applied at connect time, andSQLGetConnectAttranswers from cached state rather than issuing a liveSQLGetConnectAttron every read. This was caught by the e2esuite: the round trip is observable and msodbcsql does not make it.
Non-goals
Savepoints, explicit
BEGIN TRANSACTION, DTC/XA enlistment, MARS-scopedtransactions,
SQL_ATTR_ENLIST_IN_DTC, cursor-preserving commit behaviour andthe remaining msodbcsql-only capabilities are listed as N1–N12 in the plan, each
with a note on why it is out of scope for this PR. The skipped items are filed as
child work items under AB#46379 (47263–47269).
Related Issues
AB#46379
Testing
535 Rust unit tests pass (
cargo nextest run -p mssql-odbc), coveringargument validation, state transitions and diagnostics without a server.
37 C++ e2e tests in
tests/e2e/tests/transaction_test.cpp, run against alive SQL Server. The full 18-binary suite was run twice locally — once against
the Rust driver and once against
ODBC Driver 18 for SQL Server— and passes18/18 on both, with only the 2 intentionally-guarded divergences skipped on
the reference leg.
Two behaviours turned out to be owned by the Driver Manager, not by either
driver, and the tests assert the observable result instead (§7.1 of the plan):
SQL_ATTR_TXN_ISOLATIONand answersHY024for anythingoutside the four standard levels — including
SQL_TXN_SS_SNAPSHOT, andincluding on msodbcsql. The driver's own
HYC00is asserted by unit tests,which have no DM in the path.
SQL_CB_PRESERVE, so after a commit the DM rejects thenext statement with
24000until the cursor is closed explicitly. mssql-odbcanswers truthfully, so the statement is immediately reusable. The shared part
is asserted on both drivers; the divergence lives in its own
SKIP_IF_COMPARING_MSODBCSQL()test.Review round 1 (Copilot)
SQLEndTranno longer reports success when the connection is not connectedSQL_ATTR_AUTOCOMMITset before connect is honoured at connect timeSQLDisconnectno longer strands the connectionSQLGetInfotransaction capabilities moved out of the catch-all default armlocal_tran_startedafter a server-side abort no longer wedges the connectionSQLGetConnectAttrReview round 2 (10 comments)
SQLEndTransweeps open cursors before committing, soSQL_CB_CLOSEis honouredSQL_COPT_SS_TXN_ISOLATIONaccepts and reportsSQL_TXN_SS_SNAPSHOTSetCommitModeOptionSQLEndTranonSQL_HANDLE_ENVsurfaces a failing connection's error instead of swallowing itSQLGetInfostring capabilities return the documented types0100036 e2e tests now (4 added), 533 unit tests,
ctest18/18 on bothdrivers.
Statement diagnostics: divergence removed, now at parity
An earlier revision deliberately preserved statement diagnostics across
SQLEndTran, on the reading that ODBC clears diagnostics only on the handle thefunction was called on. msodbcsql does not behave that way, and the divergence
has been removed.
CommitAbortTransweeps the connection's statements throughSQLFreeStmt(SQL_CLOSE)(sqlctran.cpp:302-323), and that entry point callsFreeErrors(lpstmt)before it inspects either the option or the cursor state(
sqlccmd.cpp:379-380). Every child statement therefore loses its records,including one that failed and never opened a cursor.
close_all_cursorsnow clears statement diagnostics unconditionally, ahead ofits cursor-open check, so the e2e assertion runs against both drivers with no
SKIP_IF_COMPARING_MSODBCSQL(), and the unit test is renamed toclosing_cursors_clears_statement_diagnostics.Validated: 533/533 unit tests, e2e 36/36 on
mssql-odbcand 34 passed + 2skipped on
msodbcsql.Review round 3 (9 comments, David Engel (@David-Engel))
apply_post_connect_txn_settingsfrees stale errors when the claim fails, so a successful connect cannot leave a stray recordSQL_ATTR_TXN_ISOLATIONshort-circuits an unchanged value before theHY011open-transaction rejection, matchingSQL_ATTR_AUTOCOMMITset_txn_isolationuses the validated level instead of anunwrap_orfallbackvalueSQLEndTranenv fan-out routes a poisoned connection mutex toSQL_ERRORrather than skipping it as "not connected"HY000on the ENV handle, soSQLGetDiagRec(SQL_HANDLE_ENV, …)is notSQL_NO_DATAafterSQL_ERRORensure_transactionpass-through wrapper removed; call sites usebegin_transaction_if_manualdirectlyReview round 4 (4 comments, David Engel (@David-Engel))
The blocking comment was correct and caught a test that guarded nothing:
EndTranWithNoTransactionStartedStillClosesCursorsswitched to manual commitfirst, but
switch_to_manual_commitcallsclose_all_cursorsas its own firststatement (
txn.rs:432), so the cursor was already closed beforeSQLEndTranran. It passed with or without the sweep reordering.
The suggested replacement — the same test in plain autocommit mode — does not
work either, which took a live run to establish. It fails with
[HY000] Connection is busy with results for another command, raised by thisdriver on the following statement, meaning the sweep never ran: while autocommit
is on the Driver Manager answers
SQLEndTranitself and never calls the driver.So the no-transaction-started-with-an-open-cursor combination is not reachable
through the public ODBC surface at all. In manual commit, all three callers of
begin_transaction_if_manual(SQLExecute,SQLExecDirect,SQLGetTypeInfo)start a transaction before opening a cursor, so
local_tran_startedis neverfalse with a row stream open; in autocommit the DM swallows the call. That path
stays guarded by
txn::tests::end_tran_sweeps_cursors_even_with_no_transaction_started, whichcalls
end_transactiondirectly.The e2e test is retargeted at the reachable half of the same contract — a
successful
SQLEndTranleaves the connection free for other statements — asEndTranClosesCursorsAndFreesTheConnection. It does not guard the reordering(the pre-fix code swept on the
started == truepath too); it guards the sweepcontract, and it now runs on both drivers, removing one
SKIP_IF_COMPARING_MSODBCSQL().SQL_SUCCESS_WITH_INFOlogin keeps the server's INFO recordsValidated: 535/535 unit tests, e2e 37/37 on
mssql-odbcand 35 passed + 2skipped on
msodbcsql.Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses —mssql-odbcis green (535/535); themssql-tdsintegration tests fail on this machine for environment reasons only (no
.env, TLSSEC_E_WRONG_PRINCIPAL). This PR touches no file outsidemssql-odbc, so CI is the real signal here.