Skip to content

fix: apply session-level SET XACT_ABORT ON to all connections - #30

Closed
axellpadilla wants to merge 1 commit into
release/v1.12from
fix/xact-abort-session-default
Closed

fix: apply session-level SET XACT_ABORT ON to all connections#30
axellpadilla wants to merge 1 commit into
release/v1.12from
fix/xact-abort-session-default

Conversation

@axellpadilla

@axellpadilla axellpadilla commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Closes dbt-msft#718.

table_dml_refresh and other multi-statement paths (index reconcile, prebuilt full-refresh) send DML as one autocommit batch (BEGIN; DELETE; INSERT; COMMIT). A statement-abort error (NOT NULL, constraint violation, etc.) only aborts that statement, not the batch — execution falls through to COMMIT, silently persisting the DELETE on an empty target even though dbt reports failure. This also happens with no explicit transaction at all, so the fix can't depend on dbt_sqlserver_use_dbt_transactions.

Fix: SET XACT_ABORT ON as a session-level default on every connection, so a run-time error kills the whole batch and rolls back instead of falling through to COMMIT.

Changes

  • xact_abort: bool = True credentials field (in _connection_keys(), shows in dbt debug). Independent of dbt_sqlserver_use_dbt_transactions — that flag decides who owns the transaction boundary; xact_abort decides how the server reacts to a mid-batch error, which matters most exactly when there's no explicit transaction.
  • open() applies it via new _apply_session_settings(); a connection that fails this setup is closed rather than handed back to dbt.
  • Once-per-process warning when disabled, noting the current dbt_sqlserver_use_dbt_transactions state.
  • Removed the now-redundant local SET XACT_ABORT ON from indexes.sql and create.sql — session-level is the single source of truth. Comment added above table_dml_refresh pointing at Evaluate SET XACT_ABORT ON for SQL Server DML refresh transactions dbt-msft/dbt-sqlserver#718.
  • README + CHANGELOG (labeled as a behavior change, default-on in an already-published 1.11.x line).

Testing

New functional suite (test_xact_abort.py) against a real SQL Server:

  • Seed 2 rows via DML refresh, second run yields NULL → run fails and the 2 rows survive (the row-count assertion is the point; pre-fix, the run also fails but the table is empty).
  • xact_abort=True × dbt_sqlserver_use_dbt_transactions on/off: rows preserved both ways.
  • xact_abort=False + transactions on: preserved via dbt's own rollback, not XACT_ABORT.
  • xact_abort=False + transactions off (both disabled): not asserted as passing — manually confirmed this still loses rows, which is the documented, intentional limit of disabling the flag, not a regression.
  • @@OPTIONS & 16384 bit check: 1 by default, 0 when disabled.

Full suite: pytest tests/unit tests/functional -n auto600 passed, 48 skipped, 2 xfailed, 0 failed (298s).

Also fixed along the way: a pre-existing test-isolation bug where unit tests stub a process-global driver-module cache (_RUNTIME_STATE) without resetting it, leaking a fake module into real functional tests when both run in one pytest process. Added an autouse reset fixture. Also added -n auto to the integration-tests CI workflow (verified safe locally).

Summary by Sourcery

Enable session-level XACT_ABORT on all SQL Server connections by default to prevent partial commits on multi-statement batches, with an opt-out profile flag and corresponding safeguards.

New Features:

  • Add xact_abort boolean credential field, exposed in connection keys and profiles to control session-level XACT_ABORT behavior.

Bug Fixes:

  • Fix silent data loss on DML table refresh and other multi-statement batches by ensuring run-time errors abort and roll back the entire batch instead of committing partial results.
  • Ensure connections that fail session initialization (e.g. XACT_ABORT setup) are closed and marked failed rather than being returned for use.
  • Fix a test isolation issue by resetting the process-global SQL Server runtime module cache between unit tests to avoid leaking stubbed drivers into functional tests.

Enhancements:

  • Apply session-level XACT_ABORT settings during connection open, with a once-per-process warning when the protection is disabled and messaging that reflects the dbt_sqlserver_use_dbt_transactions flag state.
  • Clarify macros and comments for table DML refresh, table create, and index reconciliation to rely on the centralized session-level XACT_ABORT default rather than per-macro settings.
  • Add functional tests to validate XACT_ABORT behavior across transaction configurations and to assert the session option bit is set or unset according to the profile.

CI:

  • Run SQL Server integration/functional tests in parallel via pytest -n auto to reduce CI runtime.

Documentation:

  • Document the new xact_abort profile/connection option in the README, including its default-on behavior, interaction with dbt_sqlserver_use_dbt_transactions, and guidance for intentional opt-out.
  • Record the XACT_ABORT behavior change and opt-out mechanism in the changelog under bugfixes.

Without it, a run-time error partway through a multi-statement batch
(e.g. the DML table refresh's DELETE+INSERT swap) only aborts the
failing statement, not the batch, so a trailing COMMIT can still
persist a partial result -- most notably an emptied target after the
DELETE succeeds and the INSERT fails on a constraint violation. This
is silent, committed data loss and is independent of whether dbt owns
the transaction boundary (dbt_sqlserver_use_dbt_transactions).

- Add xact_abort credentials field (default true), applied once per
  connection in SQLServerConnectionManager.open() via a new
  _apply_session_settings(); a connection that fails this setup is
  closed rather than handed back to dbt as usable.
- Warn once per process (not per connection) when xact_abort is
  disabled, noting the current dbt_sqlserver_use_dbt_transactions
  state.
- Remove the now-redundant local `SET XACT_ABORT ON` from the index
  reconcile batch and the prebuilt full-refresh insert -- session-level
  is now the single source of truth.
- Fix a test-isolation bug found while validating this change: unit
  tests stub the process-global pyodbc/mssql-python module cache via
  configure_runtime_state_for_test() but never reset it afterward, so
  whichever test runs last leaks a fake driver module into every test
  that follows in the same process -- including real functional tests
  run in the same `pytest` invocation. Added an autouse fixture to
  reset it before/after each test.
- Run functional tests in parallel (-n auto) in CI, matching unit
  tests.

Closes dbt-msft#718.
@sourcery-ai

sourcery-ai Bot commented Jul 26, 2026

Copy link
Copy Markdown

🧙 Sourcery is reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@axellpadilla

Copy link
Copy Markdown
Owner Author

Superseded by dbt-msft#765, targeting upstream master directly instead of a fork-local release branch.

@axellpadilla
axellpadilla deleted the fix/xact-abort-session-default branch July 26, 2026 23:29
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.

1 participant