diff --git a/docs/lib/snippets/migrations/exported_eschema/drift_schema_v1.json b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v1.json new file mode 100644 index 00000000..c0910adf --- /dev/null +++ b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v1.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"todos","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"title","getter_name":"title","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":6,"max":10}}]},{"name":"body","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"category","getter_name":"category","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/docs/lib/snippets/migrations/exported_eschema/drift_schema_v2.json b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v2.json new file mode 100644 index 00000000..08aa3fa4 --- /dev/null +++ b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v2.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"todos","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"title","getter_name":"title","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":6,"max":10}}]},{"name":"body","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"category","getter_name":"category","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"due_date","getter_name":"dueDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/docs/lib/snippets/migrations/exported_eschema/drift_schema_v3.json b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v3.json new file mode 100644 index 00000000..1cd239c3 --- /dev/null +++ b/docs/lib/snippets/migrations/exported_eschema/drift_schema_v3.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"todos","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"title","getter_name":"title","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":6,"max":10}}]},{"name":"body","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"category","getter_name":"category","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"due_date","getter_name":"dueDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"priority","getter_name":"priority","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/docs/lib/snippets/migrations/migrations.dart b/docs/lib/snippets/migrations/migrations.dart index 5eb322f7..6920e660 100644 --- a/docs/lib/snippets/migrations/migrations.dart +++ b/docs/lib/snippets/migrations/migrations.dart @@ -1,5 +1,11 @@ import 'package:drift/drift.dart'; +// #docregion stepbystep +// This file was generated by `drift_dev schema steps drift_schemas lib/database/schema_versions.dart +import 'schema_versions.dart'; + +// #enddocregion stepbystep + part 'migrations.g.dart'; const kDebugMode = false; @@ -91,3 +97,27 @@ class Example extends _$Example { // #enddocregion change_type } } + +class StepByStep { + // #docregion stepbystep + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: stepByStep( + 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); + }, + ), + ); + } + // #enddocregion stepbystep +} diff --git a/docs/lib/snippets/migrations/schema_versions.dart b/docs/lib/snippets/migrations/schema_versions.dart new file mode 100644 index 00000000..c398b48f --- /dev/null +++ b/docs/lib/snippets/migrations/schema_versions.dart @@ -0,0 +1,129 @@ +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class _S2 extends i0.VersionedSchema { + _S2({required super.database}) : super(version: 2); + @override + late final List entities = [ + todos, + ]; + late final Shape0 todos = Shape0( + source: i0.VersionedTable( + entityName: 'todos', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get title => + columnsByName['title']! as i1.GeneratedColumn; + i1.GeneratedColumn get content => + columnsByName['body']! as i1.GeneratedColumn; + i1.GeneratedColumn get category => + columnsByName['category']! as i1.GeneratedColumn; + i1.GeneratedColumn get dueDate => + columnsByName['due_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + hasAutoIncrement: true, + type: i1.DriftSqlType.int, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn('title', aliasedName, false, + additionalChecks: i1.GeneratedColumn.checkTextLength( + minTextLength: 6, maxTextLength: 10), + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_2(String aliasedName) => + i1.GeneratedColumn('body', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn('category', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn('due_date', aliasedName, true, + type: i1.DriftSqlType.dateTime); + +final class _S3 extends i0.VersionedSchema { + _S3({required super.database}) : super(version: 3); + @override + late final List entities = [ + todos, + ]; + late final Shape1 todos = Shape1( + source: i0.VersionedTable( + entityName: 'todos', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get title => + columnsByName['title']! as i1.GeneratedColumn; + i1.GeneratedColumn get content => + columnsByName['body']! as i1.GeneratedColumn; + i1.GeneratedColumn get category => + columnsByName['category']! as i1.GeneratedColumn; + i1.GeneratedColumn get dueDate => + columnsByName['due_date']! as i1.GeneratedColumn; + i1.GeneratedColumn get priority => + columnsByName['priority']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn('priority', aliasedName, true, + type: i1.DriftSqlType.int); +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, +}) { + return i1.Migrator.stepByStepHelper(step: (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = _S2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + case 2: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }); +} diff --git a/docs/pages/docs/Advanced Features/migrations.md b/docs/pages/docs/Advanced Features/migrations.md index d1f7b933..09eab150 100644 --- a/docs/pages/docs/Advanced Features/migrations.md +++ b/docs/pages/docs/Advanced Features/migrations.md @@ -13,11 +13,13 @@ New features need new columns or tables, and outdated columns may have to be alt removed altogether. When making changes to your database schema, you need to write migrations enabling users with an old version of your app to convert to the database expected by the latest version. -Drift provides a set of APIs to make writing migrations easy. +With incorrect migrations, your database ends up in an inconsistent state which can cause crashes +or data loss. This is why drift provides dedicated test tools and APIs to make writing migrations +easy and safe. {% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %} -## Basics +## Manual setup {#basics} Drift provides a migration API that can be used to gradually apply schema changes after bumping the `schemaVersion` getter inside the `Database` class. To use it, override the `migration` @@ -43,7 +45,7 @@ you've actually added the column. In general, try to avoid running queries in mi `sqlite` can feel a bit limiting when it comes to migrations - there only are methods to create tables and columns. Existing columns can't be altered or removed. A workaround is described [here](https://stackoverflow.com/a/805508), it can be used together with `customStatement` to run the statements. -Alternatively, [complex migrations](#complex-migrations) help automating this. +Alternatively, [complex migrations](#complex-migrations) described on this page help automating this. ### Tips @@ -60,6 +62,241 @@ With all of this combined, a migration callback can look like this: {% include "blocks/snippet" snippets = snippets name = 'structured' %} +## Migration workflow + +While migrations can be written manually without additional help from drift, dedicated tools testing your migrations help +to ensure that they are correct and aren't loosing any data. + +Drift's migration tooling consists of the following steps: + +1. After each change to your schema, use a tool to export the current schema into a separate file. +2. Use a drift tool to generate test code able to verify that your migrations are bringing the database + into the expected schema. +3. Use generated code to make writing schema migrations easier. + +### Setup + +As described by the first step, you can export the schema of your database into a JSON file. +It is recommended to do this once intially, and then again each time you change your schema +and increase the `schemaVersion` getter in the database. + +You should store these exported files in your repository and include them in source control. +This guide assumes a top-level `drift_schemas/` folder in your project, like this: + +``` +my_app + .../ + lib/ + database/ + database.dart + database.g.dart + test/ + generated_migrations/ + schema.dart + schema_v1.dart + schema_v2.dart + drift_schemas/ + drift_schema_v1.json + drift_schema_v2.json + pubspec.yaml +``` + +Of course, you can also use another folder or a subfolder somewhere if that suits your workflow +better. + +{% block "blocks/alert" title="Examples available" %} +Exporting schemas and generating code for them can't be done with `build_runner` alone, which is +why this setup described here is necessary. + +We hope it's worth it though! Verifying migrations can give you confidence that you won't run +into issues after changing your database. +If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). + +Also there are two examples in the drift repository which may be useful as a reference: + +- A [Flutter app](https://github.com/simolus3/drift/tree/latest-release/examples/app) +- An [example specific to migrations](https://github.com/simolus3/drift/tree/latest-release/examples/migrations_example). +{% endblock %} + +#### Exporting the schema + +To begin, lets create the first schema representation: + +``` +$ mkdir drift_schemas +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ +``` + +This instructs the generator to look at the database defined in `lib/database/database.dart` and extract +its schema into the new folder. + +After making a change to your database schema, you can run the command again. For instance, let's say we +made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the +command again: + +``` +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ +``` + +You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`. + +Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your +database. +If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly: + +``` +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json +``` + +{% block "blocks/alert" title=' Dumping a database' color="success" %} +If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3 +database file, you can do that as well! `drift_dev schema dump` recognizes a sqlite3 database file as its first +argument and can extract the relevant schema from there. +{% endblock %} + +### Generating step-by-step migrations {#step-by-step} + +With all your database schemas exported into a folder, drift can generate code that makes it much +easier to write schema migrations "step-by-step" (incrementally from each version to the next one). + +This code is stored in a single-file, which you can generate like this: + +``` +$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart +``` + +The generated code contains a `stepByStep` method which you can use as a callback to the `onUpgrade` +parameter of your `MigrationStrategy`. +As an example, here is the [initial](#basics) migration shown at the top of this page, but rewritten using +the generated `stepByStep` function: + +{% include "blocks/snippet" snippets = snippets name = 'stepbystep' %} + +`stepByStep` expects a callback for each schema upgrade responsible for running the partial migration. +That callback receives two parameters: A migrator `m` (similar to the regular migrator you'd get for +`onUpgrade` callbacks) and a `schema` parameter that gives you access to the schema at the version you're +migrating to. +For instance, in the `from1To2` function, `schema` provides getters for the database schema at version 2. +The migrator passed to the function is also set up to consider that specific version by default. +A call to `m.recreateAllViews()` would re-create views at the expected state of schema version 2, for instance. + +### Writing tests + +After you've exported the database schemas into a folder, you can generate old versions of your database class +based on those schema files. +For verifications, drift will generate a much smaller database implementation that can only be used to +test migrations. + +You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`. +If we wanted to write them to `test/generated_migrations/`, we could use + +``` +$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/ +``` + +After that setup, it's finally time to write some tests! For instance, a test could look like this: + +```dart +import 'package:my_app/database/database.dart'; + +import 'package:test/test.dart'; +import 'package:drift_dev/api/migrations.dart'; + +// The generated directory from before. +import 'generated_migrations/schema.dart'; + +void main() { + late SchemaVerifier verifier; + + setUpAll(() { + // GeneratedHelper() was generated by drift, the verifier is an api + // provided by drift_dev. + verifier = SchemaVerifier(GeneratedHelper()); + }); + + test('upgrade from v1 to v2', () async { + // Use startAt(1) to obtain a database connection with all tables + // from the v1 schema. + final connection = await verifier.startAt(1); + final db = MyDatabase(connection); + + // Use this to run a migration to v2 and then validate that the + // database has the expected schema. + await verifier.migrateAndValidate(db, 2); + }); +} +``` + +In general, a test looks like this: + +1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class) + to a database with an initial schema. + This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`. +2. Create your application database with that connection - you can forward the `DatabaseConnection` to the + `GeneratedDatabase.connect()` constructor on the parent class for this. +3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`). + Unlike the database created by `startAt`, this uses the migration logic you wrote for your database. + +`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them. +If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test. + +{% block "blocks/alert" title="Writing testable migrations" %} +To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`), +you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`. +For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary. +{% endblock %} + +#### Verifying data integrity + +In addition to the changes made in your table structure, its useful to ensure that data that was present before a migration +is still there after it ran. +You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection. +This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there. + +Note that you can't use the regular database class from you app for this, since its data classes always expect the latest +schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose. +To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate` +command: + +``` +$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/ +``` + +Then, you can import the generated classes with an alias: + +```dart +import 'generated_migrations/schema_v1.dart' as v1; +import 'generated_migrations/schema_v2.dart' as v2; +``` + +This can then be used to manually create and verify data at a specific version: + +```dart +void main() { + // ... + test('upgrade from v1 to v2', () async { + final schema = await verifier.schemaAt(1); + + // Add some data to the users table, which only has an id column at v1 + final oldDb = v1.DatabaseAtV1.connect(schema.newConnection()); + await oldDb.into(oldDb.users).insert(const v1.UsersCompanion(id: Value(1))); + await oldDb.close(); + + // Run the migration and verify that it adds the name column. + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, 2); + await db.close(); + + // Make sure the user is still here + final migratedDb = v2.DatabaseAtV2.connect(schema.newConnection()); + final user = await migratedDb.select(migratedDb.users).getSingle(); + expect(user.id, 1); + expect(user.name, 'no name'); // default from the migration + await migratedDb.close(); + }); +} +``` + ## Complex migrations Sqlite has builtin statements for simple changes, like adding columns or dropping entire tables. @@ -235,208 +472,6 @@ the database file and will re-create it when installing the app again. You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/drift/issues/188#issuecomment-542682912) on how that can be achieved. -## Verifying migrations - -Drift contains **experimental** support to verify the integrity of your migrations in unit tests. - -To support this feature, drift can help you generate - -- a json representation of your database schema -- test databases operating on an older schema version - -By using those test databases, drift can help you test migrations from and to any schema version. - -{% block "blocks/alert" title="Complex topic ahead" %} -> Writing schema tests is an advanced topic that requires a fairly complex setup described here. - If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). - Also, there's a working example [in the drift repository](https://github.com/simolus3/drift/tree/latest-release/examples/migrations_example). -{% endblock %} - -### Setup - -To use this feature, drift needs to know all schemas of your database. A schema is the set of all tables, triggers -and indices that you use in your database. - -You can use the [CLI tools]({{ "../CLI.md" | pageUrl }}) to export a json representation of your schema. -In this guide, we'll assume a file layout like the following, where `my_app` is the root folder of your project: - -``` -my_app - .../ - lib/ - database/ - database.dart - database.g.dart - test/ - generated_migrations/ - schema.dart - schema_v1.dart - schema_v2.dart - drift_schemas/ - drift_schema_v1.json - drift_schema_v2.json - pubspec.yaml -``` - -The generated migrations implementation and the schema jsons will be generated by drift. -To start writing schemas, create an empty folder named `drift_schemas` in your project. -Of course, you can also choose a different name or use a nested subfolder if you want to. - -#### Exporting the schema - -To begin, let's create the first schema representation: - -``` -$ mkdir drift_schemas -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ -``` - -This instructs the generator to look at the database defined in `lib/database/database.dart` and extract -its schema into the new folder. - -After making a change to your database schema, you can run the command again. For instance, let's say we -made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the -command again: - -``` -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ -``` - -You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`. - -Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your -database. -If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly: - -``` -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json -``` - -{% block "blocks/alert" title=' Dumping a database' color="success" %} -If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3 -database file, you can do that as well! `drift_dev schema dump` recognizes a sqlite3 database file as its first -argument and can extract the relevant schema from there. -{% endblock %} - -#### Generating test code - -After you exported the database schema into a folder, you can generate old versions of your database class -based on those schema files. -For verifications, drift will generate a much smaller database implementation that can only be used to -test migrations. - -You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`. -If we wanted to write them to `test/generated_migrations/`, we could use - -``` -$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/ -``` - -### Writing tests - -After that setup, it's finally time to write some tests! For instance, a test could look like this: - -```dart -import 'package:my_app/database/database.dart'; - -import 'package:test/test.dart'; -import 'package:drift_dev/api/migrations.dart'; - -// The generated directory from before. -import 'generated_migrations/schema.dart'; - -void main() { - late SchemaVerifier verifier; - - setUpAll(() { - // GeneratedHelper() was generated by drift, the verifier is an api - // provided by drift_dev. - verifier = SchemaVerifier(GeneratedHelper()); - }); - - test('upgrade from v1 to v2', () async { - // Use startAt(1) to obtain a database connection with all tables - // from the v1 schema. - final connection = await verifier.startAt(1); - final db = MyDatabase.connect(connection); - - // Use this to run a migration to v2 and then validate that the - // database has the expected schema. - await verifier.migrateAndValidate(db, 2); - }); -} -``` - -In general, a test looks like this: - -1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class) - to a database with an initial schema. - This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`. -2. Create your application database with that connection - you can forward the `DatabaseConnection` to the - `GeneratedDatabase.connect()` constructor on the parent class for this. -3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`). - Unlike the database created by `startAt`, this uses the migration logic you wrote for your database. - -`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them. -If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test. - -{% block "blocks/alert" title="Writing testable migrations" %} -To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`), -you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`. -For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary. -{% endblock %} - -#### Verifying data integrity - -In addition to the changes made in your table structure, its useful to ensure that data that was present before a migration -is still there after it ran. -You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection. -This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there. - -Note that you can't use the regular database class from you app for this, since its data classes always expect the latest -schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose. -To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate` -command: - -``` -$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/ -``` - -Then, you can import the generated classes with an alias: - -```dart -import 'generated_migrations/schema_v1.dart' as v1; -import 'generated_migrations/schema_v2.dart' as v2; -``` - -This can then be used to manually create and verify data at a specific version: - -```dart -void main() { - // ... - test('upgrade from v1 to v2', () async { - final schema = await verifier.schemaAt(1); - - // Add some data to the users table, which only has an id column at v1 - final oldDb = v1.DatabaseAtV1.connect(schema.newConnection()); - await oldDb.into(oldDb.users).insert(const v1.UsersCompanion(id: Value(1))); - await oldDb.close(); - - // Run the migration and verify that it adds the name column. - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, 2); - await db.close(); - - // Make sure the user is still here - final migratedDb = v2.DatabaseAtV2.connect(schema.newConnection()); - final user = await migratedDb.select(migratedDb.users).getSingle(); - expect(user.id, 1); - expect(user.name, 'no name'); // default from the migration - await migratedDb.close(); - }); -} -``` - ## Verifying a database schema at runtime Instead (or in addition to) [writing tests](#verifying-migrations) to ensure your migrations work as they should, diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index 23bd7006..e18d425b 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.10.0 + +- Adds the `schema steps` command to `drift_dev`. It generates an API making it + easier to write safe schema migrations ([docs](https://drift.simonbinder.eu/docs/advanced-features/migrations/#step-by-step)). + ## 2.9.0 - Forbid `schemaVersion` returning `0`, as this causes issues in the migrator. diff --git a/drift/lib/internal/versioned_schema.dart b/drift/lib/internal/versioned_schema.dart new file mode 100644 index 00000000..70685c5e --- /dev/null +++ b/drift/lib/internal/versioned_schema.dart @@ -0,0 +1,219 @@ +/// Defines base classes to generate lightweight tables and views. This library +/// is used by code generated via `drift_dev schema steps` to generate snapshots +/// of every schema version of your database without much overhead. +/// +/// For more information on how to use that feature, see +/// https://drift.simonbinder.eu/docs/advanced-features/migrations/#step-by-step +/// +/// __Warning:__ This library is not meant to be imported into user-written +/// code, and classes defined in this library are not part of drift's stable +/// API. +library; + +import 'package:drift/drift.dart'; + +/// A snapshot of a database schema at a previous version. +/// +/// This class is meant to be extended by generated code. +abstract base class VersionedSchema { + /// The generated database instance, used to create [TableInfo] instances. + final DatabaseConnectionUser database; + + /// The [GeneratedDatabase.schemaVersion] at the time this schema was active. + final int version; + + /// Default constructor taking the database and the schema version. + VersionedSchema({required this.database, required this.version}); + + /// All drift schema entities at the time of the set [version]. + Iterable get entities; +} + +/// A drift table implementation that, instead of being generated, is constructed +/// from individual fields +/// +/// This allows the code generated for step-by-step migrations to be a lot +/// smaller than the code typically generated by drift. Features like type +/// converters or information about unique/primary keys are not present in these +/// tables. +class VersionedTable extends Table with TableInfo { + @override + final String entityName; + final String? _alias; + @override + final bool isStrict; + + @override + final bool withoutRowId; + + @override + final DatabaseConnectionUser attachedDatabase; + + @override + final List $columns; + + /// List of columns, represented as a function that returns the generated + /// column when given the resolved table name. + final List _columnFactories; + + @override + final List customConstraints; + + /// Create a table from the individual fields. + /// + /// [columns] is a list of functions returning a [GeneratedColumn] when given + /// the alias (or original name) of this table. + VersionedTable({ + required this.entityName, + required this.isStrict, + required this.withoutRowId, + required this.attachedDatabase, + required List columns, + required List tableConstraints, + String? alias, + }) : _columnFactories = columns, + customConstraints = tableConstraints, + $columns = [for (final column in columns) column(alias ?? entityName)], + _alias = alias; + + /// Create a table by copying fields from [source] and applying an [alias]. + VersionedTable.aliased({ + required VersionedTable source, + required String? alias, + }) : entityName = source.entityName, + isStrict = source.isStrict, + withoutRowId = source.withoutRowId, + attachedDatabase = source.attachedDatabase, + customConstraints = source.customConstraints, + _columnFactories = source._columnFactories, + $columns = [ + for (final column in source._columnFactories) + column(alias ?? source.entityName) + ], + _alias = alias; + + @override + String get actualTableName => entityName; + + @override + String get aliasedName => _alias ?? entityName; + + @override + bool get dontWriteConstraints => true; + + @override + QueryRow map(Map data, {String? tablePrefix}) { + return QueryRow(data, attachedDatabase); + } + + @override + VersionedTable createAlias(String alias) { + return VersionedTable.aliased(source: this, alias: alias); + } +} + +/// The version of [VersionedTable] for virtual tables. +class VersionedVirtualTable extends VersionedTable + with VirtualTableInfo { + @override + final String moduleAndArgs; + + /// Create a small virtual table from the individual fields. + VersionedVirtualTable({ + required super.entityName, + required super.attachedDatabase, + required super.columns, + required this.moduleAndArgs, + super.alias, + }) : super( + isStrict: false, + withoutRowId: false, + tableConstraints: [], + ); + + /// Create a virtual table by copying fields from [source] and applying a + /// [alias] to columns. + VersionedVirtualTable.aliased( + {required VersionedVirtualTable source, required String? alias}) + : moduleAndArgs = source.moduleAndArgs, + super.aliased(source: source, alias: alias); + + @override + VersionedVirtualTable createAlias(String alias) { + return VersionedVirtualTable.aliased( + source: this, + alias: alias, + ); + } +} + +/// A constructed from individual fields instead of being generated with a +/// dedicated class. +class VersionedView implements ViewInfo, HasResultSet { + @override + final String entityName; + final String? _alias; + + @override + final String createViewStmt; + + @override + final List $columns; + + @override + late final Map columnsByName = { + for (final column in $columns) column.name: column, + }; + + /// List of columns, represented as a function that returns the generated + /// column when given the resolved table name. + final List _columnFactories; + + @override + final DatabaseConnectionUser attachedDatabase; + + /// Create a view from the individual fields on [ViewInfo]. + VersionedView({ + required this.entityName, + required this.attachedDatabase, + required this.createViewStmt, + required List columns, + String? alias, + }) : _columnFactories = columns, + $columns = [for (final column in columns) column(alias ?? entityName)], + _alias = alias; + + /// Copy an alias to a [source] view. + VersionedView.aliased({required VersionedView source, required String? alias}) + : entityName = source.entityName, + attachedDatabase = source.attachedDatabase, + createViewStmt = source.createViewStmt, + _columnFactories = source._columnFactories, + $columns = [ + for (final column in source._columnFactories) + column(alias ?? source.entityName) + ], + _alias = alias; + + @override + String get aliasedName => _alias ?? entityName; + + @override + HasResultSet get asDslTable => this; + + @override + VersionedView createAlias(String alias) { + return VersionedView.aliased(source: this, alias: alias); + } + + @override + QueryRow map(Map data, {String? tablePrefix}) { + return QueryRow(data, attachedDatabase); + } + + @override + Query? get query => null; + + @override + Set get readTables => const {}; +} diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 8d2124e9..46513dd9 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -49,14 +49,22 @@ class MigrationStrategy { /// Runs migrations declared by a [MigrationStrategy]. class Migrator { final GeneratedDatabase _db; + final VersionedSchema? _fixedVersion; /// Used internally by drift when opening the database. - Migrator(this._db); + Migrator(this._db, [this._fixedVersion]); + + Iterable get _allSchemaEntities { + return switch (_fixedVersion) { + null => _db.allSchemaEntities, + var fixed => fixed.entities, + }; + } /// Creates all tables specified for the database, if they don't exist @Deprecated('Use createAll() instead') Future createAllTables() async { - for (final table in _db.allTables) { + for (final table in _allSchemaEntities.whereType()) { await createTable(table); } } @@ -64,7 +72,7 @@ class Migrator { /// Creates all tables, triggers, views, indexes and everything else defined /// in the database, if they don't exist. Future createAll() async { - for (final entity in _db.allSchemaEntities) { + for (final entity in _allSchemaEntities) { await create(entity); } } @@ -94,7 +102,7 @@ class Migrator { /// a view reads from may also warrant re-creating the view to make sure it's /// still valid. Future recreateAllViews() async { - for (final entity in _db.allSchemaEntities) { + for (final entity in _allSchemaEntities) { if (entity is ViewInfo) { await drop(entity); await createView(entity); @@ -119,7 +127,7 @@ class Migrator { return _issueCustomQuery(context.sql, context.boundVariables); } - /// Experimental utility method to alter columns of an existing table. + /// Alter columns of an existing tabe. /// /// Since sqlite does not provide a way to alter the type or constraint of an /// individual column, one needs to write a fairly complex migration procedure @@ -141,7 +149,6 @@ class Migrator { /// /// [other alter]: https://www.sqlite.org/lang_altertable.html#otheralter /// [drift docs]: https://drift.simonbinder.eu/docs/advanced-features/migrations/#complex-migrations - @experimental Future alterTable(TableMigration migration) async { final foreignKeysEnabled = (await _db.customSelect('PRAGMA foreign_keys').getSingle()) @@ -474,6 +481,31 @@ class Migrator { Future _issueCustomQuery(String sql, [List? args]) { return _db.customStatement(sql, args); } + + /// A helper used by drift internally to implement the [step-by-step](https://drift.simonbinder.eu/docs/advanced-features/migrations/#step-by-step) + /// migration feature. + /// + /// This method implements an [OnUpgrade] callback by repeatedly invoking + /// [step] with the current version, assuming that [step] will perform an + /// upgrade from that version to the version returned by the callback. + @experimental + static OnUpgrade stepByStepHelper({ + required Future Function( + int currentVersion, + GeneratedDatabase database, + ) step, + }) { + return (m, from, to) async { + final database = m._db; + + for (var target = from; target < to;) { + final newVersion = await step(target, database); + assert(newVersion > target); + + target = newVersion; + } + }; + } } /// Provides information about whether migrations ran before opening the diff --git a/drift/lib/src/runtime/query_builder/query_builder.dart b/drift/lib/src/runtime/query_builder/query_builder.dart index 2150aa1a..69c8f77e 100644 --- a/drift/lib/src/runtime/query_builder/query_builder.dart +++ b/drift/lib/src/runtime/query_builder/query_builder.dart @@ -6,6 +6,7 @@ import 'dart:collection'; import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:drift/internal/versioned_schema.dart'; import 'package:drift/src/dsl/dsl.dart'; import 'package:drift/src/runtime/api/options.dart'; import 'package:drift/src/runtime/api/runtime_api.dart'; diff --git a/drift/test/database/statements/schema_test.dart b/drift/test/database/statements/schema_test.dart index 3a78ca6e..683330a9 100644 --- a/drift/test/database/statements/schema_test.dart +++ b/drift/test/database/statements/schema_test.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:drift/internal/versioned_schema.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -211,6 +212,78 @@ void main() { // https://github.com/simolus3/drift/discussions/1936 verify(executor.runCustom(argThat(contains('CHECK("foo" < 3)')), [])); }); + + group('respects schema version', () { + late MockExecutor executor; + late _DefaultDb db; + + setUp(() async { + executor = MockExecutor(); + db = _DefaultDb(executor); + }); + + tearDown(() { + db.close(); + }); + + test('in createAll', () async { + final defaultMigrator = db.createMigrator(); + await defaultMigrator.createAll(); + verifyNever(executor.runCustom(any)); + + final fixedMigrator = + Migrator(db, _FakeSchemaVersion(database: db, version: 2)); + await fixedMigrator.createAll(); + verify(executor.runCustom( + 'CREATE TABLE IF NOT EXISTS "my_table" ("foo" INTEGER NOT NULL);', + [], + )); + verify(executor.runCustom( + 'CREATE VIEW my_view AS SELECT 2', + [], + )); + }); + + test('in recreateViews', () async { + final defaultMigrator = db.createMigrator(); + await defaultMigrator.recreateAllViews(); + verifyNever(executor.runCustom(any)); + + final fixedMigrator = + Migrator(db, _FakeSchemaVersion(database: db, version: 2)); + await fixedMigrator.recreateAllViews(); + + verify(executor.runCustom( + 'CREATE VIEW my_view AS SELECT 2', + [], + )); + }); + }); +} + +final class _FakeSchemaVersion extends VersionedSchema { + _FakeSchemaVersion({required super.database, required super.version}); + + @override + Iterable get entities => [ + VersionedTable( + entityName: 'my_table', + attachedDatabase: database, + columns: [ + (name) => GeneratedColumn('foo', name, false, + type: DriftSqlType.int), + ], + tableConstraints: [], + isStrict: false, + withoutRowId: false, + ), + VersionedView( + entityName: 'my_view', + attachedDatabase: database, + createViewStmt: 'CREATE VIEW my_view AS SELECT $version', + columns: [], + ), + ]; } class _DefaultDb extends GeneratedDatabase { @@ -219,6 +292,9 @@ class _DefaultDb extends GeneratedDatabase { @override List> get allTables => []; + @override + Iterable get allSchemaEntities => []; + @override int get schemaVersion => 2; } diff --git a/drift_dev/lib/src/analysis/results/table.dart b/drift_dev/lib/src/analysis/results/table.dart index 5aef07de..fcd58f63 100644 --- a/drift_dev/lib/src/analysis/results/table.dart +++ b/drift_dev/lib/src/analysis/results/table.dart @@ -49,7 +49,7 @@ class DriftTable extends DriftElementWithResultSet { /// when creating the `CREATE TABLE` statement at runtime. final bool writeDefaultConstraints; - /// When non-null, the generated table class will override the + /// When non-empty, the generated table class will override the /// `customConstraints` getter in the table class with this value. final List overrideTableConstraints; @@ -169,21 +169,21 @@ class DriftTable extends DriftElementWithResultSet { '\$${className}Table'; } -abstract class DriftTableConstraint {} +sealed class DriftTableConstraint {} -class UniqueColumns extends DriftTableConstraint { +final class UniqueColumns extends DriftTableConstraint { final Set uniqueSet; UniqueColumns(this.uniqueSet); } -class PrimaryKeyColumns extends DriftTableConstraint { +final class PrimaryKeyColumns extends DriftTableConstraint { final Set primaryKey; PrimaryKeyColumns(this.primaryKey); } -class ForeignKeyTable extends DriftTableConstraint { +final class ForeignKeyTable extends DriftTableConstraint { final List localColumns; final DriftTable otherTable; @@ -214,6 +214,12 @@ class VirtualTableData { final RecognizedVirtualTableModule? recognized; + /// The module and the arguments in a single string, suitable for `CREATE + /// VIRTUAL TABLE` statements. + String get moduleAndArgs { + return '$module(${moduleArguments.join(', ')})'; + } + VirtualTableData(this.module, this.moduleArguments, this.recognized); } diff --git a/drift_dev/lib/src/cli/cli.dart b/drift_dev/lib/src/cli/cli.dart index 3b281f4f..b41e4930 100644 --- a/drift_dev/lib/src/cli/cli.dart +++ b/drift_dev/lib/src/cli/cli.dart @@ -13,7 +13,7 @@ import 'commands/schema.dart'; import 'logging.dart'; Future run(List args) async { - final cli = MoorCli(); + final cli = DriftDevCli(); try { return await cli.run(args); } on UsageException catch (e) { @@ -21,14 +21,14 @@ Future run(List args) async { } } -class MoorCli { +class DriftDevCli { Logger get logger => Logger.root; late final CommandRunner _runner; late final MoorProject project; bool verbose = false; - MoorCli() { + DriftDevCli() { _runner = CommandRunner( 'dart run drift_dev', 'CLI utilities for the drift package, currently in an experimental state.', @@ -72,7 +72,7 @@ class MoorCli { } abstract class MoorCommand extends Command { - final MoorCli cli; + final DriftDevCli cli; MoorCommand(this.cli); } diff --git a/drift_dev/lib/src/cli/commands/analyze.dart b/drift_dev/lib/src/cli/commands/analyze.dart index 21f38d50..7aef845b 100644 --- a/drift_dev/lib/src/cli/commands/analyze.dart +++ b/drift_dev/lib/src/cli/commands/analyze.dart @@ -3,7 +3,7 @@ import 'dart:io'; import '../cli.dart'; class AnalyzeCommand extends MoorCommand { - AnalyzeCommand(MoorCli cli) : super(cli); + AnalyzeCommand(DriftDevCli cli) : super(cli); @override String get description => 'Analyze and lint drift database code'; diff --git a/drift_dev/lib/src/cli/commands/identify_databases.dart b/drift_dev/lib/src/cli/commands/identify_databases.dart index 82850221..065e31f9 100644 --- a/drift_dev/lib/src/cli/commands/identify_databases.dart +++ b/drift_dev/lib/src/cli/commands/identify_databases.dart @@ -7,7 +7,7 @@ import '../../analysis/results/results.dart'; import '../cli.dart'; class IdentifyDatabases extends MoorCommand { - IdentifyDatabases(MoorCli cli) : super(cli); + IdentifyDatabases(DriftDevCli cli) : super(cli); @override String get description => diff --git a/drift_dev/lib/src/cli/commands/migrate.dart b/drift_dev/lib/src/cli/commands/migrate.dart index 0a9bc19f..d7c2834c 100644 --- a/drift_dev/lib/src/cli/commands/migrate.dart +++ b/drift_dev/lib/src/cli/commands/migrate.dart @@ -23,7 +23,7 @@ class MigrateCommand extends MoorCommand { late final AnalysisContext context; - MigrateCommand(MoorCli cli) : super(cli); + MigrateCommand(DriftDevCli cli) : super(cli); @override String get description => 'Migrate a project from moor to drift'; diff --git a/drift_dev/lib/src/cli/commands/schema.dart b/drift_dev/lib/src/cli/commands/schema.dart index a190df2c..7454729b 100644 --- a/drift_dev/lib/src/cli/commands/schema.dart +++ b/drift_dev/lib/src/cli/commands/schema.dart @@ -1,9 +1,16 @@ -import 'package:args/command_runner.dart'; +import 'dart:convert'; +import 'dart:io'; +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as p; + +import '../../analysis/results/results.dart'; +import '../../services/schema/schema_files.dart'; import 'schema/dump.dart'; import 'schema/generate_utils.dart'; import '../cli.dart'; +import 'schema/steps.dart'; class SchemaCommand extends Command { @override @@ -12,8 +19,37 @@ class SchemaCommand extends Command { @override String get name => 'schema'; - SchemaCommand(MoorCli cli) { + SchemaCommand(DriftDevCli cli) { addSubcommand(DumpSchemaCommand(cli)); addSubcommand(GenerateUtilsCommand(cli)); + addSubcommand(WriteVersions(cli)); } } + +class ExportedSchema { + final List schema; + final Map options; + + ExportedSchema(this.schema, this.options); +} + +final _filenames = RegExp(r'(?:moor|drift)_schema_v(\d+)\.json'); + +Future> parseSchema(Directory directory) async { + final results = {}; + + await for (final entity in directory.list()) { + final basename = p.basename(entity.path); + final match = _filenames.firstMatch(basename); + + if (match == null || entity is! File) continue; + + final version = int.parse(match.group(1)!); + final rawData = json.decode(await entity.readAsString()); + + final schema = SchemaReader.readJson(rawData as Map); + results[version] = ExportedSchema(schema.entities.toList(), schema.options); + } + + return results; +} diff --git a/drift_dev/lib/src/cli/commands/schema/dump.dart b/drift_dev/lib/src/cli/commands/schema/dump.dart index 81112139..feedd5d4 100644 --- a/drift_dev/lib/src/cli/commands/schema/dump.dart +++ b/drift_dev/lib/src/cli/commands/schema/dump.dart @@ -24,7 +24,7 @@ class DumpSchemaCommand extends Command { return '${runner!.executableName} schema dump [arguments] '; } - final MoorCli cli; + final DriftDevCli cli; DumpSchemaCommand(this.cli) { argParser.addSeparator("It's recommended to run this commend from the " diff --git a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart index e38bdf2d..bd3de60d 100644 --- a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart +++ b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; @@ -13,9 +12,10 @@ import '../../../writer/database_writer.dart'; import '../../../writer/import_manager.dart'; import '../../../writer/writer.dart'; import '../../cli.dart'; +import '../schema.dart'; class GenerateUtilsCommand extends Command { - final MoorCli cli; + final DriftDevCli cli; GenerateUtilsCommand(this.cli) { argParser.addFlag( @@ -61,7 +61,7 @@ class GenerateUtilsCommand extends Command { await outputDir.create(); } - final schema = await _parseSchema(inputDir); + final schema = await parseSchema(inputDir); for (final versionAndEntities in schema.entries) { final version = versionAndEntities.key; final entities = versionAndEntities.value; @@ -81,30 +81,10 @@ class GenerateUtilsCommand extends Command { 'Wrote ${schema.length + 1} files into ${p.relative(outputDir.path)}'); } - Future> _parseSchema(Directory directory) async { - final results = {}; - - await for (final entity in directory.list()) { - final basename = p.basename(entity.path); - final match = _filenames.firstMatch(basename); - - if (match == null || entity is! File) continue; - - final version = int.parse(match.group(1)!); - final rawData = json.decode(await entity.readAsString()); - - final schema = SchemaReader.readJson(rawData as Map); - results[version] = - _ExportedSchema(schema.entities.toList(), schema.options); - } - - return results; - } - Future _writeSchemaFile( Directory output, int version, - _ExportedSchema schema, + ExportedSchema schema, bool dataClasses, bool companions, ) { @@ -184,15 +164,7 @@ class GenerateUtilsCommand extends Command { String _filenameForVersion(int version) => 'schema_v$version.dart'; - static final _filenames = RegExp(r'(?:moor|drift)_schema_v(\d+)\.json'); static final _dartfmt = DartFormatter(); static const _prefix = '// GENERATED CODE, DO NOT EDIT BY HAND.\n' '// ignore_for_file: type=lint'; } - -class _ExportedSchema { - final List schema; - final Map options; - - _ExportedSchema(this.schema, this.options); -} diff --git a/drift_dev/lib/src/cli/commands/schema/steps.dart b/drift_dev/lib/src/cli/commands/schema/steps.dart new file mode 100644 index 00000000..5bfec0ba --- /dev/null +++ b/drift_dev/lib/src/cli/commands/schema/steps.dart @@ -0,0 +1,81 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:collection/collection.dart'; +import 'package:dart_style/dart_style.dart'; + +import '../../../analysis/options.dart'; +import '../../../analysis/results/element.dart'; +import '../../../writer/import_manager.dart'; +import '../../../writer/schema_version_writer.dart'; +import '../../../writer/writer.dart'; +import '../../cli.dart'; +import '../schema.dart'; + +class WriteVersions extends Command { + final DriftDevCli cli; + + WriteVersions(this.cli); + + @override + String get name => 'steps'; + + @override + String get description => + 'Write a Dart file helping with incremental migrations between schema versions.'; + + @override + String get invocation { + return '${runner!.executableName} schema steps '; + } + + @override + Future run() async { + final rest = argResults!.rest; + if (rest.length != 2) { + usageException('Expected input and output directories'); + } + + final inputDirectory = Directory(rest[0]); + final outputFile = File(rest[1]); + final outputDirectory = outputFile.parent; + + if (!await inputDirectory.exists()) { + cli.exit('The provided input directory does not exist.'); + } + + if (!await outputDirectory.exists()) { + await outputDirectory.create(); + } + + final imports = LibraryImportManager(); + final writer = Writer( + const DriftOptions.defaults(), + generationOptions: GenerationOptions(imports: imports), + ); + imports.linkToWriter(writer); + + final schema = await parseSchema(inputDirectory); + final byVersion = [ + for (final MapEntry(key: version, value: schema) in schema.entries) + SchemaVersion( + version, + schema.schema.whereType().toList(), + schema.options, + ), + ]; + byVersion.sortBy((s) => s.version); + + writer.leaf().write("import 'package:drift/drift.dart';"); + SchemaVersionWriter(byVersion, writer.child()).write(); + + var code = writer.writeGenerated(); + try { + code = DartFormatter().format(code); + } on FormatterException { + // Ignore. Probably a bug in drift_dev, the user will notice. + } + + await outputFile.writeAsString(code); + } +} diff --git a/drift_dev/lib/src/writer/schema_version_writer.dart b/drift_dev/lib/src/writer/schema_version_writer.dart new file mode 100644 index 00000000..ed696783 --- /dev/null +++ b/drift_dev/lib/src/writer/schema_version_writer.dart @@ -0,0 +1,398 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart' show DriftSqlType; +import 'package:sqlparser/sqlparser.dart' as sql; +import 'package:sqlparser/utils/node_to_text.dart'; + +import '../analysis/results/results.dart'; +import '../utils/string_escaper.dart'; +import 'tables/table_writer.dart'; +import 'writer.dart'; + +class SchemaVersion { + final int version; + final List schema; + final Map options; + + SchemaVersion(this.version, this.schema, this.options); +} + +enum _ResultSetKind { + table, + virtualTable, + view, +} + +final class _TableShape { + final _ResultSetKind kind; + + // Map from Dart getter names to column names in SQL and the SQL type. + final Map columnTypes; + + _TableShape(this.kind, this.columnTypes); + + @override + int get hashCode => Object.hash(kind, _equality.hash(columnTypes)); + + @override + bool operator ==(Object other) { + return other is _TableShape && + other.kind == kind && + _equality.equals(other.columnTypes, columnTypes); + } + + static const _equality = MapEquality(); + + static Map columnsFrom( + DriftElementWithResultSet e) { + return { + for (final column in e.columns) + column.nameInDart: (column.nameInSql, column.sqlType), + }; + } +} + +/// A writer that writes schema code for all schema versions known to us. +/// +/// While other tools to generate code for a specific schema version exists +/// (we use it to generate test code), the generated code is very large since +/// it can contain data classes and all information known to drift. +/// Code generated by this writer is optimized to be compact by hiding table +/// metadata not strictly necessary for migrations and by re-using column +/// definitions where possible. +class SchemaVersionWriter { + static final Uri _schemaLibrary = + Uri.parse('package:drift/internal/versioned_schema.dart'); + + /// All schema versions, sorted by [SchemaVersion.version]. + final List versions; + final Scope libraryScope; + + final Map _columnCodeToFactory = {}; + final Map<_TableShape, String> _shapes = {}; + + SchemaVersionWriter(this.versions, this.libraryScope) { + assert(versions.isSortedBy((element) => element.version)); + } + + /// Since not every column changes in every schema version, we prefer to re-use + /// columns with an identical definition across tables and schema versions. + /// + /// We do this by generating a method constructing the column which can be + /// called in different places. This method looks up or creates a method for + /// the given [column], returning it if doesn't exist. + String _referenceColumn(DriftColumn column) { + final text = libraryScope.leaf(); + final (type, code) = TableOrViewWriter.instantiateColumn(column, text); + + return _columnCodeToFactory.putIfAbsent(code, () { + final methodName = '_column_${_columnCodeToFactory.length}'; + text.writeln('$type $methodName(String aliasedName) => $code;'); + return methodName; + }); + } + + void _writeColumnsArgument(List columns, TextEmitter writer) { + writer.write('columns: ['); + + for (final column in columns) { + writer + ..write(_referenceColumn(column)) + ..write(','); + } + + writer.write('],'); + } + + /// Finds a class to use for [resultSet]. + /// + /// When only minor details like column or table constraints change, we don't + /// want to introduce a new class. The interface of a class is only determined + /// by its kind (since we need to subclass from VersionedTable, + /// VersionedVirtualTable or VersionedView) and its public getters used to + /// access columns. + /// + /// This looks up a suitable class for the existing [resultSet] or creates a + /// new one, returning its name. + String _shapeClass(DriftElementWithResultSet resultSet) { + final (kind, superclass) = switch (resultSet) { + DriftTable(virtualTableData: null) => ( + _ResultSetKind.table, + 'VersionedTable' + ), + DriftTable() => (_ResultSetKind.virtualTable, 'VersionedVirtualTable'), + DriftView() => (_ResultSetKind.view, 'VersionedView'), + _ => throw ArgumentError.value(resultSet, 'resultSet', 'Unknown type'), + }; + + final shape = _TableShape(kind, _TableShape.columnsFrom(resultSet)); + return _shapes.putIfAbsent(shape, () { + final className = 'Shape${_shapes.length}'; + final classWriter = libraryScope.leaf(); + + classWriter + ..write('class $className extends ') + ..writeUriRef(_schemaLibrary, superclass) + ..writeln('{') + ..writeln( + '$className({required super.source, required super.alias}) : super.aliased();'); + + for (final MapEntry(key: getterName, value: (sqlName, type)) + in shape.columnTypes.entries) { + final columnType = AnnotatedDartCode([dartTypeNames[type]!]); + + classWriter + ..writeDriftRef('GeneratedColumn<') + ..writeDart(columnType) + ..write('> get ') + ..write(getterName) + ..write(' => columnsByName[${asDartLiteral(sqlName)}]! as ') + ..writeDriftRef('GeneratedColumn<') + ..writeDart(columnType) + ..writeln('>;'); + } + + classWriter.writeln('}'); + + return className; + }); + } + + String _writeWithResultSet( + DriftElementWithResultSet entity, TextEmitter writer) { + final getterName = entity.dbGetterName; + final shape = _shapeClass(entity); + writer + ..write('late final $shape $getterName = ') + ..write('$shape(source: '); + + switch (entity) { + case DriftTable(): + if (entity.isVirtual) { + final info = entity.virtualTableData!; + + writer + ..writeUriRef(_schemaLibrary, 'VersionedVirtualTable(') + ..write('entityName: ${asDartLiteral(entity.schemaName)},') + ..write('moduleAndArgs: ${asDartLiteral(info.moduleAndArgs)},'); + } else { + final tableConstraints = []; + + if (entity.writeDefaultConstraints) { + // We don't override primaryKey and uniqueKey in generated table + // classes to keep the code shorter. The migrator would use those + // getters to generate SQL at runtime, which means that this burden + // now falls onto the generator. + for (final constraint in entity.tableConstraints) { + final astNode = switch (constraint) { + PrimaryKeyColumns(primaryKey: var columns) => sql.KeyClause( + null, + isPrimaryKey: true, + columns: [ + for (final column in columns) + sql.IndexedColumn( + sql.Reference(columnName: column.nameInSql)) + ], + ), + UniqueColumns(uniqueSet: var columns) => sql.KeyClause( + null, + isPrimaryKey: false, + columns: [ + for (final column in columns) + sql.IndexedColumn( + sql.Reference(columnName: column.nameInSql)) + ], + ), + ForeignKeyTable() => sql.ForeignKeyTableConstraint( + null, + columns: [ + for (final column in constraint.localColumns) + sql.Reference(columnName: column.nameInSql) + ], + clause: sql.ForeignKeyClause( + foreignTable: + sql.TableReference(constraint.otherTable.schemaName), + columnNames: [ + for (final column in constraint.otherColumns) + sql.Reference(columnName: column.nameInSql) + ], + onUpdate: constraint.onUpdate, + onDelete: constraint.onDelete, + ), + ), + }; + + tableConstraints.add(astNode.toSql()); + } + } + tableConstraints.addAll(entity.overrideTableConstraints.toList()); + + writer + ..writeUriRef(_schemaLibrary, 'VersionedTable(') + ..write('entityName: ${asDartLiteral(entity.schemaName)},') + ..write('withoutRowId: ${entity.withoutRowId},') + ..write('isStrict: ${entity.strict},') + ..write('tableConstraints: ['); + + for (final constraint in tableConstraints) { + writer + ..write(asDartLiteral(constraint)) + ..write(','); + } + + writer.write('],'); + } + break; + case DriftView(): + final source = entity.source as SqlViewSource; + + writer + ..writeUriRef(_schemaLibrary, 'VersionedView(') + ..write('entityName: ${asDartLiteral(entity.schemaName)},') + ..write( + 'createViewStmt: ${asDartLiteral(source.sqlCreateViewStmt)},'); + + break; + } + + _writeColumnsArgument(entity.columns, writer); + writer.write('attachedDatabase: database,'); + writer.write('), alias: null)'); + + return getterName!; + } + + String _writeEntity({ + required DriftSchemaElement element, + required TextEmitter definition, + }) { + String name; + + if (element is DriftElementWithResultSet) { + name = _writeWithResultSet(element, definition); + } else if (element is DriftIndex) { + name = element.dbGetterName; + final index = definition.drift('Index'); + + definition + ..write('final $index $name = $index(') + ..write(asDartLiteral(element.schemaName)) + ..write(',') + ..write(asDartLiteral(element.createStmt)) + ..write(')'); + } else if (element is DriftTrigger) { + name = element.dbGetterName; + final trigger = definition.drift('Trigger'); + + definition + ..write('final $trigger $name = $trigger(') + ..write(asDartLiteral(element.createStmt)) + ..write(',') + ..write(asDartLiteral(element.schemaName)) + ..write(')'); + } else { + throw ArgumentError('Unhandled element type $element'); + } + + definition.write(';'); + return name; + } + + void write() { + libraryScope.leaf() + ..writeln('// ignore_for_file: type=lint,unused_import') + ..writeln('// GENERATED BY drift_dev, DO NOT MODIFY.'); + + // There is no need to generate schema classes for the first version, we + // only need them for versions targeted by migrations. + for (final version in versions.skip(1)) { + final versionNo = version.version; + final versionClass = '_S$versionNo'; + final versionScope = libraryScope.child(); + + // Write an _S class for each schema version x. + versionScope.leaf() + ..write('final class $versionClass extends ') + ..writeUriRef(_schemaLibrary, 'VersionedSchema') + ..writeln('{') + ..writeln('$versionClass({required super.database}): ' + 'super(version: $versionNo);'); + + // Override the allEntities getters by VersionedSchema + final allEntitiesWriter = versionScope.leaf() + ..write('@override late final ') + ..writeUriRef(AnnotatedDartCode.dartCore, 'List') + ..write('<') + ..writeDriftRef('DatabaseSchemaEntity') + ..write('> entities = ['); + + for (final entity in version.schema) { + // Creata field for the entity and include it in the list + final fieldName = + _writeEntity(element: entity, definition: versionScope.leaf()); + + allEntitiesWriter.write('$fieldName,'); + } + + allEntitiesWriter.write('];'); + versionScope.leaf().writeln('}'); + } + + // Write a stepByStep migration function that takes a callback doing a step + // for each schema to the next. We supply a special migrator that only + // considers entities from that version, as well as a typed reference to the + // _S class used to lookup elements. + final stepByStep = libraryScope.leaf() + ..writeDriftRef('OnUpgrade') + ..write(' stepByStep({'); + + for (final (current, next) in versions.withNext) { + stepByStep + ..write('required Future Function(') + ..writeDriftRef('Migrator') + ..write(' m, _S${next.version} schema)') + ..writeln('from${current.version}To${next.version},'); + } + + stepByStep + ..writeln('}) {') + ..write('return ') + ..writeDriftRef('Migrator') + ..writeln('.stepByStepHelper(step: (currentVersion, database) async {') + ..writeln('switch (currentVersion) {'); + + for (final (current, next) in versions.withNext) { + stepByStep + ..writeln('case ${current.version}:') + ..write('final schema = _S${next.version}(database: database);') + ..write('final migrator = ') + ..writeDriftRef('Migrator') + ..writeln('(database, schema);') + ..writeln( + 'await from${current.version}To${next.version}(migrator, schema);') + ..writeln('return ${next.version};'); + } + + stepByStep + ..writeln( + r"default: throw ArgumentError.value('Unknown migration from $currentVersion');") + ..writeln('}') // End of switch + ..writeln('}') // End of stepByStepHelper function + ..writeln(');') // End of stepByStepHelper call + ..writeln('}'); // End of method + } +} + +extension on Iterable { + Iterable<(T, T)> get withNext sync* { + final iterator = this.iterator; + if (!iterator.moveNext()) return; + + var a = iterator.current; + while (iterator.moveNext()) { + var b = iterator.current; + yield (a, b); + + a = b; + } + } +} diff --git a/drift_dev/lib/src/writer/tables/table_writer.dart b/drift_dev/lib/src/writer/tables/table_writer.dart index d967f6af..8f05a103 100644 --- a/drift_dev/lib/src/writer/tables/table_writer.dart +++ b/drift_dev/lib/src/writer/tables/table_writer.dart @@ -16,140 +16,24 @@ abstract class TableOrViewWriter { StringBuffer get buffer => emitter.buffer; void writeColumnGetter(DriftColumn column, bool isOverride) { - final isNullable = column.nullable; - final additionalParams = {}; - final expressionBuffer = StringBuffer(); - final constraints = defaultConstraints(column); - - for (final constraint in column.constraints) { - if (constraint is LimitingTextLength) { - final buffer = - StringBuffer(emitter.drift('GeneratedColumn.checkTextLength(')); - - if (constraint.minLength != null) { - buffer.write('minTextLength: ${constraint.minLength},'); - } - if (constraint.maxLength != null) { - buffer.write('maxTextLength: ${constraint.maxLength}'); - } - buffer.write(')'); - - additionalParams['additionalChecks'] = buffer.toString(); - } - - if (constraint is DartCheckExpression) { - final dartCheck = emitter.dartCode(constraint.dartExpression); - additionalParams['check'] = '() => $dartCheck'; - } - - if (constraint is ColumnGeneratedAs) { - final dartCode = emitter.dartCode(constraint.dartExpression); - - additionalParams['generatedAs'] = - '${emitter.drift('GeneratedAs')}($dartCode, ${constraint.stored})'; - } - - if (constraint is PrimaryKeyColumn && constraint.isAutoIncrement) { - additionalParams['hasAutoIncrement'] = 'true'; - } - } - - additionalParams['type'] = emitter.drift(column.sqlType.toString()); + bool? isRequiredForInsert; if (tableOrView is DriftTable) { - additionalParams['requiredDuringInsert'] = (tableOrView as DriftTable) - .isColumnRequiredForInsert(column) - .toString(); + isRequiredForInsert = + (tableOrView as DriftTable).isColumnRequiredForInsert(column); } - if (column.customConstraints != null) { - additionalParams['\$customConstraints'] = - asDartLiteral(column.customConstraints!); - } else if (constraints.values.any((constraint) => constraint.isNotEmpty)) { - // Use the default constraints supported by drift - - if (constraints.values.any( - (value) => value != constraints.values.first, - )) { - // One or more constraints are different depending on dialect, generate - // per-dialect constraints - - final literalEntries = [ - for (final entry in constraints.entries) - '${emitter.drift('SqlDialect.${entry.key.name}')}: ${asDartLiteral(entry.value)},', - ]; - - additionalParams['defaultConstraints'] = - '${emitter.drift('GeneratedColumn.constraintsDependsOnDialect')}({${literalEntries.join('\n')}})'; - } else { - // Constraints are the same regardless of dialect, only generate one set - // of them - - final constraint = asDartLiteral(constraints.values.first); - - additionalParams['defaultConstraints'] = - '${emitter.drift('GeneratedColumn.constraintIsAlways')}($constraint)'; - } - } - - if (column.defaultArgument != null) { - additionalParams['defaultValue'] = - emitter.dartCode(column.defaultArgument!); - } - - if (column.clientDefaultCode != null) { - additionalParams['clientDefault'] = - emitter.dartCode(column.clientDefaultCode!); - } - - final innerType = emitter.innerColumnType(column); - var type = - '${emitter.drift('GeneratedColumn')}<${emitter.dartCode(innerType)}>'; - expressionBuffer - ..write(type) - ..write( - '(${asDartLiteral(column.nameInSql)}, aliasedName, $isNullable, '); - - var first = true; - additionalParams.forEach((name, value) { - if (!first) { - expressionBuffer.write(', '); - } else { - first = false; - } - - expressionBuffer - ..write(name) - ..write(': ') - ..write(value); - }); - - expressionBuffer.write(')'); - - final converter = column.typeConverter; - if (converter != null) { - // Generate a GeneratedColumnWithTypeConverter instance, as it has - // additional methods to check for equality against a mapped value. - final mappedType = emitter.dartCode(emitter.writer.dartType(column)); - - final converterCode = emitter.dartCode(emitter.writer - .readConverter(converter, forNullable: column.nullable)); - - type = '${emitter.drift('GeneratedColumnWithTypeConverter')}' - '<$mappedType, ${emitter.dartCode(innerType)}>'; - expressionBuffer - ..write('.withConverter<') - ..write(mappedType) - ..write('>(') - ..write(converterCode) - ..write(')'); - } + final (type, expression) = instantiateColumn( + column, + emitter, + isRequiredForInsert: isRequiredForInsert, + ); writeMemoizedGetter( buffer: buffer, getterName: column.nameInDart, returnType: type, - code: expressionBuffer.toString(), + code: expression, hasOverride: isOverride, ); } @@ -276,6 +160,143 @@ abstract class TableOrViewWriter { buffer.write( '@override\n${tableOrView.entityInfoName} get asDslTable => this;\n'); } + + /// Returns the Dart type and the Dart expression creating a `GeneratedColumn` + /// instance in drift for the givne [column]. + static (String, String) instantiateColumn( + DriftColumn column, + TextEmitter emitter, { + bool? isRequiredForInsert, + }) { + final isNullable = column.nullable; + final additionalParams = {}; + final expressionBuffer = StringBuffer(); + final constraints = defaultConstraints(column); + + for (final constraint in column.constraints) { + if (constraint is LimitingTextLength) { + final buffer = + StringBuffer(emitter.drift('GeneratedColumn.checkTextLength(')); + + if (constraint.minLength != null) { + buffer.write('minTextLength: ${constraint.minLength},'); + } + if (constraint.maxLength != null) { + buffer.write('maxTextLength: ${constraint.maxLength}'); + } + buffer.write(')'); + + additionalParams['additionalChecks'] = buffer.toString(); + } + + if (constraint is DartCheckExpression) { + final dartCheck = emitter.dartCode(constraint.dartExpression); + additionalParams['check'] = '() => $dartCheck'; + } + + if (constraint is ColumnGeneratedAs) { + final dartCode = emitter.dartCode(constraint.dartExpression); + + additionalParams['generatedAs'] = + '${emitter.drift('GeneratedAs')}($dartCode, ${constraint.stored})'; + } + + if (constraint is PrimaryKeyColumn && constraint.isAutoIncrement) { + additionalParams['hasAutoIncrement'] = 'true'; + } + } + + additionalParams['type'] = emitter.drift(column.sqlType.toString()); + + if (isRequiredForInsert != null) { + additionalParams['requiredDuringInsert'] = isRequiredForInsert.toString(); + } + + if (column.customConstraints != null) { + additionalParams['\$customConstraints'] = + asDartLiteral(column.customConstraints!); + } else if (constraints.values.any((constraint) => constraint.isNotEmpty)) { + // Use the default constraints supported by drift + + if (constraints.values.any( + (value) => value != constraints.values.first, + )) { + // One or more constraints are different depending on dialect, generate + // per-dialect constraints + + final literalEntries = [ + for (final entry in constraints.entries) + '${emitter.drift('SqlDialect.${entry.key.name}')}: ${asDartLiteral(entry.value)},', + ]; + + additionalParams['defaultConstraints'] = + '${emitter.drift('GeneratedColumn.constraintsDependsOnDialect')}({${literalEntries.join('\n')}})'; + } else { + // Constraints are the same regardless of dialect, only generate one set + // of them + + final constraint = asDartLiteral(constraints.values.first); + + additionalParams['defaultConstraints'] = + '${emitter.drift('GeneratedColumn.constraintIsAlways')}($constraint)'; + } + } + + if (column.defaultArgument != null) { + additionalParams['defaultValue'] = + emitter.dartCode(column.defaultArgument!); + } + + if (column.clientDefaultCode != null) { + additionalParams['clientDefault'] = + emitter.dartCode(column.clientDefaultCode!); + } + + final innerType = emitter.innerColumnType(column); + var type = + '${emitter.drift('GeneratedColumn')}<${emitter.dartCode(innerType)}>'; + expressionBuffer + ..write(type) + ..write( + '(${asDartLiteral(column.nameInSql)}, aliasedName, $isNullable, '); + + var first = true; + additionalParams.forEach((name, value) { + if (!first) { + expressionBuffer.write(', '); + } else { + first = false; + } + + expressionBuffer + ..write(name) + ..write(': ') + ..write(value); + }); + + expressionBuffer.write(')'); + + final converter = column.typeConverter; + if (converter != null) { + // Generate a GeneratedColumnWithTypeConverter instance, as it has + // additional methods to check for equality against a mapped value. + final mappedType = emitter.dartCode(emitter.writer.dartType(column)); + + final converterCode = emitter.dartCode(emitter.writer + .readConverter(converter, forNullable: column.nullable)); + + type = '${emitter.drift('GeneratedColumnWithTypeConverter')}' + '<$mappedType, ${emitter.dartCode(innerType)}>'; + expressionBuffer + ..write('.withConverter<') + ..write(mappedType) + ..write('>(') + ..write(converterCode) + ..write(')'); + } + + return (type, expressionBuffer.toString()); + } } class TableWriter extends TableOrViewWriter { @@ -577,8 +598,7 @@ class TableWriter extends TableOrViewWriter { if (table.isVirtual) { final stmt = table.virtualTableData!; - final moduleAndArgs = - asDartLiteral('${stmt.module}(${stmt.moduleArguments.join(', ')})'); + final moduleAndArgs = asDartLiteral(stmt.moduleAndArgs); buffer ..write('@override\n') ..write('String get moduleAndArgs => $moduleAndArgs;\n'); diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index 3c9413bb..47f4121b 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -11,7 +11,7 @@ topics: - database environment: - sdk: '>=2.17.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: charcode: ^1.2.0 diff --git a/drift_dev/test/cli/utils.dart b/drift_dev/test/cli/utils.dart index fb117112..9fa5202d 100644 --- a/drift_dev/test/cli/utils.dart +++ b/drift_dev/test/cli/utils.dart @@ -13,7 +13,7 @@ class TestDriftProject { Future runDriftCli(Iterable args) { return IOOverrides.runZoned( - () => MoorCli().run(args), + () => DriftDevCli().run(args), getCurrentDirectory: () => root, ); } diff --git a/examples/app/README.md b/examples/app/README.md index f32b0096..4649ce0c 100644 --- a/examples/app/README.md +++ b/examples/app/README.md @@ -31,7 +31,7 @@ Drift databases don't depend on platform-channels or Flutter-specific features by default. This means that they can easily be used in unit tests. One such test is in `test/database_test.dart` -#### Testing migrations +### Migration After changing the structure of your database schema, for instance by adding new tables or altering columns, you need to write a migration to ensure that @@ -57,4 +57,11 @@ you can use to write unit tests for schema migrations: dart run drift_dev schema generate drift_schemas/ test/generated_migrations/ ``` +To make migrations easier, this command updates the `lib/database/schema_versions.dart` +file containing snapshots of older database schema: + +``` +dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart +``` + An example for a schema test is in `test/migration_test.dart`. diff --git a/examples/app/lib/database/database.dart b/examples/app/lib/database/database.dart index 16000ffd..da764d9b 100644 --- a/examples/app/lib/database/database.dart +++ b/examples/app/lib/database/database.dart @@ -4,6 +4,10 @@ import 'package:riverpod/riverpod.dart'; import 'connection/connection.dart' as impl; import 'tables.dart'; +// Manually generated by `drift_dev schema steps` - this file makes writing +// migrations easier. See this for details: +// https://drift.simonbinder.eu/docs/advanced-features/migrations/#step-by-step +import 'schema_versions.dart'; // Generated by drift_dev when running `build_runner build` part 'database.g.dart'; @@ -20,26 +24,22 @@ class AppDatabase extends _$AppDatabase { @override MigrationStrategy get migration { return MigrationStrategy( - onUpgrade: ((m, from, to) async { - for (var step = from + 1; step <= to; step++) { - switch (step) { - case 2: - // The todoEntries.dueDate column was added in version 2. - await m.addColumn(todoEntries, todoEntries.dueDate); - break; - case 3: - // New triggers were added in version 3: - await m.create(todosDelete); - await m.create(todosUpdate); + onUpgrade: stepByStep( + from1To2: (m, schema) async { + // The todoEntries.dueDate column was added in version 2. + await m.addColumn(schema.todoEntries, schema.todoEntries.dueDate); + }, + from2To3: (m, schema) async { + // New triggers were added in version 3: + await m.create(schema.todosDelete); + await m.create(schema.todosUpdate); - // Also, the `REFERENCES` constraint was added to - // [TodoEntries.category]. Run a table migration to rebuild all - // column constraints without loosing data. - await m.alterTable(TableMigration(todoEntries)); - break; - } - } - }), + // Also, the `REFERENCES` constraint was added to + // [TodoEntries.category]. Run a table migration to rebuild all + // column constraints without loosing data. + await m.alterTable(TableMigration(schema.todoEntries)); + }, + ), beforeOpen: (details) async { // Make sure that foreign keys are enabled await customStatement('PRAGMA foreign_keys = ON'); diff --git a/examples/app/lib/database/schema_versions.dart b/examples/app/lib/database/schema_versions.dart new file mode 100644 index 00000000..e9b465bd --- /dev/null +++ b/examples/app/lib/database/schema_versions.dart @@ -0,0 +1,202 @@ +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class _S2 extends i0.VersionedSchema { + _S2({required super.database}) : super(version: 2); + @override + late final List entities = [ + categories, + todoEntries, + textEntries, + todosInsert, + ]; + late final Shape0 categories = Shape0( + source: i0.VersionedTable( + entityName: 'categories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 todoEntries = Shape1( + source: i0.VersionedTable( + entityName: 'todo_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 textEntries = Shape2( + source: i0.VersionedVirtualTable( + entityName: 'text_entries', + moduleAndArgs: + 'fts5(description, content=todo_entries, content_rowid=id)', + columns: [ + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Trigger todosInsert = i1.Trigger( + 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN\n INSERT INTO text_entries(rowid, description) VALUES (new.id, new.description);\nEND;', + 'todos_insert'); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get color => + columnsByName['color']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + hasAutoIncrement: true, + type: i1.DriftSqlType.int, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn('name', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_2(String aliasedName) => + i1.GeneratedColumn('color', aliasedName, false, + type: i1.DriftSqlType.int); + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; + i1.GeneratedColumn get category => + columnsByName['category']! as i1.GeneratedColumn; + i1.GeneratedColumn get dueDate => + columnsByName['due_date']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, false, + type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn('category', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn('due_date', aliasedName, true, + type: i1.DriftSqlType.dateTime); + +class Shape2 extends i0.VersionedVirtualTable { + Shape2({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get description => + columnsByName['description']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_6(String aliasedName) => + i1.GeneratedColumn('description', aliasedName, false, + type: i1.DriftSqlType.string, $customConstraints: ''); + +final class _S3 extends i0.VersionedSchema { + _S3({required super.database}) : super(version: 3); + @override + late final List entities = [ + categories, + todoEntries, + textEntries, + todosInsert, + todosDelete, + todosUpdate, + ]; + late final Shape0 categories = Shape0( + source: i0.VersionedTable( + entityName: 'categories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 todoEntries = Shape1( + source: i0.VersionedTable( + entityName: 'todo_entries', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_3, + _column_7, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 textEntries = Shape2( + source: i0.VersionedVirtualTable( + entityName: 'text_entries', + moduleAndArgs: + 'fts5(description, content=todo_entries, content_rowid=id)', + columns: [ + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Trigger todosInsert = i1.Trigger( + 'CREATE TRIGGER todos_insert AFTER INSERT ON todo_entries BEGIN INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', + 'todos_insert'); + final i1.Trigger todosDelete = i1.Trigger( + 'CREATE TRIGGER todos_delete AFTER DELETE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', old.id, old.description);END', + 'todos_delete'); + final i1.Trigger todosUpdate = i1.Trigger( + 'CREATE TRIGGER todos_update AFTER UPDATE ON todo_entries BEGIN INSERT INTO text_entries (text_entries, "rowid", description) VALUES (\'delete\', new.id, new.description);INSERT INTO text_entries ("rowid", description) VALUES (new.id, new.description);END', + 'todos_update'); +} + +i1.GeneratedColumn _column_7(String aliasedName) => + i1.GeneratedColumn('category', aliasedName, true, + type: i1.DriftSqlType.int, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'REFERENCES categories (id)')); +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, +}) { + return i1.Migrator.stepByStepHelper(step: (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = _S2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + case 2: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }); +} diff --git a/examples/app/pubspec.yaml b/examples/app/pubspec.yaml index 07143f49..358b875a 100644 --- a/examples/app/pubspec.yaml +++ b/examples/app/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.16.1 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -15,9 +15,8 @@ dependencies: file_picker: ^5.2.5 flutter_colorpicker: ^1.0.3 flutter_riverpod: ^2.3.0 - go_router: ^6.2.0 + go_router: ^9.0.0 intl: ^0.18.0 - http: ^0.13.4 # used to load sqlite3 wasm files on the web sqlite3_flutter_libs: ^0.5.5 sqlite3: ^2.0.0 path_provider: ^2.0.9 @@ -34,7 +33,7 @@ dev_dependencies: # web workers for drift AND want to compile those web workers through `build_runner`. # Either way, using this package with Flutter requires a delicate setup described in # `build.yaml`. - build_web_compilers: ^3.2.2 + build_web_compilers: ^4.0.0 build: ^2.2.1 flutter: diff --git a/examples/migrations_example/README.md b/examples/migrations_example/README.md index 893e8f56..5adb1e9d 100644 --- a/examples/migrations_example/README.md +++ b/examples/migrations_example/README.md @@ -20,9 +20,9 @@ Run dart run drift_dev schema generate drift_migrations/ test/generated/ --data-classes --companions ``` -We're also using test code inside `lib/` to run migrations with older definitions of tables. -This isn't required for all migrations, but can be useful in some cases. +Since we're using the step-by-step generator to make writing migrations easier, this command +is used to generate a helper file in `lib/`: ``` -dart run drift_dev schema generate drift_migrations/ lib/src/generated +dart run drift_dev schema steps drift_migrations/ lib/src/versions.dart ``` diff --git a/examples/migrations_example/lib/database.dart b/examples/migrations_example/lib/database.dart index 7d2ef4e3..221c4b9a 100644 --- a/examples/migrations_example/lib/database.dart +++ b/examples/migrations_example/lib/database.dart @@ -2,9 +2,7 @@ import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'tables.dart'; -import 'src/generated/schema_v2.dart' as v2; -import 'src/generated/schema_v4.dart' as v4; -import 'src/generated/schema_v8.dart' as v8; +import 'src/versions.dart'; part 'database.g.dart'; @@ -20,59 +18,7 @@ class Database extends _$Database { @override MigrationStrategy get migration { return MigrationStrategy( - onUpgrade: (m, before, now) async { - for (var target = before + 1; target <= now; target++) { - switch (target) { - case 2: - // Migration from 1 to 2: Add name column in users. Use "no name" - // as a default value. - final usersAtV2 = v2.Users(this); - - await m.alterTable( - TableMigration( - usersAtV2, - columnTransformer: { - users.name: const Constant('no name'), - }, - newColumns: [usersAtV2.name], - ), - ); - break; - case 3: - // Migration from 2 to 3: We added the groups table - await m.createTable(groups); - break; - case 4: - // Migration from 3 to 4: users.name now has a default value - // No need to transform any data, just re-create the table - final usersAtV4 = v4.Users(this); - - await m.alterTable(TableMigration(usersAtV4)); - break; - case 5: - // Just add a new column that was added in version 5; - await m.addColumn(users, users.nextUser); - - // And create the view on users - await m.createView(groupCount); - break; - case 6: - await m.addColumn(users, users.birthday); - break; - case 7: - await m.createTable(notes); - break; - case 8: - // Added a unique key to the users table - await m.alterTable(TableMigration(v8.Users(this))); - break; - case 9: - // Added a check to the users table - await m.alterTable(TableMigration(users)); - break; - } - } - }, + onUpgrade: _upgrade, beforeOpen: (details) async { // For Flutter apps, this should be wrapped in an if (kDebugMode) as // suggested here: https://drift.simonbinder.eu/docs/advanced-features/migrations/#verifying-a-database-schema-at-runtime @@ -80,4 +26,50 @@ class Database extends _$Database { }, ); } + + static final _upgrade = stepByStep( + from1To2: (m, schema) async { + // Migration from 1 to 2: Add name column in users. Use "no name" + // as a default value. + + await m.alterTable( + TableMigration( + schema.users, + columnTransformer: { + schema.users.name: const Constant('no name'), + }, + newColumns: [schema.users.name], + ), + ); + }, + from2To3: (m, schema) async => m.createTable(schema.groups), + from3To4: (m, schema) async { + // Migration from 3 to 4: users.name now has a default value + // No need to transform any data, just re-create the table + final usersAtV4 = schema.users; + + await m.alterTable(TableMigration(usersAtV4)); + }, + from4To5: (m, schema) async { + // Just add a new column that was added in version 5; + await m.addColumn(schema.users, schema.users.nextUser); + + // And create the view on users + await m.createView(schema.groupCount); + }, + from5To6: (m, schema) async { + await m.addColumn(schema.users, schema.users.birthday); + }, + from6To7: (m, schema) async { + await m.createTable(schema.notes); + }, + from7To8: (m, schema) async { + // Added a unique key to the users table + await m.alterTable(TableMigration(schema.users)); + }, + from8To9: (m, schema) async { + // Added a check to the users table + await m.alterTable(TableMigration(schema.users)); + }, + ); } diff --git a/examples/migrations_example/lib/database.g.dart b/examples/migrations_example/lib/database.g.dart index 8618016f..85bb9722 100644 --- a/examples/migrations_example/lib/database.g.dart +++ b/examples/migrations_example/lib/database.g.dart @@ -874,7 +874,7 @@ class GroupCount extends ViewInfo @override Query? get query => null; @override - Set get readTables => const {'groups', 'users'}; + Set get readTables => const {'users', 'groups'}; } abstract class _$Database extends GeneratedDatabase { diff --git a/examples/migrations_example/lib/src/generated/schema.dart b/examples/migrations_example/lib/src/generated/schema.dart deleted file mode 100644 index 28f662de..00000000 --- a/examples/migrations_example/lib/src/generated/schema.dart +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; -import 'package:drift/internal/migrations.dart'; -import 'schema_v1.dart' as v1; -import 'schema_v2.dart' as v2; -import 'schema_v3.dart' as v3; -import 'schema_v4.dart' as v4; -import 'schema_v5.dart' as v5; -import 'schema_v6.dart' as v6; -import 'schema_v7.dart' as v7; -import 'schema_v8.dart' as v8; -import 'schema_v9.dart' as v9; - -class GeneratedHelper implements SchemaInstantiationHelper { - @override - GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { - switch (version) { - case 1: - return v1.DatabaseAtV1(db); - case 2: - return v2.DatabaseAtV2(db); - case 3: - return v3.DatabaseAtV3(db); - case 4: - return v4.DatabaseAtV4(db); - case 5: - return v5.DatabaseAtV5(db); - case 6: - return v6.DatabaseAtV6(db); - case 7: - return v7.DatabaseAtV7(db); - case 8: - return v8.DatabaseAtV8(db); - case 9: - return v9.DatabaseAtV9(db); - default: - throw MissingSchemaException( - version, const {1, 2, 3, 4, 5, 6, 7, 8, 9}); - } - } -} diff --git a/examples/migrations_example/lib/src/generated/schema_v1.dart b/examples/migrations_example/lib/src/generated/schema_v1.dart deleted file mode 100644 index 3745f8dc..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v1.dart +++ /dev/null @@ -1,47 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - @override - List get $columns => [id]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class DatabaseAtV1 extends GeneratedDatabase { - DatabaseAtV1(QueryExecutor e) : super(e); - late final Users users = Users(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [users]; - @override - int get schemaVersion => 1; -} diff --git a/examples/migrations_example/lib/src/generated/schema_v2.dart b/examples/migrations_example/lib/src/generated/schema_v2.dart deleted file mode 100644 index 312d0ae6..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v2.dart +++ /dev/null @@ -1,50 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, name]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class DatabaseAtV2 extends GeneratedDatabase { - DatabaseAtV2(QueryExecutor e) : super(e); - late final Users users = Users(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [users]; - @override - int get schemaVersion => 2; -} diff --git a/examples/migrations_example/lib/src/generated/schema_v3.dart b/examples/migrations_example/lib/src/generated/schema_v3.dart deleted file mode 100644 index d0a0bd53..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v3.dart +++ /dev/null @@ -1,101 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, name]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class DatabaseAtV3 extends GeneratedDatabase { - DatabaseAtV3(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [users, groups]; - @override - int get schemaVersion => 3; -} diff --git a/examples/migrations_example/lib/src/generated/schema_v4.dart b/examples/migrations_example/lib/src/generated/schema_v4.dart deleted file mode 100644 index 428a1444..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v4.dart +++ /dev/null @@ -1,103 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - @override - List get $columns => [id, name]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class DatabaseAtV4 extends GeneratedDatabase { - DatabaseAtV4(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [users, groups]; - @override - int get schemaVersion => 4; -} diff --git a/examples/migrations_example/lib/src/generated/schema_v5.dart b/examples/migrations_example/lib/src/generated/schema_v5.dart deleted file mode 100644 index f950534a..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v5.dart +++ /dev/null @@ -1,154 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES users (id)')); - @override - List get $columns => [id, name, nextUser]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class GroupCount extends ViewInfo implements HasResultSet { - final String? _alias; - @override - final DatabaseAtV5 attachedDatabase; - GroupCount(this.attachedDatabase, [this._alias]); - @override - List get $columns => [id, name, nextUser, groupCount]; - @override - String get aliasedName => _alias ?? entityName; - @override - String get entityName => 'group_count'; - @override - String get createViewStmt => - 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'; - @override - GroupCount get asDslTable => this; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - late final GeneratedColumn id = - GeneratedColumn('id', aliasedName, false, type: DriftSqlType.int); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int); - late final GeneratedColumn groupCount = GeneratedColumn( - 'group_count', aliasedName, false, - type: DriftSqlType.int); - @override - GroupCount createAlias(String alias) { - return GroupCount(attachedDatabase, alias); - } - - @override - Query? get query => null; - @override - Set get readTables => const {}; -} - -class DatabaseAtV5 extends GeneratedDatabase { - DatabaseAtV5(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - late final GroupCount groupCount = GroupCount(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [users, groups, groupCount]; - @override - int get schemaVersion => 5; -} diff --git a/examples/migrations_example/lib/src/generated/schema_v6.dart b/examples/migrations_example/lib/src/generated/schema_v6.dart deleted file mode 100644 index fcaa9831..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v6.dart +++ /dev/null @@ -1,164 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES users (id)')); - @override - List get $columns => [id, name, birthday, nextUser]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class GroupCount extends ViewInfo implements HasResultSet { - final String? _alias; - @override - final DatabaseAtV6 attachedDatabase; - GroupCount(this.attachedDatabase, [this._alias]); - @override - List get $columns => - [id, name, birthday, nextUser, groupCount]; - @override - String get aliasedName => _alias ?? entityName; - @override - String get entityName => 'group_count'; - @override - String get createViewStmt => - 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'; - @override - GroupCount get asDslTable => this; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - late final GeneratedColumn id = - GeneratedColumn('id', aliasedName, false, type: DriftSqlType.int); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int); - late final GeneratedColumn groupCount = GeneratedColumn( - 'group_count', aliasedName, false, - type: DriftSqlType.int); - @override - GroupCount createAlias(String alias) { - return GroupCount(attachedDatabase, alias); - } - - @override - Query? get query => null; - @override - Set get readTables => const {}; -} - -class DatabaseAtV6 extends GeneratedDatabase { - DatabaseAtV6(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - late final GroupCount groupCount = GroupCount(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [users, groups, groupCount]; - @override - int get schemaVersion => 6; - @override - DriftDatabaseOptions get options => - const DriftDatabaseOptions(storeDateTimeAsText: true); -} diff --git a/examples/migrations_example/lib/src/generated/schema_v7.dart b/examples/migrations_example/lib/src/generated/schema_v7.dart deleted file mode 100644 index 44a3a807..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v7.dart +++ /dev/null @@ -1,208 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES users (id)')); - @override - List get $columns => [id, name, birthday, nextUser]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class GroupCount extends ViewInfo implements HasResultSet { - final String? _alias; - @override - final DatabaseAtV7 attachedDatabase; - GroupCount(this.attachedDatabase, [this._alias]); - @override - List get $columns => - [id, name, birthday, nextUser, groupCount]; - @override - String get aliasedName => _alias ?? entityName; - @override - String get entityName => 'group_count'; - @override - String get createViewStmt => - 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'; - @override - GroupCount get asDslTable => this; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - late final GeneratedColumn id = - GeneratedColumn('id', aliasedName, false, type: DriftSqlType.int); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int); - late final GeneratedColumn groupCount = GeneratedColumn( - 'group_count', aliasedName, false, - type: DriftSqlType.int); - @override - GroupCount createAlias(String alias) { - return GroupCount(attachedDatabase, alias); - } - - @override - Query? get query => null; - @override - Set get readTables => const {}; -} - -class Notes extends Table with TableInfo, VirtualTableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Notes(this.attachedDatabase, [this._alias]); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn content = GeneratedColumn( - 'content', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn searchTerms = GeneratedColumn( - 'search_terms', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - @override - List get $columns => [title, content, searchTerms]; - @override - String get aliasedName => _alias ?? 'notes'; - @override - String get actualTableName => 'notes'; - @override - Set get $primaryKey => const {}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Notes createAlias(String alias) { - return Notes(attachedDatabase, alias); - } - - @override - String get moduleAndArgs => - 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")'; -} - -class DatabaseAtV7 extends GeneratedDatabase { - DatabaseAtV7(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - late final GroupCount groupCount = GroupCount(this); - late final Notes notes = Notes(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [users, groups, groupCount, notes]; - @override - int get schemaVersion => 7; - @override - DriftDatabaseOptions get options => - const DriftDatabaseOptions(storeDateTimeAsText: true); -} diff --git a/examples/migrations_example/lib/src/generated/schema_v8.dart b/examples/migrations_example/lib/src/generated/schema_v8.dart deleted file mode 100644 index dfdbde9c..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v8.dart +++ /dev/null @@ -1,212 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES "users" ("id")')); - @override - List get $columns => [id, name, birthday, nextUser]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - List> get uniqueKeys => [ - {name, birthday}, - ]; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users (id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY (id)']; - @override - bool get dontWriteConstraints => true; -} - -class GroupCount extends ViewInfo implements HasResultSet { - final String? _alias; - @override - final DatabaseAtV8 attachedDatabase; - GroupCount(this.attachedDatabase, [this._alias]); - @override - List get $columns => - [id, name, birthday, nextUser, groupCount]; - @override - String get aliasedName => _alias ?? entityName; - @override - String get entityName => 'group_count'; - @override - String get createViewStmt => - 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users'; - @override - GroupCount get asDslTable => this; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - late final GeneratedColumn id = - GeneratedColumn('id', aliasedName, false, type: DriftSqlType.int); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int); - late final GeneratedColumn groupCount = GeneratedColumn( - 'group_count', aliasedName, false, - type: DriftSqlType.int); - @override - GroupCount createAlias(String alias) { - return GroupCount(attachedDatabase, alias); - } - - @override - Query? get query => null; - @override - Set get readTables => const {}; -} - -class Notes extends Table with TableInfo, VirtualTableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Notes(this.attachedDatabase, [this._alias]); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn content = GeneratedColumn( - 'content', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn searchTerms = GeneratedColumn( - 'search_terms', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - @override - List get $columns => [title, content, searchTerms]; - @override - String get aliasedName => _alias ?? 'notes'; - @override - String get actualTableName => 'notes'; - @override - Set get $primaryKey => const {}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Notes createAlias(String alias) { - return Notes(attachedDatabase, alias); - } - - @override - String get moduleAndArgs => - 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")'; -} - -class DatabaseAtV8 extends GeneratedDatabase { - DatabaseAtV8(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - late final GroupCount groupCount = GroupCount(this); - late final Notes notes = Notes(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [users, groups, groupCount, notes]; - @override - int get schemaVersion => 8; - @override - DriftDatabaseOptions get options => - const DriftDatabaseOptions(storeDateTimeAsText: true); -} diff --git a/examples/migrations_example/lib/src/generated/schema_v9.dart b/examples/migrations_example/lib/src/generated/schema_v9.dart deleted file mode 100644 index 311f005c..00000000 --- a/examples/migrations_example/lib/src/generated/schema_v9.dart +++ /dev/null @@ -1,215 +0,0 @@ -// GENERATED CODE, DO NOT EDIT BY HAND. -// ignore_for_file: type=lint -//@dart=2.12 -import 'package:drift/drift.dart'; - -class Users extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Users(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('name')); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('REFERENCES users (id)')); - @override - List get $columns => [id, name, birthday, nextUser]; - @override - String get aliasedName => _alias ?? 'users'; - @override - String get actualTableName => 'users'; - @override - Set get $primaryKey => {id}; - @override - List> get uniqueKeys => [ - {name, birthday}, - ]; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Users createAlias(String alias) { - return Users(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['CHECK (LENGTH(name) < 10)']; -} - -class Groups extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Groups(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - $customConstraints: 'DEFAULT FALSE', - defaultValue: const CustomExpression('FALSE')); - late final GeneratedColumn owner = GeneratedColumn( - 'owner', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES users(id)'); - @override - List get $columns => [id, title, deleted, owner]; - @override - String get aliasedName => _alias ?? 'groups'; - @override - String get actualTableName => 'groups'; - @override - Set get $primaryKey => {id}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Groups createAlias(String alias) { - return Groups(attachedDatabase, alias); - } - - @override - List get customConstraints => const ['PRIMARY KEY(id)']; - @override - bool get dontWriteConstraints => true; -} - -class Notes extends Table with TableInfo, VirtualTableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Notes(this.attachedDatabase, [this._alias]); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn content = GeneratedColumn( - 'content', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - late final GeneratedColumn searchTerms = GeneratedColumn( - 'search_terms', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: ''); - @override - List get $columns => [title, content, searchTerms]; - @override - String get aliasedName => _alias ?? 'notes'; - @override - String get actualTableName => 'notes'; - @override - Set get $primaryKey => const {}; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - @override - Notes createAlias(String alias) { - return Notes(attachedDatabase, alias); - } - - @override - String get moduleAndArgs => - 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")'; -} - -class GroupCount extends ViewInfo implements HasResultSet { - final String? _alias; - @override - final DatabaseAtV9 attachedDatabase; - GroupCount(this.attachedDatabase, [this._alias]); - @override - List get $columns => - [id, name, birthday, nextUser, groupCount]; - @override - String get aliasedName => _alias ?? entityName; - @override - String get entityName => 'group_count'; - @override - String get createViewStmt => - 'CREATE VIEW group_count AS SELECT\n users.*,\n (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count\n FROM users;'; - @override - GroupCount get asDslTable => this; - @override - Never map(Map data, {String? tablePrefix}) { - throw UnsupportedError('TableInfo.map in schema verification code'); - } - - late final GeneratedColumn id = - GeneratedColumn('id', aliasedName, false, type: DriftSqlType.int); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string); - late final GeneratedColumn birthday = GeneratedColumn( - 'birthday', aliasedName, true, - type: DriftSqlType.dateTime); - late final GeneratedColumn nextUser = GeneratedColumn( - 'next_user', aliasedName, true, - type: DriftSqlType.int); - late final GeneratedColumn groupCount = GeneratedColumn( - 'group_count', aliasedName, false, - type: DriftSqlType.int); - @override - GroupCount createAlias(String alias) { - return GroupCount(attachedDatabase, alias); - } - - @override - Query? get query => null; - @override - Set get readTables => const {}; -} - -class DatabaseAtV9 extends GeneratedDatabase { - DatabaseAtV9(QueryExecutor e) : super(e); - late final Users users = Users(this); - late final Groups groups = Groups(this); - late final Notes notes = Notes(this); - late final GroupCount groupCount = GroupCount(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [users, groups, notes, groupCount]; - @override - int get schemaVersion => 9; - @override - DriftDatabaseOptions get options => - const DriftDatabaseOptions(storeDateTimeAsText: true); -} diff --git a/examples/migrations_example/lib/src/versions.dart b/examples/migrations_example/lib/src/versions.dart new file mode 100644 index 00000000..9c111ef0 --- /dev/null +++ b/examples/migrations_example/lib/src/versions.dart @@ -0,0 +1,644 @@ +import 'package:drift/internal/versioned_schema.dart' as i0; +import 'package:drift/drift.dart' as i1; +import 'package:drift/drift.dart'; // ignore_for_file: type=lint + +// GENERATED BY drift_dev, DO NOT MODIFY. +final class _S2 extends i0.VersionedSchema { + _S2({required super.database}) : super(version: 2); + @override + late final List entities = [ + users, + ]; + late final Shape0 users = Shape0( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape0 extends i0.VersionedTable { + Shape0({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_0(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + hasAutoIncrement: true, + type: i1.DriftSqlType.int, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); +i1.GeneratedColumn _column_1(String aliasedName) => + i1.GeneratedColumn('name', aliasedName, false, + type: i1.DriftSqlType.string); + +final class _S3 extends i0.VersionedSchema { + _S3({required super.database}) : super(version: 3); + @override + late final List entities = [ + users, + groups, + ]; + late final Shape0 users = Shape0( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape1 extends i0.VersionedTable { + Shape1({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get title => + columnsByName['title']! as i1.GeneratedColumn; + i1.GeneratedColumn get deleted => + columnsByName['deleted']! as i1.GeneratedColumn; + i1.GeneratedColumn get owner => + columnsByName['owner']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_2(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL'); +i1.GeneratedColumn _column_3(String aliasedName) => + i1.GeneratedColumn('title', aliasedName, false, + type: i1.DriftSqlType.string, $customConstraints: 'NOT NULL'); +i1.GeneratedColumn _column_4(String aliasedName) => + i1.GeneratedColumn('deleted', aliasedName, true, + type: i1.DriftSqlType.bool, + $customConstraints: 'DEFAULT FALSE', + defaultValue: const CustomExpression('FALSE')); +i1.GeneratedColumn _column_5(String aliasedName) => + i1.GeneratedColumn('owner', aliasedName, false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL REFERENCES users (id)'); + +final class _S4 extends i0.VersionedSchema { + _S4({required super.database}) : super(version: 4); + @override + late final List entities = [ + users, + groups, + ]; + late final Shape0 users = Shape0( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); +} + +i1.GeneratedColumn _column_6(String aliasedName) => + i1.GeneratedColumn('name', aliasedName, false, + type: i1.DriftSqlType.string, defaultValue: const Constant('name')); + +final class _S5 extends i0.VersionedSchema { + _S5({required super.database}) : super(version: 5); + @override + late final List entities = [ + users, + groups, + groupCount, + ]; + late final Shape2 users = Shape2( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_6, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 groupCount = Shape3( + source: i0.VersionedView( + entityName: 'group_count', + createViewStmt: + 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users', + columns: [ + _column_8, + _column_1, + _column_9, + _column_10, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape2 extends i0.VersionedTable { + Shape2({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get nextUser => + columnsByName['next_user']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_7(String aliasedName) => + i1.GeneratedColumn('next_user', aliasedName, true, + type: i1.DriftSqlType.int, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('REFERENCES users (id)')); + +class Shape3 extends i0.VersionedView { + Shape3({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get nextUser => + columnsByName['next_user']! as i1.GeneratedColumn; + i1.GeneratedColumn get groupCount => + columnsByName['group_count']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_8(String aliasedName) => + i1.GeneratedColumn('id', aliasedName, false, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_9(String aliasedName) => + i1.GeneratedColumn('next_user', aliasedName, true, + type: i1.DriftSqlType.int); +i1.GeneratedColumn _column_10(String aliasedName) => + i1.GeneratedColumn('group_count', aliasedName, false, + type: i1.DriftSqlType.int); + +final class _S6 extends i0.VersionedSchema { + _S6({required super.database}) : super(version: 6); + @override + late final List entities = [ + users, + groups, + groupCount, + ]; + late final Shape4 users = Shape4( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_6, + _column_11, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 groupCount = Shape5( + source: i0.VersionedView( + entityName: 'group_count', + createViewStmt: + 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users', + columns: [ + _column_8, + _column_1, + _column_11, + _column_9, + _column_10, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape4 extends i0.VersionedTable { + Shape4({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get birthday => + columnsByName['birthday']! as i1.GeneratedColumn; + i1.GeneratedColumn get nextUser => + columnsByName['next_user']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_11(String aliasedName) => + i1.GeneratedColumn('birthday', aliasedName, true, + type: i1.DriftSqlType.dateTime); + +class Shape5 extends i0.VersionedView { + Shape5({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get birthday => + columnsByName['birthday']! as i1.GeneratedColumn; + i1.GeneratedColumn get nextUser => + columnsByName['next_user']! as i1.GeneratedColumn; + i1.GeneratedColumn get groupCount => + columnsByName['group_count']! as i1.GeneratedColumn; +} + +final class _S7 extends i0.VersionedSchema { + _S7({required super.database}) : super(version: 7); + @override + late final List entities = [ + users, + groups, + groupCount, + notes, + ]; + late final Shape4 users = Shape4( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_6, + _column_11, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 groupCount = Shape5( + source: i0.VersionedView( + entityName: 'group_count', + createViewStmt: + 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users', + columns: [ + _column_8, + _column_1, + _column_11, + _column_9, + _column_10, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 notes = Shape6( + source: i0.VersionedVirtualTable( + entityName: 'notes', + moduleAndArgs: + 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")', + columns: [ + _column_12, + _column_13, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape6 extends i0.VersionedVirtualTable { + Shape6({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get title => + columnsByName['title']! as i1.GeneratedColumn; + i1.GeneratedColumn get content => + columnsByName['content']! as i1.GeneratedColumn; + i1.GeneratedColumn get searchTerms => + columnsByName['search_terms']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_12(String aliasedName) => + i1.GeneratedColumn('title', aliasedName, false, + type: i1.DriftSqlType.string, $customConstraints: ''); +i1.GeneratedColumn _column_13(String aliasedName) => + i1.GeneratedColumn('content', aliasedName, false, + type: i1.DriftSqlType.string, $customConstraints: ''); +i1.GeneratedColumn _column_14(String aliasedName) => + i1.GeneratedColumn('search_terms', aliasedName, false, + type: i1.DriftSqlType.string, $customConstraints: ''); + +final class _S8 extends i0.VersionedSchema { + _S8({required super.database}) : super(version: 8); + @override + late final List entities = [ + users, + groups, + groupCount, + notes, + ]; + late final Shape4 users = Shape4( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'UNIQUE(name, birthday)', + ], + columns: [ + _column_0, + _column_6, + _column_11, + _column_15, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY (id)', + ], + columns: [ + _column_2, + _column_3, + _column_4, + _column_5, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 groupCount = Shape5( + source: i0.VersionedView( + entityName: 'group_count', + createViewStmt: + 'CREATE VIEW group_count AS SELECT users.*, (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count FROM users', + columns: [ + _column_8, + _column_1, + _column_11, + _column_9, + _column_10, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 notes = Shape6( + source: i0.VersionedVirtualTable( + entityName: 'notes', + moduleAndArgs: + 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")', + columns: [ + _column_12, + _column_13, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); +} + +i1.GeneratedColumn _column_15(String aliasedName) => + i1.GeneratedColumn('next_user', aliasedName, true, + type: i1.DriftSqlType.int, + defaultConstraints: + i1.GeneratedColumn.constraintIsAlways('REFERENCES "users" ("id")')); + +final class _S9 extends i0.VersionedSchema { + _S9({required super.database}) : super(version: 9); + @override + late final List entities = [ + users, + groups, + notes, + groupCount, + ]; + late final Shape4 users = Shape4( + source: i0.VersionedTable( + entityName: 'users', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'UNIQUE(name, birthday)', + 'CHECK (LENGTH(name) < 10)', + ], + columns: [ + _column_0, + _column_6, + _column_11, + _column_7, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 groups = Shape1( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(id)', + ], + columns: [ + _column_2, + _column_3, + _column_16, + _column_17, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 notes = Shape6( + source: i0.VersionedVirtualTable( + entityName: 'notes', + moduleAndArgs: + 'fts5(title, content, search_terms, tokenize = "unicode61 tokenchars \'.\'")', + columns: [ + _column_12, + _column_13, + _column_14, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 groupCount = Shape5( + source: i0.VersionedView( + entityName: 'group_count', + createViewStmt: + 'CREATE VIEW group_count AS SELECT\n users.*,\n (SELECT COUNT(*) FROM "groups" WHERE owner = users.id) AS group_count\n FROM users;', + columns: [ + _column_8, + _column_1, + _column_11, + _column_9, + _column_10, + ], + attachedDatabase: database, + ), + alias: null); +} + +i1.GeneratedColumn _column_16(String aliasedName) => + i1.GeneratedColumn('deleted', aliasedName, true, + type: i1.DriftSqlType.bool, + $customConstraints: 'DEFAULT FALSE', + defaultValue: const CustomExpression('FALSE')); +i1.GeneratedColumn _column_17(String aliasedName) => + i1.GeneratedColumn('owner', aliasedName, false, + type: i1.DriftSqlType.int, + $customConstraints: 'NOT NULL REFERENCES users(id)'); +i1.OnUpgrade stepByStep({ + required Future Function(i1.Migrator m, _S2 schema) from1To2, + required Future Function(i1.Migrator m, _S3 schema) from2To3, + required Future Function(i1.Migrator m, _S4 schema) from3To4, + required Future Function(i1.Migrator m, _S5 schema) from4To5, + required Future Function(i1.Migrator m, _S6 schema) from5To6, + required Future Function(i1.Migrator m, _S7 schema) from6To7, + required Future Function(i1.Migrator m, _S8 schema) from7To8, + required Future Function(i1.Migrator m, _S9 schema) from8To9, +}) { + return i1.Migrator.stepByStepHelper(step: (currentVersion, database) async { + switch (currentVersion) { + case 1: + final schema = _S2(database: database); + final migrator = i1.Migrator(database, schema); + await from1To2(migrator, schema); + return 2; + case 2: + final schema = _S3(database: database); + final migrator = i1.Migrator(database, schema); + await from2To3(migrator, schema); + return 3; + case 3: + final schema = _S4(database: database); + final migrator = i1.Migrator(database, schema); + await from3To4(migrator, schema); + return 4; + case 4: + final schema = _S5(database: database); + final migrator = i1.Migrator(database, schema); + await from4To5(migrator, schema); + return 5; + case 5: + final schema = _S6(database: database); + final migrator = i1.Migrator(database, schema); + await from5To6(migrator, schema); + return 6; + case 6: + final schema = _S7(database: database); + final migrator = i1.Migrator(database, schema); + await from6To7(migrator, schema); + return 7; + case 7: + final schema = _S8(database: database); + final migrator = i1.Migrator(database, schema); + await from7To8(migrator, schema); + return 8; + case 8: + final schema = _S9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; + default: + throw ArgumentError.value('Unknown migration from $currentVersion'); + } + }); +} diff --git a/examples/migrations_example/pubspec.yaml b/examples/migrations_example/pubspec.yaml index cfd99b13..e3a9427b 100644 --- a/examples/migrations_example/pubspec.yaml +++ b/examples/migrations_example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none version: 1.0.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: drift: diff --git a/pubspec.lock b/pubspec.lock index 1e541311..b90f7069 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" collection: dependency: transitive description: @@ -85,18 +85,18 @@ packages: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" http: dependency: transitive description: @@ -133,18 +133,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" melos: dependency: "direct dev" description: name: melos - sha256: "993ac467e7a36bd832a6cdabbe18a0487c30bc52b5cca14e476a824679ebdce0" + sha256: ccbb6ecd8bb3f08ae8f9ce22920d816bff325a98940c845eda0257cd395503ac url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" meta: dependency: transitive description: @@ -253,10 +253,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: test_api - sha256: daadc9baabec998b062c9091525aa95786508b1c48e9c30f1f891b8bf6ff2e64 + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.6.0" typed_data: dependency: transitive description: