Skip to content

FIX: Context manager commits on clean exit, rolls back on exception#639

Open
bewithgaurav wants to merge 2 commits into
mainfrom
bewithgaurav/fix-635-context-manager-commit
Open

FIX: Context manager commits on clean exit, rolls back on exception#639
bewithgaurav wants to merge 2 commits into
mainfrom
bewithgaurav/fix-635-context-manager-commit

Conversation

@bewithgaurav

@bewithgaurav bewithgaurav commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Work Item / Issue Reference

AB#45829

GitHub Issue: #635


Summary

Fixes the Connection context manager (__exit__) to implement commit-on-success / rollback-on-exception semantics, matching the documented behavior in the wiki, the DevBlogs post, and the docstrings.

Previously __exit__ only called close(), which always rolled back uncommitted changes regardless of whether the block exited cleanly or via exception. Now:

  • Clean exit + autocommit=False: commits the transaction
  • Exception + autocommit=False: rolls back the transaction
  • autocommit=True: no explicit commit/rollback (same as before)
  • Connection always closed on exit

close() is unchanged. Its internal rollback becomes a harmless no-op on an already-resolved transaction.

24 subprocess-isolated tests cover: commit/rollback semantics, autocommit toggling, manual commit/rollback inside block, DDL+DML, pending cursor results, doomed transactions (XACT_ABORT), KeyboardInterrupt, generator abandonment, killed SPID, 10k-row transactions, double-exit idempotency, and GC safety. Resource-intensive tests (rapid connection cycling, 10k inserts) are marked @pytest.mark.stress.

@github-actions github-actions Bot added the pr-size: large Substantial code update label Jun 22, 2026
Comment thread tests/test_024_context_manager_transaction.py Fixed
@bewithgaurav bewithgaurav force-pushed the bewithgaurav/fix-635-context-manager-commit branch from 7a7d0d5 to 9489153 Compare June 22, 2026 07:25
return self

def __exit__(self, *args: Any) -> None:
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why *argsexc_type, exc_val, exc_tb:

the old __exit__ didn't inspect its arguments at all (just called close()), so *args was fine. now we branch on exc_type to decide commit vs rollback, so we use the canonical __exit__(self, exc_type, exc_val, exc_tb) signature from the Python data model.

this is what sqlite3, psycopg2, and every other DB-API driver uses. no behavioral change from the rename itself - just makes the branching logic readable.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

35%


🎯 Overall Coverage

80%


📈 Total Lines Covered: 6662 out of 8256
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/connection.py (35.0%): Missing lines 1718,1724-1732,1735-1737

Summary

  • Total: 20 lines
  • Missing: 13 lines
  • Coverage: 35%

mssql_python/connection.py

Lines 1714-1722

  1714         (rollback or close) are suppressed so the original user exception
  1715         propagates unchanged.
  1716         """
  1717         if self._closed:
! 1718             return
  1719         try:
  1720             if not self.autocommit:
  1721                 if exc_type is None:
  1722                     self.commit()

Lines 1720-1741

  1720             if not self.autocommit:
  1721                 if exc_type is None:
  1722                     self.commit()
  1723                 else:
! 1724                     self.rollback()
! 1725         except Exception:
! 1726             try:
! 1727                 self.close()
! 1728             except Exception:
! 1729                 pass
! 1730             if exc_type is None:
! 1731                 raise
! 1732             return
  1733         try:
  1734             self.close()
! 1735         except Exception:
! 1736             if exc_type is None:
! 1737                 raise
  1738 
  1739     def __del__(self) -> None:
  1740         """
  1741         Destructor to ensure the connection is closed when the connection object


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.connection.connection.cpp: 76.2%
mssql_python.pybind.ddbc_bindings.cpp: 76.2%
mssql_python.row.py: 76.9%
mssql_python.__init__.py: 77.3%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.connection.py: 83.4%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Fixes #635. The __exit__ implementation previously only called close(),
which always rolled back uncommitted changes. This contradicted the
documented behavior in the wiki, the DevBlogs post, and the docstrings.

The fix adds commit-on-success / rollback-on-exception semantics to
__exit__, matching the standard pattern used by sqlite3, psycopg2, and
other Python DB-API drivers. close() remains unchanged; its internal
rollback is a harmless no-op on an already-resolved transaction.

24 subprocess-isolated tests cover: clean commit, exception rollback,
autocommit modes, manual commit/rollback inside block, DDL+DML,
pending results, doomed transactions, KeyboardInterrupt, generator
abandonment, killed SPID, large transactions, and double-exit
idempotency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bewithgaurav bewithgaurav force-pushed the bewithgaurav/fix-635-context-manager-commit branch from 9489153 to b3c915a Compare June 22, 2026 07:41
@bewithgaurav bewithgaurav marked this pull request as ready for review June 22, 2026 08:33
Copilot AI review requested due to automatic review settings June 22, 2026 08:33

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

This pull request updates the Connection context manager behavior to match documented transaction semantics: commit on clean exit and rollback on exception when autocommit=False, while always closing the connection on exit.

Changes:

  • Implemented commit-on-success / rollback-on-exception logic in Connection.__exit__.
  • Added a new subprocess-isolated test suite to validate context manager transaction behavior and crash-safety edge cases.
  • Marked resource-intensive context-manager tests as @pytest.mark.stress (excluded by default via pytest.ini).

Reviewed changes

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

File Description
mssql_python/connection.py Updates Connection.__exit__ to commit/rollback based on exit condition and autocommit, then close the connection.
tests/test_024_context_manager_transaction.py Adds subprocess-based tests covering commit/rollback semantics, exception propagation, and various hardening scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mssql_python/connection.py Outdated
Comment thread tests/test_024_context_manager_transaction.py Outdated
Comment thread tests/test_024_context_manager_transaction.py Outdated
Comment on lines +822 to +852
result = _run_script("""
import os
from mssql_python import connect

cs = os.environ["DB_CONNECTION_STRING"]

setup = connect(cs)
setup.cursor().execute("IF OBJECT_ID('pytest_ctx_large') IS NOT NULL DROP TABLE pytest_ctx_large")
setup.cursor().execute("CREATE TABLE pytest_ctx_large (id INT)")
setup.commit()
setup.close()

with connect(cs) as conn:
cur = conn.cursor()
for i in range(10000):
cur.execute(f"INSERT INTO pytest_ctx_large VALUES ({i})")

verify = connect(cs)
cur = verify.cursor()
cur.execute("SELECT COUNT(*) FROM pytest_ctx_large")
count = cur.fetchone()[0]
assert count == 10000, f"FAIL: expected 10000 rows, got {count}"
verify.close()

cleanup = connect(cs)
cleanup.cursor().execute("DROP TABLE pytest_ctx_large")
cleanup.commit()
cleanup.close()
print("PASS")
""")
_assert_no_crash(result, "test_large_transaction_commit")
…eoutcomments

- Wrap final self.close() in try/except so cleanup failures don't mask
  the user's original exception on the exception path
- Remove hardcoded fallback connection string from tests (tests now
  properly skip when DB_CONNECTION_STRING is unset)
- Replace signal.Signals._value2member_map_ (private API) with
  try/except ValueError for stable cross-version behavior
- Bump subprocess timeout to 120s for 10k-insert stress test

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.

3 participants