Skip to content

test: hold the upgrade path to being repeatable - #823

Merged
blaipr merged 1 commit into
mainfrom
test/the-upgrade-path-cannot-regress
Aug 18, 2026
Merged

test: hold the upgrade path to being repeatable#823
blaipr merged 1 commit into
mainfrom
test/the-upgrade-path-cannot-regress

Conversation

@blaipr

@blaipr blaipr commented Aug 18, 2026

Copy link
Copy Markdown
Member

Two guards over the upgrade path, each holding a rule that #822 had just been broken by. Both fail today if the thing they describe is undone.

A migration that is refused part way must be runnable again

40024210101.sql gave CustomFieldData its natural primary key as two statements. DDL commits as it goes, so the drop stood alone when the key was refused on a duplicate, and every retry then died on Can't DROP COLUMN id before reaching the statement that had actually failed — with the database version unchanged, because UpgradeDatabase::apply() writes it only once every statement has succeeded.

Each version file must now be one of:

  • a single statement, which the server applies whole;
  • wholly inside one transaction, with no DDL in it — DDL would commit and end the transaction under the statements after it, so the wrapping only means something while the file is pure DML;
  • DDL written so that running it twice is harmless (IF NOT EXISTS, IF EXISTS).

Both current files satisfy it, one each way: 40024210101.sql is a single ALTER, and 40024240101.sql is 31 DML statements between start transaction and commit.

dbstructure.sql is excluded, with the reason: it builds a database rather than upgrading one, so nothing has been applied when it runs and a failure leaves nothing half-done.

Versions are applied in the order they are written

Upgrade::getTargetUpgradeHandlers() yields a handler's versions in the order getAttributes() returns them — source order. Nothing sorts them.

They ascend today, so UpgradeDatabase runs the schema change before the row migration that depends on it. A version added at the top of the attribute list would run first, and out-of-order migrations are not something an installation reports; they are something it survives or does not. A second case refuses a handler that claims the same version twice, which would apply it twice.

On the messages

They say what to do rather than only what is wrong, because whoever trips them is in the middle of adding a migration and has three ways to satisfy the rule. The DDL failure prints the offending statement.

Verification

Splitting the migration back into two statements fails the first guard; swapping the two UpgradeVersion attributes fails the second. 3973 unit tests pass.

This adds one unit-test file and touches no src and no schema, so the integration suite cannot be affected by it — CI covers it rather than a local run that could not have told us anything.

Two guards over the way #822 stopped being repeatable, both of which fail today
if the thing they describe is undone.

A migration that is refused part way has to leave the operator something to run
again. `40024210101.sql` gave CustomFieldData its natural primary key as two
statements; DDL commits as it goes, so the drop stood alone when the key was
refused on a duplicate, and every retry then died on `Can't DROP COLUMN id`
before reaching the statement that had failed. Each version file must now be a
single statement, or wholly inside one transaction with no DDL in it — DDL would
commit and end the transaction under the statements after it — or written so
that running it twice is harmless. Both files satisfy it, one each way.

`Upgrade::getTargetUpgradeHandlers()` yields a handler's versions in the order
`getAttributes()` returns them, which is the order they are written in the
class. Nothing sorts them. They ascend today, so the schema change runs before
the data migration that needs it; a version added at the top of the list would
run first. Out-of-order migrations are not something an installation reports.

The messages say what to do about it rather than only what is wrong, because
whoever sees them is adding a migration and has a choice of three ways to
satisfy the rule.

CLAUDE.md records the pattern these two guard, which covers half of the recent
fixes: a guard that is not where the change happens — a limit tested against a
row already read, a counter written back from a stale read, a hash stored
outside the transaction holding what it describes, two commits made out of one
logical change. Where the codebase gets it right the guard and the change are
one statement. The suite counts there are refreshed to match.
@blaipr
blaipr force-pushed the test/the-upgrade-path-cannot-regress branch from 1d240f3 to d2f05f5 Compare August 18, 2026 21:33
@blaipr
blaipr merged commit d9182c1 into main Aug 18, 2026
8 checks passed
@blaipr
blaipr deleted the test/the-upgrade-path-cannot-regress branch August 18, 2026 21:40
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