fix: detect server-aborted transactions to prevent silent auto-commit (XACT_ABORT)#370
Conversation
0cb82c2 to
76487e0
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #370 +/- ##
===========================================
+ Coverage 79.92% 96.59% +16.67%
===========================================
Files 34 91 +57
Lines 6656 74106 +67450
===========================================
+ Hits 5320 71586 +66266
- Misses 1065 2185 +1120
- Partials 271 335 +64
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
76487e0 to
a2d2e40
Compare
|
Does this bug exist in the message loop version of running a query (using the experimental interfaces and sqlexp.ReturnMessage) too? How do we know this fix doesn't change or break any users of sqlexp.ReturnMessage? |
a2d2e40 to
062d955
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses a correctness gap in the driver when SQL Server aborts a transaction server-side (notably with XACT_ABORT ON), ensuring errors aren’t silently swallowed and preventing subsequent statements from running outside the intended transaction.
Changes:
- Add
Conn.inTransactiontracking and a pre-query check to detect server-aborted transactions (sess.tranid == 0) and fail fast with an explicit error. - Update
Rows.Close()to capture errors surfaced via trailingdoneStructtokens during token draining. - Add a new integration regression test for the #244 scenario plus unit tests for
Rows.Close()drain behavior andcheckServerAbortedTransaction().
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
mssql.go |
Tracks “in transaction” state, detects server-aborted transactions before issuing queries, and changes Rows.Close() draining/error handling. |
xact_abort_test.go |
Integration regression test reproducing #244 with XACT_ABORT ON, validating errors surface and no partial commit occurs. |
xact_abort_unit_test.go |
Unit tests for Rows.Close() drain-time error capture and for checkServerAbortedTransaction() behavior/type. |
062d955 to
f008f08
Compare
Yes, the bug existed in the sqlexp path too. Both sqlexp users already see the original error via No breakage for sqlexp users. The guard fires in Test coverage:
|
0242063 to
d4dbb49
Compare
d4dbb49 to
57c9217
Compare
Problem
When
XACT_ABORT ONis set and a query causes a server-side error, SQL Server rolls back the transaction and sendsENVCHANGE(RollbackTran)which setssess.tranidto 0. Subsequent operations on the connection sendtranid=0in TDS headers, causing them to execute as auto-commit outside any transaction. This leads to silent data corruption where partial data is committed.PR #361 (already merged) fixed
Rows.Close()to surface the original server error from the failing query. This PR fixes the second, independent bug: the driver had no detection of server-side transaction rollback, so even when the error was surfaced, subsequent operations could still silently run as auto-commit if the caller ignored the error.Reproduction scenario (from #244):
SET XACT_ABORT ONviaSessionInitSQLINSERT INTO Orders(succeeds)SELECT ... WHERE code = @codewith integer parameter against VARCHAR column containing non-numeric valuesINSERT INTO Ordersruns as auto-commit (tranid=0), gets committedtx.Commit()fails, but the second INSERT is already committedRoot Cause
No detection of server-side transaction rollback. When SQL Server rolls back a transaction due to
XACT_ABORT, it sendsENVCHANGE(RollbackTran)that setssess.tranid = 0. The driver continued sending queries withtranid=0in the TDS headers, which the server interprets as auto-commit.Fix
Conn.inTransaction: New bool field tracking whetherBegin()was called andCommit()/Rollback()hasn't completed yet.checkServerAbortedTransaction(): Returnsmssql.ErrorwheninTransaction==truebutsess.tranid==0. The error type (notnet.Error/StreamError/ServerError) ensurescheckBadConnpasses it through without marking the connection bad or triggering retry.sendQuery()guard: Shared entry point for bothRowsandRowsqpaths, so both standard and sqlexp message-loop users are protected.Commit()guard: Returns clear error instead of sending commit withtranid=0.Rows.Close()hardening: First-error-wins semantics, document whydoneInProcStructis intentionally skipped, improve cancel-first comment.Changes
mssql.goinTransactionfield,checkServerAbortedTransaction(), guards insendQuery()andCommit(), lifecycle tracking inBegin/Commit/Rollback,Rows.Close()hardeningxact_abort_test.goTestXactAbortSurfacesError(standard path) andTestXactAbortWithReturnMessage(sqlexp message-loop path)xact_abort_unit_test.gocheckServerAbortedTransaction,Rows.Close()error handling,Commit()guardTesting
TestXactAbortSurfacesError: Reproduces exact Transaction does not return error on next statement after breaking. #244 scenario. VerifiesQueryRow().Scan()returns conversion error (8114), subsequentExecfails with guard error (number 0), 0 rows committed.TestXactAbortWithReturnMessage: Exercises sqlexp/Rowsq path. VerifiesMsgErrordelivers conversion error, subsequentExecContextfails with guard error, 0 rows committed.checkServerAbortedTransaction(not-in-txn, active, aborted, error type, Commit guard),Rows.Close()(no tokens, normal tokens, doneStruct error, first-error-wins, nextToken error priority, cancel called, doneInProc ignored, no-error-bit).TestMessageQueue,TestAdvanceResultSetAfterPartialRead,TestMessageQueueWithErrors,TestIgnoreEmptyResultsall pass.Fixes #244
Community contribution by @mgregur (issue report).