Add docs for step-by-step migrations

This commit is contained in:
Simon Binder 2023-07-01 18:11:41 +02:00
parent ed9aeb866a
commit 312fa3219f
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
14 changed files with 796 additions and 325 deletions

View File

@ -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":[]}}]}

View File

@ -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":[]}}]}

View File

@ -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":[]}}]}

View File

@ -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
}

View File

@ -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<i1.DatabaseSchemaEntity> 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<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get title =>
columnsByName['title']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['body']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get category =>
columnsByName['category']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get dueDate =>
columnsByName['due_date']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('title', aliasedName, false,
additionalChecks: i1.GeneratedColumn.checkTextLength(
minTextLength: 6, maxTextLength: 10),
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>('body', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
i1.GeneratedColumn<int>('category', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_4(String aliasedName) =>
i1.GeneratedColumn<DateTime>('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<i1.DatabaseSchemaEntity> 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<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get title =>
columnsByName['title']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['body']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get category =>
columnsByName['category']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get dueDate =>
columnsByName['due_date']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get priority =>
columnsByName['priority']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_5(String aliasedName) =>
i1.GeneratedColumn<int>('priority', aliasedName, true,
type: i1.DriftSqlType.int);
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> 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');
}
});
}

View File

@ -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='<i class="fas fa-lightbulb"></i> 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='<i class="fas fa-lightbulb"></i> 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,

View File

