5.5 KiB
| title | description |
|---|---|
| Migrations | Manage Drift schema migrations safely in Flutter apps |
Default Rule
Use Drift's guided make-migrations workflow for existing apps. Manual migrations are error-prone and should be a fallback only when generated migration tooling cannot be adopted.
Do not bump schemaVersion unless the migration path for existing user databases is known.
Configure Guided Migrations
Add every database entry point to build.yaml:
targets:
$default:
builders:
drift_dev:
options:
databases:
app_database: lib/database.dart
schema_dir: drift_schemas/
test_dir: test/drift/
databases is required for make-migrations. schema_dir and test_dir may use the defaults, but declaring them makes the workflow explicit.
Initial Schema
Before changing a database schema, generate the initial schema snapshot:
dart run drift_dev make-migrations
Commit the generated schema files and migration test scaffolding. They are part of the migration contract.
Schema Change Workflow
- Modify table declarations.
- Bump
schemaVersion. - Run:
dart run drift_dev make-migrations
- Implement the generated step-by-step migration file.
- Run generated migration tests.
- Run
dart run build_runner build. - Run
flutter analyzeand app tests.
Step-By-Step Migration
After make-migrations, import the generated steps file and wire onUpgrade to the generated stepByStep helper.
import 'database.steps.dart';
@DriftDatabase(tables: [Categories, TodoItems])
class AppDatabase extends _$AppDatabase {
AppDatabase(QueryExecutor executor) : super(executor);
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async => m.createAll(),
onUpgrade: _schemaUpgrade,
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
}
extension Migrations on GeneratedDatabase {
OnUpgrade get _schemaUpgrade => stepByStep(
from1To2: (m, schema) async {
await m.addColumn(
schema.todoItems,
schema.todoItems.priority,
);
},
);
}
Keep the stepByStep call outside the database class body when possible so migration code does not accidentally reference the current schema instead of generated schema snapshots.
Common Operations
Add a nullable column or a column with a SQL default:
from1To2: (m, schema) async {
await m.addColumn(schema.todoItems, schema.todoItems.priority);
}
Create a new table:
from1To2: (m, schema) async {
await m.createTable(schema.categories);
}
Alter a table when constraints or generated columns require a rebuild:
from2To3: (m, schema) async {
await m.alterTable(TableMigration(schema.todoItems));
}
Create an index generated from a @TableIndex annotation:
from2To3: (m, schema) async {
await m.create(schema.todoItemsCompleted);
}
Use custom SQL only when Drift's migrator API cannot express the operation:
from2To3: (m, schema) async {
await m.customStatement('''
UPDATE todo_items
SET priority = 0
WHERE priority IS NULL
''');
}
Data Migrations
Prefer SQL statements over reading through current generated row classes during migration. Drift query builders expect the latest schema, which can be unsafe while upgrading older schemas.
from1To2: (m, schema) async {
await m.addColumn(schema.todoItems, schema.todoItems.priority);
await m.customStatement('''
UPDATE todo_items
SET priority = 0
WHERE priority IS NULL
''');
}
If Dart transformation is unavoidable, run it only after the columns or tables it needs exist.
Post-Migration Callbacks
Use beforeOpen for pragmas and seed data. Guard seed data with details.wasCreated or details.hadUpgrade.
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async => m.createAll(),
onUpgrade: _schemaUpgrade,
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
if (details.wasCreated) {
final inboxId = await into(categories).insert(
CategoriesCompanion.insert(name: 'Inbox'),
);
await into(todoItems).insert(
TodoItemsCompanion.insert(
title: 'First todo',
category: Value(inboxId),
),
);
}
},
);
}
Testing Migrations
make-migrations generates migration tests. Run them after every schema change:
dart test test/drift/
Migration tests should verify:
- a new database can be created at the latest schema;
- every old schema can migrate to the latest schema;
- important user data survives migration;
- indexes, constraints, and foreign keys match the expected schema.
Manual Fallback
Use manual migrations only when generated migration files cannot be used. Keep conditions incremental with from < version, not only exact equality, so users can migrate across multiple versions.
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) {
await m.addColumn(todoItems, todoItems.priority);
}
if (from < 3) {
await m.createTable(categories);
}
},
);
}
Report manual migration fallback as a residual risk unless it has equivalent tests.