From f494483c61dd1fd477dff3f607223c3c15c0a0ce Mon Sep 17 00:00:00 2001
From: Artem Efimov <41889057+efim0v@users.noreply.github.com>
Date: Tue, 13 Oct 2026 08:00:31 +0500
Subject: [PATCH] Docs: Run step-by-step migrations in transactions
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
---
docs/content/migrations/step_by_step.md | 26 +++++++++++++++--
docs/lib/src/generated_snippets.dart | 2 +-
.../src/snippets/migrations/step_by_step.dart | 29 +++++++++++--------
3 files changed, 42 insertions(+), 15 deletions(-)
diff --git a/docs/content/migrations/step_by_step.md b/docs/content/migrations/step_by_step.md
index 7eee3eba6..6ba92b9fe 100644
--- a/docs/content/migrations/step_by_step.md
+++ b/docs/content/migrations/step_by_step.md
@@ -69,8 +69,8 @@ This simplifies writing migrations a lot, since:
The `stepByStep` function generated by the `drift_dev schema steps` command gives you an
`OnUpgrade` callback.
-But you might want to customize the upgrade behavior, for instance by adding foreign key
-checks afterwards.
+But you might want to customize the upgrade behavior, for instance by running each step in a
+transaction or by adding foreign key checks afterwards.
The `Migrator.runMigrationSteps` helper method can be used for that, as this example
shows:
@@ -81,6 +81,28 @@ Here, foreign keys are disabled before running the migration and re-enabled afte
A check ensuring no inconsistencies occurred helps to catch issues with the migration
in debug modes.
+### Transactions
+
+Drift doesn't run migrations in a transaction by default, since some pragmas can't be changed
+inside a transaction. Without one, a step that throws or gets interrupted (for instance because
+the app was terminated) leaves the database partially migrated: the statements of that step that
+already ran are kept, but the stored schema version is still the one from before that step.
+The next time the database is opened, the step runs again and can fail, for instance because a
+column it renames no longer exists under its old name.
+
+Running each step in its own transaction, as the example above does, avoids this: either all
+changes of a step are applied or none of them are. Since drift updates the schema version
+(`PRAGMA user_version`) after each step, the next attempt starts with the step that didn't
+complete. To apply the entire upgrade atomically instead, wrap the `runMigrationSteps` call in
+a single `transaction`.
+
+Pragmas that can't be changed in a transaction need to be set before the migration steps run.
+In particular, `PRAGMA foreign_keys` has no effect inside a transaction, which is why the example
+disables foreign keys before calling `runMigrationSteps`. This also matters for `alterTable`: it
+disables foreign keys while re-creating a table, which doesn't work inside a transaction. Unless
+foreign keys were disabled before the transaction started, dropping the old table fails or runs
+`ON DELETE` actions on tables referencing it.
+
## Moving to step-by-step migrations
If you've been using drift before `stepByStep` was added to the library, or if you've never exported a schema,
diff --git a/docs/lib/src/generated_snippets.dart b/docs/lib/src/generated_snippets.dart
index e70785d76..a094ab666 100644
--- a/docs/lib/src/generated_snippets.dart
+++ b/docs/lib/src/generated_snippets.dart
@@ -43,7 +43,7 @@ const generatedSnippets = {
'lib/src/snippets/migrations/runtime_verification.dart.snippet.json': {'(full)': 'import \'package:drift/drift.dart\';\n\n\nimport \'package:drift_dev/api/migrations_native.dart\';\n\n\nconst kDebugMode = true;\n\nabstract class _\$MyDatabase extends GeneratedDatabase {\n _\$MyDatabase(super.executor);\n}\n\n\nclass MyDatabase extends _\$MyDatabase {\n MyDatabase(super.executor);\n\n @override\n Iterable<TableInfo<Table, dynamic>> get allTables =>\n throw UnimplementedError();\n\n @override\n int get schemaVersion => throw UnimplementedError();\n\n @override\n MigrationStrategy get migration => MigrationStrategy(\n onCreate: (m) async {\n \n },\n onUpgrade: (m, from, to) async {\n \n },\n beforeOpen: (details) async {\n \n\n if (kDebugMode) {\n \n \n await validateDatabaseSchema();\n }\n },\n );\n}\n\n','native': '\nimport \'package:drift_dev/api/migrations_native.dart\';\n\n\nclass MyDatabase extends _\$MyDatabase {\n @override\n MigrationStrategy get migration => MigrationStrategy(\n onCreate: (m) async {\n \n },\n onUpgrade: (m, from, to) async {\n \n },\n beforeOpen: (details) async {\n \n\n if (kDebugMode) {\n \n \n await validateDatabaseSchema();\n }\n },\n );\n}\n\n',},
'lib/src/snippets/migrations/runtime_verification_web.dart.snippet.json': {'(full)': 'import \'package:drift/drift.dart\';\n\n\nimport \'package:drift_dev/api/migrations_web.dart\';\nimport \'package:sqlite3/wasm.dart\';\n\n\nconst kDebugMode = true;\n\nabstract class _\$MyDatabase extends GeneratedDatabase {\n _\$MyDatabase(super.executor);\n}\n\n\nclass MyDatabase extends _\$MyDatabase {\n MyDatabase(super.executor);\n\n @override\n Iterable<TableInfo<Table, dynamic>> get allTables =>\n throw UnimplementedError();\n\n @override\n int get schemaVersion => throw UnimplementedError();\n\n @override\n MigrationStrategy get migration => MigrationStrategy(\n onCreate: (m) async {\n \n },\n onUpgrade: (m, from, to) async {\n \n },\n beforeOpen: (details) async {\n \n\n \n \n if (kDebugMode) {\n \n \n final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse(\'/\'));\n sqlite3.registerVirtualFileSystem(\n InMemoryFileSystem(),\n makeDefault: true,\n );\n await validateDatabaseSchema(sqlite3: sqlite3);\n }\n },\n );\n}\n\n','web': '\nimport \'package:drift_dev/api/migrations_web.dart\';\nimport \'package:sqlite3/wasm.dart\';\n\n\nclass MyDatabase extends _\$MyDatabase {\n @override\n MigrationStrategy get migration => MigrationStrategy(\n onCreate: (m) async {\n \n },\n onUpgrade: (m, from, to) async {\n \n },\n beforeOpen: (details) async {\n \n\n \n \n if (kDebugMode) {\n \n \n final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse(\'/\'));\n sqlite3.registerVirtualFileSystem(\n InMemoryFileSystem(),\n makeDefault: true,\n );\n await validateDatabaseSchema(sqlite3: sqlite3);\n }\n },\n );\n}\n\n',},
'lib/src/snippets/migrations/schema_versions.dart.snippet.json': {'(full)': '\nimport \'package:drift/internal/versioned_schema.dart\' as i0;\nimport \'package:drift/drift.dart\' as i1;\nimport \'package:drift/drift.dart\'; \n\n\nfinal class Schema2 extends i0.VersionedSchema {\n Schema2({required super.database}) : super(version: 2);\n @override\n late final List<i1.DatabaseSchemaEntity> entities = [todos];\n late final Shape0 todos = Shape0(\n source: i0.VersionedTable(\n entityName: \'todos\',\n withoutRowId: false,\n isStrict: false,\n tableConstraints: [],\n columns: [_column_0, _column_1, _column_2, _column_3, _column_4],\n attachedDatabase: database,\n ),\n alias: null,\n );\n}\n\nclass Shape0 extends i0.VersionedTable {\n Shape0({required super.source, required super.alias}) : super.aliased();\n i1.GeneratedColumn<int> get id =>\n columnsByName[\'id\']! as i1.GeneratedColumn<int>;\n i1.GeneratedColumn<String> get title =>\n columnsByName[\'title\']! as i1.GeneratedColumn<String>;\n i1.GeneratedColumn<String> get content =>\n columnsByName[\'body\']! as i1.GeneratedColumn<String>;\n i1.GeneratedColumn<int> get category =>\n columnsByName[\'category\']! as i1.GeneratedColumn<int>;\n i1.GeneratedColumn<DateTime> get dueDate =>\n columnsByName[\'due_date\']! as i1.GeneratedColumn<DateTime>;\n}\n\ni1.GeneratedColumn<int> _column_0(String aliasedName) =>\n i1.GeneratedColumn<int>(\n \'id\',\n aliasedName,\n false,\n hasAutoIncrement: true,\n type: i1.DriftSqlType.int,\n defaultConstraints: i1.GeneratedColumn.constraintIsAlways(\n \'PRIMARY KEY AUTOINCREMENT\',\n ),\n );\ni1.GeneratedColumn<String> _column_1(String aliasedName) =>\n i1.GeneratedColumn<String>(\n \'title\',\n aliasedName,\n false,\n additionalChecks: i1.GeneratedColumn.checkTextLength(\n minTextLength: 6,\n maxTextLength: 10,\n ),\n type: i1.DriftSqlType.string,\n );\ni1.GeneratedColumn<String> _column_2(String aliasedName) =>\n i1.GeneratedColumn<String>(\n \'body\',\n aliasedName,\n false,\n type: i1.DriftSqlType.string,\n );\ni1.GeneratedColumn<int> _column_3(String aliasedName) =>\n i1.GeneratedColumn<int>(\n \'category\',\n aliasedName,\n true,\n type: i1.DriftSqlType.int,\n );\ni1.GeneratedColumn<DateTime> _column_4(String aliasedName) =>\n i1.GeneratedColumn<DateTime>(\n \'due_date\',\n aliasedName,\n true,\n type: i1.DriftSqlType.dateTime,\n );\n\nfinal class Schema3 extends i0.VersionedSchema {\n Schema3({required super.database}) : super(version: 3);\n @override\n late final List<i1.DatabaseSchemaEntity> entities = [todos];\n late final Shape1 todos = Shape1(\n source: i0.VersionedTable(\n entityName: \'todos\',\n withoutRowId: false,\n isStrict: false,\n tableConstraints: [],\n columns: [\n _column_0,\n _column_1,\n _column_2,\n _column_3,\n _column_4,\n _column_5,\n ],\n attachedDatabase: database,\n ),\n alias: null,\n );\n}\n\nclass Shape1 extends i0.VersionedTable {\n Shape1({required super.source, required super.alias}) : super.aliased();\n i1.GeneratedColumn<int> get id =>\n columnsByName[\'id\']! as i1.GeneratedColumn<int>;\n i1.GeneratedColumn<String> get title =>\n columnsByName[\'title\']! as i1.GeneratedColumn<String>;\n i1.GeneratedColumn<String> get content =>\n columnsByName[\'body\']! as i1.GeneratedColumn<String>;\n i1.GeneratedColumn<int> get category =>\n columnsByName[\'category\']! as i1.GeneratedColumn<int>;\n i1.GeneratedColumn<DateTime> get dueDate =>\n columnsByName[\'due_date\']! as i1.GeneratedColumn<DateTime>;\n i1.GeneratedColumn<int> get priority =>\n columnsByName[\'priority\']! as i1.GeneratedColumn<int>;\n}\n\ni1.GeneratedColumn<int> _column_5(String aliasedName) =>\n i1.GeneratedColumn<int>(\n \'priority\',\n aliasedName,\n true,\n type: i1.DriftSqlType.int,\n );\ni0.MigrationStepWithVersion migrationSteps({\n required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,\n required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,\n}) {\n return (currentVersion, database) async {\n switch (currentVersion) {\n case 1:\n final schema = Schema2(database: database);\n final migrator = i1.Migrator(database, schema);\n await from1To2(migrator, schema);\n return 2;\n case 2:\n final schema = Schema3(database: database);\n final migrator = i1.Migrator(database, schema);\n await from2To3(migrator, schema);\n return 3;\n default:\n throw ArgumentError.value(\'Unknown migration from \$currentVersion\');\n }\n };\n}\n\ni1.OnUpgrade stepByStep({\n required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,\n required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,\n}) => i0.VersionedSchema.stepByStepHelper(\n step: migrationSteps(from1To2: from1To2, from2To3: from2To3),\n);\n',},
-'lib/src/snippets/migrations/step_by_step.dart.snippet.json': {'(full)': 'import \'dart:math\' as math;\n\nimport \'package:drift/drift.dart\';\n\nimport \'migrations.dart\';\n\n\nimport \'schema_versions.dart\';\n\n\nclass StepByStep {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: stepByStep(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n }\n\n}\n\nextension StepByStep2 on GeneratedDatabase {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n steps: migrationSteps(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n );\n }\n}\n\nextension StepByStep3 on MyDatabase {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n \n \n if (from < 2) {\n \n \n await m.addColumn(todos, todos.dueDate);\n }\n\n \n \n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from1To2: (m, schema) async {\n throw \'users would not have this as v2 would be their first schema\';\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n );\n }\n}\n','stepbystep': '\nimport \'schema_versions.dart\';\n\nMigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: stepByStep(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n}\n\n','stepbystep2': 'onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n steps: migrationSteps(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n},\n','stepbystep3': 'onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n \n \n if (from < 2) {\n \n \n await m.addColumn(todos, todos.dueDate);\n }\n\n \n \n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n',},
+'lib/src/snippets/migrations/step_by_step.dart.snippet.json': {'(full)': 'import \'dart:math\' as math;\n\nimport \'package:drift/drift.dart\';\n\nimport \'migrations.dart\';\n\n\nimport \'schema_versions.dart\';\n\n\nclass StepByStep {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: stepByStep(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n }\n\n}\n\nextension StepByStep2 on GeneratedDatabase {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n final steps = migrationSteps(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n );\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n \n \n steps: (currentVersion, database) =>\n database.transaction(() => steps(currentVersion, database)),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n );\n }\n}\n\nextension StepByStep3 on MyDatabase {\n MigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n \n \n if (from < 2) {\n \n \n await m.addColumn(todos, todos.dueDate);\n }\n\n \n \n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from1To2: (m, schema) async {\n throw \'users would not have this as v2 would be their first schema\';\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n );\n }\n}\n','stepbystep': '\nimport \'schema_versions.dart\';\n\nMigrationStrategy get migration {\n return MigrationStrategy(\n onCreate: (Migrator m) async {\n await m.createAll();\n },\n onUpgrade: stepByStep(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n}\n\n','stepbystep2': 'onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n final steps = migrationSteps(\n from1To2: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n );\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n \n \n steps: (currentVersion, database) =>\n database.transaction(() => steps(currentVersion, database)),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n},\n','stepbystep3': 'onUpgrade: (m, from, to) async {\n \n \n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n \n \n if (from < 2) {\n \n \n await m.addColumn(todos, todos.dueDate);\n }\n\n \n \n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from2To3: (m, schema) async {\n \n \n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n \n final wrongForeignKeys = await customSelect(\n \'PRAGMA foreign_key_check\',\n ).get();\n assert(\n wrongForeignKeys.isEmpty,\n \'\${wrongForeignKeys.map((e) => e.data)}\',\n );\n }\n\n await customStatement(\'PRAGMA foreign_keys = ON;\');\n },\n',},
'lib/src/snippets/migrations/tests/generated_migrations/schema.dart.snippet.json': {'(full)': '\n\n\nimport \'package:drift/drift.dart\';\nimport \'package:drift/internal/migrations.dart\';\nimport \'schema_v1.dart\' as v1;\nimport \'schema_v2.dart\' as v2;\nimport \'schema_v3.dart\' as v3;\n\nclass GeneratedHelper implements SchemaInstantiationHelper {\n @override\n GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {\n switch (version) {\n case 1:\n return v1.DatabaseAtV1(db);\n case 2:\n return v2.DatabaseAtV2(db);\n case 3:\n return v3.DatabaseAtV3(db);\n default:\n throw MissingSchemaException(version, versions);\n }\n }\n\n static const versions = const [1, 2, 3];\n}\n',},
'lib/src/snippets/migrations/tests/generated_migrations/schema_v1.dart.snippet.json': {'(full)': '\n\n\nimport \'package:drift/drift.dart\';\n\nclass Todos extends Table with TableInfo<Todos, TodosData> {\n @override\n final GeneratedDatabase attachedDatabase;\n final String? _alias;\n Todos(this.attachedDatabase, [this._alias]);\n late final GeneratedColumn<int> id = GeneratedColumn<int>(\n \'id\',\n aliasedName,\n false,\n hasAutoIncrement: true,\n type: DriftSqlType.int,\n requiredDuringInsert: false,\n defaultConstraints: GeneratedColumn.constraintIsAlways(\n \'PRIMARY KEY AUTOINCREMENT\',\n ),\n );\n late final GeneratedColumn<String> title = GeneratedColumn<String>(\n \'title\',\n aliasedName,\n false,\n additionalChecks: GeneratedColumn.checkTextLength(\n minTextLength: 6,\n maxTextLength: 10,\n ),\n type: DriftSqlType.string,\n requiredDuringInsert: true,\n );\n late final GeneratedColumn<String> content = GeneratedColumn<String>(\n \'body\',\n aliasedName,\n false,\n type: DriftSqlType.string,\n requiredDuringInsert: true,\n );\n late final GeneratedColumn<int> category = GeneratedColumn<int>(\n \'category\',\n aliasedName,\n true,\n type: DriftSqlType.int,\n requiredDuringInsert: false,\n );\n @override\n List<GeneratedColumn> get \$columns => [id, title, content, category];\n @override\n String get aliasedName => _alias ?? actualTableName;\n @override\n String get actualTableName => \$name;\n static const String \$name = \'todos\';\n @override\n Set<GeneratedColumn> get \$primaryKey => {id};\n @override\n TodosData map(Map<String, dynamic> data, {String? tablePrefix}) {\n final effectivePrefix = tablePrefix != null ? \'\$tablePrefix.\' : \'\';\n return TodosData(\n id: attachedDatabase.typeMapping.read(\n DriftSqlType.int,\n data[\'\${effectivePrefix}id\'],\n )!,\n title: attachedDatabase.typeMapping.read(\n DriftSqlType.string,\n data[\'\${effectivePrefix}title\'],\n )!,\n content: attachedDatabase.typeMapping.read(\n DriftSqlType.string,\n data[\'\${effectivePrefix}body\'],\n )!,\n category: attachedDatabase.typeMapping.read(\n DriftSqlType.int,\n data[\'\${effectivePrefix}category\'],\n ),\n );\n }\n\n @override\n Todos createAlias(String alias) {\n return Todos(attachedDatabase, alias);\n }\n}\n\nclass TodosData extends DataClass implements Insertable<TodosData> {\n final int id;\n final String title;\n final String content;\n final int? category;\n const TodosData({\n required this.id,\n required this.title,\n required this.content,\n this.category,\n });\n @override\n Map<String, Expression> toColumns(bool nullToAbsent) {\n final map = <String, Expression>{};\n map[\'id\'] = Variable<int>(id);\n map[\'title\'] = Variable<String>(title);\n map[\'body\'] = Variable<String>(content);\n if (!nullToAbsent || category != null) {\n map[\'category\'] = Variable<int>(category);\n }\n return map;\n }\n\n TodosCompanion toCompanion(bool nullToAbsent) {\n return TodosCompanion(\n id: Value(id),\n title: Value(title),\n content: Value(content),\n category: category == null && nullToAbsent\n ? const Value.absent()\n : Value(category),\n );\n }\n\n factory TodosData.fromJson(\n Map<String, dynamic> json, {\n ValueSerializer? serializer,\n }) {\n serializer ??= driftRuntimeOptions.defaultSerializer;\n return TodosData(\n id: serializer.fromJson<int>(json[\'id\']),\n title: serializer.fromJson<String>(json[\'title\']),\n content: serializer.fromJson<String>(json[\'content\']),\n category: serializer.fromJson<int?>(json[\'category\']),\n );\n }\n @override\n Map<String, dynamic> toJson({ValueSerializer? serializer}) {\n serializer ??= driftRuntimeOptions.defaultSerializer;\n return <String, dynamic>{\n \'id\': serializer.toJson<int>(id),\n \'title\': serializer.toJson<String>(title),\n \'content\': serializer.toJson<String>(content),\n \'category\': serializer.toJson<int?>(category),\n };\n }\n\n TodosData copyWith({\n int? id,\n String? title,\n String? content,\n Value<int?> category = const Value.absent(),\n }) => TodosData(\n id: id ?? this.id,\n title: title ?? this.title,\n content: content ?? this.content,\n category: category.present ? category.value : this.category,\n );\n TodosData copyWithCompanion(TodosCompanion data) {\n return TodosData(\n id: data.id.present ? data.id.value : this.id,\n title: data.title.present ? data.title.value : this.title,\n content: data.content.present ? data.content.value : this.content,\n category: data.category.present ? data.category.value : this.category,\n );\n }\n\n @override\n String toString() {\n return (StringBuffer(\'TodosData(\')\n ..write(\'id: \$id, \')\n ..write(\'title: \$title, \')\n ..write(\'content: \$content, \')\n ..write(\'category: \$category\')\n ..write(\')\'))\n .toString();\n }\n\n @override\n int get hashCode => Object.hash(id, title, content, category);\n @override\n bool operator ==(Object other) =>\n identical(this, other) ||\n (other is TodosData &&\n other.id == this.id &&\n other.title == this.title &&\n other.content == this.content &&\n other.category == this.category);\n}\n\nclass TodosCompanion extends UpdateCompanion<TodosData> {\n final Value<int> id;\n final Value<String> title;\n final Value<String> content;\n final Value<int?> category;\n const TodosCompanion({\n this.id = const Value.absent(),\n this.title = const Value.absent(),\n this.content = const Value.absent(),\n this.category = const Value.absent(),\n });\n TodosCompanion.insert({\n this.id = const Value.absent(),\n required String title,\n required String content,\n this.category = const Value.absent(),\n }) : title = Value(title),\n content = Value(content);\n static Insertable<TodosData> custom({\n Expression<int>? id,\n Expression<String>? title,\n Expression<String>? content,\n Expression<int>? category,\n }) {\n return RawValuesInsertable({\n if (id != null) \'id\': id,\n if (title != null) \'title\': title,\n if (content != null) \'body\': content,\n if (category != null) \'category\': category,\n });\n }\n\n TodosCompanion copyWith({\n Value<int>? id,\n Value<String>? title,\n Value<String>? content,\n Value<int?>? category,\n }) {\n return TodosCompanion(\n id: id ?? this.id,\n title: title ?? this.title,\n content: content ?? this.content,\n category: category ?? this.category,\n );\n }\n\n @override\n Map<String, Expression> toColumns(bool nullToAbsent) {\n final map = <String, Expression>{};\n if (id.present) {\n map[\'id\'] = Variable<int>(id.value);\n }\n if (title.present) {\n map[\'title\'] = Variable<String>(title.value);\n }\n if (content.present) {\n map[\'body\'] = Variable<String>(content.value);\n }\n if (category.present) {\n map[\'category\'] = Variable<int>(category.value);\n }\n return map;\n }\n\n @override\n String toString() {\n return (StringBuffer(\'TodosCompanion(\')\n ..write(\'id: \$id, \')\n ..write(\'title: \$title, \')\n ..write(\'content: \$content, \')\n ..write(\'category: \$category\')\n ..write(\')\'))\n .toString();\n }\n}\n\nclass DatabaseAtV1 extends GeneratedDatabase {\n DatabaseAtV1(QueryExecutor e) : super(e);\n late final Todos todos = Todos(this);\n @override\n Iterable<TableInfo<Table, Object?>> get allTables =>\n allSchemaEntities.whereType<TableInfo<Table, Object?>>();\n @override\n List<DatabaseSchemaEntity> get allSchemaEntities => [todos];\n @override\n int get schemaVersion => 1;\n}\n',},
'lib/src/snippets/migrations/tests/generated_migrations/schema_v2.dart.snippet.json': {'(full)': '\n\n\nimport \'package:drift/drift.dart\';\n\nclass Todos extends Table with TableInfo<Todos, TodosData> {\n @override\n final GeneratedDatabase attachedDatabase;\n final String? _alias;\n Todos(this.attachedDatabase, [this._alias]);\n late final GeneratedColumn<int> id = GeneratedColumn<int>(\n \'id\',\n aliasedName,\n false,\n hasAutoIncrement: true,\n type: DriftSqlType.int,\n requiredDuringInsert: false,\n defaultConstraints: GeneratedColumn.constraintIsAlways(\n \'PRIMARY KEY AUTOINCREMENT\',\n ),\n );\n late final GeneratedColumn<String> title = GeneratedColumn<String>(\n \'title\',\n aliasedName,\n false,\n additionalChecks: GeneratedColumn.checkTextLength(\n minTextLength: 6,\n maxTextLength: 10,\n ),\n type: DriftSqlType.string,\n requiredDuringInsert: true,\n );\n late final GeneratedColumn<String> content = GeneratedColumn<String>(\n \'body\',\n aliasedName,\n false,\n type: DriftSqlType.string,\n requiredDuringInsert: true,\n );\n late final GeneratedColumn<int> category = GeneratedColumn<int>(\n \'category\',\n aliasedName,\n true,\n type: DriftSqlType.int,\n requiredDuringInsert: false,\n );\n late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(\n \'due_date\',\n aliasedName,\n true,\n type: DriftSqlType.dateTime,\n requiredDuringInsert: false,\n );\n @override\n List<GeneratedColumn> get \$columns => [id, title, content, category, dueDate];\n @override\n String get aliasedName => _alias ?? actualTableName;\n @override\n String get actualTableName => \$name;\n static const String \$name = \'todos\';\n @override\n Set<GeneratedColumn> get \$primaryKey => {id};\n @override\n TodosData map(Map<String, dynamic> data, {String? tablePrefix}) {\n final effectivePrefix = tablePrefix != null ? \'\$tablePrefix.\' : \'\';\n return TodosData(\n id: attachedDatabase.typeMapping.read(\n DriftSqlType.int,\n data[\'\${effectivePrefix}id\'],\n )!,\n title: attachedDatabase.typeMapping.read(\n DriftSqlType.string,\n data[\'\${effectivePrefix}title\'],\n )!,\n content: attachedDatabase.typeMapping.read(\n DriftSqlType.string,\n data[\'\${effectivePrefix}body\'],\n )!,\n category: attachedDatabase.typeMapping.read(\n DriftSqlType.int,\n data[\'\${effectivePrefix}category\'],\n ),\n dueDate: attachedDatabase.typeMapping.read(\n DriftSqlType.dateTime,\n data[\'\${effectivePrefix}due_date\'],\n ),\n );\n }\n\n @override\n Todos createAlias(String alias) {\n return Todos(attachedDatabase, alias);\n }\n}\n\nclass TodosData extends DataClass implements Insertable<TodosData> {\n final int id;\n final String title;\n final String content;\n final int? category;\n final DateTime? dueDate;\n const TodosData({\n required this.id,\n required this.title,\n required this.content,\n this.category,\n this.dueDate,\n });\n @override\n Map<String, Expression> toColumns(bool nullToAbsent) {\n final map = <String, Expression>{};\n map[\'id\'] = Variable<int>(id);\n map[\'title\'] = Variable<String>(title);\n map[\'body\'] = Variable<String>(content);\n if (!nullToAbsent || category != null) {\n map[\'category\'] = Variable<int>(category);\n }\n if (!nullToAbsent || dueDate != null) {\n map[\'due_date\'] = Variable<DateTime>(dueDate);\n }\n return map;\n }\n\n TodosCompanion toCompanion(bool nullToAbsent) {\n return TodosCompanion(\n id: Value(id),\n title: Value(title),\n content: Value(content),\n category: category == null && nullToAbsent\n ? const Value.absent()\n : Value(category),\n dueDate: dueDate == null && nullToAbsent\n ? const Value.absent()\n : Value(dueDate),\n );\n }\n\n factory TodosData.fromJson(\n Map<String, dynamic> json, {\n ValueSerializer? serializer,\n }) {\n serializer ??= driftRuntimeOptions.defaultSerializer;\n return TodosData(\n id: serializer.fromJson<int>(json[\'id\']),\n title: serializer.fromJson<String>(json[\'title\']),\n content: serializer.fromJson<String>(json[\'content\']),\n category: serializer.fromJson<int?>(json[\'category\']),\n dueDate: serializer.fromJson<DateTime?>(json[\'dueDate\']),\n );\n }\n @override\n Map<String, dynamic> toJson({ValueSerializer? serializer}) {\n serializer ??= driftRuntimeOptions.defaultSerializer;\n return <String, dynamic>{\n \'id\': serializer.toJson<int>(id),\n \'title\': serializer.toJson<String>(title),\n \'content\': serializer.toJson<String>(content),\n \'category\': serializer.toJson<int?>(category),\n \'dueDate\': serializer.toJson<DateTime?>(dueDate),\n };\n }\n\n TodosData copyWith({\n int? id,\n String? title,\n String? content,\n Value<int?> category = const Value.absent(),\n Value<DateTime?> dueDate = const Value.absent(),\n }) => TodosData(\n id: id ?? this.id,\n title: title ?? this.title,\n content: content ?? this.content,\n category: category.present ? category.value : this.category,\n dueDate: dueDate.present ? dueDate.value : this.dueDate,\n );\n TodosData copyWithCompanion(TodosCompanion data) {\n return TodosData(\n id: data.id.present ? data.id.value : this.id,\n title: data.title.present ? data.title.value : this.title,\n content: data.content.present ? data.content.value : this.content,\n category: data.category.present ? data.category.value : this.category,\n dueDate: data.dueDate.present ? data.dueDate.value : this.dueDate,\n );\n }\n\n @override\n String toString() {\n return (StringBuffer(\'TodosData(\')\n ..write(\'id: \$id, \')\n ..write(\'title: \$title, \')\n ..write(\'content: \$content, \')\n ..write(\'category: \$category, \')\n ..write(\'dueDate: \$dueDate\')\n ..write(\')\'))\n .toString();\n }\n\n @override\n int get hashCode => Object.hash(id, title, content, category, dueDate);\n @override\n bool operator ==(Object other) =>\n identical(this, other) ||\n (other is TodosData &&\n other.id == this.id &&\n other.title == this.title &&\n other.content == this.content &&\n other.category == this.category &&\n other.dueDate == this.dueDate);\n}\n\nclass TodosCompanion extends UpdateCompanion<TodosData> {\n final Value<int> id;\n final Value<String> title;\n final Value<String> content;\n final Value<int?> category;\n final Value<DateTime?> dueDate;\n const TodosCompanion({\n this.id = const Value.absent(),\n this.title = const Value.absent(),\n this.content = const Value.absent(),\n this.category = const Value.absent(),\n this.dueDate = const Value.absent(),\n });\n TodosCompanion.insert({\n this.id = const Value.absent(),\n required String title,\n required String content,\n this.category = const Value.absent(),\n this.dueDate = const Value.absent(),\n }) : title = Value(title),\n content = Value(content);\n static Insertable<TodosData> custom({\n Expression<int>? id,\n Expression<String>? title,\n Expression<String>? content,\n Expression<int>? category,\n Expression<DateTime>? dueDate,\n }) {\n return RawValuesInsertable({\n if (id != null) \'id\': id,\n if (title != null) \'title\': title,\n if (content != null) \'body\': content,\n if (category != null) \'category\': category,\n if (dueDate != null) \'due_date\': dueDate,\n });\n }\n\n TodosCompanion copyWith({\n Value<int>? id,\n Value<String>? title,\n Value<String>? content,\n Value<int?>? category,\n Value<DateTime?>? dueDate,\n }) {\n return TodosCompanion(\n id: id ?? this.id,\n title: title ?? this.title,\n content: content ?? this.content,\n category: category ?? this.category,\n dueDate: dueDate ?? this.dueDate,\n );\n }\n\n @override\n Map<String, Expression> toColumns(bool nullToAbsent) {\n final map = <String, Expression>{};\n if (id.present) {\n map[\'id\'] = Variable<int>(id.value);\n }\n if (title.present) {\n map[\'title\'] = Variable<String>(title.value);\n }\n if (content.present) {\n map[\'body\'] = Variable<String>(content.value);\n }\n if (category.present) {\n map[\'category\'] = Variable<int>(category.value);\n }\n if (dueDate.present) {\n map[\'due_date\'] = Variable<DateTime>(dueDate.value);\n }\n return map;\n }\n\n @override\n String toString() {\n return (StringBuffer(\'TodosCompanion(\')\n ..write(\'id: \$id, \')\n ..write(\'title: \$title, \')\n ..write(\'content: \$content, \')\n ..write(\'category: \$category, \')\n ..write(\'dueDate: \$dueDate\')\n ..write(\')\'))\n .toString();\n }\n}\n\nclass DatabaseAtV2 extends GeneratedDatabase {\n DatabaseAtV2(QueryExecutor e) : super(e);\n late final Todos todos = Todos(this);\n @override\n Iterable<TableInfo<Table, Object?>> get allTables =>\n allSchemaEntities.whereType<TableInfo<Table, Object?>>();\n @override\n List<DatabaseSchemaEntity> get allSchemaEntities => [todos];\n @override\n int get schemaVersion => 2;\n}\n',},
diff --git a/docs/lib/src/snippets/migrations/step_by_step.dart b/docs/lib/src/snippets/migrations/step_by_step.dart
index 27b0684e3..6d46a3fbe 100644
--- a/docs/lib/src/snippets/migrations/step_by_step.dart
+++ b/docs/lib/src/snippets/migrations/step_by_step.dart
@@ -47,21 +47,26 @@ extension StepByStep2 on GeneratedDatabase {
// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
await customStatement('PRAGMA foreign_keys = OFF');
+ final steps = migrationSteps(
+ from1To2: (m, schema) async {
+ // we added the dueDate property in the change from version 1 to
+ // version 2
+ await m.addColumn(schema.todos, schema.todos.dueDate);
+ },
+ from2To3: (m, schema) async {
+ // we added the priority property in the change from version 1 or 2
+ // to version 3
+ await m.addColumn(schema.todos, schema.todos.priority);
+ },
+ );
+
await m.runMigrationSteps(
from: from,
to: to,
- steps: migrationSteps(
- from1To2: (m, schema) async {
- // we added the dueDate property in the change from version 1 to
- // version 2
- await m.addColumn(schema.todos, schema.todos.dueDate);
- },
- from2To3: (m, schema) async {
- // we added the priority property in the change from version 1 or 2
- // to version 3
- await m.addColumn(schema.todos, schema.todos.priority);
- },
- ),
+ // Running each step in a transaction ensures that a step is either
+ // applied completely or not at all.
+ steps: (currentVersion, database) =>
+ database.transaction(() => steps(currentVersion, database)),
);
if (kDebugMode) {