Skip to content

fix: prints, errors and results after a failed statement in a batch were lost - #475

Merged
TimelordUK merged 2 commits into
masterfrom
fix/messages-after-error
Oct 11, 2026
Merged

TimelordUK merged 2 commits into
masterfrom
fix/messages-after-error

Conversation

@TimelordUK

Copy link
Copy Markdown
Owner

Problem

A statement failing part way through a batch (raiserror severity 11–16, a failed insert, a failing step in a proc) doesn't end the batch: SQL Server runs the statements after it. The driver stopped at the first error, so:

  • later prints, errors and result sets were dropped
  • proc output params and the return code never arrived
  • done never fired
print 'p1'
raiserror('e1', 16, 1)
print 'p2'
raiserror('e2', 16, 1)
print 'p3'
select 1 as n

Before, the driver delivered info p1 and error e1, then stopped. Now it delivers info p1, error e1, info p2, error e2, info p3, meta, row and done, in both polling and non-polling mode.

Fix

C++ (odbc_statement_legacy.cpp)

  • After a statement error, the driver looks ahead with SQLMoreResults.
    • SQL_NO_DATA means the batch really ended with the error, so behaviour is unchanged: one callback with more = false.
    • Otherwise, the result it moved to is handed to the next try_read_next_result.
  • A cancel, a timeout (HYT00), a broken connection (08xxx) and statement state errors (HY010, HY007, 24000) still end the query.
  • The SQLMoreResults error path no longer reads that result's columns, which is what fixed ODBC errors not bubbling up when PRINT + THROW are used. #396. It now just moves on.
  • Bound params are now kept from the moment they are bound. Proc output params are read at the end of the batch, and before this change they were lost if the first statement had failed.

JS

  • reader.js: an error from the next result is reported, and the query carries on when more results follow. The more flag is true until the batch ends, for callbacks and for error events.
  • procedure-bound.js: the proc callback now passes more along with an error.
  • query-aggregator.js: the rejected error (the last one) always carries err._results: every error, info message, result set and output param.

Behaviour change (worth a changelog line)

  • Code that treats the first error as "the query is over" will now see more events and callbacks after it when the batch carries on. When the batch does end with the error, there is still exactly one callback with more = false.
  • done is still not raised for a batch that ends on an error, the same as before. That's left for a separate change.

Tests

New test/batch-errors.test.js (11 tests):

  • events in polling and non-polling mode
  • promise: rejects with the last error, which carries all the results
  • callback: the more sequence
  • result sets either side of an error
  • a failed insert part way through
  • a proc that carries on after a raiserror and still returns its output and return code
  • batches that should still stop: throw, a conversion error, several compile errors, and a query timeout

Local (Windows, ODBC 18, SQL Server 2022): full suite 821 passing, 1 pending. Lint is clean.

🤖 Generated with Claude Code

TimelordUK and others added 2 commits October 11, 2026 13:15
…ere lost

A statement failing part way through a batch (raiserror severity 11-16, a failed
insert, ...) does not end the batch: SQL Server runs the statements after it. The
driver stopped at the first error, so every later print, error, result set and
proc output param was dropped, and 'done' never fired.

- C++: after a statement error, look ahead with SQLMoreResults. SQL_NO_DATA means
  the batch really ended with the error (one callback, more = false, as before).
  Otherwise the result it moved to is handed to the next try_read_next_result.
  Cancel, timeout, broken connection and statement state errors still end it.
- Bound params are kept from bind time, so proc output params and the return
  code arrive even when an earlier statement in the proc failed.
- JS: an error from the next result is reported and the query carries on when
  more results follow. Callback / event 'more' is true until the batch ends; the
  proc callback now passes 'more' with an error. A promise rejects with the last
  error, which now always carries the collected results in err._results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…BC first-result case

- reader.js: info messages in the same diagnostics as an error that ends the batch
  were dropped (e.g. a print just before a throw inside a proc with nocount on).
  They are now routed as info as well as the errors being returned.
- batch-errors tests: the proc test now produces a result before its raiserror,
  so it covers continuing after an error on every platform. When the first result
  of a batch is itself the error, SQLExecDirect fails and the unixODBC driver
  manager (linux / macOS) answers SQLMoreResults with SQL_NO_DATA without asking
  the driver, so the batch ends there; on windows it carries on. A new test pins
  each platform's behaviour, and another checks the print-with-final-error case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@TimelordUK

Copy link
Copy Markdown
Owner Author

Linux CI found a platform difference, which I've now reproduced locally on WSL Ubuntu 22.04 with ODBC Driver 18.

When the first result of a batch is itself the error, SQLExecDirect returns SQL_ERROR. The unixODBC driver manager then answers the next SQLMoreResults with SQL_NO_DATA, apparently without passing the call to the driver. My best explanation is its statement state machine, but I haven't confirmed that in the unixODBC source. So on Linux and macOS the batch ends there; on Windows it carries on. Errors after the first result (anything with a select, print or rowcount before it) continue on every platform.

This matters for procedures with set nocount on: everything before the first select arrives together, so a proc that fails before producing any result stops at the error on Linux.

Follow-up commit 217a7f3:

  • Prints that arrive in the same diagnostics as a batch's final error were dropped, on every platform. They are now delivered.
  • The proc test now produces a result before its raiserror. A new test records each platform's behaviour for the first-result case, and another covers the print-with-final-error case.

Local results:

  • Windows: 823 passing.
  • WSL Linux: 781 passing. The 42 failures are all Always Encrypted tests, because my local database's keys live in the Windows certificate store. CI's fresh server doesn't have this problem.

@TimelordUK
TimelordUK merged commit 6216808 into master Oct 11, 2026
11 checks passed
TimelordUK added a commit that referenced this pull request Oct 17, 2026
A paused query holds the connection. Rather than queue new work behind it
indefinitely, a query submitted while another is paused is refused at once:
its callback (or 'error') gets an error with sqlstate HY010 (function sequence
error), then 'done' and 'free', and no 'submitted'. beginTransaction, commit,
rollback and prepare get the error in their callback. Work queued before the
pause still waits its turn.

Once a cancel of a paused query is accepted, the query no longer holds the
connection, so work submitted from the cancel callback queues behind it.

Corpus: queue/paused-then-{submit,cancel-queued-before,resume-queued-before};
the submit and resume cases are agreed.

The Linux CI runs showed 4 error scenarios that differ from Windows: a batch
whose first statement fails at execute loses the rest of the batch on Linux
(more=false, no done). The base commit does the same, so it is older than
this branch (5.6.0's #475 look-ahead). Pinned as .linux.txt transcripts with
status known-bug, captured on WSL Fedora, and described in HANDOVER.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.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.

ODBC errors not bubbling up when PRINT + THROW are used.

1 participant