Skip to content

Docs: Run step-by-step migrations in transactions - #3876

Merged
simolus3 merged 1 commit into
simolus3:developfrom
efim0v:fix/3725-migration-step-transactions
Oct 14, 2026
Merged

simolus3 merged 1 commit into
simolus3:developfrom
efim0v:fix/3725-migration-step-transactions

Conversation

@efim0v

@efim0v efim0v commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Closes #3725

In #3725, an app was terminated during a step-by-step migration after a step had already renamed a column. The stored schema version was still the old one, so the step ran again on the next start and renameColumn failed. As discussed in the issue, the way to avoid this is to run migration steps in transactions, which the step-by-step migrations page didn't cover yet.

Changes:

  • The runMigrationSteps example on migrations/step_by_step.md now runs each step in database.transaction, using the wrapper suggested in the issue.
  • A new "Transactions" section explains why migrations don't use a transaction by default, what happens to an interrupted step without one, and that the next attempt starts with the step that didn't complete because runMigrationSteps stores the schema version after each step. It mentions wrapping the whole runMigrationSteps call in one transaction as the alternative, and that PRAGMA foreign_keys has to be changed outside the transaction, which also affects alterTable.
  • docs/lib/src/generated_snippets.dart is regenerated with build_runner. Only the step_by_step.dart entry changed; the same build on develop leaves the file untouched.

Testing, in docs/:

  • dart format -o none --set-exit-if-changed ., dart analyze --fatal-infos --fatal-warnings, dart run drift_dev analyze and dart test pass.
  • dart run tool/build_search_index.dart renders all pages, and the rendered step-by-step page contains the new section and the updated snippet. I didn't run the full tool/build.sh.
  • With a temporary test (not part of this PR), I interrupted the StepByStep2 snippet by overriding customStatement in a database subclass to throw right after the ADD COLUMN "due_date" statement of the 1 to 2 step, then opened the database again. With the old snippet, the second open fails with duplicate column name: due_date because the column was kept while user_version was still 1. With the new snippet, the step is rolled back and the second open migrates to version 3.
  • The same test also checked the other statements in the new section: a step that renames a column and is interrupted afterwards fails on the next start without a transaction (no such column: "legacy_body") and succeeds with one; when the 2 to 3 step is interrupted, per-step transactions keep the completed 1 to 2 step while a single transaction around runMigrationSteps rolls back to version 1; and alterTable inside a per-step transaction fails with FOREIGN KEY constraint failed (or deletes the referencing rows if they use ON DELETE CASCADE) unless foreign keys were disabled before the transaction.

Wrap each step of the runMigrationSteps example in a transaction and
explain why: without one, a step that fails or is interrupted halfway
stays partially applied while the stored schema version is not updated,
so the step fails when it runs again on the next start. Also note that
pragmas like foreign_keys have to be changed outside of the transaction,
which matters for alterTable as well.

Closes #3725
@simolus3
simolus3 merged commit 073cc46 into simolus3:develop Oct 14, 2026
10 of 12 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.

Migration.renameColumn and Migration.renameTable are not idempotent

2 participants