Skip to content

fix: the prepared statement lifecycle - free once, refused after - #492

Merged
TimelordUK merged 3 commits into
masterfrom
fix/prepared-lifecycle
Oct 18, 2026
Merged

TimelordUK merged 3 commits into
masterfrom
fix/prepared-lifecycle

Conversation

@TimelordUK

Copy link
Copy Markdown
Owner

Contract:

  • Each execution of a prepared statement is submitted … done, with no free.
  • free is raised once, by ps.free(), when the native statement is released.
  • After that, any use of the statement is an error.

What was wrong, recorded in the new scripts/prepared (agreed):

  • After a failed execution (divide by zero), the next execution failed with 24000 "Invalid cursor state" and then reported the previous error again. Stale errors kept being reported even after free.
    • Native fix (bind_fetch): each execution clears the last one's diagnostics and closes any cursor left open.
  • An execution after free() ran natively on the released handle. It got two callbacks: the garbled "prepared statement has been Debugd", then "Invalid statement handle".
    • Now refused with HY010, "prepared statement N has been freed": the error or callback, then done, with no native call.
  • A second free() threw "Invalid statement handle", and the connection could no longer close.
    • Now refused in its callback.
  • free() while an execution runs waits for it: the execution's done, then free.

Corpus tooling: new script helpers t.watch(label, q), which logs a query object made some other way, and t.next(label, event). Prepared executions share one query object and never free, so t.query does not fit them.

Found, not fixed (in HANDOVER): a prepared select with no table behind it (select 10 / ? as n) is described with no columns, so its executions return no rows.

Seen once, not reproduced: cancel/again-after-confirm-at-100 failed once in about 50 local runs, and the transcript was not kept. If CI shows it, the failure prints the variant.

Local (Windows): full suite 1075 passing, replay 150 passing.

🤖 Generated with Claude Code

TimelordUK and others added 3 commits October 18, 2026 15:31
Each execution of a prepared statement is submitted ... done, with no free;
free is raised once, by ps.free(), when the native statement is released.
After that, any use is an error.

- native (bind_fetch): each execution clears the last one's diagnostics and
  closes the cursor it left open. after an execution failed part way, the
  next failed with 24000 and reported the old error again
- an execution after free() is refused with HY010 (the error or callback,
  then done), with no native call; it ran on the released handle before, with
  two callbacks and a garbled message
- a second free() is refused in its callback; it threw and left the
  connection unable to close
- free() asked for while an execution runs waits for its done

Corpus: scripts/prepared (agreed), with t.watch / t.next script helpers.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
… diagnostics

A current state at the top (the four issues, what guards them, the corpus
status, what is left before v6), the contract as it now stands in full, and a
Next plan led by never dropping what the driver sends (a new info/ corpus
group first). The older sections are marked as history.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The cancel callback and the HY008 from the fetch in flight are two native
completions on different threads; CI saw the other order. Both meet the
contract, so the transcript lists both (--- or).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@TimelordUK
TimelordUK merged commit 30fce77 into master Oct 18, 2026
11 checks passed
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