Skip to content

Two sections described routines nobody built, and Serializable lets an invariant through - #205

Merged
dmitrat merged 1 commit into
mainfrom
isolation-and-document-truth
Aug 15, 2026
Merged

Two sections described routines nobody built, and Serializable lets an invariant through#205
dmitrat merged 1 commit into
mainfrom
isolation-and-document-truth

Conversation

@dmitrat

@dmitrat dmitrat commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Items 6 and 7 of Docs/FIX-PLAN-FROM-SITE-FINDINGS-2026-08-15.md. No behaviour changes — what lands is what was measured, in the XML comments, in WitSQL.md, and in tests that fail if any of it drifts.

Isolation: two of the four outcomes contradicted what was written down

outcome
Serializable / RepeatableRead — read an empty range, another commits into it, then write and commit refused, serialization failure
the same, but the first transaction only reads commits cleanly, correctly — a read-only transaction can always be ordered first
Serializable and Snapshot — two transactions, same rows read, different rows written both commit; the invariant is gone
either level, both write the same row second is refused

Write skew is permitted at Serializable. The standard doctors example holds here exactly: each transaction checks that somebody else is on call, each takes a different doctor off, both commit, and the ward ends with nobody on call. Nothing detects it, because neither transaction touched what the other wrote. An application whose correctness rests on an invariant across rows has to enforce it itself.

And the comment that was simply wrong. WitIsolationLevel.RepeatableRead promised that read locks are held for the transaction's duration. Every level above ReadCommitted here is optimistic — the conflict is found at commit and raised as an exception. A caller who believed the comment wrote no retry at all, because a lock blocks and never fails.

WhatEachIsolationLevelPreventsTests pins all six. These pass by construction and that is not the point of them: each names one outcome with the reason in its message, so a change to the MVCC validation shows up here, named, rather than as a surprise in somebody's application. The write-skew cases are labelled as pinning permitted behaviour, so that a future change which starts refusing it is a visible decision. REPEATABLE READ turned out to refuse the phantom-then-write as well — not known before this run, now a parameterised arm.

WitSQL.md §§ 22–23 described a design that was considered and not taken

Both said "not implemented as of 2026-07-29" and described table-valued functions, IF/WHILE bodies, OUT parameters and multiple result sets. Routines shipped in 11.0.0 and are none of that:

  • a function body is one expression, which is exactly what makes calling one safe from a path evaluated once per row — no statements, no transaction, no nesting budget;
  • a procedure body is a sequence of statements, and the last statement's result is the call's result.

Replaced rather than edited, because nothing in them survived.

Sequences had no section at all

The ID-generation table documented INCREMENT and LASTINCREMENT without ever connecting them to CREATE SEQUENCE, and never mentioned NEXTVAL/CURRVAL — the names anyone arriving from PostgreSQL tries first. § 23.1 now states the whole surface, measured:

CREATE SEQUENCE [IF NOT EXISTS] name [START WITH n]
ALTER SEQUENCE name RESTART [WITH n]
DROP SEQUENCE [IF EXISTS] name

No INCREMENT BY — it is a syntax error — no MINVALUE, no MAXVALUE, no CYCLE, and the step is always 1.

Three entries in KnownIssues

Each is behaviour to build around rather than a defect to fix by surprise: write skew at every level; partial indexes the planner never selects, so they cost every write and are never read; and no hit-rate counter in either paged cache — a dashboard plotting one is plotting something that does not exist.

The rule underneath both sections

A note carrying a date rather than a version is a claim that stopped being checked the day it was written. Both of these were, for over a year.

Core 2371, Engine 2549.

🤖 Generated with Claude Code

…n invariant through

Items 6 and 7 of Docs/FIX-PLAN-FROM-SITE-FINDINGS-2026-08-15. No behaviour changes:
what lands is what was measured, in the XML comments, in WitSQL.md, and in tests that
fail if any of it drifts.

ISOLATION. Every level above ReadCommitted here is optimistic - the conflict is found at
commit and raised as an exception - and WitIsolationLevel.RepeatableRead promised the
opposite, that read locks are held for the transaction's duration. A caller who believed
that comment wrote no retry at all, because a lock blocks and never fails.

The four outcomes are measured rather than taken from the standard's table, and two of
them contradicted what was written down. Serializable DOES refuse a transaction that acted
on a range another transaction filled, and it refuses two writers on one row. It does NOT
prevent write skew: two transactions read the same rows, each writes a different one, both
commit, and an invariant that held for each separately is gone - the standard doctors
example holds here exactly, and the ward ends with nobody on call. Snapshot permits it
too, which is the ordinary caveat; Serializable permitting it is the one worth reading
before choosing a level.

WhatEachIsolationLevelPreventsTests pins all six outcomes. These pass by construction and
that is not the point of them: each names one outcome with the reason in its message, so a
change to the MVCC validation shows up here rather than in an application. The write-skew
cases are labelled as pinning permitted behaviour, not a defect, so that a future change
which starts refusing it is a visible decision. REPEATABLE READ turned out to refuse the
phantom-then-write too, which was not known before this run and is now a parameterised
arm.

ROUTINES. WitSQL.md sections 22 and 23 both said "not implemented as of 2026-07-29" and
described table-valued functions, IF/WHILE bodies, OUT parameters and multiple result
sets. Routines shipped in 11.0.0 and are none of that: a function body is ONE expression,
which is what makes calling one safe from a path evaluated per row; a procedure body is a
sequence of statements and the last statement's result is the call's result. Both sections
are replaced rather than edited, because nothing in them survived.

SEQUENCES had no section at all, and the ID-generation table documented INCREMENT and
LASTINCREMENT without ever connecting them to CREATE SEQUENCE - while NEXTVAL and CURRVAL,
which anyone arriving from PostgreSQL tries first, were not mentioned anywhere. Section
23.1 states the whole surface, measured: CREATE SEQUENCE [IF NOT EXISTS] name [START WITH
n], ALTER SEQUENCE name RESTART [WITH n], DROP SEQUENCE. There is no INCREMENT BY - it is
a syntax error - no MINVALUE, no MAXVALUE, no CYCLE, and the step is always 1.

KnownIssues gains three entries, each behaviour to build around rather than a defect to
fix by surprise: write skew at every level, partial indexes that the planner never selects
(so they cost writes and are never read), and the absence of any hit-rate counter in the
paged caches - a dashboard plotting one is plotting something that does not exist.

And the rule underneath both sections: a note carrying a DATE rather than a version is a
claim that stopped being checked the day it was written. Both of these were.

Core 2371, Engine 2549.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmitrat
dmitrat merged commit 5afd391 into main Aug 15, 2026
1 check passed
@dmitrat
dmitrat deleted the isolation-and-document-truth branch August 15, 2026 12:47
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