Error: Drift migration step fails, database left partially updated
0 reputation · 28 Jun 2025, 14:15 UTC
0 reputation · 28 Jun 2025, 14:15 UTC
To guarantee that a schema change applied via Drift’s MigrationStep is atomic, so that if any step fails the database is rolled back to its original state.
Drift’s migration API allows each step to run arbitrary SQL and may be asynchronous. The documentation states that a failure aborts the migration but does not automatically roll back changes made by previous steps. The onError callback is only for logging and does not trigger a rollback. There is no declarative rollback‑safe migration feature in the current stable release.
Should Drift provide a built‑in mechanism to wrap multiple MigrationStep instances in a single transaction, or is manual transaction handling the only supported approach?
MigrationStep instances be executed within a single SQLite transaction in Drift, and if so, how is this expressed in code?MigrationStep API?Yes – you can execute several MigrationStep instances inside a single SQLite transaction by wrapping them in a manual database.transaction call. This makes the whole group atomic; if any step throws, Drift rolls back the transaction and the database schema stays at the version before the migration started.
By default Drift runs each MigrationStep in its own transaction, so a failure does not automatically undo earlier steps. The onError callback only logs and does not trigger a rollback. As of the latest stable Drift 2.x release there is no declarative rollback‑safe migration feature; manual transaction handling remains the only supported approach.
MigrationStep (or use the onUpgrade callback of a MigrationStrategy) that opens a transaction.apply method (or execute the SQL directly).
class AddUserTableStep extends MigrationStep {
@override
Future apply(DriftDatabase db) => db.transaction(() async {
await db.into(db.users).insert(UserTableCompanion.insert(...));
// … other schema changes …
});
}
// Usage in a migration strategy
final strategy = MigrationStrategy(
onUpgrade: (version, from, to) async {
if (from < 2) {
await database.transaction(() async {
await AddUserTableStep().apply(database);
await AddIndexStep().apply(database);
// more steps …
});
}
},
);
Verification: run a test migration where the second step throws an exception and confirm that the schema remains at version 1 (or whatever the starting version was). Drift’s own test suite shows this behavior when steps are wrapped in database.transaction.
Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 28 Jun 2025, 20:35 UTC
Drift’s database.transaction() opens a SQLite IMMEDIATE transaction by default. This acquires a reserved lock immediately, preventing other writers but allowing readers. If your migration runs long or touches large tables, consider database.transaction(exclusive: true) to serialize access and avoid SQLITE_BUSY during the migration window.
SQLite only enforces foreign-key constraints when PRAGMA foreign_keys = ON is active before the transaction starts. Drift enables this by default on new connections, but if you reuse a pooled connection or disable it elsewhere, the manual transaction will not re-enable it automatically. Verify with db.customSelect('PRAGMA foreign_keys').getSingle() inside the transaction if constraint checks are critical.
Use sqlite3 CLI’s .schema and PRAGMA integrity_check after a forced failure to confirm the schema version and referential integrity are exactly as they were pre-migration.