@ -1,18 +1,41 @@
/// 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<DatabaseSchemaEntity> get entities;
DatabaseSchemaEntity lookup(String name) {
return entities.singleWhere((element) => element.entityName == name);
}
}
/// 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<Table, QueryRow> {
@override
final String entityName;
@ -36,6 +59,10 @@ class VersionedTable extends Table with TableInfo<Table, QueryRow> {
@override
final List<String> 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,
@ -49,6 +76,7 @@ class VersionedTable extends Table with TableInfo<Table, QueryRow> {
$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,
@ -84,11 +112,13 @@ class VersionedTable extends Table with TableInfo<Table, QueryRow> {
}
}
/// The version of [VersionedTable] for virtual tables.
class VersionedVirtualTable extends VersionedTable
with VirtualTableInfo<Table, QueryRow> {
@override
final String moduleAndArgs;
/// Create a small virtual table from the individual fields.
VersionedVirtualTable({
required super.entityName,
required super.attachedDatabase,
@ -101,6 +131,8 @@ class VersionedVirtualTable extends VersionedTable
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,
@ -115,6 +147,8 @@ class VersionedVirtualTable extends VersionedTable
}
}
/// A constructed from individual fields instead of being generated with a
/// dedicated class.
class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
@override
final String entityName;
@ -138,6 +172,7 @@ class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
@override
final DatabaseConnectionUser attachedDatabase;
/// Create a view from the individual fields on [ViewInfo].
VersionedView({
required this.entityName,
required this.attachedDatabase,
@ -148,6 +183,7 @@ class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
$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,

View File

@ -482,6 +482,12 @@ class Migrator {
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<int> Function(
@ -493,7 +499,10 @@ class Migrator {
final database = m._db;
for (var target = from; target < to;) {
target = await step(target, database);
final newVersion = await step(target, database);
assert(newVersion > target);
target = newVersion;
}
};
}

View File

@ -24,6 +24,7 @@ enum _ResultSetKind {
final class _TableShape {
final _ResultSetKind kind;
// Map from Dart getter names to column names in SQL and the SQL type.
final Map<String, (String, DriftSqlType)> columnTypes;
@ -50,6 +51,14 @@ final class _TableShape {
}
}
/// 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');
@ -61,8 +70,16 @@ class SchemaVersionWriter {
final Map<String, String> _columnCodeToFactory = {};
final Map<_TableShape, String> _shapes = {};
SchemaVersionWriter(this.versions, this.libraryScope);
SchemaVersionWriter(this.versions, this.libraryScope) {
assert(versions.isSortedBy<num>((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);
@ -86,6 +103,16 @@ class SchemaVersionWriter {
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) => (
@ -130,11 +157,11 @@ class SchemaVersionWriter {
});
}
void _writeWithResultSet(DriftElementWithResultSet entity, Scope classScope,
TextEmitter intoListWriter) {
String _writeWithResultSet(
DriftElementWithResultSet entity, TextEmitter writer) {
final getterName = entity.dbGetterName;
final shape = _shapeClass(entity);
final writer = classScope.leaf()
writer
..write('late final $shape $getterName = ')
..write('$shape(source: ');
@ -229,43 +256,60 @@ class SchemaVersionWriter {
_writeColumnsArgument(entity.columns, writer);
writer.write('attachedDatabase: database,');
writer.write('), alias: null);');
writer.write('), alias: null)');
intoListWriter.write(getterName);
return getterName!;
}
void _writeEntity({
String _writeEntity({
required DriftSchemaElement element,
required Scope classScope,
required TextEmitter writer,
required TextEmitter definition,
}) {
String name;
if (element is DriftElementWithResultSet) {
_writeWithResultSet(element, classScope, writer);
name = _writeWithResultSet(element, definition);
} else if (element is DriftIndex) {
writer
..writeDriftRef('Index(')
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) {
writer
..writeDriftRef('Trigger(')
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 {
writer.writeln('null');
throw ArgumentError('Unhandled element type $element');
}
definition.write(';');
return name;
}
void write() {
for (final version in versions) {
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<x> class for each schema version x.
versionScope.leaf()
..write('final class $versionClass extends ')
..writeUriRef(_schemaLibrary, 'VersionedSchema')
@ -273,27 +317,30 @@ class SchemaVersionWriter {
..writeln('$versionClass({required super.database}): '
'super(version: $versionNo);');
// Override the allEntities getters by VersionedSchema
final allEntitiesWriter = versionScope.leaf()
..write('@override')
..write(' late final ')
..write('@override late final ')
..writeUriRef(AnnotatedDartCode.dartCore, 'List')
..write('<')
..writeDriftRef('DatabaseSchemaEntity')
..write('> entities = [');
for (final entity in version.schema) {
_writeEntity(
element: entity,
classScope: versionScope,
writer: allEntitiesWriter,
);
allEntitiesWriter.write(',');
// 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<x> class used to lookup elements.
final stepByStep = libraryScope.leaf()
..writeDriftRef('OnUpgrade')
..write(' stepByStep({');

View File

@ -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`.

View File

@ -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');

View File

@ -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<i1.DatabaseSchemaEntity> 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<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get color =>
columnsByName['color']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_2(String aliasedName) =>
i1.GeneratedColumn<int>('color', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get category =>
columnsByName['category']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get dueDate =>
columnsByName['due_date']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_4(String aliasedName) =>
i1.GeneratedColumn<int>('category', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
i1.GeneratedColumn<DateTime>('due_date', aliasedName, true,
type: i1.DriftSqlType.dateTime);
class Shape2 extends i0.VersionedVirtualTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
i1.GeneratedColumn<String>('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<i1.DatabaseSchemaEntity> 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<int> _column_7(String aliasedName) =>
i1.GeneratedColumn<int>('category', aliasedName, true,
type: i1.DriftSqlType.int,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES categories (id)'));
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, _S2 schema) from1To2,
required Future<void> 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');
}
});
}

View File

@ -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:

View File

@ -1,9 +1,10 @@
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart';
import 'package:drift/drift.dart'; // ignore_for_file: type=lint
final class _S1 extends i0.VersionedSchema {
_S1({required super.database}) : super(version: 1);
// GENERATED BY drift_dev, DO NOT MODIFY.
final class _S2 extends i0.VersionedSchema {
_S2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
users,
@ -16,6 +17,7 @@ final class _S1 extends i0.VersionedSchema {
tableConstraints: [],
columns: [
_column_0,
_column_1,
],
attachedDatabase: database,
),
@ -26,6 +28,8 @@ class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
@ -34,36 +38,6 @@ i1.GeneratedColumn<int> _column_0(String aliasedName) =>
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
final class _S2 extends i0.VersionedSchema {
_S2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
users,
];
late final Shape1 users = Shape1(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
],
attachedDatabase: database,
),
alias: null);
}
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
@ -75,7 +49,7 @@ final class _S3 extends i0.VersionedSchema {
users,
groups,
];
late final Shape1 users = Shape1(
late final Shape0 users = Shape0(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -88,7 +62,7 @@ final class _S3 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -107,8 +81,8 @@ final class _S3 extends i0.VersionedSchema {
alias: null);
}
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get title =>
@ -142,7 +116,7 @@ final class _S4 extends i0.VersionedSchema {
users,
groups,
];
late final Shape1 users = Shape1(
late final Shape0 users = Shape0(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -155,7 +129,7 @@ final class _S4 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -186,7 +160,7 @@ final class _S5 extends i0.VersionedSchema {
groups,
groupCount,
];
late final Shape3 users = Shape3(
late final Shape2 users = Shape2(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -200,7 +174,7 @@ final class _S5 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -217,7 +191,7 @@ final class _S5 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape4 groupCount = Shape4(
late final Shape3 groupCount = Shape3(
source: i0.VersionedView(
entityName: 'group_count',
createViewStmt:
@ -233,8 +207,8 @@ final class _S5 extends i0.VersionedSchema {
alias: null);
}
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
@ -249,8 +223,8 @@ i1.GeneratedColumn<int> _column_7(String aliasedName) =>
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('REFERENCES users (id)'));
class Shape4 extends i0.VersionedView {
Shape4({required super.source, required super.alias}) : super.aliased();
class Shape3 extends i0.VersionedView {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
@ -279,7 +253,7 @@ final class _S6 extends i0.VersionedSchema {
groups,
groupCount,
];
late final Shape5 users = Shape5(
late final Shape4 users = Shape4(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -294,7 +268,7 @@ final class _S6 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -311,7 +285,7 @@ final class _S6 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape6 groupCount = Shape6(
late final Shape5 groupCount = Shape5(
source: i0.VersionedView(
entityName: 'group_count',
createViewStmt:
@ -328,8 +302,8 @@ final class _S6 extends i0.VersionedSchema {
alias: null);
}
class Shape5 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased();
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
@ -344,8 +318,8 @@ i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
i1.GeneratedColumn<DateTime>('birthday', aliasedName, true,
type: i1.DriftSqlType.dateTime);
class Shape6 extends i0.VersionedView {
Shape6({required super.source, required super.alias}) : super.aliased();
class Shape5 extends i0.VersionedView {
Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
@ -367,7 +341,7 @@ final class _S7 extends i0.VersionedSchema {
groupCount,
notes,
];
late final Shape5 users = Shape5(
late final Shape4 users = Shape4(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -382,7 +356,7 @@ final class _S7 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -399,7 +373,7 @@ final class _S7 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape6 groupCount = Shape6(
late final Shape5 groupCount = Shape5(
source: i0.VersionedView(
entityName: 'group_count',
createViewStmt:
@ -414,7 +388,7 @@ final class _S7 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape7 notes = Shape7(
late final Shape6 notes = Shape6(
source: i0.VersionedVirtualTable(
entityName: 'notes',
moduleAndArgs:
@ -429,8 +403,8 @@ final class _S7 extends i0.VersionedSchema {
alias: null);
}
class Shape7 extends i0.VersionedVirtualTable {
Shape7({required super.source, required super.alias}) : super.aliased();
class Shape6 extends i0.VersionedVirtualTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get title =>
columnsByName['title']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
@ -458,7 +432,7 @@ final class _S8 extends i0.VersionedSchema {
groupCount,
notes,
];
late final Shape5 users = Shape5(
late final Shape4 users = Shape4(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -475,7 +449,7 @@ final class _S8 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -492,7 +466,7 @@ final class _S8 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape6 groupCount = Shape6(
late final Shape5 groupCount = Shape5(
source: i0.VersionedView(
entityName: 'group_count',
createViewStmt:
@ -507,7 +481,7 @@ final class _S8 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape7 notes = Shape7(
late final Shape6 notes = Shape6(
source: i0.VersionedVirtualTable(
entityName: 'notes',
moduleAndArgs:
@ -537,7 +511,7 @@ final class _S9 extends i0.VersionedSchema {
notes,
groupCount,
];
late final Shape5 users = Shape5(
late final Shape4 users = Shape4(
source: i0.VersionedTable(
entityName: 'users',
withoutRowId: false,
@ -555,7 +529,7 @@ final class _S9 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape2 groups = Shape2(
late final Shape1 groups = Shape1(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
@ -572,7 +546,7 @@ final class _S9 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape7 notes = Shape7(
late final Shape6 notes = Shape6(
source: i0.VersionedVirtualTable(
entityName: 'notes',
moduleAndArgs:
@ -585,7 +559,7 @@ final class _S9 extends i0.VersionedSchema {
attachedDatabase: database,
),
alias: null);
late final Shape6 groupCount = Shape6(
late final Shape5 groupCount = Shape5(
source: i0.VersionedView(
entityName: 'group_count',
createViewStmt: