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// import the migrations tooling\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 /* your existing migration logic */\n },\n beforeOpen: (details) async {\n // your existing beforeOpen callback, enable foreign keys, etc.\n\n if (kDebugMode) {\n // This check pulls in a fair amount of code that\'s not needed\n // anywhere else, so we recommend only doing it in debug builds.\n await validateDatabaseSchema();\n }\n },\n );\n}\n\n','native': '// import the migrations tooling\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 /* your existing migration logic */\n },\n beforeOpen: (details) async {\n // your existing beforeOpen callback, enable foreign keys, etc.\n\n if (kDebugMode) {\n // This check pulls in a fair amount of code that\'s not needed\n // anywhere else, so we recommend only doing it in debug builds.\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// import the migrations tooling\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 /* your existing migration logic */\n },\n beforeOpen: (details) async {\n // your existing beforeOpen callback, enable foreign keys, etc.\n\n // This check pulls in a fair amount of code that\'s not needed\n // anywhere else, so we recommend only doing it in debug builds.\n if (kDebugMode) {\n // The web schema verifier needs a sqlite3 instance to open another\n // version of your database so that the two can be compared.\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': '// import the migrations tooling\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 /* your existing migration logic */\n },\n beforeOpen: (details) async {\n // your existing beforeOpen callback, enable foreign keys, etc.\n\n // This check pulls in a fair amount of code that\'s not needed\n // anywhere else, so we recommend only doing it in debug builds.\n if (kDebugMode) {\n // The web schema verifier needs a sqlite3 instance to open another\n // version of your database so that the two can be compared.\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)': '// dart format width=80\nimport \'package:drift/internal/versioned_schema.dart\' as i0;\nimport \'package:drift/drift.dart\' as i1;\nimport \'package:drift/drift.dart\'; // ignore_for_file: type=lint,unused_import\n\n// GENERATED BY drift_dev, DO NOT MODIFY.\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// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n // Manually running migrations up to schema version 2, after which we\'ve\n // enabled step-by-step migrations.\n if (from < 2) {\n // we added the dueDate property in the change from version 1 to\n // version 2 - before switching to step-by-step migrations.\n await m.addColumn(todos, todos.dueDate);\n }\n\n // At this point, we should be migrated to schema 3. For future schema\n // changes, we will "start" at schema 3.\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 // we added the priority property in the change from version 1 or\n // 2 to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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': '// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n}\n\n','stepbystep2': 'onUpgrade: (m, from, to) async {\n // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n // Manually running migrations up to schema version 2, after which we\'ve\n // enabled step-by-step migrations.\n if (from < 2) {\n // we added the dueDate property in the change from version 1 to\n // version 2 - before switching to step-by-step migrations.\n await m.addColumn(todos, todos.dueDate);\n }\n\n // At this point, we should be migrated to schema 3. For future schema\n // changes, we will "start" at schema 3.\n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or\n // 2 to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n final steps = migrationSteps(\n from1To2: (m, schema) async {\n // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n );\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n // Running each step in a transaction ensures that a step is either\n // applied completely or not at all.\n steps: (currentVersion, database) =>\n database.transaction(() => steps(currentVersion, database)),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n // Manually running migrations up to schema version 2, after which we\'ve\n // enabled step-by-step migrations.\n if (from < 2) {\n // we added the dueDate property in the change from version 1 to\n // version 2 - before switching to step-by-step migrations.\n await m.addColumn(todos, todos.dueDate);\n }\n\n // At this point, we should be migrated to schema 3. For future schema\n // changes, we will "start" at schema 3.\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 // we added the priority property in the change from version 1 or\n // 2 to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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': '// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`\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 // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n}\n\n','stepbystep2': 'onUpgrade: (m, from, to) async {\n // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n final steps = migrationSteps(\n from1To2: (m, schema) async {\n // we added the dueDate property in the change from version 1 to\n // version 2\n await m.addColumn(schema.todos, schema.todos.dueDate);\n },\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or 2\n // to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n );\n\n await m.runMigrationSteps(\n from: from,\n to: to,\n // Running each step in a transaction ensures that a step is either\n // applied completely or not at all.\n steps: (currentVersion, database) =>\n database.transaction(() => steps(currentVersion, database)),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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 // Run migration steps without foreign keys and re-enable them later\n // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)\n await customStatement(\'PRAGMA foreign_keys = OFF\');\n\n // Manually running migrations up to schema version 2, after which we\'ve\n // enabled step-by-step migrations.\n if (from < 2) {\n // we added the dueDate property in the change from version 1 to\n // version 2 - before switching to step-by-step migrations.\n await m.addColumn(todos, todos.dueDate);\n }\n\n // At this point, we should be migrated to schema 3. For future schema\n // changes, we will "start" at schema 3.\n await m.runMigrationSteps(\n from: math.max(2, from),\n to: to,\n steps: migrationSteps(\n from2To3: (m, schema) async {\n // we added the priority property in the change from version 1 or\n // 2 to version 3\n await m.addColumn(schema.todos, schema.todos.priority);\n },\n ),\n );\n\n if (kDebugMode) {\n // Fail if the migration broke foreign keys\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)': '// dart format width=80\n// GENERATED CODE, DO NOT EDIT BY HAND.\n// ignore_for_file: type=lint\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)': '// dart format width=80\n// GENERATED CODE, DO NOT EDIT BY HAND.\n// ignore_for_file: type=lint\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)': '// dart format width=80\n// GENERATED CODE, DO NOT EDIT BY HAND.\n// ignore_for_file: type=lint\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) {