Skip to content

Event contract corpus; fix the pause crash, #480 and #481 (v6 groundwork) - #482

Merged
TimelordUK merged 4 commits into
masterfrom
test/event-contract-corpus
Oct 17, 2026
Merged

TimelordUK merged 4 commits into
masterfrom
test/event-contract-corpus

Conversation

@TimelordUK

@TimelordUK TimelordUK commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Groundwork for v6. It is not for release yet. The issues are referenced but not closed: they should close when a release carries the fix.

What's here

An event contract corpus (test/contract/): 77 scenarios whose exact event sequence (submitted, meta, row, info, error, done, free, callbacks, cancel callbacks) is pinned in corpus/ as reviewable transcripts. It covers #478 – #481. The findings and the plan are in test/contract/HANDOVER.md.

Fixed: pause/resume crash (0xC0000005), #480, and the endless rowcount 0 loop. One cause: a pause on the last row of a batch was ignored, and resume() then took the same step again. Two nextResultSet calls were in flight on one statement (crash), or a result set was skipped (#480). A query now has one next step at a time: running, or parked by a pause.

Fixed: #481, the work queue. WorkQueue.finish(op) replaces the blind nextOp(): each owner finishes its own item, once, by identity. A statement's unbind and free run straight after their query. Each query keeps its own queue item: a prepared statement shares one notify across its executions.

A paused query holds the connection. Work queued before the pause waits its turn. Work submitted while a query is paused is refused: HY010, then done and free (no submitted). Transaction calls and prepare get the error in their callback. A newly submitted query no longer frees the paused one. Cancelling a paused query ends it with HY008 → free, and close() cancels a paused query first. This also fixes a 5.6.0 bug: a query queued before another one paused was silently dropped, never submitted, and its callback never called.

Behaviour changes (v6)

  • Nothing is emitted while paused, done included.
  • A query submitted while another is paused is refused with HY010, instead of freeing the paused one. The same applies to transaction calls and prepare.
  • Cancelling a paused query raises error HY008, as any cancel does. Before, it was freed silently.
  • The relevant tests in pause.test.js and querycancel.test.js are rewritten to these rules.

Still to do (see HANDOVER.md)

Testing

  • Full mocha suite locally (Windows, ODBC 18, localdb): 900 passing.
  • pause/every-row: 23 identical runs, no crash. Before: about 1 run in 3 crashed.
  • First CI run: Windows and macOS green. Linux failed on 4 errors/* scenarios only. A batch whose first statement fails at execute loses the rest of the batch on Linux (more=false, no done). That is reproduced on WSL Fedora, and it happens with the base commit's lib/ too, so it predates this PR: 5.6.0's fix: prints, errors and results after a failed statement in a batch were lost #475 look-ahead does not hold on Linux. It is pinned as .linux.txt transcripts (# status: known-bug) so CI stays green, and it needs its own fix.
  • 905 passing locally after the refusal change.

🤖 Generated with Claude Code

TimelordUK and others added 4 commits October 15, 2026 18:25
Adds test/contract: scenarios whose exact sequence of events (submitted, meta,
row, column, info, error, done, free, callbacks, cancel callbacks) is pinned in
corpus/ as reviewable transcripts, with capture.js to record and compare them
(including against another build) and contract.test.js to enforce them. Each
run is in its own process, so hangs, runaways and crashes stay contained.

The corpus currently pins what 5.6.0 does, bugs included (status: unreviewed),
covering #478 - #481 plus a crash found on pause/resume every row. The issue
repros are in test/contract/repro and the findings in test/contract/HANDOVER.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… once

A pause on the last row of a batch was ignored: dispatch() went on to the
next result set (or ended the query) while paused, and resume() then did the
same step again from the stale batch. Two nextResultSet calls in flight on one
statement crashed the process (0xC0000005, pause/every-row about 1 run in 3).
Otherwise the second call skipped a result set (#480), or after the last row
done/free came twice followed by an endless rowcount 0 loop.

A query now has one next step at a time: running, or parked by a pause.
resume() runs the parked step and never starts another. Nothing, done
included, is emitted while paused.

The three pause transcripts are now agreed; the pause test that expected
done while still paused now resumes first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry holds the connection (#481)

The queue advanced by removing whatever was at its head, so a query that
failed advanced once too often: work queued behind it was removed just after
it started, two statements overlapped ("Connection is busy with results for
another command"), and a later query could no longer be cancelled.

- WorkQueue.finish(op) removes that item only, by identity, starts the next
  only if it was the head, and is a no-op the second time. nextOp() is gone.
- Each query keeps its own queue item. A prepared statement shares one notify
  across its executions, so the notify's operation was only the latest one.
- A statement's unbind and free run straight after their query, before work
  already waiting (enqueueFollowUp), so 'free' is no longer delayed behind it.
- close() always finishes its item: the cancelled branch of moveToNextResult
  cleared 'running' first, which is what the extra advance in end() covered.
- dropItem matches by identity, not priority.

A paused query now holds the connection. It keeps its place at the head,
rather than moving to the back and removing the item waiting behind it without
running it (that query was never submitted, and its callback never fired).
A query submitted while another is paused waits for it, rather than freeing
it. Cancelling a paused query ends it as any cancel does (HY008, then free),
rather than freeing it silently, and close() cancels a paused query first.

Corpus: the queue and paused-cancel transcripts are re-recorded and now
deterministic; queue/ok-then-two-on-* are agreed. pause.test.js and
querycancel.test.js are changed to the new rules.

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

After a query fails, the connection's work queue advances once too often, so later queries can overlap (5.1.0 – 5.6.0)

1 participant